feat(本地化与工具): 为排序功能添加升序/降序翻译并统一 ID 生成

- 为所有支持的语言添加排序功能的 "ascending" 和 "descending" 翻译
- 创建统一的 ID 生成工具函数 generateId,用于生成带前缀的业务 ID
- 重构多个逻辑模块(battleLogic、moonLogic 等)使用新的 ID 生成函数
- 改进 GM 视图的类型安全,添加预设数据验证和类型保护
This commit is contained in:
wenyu
2026-03-18 19:29:08 +08:00
parent 28c3da2582
commit a8ab2b0f1a
16 changed files with 165 additions and 86 deletions

View File

@@ -1201,6 +1201,8 @@ export default {
diplomacy: {
sort: {
label: 'Sortieren',
ascending: 'Aufsteigend',
descending: 'Absteigend',
reputation: 'Ruf',
planets: 'Planeten',
difficulty: 'Schwierigkeit',

View File

@@ -1307,6 +1307,8 @@ export default {
searchPlaceholder: 'Search NPC name...',
sort: {
label: 'Sort',
ascending: 'Ascending',
descending: 'Descending',
reputation: 'Reputation',
planets: 'Planets',
difficulty: 'Difficulty',

View File

@@ -1209,6 +1209,8 @@ export default {
diplomacy: {
sort: {
label: 'Ordenar',
ascending: 'Ascendente',
descending: 'Descendente',
reputation: 'Reputación',
planets: 'Planetas',
difficulty: 'Dificultad',

View File

@@ -1226,6 +1226,8 @@ export default {
diplomacy: {
sort: {
label: '並び替え',
ascending: '昇順',
descending: '降順',
reputation: '評判',
planets: '惑星',
difficulty: '難易度',

View File

@@ -1176,6 +1176,8 @@ export default {
diplomacy: {
sort: {
label: '정렬',
ascending: '오름차순',
descending: '내림차순',
reputation: '평판',
planets: '행성',
difficulty: '난이도',

View File

@@ -1202,6 +1202,8 @@ export default {
diplomacy: {
sort: {
label: 'Сортировка',
ascending: 'По возрастанию',
descending: 'По убыванию',
reputation: 'Репутация',
planets: 'Планеты',
difficulty: 'Сложность',

View File

@@ -1275,6 +1275,8 @@ export default {
searchPlaceholder: '搜索NPC名称...',
sort: {
label: '排序',
ascending: '升序',
descending: '降序',
reputation: '好感度',
planets: '星球数量',
difficulty: '难度',

View File

@@ -1195,6 +1195,8 @@ export default {
diplomacy: {
sort: {
label: '排序',
ascending: '升序',
descending: '降序',
reputation: '聲望',
planets: '星球',
difficulty: '難度',

View File

@@ -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: '',

View File

@@ -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 || '',

View File

@@ -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

View File

@@ -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,

View File

@@ -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}`,

7
src/utils/id.ts Normal file
View File

@@ -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)}`
}

View File

@@ -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')"
>
<ArrowUpDown class="h-4 w-4" />
</Button>

View File

@@ -80,7 +80,7 @@
<!-- 建筑/科技/舰船/防御/军官 - 统一配置渲染 -->
<TabsContent v-for="section in gmSections" :key="section.tabValue" :value="section.tabValue" class="space-y-4">
<!-- 预设操作区 -->
<Card v-if="section.tabValue !== 'officers'" class="mb-4">
<Card v-if="isPresettableSection(section)" class="mb-4">
<CardHeader class="pb-3">
<CardTitle class="text-lg">{{ t('gmView.presets') || 'Presets' }}</CardTitle>
</CardHeader>
@@ -327,20 +327,81 @@
values: Record<string, number>
}
const presetOverwriteDialogOpen = ref(false)
const pendingPresetToOverwrite = ref<{
section: any
type GMSectionTabValue = 'buildings' | 'research' | 'ships' | 'defense' | 'officers'
type GMPresetSectionKey = Exclude<GMSectionTabValue, 'officers'>
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<string, number>
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<GMPreset>
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<PendingPresetOverwrite | null>(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<GMSection[]>(() => [
{
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
}
}
}