refactor frontend (not done)

This commit is contained in:
bridge
2026-01-11 22:29:53 +08:00
parent 08e28f52c7
commit 879a3c0d1f
24 changed files with 759 additions and 519 deletions

View File

@@ -0,0 +1,160 @@
import { ref, watch, onMounted, type Ref } from 'vue'
import { llmApi } from '@/api'
import { useUiStore } from '@/stores/ui'
import { useSystemStore } from '@/stores/system'
import { message } from '@/utils/discreteApi'
import { storeToRefs } from 'pinia'
export function useGameControl(gameInitialized: Ref<boolean>) {
const uiStore = useUiStore()
const systemStore = useSystemStore()
const { isManualPaused } = storeToRefs(systemStore)
const showMenu = ref(false)
const menuDefaultTab = ref<'save' | 'load' | 'create' | 'delete' | 'llm' | 'start'>('load')
const canCloseMenu = ref(true)
// 监听菜单状态和手动暂停状态,控制游戏暂停/继续
watch([showMenu, isManualPaused], ([menuVisible, manualPaused]) => {
// 只在游戏已准备好时控制暂停
if (!gameInitialized.value) return
if (menuVisible || manualPaused) {
// 如果不是因为菜单打开而暂停(即用户手动暂停),则不需额外操作,因为状态已经在 store 里了
// 但如果是菜单打开导致需要暂停,我们需要调用 pause
if (menuVisible && !manualPaused) {
systemStore.pause().catch(console.error)
} else if (!menuVisible && !manualPaused) {
// 菜单关闭且非手动暂停 -> 恢复
systemStore.resume().catch(console.error)
} else if (manualPaused) {
// 只要是手动暂停,就确保是暂停状态
systemStore.pause().catch(console.error)
}
} else {
systemStore.resume().catch(console.error)
}
})
// 优化:简化 Watch 逻辑
// 核心规则:菜单打开 OR 手动暂停 => 必须暂停
// 菜单关闭 AND 手动播放 => 恢复
watch([showMenu], ([menuVisible]) => {
if (!gameInitialized.value) return
if (menuVisible) {
systemStore.pause().catch(console.error)
} else {
// 关闭菜单时,如果不是手动暂停状态,则恢复
if (!isManualPaused.value) {
systemStore.resume().catch(console.error)
}
}
})
// Watch 手动暂停状态的变化,同步到后端
watch(isManualPaused, (val) => {
if (!gameInitialized.value) return
if (val) systemStore.pause().catch(console.error)
else if (!showMenu.value) systemStore.resume().catch(console.error)
})
// 快捷键处理
function handleKeydown(e: KeyboardEvent) {
if (e.key === 'Escape') {
if (uiStore.selectedTarget) {
uiStore.clearSelection()
} else {
if (showMenu.value) {
// 如果菜单打开,尝试关闭(如果允许)
if (canCloseMenu.value) {
showMenu.value = false
}
} else {
// 打开菜单
showMenu.value = true
menuDefaultTab.value = 'load'
}
}
}
}
// LLM 相关控制逻辑
async function performStartupCheck() {
try {
const res = await llmApi.fetchStatus()
if (!res.configured) {
// 未配置 -> 强制进入 LLM 配置,禁止关闭
showMenu.value = true
menuDefaultTab.value = 'llm'
canCloseMenu.value = false
message.warning('检测到 LLM 未配置,请先完成设置')
} else {
// 已配置 -> 验证连通性
try {
const configRes = await llmApi.fetchConfig()
await llmApi.testConnection(configRes)
// 测试通过 -> 允许进入开始游戏
menuDefaultTab.value = 'start'
canCloseMenu.value = true
showMenu.value = true
} catch (connErr) {
// 连接失败 -> 强制进入配置
console.error('LLM Connection check failed:', connErr)
showMenu.value = true
menuDefaultTab.value = 'llm'
canCloseMenu.value = false
message.error('LLM 连接测试失败,请重新配置')
}
}
} catch (e) {
console.error('Failed to check LLM status:', e)
// Fallback
showMenu.value = true
menuDefaultTab.value = 'llm'
canCloseMenu.value = false
message.error('无法获取系统状态')
}
}
function handleLLMReady() {
canCloseMenu.value = true
menuDefaultTab.value = 'start'
message.success('LLM 配置成功,请开始游戏')
}
function handleMenuClose() {
if (canCloseMenu.value) {
showMenu.value = false
}
}
function toggleManualPause() {
systemStore.togglePause()
}
function openLLMConfig() {
menuDefaultTab.value = 'llm'
showMenu.value = true
}
onMounted(() => {
// 暴露给全局以便 socket store 可以调用
;(window as any).__openLLMConfig = openLLMConfig
})
return {
showMenu,
isManualPaused,
menuDefaultTab,
canCloseMenu,
handleKeydown,
performStartupCheck,
handleLLMReady,
handleMenuClose,
toggleManualPause,
openLLMConfig
}
}

View File

@@ -0,0 +1,109 @@
import { ref, onMounted, onUnmounted } from 'vue'
import { useSystemStore } from '@/stores/system'
import { useWorldStore } from '@/stores/world'
import { useSocketStore } from '@/stores/socket'
import { GAME_PHASES } from '@/constants/game'
import { storeToRefs } from 'pinia'
interface UseGameInitOptions {
onIdle?: () => void
}
export function useGameInit(options: UseGameInitOptions = {}) {
const systemStore = useSystemStore()
const worldStore = useWorldStore()
const socketStore = useSocketStore()
const { initStatus, isInitialized, isLoading } = storeToRefs(systemStore)
// 内部变量
const mapPreloaded = ref(false)
const avatarsPreloaded = ref(false)
let pollInterval: ReturnType<typeof setInterval> | null = null
// Methods
async function initializeGame() {
if (isInitialized.value) {
// 重新加载存档时,重新初始化
worldStore.reset()
}
// 初始化 Socket 连接
if (!socketStore.isConnected) {
socketStore.init()
}
// 初始化世界状态
await worldStore.initialize()
systemStore.setInitialized(true)
console.log('[GameInit] Game initialized.')
}
async function pollInitStatus() {
try {
const prevStatus = initStatus.value?.status
const res = await systemStore.fetchInitStatus()
if (!res) return
// Idle check
if (res.status === 'idle' && prevStatus !== 'idle') {
options.onIdle?.()
}
// 提前加载地图
if (!mapPreloaded.value && GAME_PHASES.MAP_READY.includes(res.phase_name as any)) {
mapPreloaded.value = true
worldStore.preloadMap()
}
// 提前加载角色
if (!avatarsPreloaded.value && GAME_PHASES.AVATAR_READY.includes(res.phase_name as any)) {
avatarsPreloaded.value = true
worldStore.preloadAvatars()
}
// 状态跃迁:非 Ready -> Ready
if (prevStatus !== 'ready' && res.status === 'ready') {
await initializeGame()
stopPolling()
}
} catch (e) {
console.error('Failed to fetch init status:', e)
}
}
function startPolling() {
pollInitStatus()
pollInterval = setInterval(pollInitStatus, 1000)
}
function stopPolling() {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
}
onMounted(() => {
startPolling()
})
onUnmounted(() => {
stopPolling()
socketStore.disconnect()
})
return {
initStatus,
gameInitialized: isInitialized, // Alias for compatibility
showLoading: isLoading,
mapPreloaded,
avatarsPreloaded,
initializeGame,
startPolling,
stopPolling
}
}