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

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