mirror of
https://github.com/setube/ogame-vue-ts.git
synced 2026-05-12 07:55:11 +08:00
Merge pull request #15 from StarsEnd33A2D17/notification
feat: 添加了浏览器通知和页面内通知
This commit is contained in:
50
src/App.vue
50
src/App.vue
@@ -424,7 +424,7 @@
|
|||||||
import UpdateDialog from '@/components/UpdateDialog.vue'
|
import UpdateDialog from '@/components/UpdateDialog.vue'
|
||||||
import TutorialOverlay from '@/components/TutorialOverlay.vue'
|
import TutorialOverlay from '@/components/TutorialOverlay.vue'
|
||||||
import Sonner from '@/components/ui/sonner/Sonner.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 type { FleetMission, NPC, IncomingFleetAlert, MissileAttack } from '@/types/game'
|
||||||
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
import { DIPLOMATIC_CONFIG } from '@/config/gameConfig'
|
||||||
import type { VersionInfo } from '@/utils/versionCheck'
|
import type { VersionInfo } from '@/utils/versionCheck'
|
||||||
@@ -480,7 +480,7 @@
|
|||||||
const npcStore = useNPCStore()
|
const npcStore = useNPCStore()
|
||||||
const { isDark } = useTheme()
|
const { isDark } = useTheme()
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
const { BUILDINGS } = useGameConfig()
|
const { BUILDINGS, TECHNOLOGIES } = useGameConfig()
|
||||||
const { startTutorial, tutorialState, currentStep } = useTutorial()
|
const { startTutorial, tutorialState, currentStep } = useTutorial()
|
||||||
|
|
||||||
// ConfirmDialog 状态
|
// ConfirmDialog 状态
|
||||||
@@ -494,6 +494,50 @@
|
|||||||
const showUpdateDialog = ref(false)
|
const showUpdateDialog = ref(false)
|
||||||
const updateInfo = ref<VersionInfo | null>(null)
|
const updateInfo = ref<VersionInfo | null>(null)
|
||||||
|
|
||||||
|
const handleNotification = (type: string, itemType: string, level?: number) => {
|
||||||
|
const settings = gameStore.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 = () => {
|
const handleConfirmDialogConfirm = () => {
|
||||||
if (confirmDialogAction.value) {
|
if (confirmDialogAction.value) {
|
||||||
confirmDialogAction.value()
|
confirmDialogAction.value()
|
||||||
@@ -560,7 +604,7 @@
|
|||||||
// 检查军官过期
|
// 检查军官过期
|
||||||
gameLogic.checkOfficersExpiration(gameStore.player.officers, now)
|
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.researchQueue = result.updatedResearchQueue
|
||||||
// 处理舰队任务
|
// 处理舰队任务
|
||||||
gameStore.player.fleetMissions.forEach(mission => {
|
gameStore.player.fleetMissions.forEach(mission => {
|
||||||
|
|||||||
48
src/components/ui/switch/Switch.vue
Normal file
48
src/components/ui/switch/Switch.vue
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<template>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
:aria-checked="checked"
|
||||||
|
:data-state="checked ? 'checked' : 'unchecked'"
|
||||||
|
:disabled="disabled"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50',
|
||||||
|
checked ? 'bg-primary' : 'bg-input',
|
||||||
|
props.class
|
||||||
|
)
|
||||||
|
"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:data-state="checked ? 'checked' : 'unchecked'"
|
||||||
|
:class="
|
||||||
|
cn(
|
||||||
|
'pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform',
|
||||||
|
checked ? 'translate-x-5' : 'translate-x-0'
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { type HTMLAttributes } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
checked?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'update:checked', value: boolean): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const toggle = () => {
|
||||||
|
if (props.disabled) return
|
||||||
|
emit('update:checked', !props.checked)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
2
src/components/ui/switch/index.ts
Normal file
2
src/components/ui/switch/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as Switch } from './Switch.vue'
|
||||||
|
|
||||||
@@ -795,7 +795,26 @@ export default {
|
|||||||
buildDate: 'Build-Datum',
|
buildDate: 'Build-Datum',
|
||||||
community: 'Community',
|
community: 'Community',
|
||||||
github: 'GitHub-Repository',
|
github: 'GitHub-Repository',
|
||||||
qqGroup: 'QQ-Gruppe'
|
qqGroup: 'QQ-Gruppe',
|
||||||
|
notifications: 'Benachrichtigungseinstellungen',
|
||||||
|
notificationsDesc: 'Verwalten Sie Benachrichtigungen im Spiel',
|
||||||
|
notificationTypes: 'Benachrichtigungstypen',
|
||||||
|
browserNotifications: 'Browser-Benachrichtigungen',
|
||||||
|
inAppNotifications: 'In-App-Benachrichtigungen',
|
||||||
|
constructionComplete: 'Bau abgeschlossen',
|
||||||
|
researchComplete: 'Forschung abgeschlossen',
|
||||||
|
browserPermission: 'Browser-Benachrichtigungen aktivieren',
|
||||||
|
permissionGranted: 'Erlaubnis erteilt',
|
||||||
|
permissionDenied: 'Erlaubnis verweigert/nicht erteilt',
|
||||||
|
inAppNotificationsDesc: 'Über In-Page-Popups anzeigen',
|
||||||
|
notificationsDisabled: 'Aktivieren Sie einen der obigen Schalter, um spezifische Benachrichtigungen zu konfigurieren',
|
||||||
|
suppressInFocus: 'Browser-Benachrichtigungen unterdrücken, wenn Seite fokussiert ist',
|
||||||
|
expandTypes: 'Details anzeigen',
|
||||||
|
collapseTypes: 'Details ausblenden'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: 'Bau abgeschlossen',
|
||||||
|
researchComplete: 'Forschung abgeschlossen'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM-Kontrollpanel',
|
title: 'GM-Kontrollpanel',
|
||||||
|
|||||||
@@ -788,7 +788,26 @@ export default {
|
|||||||
buildDate: 'Build Date',
|
buildDate: 'Build Date',
|
||||||
community: 'Community',
|
community: 'Community',
|
||||||
github: 'GitHub Repository',
|
github: 'GitHub Repository',
|
||||||
qqGroup: 'QQ Group'
|
qqGroup: 'QQ Group',
|
||||||
|
notifications: 'Notification Settings',
|
||||||
|
notificationsDesc: 'Manage in-game notification alerts',
|
||||||
|
notificationTypes: 'Notification Types',
|
||||||
|
browserNotifications: 'Browser Notifications',
|
||||||
|
inAppNotifications: 'In-App Notifications',
|
||||||
|
constructionComplete: 'Construction Complete',
|
||||||
|
researchComplete: 'Research Complete',
|
||||||
|
browserPermission: 'Enable Browser Notifications',
|
||||||
|
permissionGranted: 'Permission Granted',
|
||||||
|
permissionDenied: 'Permission Denied/Not Granted',
|
||||||
|
inAppNotificationsDesc: 'Show via in-page popups',
|
||||||
|
notificationsDisabled: 'Enable any switch above to configure specific notifications',
|
||||||
|
suppressInFocus: 'Suppress browser notifications when page is focused',
|
||||||
|
expandTypes: 'Expand Details',
|
||||||
|
collapseTypes: 'Collapse Details'
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
constructionComplete: 'Construction Complete',
|
||||||
|
researchComplete: 'Research Complete'
|
||||||
},
|
},
|
||||||
gmView: {
|
gmView: {
|
||||||
title: 'GM Control Panel',
|
title: 'GM Control Panel',
|
||||||
|
|||||||
@@ -786,7 +786,26 @@ export default {
|
|||||||
buildDate: 'ビルド日',
|
buildDate: 'ビルド日',
|
||||||
community: 'コミュニティ',
|
community: 'コミュニティ',
|
||||||
github: 'GitHubリポジトリ',
|
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: {
|
gmView: {
|
||||||
title: 'GMコントロールパネル',
|
title: 'GMコントロールパネル',
|
||||||
|
|||||||
@@ -787,7 +787,26 @@ export default {
|
|||||||
buildDate: '빌드 날짜',
|
buildDate: '빌드 날짜',
|
||||||
community: '커뮤니티',
|
community: '커뮤니티',
|
||||||
github: 'GitHub 저장소',
|
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: {
|
gmView: {
|
||||||
title: 'GM 제어판',
|
title: 'GM 제어판',
|
||||||
|
|||||||
@@ -794,7 +794,26 @@ export default {
|
|||||||
buildDate: 'Дата сборки',
|
buildDate: 'Дата сборки',
|
||||||
community: 'Сообщество',
|
community: 'Сообщество',
|
||||||
github: 'Репозиторий GitHub',
|
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: {
|
gmView: {
|
||||||
title: 'Панель управления GM',
|
title: 'Панель управления GM',
|
||||||
|
|||||||
@@ -782,7 +782,26 @@ export default {
|
|||||||
buildDate: '构建日期',
|
buildDate: '构建日期',
|
||||||
community: '社区',
|
community: '社区',
|
||||||
github: 'GitHub 仓库',
|
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: {
|
gmView: {
|
||||||
title: 'GM 管理面板',
|
title: 'GM 管理面板',
|
||||||
|
|||||||
@@ -788,7 +788,26 @@ export default {
|
|||||||
buildDate: '建置日期',
|
buildDate: '建置日期',
|
||||||
community: '社群',
|
community: '社群',
|
||||||
github: 'GitHub 儲存庫',
|
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: {
|
gmView: {
|
||||||
title: 'GM 管理面板',
|
title: 'GM 管理面板',
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export const createBuildQueueItem = (buildingType: BuildingType, targetLevel: nu
|
|||||||
export const completeBuildQueue = (
|
export const completeBuildQueue = (
|
||||||
planet: Planet,
|
planet: Planet,
|
||||||
now: number,
|
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 => {
|
): void => {
|
||||||
planet.buildQueue = planet.buildQueue.filter(item => {
|
planet.buildQueue = planet.buildQueue.filter(item => {
|
||||||
if (now >= item.endTime) {
|
if (now >= item.endTime) {
|
||||||
@@ -137,6 +138,10 @@ export const completeBuildQueue = (
|
|||||||
const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel)
|
const points = pointsLogic.calculateBuildingPoints(item.itemType as BuildingType, oldLevel, newLevel)
|
||||||
onPointsEarned(points, 'building', item.itemType, newLevel)
|
onPointsEarned(points, 'building', item.itemType, newLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (onCompleted) {
|
||||||
|
onCompleted('building', item.itemType, newLevel)
|
||||||
|
}
|
||||||
} else if (item.type === 'ship') {
|
} else if (item.type === 'ship') {
|
||||||
const shipType = item.itemType as ShipType
|
const shipType = item.itemType as ShipType
|
||||||
const quantity = item.quantity || 0
|
const quantity = item.quantity || 0
|
||||||
|
|||||||
@@ -101,7 +101,8 @@ export const generatePositionKey = (galaxy: number, system: number, position: nu
|
|||||||
export const processGameUpdate = (
|
export const processGameUpdate = (
|
||||||
player: Player,
|
player: Player,
|
||||||
now: number,
|
now: number,
|
||||||
gameSpeed: number = 1
|
gameSpeed: number = 1,
|
||||||
|
onNotification?: (type: string, itemType: string, level?: number) => void
|
||||||
): {
|
): {
|
||||||
updatedResearchQueue: BuildQueueItem[]
|
updatedResearchQueue: BuildQueueItem[]
|
||||||
} => {
|
} => {
|
||||||
@@ -113,6 +114,13 @@ export const processGameUpdate = (
|
|||||||
pointsLogic.addPoints(player, points)
|
pointsLogic.addPoints(player, points)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知回调
|
||||||
|
const onCompleted = (type: string, itemType: string, level?: number, _quantity?: number) => {
|
||||||
|
if (onNotification) {
|
||||||
|
onNotification(type, itemType, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
|
// 更新所有星球资源(直接同步计算,避免 Worker 通信开销)
|
||||||
player.planets.forEach(planet => {
|
player.planets.forEach(planet => {
|
||||||
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed)
|
resourceLogic.updatePlanetResources(planet, now, bonuses, gameSpeed)
|
||||||
@@ -121,7 +129,7 @@ export const processGameUpdate = (
|
|||||||
// 更新所有星球其他状态
|
// 更新所有星球其他状态
|
||||||
player.planets.forEach(planet => {
|
player.planets.forEach(planet => {
|
||||||
// 检查建造队列
|
// 检查建造队列
|
||||||
buildingLogic.completeBuildQueue(planet, now, onPointsEarned)
|
buildingLogic.completeBuildQueue(planet, now, onPointsEarned, onCompleted)
|
||||||
|
|
||||||
// 更新星球最大空间
|
// 更新星球最大空间
|
||||||
if (planet.isMoon) {
|
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 {
|
return {
|
||||||
updatedResearchQueue
|
updatedResearchQueue
|
||||||
|
|||||||
@@ -98,7 +98,8 @@ export const completeResearchQueue = (
|
|||||||
researchQueue: BuildQueueItem[],
|
researchQueue: BuildQueueItem[],
|
||||||
technologies: Partial<Record<TechnologyType, number>>,
|
technologies: Partial<Record<TechnologyType, number>>,
|
||||||
now: number,
|
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[] => {
|
): BuildQueueItem[] => {
|
||||||
return researchQueue.filter(item => {
|
return researchQueue.filter(item => {
|
||||||
if (now >= item.endTime) {
|
if (now >= item.endTime) {
|
||||||
@@ -112,6 +113,12 @@ export const completeResearchQueue = (
|
|||||||
const points = pointsLogic.calculateTechnologyPoints(item.itemType as TechnologyType, oldLevel, newLevel)
|
const points = pointsLogic.calculateTechnologyPoints(item.itemType as TechnologyType, oldLevel, newLevel)
|
||||||
onPointsEarned(points, 'technology', item.itemType, newLevel)
|
onPointsEarned(points, 'technology', item.itemType, newLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 通知完成
|
||||||
|
if (onCompleted) {
|
||||||
|
onCompleted('technology', item.itemType, newLevel)
|
||||||
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -45,8 +45,27 @@ export const useGameStore = defineStore('game', {
|
|||||||
} as Player,
|
} as Player,
|
||||||
currentPlanetId: '',
|
currentPlanetId: '',
|
||||||
isDark: '',
|
isDark: '',
|
||||||
locale: 'zh-CN' as Locale
|
locale: 'zh-CN' as Locale,
|
||||||
|
notificationSettings: {
|
||||||
|
browser: false,
|
||||||
|
inApp: true,
|
||||||
|
suppressInFocus: false,
|
||||||
|
types: {
|
||||||
|
construction: true,
|
||||||
|
research: true
|
||||||
|
}
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
|
actions: {
|
||||||
|
async requestBrowserPermission(): Promise<boolean> {
|
||||||
|
if (!('Notification' in window)) return false
|
||||||
|
|
||||||
|
if (Notification.permission === 'granted') return true
|
||||||
|
|
||||||
|
const permission = await Notification.requestPermission()
|
||||||
|
return permission === 'granted'
|
||||||
|
}
|
||||||
|
},
|
||||||
getters: {
|
getters: {
|
||||||
currentPlanet(): Planet | undefined {
|
currentPlanet(): Planet | undefined {
|
||||||
return this.player.planets.find(p => p.id === this.currentPlanetId)
|
return this.player.planets.find(p => p.id === this.currentPlanetId)
|
||||||
|
|||||||
@@ -576,6 +576,17 @@ export interface Player {
|
|||||||
tutorialProgress?: TutorialProgress // 新手引导进度
|
tutorialProgress?: TutorialProgress // 新手引导进度
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NotificationSettings {
|
||||||
|
browser: boolean
|
||||||
|
inApp: boolean
|
||||||
|
suppressInFocus: boolean // 当页面聚焦时是否浏览器通知
|
||||||
|
types: {
|
||||||
|
construction: boolean
|
||||||
|
research: boolean
|
||||||
|
[key: string]: boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 游戏状态
|
// 游戏状态
|
||||||
export interface GameState {
|
export interface GameState {
|
||||||
player: Player
|
player: Player
|
||||||
|
|||||||
@@ -89,6 +89,85 @@
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<!-- 通知设置 -->
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{{ t('settings.notifications') }}</CardTitle>
|
||||||
|
<CardDescription>{{ t('settings.notificationsDesc') }}</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent class="space-y-4">
|
||||||
|
<!-- 浏览器通知 -->
|
||||||
|
<div class="flex flex-col gap-4 p-4 border rounded-lg">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.browserNotifications') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('settings.browserPermission') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch :checked="gameStore.notificationSettings?.browser" @update:checked="handleBrowserSwitch" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面聚焦时不发送 -->
|
||||||
|
<div class="flex items-center justify-between pl-4 border-l-2" :class="{ 'opacity-50 pointer-events-none': !gameStore.notificationSettings?.browser }">
|
||||||
|
<Label class="font-normal">{{ t('settings.suppressInFocus') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.suppressInFocus"
|
||||||
|
@update:checked="updateSuppressSetting"
|
||||||
|
:disabled="!gameStore.notificationSettings?.browser"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 页面内通知 -->
|
||||||
|
<div class="flex items-center justify-between p-4 border rounded-lg">
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.inAppNotifications') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">{{ t('settings.inAppNotificationsDesc') || t('settings.inAppNotifications') }}</p>
|
||||||
|
</div>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.inApp"
|
||||||
|
@update:checked="(val: boolean) => updateInAppSetting(val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 具体通知类型 -->
|
||||||
|
<div class="border rounded-lg overflow-hidden" :class="{ 'opacity-50 pointer-events-none': areMainSwitchesOff }">
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between p-4 bg-muted/50 cursor-pointer select-none"
|
||||||
|
@click="!areMainSwitchesOff && (isTypesExpanded = !isTypesExpanded)"
|
||||||
|
>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<h3 class="font-medium">{{ t('settings.notificationTypes') }}</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">
|
||||||
|
{{ areMainSwitchesOff ? t('settings.notificationsDisabled') : (isTypesExpanded ? t('settings.collapseTypes') : t('settings.expandTypes')) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button variant="ghost" size="sm" class="h-8 w-8 p-0">
|
||||||
|
<component :is="isTypesExpanded ? ChevronUp : ChevronDown" class="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="isTypesExpanded && !areMainSwitchesOff" class="p-4 space-y-4 border-t bg-card">
|
||||||
|
<!-- 建造完成 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class="font-normal cursor-pointer" @click="toggleType('construction')">{{ t('settings.constructionComplete') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.types.construction"
|
||||||
|
@update:checked="(val: boolean) => updateTypeSetting('construction', val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- 研究完成 -->
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<Label class="font-normal cursor-pointer" @click="toggleType('research')">{{ t('settings.researchComplete') }}</Label>
|
||||||
|
<Switch
|
||||||
|
:checked="gameStore.notificationSettings?.types.research"
|
||||||
|
@update:checked="(val: boolean) => updateTypeSetting('research', val)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
<!-- 关于 -->
|
<!-- 关于 -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -164,6 +243,8 @@
|
|||||||
import { useI18n } from '@/composables/useI18n'
|
import { useI18n } from '@/composables/useI18n'
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Switch } from '@/components/ui/switch'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@@ -174,7 +255,7 @@
|
|||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle
|
AlertDialogTitle
|
||||||
} from '@/components/ui/alert-dialog'
|
} 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 { saveAs } from 'file-saver'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import pkg from '../../package.json'
|
import pkg from '../../package.json'
|
||||||
@@ -195,6 +276,70 @@
|
|||||||
const confirmMessage = ref('')
|
const confirmMessage = ref('')
|
||||||
let confirmCallback: (() => void) | null = null
|
let confirmCallback: (() => void) | null = null
|
||||||
|
|
||||||
|
const isTypesExpanded = ref(false)
|
||||||
|
|
||||||
|
// Ensure notification settings exist
|
||||||
|
if (!gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings = {
|
||||||
|
browser: false,
|
||||||
|
inApp: true,
|
||||||
|
suppressInFocus: false,
|
||||||
|
types: { construction: true, research: true }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const areMainSwitchesOff = computed(() => {
|
||||||
|
const s = gameStore.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.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.inApp = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateSuppressSetting = (val: boolean) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.suppressInFocus = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTypeSetting = (key: string, val: boolean) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
gameStore.notificationSettings.types[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleType = (key: string) => {
|
||||||
|
if (gameStore.notificationSettings) {
|
||||||
|
const current = gameStore.notificationSettings.types[key]
|
||||||
|
gameStore.notificationSettings.types[key] = !current
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleBrowserSwitch = async (checked: boolean) => {
|
||||||
|
if (!gameStore.notificationSettings) return
|
||||||
|
|
||||||
|
if (checked) {
|
||||||
|
const granted = await gameStore.requestBrowserPermission()
|
||||||
|
if (granted) {
|
||||||
|
gameStore.notificationSettings.browser = true
|
||||||
|
toast.success(t('settings.permissionGranted'))
|
||||||
|
} else {
|
||||||
|
gameStore.notificationSettings.browser = false
|
||||||
|
toast.error(t('settings.permissionDenied'))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gameStore.notificationSettings.browser = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 计算是否可以检查版本(主动检测:5分钟内不能重复检查)
|
// 计算是否可以检查版本(主动检测:5分钟内不能重复检查)
|
||||||
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
|
const canCheck = computed(() => canCheckVersion(gameStore.player.lastManualUpdateCheck || 0))
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user