保存
This commit is contained in:
@@ -13,12 +13,22 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全局窗口管理器,用于管理所有内置应用窗口 -->
|
||||
<WindowManager ref="windowManagerRef" />
|
||||
</n-config-provider>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { configProviderProps } from '@/core/common/naive-ui/theme.ts'
|
||||
import { ref, provide } from 'vue'
|
||||
import { configProviderProps } from '@/common/naive-ui/theme.ts'
|
||||
import DesktopContainer from '@/ui/desktop-container/DesktopContainer.vue'
|
||||
import WindowManager from '@/ui/components/WindowManager.vue'
|
||||
|
||||
const windowManagerRef = ref<InstanceType<typeof WindowManager>>()
|
||||
|
||||
// 提供窗口管理器给子组件使用
|
||||
provide('windowManager', windowManagerRef)
|
||||
|
||||
const onContextMenu = (e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
|
||||
289
src/ui/components/AppRenderer.vue
Normal file
289
src/ui/components/AppRenderer.vue
Normal file
@@ -0,0 +1,289 @@
|
||||
<template>
|
||||
<div class="app-renderer" :class="`app-${appId}`">
|
||||
<!-- 内置Vue应用 - 立即渲染,无加载状态 -->
|
||||
<component
|
||||
v-if="isBuiltInApp && appComponent"
|
||||
:is="appComponent"
|
||||
:key="appId"
|
||||
/>
|
||||
|
||||
<!-- 外部iframe应用 -->
|
||||
<iframe
|
||||
v-else-if="!isBuiltInApp && iframeUrl"
|
||||
:src="iframeUrl"
|
||||
:sandbox="sandboxAttributes"
|
||||
class="external-app-iframe"
|
||||
@load="onIframeLoad"
|
||||
@error="onIframeError"
|
||||
/>
|
||||
|
||||
<!-- 加载中状态(仅用于外部应用) -->
|
||||
<div v-else-if="!isBuiltInApp && isLoading" class="loading-state">
|
||||
<div class="loading-spinner"></div>
|
||||
<p>正在加载应用...</p>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div v-else-if="hasError" class="error-state">
|
||||
<div class="error-icon">⚠️</div>
|
||||
<h3>应用加载失败</h3>
|
||||
<p>{{ errorMessage }}</p>
|
||||
<button @click="retry" class="retry-btn">重试</button>
|
||||
</div>
|
||||
|
||||
<!-- 内置应用未找到的后备显示 -->
|
||||
<div v-else-if="isBuiltInApp && !appComponent" class="error-state">
|
||||
<div class="error-icon">📱</div>
|
||||
<h3>应用不存在</h3>
|
||||
<p>内置应用 "{{ appId }}" 未找到</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted, inject, watch } from 'vue'
|
||||
import { appRegistry } from '@/apps'
|
||||
import { externalAppDiscovery } from '@/services/ExternalAppDiscovery'
|
||||
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||
|
||||
const props = defineProps<{
|
||||
appId: string
|
||||
windowId?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
loaded: []
|
||||
error: [error: Error]
|
||||
}>()
|
||||
|
||||
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||
|
||||
// 检查是否为内置应用
|
||||
const isBuiltInApp = computed(() => {
|
||||
return appRegistry.hasApp(props.appId)
|
||||
})
|
||||
|
||||
// 检查是否为外置应用
|
||||
const isExternalApp = computed(() => {
|
||||
return externalAppDiscovery.hasApp(props.appId)
|
||||
})
|
||||
|
||||
// 内置应用组件 - 立即获取,无需异步
|
||||
const appComponent = computed(() => {
|
||||
if (isBuiltInApp.value) {
|
||||
const appRegistration = appRegistry.getApp(props.appId)
|
||||
return appRegistration?.component
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
// 外部应用相关状态
|
||||
const isLoading = ref(false) // 默认不loading,只有外部应用需要
|
||||
const hasError = ref(false)
|
||||
const errorMessage = ref('')
|
||||
const iframeUrl = ref('')
|
||||
|
||||
// 沙箱属性(仅用于外部应用)
|
||||
const sandboxAttributes = computed(() => {
|
||||
return 'allow-scripts allow-forms allow-popups'
|
||||
})
|
||||
|
||||
// 初始化应用
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
if (isBuiltInApp.value) {
|
||||
// 内置应用立即可用,无需异步加载
|
||||
if (appComponent.value) {
|
||||
emit('loaded')
|
||||
} else {
|
||||
hasError.value = true
|
||||
errorMessage.value = '内置应用未找到'
|
||||
emit('error', new Error('内置应用未找到'))
|
||||
}
|
||||
} else if (isExternalApp.value) {
|
||||
// 外置应用需要异步加载
|
||||
isLoading.value = true
|
||||
hasError.value = false
|
||||
errorMessage.value = ''
|
||||
await loadExternalApp()
|
||||
} else {
|
||||
throw new Error(`应用 ${props.appId} 未找到(不是内置也不是外置应用)`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('应用初始化失败:', error)
|
||||
hasError.value = true
|
||||
errorMessage.value = (error as Error).message
|
||||
isLoading.value = false
|
||||
emit('error', error as Error)
|
||||
}
|
||||
}
|
||||
|
||||
// 加载外部应用
|
||||
const loadExternalApp = async () => {
|
||||
try {
|
||||
console.log(`[AppRenderer] 加载外置应用: ${props.appId}`)
|
||||
|
||||
// 直接从外置应用发现服务获取应用信息
|
||||
const externalApp = externalAppDiscovery.getApp(props.appId)
|
||||
if (!externalApp) {
|
||||
throw new Error('外置应用未找到')
|
||||
}
|
||||
|
||||
// 直接使用外置应用的入口路径
|
||||
iframeUrl.value = externalApp.entryPath
|
||||
console.log(`[AppRenderer] 外置应用加载路径: ${iframeUrl.value}`)
|
||||
|
||||
} catch (error) {
|
||||
console.error(`[AppRenderer] 外置应用加载失败:`, error)
|
||||
throw new Error(`外部应用加载失败: ${(error as Error).message}`)
|
||||
}
|
||||
}
|
||||
|
||||
// iframe加载完成
|
||||
const onIframeLoad = () => {
|
||||
isLoading.value = false
|
||||
emit('loaded')
|
||||
}
|
||||
|
||||
// iframe加载错误
|
||||
const onIframeError = (event: Event) => {
|
||||
hasError.value = true
|
||||
errorMessage.value = '外部应用加载失败'
|
||||
isLoading.value = false
|
||||
emit('error', new Error('iframe加载失败'))
|
||||
}
|
||||
|
||||
// 重试加载
|
||||
const retry = () => {
|
||||
initializeApp()
|
||||
}
|
||||
|
||||
// 监听内置应用组件的可用性,立即发送loaded事件
|
||||
watch(appComponent, (newComponent) => {
|
||||
if (isBuiltInApp.value && newComponent) {
|
||||
emit('loaded')
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
onMounted(() => {
|
||||
// 内置应用无需额外初始化,只处理外部应用
|
||||
if (!isBuiltInApp.value) {
|
||||
if (isExternalApp.value) {
|
||||
initializeApp()
|
||||
} else {
|
||||
// 应用不存在
|
||||
hasError.value = true
|
||||
errorMessage.value = `应用 ${props.appId} 未找到`
|
||||
emit('error', new Error('应用未找到'))
|
||||
}
|
||||
} else if (!appComponent.value) {
|
||||
// 内置应用不存在
|
||||
hasError.value = true
|
||||
errorMessage.value = '内置应用未找到'
|
||||
emit('error', new Error('内置应用未找到'))
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
// 清理iframe URL(只有当是blob URL时才需要清理)
|
||||
if (iframeUrl.value && iframeUrl.value.startsWith('blob:')) {
|
||||
URL.revokeObjectURL(iframeUrl.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.app-renderer {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.external-app-iframe {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #6c757d;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid #f3f3f3;
|
||||
border-top: 4px solid #007bff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
.error-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.error-state h3 {
|
||||
margin-bottom: 8px;
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.error-state p {
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.retry-btn {
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.retry-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
|
||||
/* 应用特定样式 */
|
||||
.app-calculator {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
.app-notepad {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.app-todo {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
</style>
|
||||
138
src/ui/components/WindowManager.vue
Normal file
138
src/ui/components/WindowManager.vue
Normal file
@@ -0,0 +1,138 @@
|
||||
<template>
|
||||
<div class="window-manager">
|
||||
<!-- 所有已打开的内置应用窗口 -->
|
||||
<teleport
|
||||
v-for="window in builtInWindows"
|
||||
:key="window.id"
|
||||
:to="`#app-container-${window.appId}`"
|
||||
>
|
||||
<component
|
||||
:is="window.component"
|
||||
:key="window.id"
|
||||
v-bind="window.props"
|
||||
@close="closeWindow(window.id)"
|
||||
/>
|
||||
</teleport>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
||||
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||
import { appRegistry } from '@/apps'
|
||||
|
||||
interface BuiltInWindow {
|
||||
id: string
|
||||
appId: string
|
||||
component: any
|
||||
props: Record<string, any>
|
||||
}
|
||||
|
||||
// 存储所有已打开的内置应用窗口
|
||||
const builtInWindows = ref<BuiltInWindow[]>([])
|
||||
|
||||
// 注入系统服务
|
||||
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||
|
||||
// 添加内置应用窗口
|
||||
const addBuiltInWindow = (windowId: string, appId: string) => {
|
||||
// 检查应用是否存在
|
||||
const appRegistration = appRegistry.getApp(appId)
|
||||
if (!appRegistration) {
|
||||
console.error(`内置应用 ${appId} 不存在`)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查窗口是否已存在
|
||||
const existingWindow = builtInWindows.value.find(w => w.id === windowId)
|
||||
if (existingWindow) {
|
||||
console.warn(`窗口 ${windowId} 已存在`)
|
||||
return
|
||||
}
|
||||
|
||||
// 添加窗口
|
||||
const window: BuiltInWindow = {
|
||||
id: windowId,
|
||||
appId,
|
||||
component: appRegistration.component,
|
||||
props: {
|
||||
windowId,
|
||||
appId
|
||||
}
|
||||
}
|
||||
|
||||
builtInWindows.value.push(window)
|
||||
console.log(`[WindowManager] 添加内置应用窗口: ${appId} (${windowId})`)
|
||||
}
|
||||
|
||||
// 关闭内置应用窗口
|
||||
const closeWindow = (windowId: string) => {
|
||||
const index = builtInWindows.value.findIndex(w => w.id === windowId)
|
||||
if (index > -1) {
|
||||
const window = builtInWindows.value[index]
|
||||
builtInWindows.value.splice(index, 1)
|
||||
console.log(`[WindowManager] 关闭内置应用窗口: ${window.appId} (${windowId})`)
|
||||
|
||||
// 通知系统服务关闭窗口
|
||||
if (systemService) {
|
||||
const windowService = systemService.getWindowService()
|
||||
windowService.destroyWindow(windowId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除内置应用窗口(不关闭系统窗口)
|
||||
const removeBuiltInWindow = (windowId: string) => {
|
||||
const index = builtInWindows.value.findIndex(w => w.id === windowId)
|
||||
if (index > -1) {
|
||||
const window = builtInWindows.value[index]
|
||||
builtInWindows.value.splice(index, 1)
|
||||
console.log(`[WindowManager] 移除内置应用窗口: ${window.appId} (${windowId})`)
|
||||
}
|
||||
}
|
||||
|
||||
// 监听窗口事件
|
||||
let eventUnsubscriber: (() => void) | null = null
|
||||
|
||||
onMounted(() => {
|
||||
if (systemService) {
|
||||
const eventService = systemService.getEventService()
|
||||
|
||||
// 监听内置应用窗口创建事件
|
||||
const subscriberId = eventService.subscribe('system', 'built-in-window-created', (message) => {
|
||||
const { windowId, appId } = message.payload
|
||||
addBuiltInWindow(windowId, appId)
|
||||
})
|
||||
|
||||
// 监听内置应用窗口关闭事件
|
||||
const closeSubscriberId = eventService.subscribe('system', 'built-in-window-closed', (message) => {
|
||||
const { windowId } = message.payload
|
||||
removeBuiltInWindow(windowId)
|
||||
})
|
||||
|
||||
eventUnsubscriber = () => {
|
||||
eventService.unsubscribe(subscriberId)
|
||||
eventService.unsubscribe(closeSubscriberId)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (eventUnsubscriber) {
|
||||
eventUnsubscriber()
|
||||
}
|
||||
})
|
||||
|
||||
// 暴露给全局使用
|
||||
defineExpose({
|
||||
addBuiltInWindow,
|
||||
removeBuiltInWindow,
|
||||
closeWindow
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.window-manager {
|
||||
/* 这个组件本身不需要样式,它只是用来管理 teleport */
|
||||
}
|
||||
</style>
|
||||
@@ -1,23 +1,275 @@
|
||||
<template>
|
||||
<div class="desktop-icons-container" :style="gridStyle">
|
||||
<AppIcon
|
||||
v-for="(appIcon, i) in appIconsRef"
|
||||
:key="i"
|
||||
:iconInfo="appIcon"
|
||||
:gridTemplate="gridTemplate"
|
||||
@dblclick="runApp(appIcon)"
|
||||
/>
|
||||
<AppIcon v-for="(appIcon, i) in appIconsRef" :key="i" :iconInfo="appIcon" :gridTemplate="gridTemplate"
|
||||
@dblclick="runApp(appIcon)" />
|
||||
|
||||
<!-- 系统状态显示 -->
|
||||
<div v-if="showSystemStatus" class="system-status-overlay">
|
||||
<div class="status-panel">
|
||||
<h3>系统状态</h3>
|
||||
<div class="status-item">
|
||||
<span>运行状态:</span>
|
||||
<span :class="{ 'status-ok': systemStatus.running, 'status-error': !systemStatus.running }">
|
||||
{{ systemStatus.running ? '正常' : '错误' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>活跃应用:</span>
|
||||
<span>{{ systemStatus.performance.activeApps }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>活跃窗体:</span>
|
||||
<span>{{ systemStatus.performance.activeWindows }}</span>
|
||||
</div>
|
||||
<div class="status-item">
|
||||
<span>内存使用:</span>
|
||||
<span>{{ Math.round(systemStatus.performance.memoryUsage) }}MB</span>
|
||||
</div>
|
||||
<button @click="showSystemStatus = false" class="close-btn">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, ref, onMounted, onUnmounted } from 'vue'
|
||||
import AppIcon from '@/ui/desktop-container/AppIcon.vue'
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import { useDesktopContainerInit } from '@/ui/desktop-container/useDesktopContainerInit.ts'
|
||||
import { useDynamicAppIcons, getAppIdFromIcon } from '@/ui/desktop-container/useDynamicAppIcons.ts'
|
||||
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
|
||||
import type { SystemStatus } from '@/services/SystemServiceIntegration'
|
||||
import { appRegistry } from '@/apps'
|
||||
import { externalAppDiscovery } from '@/services/ExternalAppDiscovery'
|
||||
|
||||
const { appIconsRef, gridStyle, gridTemplate } = useDesktopContainerInit('.desktop-icons-container')
|
||||
const { getAppIconsWithPositions, saveIconPositions, refreshApps } = useDynamicAppIcons()
|
||||
|
||||
const runApp = (appIcon: IDesktopAppIcon) => {}
|
||||
// 使用动态应用图标替换静态列表
|
||||
const updateAppIcons = () => {
|
||||
const dynamicIcons = getAppIconsWithPositions()
|
||||
appIconsRef.value = dynamicIcons
|
||||
}
|
||||
|
||||
// 初始加载应用图标
|
||||
updateAppIcons()
|
||||
|
||||
// 注入系统服务和窗口管理器
|
||||
const systemService = inject<SystemServiceIntegration>('systemService')
|
||||
const windowManager = inject<any>('windowManager')
|
||||
const showSystemStatus = ref(false)
|
||||
const systemStatus = ref<SystemStatus>({
|
||||
initialized: false,
|
||||
running: false,
|
||||
servicesStatus: {
|
||||
windowService: false,
|
||||
resourceService: false,
|
||||
eventService: false,
|
||||
sandboxEngine: false,
|
||||
lifecycleManager: false
|
||||
},
|
||||
performance: {
|
||||
memoryUsage: 0,
|
||||
cpuUsage: 0,
|
||||
activeApps: 0,
|
||||
activeWindows: 0
|
||||
},
|
||||
uptime: 0
|
||||
})
|
||||
|
||||
let statusUpdateInterval: number | null = null
|
||||
|
||||
// 更新系统状态
|
||||
const updateSystemStatus = async () => {
|
||||
if (!systemService) return
|
||||
|
||||
try {
|
||||
const status = await systemService.getSystemStatus()
|
||||
systemStatus.value = status
|
||||
} catch (error) {
|
||||
console.error('获取系统状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 运行应用
|
||||
const runApp = async (appIcon: IDesktopAppIcon) => {
|
||||
if (!systemService) {
|
||||
console.error('系统服务未初始化')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (appIcon.name === '系统状态') {
|
||||
showSystemStatus.value = true
|
||||
updateSystemStatus()
|
||||
} else {
|
||||
const appId = getAppIdFromIcon(appIcon)
|
||||
await startApp(appId)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动应用失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 启动应用(支持内置和外部应用)
|
||||
const startApp = async (appId: string) => {
|
||||
if (!systemService) return
|
||||
|
||||
try {
|
||||
const lifecycleManager = systemService.getLifecycleManager()
|
||||
|
||||
// 检查是否为内置应用
|
||||
if (appRegistry.hasApp(appId)) {
|
||||
// 内置应用:使用主应用的窗口管理器
|
||||
const appRegistration = appRegistry.getApp(appId)!
|
||||
|
||||
// 检查是否已在运行
|
||||
if (lifecycleManager.isAppRunning(appId)) {
|
||||
console.log(`应用 ${appRegistration.manifest.name} 已在运行`)
|
||||
return
|
||||
}
|
||||
|
||||
// 创建窗口
|
||||
const windowService = systemService.getWindowService()
|
||||
const windowConfig = {
|
||||
title: appRegistration.manifest.name,
|
||||
width: appRegistration.manifest.window.width,
|
||||
height: appRegistration.manifest.window.height,
|
||||
minWidth: appRegistration.manifest.window.minWidth,
|
||||
minHeight: appRegistration.manifest.window.minHeight,
|
||||
resizable: appRegistration.manifest.window.resizable !== false
|
||||
}
|
||||
|
||||
const windowInstance = await windowService.createWindow(appId, windowConfig)
|
||||
|
||||
// 使用主应用的窗口管理器来渲染内置应用
|
||||
if (windowManager?.value) {
|
||||
windowManager.value.addBuiltInWindow(windowInstance.id, appId)
|
||||
console.log(`[主应用] 使用窗口管理器渲染内置应用: ${appId}`)
|
||||
} else {
|
||||
console.error('窗口管理器未初始化')
|
||||
}
|
||||
|
||||
} else if (externalAppDiscovery.hasApp(appId)) {
|
||||
// 外置应用:直接使用ApplicationLifecycleManager
|
||||
console.log(`启动外置应用: ${appId}`)
|
||||
|
||||
if (!lifecycleManager.isAppRunning(appId)) {
|
||||
await lifecycleManager.startApp(appId)
|
||||
console.log(`外置应用 ${appId} 启动成功`)
|
||||
} else {
|
||||
console.log(`外置应用 ${appId} 已在运行`)
|
||||
}
|
||||
} else {
|
||||
console.error(`未知的应用: ${appId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('启动应用失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 手动刷新应用列表(通过右键菜单调用)
|
||||
const manualRefreshApps = async () => {
|
||||
try {
|
||||
await refreshApps()
|
||||
updateAppIcons()
|
||||
console.log('[DesktopContainer] 应用列表已手动刷新')
|
||||
} catch (error) {
|
||||
console.error('[DesktopContainer] 手动刷新应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露手动刷新方法给父组件使用
|
||||
defineExpose({
|
||||
manualRefreshApps
|
||||
})
|
||||
|
||||
// 初始化和清理
|
||||
onMounted(async () => {
|
||||
// 开始系统状态更新
|
||||
statusUpdateInterval = setInterval(updateSystemStatus, 5000)
|
||||
updateSystemStatus()
|
||||
|
||||
// 不再设置自动刷新定时器,只在需要时手动刷新
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (statusUpdateInterval) {
|
||||
clearInterval(statusUpdateInterval)
|
||||
statusUpdateInterval = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.desktop-icons-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.system-status-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.status-panel {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
min-width: 300px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.status-panel h3 {
|
||||
margin: 0 0 16px 0;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.status-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.status-item:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.status-ok {
|
||||
color: #28a745;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
width: 100%;
|
||||
padding: 8px 16px;
|
||||
background: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin-top: 16px;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.close-btn:hover {
|
||||
background: #0056b3;
|
||||
}
|
||||
</style>
|
||||
@@ -1,19 +1,10 @@
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
reactive,
|
||||
ref,
|
||||
toRaw,
|
||||
toValue,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { computed, onMounted, onUnmounted, reactive, ref, toRaw, toValue, watch } from 'vue'
|
||||
import type { IGridTemplateParams } from '@/ui/types/IGridTemplateParams.ts'
|
||||
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
|
||||
|
||||
export function useDesktopContainerInit(containerStr: string) {
|
||||
let container:HTMLElement
|
||||
let container: HTMLElement
|
||||
// 初始值
|
||||
const gridTemplate = reactive<IGridTemplateParams>({
|
||||
cellExpectWidth: 90,
|
||||
@@ -23,22 +14,28 @@ export function useDesktopContainerInit(containerStr: string) {
|
||||
gapX: 4,
|
||||
gapY: 4,
|
||||
colCount: 1,
|
||||
rowCount: 1
|
||||
rowCount: 1,
|
||||
})
|
||||
|
||||
const gridStyle = computed(() => ({
|
||||
gridTemplateColumns: `repeat(${gridTemplate.colCount}, minmax(${gridTemplate.cellExpectWidth}px, 1fr))`,
|
||||
gridTemplateRows: `repeat(${gridTemplate.rowCount}, minmax(${gridTemplate.cellExpectHeight}px, 1fr))`,
|
||||
gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`
|
||||
gap: `${gridTemplate.gapX}px ${gridTemplate.gapY}px`,
|
||||
}))
|
||||
|
||||
const ro = new ResizeObserver(entries => {
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
const containerRect = container.getBoundingClientRect()
|
||||
gridTemplate.colCount = Math.floor((containerRect.width + gridTemplate.gapX) / (gridTemplate.cellExpectWidth + gridTemplate.gapX));
|
||||
gridTemplate.rowCount = Math.floor((containerRect.height + gridTemplate.gapY) / (gridTemplate.cellExpectHeight + gridTemplate.gapY));
|
||||
gridTemplate.colCount = Math.floor(
|
||||
(containerRect.width + gridTemplate.gapX) /
|
||||
(gridTemplate.cellExpectWidth + gridTemplate.gapX),
|
||||
)
|
||||
gridTemplate.rowCount = Math.floor(
|
||||
(containerRect.height + gridTemplate.gapY) /
|
||||
(gridTemplate.cellExpectHeight + gridTemplate.gapY),
|
||||
)
|
||||
|
||||
const w = containerRect.width - (gridTemplate.gapX * (gridTemplate.colCount - 1))
|
||||
const h = containerRect.height - (gridTemplate.gapY * (gridTemplate.rowCount - 1))
|
||||
const w = containerRect.width - gridTemplate.gapX * (gridTemplate.colCount - 1)
|
||||
const h = containerRect.height - gridTemplate.gapY * (gridTemplate.rowCount - 1)
|
||||
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
|
||||
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
|
||||
})
|
||||
@@ -52,45 +49,101 @@ export function useDesktopContainerInit(containerStr: string) {
|
||||
ro.disconnect()
|
||||
})
|
||||
|
||||
// 默认系统应用
|
||||
const defaultApps: IDesktopAppIcon[] = [
|
||||
{
|
||||
name: '计算器',
|
||||
icon: '🧮',
|
||||
path: 'calculator',
|
||||
x: 1,
|
||||
y: 1,
|
||||
},
|
||||
{
|
||||
name: '记事本',
|
||||
icon: '📝',
|
||||
path: 'notepad',
|
||||
x: 2,
|
||||
y: 1,
|
||||
},
|
||||
{
|
||||
name: '系统状态',
|
||||
icon: '⚙️',
|
||||
path: 'system-status',
|
||||
x: 3,
|
||||
y: 1,
|
||||
},
|
||||
{
|
||||
name: '文件管理器',
|
||||
icon: '📁',
|
||||
path: 'file-manager',
|
||||
x: 1,
|
||||
y: 2,
|
||||
},
|
||||
{
|
||||
name: '浏览器',
|
||||
icon: '🌐',
|
||||
path: 'browser',
|
||||
x: 2,
|
||||
y: 2,
|
||||
},
|
||||
{
|
||||
name: '待办事项',
|
||||
icon: '✓',
|
||||
path: 'todo-app',
|
||||
x: 3,
|
||||
y: 2,
|
||||
},
|
||||
]
|
||||
|
||||
// 有桌面图标的app
|
||||
// const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess)
|
||||
const appInfos: IProcessInfo[] = []
|
||||
const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]')
|
||||
const appIcons: IDesktopAppIcon[] = appInfos.map((processInfo, index) => {
|
||||
const oldAppIcon = oldAppIcons.find(oldAppIcon => oldAppIcon.name === processInfo.name)
|
||||
const appInfos: IProcessInfo[] = defaultApps
|
||||
const oldAppIcons: IDesktopAppIcon[] =
|
||||
JSON.parse(localStorage.getItem('desktopAppIconInfo') || 'null') || defaultApps
|
||||
const appIcons: IDesktopAppIcon[] =
|
||||
appInfos.length > 0
|
||||
? appInfos.map((processInfo, index) => {
|
||||
const oldAppIcon = oldAppIcons.find((oldAppIcon) => oldAppIcon.name === processInfo.name)
|
||||
|
||||
// 左上角坐标原点,从上到下从左到右 索引从1开始
|
||||
const x = index % gridTemplate.rowCount + 1
|
||||
const y = Math.floor(index / gridTemplate.rowCount) + 1
|
||||
// 左上角坐标原点,从上到下从左到右 索引从1开始
|
||||
const x = (index % gridTemplate.rowCount) + 1
|
||||
const y = Math.floor(index / gridTemplate.rowCount) + 1
|
||||
|
||||
return {
|
||||
name: processInfo.name,
|
||||
icon: processInfo.icon,
|
||||
path: processInfo.startName,
|
||||
x: oldAppIcon ? oldAppIcon.x : x,
|
||||
y: oldAppIcon ? oldAppIcon.y : y
|
||||
}
|
||||
})
|
||||
return {
|
||||
name: processInfo.name,
|
||||
icon: processInfo.icon,
|
||||
path: processInfo.startName,
|
||||
x: oldAppIcon ? oldAppIcon.x : x,
|
||||
y: oldAppIcon ? oldAppIcon.y : y,
|
||||
}
|
||||
})
|
||||
: oldAppIcons
|
||||
|
||||
const appIconsRef = ref(appIcons)
|
||||
const exceedApp = ref<IDesktopAppIcon[]>([])
|
||||
|
||||
watch(() => [gridTemplate.colCount, gridTemplate.rowCount], ([nCols, nRows], [oCols, oRows]) => {
|
||||
// if (oCols == 1 && oRows == 1) return
|
||||
if (oCols === nCols && oRows === nRows) return
|
||||
const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows)
|
||||
appIconsRef.value = appIcons
|
||||
exceedApp.value = hideAppIcons
|
||||
})
|
||||
watch(
|
||||
() => [gridTemplate.colCount, gridTemplate.rowCount],
|
||||
([nCols, nRows], [oCols, oRows]) => {
|
||||
// if (oCols == 1 && oRows == 1) return
|
||||
if (oCols === nCols && oRows === nRows) return
|
||||
const { appIcons, hideAppIcons } = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows)
|
||||
appIconsRef.value = appIcons
|
||||
exceedApp.value = hideAppIcons
|
||||
},
|
||||
)
|
||||
|
||||
watch(() => appIconsRef.value, (appIcons) => {
|
||||
localStorage.setItem('desktopAppIconInfo', JSON.stringify(appIcons))
|
||||
})
|
||||
watch(
|
||||
() => appIconsRef.value,
|
||||
(appIcons) => {
|
||||
localStorage.setItem('desktopAppIconInfo', JSON.stringify(appIcons))
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
gridTemplate,
|
||||
appIconsRef,
|
||||
gridStyle
|
||||
gridStyle,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,12 +156,12 @@ export function useDesktopContainerInit(containerStr: string) {
|
||||
function rearrangeIcons(
|
||||
appIconInfos: IDesktopAppIcon[],
|
||||
maxCol: number,
|
||||
maxRow: number
|
||||
maxRow: number,
|
||||
): IRearrangeInfo {
|
||||
const occupied = new Set<string>();
|
||||
const occupied = new Set<string>()
|
||||
|
||||
function key(x: number, y: number) {
|
||||
return `${x},${y}`;
|
||||
return `${x},${y}`
|
||||
}
|
||||
|
||||
const appIcons: IDesktopAppIcon[] = []
|
||||
@@ -116,7 +169,7 @@ function rearrangeIcons(
|
||||
const temp: IDesktopAppIcon[] = []
|
||||
|
||||
for (const appIcon of appIconInfos) {
|
||||
const { x, y } = appIcon;
|
||||
const { x, y } = appIcon
|
||||
|
||||
if (x <= maxCol && y <= maxRow) {
|
||||
if (!occupied.has(key(x, y))) {
|
||||
@@ -132,17 +185,17 @@ function rearrangeIcons(
|
||||
for (const appIcon of temp) {
|
||||
if (appIcons.length < max) {
|
||||
// 最后格子也被占 → 从 (1,1) 开始找空位
|
||||
let placed = false;
|
||||
let placed = false
|
||||
for (let c = 1; c <= maxCol; c++) {
|
||||
for (let r = 1; r <= maxRow; r++) {
|
||||
if (!occupied.has(key(c, r))) {
|
||||
occupied.add(key(c, r));
|
||||
appIcons.push({ ...appIcon, x: c, y: r });
|
||||
placed = true;
|
||||
break;
|
||||
occupied.add(key(c, r))
|
||||
appIcons.push({ ...appIcon, x: c, y: r })
|
||||
placed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (placed) break;
|
||||
if (placed) break
|
||||
}
|
||||
} else {
|
||||
// 放不下了
|
||||
@@ -152,13 +205,13 @@ function rearrangeIcons(
|
||||
|
||||
return {
|
||||
appIcons,
|
||||
hideAppIcons
|
||||
};
|
||||
hideAppIcons,
|
||||
}
|
||||
}
|
||||
|
||||
interface IRearrangeInfo {
|
||||
/** 正常的桌面图标信息 */
|
||||
appIcons: IDesktopAppIcon[];
|
||||
appIcons: IDesktopAppIcon[]
|
||||
/** 隐藏的桌面图标信息(超出屏幕显示的) */
|
||||
hideAppIcons: IDesktopAppIcon[];
|
||||
hideAppIcons: IDesktopAppIcon[]
|
||||
}
|
||||
|
||||
142
src/ui/desktop-container/useDynamicAppIcons.ts
Normal file
142
src/ui/desktop-container/useDynamicAppIcons.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { computed, watch } from 'vue'
|
||||
import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
|
||||
import { appRegistry } from '@/apps'
|
||||
import { externalAppDiscovery } from '@/services/ExternalAppDiscovery'
|
||||
|
||||
/**
|
||||
* 动态应用图标管理
|
||||
* 合并内置应用和外置应用生成图标列表
|
||||
*/
|
||||
export function useDynamicAppIcons() {
|
||||
|
||||
// 生成所有可用的应用图标
|
||||
const generateAppIcons = (): IDesktopAppIcon[] => {
|
||||
const icons: IDesktopAppIcon[] = []
|
||||
let position = 1
|
||||
|
||||
// 添加内置应用
|
||||
const builtInApps = appRegistry.getAllApps()
|
||||
for (const app of builtInApps) {
|
||||
const x = ((position - 1) % 4) + 1 // 每行4个图标
|
||||
const y = Math.floor((position - 1) / 4) + 1
|
||||
|
||||
icons.push({
|
||||
name: app.manifest.name,
|
||||
icon: app.manifest.icon,
|
||||
path: app.manifest.id,
|
||||
x,
|
||||
y
|
||||
})
|
||||
position++
|
||||
}
|
||||
|
||||
// 添加外置应用
|
||||
const externalApps = externalAppDiscovery.getDiscoveredApps()
|
||||
for (const app of externalApps) {
|
||||
const x = ((position - 1) % 4) + 1
|
||||
const y = Math.floor((position - 1) / 4) + 1
|
||||
|
||||
icons.push({
|
||||
name: app.manifest.name,
|
||||
icon: app.manifest.icon || '📱', // 默认图标
|
||||
path: app.manifest.id,
|
||||
x,
|
||||
y
|
||||
})
|
||||
position++
|
||||
}
|
||||
|
||||
// 添加系统状态应用
|
||||
icons.push({
|
||||
name: '系统状态',
|
||||
icon: '⚙️',
|
||||
path: 'system-status',
|
||||
x: ((position - 1) % 4) + 1,
|
||||
y: Math.floor((position - 1) / 4) + 1
|
||||
})
|
||||
|
||||
return icons
|
||||
}
|
||||
|
||||
// 计算应用图标(响应式)
|
||||
const appIcons = computed(() => {
|
||||
return generateAppIcons()
|
||||
})
|
||||
|
||||
// 从本地存储加载位置信息
|
||||
const loadIconPositions = (): Record<string, { x: number, y: number }> => {
|
||||
try {
|
||||
const saved = localStorage.getItem('desktopAppIconPositions')
|
||||
return saved ? JSON.parse(saved) : {}
|
||||
} catch (error) {
|
||||
console.warn('加载图标位置信息失败:', error)
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存位置信息到本地存储
|
||||
const saveIconPositions = (icons: IDesktopAppIcon[]) => {
|
||||
try {
|
||||
const positions = icons.reduce((acc, icon) => {
|
||||
acc[icon.path] = { x: icon.x, y: icon.y }
|
||||
return acc
|
||||
}, {} as Record<string, { x: number, y: number }>)
|
||||
|
||||
localStorage.setItem('desktopAppIconPositions', JSON.stringify(positions))
|
||||
} catch (error) {
|
||||
console.warn('保存图标位置信息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 应用保存的位置信息
|
||||
const applyIconPositions = (icons: IDesktopAppIcon[]): IDesktopAppIcon[] => {
|
||||
const savedPositions = loadIconPositions()
|
||||
|
||||
return icons.map(icon => {
|
||||
const savedPos = savedPositions[icon.path]
|
||||
if (savedPos) {
|
||||
return { ...icon, x: savedPos.x, y: savedPos.y }
|
||||
}
|
||||
return icon
|
||||
})
|
||||
}
|
||||
|
||||
// 获取带位置信息的应用图标
|
||||
const getAppIconsWithPositions = (): IDesktopAppIcon[] => {
|
||||
const icons = appIcons.value
|
||||
return applyIconPositions(icons)
|
||||
}
|
||||
|
||||
// 刷新应用列表(仅在需要时手动调用)
|
||||
const refreshApps = async () => {
|
||||
try {
|
||||
// 只有在系统服务已启动的情况下才刷新
|
||||
if (externalAppDiscovery['hasStarted']) {
|
||||
await externalAppDiscovery.refresh()
|
||||
console.log('[DynamicAppIcons] 应用列表已刷新')
|
||||
} else {
|
||||
console.log('[DynamicAppIcons] 系统服务未启动,跳过刷新')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[DynamicAppIcons] 刷新应用列表失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
appIcons,
|
||||
getAppIconsWithPositions,
|
||||
saveIconPositions,
|
||||
refreshApps
|
||||
}
|
||||
}
|
||||
|
||||
// 获取应用ID映射
|
||||
export function getAppIdFromIcon(iconInfo: IDesktopAppIcon): string {
|
||||
// 特殊处理系统应用
|
||||
if (iconInfo.path === 'system-status') {
|
||||
return 'system-status'
|
||||
}
|
||||
|
||||
// 对于其他应用,path就是appId
|
||||
return iconInfo.path
|
||||
}
|
||||
Reference in New Issue
Block a user