From 8e49998205b4e8366ad8ad281e028960b0a9fd6a Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 17:59:00 +0800 Subject: [PATCH 01/11] =?UTF-8?q?feat:=20=E4=B8=BA=20ID=20=E7=94=9F?= =?UTF-8?q?=E6=88=90=E6=B7=BB=E5=8A=A0=E9=9A=8F=E6=9C=BA=E5=90=8E=E7=BC=80?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=BC=BA=20GM=20=E9=9D=A2=E6=9D=BF=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为战斗报告、星球、月球、任务通知和舰队任务等 ID 添加随机后缀,避免重复 ID - 在 GM 面板中添加预设管理系统,支持保存和应用建筑、研究、舰船和防御的配置 - 在外交界面添加 NPC 排序功能,支持按声望、星球数量、难度和盟友数量排序 - 修复数据迁移中的重复星球 ID 问题,确保月球与母星关联正确 - 优化 GM 面板的资源最大化功能,基于实际存储容量设置资源 - 为所有支持的语言添加相关翻译文本 --- src/locales/de.ts | 16 +++ src/locales/en.ts | 16 +++ src/locales/es-LA.ts | 16 +++ src/locales/ja.ts | 16 +++ src/locales/ko.ts | 16 +++ src/locales/ru.ts | 16 +++ src/locales/zh-CN.ts | 16 +++ src/locales/zh-TW.ts | 16 +++ src/logic/battleLogic.ts | 2 +- src/logic/campaignLogic.ts | 2 +- src/logic/fleetLogic.ts | 6 +- src/logic/moonLogic.ts | 2 +- src/logic/planetLogic.ts | 2 +- src/utils/migration.ts | 33 ++++++ src/views/DiplomacyView.vue | 105 +++++++++++++++++--- src/views/GMView.vue | 193 ++++++++++++++++++++++++++++++++++-- 16 files changed, 448 insertions(+), 25 deletions(-) diff --git a/src/locales/de.ts b/src/locales/de.ts index 99f33a4..08cba2f 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1085,6 +1085,15 @@ export default { }, gmView: { title: 'GM-Kontrollpanel', + presets: 'Vorlagen', + choosePreset: 'Vorlage wählen', + defaultPreset: 'Standardvorlage', + applyPreset: 'Vorlage anwenden', + savePreset: 'Vorlage speichern', + presetName: 'Vorlagenname', + presetNameRequired: 'Bitte geben Sie einen Namen ein', + presetSaved: 'Vorlage gespeichert', + presetApplied: 'Vorlage angewendet', adminOnly: 'Nur Admin', selectPlanet: 'Planet auswählen', choosePlanet: 'Einen Planeten auswählen', @@ -1185,6 +1194,13 @@ export default { } }, diplomacy: { + sort: { + label: 'Sortieren', + reputation: 'Ruf', + planets: 'Planeten', + difficulty: 'Schwierigkeit', + allies: 'Verbündete' + }, title: 'Diplomatie', description: 'Verwalte diplomatische Beziehungen mit NPCs', tabs: { diff --git a/src/locales/en.ts b/src/locales/en.ts index 26b6422..8b9786e 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1101,6 +1101,15 @@ export default { }, gmView: { title: 'GM Control Panel', + presets: 'Presets', + choosePreset: 'Choose Preset', + defaultPreset: 'Default Preset', + applyPreset: 'Apply Preset', + savePreset: 'Save Preset', + presetName: 'Preset Name', + presetNameRequired: 'Please enter a preset name', + presetSaved: 'Preset saved', + presetApplied: 'Preset applied', adminOnly: 'Admin Only', selectPlanet: 'Select Planet', choosePlanet: 'Choose a planet', @@ -1291,6 +1300,13 @@ export default { npcEliminatedMessage: "You destroyed all of {npcName}'s planets! This faction has been completely wiped out." }, searchPlaceholder: 'Search NPC name...', + sort: { + label: 'Sort', + reputation: 'Reputation', + planets: 'Planets', + difficulty: 'Difficulty', + allies: 'Allies' + }, // Notification types notificationType: { tradeOffer: 'Trade Offer', diff --git a/src/locales/es-LA.ts b/src/locales/es-LA.ts index 8f3614f..a23e1f5 100644 --- a/src/locales/es-LA.ts +++ b/src/locales/es-LA.ts @@ -1093,6 +1093,15 @@ export default { }, gmView: { title: 'Panel de Control GM', + presets: 'Preajustes', + choosePreset: 'Elegir preajuste', + defaultPreset: 'Preajuste predeterminado', + applyPreset: 'Aplicar preajuste', + savePreset: 'Guardar preajuste', + presetName: 'Nombre del preajuste', + presetNameRequired: 'Ingrese el nombre del preajuste', + presetSaved: 'Preajuste guardado', + presetApplied: 'Preajuste aplicado', adminOnly: 'Solo Administrador', selectPlanet: 'Seleccionar Planeta', choosePlanet: 'Elige un planeta', @@ -1193,6 +1202,13 @@ export default { } }, diplomacy: { + sort: { + label: 'Ordenar', + reputation: 'Reputación', + planets: 'Planetas', + difficulty: 'Dificultad', + allies: 'Aliados' + }, title: 'Diplomacia', description: 'Gestionar relaciones diplomáticas con NPCs', tabs: { diff --git a/src/locales/ja.ts b/src/locales/ja.ts index d149f87..40d1886 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1111,6 +1111,15 @@ export default { }, gmView: { title: 'GMコントロールパネル', + presets: 'プリセット', + choosePreset: 'プリセット選択', + defaultPreset: 'デフォルト', + applyPreset: '適用', + savePreset: '保存', + presetName: 'プリセット名', + presetNameRequired: 'プリセット名を入力してください', + presetSaved: '保存しました', + presetApplied: '適用しました', adminOnly: '管理者専用', selectPlanet: '惑星を選択', choosePlanet: '惑星を選択してください', @@ -1210,6 +1219,13 @@ export default { } }, diplomacy: { + sort: { + label: '並び替え', + reputation: '評判', + planets: '惑星', + difficulty: '難易度', + allies: '同盟' + }, title: '外交', description: 'NPCとの外交関係を管理', tabs: { diff --git a/src/locales/ko.ts b/src/locales/ko.ts index bd03c41..db0adb9 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1061,6 +1061,15 @@ export default { }, gmView: { title: 'GM 제어판', + presets: '프리셋', + choosePreset: '프리셋 선택', + defaultPreset: '기본 프리셋', + applyPreset: '프리셋 적용', + savePreset: '프리셋 저장', + presetName: '프리셋 이름', + presetNameRequired: '프리셋 이름을 입력하세요', + presetSaved: '프리셋 저장됨', + presetApplied: '프리셋 적용됨', adminOnly: '관리자 전용', selectPlanet: '행성 선택', choosePlanet: '행성을 선택하세요', @@ -1160,6 +1169,13 @@ export default { } }, diplomacy: { + sort: { + label: '정렬', + reputation: '평판', + planets: '행성', + difficulty: '난이도', + allies: '동맹' + }, title: '외교', description: 'NPC와의 외교 관계 관리', tabs: { diff --git a/src/locales/ru.ts b/src/locales/ru.ts index aa3b1fb..d891a67 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1087,6 +1087,15 @@ export default { }, gmView: { title: 'Панель управления GM', + presets: 'Предустановки', + choosePreset: 'Выбрать предустановку', + defaultPreset: 'Стандартная', + applyPreset: 'Применить', + savePreset: 'Сохранить', + presetName: 'Название', + presetNameRequired: 'Введите название', + presetSaved: 'Сохранено', + presetApplied: 'Применено', adminOnly: 'Только для администратора', selectPlanet: 'Выбрать планету', choosePlanet: 'Выберите планету', @@ -1186,6 +1195,13 @@ export default { } }, diplomacy: { + sort: { + label: 'Сортировка', + reputation: 'Репутация', + planets: 'Планеты', + difficulty: 'Сложность', + allies: 'Союзники' + }, title: 'Дипломатия', description: 'Управление дипломатическими отношениями с NPC', tabs: { diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c05e298..c44c2e3 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1095,6 +1095,15 @@ export default { modifyOfficers: '修改军官', officersDesc: '快速设置军官到期时间', days: '天', + presets: '预设', + choosePreset: '选择预设', + defaultPreset: '默认预设', + applyPreset: '应用预设', + presetName: '预设名称', + savePreset: '保存预设', + presetNameRequired: '请输入预设名称', + presetSaved: '预设保存成功', + presetApplied: '预设应用成功', npcTesting: 'NPC 测试', npcTestingDesc: '测试NPC侦查和攻击行为', selectNPC: '选择NPC', @@ -1259,6 +1268,13 @@ export default { npcEliminatedMessage: '你消灭了{npcName}的所有星球!该势力已被彻底摧毁。' }, searchPlaceholder: '搜索NPC名称...', + sort: { + label: '排序', + reputation: '好感度', + planets: '星球数量', + difficulty: '难度', + allies: '盟友数量' + }, // 通知类型 notificationType: { tradeOffer: '贸易提议', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index f1d6fd1..4df3e95 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1080,6 +1080,15 @@ export default { }, gmView: { title: 'GM 管理面板', + presets: '預設', + choosePreset: '選擇預設', + defaultPreset: '預設範本', + applyPreset: '套用預設', + savePreset: '儲存預設', + presetName: '預設名稱', + presetNameRequired: '請輸入預設名稱', + presetSaved: '預設已儲存', + presetApplied: '預設已套用', adminOnly: '僅管理員', selectPlanet: '選擇星球', choosePlanet: '選擇一個星球', @@ -1179,6 +1188,13 @@ export default { } }, diplomacy: { + sort: { + label: '排序', + reputation: '聲望', + planets: '星球', + difficulty: '難度', + allies: '盟友' + }, title: '外交', description: '管理與NPC的外交關係', tabs: { diff --git a/src/logic/battleLogic.ts b/src/logic/battleLogic.ts index 00159af..4244a1a 100644 --- a/src/logic/battleLogic.ts +++ b/src/logic/battleLogic.ts @@ -66,7 +66,7 @@ export const simulateBattle = async ( // 生成战斗报告 const battleResult: BattleResult = { - id: `battle_${Date.now()}`, + id: `battle_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, timestamp: Date.now(), attackerId: '', defenderId: '', diff --git a/src/logic/campaignLogic.ts b/src/logic/campaignLogic.ts index 1f817e5..0820654 100644 --- a/src/logic/campaignLogic.ts +++ b/src/logic/campaignLogic.ts @@ -515,7 +515,7 @@ export const createQuestNotification = ( ): QuestNotification => { const quest = getQuestById(questId) return { - id: `quest_notification_${Date.now()}`, + id: `quest_notification_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, timestamp: Date.now(), questId, questTitleKey: quest?.titleKey || '', diff --git a/src/logic/fleetLogic.ts b/src/logic/fleetLogic.ts index 9d67b3e..a377037 100644 --- a/src/logic/fleetLogic.ts +++ b/src/logic/fleetLogic.ts @@ -61,7 +61,7 @@ export const createFleetMission = ( ): FleetMission => { const now = Date.now() return { - id: `mission_${now}`, + id: `mission_${now}_${Math.random().toString(36).substring(2, 9)}`, playerId, originPlanetId, // 深拷贝targetPosition,避免多个任务共享同一个引用 @@ -414,7 +414,7 @@ export const processColonizeArrival = ( // 创建新殖民地 const newPlanet: Planet = { - id: `planet_${Date.now()}`, + id: `planet_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, name: `${colonyNameTemplate} ${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}`, ownerId: player.id, position: mission.targetPosition, @@ -564,7 +564,7 @@ export const processSpyArrival = ( const wasDetected = Math.random() < detectionChance const spyReport: SpyReport = { - id: `spy_${Date.now()}`, + id: `spy_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, timestamp: Date.now(), spyId: attacker.id, targetPlanetId: targetPlanet.id, diff --git a/src/logic/moonLogic.ts b/src/logic/moonLogic.ts index 47105d2..ac8378a 100644 --- a/src/logic/moonLogic.ts +++ b/src/logic/moonLogic.ts @@ -79,7 +79,7 @@ export const tryGenerateMoon = ( // 生成月球 const moon: Planet = { - id: `moon_${Date.now()}`, + id: `moon_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, name: `Moon [${planetPosition.galaxy}:${planetPosition.system}:${planetPosition.position}]`, ownerId: playerId, position: planetPosition, diff --git a/src/logic/planetLogic.ts b/src/logic/planetLogic.ts index 08b1aec..a13a5b2 100644 --- a/src/logic/planetLogic.ts +++ b/src/logic/planetLogic.ts @@ -173,7 +173,7 @@ export const createMoon = ( moonSuffix: string = "'s Moon", diameter?: number ): Planet => { - const moonId = `moon_${Date.now()}` + const moonId = `moon_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` const moon: Planet = { id: moonId, name: `${parentPlanet.name}${moonSuffix}`, diff --git a/src/utils/migration.ts b/src/utils/migration.ts index b6053a0..a97446f 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -100,6 +100,39 @@ export const migrateGameData = (): void => { needsSave = true } + // 修复重复的星球ID + if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { + const idCounts = new Map() + const idMap = new Map() // 映射:原始ID + 坐标 -> 新ID + + oldData.player.planets.forEach((planet: Planet) => { + const count = idCounts.get(planet.id) || 0 + if (count > 0) { + // 发现重复ID + const newId = `${planet.id}_${Math.random().toString(36).substring(2, 9)}` + const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` + idMap.set(`${planet.id}_${posKey}`, newId) + planet.id = newId + needsSave = true + } + idCounts.set(planet.id, count + 1) + }) + + // 如果有ID被修改,需要更新月球的 parentPlanetId + if (idMap.size > 0) { + oldData.player.planets.forEach((planet: Planet) => { + if (planet.isMoon && planet.parentPlanetId) { + const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` + const newParentId = idMap.get(`${planet.parentPlanetId}_${posKey}`) + if (newParentId) { + planet.parentPlanetId = newParentId + needsSave = true + } + } + }) + } + } + // 迁移温度数据:为没有温度的星球生成温度 // 玩家星球 if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index 1a44377..d1d0939 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -181,10 +181,37 @@ - -
- - + +
+ +
+ + +
+ + +
+ + + +
@@ -379,6 +406,13 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' + import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue + } from '@/components/ui/select' import { Dialog, DialogDescription, DialogTitle } from '@/components/ui/dialog' import ScrollableDialogContent from '@/components/ui/dialog/ScrollableDialogContent.vue' import { @@ -403,7 +437,8 @@ Swords, Activity, LayoutGrid, - List + List, + ArrowUpDown } from 'lucide-vue-next' import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty' @@ -435,6 +470,52 @@ // 搜索功能 const searchQuery = ref('') + // 排序状态 + const sortBy = ref('reputation') + const sortOrder = ref<'asc' | 'desc'>('desc') + + // 排序函数 + const sortNpcs = (npcs: typeof npcStore.npcs) => { + return [...npcs].sort((a, b) => { + let valA = 0 + let valB = 0 + + switch (sortBy.value) { + case 'reputation': + valA = getRelation(a.id)?.reputation || 0 + valB = getRelation(b.id)?.reputation || 0 + break + case 'planets': + valA = a.planets.length + valB = b.planets.length + break + case 'difficulty': + // 简单=1, 普通=2, 困难=3 + // eslint-disable-next-line no-case-declarations + const getDifficultyVal = (diff: string) => { + if (diff === 'hard') return 3 + if (diff === 'medium') return 2 + return 1 + } + valA = a.difficultyLevel || getDifficultyVal(a.difficulty) + valB = b.difficultyLevel || getDifficultyVal(b.difficulty) + break + case 'allies': + valA = a.allies?.length || 0 + valB = b.allies?.length || 0 + break + default: + return 0 + } + + if (sortOrder.value === 'asc') { + return valA - valB + } else { + return valB - valA + } + }) + } + // NPC诊断功能 const npcDiagnosticOpen = ref(false) const npcDiagnostics = ref([]) @@ -627,30 +708,30 @@ } // 按关系状态分类NPC(同时应用搜索过滤) - const allNpcs = computed(() => npcStore.npcs.filter(matchesSearch)) + const allNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) const friendlyNpcs = computed(() => { - return npcStore.npcs.filter(npc => { + return sortNpcs(npcStore.npcs.filter(npc => { if (!matchesSearch(npc)) return false const relation = getRelation(npc.id) return relation?.status === RelationStatus.Friendly - }) + })) }) const neutralNpcs = computed(() => { - return npcStore.npcs.filter(npc => { + return sortNpcs(npcStore.npcs.filter(npc => { if (!matchesSearch(npc)) return false const relation = getRelation(npc.id) return !relation || relation.status === RelationStatus.Neutral - }) + })) }) const hostileNpcs = computed(() => { - return npcStore.npcs.filter(npc => { + return sortNpcs(npcStore.npcs.filter(npc => { if (!matchesSearch(npc)) return false const relation = getRelation(npc.id) return relation?.status === RelationStatus.Hostile - }) + })) }) // 分页辅助函数 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 7e4eed8..9f2add6 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -79,6 +79,35 @@ + + + + {{ t('gmView.presets') || 'Presets' }} + + +
+
+ + +
+
+ + +
+
+
+
+ {{ t(section.titleKey) }} @@ -263,8 +292,157 @@ import { BuildingType, TechnologyType, ShipType, DefenseType, OfficerType } from '@/types/game' import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import * as publicLogic from '@/logic/publicLogic' + import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' import { Home } from 'lucide-vue-next' + // --- 预设系统 --- + interface GMPreset { + id: string + name: string + values: Record + } + + const getPresets = (type: string): GMPreset[] => { + const data = localStorage.getItem(`gm_presets_${type}`) + return data ? JSON.parse(data) : [] + } + + const savePresets = (type: string, presets: GMPreset[]) => { + localStorage.setItem(`gm_presets_${type}`, JSON.stringify(presets)) + } + + const presetNames = ref>({ + buildings: '', + research: '', + ships: '', + defense: '' + }) + + const selectedPresets = ref>({ + buildings: 'default', + research: 'default', + ships: 'default', + defense: 'default' + }) + + const customPresets = ref>({ + buildings: getPresets('buildings'), + research: getPresets('research'), + ships: getPresets('ships'), + defense: getPresets('defense') + }) + + const handleSavePreset = (section: any) => { + const name = presetNames.value[section.tabValue]?.trim() + if (!name) { + toast.error(t('gmView.presetNameRequired') || '请输入预设名称') + return + } + + const values: Record = {} + section.items.forEach((item: string) => { + values[item] = section.getValue(item) + }) + + const newPreset: GMPreset = { + id: Date.now().toString(), + name, + values + } + + if (customPresets.value[section.tabValue]) { + customPresets.value[section.tabValue]!.push(newPreset) + savePresets(section.tabValue, customPresets.value[section.tabValue]!) + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = newPreset.id + toast.success(t('gmView.presetSaved') || '预设保存成功') + } + } + + const handleApplyPreset = (section: any) => { + const presetId = selectedPresets.value[section.tabValue] + if (!presetId) return + + if (presetId === 'default') { + if (section.tabValue === 'buildings') { + const explicitMax: Record = { + [BuildingType.NaniteFactory]: 10, + [BuildingType.MissileSilo]: 10, + [BuildingType.JumpGate]: 5, + [BuildingType.PlanetDestroyerFactory]: 3, + [BuildingType.GeoResearchStation]: 10, + [BuildingType.DeepDrillingFacility]: 10, + [BuildingType.University]: 10 + } + section.items.forEach((item: string) => { + section.setValue(item, explicitMax[item] || 50) + }) + } else if (section.tabValue === 'research') { + const explicitMax: Record = { + [TechnologyType.ComputerTechnology]: 10, + [TechnologyType.GravitonTechnology]: 1, + [TechnologyType.PlanetDestructionTech]: 10, + [TechnologyType.MiningTechnology]: 15, + [TechnologyType.IntergalacticResearchNetwork]: 10, + [TechnologyType.MineralResearch]: 20, + [TechnologyType.CrystalResearch]: 20, + [TechnologyType.FuelResearch]: 20 + } + section.items.forEach((item: string) => { + section.setValue(item, explicitMax[item] || 100) + }) + } else if (section.tabValue === 'ships') { + if (!selectedPlanet.value) return + + // 重新计算最大舰队仓储,确保数据是最新的 + const maxStorage = calculateMaxFleetStorage(selectedPlanet.value, gameStore.player.technologies) + + const shipTypes = Object.values(ShipType) + // 将总容量平均分配给每种舰船 + const storagePerShip = maxStorage / shipTypes.length + + shipTypes.forEach(type => { + const usage = SHIPS.value[type]?.storageUsage || 1 + // 如果 usage 为 0 (如某些特殊单位),则给予一个默认数量,或者跳过 + if (usage <= 0) { + section.setValue(type, 100) // 防止除以0,给予固定值 + } else { + section.setValue(type, Math.floor(storagePerShip / usage)) + } + }) + } else if (section.tabValue === 'defense') { + if (!selectedPlanet.value) return + const siloLevel = selectedPlanet.value.buildings[BuildingType.MissileSilo] || 0 + const missileCapacity = siloLevel * 10 + const halfCapacity = missileCapacity / 2 + + section.items.forEach((item: string) => { + if (item === DefenseType.AntiBallisticMissile) { + // 反弹道导弹占用1个空间,分配一半容量 + section.setValue(item, Math.floor(halfCapacity)) + } else if (item === DefenseType.InterplanetaryMissile) { + // 星际导弹占用2个空间,分配一半容量 + section.setValue(item, Math.floor(halfCapacity)) + } else { + section.setValue(item, 10000) + } + }) + } + toast.success(t('gmView.presetApplied') || '默认预设应用成功') + } else { + if (customPresets.value[section.tabValue]) { + const customPreset = customPresets.value[section.tabValue]!.find((p: GMPreset) => p.id === presetId) + if (customPreset) { + Object.entries(customPreset.values).forEach(([k, v]) => { + section.setValue(k, v as number) + }) + toast.success(t('gmView.presetApplied') || '预设应用成功') + } + } + } + } + // --- 预设系统结束 --- + const router = useRouter() const gameStore = useGameStore() const npcStore = useNPCStore() @@ -281,7 +459,8 @@ router.push('/') } - const selectedPlanetId = ref(gameStore.player.planets[0]?.id || '') + // 默认选中当前正在游玩的星球 + const selectedPlanetId = ref(gameStore.currentPlanetId || gameStore.player.planets[0]?.id || '') const officerDays = ref>({} as Record) const selectedNPCId = ref(npcStore.npcs[0]?.id || '') const targetPlanetIndex = ref('0') @@ -659,11 +838,13 @@ const maxAllResources = () => { if (!selectedPlanet.value) return - const maxAmount = 1000000000000000000 - selectedPlanet.value.resources.metal = maxAmount - selectedPlanet.value.resources.crystal = maxAmount - selectedPlanet.value.resources.deuterium = maxAmount - selectedPlanet.value.resources.darkMatter = maxAmount + // 计算当前星球的资源存储上限 + const capacity = publicLogic.getResourceCapacity(selectedPlanet.value, gameStore.player.officers) + + selectedPlanet.value.resources.metal = capacity.metal + selectedPlanet.value.resources.crystal = capacity.crystal + selectedPlanet.value.resources.deuterium = capacity.deuterium + selectedPlanet.value.resources.darkMatter = capacity.darkMatter toast.success(t('gmView.maxAllResourcesSuccess')) } From bd46c248249edfcade5ecba10916894a17e2f8db Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 18:03:38 +0800 Subject: [PATCH 02/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E4=BF=9D?= =?UTF-8?q?=E5=AD=98=E9=A2=84=E8=AE=BE=E6=97=B6=E6=9C=AA=E5=88=9D=E5=A7=8B?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E7=BB=84=E5=92=8C=E9=87=8D=E5=A4=8D=E6=98=9F?= =?UTF-8?q?=E7=90=83=20ID=20=E8=BF=81=E7=A7=BB=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复 GM 视图中保存自定义预设时,当对应标签页的预设数组未初始化导致的保存失败问题。同时改进迁移工具中重复星球 ID 的处理逻辑,确保正确分组并更新关联的月球数据。 --- src/utils/migration.ts | 52 +++++++++++++++++++++++++++++------------- src/views/GMView.vue | 13 ++++++----- 2 files changed, 43 insertions(+), 22 deletions(-) diff --git a/src/utils/migration.ts b/src/utils/migration.ts index a97446f..e6f586c 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -102,31 +102,51 @@ export const migrateGameData = (): void => { // 修复重复的星球ID if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { - const idCounts = new Map() - const idMap = new Map() // 映射:原始ID + 坐标 -> 新ID + const planetsByOriginalId = new Map() + // 第一步:按ID分组 oldData.player.planets.forEach((planet: Planet) => { - const count = idCounts.get(planet.id) || 0 - if (count > 0) { - // 发现重复ID - const newId = `${planet.id}_${Math.random().toString(36).substring(2, 9)}` - const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - idMap.set(`${planet.id}_${posKey}`, newId) - planet.id = newId - needsSave = true + if (!planetsByOriginalId.has(planet.id)) { + planetsByOriginalId.set(planet.id, []) + } + planetsByOriginalId.get(planet.id)!.push(planet) + }) + + // 第二步:处理重复ID并建立映射 + const idMap = new Map() // Key: oldId_galaxy:system:position -> Value: newId + + planetsByOriginalId.forEach((planets, originalId) => { + if (planets.length > 1) { + // 对重复组中的星球,保留第一个,修改后续的 + planets.forEach((planet, index) => { + if (index > 0) { + const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` + const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` + // 记录映射:原始ID + 坐标 -> 新ID + idMap.set(`${originalId}_${posKey}`, newId) + + // 修改星球ID + planet.id = newId + needsSave = true + } + }) } - idCounts.set(planet.id, count + 1) }) - // 如果有ID被修改,需要更新月球的 parentPlanetId + // 第三步:更新月球的 parentPlanetId if (idMap.size > 0) { oldData.player.planets.forEach((planet: Planet) => { if (planet.isMoon && planet.parentPlanetId) { + // 假设月球和母星坐标一致,通过月球坐标查找母星的新ID const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - const newParentId = idMap.get(`${planet.parentPlanetId}_${posKey}`) - if (newParentId) { - planet.parentPlanetId = newParentId - needsSave = true + const mapKey = `${planet.parentPlanetId}_${posKey}` + + if (idMap.has(mapKey)) { + const newParentId = idMap.get(mapKey) + if (newParentId) { + planet.parentPlanetId = newParentId + needsSave = true + } } } }) diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 9f2add6..8fa1feb 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -350,13 +350,14 @@ values } - if (customPresets.value[section.tabValue]) { - customPresets.value[section.tabValue]!.push(newPreset) - savePresets(section.tabValue, customPresets.value[section.tabValue]!) - presetNames.value[section.tabValue] = '' - selectedPresets.value[section.tabValue] = newPreset.id - toast.success(t('gmView.presetSaved') || '预设保存成功') + if (!customPresets.value[section.tabValue]) { + customPresets.value[section.tabValue] = [] } + customPresets.value[section.tabValue]!.push(newPreset) + savePresets(section.tabValue, customPresets.value[section.tabValue]!) + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = newPreset.id + toast.success(t('gmView.presetSaved') || '预设保存成功') } const handleApplyPreset = (section: any) => { From b0a7b5ce908ad894781dc89029fcd728775d000a Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 18:30:54 +0800 Subject: [PATCH 03/11] =?UTF-8?q?refactor(migration):=20=E7=AE=80=E5=8C=96?= =?UTF-8?q?=E7=88=B6=E8=A1=8C=E6=98=9F=20ID=20=E7=9A=84=E6=98=A0=E5=B0=84?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除冗余的 idMap.has 检查,直接使用 idMap.get 获取新 ID --- src/utils/migration.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/utils/migration.ts b/src/utils/migration.ts index e6f586c..cc871b9 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -141,12 +141,10 @@ export const migrateGameData = (): void => { const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` const mapKey = `${planet.parentPlanetId}_${posKey}` - if (idMap.has(mapKey)) { - const newParentId = idMap.get(mapKey) - if (newParentId) { - planet.parentPlanetId = newParentId - needsSave = true - } + const newParentId = idMap.get(mapKey) + if (newParentId) { + planet.parentPlanetId = newParentId + needsSave = true } } }) From 28c3da258261c636a51819ae5a2907f23ad1eb63 Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 18:47:22 +0800 Subject: [PATCH 04/11] =?UTF-8?q?feat(GMView):=20=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E9=A2=84=E8=AE=BE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD=EF=BC=8C?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E5=88=A0=E9=99=A4=E5=92=8C=E8=A6=86=E7=9B=96?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为所有语言文件添加删除预设、确认覆盖等翻译键 - 在预设选择器旁添加删除按钮,允许删除自定义预设 - 保存同名预设时弹出确认对话框,防止意外覆盖 - 禁止删除默认预设,并提供相应的错误提示 --- src/locales/de.ts | 5 +++ src/locales/en.ts | 5 +++ src/locales/es-LA.ts | 5 +++ src/locales/ja.ts | 5 +++ src/locales/ko.ts | 5 +++ src/locales/ru.ts | 5 +++ src/locales/zh-CN.ts | 5 +++ src/locales/zh-TW.ts | 5 +++ src/views/GMView.vue | 92 +++++++++++++++++++++++++++++++++++++++++++- 9 files changed, 131 insertions(+), 1 deletion(-) diff --git a/src/locales/de.ts b/src/locales/de.ts index 08cba2f..3ef58e2 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1094,6 +1094,11 @@ export default { presetNameRequired: 'Bitte geben Sie einen Namen ein', presetSaved: 'Vorlage gespeichert', presetApplied: 'Vorlage angewendet', + deletePreset: 'Vorlage löschen', + presetDeleted: 'Vorlage gelöscht', + confirmOverwriteTitle: 'Vorlage existiert bereits', + confirmOverwriteMessage: 'Eine Vorlage mit dem Namen "{name}" existiert bereits. Überschreiben?', + cannotDeleteDefault: 'Standardvorlage kann nicht gelöscht werden', adminOnly: 'Nur Admin', selectPlanet: 'Planet auswählen', choosePlanet: 'Einen Planeten auswählen', diff --git a/src/locales/en.ts b/src/locales/en.ts index 8b9786e..f7c1a0d 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1110,6 +1110,11 @@ export default { presetNameRequired: 'Please enter a preset name', presetSaved: 'Preset saved', presetApplied: 'Preset applied', + deletePreset: 'Delete Preset', + presetDeleted: 'Preset deleted', + confirmOverwriteTitle: 'Preset Already Exists', + confirmOverwriteMessage: 'Preset with name "{name}" already exists. Overwrite?', + cannotDeleteDefault: 'Cannot delete default preset', adminOnly: 'Admin Only', selectPlanet: 'Select Planet', choosePlanet: 'Choose a planet', diff --git a/src/locales/es-LA.ts b/src/locales/es-LA.ts index a23e1f5..19d8899 100644 --- a/src/locales/es-LA.ts +++ b/src/locales/es-LA.ts @@ -1102,6 +1102,11 @@ export default { presetNameRequired: 'Ingrese el nombre del preajuste', presetSaved: 'Preajuste guardado', presetApplied: 'Preajuste aplicado', + deletePreset: 'Eliminar preajuste', + presetDeleted: 'Preajuste eliminado', + confirmOverwriteTitle: 'El preajuste ya existe', + confirmOverwriteMessage: 'El preajuste con el nombre "{name}" ya existe. ¿Sobrescribir?', + cannotDeleteDefault: 'No se puede eliminar el preajuste predeterminado', adminOnly: 'Solo Administrador', selectPlanet: 'Seleccionar Planeta', choosePlanet: 'Elige un planeta', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 40d1886..14956bb 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1120,6 +1120,11 @@ export default { presetNameRequired: 'プリセット名を入力してください', presetSaved: '保存しました', presetApplied: '適用しました', + deletePreset: 'プリセット削除', + presetDeleted: 'プリセットを削除しました', + confirmOverwriteTitle: 'プリセットは既に存在します', + confirmOverwriteMessage: 'プリセット名 "{name}" は既に存在します。上書きしますか?', + cannotDeleteDefault: 'デフォルトプリセットは削除できません', adminOnly: '管理者専用', selectPlanet: '惑星を選択', choosePlanet: '惑星を選択してください', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index db0adb9..41b2bb8 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1070,6 +1070,11 @@ export default { presetNameRequired: '프리셋 이름을 입력하세요', presetSaved: '프리셋 저장됨', presetApplied: '프리셋 적용됨', + deletePreset: '프리셋 삭제', + presetDeleted: '프리셋 삭제됨', + confirmOverwriteTitle: '프리셋이 이미 존재함', + confirmOverwriteMessage: '"{name}" 이름의 프리셋이 이미 존재합니다. 덮어쓰시겠습니까?', + cannotDeleteDefault: '기본 프리셋은 삭제할 수 없습니다', adminOnly: '관리자 전용', selectPlanet: '행성 선택', choosePlanet: '행성을 선택하세요', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index d891a67..567a6a9 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1096,6 +1096,11 @@ export default { presetNameRequired: 'Введите название', presetSaved: 'Сохранено', presetApplied: 'Применено', + deletePreset: 'Удалить', + presetDeleted: 'Удалено', + confirmOverwriteTitle: 'Уже существует', + confirmOverwriteMessage: 'Предустановка с именем "{name}" уже существует. Перезаписать?', + cannotDeleteDefault: 'Нельзя удалить стандартную предустановку', adminOnly: 'Только для администратора', selectPlanet: 'Выбрать планету', choosePlanet: 'Выберите планету', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c44c2e3..6858d40 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1104,6 +1104,11 @@ export default { presetNameRequired: '请输入预设名称', presetSaved: '预设保存成功', presetApplied: '预设应用成功', + deletePreset: '删除预设', + presetDeleted: '预设已删除', + confirmOverwriteTitle: '预设已存在', + confirmOverwriteMessage: '名为 "{name}" 的预设已存在,是否覆盖?', + cannotDeleteDefault: '无法删除默认预设', npcTesting: 'NPC 测试', npcTestingDesc: '测试NPC侦查和攻击行为', selectNPC: '选择NPC', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 4df3e95..6df86c9 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1089,6 +1089,11 @@ export default { presetNameRequired: '請輸入預設名稱', presetSaved: '預設已儲存', presetApplied: '預設已套用', + deletePreset: '刪除預設', + presetDeleted: '預設已刪除', + confirmOverwriteTitle: '預設已存在', + confirmOverwriteMessage: '名為 "{name}" 的預設已存在,是否覆蓋?', + cannotDeleteDefault: '無法刪除預設範本', adminOnly: '僅管理員', selectPlanet: '選擇星球', choosePlanet: '選擇一個星球', diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 8fa1feb..fcd5751 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -99,6 +99,15 @@ +
@@ -242,6 +251,22 @@ + + + + + {{ t('gmView.confirmOverwriteTitle') || 'Preset Already Exists' }} + + {{ t('gmView.confirmOverwriteMessage', { name: pendingPresetToOverwrite?.name || '' }) || `Preset with name "${pendingPresetToOverwrite?.name}" already exists. Overwrite?` }} + + + + {{ t('common.cancel') }} + {{ t('common.confirm') }} + + + + @@ -293,7 +318,7 @@ import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import * as publicLogic from '@/logic/publicLogic' import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' - import { Home } from 'lucide-vue-next' + import { Home, Trash2 } from 'lucide-vue-next' // --- 预设系统 --- interface GMPreset { @@ -302,6 +327,14 @@ values: Record } + const presetOverwriteDialogOpen = ref(false) + const pendingPresetToOverwrite = ref<{ + section: any + name: string + values: Record + existingIndex: number + } | null>(null) + const getPresets = (type: string): GMPreset[] => { const data = localStorage.getItem(`gm_presets_${type}`) return data ? JSON.parse(data) : [] @@ -343,6 +376,20 @@ section.items.forEach((item: string) => { values[item] = section.getValue(item) }) + + // 检查是否存在同名预设 + const existingIndex = customPresets.value[section.tabValue]?.findIndex(p => p.name === name) ?? -1 + + if (existingIndex !== -1) { + pendingPresetToOverwrite.value = { + section, + name, + values, + existingIndex + } + presetOverwriteDialogOpen.value = true + return + } const newPreset: GMPreset = { id: Date.now().toString(), @@ -360,6 +407,49 @@ toast.success(t('gmView.presetSaved') || '预设保存成功') } + const handleConfirmOverwrite = () => { + if (!pendingPresetToOverwrite.value) return + + const { section, values, existingIndex } = pendingPresetToOverwrite.value + + if (customPresets.value[section.tabValue]) { + const presets = customPresets.value[section.tabValue]! + + if (presets[existingIndex]) { + // 更新现有预设的值,保持ID不变 + presets[existingIndex].values = values + + savePresets(section.tabValue, presets) + + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = presets[existingIndex].id + + toast.success(t('gmView.presetSaved') || '预设保存成功') + } + } + + presetOverwriteDialogOpen.value = false + pendingPresetToOverwrite.value = null + } + + const handleDeletePreset = (section: any) => { + const presetId = selectedPresets.value[section.tabValue] + if (!presetId || presetId === 'default') { + toast.error(t('gmView.cannotDeleteDefault') || '无法删除默认预设') + return + } + + const presets = customPresets.value[section.tabValue] || [] + const index = presets.findIndex(p => p.id === presetId) + + if (index !== -1) { + presets.splice(index, 1) + savePresets(section.tabValue, presets) + selectedPresets.value[section.tabValue] = 'default' + toast.success(t('gmView.presetDeleted') || '预设已删除') + } + } + const handleApplyPreset = (section: any) => { const presetId = selectedPresets.value[section.tabValue] if (!presetId) return From a8ab2b0f1a3417a6de887fd784cb102fe154498d Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 19:29:08 +0800 Subject: [PATCH 05/11] =?UTF-8?q?feat(=E6=9C=AC=E5=9C=B0=E5=8C=96=E4=B8=8E?= =?UTF-8?q?=E5=B7=A5=E5=85=B7):=20=E4=B8=BA=E6=8E=92=E5=BA=8F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E6=B7=BB=E5=8A=A0=E5=8D=87=E5=BA=8F/=E9=99=8D?= =?UTF-8?q?=E5=BA=8F=E7=BF=BB=E8=AF=91=E5=B9=B6=E7=BB=9F=E4=B8=80=20ID=20?= =?UTF-8?q?=E7=94=9F=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为所有支持的语言添加排序功能的 "ascending" 和 "descending" 翻译 - 创建统一的 ID 生成工具函数 generateId,用于生成带前缀的业务 ID - 重构多个逻辑模块(battleLogic、moonLogic 等)使用新的 ID 生成函数 - 改进 GM 视图的类型安全,添加预设数据验证和类型保护 --- src/locales/de.ts | 2 + src/locales/en.ts | 2 + src/locales/es-LA.ts | 2 + src/locales/ja.ts | 2 + src/locales/ko.ts | 2 + src/locales/ru.ts | 2 + src/locales/zh-CN.ts | 2 + src/locales/zh-TW.ts | 2 + src/logic/battleLogic.ts | 3 +- src/logic/campaignLogic.ts | 3 +- src/logic/fleetLogic.ts | 13 +-- src/logic/moonLogic.ts | 3 +- src/logic/planetLogic.ts | 3 +- src/utils/id.ts | 7 ++ src/views/DiplomacyView.vue | 2 +- src/views/GMView.vue | 201 ++++++++++++++++++++++-------------- 16 files changed, 165 insertions(+), 86 deletions(-) create mode 100644 src/utils/id.ts diff --git a/src/locales/de.ts b/src/locales/de.ts index 3ef58e2..7c09158 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1201,6 +1201,8 @@ export default { diplomacy: { sort: { label: 'Sortieren', + ascending: 'Aufsteigend', + descending: 'Absteigend', reputation: 'Ruf', planets: 'Planeten', difficulty: 'Schwierigkeit', diff --git a/src/locales/en.ts b/src/locales/en.ts index f7c1a0d..9fa0b90 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1307,6 +1307,8 @@ export default { searchPlaceholder: 'Search NPC name...', sort: { label: 'Sort', + ascending: 'Ascending', + descending: 'Descending', reputation: 'Reputation', planets: 'Planets', difficulty: 'Difficulty', diff --git a/src/locales/es-LA.ts b/src/locales/es-LA.ts index 19d8899..9067712 100644 --- a/src/locales/es-LA.ts +++ b/src/locales/es-LA.ts @@ -1209,6 +1209,8 @@ export default { diplomacy: { sort: { label: 'Ordenar', + ascending: 'Ascendente', + descending: 'Descendente', reputation: 'Reputación', planets: 'Planetas', difficulty: 'Dificultad', diff --git a/src/locales/ja.ts b/src/locales/ja.ts index 14956bb..ab3d6dc 100644 --- a/src/locales/ja.ts +++ b/src/locales/ja.ts @@ -1226,6 +1226,8 @@ export default { diplomacy: { sort: { label: '並び替え', + ascending: '昇順', + descending: '降順', reputation: '評判', planets: '惑星', difficulty: '難易度', diff --git a/src/locales/ko.ts b/src/locales/ko.ts index 41b2bb8..c4553cd 100644 --- a/src/locales/ko.ts +++ b/src/locales/ko.ts @@ -1176,6 +1176,8 @@ export default { diplomacy: { sort: { label: '정렬', + ascending: '오름차순', + descending: '내림차순', reputation: '평판', planets: '행성', difficulty: '난이도', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 567a6a9..8823039 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1202,6 +1202,8 @@ export default { diplomacy: { sort: { label: 'Сортировка', + ascending: 'По возрастанию', + descending: 'По убыванию', reputation: 'Репутация', planets: 'Планеты', difficulty: 'Сложность', diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 6858d40..f6e5996 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -1275,6 +1275,8 @@ export default { searchPlaceholder: '搜索NPC名称...', sort: { label: '排序', + ascending: '升序', + descending: '降序', reputation: '好感度', planets: '星球数量', difficulty: '难度', diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 6df86c9..3084b05 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -1195,6 +1195,8 @@ export default { diplomacy: { sort: { label: '排序', + ascending: '升序', + descending: '降序', reputation: '聲望', planets: '星球', difficulty: '難度', diff --git a/src/logic/battleLogic.ts b/src/logic/battleLogic.ts index 4244a1a..1cf52b2 100644 --- a/src/logic/battleLogic.ts +++ b/src/logic/battleLogic.ts @@ -2,6 +2,7 @@ import type { Fleet, Resources, BattleResult, Officer, TechnologyType } from '@/ import { DefenseType, OfficerType } from '@/types/game' import { workerManager } from '@/workers/workerManager' import { MOON_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' /** * 执行战斗模拟 @@ -66,7 +67,7 @@ export const simulateBattle = async ( // 生成战斗报告 const battleResult: BattleResult = { - id: `battle_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('battle'), timestamp: Date.now(), attackerId: '', defenderId: '', diff --git a/src/logic/campaignLogic.ts b/src/logic/campaignLogic.ts index 0820654..b16a509 100644 --- a/src/logic/campaignLogic.ts +++ b/src/logic/campaignLogic.ts @@ -19,6 +19,7 @@ import { type ShipType } from '@/types/game' import { MAIN_CAMPAIGN, getAllQuests, getQuestById, getQuestsByChapter } from '@/config/campaignConfig' +import { generateId } from '@/utils/id' import * as resourceLogic from './resourceLogic' /** @@ -515,7 +516,7 @@ export const createQuestNotification = ( ): QuestNotification => { const quest = getQuestById(questId) return { - id: `quest_notification_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('quest_notification'), timestamp: Date.now(), questId, questTitleKey: quest?.titleKey || '', diff --git a/src/logic/fleetLogic.ts b/src/logic/fleetLogic.ts index a377037..b6e9c42 100644 --- a/src/logic/fleetLogic.ts +++ b/src/logic/fleetLogic.ts @@ -3,6 +3,7 @@ import type { Locale } from '@/locales' import { ShipType, DefenseType, MissionType, BuildingType, OfficerType, TechnologyType, ExpeditionZone } from '@/types/game' import { FLEET_STORAGE_CONFIG, EXPEDITION_ZONES } from '@/config/gameConfig' import { useGameStore } from '@/stores/gameStore' +import { generateId } from '@/utils/id' import * as battleLogic from './battleLogic' import * as moonLogic from './moonLogic' import * as moonValidation from './moonValidation' @@ -61,7 +62,7 @@ export const createFleetMission = ( ): FleetMission => { const now = Date.now() return { - id: `mission_${now}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('mission', now), playerId, originPlanetId, // 深拷贝targetPosition,避免多个任务共享同一个引用 @@ -171,7 +172,7 @@ export const processAttackArrival = async ( ) // 更新战斗报告ID - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = attacker.id battleResult.defenderId = targetPlanet.ownerId || 'unknown' battleResult.attackerPlanetId = mission.originPlanetId @@ -267,7 +268,7 @@ export const processNPCAttackArrival = async ( ) // 更新战斗报告ID和参与者信息 - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = npc.id battleResult.defenderId = defender.id battleResult.attackerPlanetId = mission.originPlanetId @@ -414,7 +415,7 @@ export const processColonizeArrival = ( // 创建新殖民地 const newPlanet: Planet = { - id: `planet_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('planet'), name: `${colonyNameTemplate} ${mission.targetPosition.galaxy}:${mission.targetPosition.system}:${mission.targetPosition.position}`, ownerId: player.id, position: mission.targetPosition, @@ -564,7 +565,7 @@ export const processSpyArrival = ( const wasDetected = Math.random() < detectionChance const spyReport: SpyReport = { - id: `spy_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('spy'), timestamp: Date.now(), spyId: attacker.id, targetPlanetId: targetPlanet.id, @@ -1050,7 +1051,7 @@ export const processDestroyArrival = async ( ) // 更新战斗报告 - battleResult.id = `battle_${Date.now()}` + battleResult.id = generateId('battle') battleResult.attackerId = attacker.id battleResult.defenderId = targetPlanet.ownerId || 'unknown' battleResult.attackerPlanetId = mission.originPlanetId diff --git a/src/logic/moonLogic.ts b/src/logic/moonLogic.ts index ac8378a..df85588 100644 --- a/src/logic/moonLogic.ts +++ b/src/logic/moonLogic.ts @@ -1,6 +1,7 @@ import type { Planet, Resources } from '@/types/game' import { BuildingType, ShipType, DefenseType } from '@/types/game' import { MOON_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' /** * 计算月球生成概率 @@ -79,7 +80,7 @@ export const tryGenerateMoon = ( // 生成月球 const moon: Planet = { - id: `moon_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`, + id: generateId('moon'), name: `Moon [${planetPosition.galaxy}:${planetPosition.system}:${planetPosition.position}]`, ownerId: playerId, position: planetPosition, diff --git a/src/logic/planetLogic.ts b/src/logic/planetLogic.ts index a13a5b2..6e8cf45 100644 --- a/src/logic/planetLogic.ts +++ b/src/logic/planetLogic.ts @@ -1,6 +1,7 @@ import type { Planet, Resources } from '@/types/game' import { ShipType, DefenseType, BuildingType } from '@/types/game' import { MOON_CONFIG, PLANET_CONFIG, FLEET_STORAGE_CONFIG } from '@/config/gameConfig' +import { generateId } from '@/utils/id' import * as oreDepositLogic from './oreDepositLogic' /** @@ -173,7 +174,7 @@ export const createMoon = ( moonSuffix: string = "'s Moon", diameter?: number ): Planet => { - const moonId = `moon_${Date.now()}_${Math.random().toString(36).substring(2, 9)}` + const moonId = generateId('moon') const moon: Planet = { id: moonId, name: `${parentPlanet.name}${moonSuffix}`, diff --git a/src/utils/id.ts b/src/utils/id.ts new file mode 100644 index 0000000..56c6515 --- /dev/null +++ b/src/utils/id.ts @@ -0,0 +1,7 @@ +/** + * 统一生成带前缀的业务ID + * 便于后续集中调整ID规则 + */ +export const generateId = (prefix: string, timestamp: number = Date.now()): string => { + return `${prefix}_${timestamp}_${Math.random().toString(36).slice(2, 9)}` +} diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index d1d0939..aa3d78a 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -207,7 +207,7 @@ variant="outline" size="icon" @click="sortOrder = sortOrder === 'asc' ? 'desc' : 'asc'" - :title="sortOrder === 'asc' ? 'Ascending' : 'Descending'" + :title="sortOrder === 'asc' ? t('diplomacy.sort.ascending') : t('diplomacy.sort.descending')" > diff --git a/src/views/GMView.vue b/src/views/GMView.vue index fcd5751..0603e40 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -80,7 +80,7 @@ - + {{ t('gmView.presets') || 'Presets' }} @@ -327,20 +327,81 @@ values: Record } - const presetOverwriteDialogOpen = ref(false) - const pendingPresetToOverwrite = ref<{ - section: any + type GMSectionTabValue = 'buildings' | 'research' | 'ships' | 'defense' | 'officers' + type GMPresetSectionKey = Exclude + + type GMSection = { + tabValue: GMSectionTabValue + titleKey: string + descKey: string + items: string[] + max?: number + placeholder?: string + buttons: { label: string; value: number }[] + getItemName: (item: string) => string + getValue: (item: string) => number + setValue: (item: string, val: number) => void + onButtonClick: (item: string, val: number) => void + } + + type GMPresetSection = GMSection & { + tabValue: GMPresetSectionKey + } + + interface PendingPresetOverwrite { + section: GMPresetSection name: string values: Record existingIndex: number - } | null>(null) - - const getPresets = (type: string): GMPreset[] => { - const data = localStorage.getItem(`gm_presets_${type}`) - return data ? JSON.parse(data) : [] } - const savePresets = (type: string, presets: GMPreset[]) => { + // 校验预设结构,避免历史脏数据污染当前视图 + const isGMPreset = (value: unknown): value is GMPreset => { + if (typeof value !== 'object' || value === null) { + return false + } + + const preset = value as Partial + return typeof preset.id === 'string' && typeof preset.name === 'string' && typeof preset.values === 'object' && preset.values !== null + } + + // 只有建筑/科技/舰船/防御页支持预设 + const isPresettableSection = (section: GMSection): section is GMPresetSection => { + return section.tabValue !== 'officers' + } + + const presetOverwriteDialogOpen = ref(false) + const pendingPresetToOverwrite = ref(null) + + const getPresets = (type: GMPresetSectionKey): GMPreset[] => { + const key = `gm_presets_${type}` + const data = localStorage.getItem(key) + if (!data) { + return [] + } + + try { + // 兼容旧版本或手动修改导致的损坏数据,避免页面因解析失败崩溃 + const parsed = JSON.parse(data) + if (!Array.isArray(parsed)) { + localStorage.removeItem(key) + return [] + } + + const presets = parsed.filter(isGMPreset) + // 过滤掉结构不完整的预设,并顺手回写清理后的结果 + if (presets.length !== parsed.length) { + localStorage.setItem(key, JSON.stringify(presets)) + } + + return presets + } catch { + localStorage.removeItem(key) + return [] + } + } + + const savePresets = (type: GMPresetSectionKey, presets: GMPreset[]) => { localStorage.setItem(`gm_presets_${type}`, JSON.stringify(presets)) } @@ -365,7 +426,9 @@ defense: getPresets('defense') }) - const handleSavePreset = (section: any) => { + const handleSavePreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + const name = presetNames.value[section.tabValue]?.trim() if (!name) { toast.error(t('gmView.presetNameRequired') || '请输入预设名称') @@ -432,7 +495,9 @@ pendingPresetToOverwrite.value = null } - const handleDeletePreset = (section: any) => { + const handleDeletePreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + const presetId = selectedPresets.value[section.tabValue] if (!presetId || presetId === 'default') { toast.error(t('gmView.cannotDeleteDefault') || '无法删除默认预设') @@ -450,7 +515,9 @@ } } - const handleApplyPreset = (section: any) => { + const handleApplyPreset = (section: GMSection) => { + if (!isPresettableSection(section)) return + const presetId = selectedPresets.value[section.tabValue] if (!presetId) return @@ -487,18 +554,17 @@ // 重新计算最大舰队仓储,确保数据是最新的 const maxStorage = calculateMaxFleetStorage(selectedPlanet.value, gameStore.player.technologies) - - const shipTypes = Object.values(ShipType) + // 将总容量平均分配给每种舰船 - const storagePerShip = maxStorage / shipTypes.length - - shipTypes.forEach(type => { - const usage = SHIPS.value[type]?.storageUsage || 1 + const storagePerShip = maxStorage / section.items.length + + section.items.forEach(item => { + const usage = SHIPS.value[item as ShipType]?.storageUsage || 1 // 如果 usage 为 0 (如某些特殊单位),则给予一个默认数量,或者跳过 if (usage <= 0) { - section.setValue(type, 100) // 防止除以0,给予固定值 + section.setValue(item, 100) // 防止除以0,给予固定值 } else { - section.setValue(type, Math.floor(storagePerShip / usage)) + section.setValue(item, Math.floor(storagePerShip / usage)) } }) } else if (section.tabValue === 'defense') { @@ -512,7 +578,7 @@ // 反弹道导弹占用1个空间,分配一半容量 section.setValue(item, Math.floor(halfCapacity)) } else if (item === DefenseType.InterplanetaryMissile) { - // 星际导弹占用2个空间,分配一半容量 + // 星际导弹占用1个空间,分配一半容量 section.setValue(item, Math.floor(halfCapacity)) } else { section.setValue(item, 10000) @@ -606,22 +672,6 @@ } } - // GM编辑区块配置 - 统一管理建筑/科技/舰船/防御/军官 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - type GMSection = { - tabValue: string - titleKey: string - descKey: string - items: string[] - max?: number - placeholder?: string - buttons: { label: string; value: number }[] - getItemName: (item: any) => string - getValue: (item: any) => number - setValue: (item: any, val: number) => void - onButtonClick: (item: any, val: number) => void - } - const gmSections = computed(() => [ { tabValue: 'buildings', @@ -634,17 +684,17 @@ { label: 'Lv 10', value: 10 }, { label: 'Lv 30', value: 30 } ], - getItemName: (item: BuildingType) => BUILDINGS.value[item].name, - getValue: (item: BuildingType) => selectedPlanet.value?.buildings[item] || 0, - setValue: (item: BuildingType, val: number) => { + getItemName: item => BUILDINGS.value[item as BuildingType].name, + getValue: item => selectedPlanet.value?.buildings[item as BuildingType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.buildings[item] = val + selectedPlanet.value.buildings[item as BuildingType] = val updatePlayerPoints() } }, - onButtonClick: (item: BuildingType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.buildings[item] = val + selectedPlanet.value.buildings[item as BuildingType] = val updatePlayerPoints() } } @@ -660,14 +710,14 @@ { label: 'Lv 10', value: 10 }, { label: 'Lv 20', value: 20 } ], - getItemName: (item: TechnologyType) => TECHNOLOGIES.value[item].name, - getValue: (item: TechnologyType) => gameStore.player.technologies[item] || 0, - setValue: (item: TechnologyType, val: number) => { - gameStore.player.technologies[item] = val + getItemName: item => TECHNOLOGIES.value[item as TechnologyType].name, + getValue: item => gameStore.player.technologies[item as TechnologyType] || 0, + setValue: (item, val) => { + gameStore.player.technologies[item as TechnologyType] = val updatePlayerPoints() }, - onButtonClick: (item: TechnologyType, val: number) => { - gameStore.player.technologies[item] = val + onButtonClick: (item, val) => { + gameStore.player.technologies[item as TechnologyType] = val updatePlayerPoints() } }, @@ -682,17 +732,17 @@ { label: '+100', value: 100 }, { label: '+1K', value: 1000 } ], - getItemName: (item: ShipType) => SHIPS.value[item].name, - getValue: (item: ShipType) => selectedPlanet.value?.fleet[item] || 0, - setValue: (item: ShipType, val: number) => { + getItemName: item => SHIPS.value[item as ShipType].name, + getValue: item => selectedPlanet.value?.fleet[item as ShipType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.fleet[item] = val + selectedPlanet.value.fleet[item as ShipType] = val updatePlayerPoints() } }, - onButtonClick: (item: ShipType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.fleet[item] = (selectedPlanet.value.fleet[item] || 0) + val + selectedPlanet.value.fleet[item as ShipType] = (selectedPlanet.value.fleet[item as ShipType] || 0) + val updatePlayerPoints() } } @@ -708,17 +758,17 @@ { label: '+100', value: 100 }, { label: '+1K', value: 1000 } ], - getItemName: (item: DefenseType) => DEFENSES.value[item].name, - getValue: (item: DefenseType) => selectedPlanet.value?.defense[item] || 0, - setValue: (item: DefenseType, val: number) => { + getItemName: item => DEFENSES.value[item as DefenseType].name, + getValue: item => selectedPlanet.value?.defense[item as DefenseType] || 0, + setValue: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.defense[item] = val + selectedPlanet.value.defense[item as DefenseType] = val updatePlayerPoints() } }, - onButtonClick: (item: DefenseType, val: number) => { + onButtonClick: (item, val) => { if (selectedPlanet.value) { - selectedPlanet.value.defense[item] = (selectedPlanet.value.defense[item] || 0) + val + selectedPlanet.value.defense[item as DefenseType] = (selectedPlanet.value.defense[item as DefenseType] || 0) + val updatePlayerPoints() } } @@ -735,27 +785,28 @@ { label: `30${t('gmView.days')}`, value: 30 }, { label: `365${t('gmView.days')}`, value: 365 } ], - getItemName: (item: OfficerType) => OFFICERS.value[item].name, - getValue: (item: OfficerType) => officerDays.value[item] || 0, - setValue: (item: OfficerType, val: number) => { - officerDays.value[item] = val + getItemName: item => OFFICERS.value[item as OfficerType].name, + getValue: item => officerDays.value[item as OfficerType] || 0, + setValue: (item, val) => { + officerDays.value[item as OfficerType] = val }, - onButtonClick: (item: OfficerType, days: number) => { - officerDays.value[item] = days + onButtonClick: (item, days) => { + const officerType = item as OfficerType + officerDays.value[officerType] = days const now = Date.now() const expiresAt = now + days * 24 * 60 * 60 * 1000 - if (!gameStore.player.officers[item]) { - gameStore.player.officers[item] = { - type: item, + if (!gameStore.player.officers[officerType]) { + gameStore.player.officers[officerType] = { + type: officerType, active: true, hiredAt: now, expiresAt: expiresAt } } else { - gameStore.player.officers[item].expiresAt = expiresAt - gameStore.player.officers[item].active = true - if (!gameStore.player.officers[item].hiredAt) { - gameStore.player.officers[item].hiredAt = now + gameStore.player.officers[officerType].expiresAt = expiresAt + gameStore.player.officers[officerType].active = true + if (!gameStore.player.officers[officerType].hiredAt) { + gameStore.player.officers[officerType].hiredAt = now } } } From 8f29a63756af8a0997378e2dbd373c3abadb23cb Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 19:37:18 +0800 Subject: [PATCH 06/11] =?UTF-8?q?refactor(=E8=A7=86=E5=9B=BE):=20=E6=8F=90?= =?UTF-8?q?=E5=8F=96=E5=AF=BC=E5=BC=B9=E5=8F=91=E5=B0=84=E4=BA=95=E5=AE=B9?= =?UTF-8?q?=E9=87=8F=E8=AE=A1=E7=AE=97=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将导弹发射井容量计算从 GMView.vue 中提取到专用逻辑模块 简化防御设置中的导弹数量分配逻辑,统一使用提取的函数 --- src/views/GMView.vue | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 0603e40..f6867a9 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -318,6 +318,7 @@ import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import * as publicLogic from '@/logic/publicLogic' import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' + import { calculateMissileSiloCapacity } from '@/logic/shipLogic' import { Home, Trash2 } from 'lucide-vue-next' // --- 预设系统 --- @@ -569,17 +570,13 @@ }) } else if (section.tabValue === 'defense') { if (!selectedPlanet.value) return - const siloLevel = selectedPlanet.value.buildings[BuildingType.MissileSilo] || 0 - const missileCapacity = siloLevel * 10 - const halfCapacity = missileCapacity / 2 - + const missileCapacity = calculateMissileSiloCapacity(selectedPlanet.value.buildings) + const defaultMissileCount = Math.floor(missileCapacity / 2) + section.items.forEach((item: string) => { - if (item === DefenseType.AntiBallisticMissile) { - // 反弹道导弹占用1个空间,分配一半容量 - section.setValue(item, Math.floor(halfCapacity)) - } else if (item === DefenseType.InterplanetaryMissile) { - // 星际导弹占用1个空间,分配一半容量 - section.setValue(item, Math.floor(halfCapacity)) + // 两种导弹都占用1格空间,默认各分配一半容量 + if (item === DefenseType.AntiBallisticMissile || item === DefenseType.InterplanetaryMissile) { + section.setValue(item, defaultMissileCount) } else { section.setValue(item, 10000) } From b1cf0acaaefff2bb065f361fdb04672db0bc1674 Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 19:41:48 +0800 Subject: [PATCH 07/11] =?UTF-8?q?refactor(logic):=20=E5=B0=86=E5=AF=BC?= =?UTF-8?q?=E5=BC=B9=E7=9B=B8=E5=85=B3=E9=80=BB=E8=BE=91=E4=BB=8E=20shipLo?= =?UTF-8?q?gic=20=E7=A7=BB=E5=8A=A8=E5=88=B0=20missileLogic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构代码结构,将与导弹容量计算和验证相关的函数从 shipLogic 模块提取到新创建的 missileLogic 模块,以提高代码的模块化和可维护性。同时更新所有相关导入路径以引用新的模块。 --- src/logic/missileLogic.ts | 62 +++++++++++++++++++++++++++++++++++-- src/logic/shipLogic.ts | 58 ---------------------------------- src/logic/shipValidation.ts | 3 +- src/views/DefenseView.vue | 5 +-- src/views/GMView.vue | 2 +- 5 files changed, 66 insertions(+), 64 deletions(-) diff --git a/src/logic/missileLogic.ts b/src/logic/missileLogic.ts index aa13252..714e0ba 100644 --- a/src/logic/missileLogic.ts +++ b/src/logic/missileLogic.ts @@ -3,8 +3,66 @@ * 处理星际导弹攻击、射程计算、拦截等 */ -import type { Planet, MissileAttack, DefenseType, TechnologyType, Position } from '@/types/game' -import { DefenseType as DefenseTypes } from '@/types/game' +import type { Planet, MissileAttack, DefenseType, TechnologyType, Position, BuildQueueItem } from '@/types/game' +import { DefenseType as DefenseTypes, BuildingType } from '@/types/game' + +/** + * 计算导弹发射井容量 + */ +export const calculateMissileSiloCapacity = (buildings: Partial>): number => { + const siloLevel = buildings[BuildingType.MissileSilo] || 0 + return siloLevel * 10 // 每级存储10枚导弹 +} + +/** + * 计算当前导弹总数 + */ +export const calculateCurrentMissileCount = (defense: Partial>): number => { + const interplanetaryMissiles = defense[DefenseTypes.InterplanetaryMissile] || 0 + const antiBallisticMissiles = defense[DefenseTypes.AntiBallisticMissile] || 0 + return interplanetaryMissiles + antiBallisticMissiles +} + +/** + * 计算建造队列中的导弹总数 + */ +export const calculateQueueMissileCount = (buildQueue: BuildQueueItem[]): number => { + let queueMissileCount = 0 + + for (const item of buildQueue) { + if (item.type === 'defense') { + const defenseType = item.itemType as DefenseType + if (defenseType === DefenseTypes.InterplanetaryMissile || defenseType === DefenseTypes.AntiBallisticMissile) { + queueMissileCount += item.quantity || 0 + } + } + } + + return queueMissileCount +} + +/** + * 检查导弹容量限制 + */ +export const checkMissileSiloLimit = ( + defenseType: DefenseType, + currentDefense: Partial>, + buildings: Partial>, + quantity: number, + buildQueue?: BuildQueueItem[] +): boolean => { + // 只对导弹类型进行检查 + if (defenseType !== DefenseTypes.InterplanetaryMissile && defenseType !== DefenseTypes.AntiBallisticMissile) { + return true + } + + const maxCapacity = calculateMissileSiloCapacity(buildings) + const currentCount = calculateCurrentMissileCount(currentDefense) + const queueCount = buildQueue ? calculateQueueMissileCount(buildQueue) : 0 + const newCount = currentCount + queueCount + quantity + + return newCount <= maxCapacity +} /** * 计算导弹射程(基于脉冲引擎等级) diff --git a/src/logic/shipLogic.ts b/src/logic/shipLogic.ts index 35e36a0..4659d65 100644 --- a/src/logic/shipLogic.ts +++ b/src/logic/shipLogic.ts @@ -156,64 +156,6 @@ export const checkShieldDomeLimit = ( return true } -/** - * 计算导弹发射井容量 - */ -export const calculateMissileSiloCapacity = (buildings: Partial>): number => { - const siloLevel = buildings[BuildingType.MissileSilo] || 0 - return siloLevel * 10 // 每级存储10枚导弹 -} - -/** - * 计算当前导弹总数 - */ -export const calculateCurrentMissileCount = (defense: Partial>): number => { - const interplanetaryMissiles = defense[DefenseType.InterplanetaryMissile] || 0 - const antiBallisticMissiles = defense[DefenseType.AntiBallisticMissile] || 0 - return interplanetaryMissiles + antiBallisticMissiles -} - -/** - * 计算建造队列中的导弹总数 - */ -export const calculateQueueMissileCount = (buildQueue: Array<{ type: string; itemType: string; quantity?: number }>): number => { - let queueMissileCount = 0 - - for (const item of buildQueue) { - if (item.type === 'defense') { - const defenseType = item.itemType as DefenseType - if (defenseType === DefenseType.InterplanetaryMissile || defenseType === DefenseType.AntiBallisticMissile) { - queueMissileCount += item.quantity || 0 - } - } - } - - return queueMissileCount -} - -/** - * 检查导弹容量限制 - */ -export const checkMissileSiloLimit = ( - defenseType: DefenseType, - currentDefense: Partial>, - buildings: Partial>, - quantity: number, - buildQueue?: Array<{ type: string; itemType: string; quantity?: number }> -): boolean => { - // 只对导弹类型进行检查 - if (defenseType !== DefenseType.InterplanetaryMissile && defenseType !== DefenseType.AntiBallisticMissile) { - return true - } - - const maxCapacity = calculateMissileSiloCapacity(buildings) - const currentCount = calculateCurrentMissileCount(currentDefense) - const queueCount = buildQueue ? calculateQueueMissileCount(buildQueue) : 0 - const newCount = currentCount + queueCount + quantity - - return newCount <= maxCapacity -} - /** * 创建舰船建造队列项 */ diff --git a/src/logic/shipValidation.ts b/src/logic/shipValidation.ts index 0a1ba54..58d854e 100644 --- a/src/logic/shipValidation.ts +++ b/src/logic/shipValidation.ts @@ -1,6 +1,7 @@ import type { Planet, Resources, BuildQueueItem, Fleet, Officer } from '@/types/game' import { ShipType, DefenseType, TechnologyType, OfficerType, BuildingType } from '@/types/game' import * as shipLogic from './shipLogic' +import * as missileLogic from './missileLogic' import * as resourceLogic from './resourceLogic' import * as officerLogic from './officerLogic' import * as publicLogic from './publicLogic' @@ -101,7 +102,7 @@ export const validateDefenseBuild = ( } // 导弹发射井容量限制 - if (!shipLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity, planet.buildQueue)) { + if (!missileLogic.checkMissileSiloLimit(defenseType, planet.defense, planet.buildings, quantity, planet.buildQueue)) { return { valid: false, reason: 'errors.missileSiloLimit' } } diff --git a/src/views/DefenseView.vue b/src/views/DefenseView.vue index 9555adf..cb006b5 100644 --- a/src/views/DefenseView.vue +++ b/src/views/DefenseView.vue @@ -190,6 +190,7 @@ import * as publicLogic from '@/logic/publicLogic' import * as shipValidation from '@/logic/shipValidation' import * as shipLogic from '@/logic/shipLogic' + import * as missileLogic from '@/logic/missileLogic' import * as gameLogic from '@/logic/gameLogic' import * as waitingQueueLogic from '@/logic/waitingQueueLogic' import * as officerLogic from '@/logic/officerLogic' @@ -204,12 +205,12 @@ // 导弹容量相关计算 const missileSiloCapacity = computed(() => { if (!planet.value) return 0 - return shipLogic.calculateMissileSiloCapacity(planet.value.buildings) + return missileLogic.calculateMissileSiloCapacity(planet.value.buildings) }) const currentMissileCount = computed(() => { if (!planet.value) return 0 - return shipLogic.calculateCurrentMissileCount(planet.value.defense) + return missileLogic.calculateCurrentMissileCount(planet.value.defense) }) // AlertDialog 状态 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index f6867a9..fa5fd74 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -318,7 +318,7 @@ import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import * as publicLogic from '@/logic/publicLogic' import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' - import { calculateMissileSiloCapacity } from '@/logic/shipLogic' + import { calculateMissileSiloCapacity } from '@/logic/missileLogic' import { Home, Trash2 } from 'lucide-vue-next' // --- 预设系统 --- From 2cfa275c7af67de357b91b74047ad6ece40520f0 Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 20:26:06 +0800 Subject: [PATCH 08/11] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=98=9F=E7=90=83=20ID=20=E5=B9=B6=E4=BC=98=E5=8C=96?= =?UTF-8?q?=20NPC=20=E5=88=97=E8=A1=A8=E6=8E=92=E5=BA=8F=E6=80=A7=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 修复玩家星球重复 ID 问题,通过构建映射关系更新相关引用(舰队任务、间谍报告等),避免数据指向错误目标。同时优化外交界面 NPC 列表计算,避免重复排序操作提升性能,并添加空列表检查防止除零错误。 --- src/utils/migration.ts | 447 +++++++++++++++++++++++++++++++----- src/views/DiplomacyView.vue | 20 +- src/views/GMView.vue | 2 + 3 files changed, 406 insertions(+), 63 deletions(-) diff --git a/src/utils/migration.ts b/src/utils/migration.ts index cc871b9..84af296 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -1,4 +1,18 @@ -import type { Planet, DebrisField, NPC } from '@/types/game' +import type { + AllyDefenseNotification, + DebrisField, + FleetMission, + IncomingFleetAlert, + JointAttackInvite, + MissionReport, + NPC, + NPCActivityNotification, + Planet, + Player, + Position, + SpiedNotification, + SpyReport +} from '@/types/game' import { decryptData, encryptData } from './crypto' import { generatePlanetTemperature } from '@/logic/planetLogic' import pkg from '../../package.json' @@ -8,6 +22,378 @@ import pkg from '../../package.json' * 用于从旧版本数据结构迁移到新版本 */ +type PlanetKind = 'planet' | 'moon' + +// oldPlanetId -> position -> planet/moon -> remapped target +type DuplicatePlanetIdMap = Map< + string, + Map> +> + +interface MigratablePlayer extends Player { + diplomaticRelations?: Record +} + +interface MigratableGameData { + currentPlanetId?: string + player?: MigratablePlayer + npcs?: NPC[] + universePlanets?: Record + debrisFields?: Record +} + +interface PlanetReferenceContext { + position?: Position + isMoon?: boolean + planetName?: string +} + +const getPlanetPositionKey = (position: Position): string => { + return `${position.galaxy}:${position.system}:${position.position}` +} + +const getPlanetKindKey = (isMoon?: boolean): PlanetKind => { + return isMoon ? 'moon' : 'planet' +} + +const buildDuplicatePlanetIdMap = (player: Player): DuplicatePlanetIdMap => { + const planetsByOriginalId = new Map() + + player.planets.forEach(planet => { + let group = planetsByOriginalId.get(planet.id) + if (!group) { + group = [] + planetsByOriginalId.set(planet.id, group) + } + group.push(planet) + }) + + const idMap: DuplicatePlanetIdMap = new Map() + + planetsByOriginalId.forEach((planets, originalId) => { + if (planets.length <= 1) return + + planets.forEach((planet, index) => { + if (index === 0) return + + const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` + const positionKey = getPlanetPositionKey(planet.position) + + let byPosition = idMap.get(originalId) + if (!byPosition) { + byPosition = new Map() + idMap.set(originalId, byPosition) + } + + let byKind = byPosition.get(positionKey) + if (!byKind) { + byKind = new Map() + byPosition.set(positionKey, byKind) + } + + byKind.set(getPlanetKindKey(planet.isMoon), { + newId, + name: planet.name + }) + + planet.id = newId + }) + }) + + return idMap +} + +const resolveRemappedPlanetId = ( + planetId: string | undefined, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): string | undefined => { + if (!planetId) return undefined + + const byPosition = idMap.get(planetId) + if (!byPosition) return undefined + + if (context.position) { + const byKind = byPosition.get(getPlanetPositionKey(context.position)) + if (!byKind) return undefined + + // 只有在位置或名称足够区分目标时才重写引用,避免把旧引用误指到错误星球 + if (context.isMoon !== undefined) { + return byKind.get(getPlanetKindKey(context.isMoon))?.newId + } + + if (context.planetName) { + const matchedByName = Array.from(byKind.values()).filter(entry => entry.name === context.planetName) + if (matchedByName.length === 1) { + const [matchedEntry] = matchedByName + if (matchedEntry) { + return matchedEntry.newId + } + } + } + + if (byKind.size === 1) { + return Array.from(byKind.values())[0]?.newId + } + + return undefined + } + + if (context.planetName) { + const matchedByName: Array<{ newId: string; name: string }> = [] + + byPosition.forEach(byKind => { + byKind.forEach(entry => { + if (entry.name === context.planetName) { + matchedByName.push(entry) + } + }) + }) + + if (matchedByName.length === 1) { + const [matchedEntry] = matchedByName + if (matchedEntry) { + return matchedEntry.newId + } + } + } + + return undefined +} + +const updatePlanetIdField = < + T extends Record, + K extends keyof T +>( + target: T, + key: K, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const currentValue = target[key] + if (typeof currentValue !== 'string') return false + + const remappedPlanetId = resolveRemappedPlanetId(currentValue, idMap, context) + if (!remappedPlanetId || remappedPlanetId === currentValue) return false + + target[key] = remappedPlanetId as T[K] + return true +} + +const updateMissionTargetPlanetId = (mission: FleetMission, idMap: DuplicatePlanetIdMap): boolean => { + return updatePlanetIdField(mission as unknown as Record, 'targetPlanetId', idMap, { + position: mission.targetPosition, + isMoon: mission.targetIsMoon + }) +} + +const updateSpyReportTargetPlanetId = (report: SpyReport, idMap: DuplicatePlanetIdMap): boolean => { + return updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) +} + +const updateSpiedNotificationTargetPlanetId = ( + notification: SpiedNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateNPCActivityTargetPlanetId = ( + notification: NPCActivityNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + position: notification.targetPosition, + planetName: notification.targetPlanetName + }) +} + +const updateIncomingAlertTargetPlanetId = ( + alert: IncomingFleetAlert, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(alert as unknown as Record, 'targetPlanetId', idMap, { + planetName: alert.targetPlanetName + }) +} + +const updateJointAttackTargetPlanetId = ( + invite: JointAttackInvite, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(invite as unknown as Record, 'targetPlanetId', idMap, { + position: invite.targetPosition + }) +} + +const updateAllyDefenseTargetPlanetId = ( + notification: AllyDefenseNotification, + idMap: DuplicatePlanetIdMap +): boolean => { + return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + planetName: notification.targetPlanetName + }) +} + +const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + if (updatePlanetIdField(report as unknown as Record, 'originPlanetId', idMap, { + planetName: report.originPlanetName + })) { + mutated = true + } + + if (updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + })) { + mutated = true + } + + if (report.details?.newPlanetId) { + const remappedNewPlanetId = resolveRemappedPlanetId(report.details.newPlanetId, idMap, { + position: report.targetPosition, + planetName: report.details.newPlanetName || report.targetPlanetName + }) + + if (remappedNewPlanetId && remappedNewPlanetId !== report.details.newPlanetId) { + report.details.newPlanetId = remappedNewPlanetId + mutated = true + } + } + + return mutated +} + +/** + * 修复玩家星球的重复ID,并同步更新可被可靠识别的旧引用。 + * 缺少位置或名称上下文、无法安全判定归属的旧引用会保留原ID, + * 继续指向保留下来的首个星球,避免把数据误指到错误目标。 + */ +const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { + const player = data.player + if (!player || !Array.isArray(player.planets) || player.planets.length === 0) { + return false + } + + const idMap = buildDuplicatePlanetIdMap(player) + if (idMap.size === 0) { + return false + } + + let mutated = true + + player.planets.forEach(planet => { + if (planet.isMoon && updatePlanetIdField(planet as unknown as Record, 'parentPlanetId', idMap, { + position: planet.position, + isMoon: false + })) { + mutated = true + } + + // 等待队列里的 planetId 应始终与所属星球保持一致 + planet.waitingBuildQueue?.forEach(item => { + if (item.planetId && item.planetId !== planet.id) { + item.planetId = planet.id + mutated = true + } + }) + }) + + if (updatePlanetIdField(data as unknown as Record, 'currentPlanetId', idMap)) { + mutated = true + } + + player.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + + player.spyReports?.forEach(report => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + }) + + player.spiedNotifications?.forEach(notification => { + if (updateSpiedNotificationTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.npcActivityNotifications?.forEach(notification => { + if (updateNPCActivityTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + player.missionReports?.forEach(report => { + if (updateMissionReportPlanetIds(report, idMap)) { + mutated = true + } + }) + + player.incomingFleetAlerts?.forEach(alert => { + if (updateIncomingAlertTargetPlanetId(alert, idMap)) { + mutated = true + } + }) + + player.jointAttackInvites?.forEach(invite => { + if (updateJointAttackTargetPlanetId(invite, idMap)) { + mutated = true + } + }) + + player.allyDefenseNotifications?.forEach(notification => { + if (updateAllyDefenseTargetPlanetId(notification, idMap)) { + mutated = true + } + }) + + data.npcs?.forEach(npc => { + if (npc.playerSpyReports) { + // playerSpyReports 的 key 就是玩家星球 ID,需要和报告内容一起迁移 + const remappedPlayerSpyReports: Record = {} + + Object.entries(npc.playerSpyReports).forEach(([planetId, report]) => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } + + const remappedPlanetId = resolveRemappedPlanetId(planetId, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) + + if (remappedPlanetId && remappedPlanetId !== planetId) { + remappedPlayerSpyReports[remappedPlanetId] = report + mutated = true + } else { + remappedPlayerSpyReports[planetId] = report + } + }) + + npc.playerSpyReports = remappedPlayerSpyReports + } + + npc.fleetMissions?.forEach(mission => { + if (updateMissionTargetPlanetId(mission, idMap)) { + mutated = true + } + }) + }) + + return mutated +} + /** * 执行数据迁移 * 将旧版本的 universePlanets 和 debrisFields 从 gameStore 迁移到 universeStore @@ -22,13 +408,13 @@ export const migrateGameData = (): void => { if (!oldEncryptedData) return // 尝试解密(如果是加密格式) - let oldData: any + let oldData: MigratableGameData try { - oldData = decryptData(oldEncryptedData) + oldData = decryptData(oldEncryptedData) as MigratableGameData } catch { // 解密失败,可能是新格式(未加密),直接解析 try { - oldData = JSON.parse(oldEncryptedData) + oldData = JSON.parse(oldEncryptedData) as MigratableGameData } catch { return // 无法解析,放弃迁移 } @@ -101,54 +487,8 @@ export const migrateGameData = (): void => { } // 修复重复的星球ID - if (oldData.player?.planets && Array.isArray(oldData.player.planets)) { - const planetsByOriginalId = new Map() - - // 第一步:按ID分组 - oldData.player.planets.forEach((planet: Planet) => { - if (!planetsByOriginalId.has(planet.id)) { - planetsByOriginalId.set(planet.id, []) - } - planetsByOriginalId.get(planet.id)!.push(planet) - }) - - // 第二步:处理重复ID并建立映射 - const idMap = new Map() // Key: oldId_galaxy:system:position -> Value: newId - - planetsByOriginalId.forEach((planets, originalId) => { - if (planets.length > 1) { - // 对重复组中的星球,保留第一个,修改后续的 - planets.forEach((planet, index) => { - if (index > 0) { - const newId = `${originalId}_${Math.random().toString(36).substring(2, 9)}` - const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - // 记录映射:原始ID + 坐标 -> 新ID - idMap.set(`${originalId}_${posKey}`, newId) - - // 修改星球ID - planet.id = newId - needsSave = true - } - }) - } - }) - - // 第三步:更新月球的 parentPlanetId - if (idMap.size > 0) { - oldData.player.planets.forEach((planet: Planet) => { - if (planet.isMoon && planet.parentPlanetId) { - // 假设月球和母星坐标一致,通过月球坐标查找母星的新ID - const posKey = `${planet.position.galaxy}:${planet.position.system}:${planet.position.position}` - const mapKey = `${planet.parentPlanetId}_${posKey}` - - const newParentId = idMap.get(mapKey) - if (newParentId) { - planet.parentPlanetId = newParentId - needsSave = true - } - } - }) - } + if (fixDuplicatePlanetIds(oldData)) { + needsSave = true } // 迁移温度数据:为没有温度的星球生成温度 @@ -215,10 +555,11 @@ export const migrateGameData = (): void => { // 新版本统一使用 npc.relations[playerId] 存储NPC对玩家的关系 if (oldData.player?.diplomaticRelations && oldData.npcs && Array.isArray(oldData.npcs)) { const playerId = oldData.player.id + const npcs = oldData.npcs const playerRelations = oldData.player.diplomaticRelations as Record Object.entries(playerRelations).forEach(([npcId, relation]) => { - const npc = oldData.npcs.find((n: NPC) => n.id === npcId) + const npc = npcs.find((n: NPC) => n.id === npcId) if (npc) { if (!npc.relations) { npc.relations = {} diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index aa3d78a..2fa786a 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -708,30 +708,30 @@ } // 按关系状态分类NPC(同时应用搜索过滤) - const allNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) + // 先统一排序一次,避免不同标签页在同一批数据上重复排序 + const sortedNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) + + const allNpcs = computed(() => sortedNpcs.value) const friendlyNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Friendly - })) + }) }) const neutralNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return !relation || relation.status === RelationStatus.Neutral - })) + }) }) const hostileNpcs = computed(() => { - return sortNpcs(npcStore.npcs.filter(npc => { - if (!matchesSearch(npc)) return false + return sortedNpcs.value.filter(npc => { const relation = getRelation(npc.id) return relation?.status === RelationStatus.Hostile - })) + }) }) // 分页辅助函数 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index fa5fd74..bb5a3d4 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -552,6 +552,8 @@ }) } else if (section.tabValue === 'ships') { if (!selectedPlanet.value) return + // 某些过滤场景下舰船列表可能为空,避免平均分配时除以 0 + if (!section.items.length) return // 重新计算最大舰队仓储,确保数据是最新的 const maxStorage = calculateMaxFleetStorage(selectedPlanet.value, gameStore.player.technologies) From d4f55f6916f8037cce648d0eb83a7922d3d062cd Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 20:30:51 +0800 Subject: [PATCH 09/11] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E9=87=8D=E5=A4=8D=E6=98=9F=E7=90=83=20ID=20=E7=9A=84?= =?UTF-8?q?=E6=B3=A8=E9=87=8A=E4=BB=A5=E6=BE=84=E6=B8=85=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 更新 fixDuplicatePlanetIds 函数中的注释,明确说明 buildDuplicatePlanetIdMap 已在上一步修复重复 ID,当前函数仅通过检查 idMap 是否非空来判断迁移是否发生实际修改。 --- src/utils/migration.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/migration.ts b/src/utils/migration.ts index 84af296..5a9e132 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -287,6 +287,8 @@ const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { return false } + // buildDuplicatePlanetIdMap 已经在上一步直接修复了重复星球 ID, + // 只要 idMap 非空,就说明当前迁移已经发生了实际修改。 let mutated = true player.planets.forEach(planet => { From 15eccd8f0d9abb1c194db517fb424a42f9e72bfa Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 20:47:14 +0800 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20=E7=AE=80=E5=8C=96=E7=94=9F?= =?UTF-8?q?=E6=88=90=20ID=20=E5=87=BD=E6=95=B0=E5=B9=B6=E6=94=B9=E8=BF=9B?= =?UTF-8?q?=E7=B1=BB=E5=9E=8B=E5=AE=89=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 generateId 函数的 timestamp 参数,改为在函数内部获取当前时间戳 - 在 DiplomacyView 中为 NPC 排序添加类型定义和 exhaustive 检查 - 在 GMView 中为预设管理添加更精确的类型映射 - 重构 migration 工具函数,提取辅助函数并改进类型定义 --- src/logic/fleetLogic.ts | 2 +- src/utils/id.ts | 3 +- src/utils/migration.ts | 328 +++++++++++++++++++++++++----------- src/views/DiplomacyView.vue | 10 +- src/views/GMView.vue | 62 ++++--- 5 files changed, 272 insertions(+), 133 deletions(-) diff --git a/src/logic/fleetLogic.ts b/src/logic/fleetLogic.ts index b6e9c42..1155d17 100644 --- a/src/logic/fleetLogic.ts +++ b/src/logic/fleetLogic.ts @@ -62,7 +62,7 @@ export const createFleetMission = ( ): FleetMission => { const now = Date.now() return { - id: generateId('mission', now), + id: generateId('mission'), playerId, originPlanetId, // 深拷贝targetPosition,避免多个任务共享同一个引用 diff --git a/src/utils/id.ts b/src/utils/id.ts index 56c6515..ae96044 100644 --- a/src/utils/id.ts +++ b/src/utils/id.ts @@ -2,6 +2,7 @@ * 统一生成带前缀的业务ID * 便于后续集中调整ID规则 */ -export const generateId = (prefix: string, timestamp: number = Date.now()): string => { +export const generateId = (prefix: string): string => { + const timestamp = Date.now() return `${prefix}_${timestamp}_${Math.random().toString(36).slice(2, 9)}` } diff --git a/src/utils/migration.ts b/src/utils/migration.ts index 5a9e132..532820b 100644 --- a/src/utils/migration.ts +++ b/src/utils/migration.ts @@ -23,12 +23,12 @@ import pkg from '../../package.json' */ type PlanetKind = 'planet' | 'moon' +type RemappedPlanetEntry = { newId: string; name: string } +type DuplicatePlanetKindMap = Map +type DuplicatePlanetPositionMap = Map // oldPlanetId -> position -> planet/moon -> remapped target -type DuplicatePlanetIdMap = Map< - string, - Map> -> +type DuplicatePlanetIdMap = Map interface MigratablePlayer extends Player { diplomaticRelations?: Record @@ -48,6 +48,22 @@ interface PlanetReferenceContext { planetName?: string } +interface HasTargetPlanetId { + targetPlanetId?: string +} + +interface HasOriginPlanetId { + originPlanetId?: string +} + +interface HasParentPlanetId { + parentPlanetId?: string +} + +interface HasCurrentPlanetId { + currentPlanetId?: string +} + const getPlanetPositionKey = (position: Position): string => { return `${position.galaxy}:${position.system}:${position.position}` } @@ -56,6 +72,53 @@ const getPlanetKindKey = (isMoon?: boolean): PlanetKind => { return isMoon ? 'moon' : 'planet' } +const getPlanetEntriesFor = ( + planetId: string, + idMap: DuplicatePlanetIdMap, + position?: Position +): DuplicatePlanetKindMap | undefined => { + if (!position) return undefined + + return idMap.get(planetId)?.get(getPlanetPositionKey(position)) +} + +const getEntriesByName = (entries: Iterable, planetName?: string): RemappedPlanetEntry[] => { + if (!planetName) { + return [] + } + + return Array.from(entries).filter(entry => entry.name === planetName) +} + +const getUniqueEntryByName = (entries: Iterable, planetName?: string): RemappedPlanetEntry | undefined => { + const matchedEntries = getEntriesByName(entries, planetName) + if (matchedEntries.length !== 1) { + return undefined + } + + return matchedEntries[0] +} + +const getOnlyEntry = (entries: DuplicatePlanetKindMap): RemappedPlanetEntry | undefined => { + if (entries.size !== 1) { + return undefined + } + + return Array.from(entries.values())[0] +} + +const getEntriesAcrossPositions = (byPosition: DuplicatePlanetPositionMap): RemappedPlanetEntry[] => { + const entries: RemappedPlanetEntry[] = [] + + byPosition.forEach(byKind => { + byKind.forEach(entry => { + entries.push(entry) + }) + }) + + return entries +} + const buildDuplicatePlanetIdMap = (player: Player): DuplicatePlanetIdMap => { const planetsByOriginalId = new Map() @@ -114,7 +177,7 @@ const resolveRemappedPlanetId = ( if (!byPosition) return undefined if (context.position) { - const byKind = byPosition.get(getPlanetPositionKey(context.position)) + const byKind = getPlanetEntriesFor(planetId, idMap, context.position) if (!byKind) return undefined // 只有在位置或名称足够区分目标时才重写引用,避免把旧引用误指到错误星球 @@ -122,73 +185,99 @@ const resolveRemappedPlanetId = ( return byKind.get(getPlanetKindKey(context.isMoon))?.newId } - if (context.planetName) { - const matchedByName = Array.from(byKind.values()).filter(entry => entry.name === context.planetName) - if (matchedByName.length === 1) { - const [matchedEntry] = matchedByName - if (matchedEntry) { - return matchedEntry.newId - } - } + const matchedByName = getUniqueEntryByName(byKind.values(), context.planetName) + if (matchedByName) { + return matchedByName.newId } - if (byKind.size === 1) { - return Array.from(byKind.values())[0]?.newId - } - - return undefined + return getOnlyEntry(byKind)?.newId } if (context.planetName) { - const matchedByName: Array<{ newId: string; name: string }> = [] - - byPosition.forEach(byKind => { - byKind.forEach(entry => { - if (entry.name === context.planetName) { - matchedByName.push(entry) - } - }) - }) - - if (matchedByName.length === 1) { - const [matchedEntry] = matchedByName - if (matchedEntry) { - return matchedEntry.newId - } - } + return getUniqueEntryByName(getEntriesAcrossPositions(byPosition), context.planetName)?.newId } return undefined } -const updatePlanetIdField = < - T extends Record, - K extends keyof T ->( - target: T, - key: K, +const getUpdatedPlanetId = ( + currentPlanetId: string | undefined, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): string | undefined => { + const remappedPlanetId = resolveRemappedPlanetId(currentPlanetId, idMap, context) + if (!remappedPlanetId || remappedPlanetId === currentPlanetId) { + return undefined + } + + return remappedPlanetId +} + +const updateTargetPlanetId = ( + target: HasTargetPlanetId, idMap: DuplicatePlanetIdMap, context: PlanetReferenceContext = {} ): boolean => { - const currentValue = target[key] - if (typeof currentValue !== 'string') return false + const remappedPlanetId = getUpdatedPlanetId(target.targetPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } - const remappedPlanetId = resolveRemappedPlanetId(currentValue, idMap, context) - if (!remappedPlanetId || remappedPlanetId === currentValue) return false + target.targetPlanetId = remappedPlanetId + return true +} - target[key] = remappedPlanetId as T[K] +const updateOriginPlanetId = ( + target: HasOriginPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.originPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.originPlanetId = remappedPlanetId + return true +} + +const updateParentPlanetId = ( + target: HasParentPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.parentPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.parentPlanetId = remappedPlanetId + return true +} + +const updateCurrentPlanetId = ( + target: HasCurrentPlanetId, + idMap: DuplicatePlanetIdMap, + context: PlanetReferenceContext = {} +): boolean => { + const remappedPlanetId = getUpdatedPlanetId(target.currentPlanetId, idMap, context) + if (!remappedPlanetId) { + return false + } + + target.currentPlanetId = remappedPlanetId return true } const updateMissionTargetPlanetId = (mission: FleetMission, idMap: DuplicatePlanetIdMap): boolean => { - return updatePlanetIdField(mission as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(mission, idMap, { position: mission.targetPosition, isMoon: mission.targetIsMoon }) } const updateSpyReportTargetPlanetId = (report: SpyReport, idMap: DuplicatePlanetIdMap): boolean => { - return updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(report, idMap, { position: report.targetPosition, planetName: report.targetPlanetName }) @@ -198,7 +287,7 @@ const updateSpiedNotificationTargetPlanetId = ( notification: SpiedNotification, idMap: DuplicatePlanetIdMap ): boolean => { - return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(notification, idMap, { planetName: notification.targetPlanetName }) } @@ -207,7 +296,7 @@ const updateNPCActivityTargetPlanetId = ( notification: NPCActivityNotification, idMap: DuplicatePlanetIdMap ): boolean => { - return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(notification, idMap, { position: notification.targetPosition, planetName: notification.targetPlanetName }) @@ -217,7 +306,7 @@ const updateIncomingAlertTargetPlanetId = ( alert: IncomingFleetAlert, idMap: DuplicatePlanetIdMap ): boolean => { - return updatePlanetIdField(alert as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(alert, idMap, { planetName: alert.targetPlanetName }) } @@ -226,7 +315,7 @@ const updateJointAttackTargetPlanetId = ( invite: JointAttackInvite, idMap: DuplicatePlanetIdMap ): boolean => { - return updatePlanetIdField(invite as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(invite, idMap, { position: invite.targetPosition }) } @@ -235,7 +324,7 @@ const updateAllyDefenseTargetPlanetId = ( notification: AllyDefenseNotification, idMap: DuplicatePlanetIdMap ): boolean => { - return updatePlanetIdField(notification as unknown as Record, 'targetPlanetId', idMap, { + return updateTargetPlanetId(notification, idMap, { planetName: notification.targetPlanetName }) } @@ -243,13 +332,13 @@ const updateAllyDefenseTargetPlanetId = ( const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePlanetIdMap): boolean => { let mutated = false - if (updatePlanetIdField(report as unknown as Record, 'originPlanetId', idMap, { + if (updateOriginPlanetId(report, idMap, { planetName: report.originPlanetName })) { mutated = true } - if (updatePlanetIdField(report as unknown as Record, 'targetPlanetId', idMap, { + if (updateTargetPlanetId(report, idMap, { position: report.targetPosition, planetName: report.targetPlanetName })) { @@ -257,12 +346,12 @@ const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePla } if (report.details?.newPlanetId) { - const remappedNewPlanetId = resolveRemappedPlanetId(report.details.newPlanetId, idMap, { + const remappedNewPlanetId = getUpdatedPlanetId(report.details.newPlanetId, idMap, { position: report.targetPosition, planetName: report.details.newPlanetName || report.targetPlanetName }) - if (remappedNewPlanetId && remappedNewPlanetId !== report.details.newPlanetId) { + if (remappedNewPlanetId) { report.details.newPlanetId = remappedNewPlanetId mutated = true } @@ -271,28 +360,11 @@ const updateMissionReportPlanetIds = (report: MissionReport, idMap: DuplicatePla return mutated } -/** - * 修复玩家星球的重复ID,并同步更新可被可靠识别的旧引用。 - * 缺少位置或名称上下文、无法安全判定归属的旧引用会保留原ID, - * 继续指向保留下来的首个星球,避免把数据误指到错误目标。 - */ -const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { - const player = data.player - if (!player || !Array.isArray(player.planets) || player.planets.length === 0) { - return false - } - - const idMap = buildDuplicatePlanetIdMap(player) - if (idMap.size === 0) { - return false - } - - // buildDuplicatePlanetIdMap 已经在上一步直接修复了重复星球 ID, - // 只要 idMap 非空,就说明当前迁移已经发生了实际修改。 - let mutated = true +const fixPlayerPlanetsAndQueues = (player: Player, idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false player.planets.forEach(planet => { - if (planet.isMoon && updatePlanetIdField(planet as unknown as Record, 'parentPlanetId', idMap, { + if (planet.isMoon && updateParentPlanetId(planet, idMap, { position: planet.position, isMoon: false })) { @@ -308,7 +380,17 @@ const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { }) }) - if (updatePlanetIdField(data as unknown as Record, 'currentPlanetId', idMap)) { + return mutated +} + +const fixPlayerReferences = ( + player: Player, + data: MigratableGameData, + idMap: DuplicatePlanetIdMap +): boolean => { + let mutated = false + + if (updateCurrentPlanetId(data, idMap)) { mutated = true } @@ -360,30 +442,47 @@ const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { } }) - data.npcs?.forEach(npc => { - if (npc.playerSpyReports) { - // playerSpyReports 的 key 就是玩家星球 ID,需要和报告内容一起迁移 - const remappedPlayerSpyReports: Record = {} + return mutated +} - Object.entries(npc.playerSpyReports).forEach(([planetId, report]) => { - if (updateSpyReportTargetPlanetId(report, idMap)) { - mutated = true - } +const fixNpcPlayerSpyReports = (npc: NPC, idMap: DuplicatePlanetIdMap): boolean => { + if (!npc.playerSpyReports) { + return false + } - const remappedPlanetId = resolveRemappedPlanetId(planetId, idMap, { - position: report.targetPosition, - planetName: report.targetPlanetName - }) + let mutated = false + const remappedPlayerSpyReports: Record = {} - if (remappedPlanetId && remappedPlanetId !== planetId) { - remappedPlayerSpyReports[remappedPlanetId] = report - mutated = true - } else { - remappedPlayerSpyReports[planetId] = report - } - }) + // playerSpyReports 的 key 就是玩家星球 ID,需要和报告内容一起迁移 + Object.entries(npc.playerSpyReports).forEach(([planetId, report]) => { + if (updateSpyReportTargetPlanetId(report, idMap)) { + mutated = true + } - npc.playerSpyReports = remappedPlayerSpyReports + const remappedPlanetId = getUpdatedPlanetId(planetId, idMap, { + position: report.targetPosition, + planetName: report.targetPlanetName + }) + + if (remappedPlanetId) { + remappedPlayerSpyReports[remappedPlanetId] = report + mutated = true + return + } + + remappedPlayerSpyReports[planetId] = report + }) + + npc.playerSpyReports = remappedPlayerSpyReports + return mutated +} + +const fixNpcReferences = (npcs: NPC[], idMap: DuplicatePlanetIdMap): boolean => { + let mutated = false + + npcs.forEach(npc => { + if (fixNpcPlayerSpyReports(npc, idMap)) { + mutated = true } npc.fleetMissions?.forEach(mission => { @@ -396,6 +495,41 @@ const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { return mutated } +/** + * 修复玩家星球的重复ID,并同步更新可被可靠识别的旧引用。 + * 缺少位置或名称上下文、无法安全判定归属的旧引用会保留原ID, + * 继续指向保留下来的首个星球,避免把数据误指到错误目标。 + */ +const fixDuplicatePlanetIds = (data: MigratableGameData): boolean => { + const player = data.player + if (!player || !Array.isArray(player.planets) || player.planets.length === 0) { + return false + } + + const idMap = buildDuplicatePlanetIdMap(player) + if (idMap.size === 0) { + return false + } + + // buildDuplicatePlanetIdMap 已经在上一步直接修复了重复星球 ID, + // 只要 idMap 非空,就说明当前迁移已经发生了实际修改。 + let mutated = true + + if (fixPlayerPlanetsAndQueues(player, idMap)) { + mutated = true + } + + if (fixPlayerReferences(player, data, idMap)) { + mutated = true + } + + if (data.npcs && fixNpcReferences(data.npcs, idMap)) { + mutated = true + } + + return mutated +} + /** * 执行数据迁移 * 将旧版本的 universePlanets 和 debrisFields 从 gameStore 迁移到 universeStore diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index 2fa786a..2e8c057 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -442,6 +442,8 @@ } from 'lucide-vue-next' import { Empty, EmptyContent, EmptyDescription } from '@/components/ui/empty' + type NPCSortBy = 'reputation' | 'planets' | 'difficulty' | 'allies' + const route = useRoute() const gameStore = useGameStore() const npcStore = useNPCStore() @@ -471,9 +473,13 @@ const searchQuery = ref('') // 排序状态 - const sortBy = ref('reputation') + const sortBy = ref('reputation') const sortOrder = ref<'asc' | 'desc'>('desc') + const assertNever = (value: never): never => { + throw new Error(`Unexpected NPC sort type: ${value}`) + } + // 排序函数 const sortNpcs = (npcs: typeof npcStore.npcs) => { return [...npcs].sort((a, b) => { @@ -505,7 +511,7 @@ valB = b.allies?.length || 0 break default: - return 0 + return assertNever(sortBy.value) } if (sortOrder.value === 'asc') { diff --git a/src/views/GMView.vue b/src/views/GMView.vue index bb5a3d4..9e1a260 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -349,6 +349,10 @@ tabValue: GMPresetSectionKey } + type GMPresetNameMap = Record + type GMSelectedPresetMap = Record + type GMCustomPresetMap = Record + interface PendingPresetOverwrite { section: GMPresetSection name: string @@ -406,21 +410,21 @@ localStorage.setItem(`gm_presets_${type}`, JSON.stringify(presets)) } - const presetNames = ref>({ + const presetNames = ref({ buildings: '', research: '', ships: '', defense: '' }) - const selectedPresets = ref>({ + const selectedPresets = ref({ buildings: 'default', research: 'default', ships: 'default', defense: 'default' }) - const customPresets = ref>({ + const customPresets = ref({ buildings: getPresets('buildings'), research: getPresets('research'), ships: getPresets('ships'), @@ -442,7 +446,8 @@ }) // 检查是否存在同名预设 - const existingIndex = customPresets.value[section.tabValue]?.findIndex(p => p.name === name) ?? -1 + const presets = customPresets.value[section.tabValue] + const existingIndex = presets.findIndex(p => p.name === name) if (existingIndex !== -1) { pendingPresetToOverwrite.value = { @@ -461,11 +466,8 @@ values } - if (!customPresets.value[section.tabValue]) { - customPresets.value[section.tabValue] = [] - } - customPresets.value[section.tabValue]!.push(newPreset) - savePresets(section.tabValue, customPresets.value[section.tabValue]!) + presets.push(newPreset) + savePresets(section.tabValue, presets) presetNames.value[section.tabValue] = '' selectedPresets.value[section.tabValue] = newPreset.id toast.success(t('gmView.presetSaved') || '预设保存成功') @@ -476,20 +478,18 @@ const { section, values, existingIndex } = pendingPresetToOverwrite.value - if (customPresets.value[section.tabValue]) { - const presets = customPresets.value[section.tabValue]! - - if (presets[existingIndex]) { - // 更新现有预设的值,保持ID不变 - presets[existingIndex].values = values - - savePresets(section.tabValue, presets) - - presetNames.value[section.tabValue] = '' - selectedPresets.value[section.tabValue] = presets[existingIndex].id - - toast.success(t('gmView.presetSaved') || '预设保存成功') - } + const presets = customPresets.value[section.tabValue] + + if (presets[existingIndex]) { + // 更新现有预设的值,保持ID不变 + presets[existingIndex].values = values + + savePresets(section.tabValue, presets) + + presetNames.value[section.tabValue] = '' + selectedPresets.value[section.tabValue] = presets[existingIndex].id + + toast.success(t('gmView.presetSaved') || '预设保存成功') } presetOverwriteDialogOpen.value = false @@ -505,7 +505,7 @@ return } - const presets = customPresets.value[section.tabValue] || [] + const presets = customPresets.value[section.tabValue] const index = presets.findIndex(p => p.id === presetId) if (index !== -1) { @@ -586,14 +586,12 @@ } toast.success(t('gmView.presetApplied') || '默认预设应用成功') } else { - if (customPresets.value[section.tabValue]) { - const customPreset = customPresets.value[section.tabValue]!.find((p: GMPreset) => p.id === presetId) - if (customPreset) { - Object.entries(customPreset.values).forEach(([k, v]) => { - section.setValue(k, v as number) - }) - toast.success(t('gmView.presetApplied') || '预设应用成功') - } + const customPreset = customPresets.value[section.tabValue].find((p: GMPreset) => p.id === presetId) + if (customPreset) { + Object.entries(customPreset.values).forEach(([k, v]) => { + section.setValue(k, v as number) + }) + toast.success(t('gmView.presetApplied') || '预设应用成功') } } } From d8dd4e7317af1b1ab844b0bc5dd1c813c6ec13da Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 21:04:11 +0800 Subject: [PATCH 11/11] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=20generateId=20=E5=87=BD=E6=95=B0=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E6=A0=87=E8=AF=86=E7=AC=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 GMView、FleetView 中替换 Date.now() 生成 ID 的方式 - 在 DiplomacyView 中优化排序函数,避免重复过滤与排序 --- src/views/DiplomacyView.vue | 10 +++++----- src/views/FleetView.vue | 3 ++- src/views/GMView.vue | 3 ++- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/views/DiplomacyView.vue b/src/views/DiplomacyView.vue index 2e8c057..fb4dc33 100644 --- a/src/views/DiplomacyView.vue +++ b/src/views/DiplomacyView.vue @@ -427,7 +427,7 @@ 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 type { DiplomaticRelation, NPC } from '@/types/game' import * as npcBehaviorLogic from '@/logic/npcBehaviorLogic' import { Search, @@ -481,8 +481,8 @@ } // 排序函数 - const sortNpcs = (npcs: typeof npcStore.npcs) => { - return [...npcs].sort((a, b) => { + const sortNpcs = (npcs: NPC[], predicate: (npc: NPC) => boolean = () => true) => { + return npcs.filter(predicate).sort((a, b) => { let valA = 0 let valB = 0 @@ -707,7 +707,7 @@ } // 搜索过滤函数 - const matchesSearch = (npc: (typeof npcStore.npcs)[0]) => { + const matchesSearch = (npc: NPC) => { if (!searchQuery.value.trim()) return true const query = searchQuery.value.toLowerCase().trim() return npc.name.toLowerCase().includes(query) || npc.id.toLowerCase().includes(query) @@ -715,7 +715,7 @@ // 按关系状态分类NPC(同时应用搜索过滤) // 先统一排序一次,避免不同标签页在同一批数据上重复排序 - const sortedNpcs = computed(() => sortNpcs(npcStore.npcs.filter(matchesSearch))) + const sortedNpcs = computed(() => sortNpcs(npcStore.npcs, matchesSearch)) const allNpcs = computed(() => sortedNpcs.value) diff --git a/src/views/FleetView.vue b/src/views/FleetView.vue index b9c5dd5..ad20566 100644 --- a/src/views/FleetView.vue +++ b/src/views/FleetView.vue @@ -642,6 +642,7 @@ import * as diplomaticLogic from '@/logic/diplomaticLogic' import * as gameLogic from '@/logic/gameLogic' import * as moonLogic from '@/logic/moonLogic' + import { generateId } from '@/utils/id' const route = useRoute() const gameStore = useGameStore() @@ -934,7 +935,7 @@ // 生成唯一ID const generatePresetId = (): string => { - return `preset_${Date.now()}_${Math.random().toString(36).substr(2, 9)}` + return generateId('fleet_preset') } // 保存当前配置为预设 diff --git a/src/views/GMView.vue b/src/views/GMView.vue index 9e1a260..e738b3d 100644 --- a/src/views/GMView.vue +++ b/src/views/GMView.vue @@ -319,6 +319,7 @@ import * as publicLogic from '@/logic/publicLogic' import { calculateMaxFleetStorage } from '@/logic/fleetStorageLogic' import { calculateMissileSiloCapacity } from '@/logic/missileLogic' + import { generateId } from '@/utils/id' import { Home, Trash2 } from 'lucide-vue-next' // --- 预设系统 --- @@ -461,7 +462,7 @@ } const newPreset: GMPreset = { - id: Date.now().toString(), + id: generateId('gm_preset'), name, values }