From 15eccd8f0d9abb1c194db517fb424a42f9e72bfa Mon Sep 17 00:00:00 2001 From: wenyu Date: Wed, 18 Mar 2026 20:47:14 +0800 Subject: [PATCH] =?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') || '预设应用成功') } } }