This commit is contained in:
2025-09-24 16:43:10 +08:00
parent 12f46e6f8e
commit 9dbc054483
130 changed files with 16474 additions and 4660 deletions

View File

@@ -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()

View 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>

View 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>

View File

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

View File

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

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