From e8590d54c7bcf6211a8bcd7cbd5753efe094050f Mon Sep 17 00:00:00 2001 From: StarsEnd Date: Thu, 18 Dec 2025 01:34:29 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=BA=86=E6=B5=8F?= =?UTF-8?q?=E8=A7=88=E5=99=A8=E9=80=9A=E7=9F=A5=E5=92=8C=E9=A1=B5=E9=9D=A2?= =?UTF-8?q?=E5=86=85=E9=80=9A=E7=9F=A5=20=E6=9A=82=E5=8C=85=E5=90=AB?= =?UTF-8?q?=E5=BB=BA=E9=80=A0=E5=AE=8C=E6=88=90=E5=92=8C=E7=A7=91=E7=A0=94?= =?UTF-8?q?=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 50 +++++++++- src/components/ui/switch/Switch.vue | 48 +++++++++ src/components/ui/switch/index.ts | 2 + src/locales/zh-CN.ts | 21 +++- src/logic/buildingLogic.ts | 7 +- src/logic/gameLogic.ts | 20 +++- src/logic/researchLogic.ts | 9 +- src/stores/gameStore.ts | 21 +++- src/types/game.ts | 13 +++ src/views/SettingsView.vue | 147 +++++++++++++++++++++++++++- 10 files changed, 327 insertions(+), 11 deletions(-) create mode 100644 src/components/ui/switch/Switch.vue create mode 100644 src/components/ui/switch/index.ts diff --git a/src/App.vue b/src/App.vue index bf59852..6fad2df 100644 --- a/src/App.vue +++ b/src/App.vue @@ -438,7 +438,7 @@ import UpdateDialog from '@/components/UpdateDialog.vue' import TutorialOverlay from '@/components/TutorialOverlay.vue' import Sonner from '@/components/ui/sonner/Sonner.vue' - import { MissionType, BuildingType, DiplomaticEventType } from '@/types/game' + import { MissionType, BuildingType, TechnologyType, DiplomaticEventType } from '@/types/game' import type { FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game' import { DIPLOMATIC_CONFIG } from '@/config/gameConfig' import type { VersionInfo } from '@/utils/versionCheck' @@ -493,7 +493,7 @@ const npcStore = useNPCStore() const { isDark } = useTheme() const { t } = useI18n() - const { BUILDINGS } = useGameConfig() + const { BUILDINGS, TECHNOLOGIES } = useGameConfig() const { startTutorial, tutorialState, currentStep } = useTutorial() // ConfirmDialog 状态 @@ -507,6 +507,50 @@ const showUpdateDialog = ref(false) const updateInfo = ref(null) + const handleNotification = (type: string, itemType: string, level?: number) => { + const settings = gameStore.player.notificationSettings + if (!settings) return + + // 检查主开关 + if (!settings.browser && !settings.inApp) return + + // 检查具体类型开关 + let typeKey = '' + let title = '' + let body = '' + + if (type === 'building') { + typeKey = 'construction' + const buildingType = itemType as BuildingType + const name = BUILDINGS.value[buildingType]?.name || itemType + title = t('notifications.constructionComplete') + body = `${name} Lv ${level}` + } else if (type === 'technology') { + typeKey = 'research' + const technologyType = itemType as TechnologyType + const name = TECHNOLOGIES.value[technologyType]?.name || itemType + title = t('notifications.researchComplete') + body = `${name} Lv ${level}` + } else { + return + } + + if (!settings.types[typeKey]) return + + // browser + if (settings.browser && 'Notification' in window && Notification.permission === 'granted') { + const shouldSuppress = settings.suppressInFocus && document.hasFocus() + if (!shouldSuppress) { + new Notification(title, { body, icon: '/favicon.ico' }) + } + } + + // toast + if (settings.inApp) { + toast.success(title, { description: body }) + } + } + const handleConfirmDialogConfirm = () => { if (confirmDialogAction.value) { confirmDialogAction.value() @@ -567,7 +611,7 @@ // 检查军官过期 gameLogic.checkOfficersExpiration(gameStore.player.officers, now) // 处理游戏更新(建造队列、研究队列等) - const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed) + const result = gameLogic.processGameUpdate(gameStore.player, now, gameStore.gameSpeed, handleNotification) gameStore.player.researchQueue = result.updatedResearchQueue // 处理舰队任务 gameStore.player.fleetMissions.forEach(mission => { diff --git a/src/components/ui/switch/Switch.vue b/src/components/ui/switch/Switch.vue new file mode 100644 index 0000000..8e6a20e --- /dev/null +++ b/src/components/ui/switch/Switch.vue @@ -0,0 +1,48 @@ + + + + diff --git a/src/components/ui/switch/index.ts b/src/components/ui/switch/index.ts new file mode 100644 index 0000000..872f5c3 --- /dev/null +++ b/src/components/ui/switch/index.ts @@ -0,0 +1,2 @@ +export { default as Switch } from './Switch.vue' + diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index c6447c8..9f229dc 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -779,7 +779,26 @@ export default { buildDate: '构建日期', community: '社区', github: 'GitHub 仓库', - qqGroup: 'QQ 交流群' + qqGroup: 'QQ 交流群', + notifications: '通知设置', + notificationsDesc: '管理游戏内的通知提醒', + notificationTypes: '通知类型', + browserNotifications: '浏览器通知', + inAppNotifications: '页面内通知', + constructionComplete: '建筑完成', + researchComplete: '研究完成', + browserPermission: '启用浏览器通知', + permissionGranted: '已获得权限', + permissionDenied: '权限被拒绝/未获得', + inAppNotificationsDesc: '通过页面弹窗显示', + notificationsDisabled: '启用上方任一开关以配置具体通知', + suppressInFocus: '页面聚焦时不发送浏览器通知', + expandTypes: '展开详细设置', + collapseTypes: '收起详细设置' + }, + notifications: { + constructionComplete: '建造完成', + researchComplete: '研究完成' }, gmView: { title: 'GM 管理面板', diff --git a/src/logic/buildingLogic.ts b/src/logic/buildingLogic.ts index 940bcde..9f079f8 100644 --- a/src/logic/buildingLogic.ts +++ b/src/logic/buildingLogic.ts @@ -122,7 +122,8 @@ export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: nu export const completeBuildQueue = ( planet: Planet, now: number, - onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void + onPointsEarned?: (points: number, type: 'building' | 'ship' | 'defense', itemType: string, level?: number, quantity?: number) => void, + onCompleted?: (type: 'building' | 'ship' | 'defense' | 'demolish', itemType: string, level?: number, quantity?: number) => void ): void => { planet.buildQueue = planet.buildQueue.filter(item => { if (now >= item.endTime) { @@ -137,6 +138,10 @@ export const completeBuildQueue = ( const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel) onPointsEarned(points, 'building', item.itemType, newLevel) } + + if (onCompleted) { + onCompleted('building', item.itemType, newLevel) + } } else if (item.type === 'ship') { const shipType = item.itemType as ShipType const quantity = item.quantity || 0 diff --git a/src/logic/gameLogic.ts b/src/logic/gameLogic.ts index 1cb7c98..752634a 100644 --- a/src/logic/gameLogic.ts +++ b/src/logic/gameLogic.ts @@ -101,7 +101,8 @@ export const generatePositionKey = (galaxy: number, system: number, position: nu export const processGameUpdate = ( player: Player, now: number, - gameSpeed: number = 1 + gameSpeed: number = 1, + onNotification?: (type: string, itemType: string, level?: number) => void ): { updatedResearchQueue: BuildQueueItem[] } => { @@ -113,6 +114,13 @@ export const processGameUpdate = ( pointsLogic.addPoints(player, points) } + // 通知回调 + const onCompleted = (type: string, itemType: string, level?: number, _quantity?: number) => { + if (onNotification) { + onNotification(type, itemType, level) + } + } + // 更新所有星球资源(直接同步计算,避免 Worker 通信开销) player.planets.forEach(planet => { resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed) @@ -121,7 +129,7 @@ export const processGameUpdate = ( // 更新所有星球其他状态 player.planets.forEach(planet => { // 检查建造队列 - buildingLogic.completeBuildQueue(planet, now, onPointsEarned) + buildingLogic.completeBuildQueue(planet, now, onPointsEarned, onCompleted) // 更新星球最大空间 if (planet.isMoon) { @@ -133,7 +141,13 @@ export const processGameUpdate = ( }) // 检查研究队列 - const updatedResearchQueue = researchLogic.completeResearchQueue(player.researchQueue, player.technologies, now, onPointsEarned) + const updatedResearchQueue = researchLogic.completeResearchQueue( + player.researchQueue, + player.technologies, + now, + onPointsEarned, + onCompleted + ) return { updatedResearchQueue diff --git a/src/logic/researchLogic.ts b/src/logic/researchLogic.ts index 9144fc4..8c35a23 100644 --- a/src/logic/researchLogic.ts +++ b/src/logic/researchLogic.ts @@ -98,7 +98,8 @@ export const completeResearchQueue = ( researchQueue: BuildQueueItem[], technologies: Partial>, now: number, - onPointsEarned?: (points: number, type: 'technology', itemType: string, level: number) => void + onPointsEarned?: (points: number, type: 'technology', itemType: string, level: number) => void, + onCompleted?: (type: 'technology', itemType: string, level: number) => void ): BuildQueueItem[] => { return researchQueue.filter(item => { if (now >= item.endTime) { @@ -112,6 +113,12 @@ export const completeResearchQueue = ( const points = pointsLogic.calculateTechnologyPoints(item.itemType as TechnologyType, oldLevel, newLevel) onPointsEarned(points, 'technology', item.itemType, newLevel) } + + // 通知完成 + if (onCompleted) { + onCompleted('technology', item.itemType, newLevel) + } + return false } return true diff --git a/src/stores/gameStore.ts b/src/stores/gameStore.ts index a5f7bb7..4e0cba9 100644 --- a/src/stores/gameStore.ts +++ b/src/stores/gameStore.ts @@ -41,12 +41,31 @@ export const useGameStore = defineStore('game', { giftRejectedNotifications: [], points: 0, isGMEnabled: false, // 明确设置 GM 模式默认为 false - lastVersionCheckTime: 0 // 最后一次检查版本的时间戳,默认为0 + lastVersionCheckTime: 0, // 最后一次检查版本的时间戳,默认为0 + notificationSettings: { + browser: false, + inApp: true, + suppressInFocus: false, + types: { + construction: true, + research: true + } + } } as Player, currentPlanetId: '', isDark: '', locale: 'zh-CN' as Locale }), + actions: { + async requestBrowserPermission(): Promise { + if (!('Notification' in window)) return false + + if (Notification.permission === 'granted') return true + + const permission = await Notification.requestPermission() + return permission === 'granted' + } + }, getters: { currentPlanet(): Planet | undefined { return this.player.planets.find(p => p.id === this.currentPlanetId) diff --git a/src/types/game.ts b/src/types/game.ts index ac7b331..88df4a2 100644 --- a/src/types/game.ts +++ b/src/types/game.ts @@ -574,6 +574,19 @@ export interface Player { diplomaticReports?: DiplomaticReport[] // 外交变化报告 // 新手引导字段 tutorialProgress?: TutorialProgress // 新手引导进度 + // 通知设置 + notificationSettings?: NotificationSettings +} + +export interface NotificationSettings { + browser: boolean + inApp: boolean + suppressInFocus: boolean // 当页面聚焦时是否浏览器通知 + types: { + construction: boolean + research: boolean + [key: string]: boolean + } } // 游戏状态 diff --git a/src/views/SettingsView.vue b/src/views/SettingsView.vue index e7a27cf..42e0008 100644 --- a/src/views/SettingsView.vue +++ b/src/views/SettingsView.vue @@ -89,6 +89,85 @@ + + + + {{ t('settings.notifications') }} + {{ t('settings.notificationsDesc') }} + + + +
+
+
+

{{ t('settings.browserNotifications') }}

+

{{ t('settings.browserPermission') }}

+
+ +
+ + +
+ + +
+
+ + +
+
+

{{ t('settings.inAppNotifications') }}

+

{{ t('settings.inAppNotificationsDesc') || t('settings.inAppNotifications') }}

+
+ +
+ + +
+
+
+

{{ t('settings.notificationTypes') }}

+

+ {{ areMainSwitchesOff ? t('settings.notificationsDisabled') : (isTypesExpanded ? t('settings.collapseTypes') : t('settings.expandTypes')) }} +

+
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+
+
+
+ @@ -164,6 +243,8 @@ import { useI18n } from '@/composables/useI18n' import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card' import { Button } from '@/components/ui/button' + import { Switch } from '@/components/ui/switch' + import { Label } from '@/components/ui/label' import { AlertDialog, AlertDialogAction, @@ -174,7 +255,7 @@ AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog' - import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw } from 'lucide-vue-next' + import { Download, Upload, Trash2, ExternalLink, MessagesSquare, Play, Pause, RefreshCw, ChevronDown, ChevronUp } from 'lucide-vue-next' import { saveAs } from 'file-saver' import { toast } from 'vue-sonner' import pkg from '../../package.json' @@ -195,6 +276,70 @@ const confirmMessage = ref('') let confirmCallback: (() => void) | null = null + const isTypesExpanded = ref(false) + + // Ensure notification settings exist + if (!gameStore.player.notificationSettings) { + gameStore.player.notificationSettings = { + browser: false, + inApp: true, + suppressInFocus: false, + types: { construction: true, research: true } + } + } + + const areMainSwitchesOff = computed(() => { + const s = gameStore.player.notificationSettings + return !s?.browser && !s?.inApp + }) + + // Auto-collapse if main switches are off + // watch(areMainSwitchesOff, (val) => { + // if (val) isTypesExpanded.value = false + // }) + + const updateInAppSetting = (val: boolean) => { + if (gameStore.player.notificationSettings) { + gameStore.player.notificationSettings.inApp = val + } + } + + const updateSuppressSetting = (val: boolean) => { + if (gameStore.player.notificationSettings) { + gameStore.player.notificationSettings.suppressInFocus = val + } + } + + const updateTypeSetting = (key: string, val: boolean) => { + if (gameStore.player.notificationSettings) { + gameStore.player.notificationSettings.types[key] = val + } + } + + const toggleType = (key: string) => { + if (gameStore.player.notificationSettings) { + const current = gameStore.player.notificationSettings.types[key] + gameStore.player.notificationSettings.types[key] = !current + } + } + + const handleBrowserSwitch = async (checked: boolean) => { + if (!gameStore.player.notificationSettings) return + + if (checked) { + const granted = await gameStore.requestBrowserPermission() + if (granted) { + gameStore.player.notificationSettings.browser = true + toast.success(t('settings.permissionGranted')) + } else { + gameStore.player.notificationSettings.browser = false + toast.error(t('settings.permissionDenied')) + } + } else { + gameStore.player.notificationSettings.browser = false + } + } + // 计算是否可以检查版本(主动检测:5分钟内不能重复检查) const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))