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

97
src/apps/AppRegistry.ts Normal file
View File

@@ -0,0 +1,97 @@
import type { AppRegistration } from './types/AppManifest'
import { reactive, markRaw } from 'vue'
/**
* 应用注册中心
* 管理所有内置应用的注册和获取
*/
export class AppRegistry {
private static instance: AppRegistry | null = null
private apps = reactive(new Map<string, AppRegistration>())
private constructor() {}
static getInstance(): AppRegistry {
if (!AppRegistry.instance) {
AppRegistry.instance = new AppRegistry()
}
return AppRegistry.instance
}
/**
* 注册内置应用
*/
registerApp(registration: AppRegistration): void {
// 使用 markRaw 标记组件,避免被设为响应式
const safeRegistration = {
...registration,
component: registration.component ? markRaw(registration.component) : registration.component
}
this.apps.set(registration.manifest.id, safeRegistration)
console.log(`已注册内置应用: ${registration.manifest.name}`)
}
/**
* 获取应用
*/
getApp(appId: string): AppRegistration | undefined {
return this.apps.get(appId)
}
/**
* 获取所有应用
*/
getAllApps(): AppRegistration[] {
return Array.from(this.apps.values())
}
/**
* 获取所有内置应用
*/
getBuiltInApps(): AppRegistration[] {
return Array.from(this.apps.values()).filter(app => app.isBuiltIn)
}
/**
* 检查应用是否存在
*/
hasApp(appId: string): boolean {
return this.apps.has(appId)
}
/**
* 按类别获取应用
*/
getAppsByCategory(category: string): AppRegistration[] {
return Array.from(this.apps.values()).filter(
app => app.manifest.category === category
)
}
/**
* 搜索应用
*/
searchApps(query: string): AppRegistration[] {
const lowercaseQuery = query.toLowerCase()
return Array.from(this.apps.values()).filter(app => {
const manifest = app.manifest
return (
manifest.name.toLowerCase().includes(lowercaseQuery) ||
manifest.description.toLowerCase().includes(lowercaseQuery) ||
manifest.keywords?.some(keyword =>
keyword.toLowerCase().includes(lowercaseQuery)
)
)
})
}
/**
* 清空所有应用
*/
clear(): void {
this.apps.clear()
}
}
// 导出单例实例
export const appRegistry = AppRegistry.getInstance()

View File

@@ -0,0 +1,348 @@
<template>
<BuiltInApp app-id="calculator" title="计算器">
<div class="calculator">
<div class="display">
<input
v-model="displayValue"
type="text"
readonly
class="display-input"
:class="{ 'error': hasError }"
>
</div>
<div class="buttons">
<!-- 第一行 -->
<button @click="clear" class="btn btn-clear">C</button>
<button @click="deleteLast" class="btn btn-operation"></button>
<button @click="appendOperation('/')" class="btn btn-operation">÷</button>
<button @click="appendOperation('*')" class="btn btn-operation">×</button>
<!-- 第二行 -->
<button @click="appendNumber('7')" class="btn btn-number">7</button>
<button @click="appendNumber('8')" class="btn btn-number">8</button>
<button @click="appendNumber('9')" class="btn btn-number">9</button>
<button @click="appendOperation('-')" class="btn btn-operation">-</button>
<!-- 第三行 -->
<button @click="appendNumber('4')" class="btn btn-number">4</button>
<button @click="appendNumber('5')" class="btn btn-number">5</button>
<button @click="appendNumber('6')" class="btn btn-number">6</button>
<button @click="appendOperation('+')" class="btn btn-operation">+</button>
<!-- 第四行 -->
<button @click="appendNumber('1')" class="btn btn-number">1</button>
<button @click="appendNumber('2')" class="btn btn-number">2</button>
<button @click="appendNumber('3')" class="btn btn-number">3</button>
<button @click="calculate" class="btn btn-equals" rowspan="2">=</button>
<!-- 第五行 -->
<button @click="appendNumber('0')" class="btn btn-number btn-zero">0</button>
<button @click="appendNumber('.')" class="btn btn-number">.</button>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, onMounted, inject } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
// 直接获取系统服务 - 无需通过SDK
const systemService = inject<SystemServiceIntegration>('systemService')
const displayValue = ref('0')
const hasError = ref(false)
const lastResult = ref<number | null>(null)
const shouldResetDisplay = ref(false)
// 直接使用系统存储服务保存历史记录
const saveHistory = async (expression: string, result: string) => {
try {
if (systemService) {
const resourceService = systemService.getResourceService()
const history = await resourceService.getStorage('calculator', 'history') || []
history.push({
expression,
result,
timestamp: new Date().toISOString()
})
// 只保存最近30条记录
if (history.length > 30) {
history.shift()
}
await resourceService.setStorage('calculator', 'history', history)
}
} catch (error) {
console.error('保存历史记录失败:', error)
}
}
// 直接使用事件服务发送通知
const showNotification = (message: string) => {
if (systemService) {
const eventService = systemService.getEventService()
eventService.sendMessage('calculator', 'user-interaction', {
type: 'notification',
message,
timestamp: new Date()
})
}
}
// 添加数字
const appendNumber = (num: string) => {
hasError.value = false
if (shouldResetDisplay.value) {
displayValue.value = '0'
shouldResetDisplay.value = false
}
if (num === '.') {
if (!displayValue.value.includes('.')) {
displayValue.value += num
}
} else {
if (displayValue.value === '0') {
displayValue.value = num
} else {
displayValue.value += num
}
}
}
// 添加运算符
const appendOperation = (op: string) => {
hasError.value = false
shouldResetDisplay.value = false
const lastChar = displayValue.value.slice(-1)
const operations = ['+', '-', '*', '/']
// 如果最后一个字符是运算符,替换它
if (operations.includes(lastChar)) {
displayValue.value = displayValue.value.slice(0, -1) + op
} else {
displayValue.value += op
}
}
// 计算结果
const calculate = async () => {
try {
hasError.value = false
let expression = displayValue.value
.replace(/×/g, '*')
.replace(/÷/g, '/')
// 简单的表达式验证
if (/[+\-*/]$/.test(expression)) {
return // 以运算符结尾,不计算
}
const originalExpression = displayValue.value
const result = eval(expression)
if (!isFinite(result)) {
throw new Error('除零错误')
}
displayValue.value = result.toString()
lastResult.value = result
shouldResetDisplay.value = true
// 保存历史记录
await saveHistory(originalExpression, result.toString())
// 发送通知
showNotification(`计算结果: ${result}`)
} catch (error) {
hasError.value = true
displayValue.value = '错误'
setTimeout(() => {
clear()
}, 1000)
}
}
// 清空
const clear = () => {
displayValue.value = '0'
hasError.value = false
lastResult.value = null
shouldResetDisplay.value = false
}
// 删除最后一个字符
const deleteLast = () => {
hasError.value = false
if (shouldResetDisplay.value) {
clear()
return
}
if (displayValue.value.length > 1) {
displayValue.value = displayValue.value.slice(0, -1)
} else {
displayValue.value = '0'
}
}
// 键盘事件处理
const handleKeyboard = (event: KeyboardEvent) => {
event.preventDefault()
const key = event.key
if (/[0-9.]/.test(key)) {
appendNumber(key)
} else if (['+', '-', '*', '/'].includes(key)) {
appendOperation(key)
} else if (key === 'Enter' || key === '=') {
calculate()
} else if (key === 'Escape' || key === 'c' || key === 'C') {
clear()
} else if (key === 'Backspace') {
deleteLast()
}
}
onMounted(() => {
// 添加键盘事件监听
document.addEventListener('keydown', handleKeyboard)
})
</script>
<style scoped>
.calculator {
background: white;
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
max-width: 320px;
margin: 20px auto;
}
.display {
margin-bottom: 20px;
}
.display-input {
width: 100%;
height: 80px;
font-size: 32px;
text-align: right;
padding: 0 20px;
border: 2px solid #e0e0e0;
border-radius: 8px;
background: #f8f9fa;
color: #333;
outline: none;
font-family: 'Courier New', monospace;
box-sizing: border-box;
}
.display-input.error {
color: #e74c3c;
border-color: #e74c3c;
}
.buttons {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 12px;
height: 320px;
}
.btn {
border: none;
border-radius: 8px;
font-size: 20px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
outline: none;
display: flex;
align-items: center;
justify-content: center;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
}
.btn-number {
background: #ffffff;
color: #333;
border: 2px solid #e0e0e0;
}
.btn-number:hover {
background: #f8f9fa;
border-color: #007bff;
}
.btn-operation {
background: #007bff;
color: white;
}
.btn-operation:hover {
background: #0056b3;
}
.btn-clear {
background: #dc3545;
color: white;
}
.btn-clear:hover {
background: #c82333;
}
.btn-equals {
background: #28a745;
color: white;
grid-row: span 2;
}
.btn-equals:hover {
background: #218838;
}
.btn-zero {
grid-column: span 2;
}
/* 响应式设计 */
@media (max-width: 400px) {
.calculator {
margin: 10px;
padding: 15px;
}
.display-input {
height: 60px;
font-size: 24px;
}
.buttons {
height: 280px;
gap: 8px;
}
.btn {
font-size: 16px;
}
}
</style>

View File

@@ -0,0 +1,125 @@
<template>
<div class="built-in-app" :class="[`app-${appId}`, { 'fullscreen': isFullscreen }]">
<div class="app-container">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { inject, onMounted, onUnmounted, ref, provide } from 'vue'
import type { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
const props = defineProps<{
appId: string
title?: string
}>()
const isFullscreen = ref(false)
const systemService = inject<SystemServiceIntegration>('systemService')
// 为子组件提供应用上下文
const appContext = {
appId: props.appId,
title: props.title || props.appId,
systemService,
// 直接暴露系统服务的方法,简化调用
storage: systemService?.getResourceService(),
events: systemService?.getEventService(),
lifecycle: systemService?.getLifecycleManager(),
window: {
setTitle: (title: string) => {
if (window.SystemSDK) {
window.SystemSDK.window.setTitle(title)
}
},
toggleFullscreen: () => {
isFullscreen.value = !isFullscreen.value
}
}
}
// 提供应用上下文给子组件
provide('appContext', appContext)
// SDK初始化 - 简化版本
onMounted(async () => {
try {
if (window.SystemSDK) {
await window.SystemSDK.init({
appId: props.appId,
appName: props.title || props.appId,
version: '1.0.0',
permissions: ['storage', 'notification']
})
if (props.title) {
await window.SystemSDK.window.setTitle(props.title)
}
console.log(`内置应用 ${props.appId} 初始化成功`)
}
} catch (error) {
console.error(`内置应用 ${props.appId} 初始化失败:`, error)
}
})
// 清理
onUnmounted(() => {
if (window.SystemSDK && window.SystemSDK.initialized) {
window.SystemSDK.destroy?.()
}
})
// 切换全屏模式
const toggleFullscreen = () => {
isFullscreen.value = !isFullscreen.value
}
// 暴露方法给父组件
defineExpose({
toggleFullscreen,
appContext
})
</script>
<style scoped>
.built-in-app {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
background: white;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}
.app-container {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
}
.fullscreen {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
}
/* 应用特定样式 */
.app-calculator {
max-width: 400px;
margin: 0 auto;
}
.app-notepad {
padding: 10px;
}
.app-todo {
/* 待办事项应用样式 */
}
</style>

90
src/apps/index.ts Normal file
View File

@@ -0,0 +1,90 @@
import { appRegistry } from './AppRegistry'
import { markRaw } from 'vue'
import Calculator from './calculator/Calculator.vue'
import Notepad from './notepad/Notepad.vue'
import Todo from './todo/Todo.vue'
/**
* 注册所有内置应用
*/
export function registerBuiltInApps() {
// 注册计算器应用
appRegistry.registerApp({
manifest: {
id: 'calculator',
name: '计算器',
version: '1.0.0',
description: '简单而功能强大的计算器,支持基本数学运算',
author: 'System',
icon: '🧮',
permissions: ['storage'],
window: {
width: 400,
height: 600,
minWidth: 320,
minHeight: 480,
resizable: true,
minimizable: true,
maximizable: false
},
category: 'utilities',
keywords: ['计算器', '数学', '运算', 'calculator', 'math']
},
component: markRaw(Calculator),
isBuiltIn: true
})
// 注册记事本应用
appRegistry.registerApp({
manifest: {
id: 'notepad',
name: '记事本',
version: '1.0.0',
description: '功能丰富的文本编辑器,支持文件管理和多种编辑选项',
author: 'System',
icon: '📝',
permissions: ['storage', 'notification'],
window: {
width: 800,
height: 600,
minWidth: 400,
minHeight: 300,
resizable: true
},
category: 'productivity',
keywords: ['记事本', '文本编辑', '笔记', 'notepad', 'text', 'editor']
},
component: markRaw(Notepad),
isBuiltIn: true
})
// 注册待办事项应用
appRegistry.registerApp({
manifest: {
id: 'todo',
name: '待办事项',
version: '1.0.0',
description: '高效的任务管理工具,帮助您组织和跟踪日常任务',
author: 'System',
icon: '✅',
permissions: ['storage', 'notification'],
window: {
width: 600,
height: 700,
minWidth: 400,
minHeight: 500,
resizable: true
},
category: 'productivity',
keywords: ['待办事项', '任务管理', 'todo', 'task', 'productivity']
},
component: markRaw(Todo),
isBuiltIn: true
})
console.log('内置应用注册完成')
}
// 导出应用注册中心
export { appRegistry } from './AppRegistry'
export type { InternalAppManifest, AppRegistration } from './types/AppManifest'

View File

@@ -0,0 +1,527 @@
<template>
<BuiltInApp app-id="notepad" title="记事本">
<div class="notepad">
<div class="toolbar">
<div class="toolbar-group">
<button @click="newFile" class="btn btn-secondary">
<span class="icon">📄</span>
新建
</button>
<button @click="saveFile" class="btn btn-primary" :disabled="!hasChanges">
<span class="icon">💾</span>
保存
</button>
<button @click="openFile" class="btn btn-secondary">
<span class="icon">📂</span>
打开
</button>
</div>
<div class="toolbar-group">
<select v-model="fontSize" @change="updateFontSize" class="font-size-select">
<option value="12">12px</option>
<option value="14">14px</option>
<option value="16">16px</option>
<option value="18">18px</option>
<option value="20">20px</option>
<option value="24">24px</option>
</select>
<button
@click="toggleWordWrap"
class="btn btn-secondary"
:class="{ active: wordWrap }"
>
<span class="icon">📄</span>
自动换行
</button>
</div>
<div class="file-info">
<span class="filename">{{ currentFileName }}</span>
<span v-if="hasChanges" class="unsaved">*</span>
</div>
</div>
<div class="editor-container">
<textarea
ref="editorRef"
v-model="content"
class="editor"
:style="editorStyle"
placeholder="开始输入文本..."
@input="onContentChange"
@keydown="handleKeydown"
></textarea>
</div>
<div class="status-bar">
<div class="status-left">
<span>: {{ currentLine }}</span>
<span>: {{ currentColumn }}</span>
<span>字符数: {{ characterCount }}</span>
</div>
<div class="status-right">
<span>{{ fileEncoding }}</span>
<span>{{ lastSaved ? `上次保存: ${formatTime(lastSaved)}` : '未保存' }}</span>
</div>
</div>
</div>
<!-- 文件列表对话框 -->
<div v-if="showFileList" class="modal-overlay" @click="showFileList = false">
<div class="modal" @click.stop>
<h3>选择文件</h3>
<div class="file-list">
<div
v-for="file in savedFiles"
:key="file.name"
class="file-item"
@click="loadFile(file.name)"
>
<span class="file-icon">📄</span>
<span class="file-name">{{ file.name }}</span>
<span class="file-date">{{ formatTime(file.date) }}</span>
<button @click.stop="deleteFile(file.name)" class="btn-delete">🗑</button>
</div>
</div>
<div class="modal-actions">
<button @click="showFileList = false" class="btn btn-secondary">取消</button>
</div>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
const editorRef = ref<HTMLTextAreaElement>()
const content = ref('')
const currentFileName = ref('新文档.txt')
const hasChanges = ref(false)
const fontSize = ref('16')
const wordWrap = ref(true)
const lastSaved = ref<Date | null>(null)
const fileEncoding = ref('UTF-8')
const showFileList = ref(false)
const savedFiles = ref<Array<{name: string, date: Date}>>([])
// 计算属性
const editorStyle = computed(() => ({
fontSize: fontSize.value + 'px',
whiteSpace: wordWrap.value ? 'pre-wrap' : 'pre'
}))
const characterCount = computed(() => content.value.length)
const currentLine = ref(1)
const currentColumn = ref(1)
// 获取光标位置
const updateCursorPosition = () => {
if (!editorRef.value) return
const textarea = editorRef.value
const text = textarea.value
const position = textarea.selectionStart
const lines = text.substring(0, position).split('\n')
currentLine.value = lines.length
currentColumn.value = lines[lines.length - 1].length + 1
}
// 内容变化处理
const onContentChange = () => {
hasChanges.value = true
updateCursorPosition()
}
// 新建文件
const newFile = async () => {
if (hasChanges.value) {
if (confirm('当前文档未保存,是否保存?')) {
await saveFile()
}
}
content.value = ''
currentFileName.value = '新文档.txt'
hasChanges.value = false
lastSaved.value = null
updateTitle()
}
// 保存文件
const saveFile = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.set(`notepad_${currentFileName.value}`, content.value)
hasChanges.value = false
lastSaved.value = new Date()
// 更新文件列表
await updateFilesList()
// 显示通知
if (window.SystemSDK.ui?.showNotification) {
await window.SystemSDK.ui.showNotification({
title: '保存成功',
body: `文件 "${currentFileName.value}" 已保存`,
duration: 2000
})
}
} else {
alert('系统SDK未初始化无法保存文件')
}
} catch (error) {
console.error('保存文件失败:', error)
alert('保存文件失败: ' + (error as Error).message)
}
}
// 打开文件
const openFile = async () => {
await updateFilesList()
showFileList.value = true
}
// 加载文件
const loadFile = async (fileName: string) => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get(`notepad_${fileName}`)
if (result.success && result.data) {
content.value = result.data
currentFileName.value = fileName
hasChanges.value = false
lastSaved.value = new Date()
showFileList.value = false
updateTitle()
} else {
alert('文件不存在或为空')
}
}
} catch (error) {
console.error('打开文件失败:', error)
alert('打开文件失败: ' + (error as Error).message)
}
}
// 删除文件
const deleteFile = async (fileName: string) => {
if (confirm(`确定要删除文件 "${fileName}" 吗?`)) {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.remove(`notepad_${fileName}`)
await updateFilesList()
}
} catch (error) {
console.error('删除文件失败:', error)
alert('删除文件失败: ' + (error as Error).message)
}
}
}
// 更新文件列表
const updateFilesList = async () => {
// 这里需要实现获取所有以 notepad_ 开头的存储键
// 由于当前SDK没有提供列出所有键的功能我们先使用一个简化的实现
savedFiles.value = []
// 临时实现:尝试加载一些常见的文件名
const commonNames = ['新文档.txt', '笔记.txt', '备忘录.txt', '临时.txt']
for (const name of commonNames) {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get(`notepad_${name}`)
if (result.success && result.data) {
savedFiles.value.push({
name,
date: new Date() // 暂时使用当前时间
})
}
}
} catch (error) {
// 忽略错误
}
}
}
// 更新字体大小
const updateFontSize = () => {
if (editorRef.value) {
editorRef.value.focus()
}
}
// 切换自动换行
const toggleWordWrap = () => {
wordWrap.value = !wordWrap.value
if (editorRef.value) {
editorRef.value.focus()
}
}
// 更新标题
const updateTitle = () => {
if (window.SystemSDK && window.SystemSDK.initialized) {
const title = `记事本 - ${currentFileName.value}${hasChanges.value ? ' *' : ''}`
window.SystemSDK.window.setTitle(title)
}
}
// 格式化时间
const formatTime = (date: Date) => {
return date.toLocaleString('zh-CN')
}
// 处理键盘快捷键
const handleKeydown = (event: KeyboardEvent) => {
if (event.ctrlKey) {
switch (event.key) {
case 's':
event.preventDefault()
saveFile()
break
case 'n':
event.preventDefault()
newFile()
break
case 'o':
event.preventDefault()
openFile()
break
}
}
// 更新光标位置
nextTick(() => {
updateCursorPosition()
})
}
onMounted(() => {
// 初始化光标位置
updateCursorPosition()
// 监听编辑器点击和键盘事件
if (editorRef.value) {
editorRef.value.addEventListener('click', updateCursorPosition)
editorRef.value.addEventListener('keyup', updateCursorPosition)
}
})
</script>
<style scoped>
.notepad {
height: 100%;
display: flex;
flex-direction: column;
background: white;
}
.toolbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
flex-wrap: wrap;
gap: 10px;
}
.toolbar-group {
display: flex;
align-items: center;
gap: 8px;
}
.btn {
display: flex;
align-items: center;
gap: 4px;
padding: 6px 12px;
border: 1px solid #dee2e6;
border-radius: 4px;
background: white;
cursor: pointer;
font-size: 12px;
transition: all 0.2s;
}
.btn:hover:not(:disabled) {
background: #e9ecef;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-primary {
background: #007bff;
color: white;
border-color: #007bff;
}
.btn-primary:hover:not(:disabled) {
background: #0056b3;
}
.btn.active {
background: #007bff;
color: white;
}
.font-size-select {
padding: 4px 8px;
border: 1px solid #dee2e6;
border-radius: 4px;
font-size: 12px;
}
.file-info {
display: flex;
align-items: center;
font-size: 12px;
color: #6c757d;
}
.filename {
font-weight: 500;
}
.unsaved {
color: #dc3545;
margin-left: 4px;
}
.editor-container {
flex: 1;
position: relative;
}
.editor {
width: 100%;
height: 100%;
border: none;
padding: 16px;
font-family: 'Courier New', 'Consolas', monospace;
font-size: 16px;
line-height: 1.5;
resize: none;
outline: none;
background: white;
color: #333;
}
.status-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 12px;
background: #f8f9fa;
border-top: 1px solid #e9ecef;
font-size: 11px;
color: #6c757d;
}
.status-left,
.status-right {
display: flex;
gap: 12px;
}
.modal-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;
}
.modal {
background: white;
border-radius: 8px;
padding: 20px;
min-width: 400px;
max-width: 600px;
max-height: 80vh;
overflow: auto;
}
.modal h3 {
margin: 0 0 16px 0;
color: #333;
}
.file-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid #e9ecef;
border-radius: 4px;
}
.file-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid #f8f9fa;
cursor: pointer;
transition: background 0.2s;
}
.file-item:hover {
background: #f8f9fa;
}
.file-item:last-child {
border-bottom: none;
}
.file-icon {
margin-right: 8px;
font-size: 16px;
}
.file-name {
flex: 1;
font-weight: 500;
}
.file-date {
font-size: 11px;
color: #6c757d;
margin-right: 8px;
}
.btn-delete {
background: none;
border: none;
cursor: pointer;
font-size: 14px;
opacity: 0.7;
transition: opacity 0.2s;
}
.btn-delete:hover {
opacity: 1;
}
.modal-actions {
margin-top: 16px;
text-align: right;
}
.icon {
font-size: 14px;
}
</style>

658
src/apps/todo/Todo.vue Normal file
View File

@@ -0,0 +1,658 @@
<template>
<BuiltInApp app-id="todo" title="待办事项">
<div class="todo-app">
<div class="header">
<h1>📝 待办事项</h1>
<div class="stats">
总计: <span class="count">{{ todos.length }}</span> |
已完成: <span class="count">{{ completedCount }}</span> |
待办: <span class="count">{{ pendingCount }}</span>
</div>
</div>
<div class="add-todo">
<div class="input-group">
<input
v-model="newTodoText"
type="text"
class="todo-input"
placeholder="添加新的待办事项..."
maxlength="200"
@keypress.enter="addTodo"
>
<button @click="addTodo" class="add-btn" :disabled="!newTodoText.trim()">
添加
</button>
</div>
</div>
<div class="filters">
<button
v-for="filter in filters"
:key="filter.key"
class="filter-btn"
:class="{ active: currentFilter === filter.key }"
@click="setFilter(filter.key)"
>
{{ filter.label }}
</button>
</div>
<div class="todo-list" v-if="filteredTodos.length > 0">
<TransitionGroup name="todo-list" tag="div">
<div
v-for="todo in filteredTodos"
:key="todo.id"
class="todo-item"
:class="{ 'completed': todo.completed }"
>
<input
type="checkbox"
class="todo-checkbox"
:checked="todo.completed"
@change="toggleTodo(todo.id)"
>
<div class="todo-content" v-if="!todo.editing">
<span class="todo-text" :class="{ 'completed': todo.completed }">
{{ todo.text }}
</span>
<span class="todo-date">{{ formatDate(todo.createdAt) }}</span>
</div>
<div class="todo-edit" v-else>
<input
v-model="todo.editText"
type="text"
class="edit-input"
@keypress.enter="saveEdit(todo)"
@keyup.esc="cancelEdit(todo)"
@blur="saveEdit(todo)"
ref="editInputs"
>
</div>
<div class="todo-actions">
<button
v-if="!todo.editing"
@click="startEdit(todo)"
class="edit-btn"
title="编辑"
>
</button>
<button
@click="deleteTodo(todo.id)"
class="delete-btn"
title="删除"
>
🗑
</button>
</div>
</div>
</TransitionGroup>
</div>
<div v-else class="empty-state">
<div class="empty-icon">📋</div>
<h3>{{ getEmptyMessage() }}</h3>
<p>{{ getEmptyDescription() }}</p>
</div>
<!-- 批量操作 -->
<div class="bulk-actions" v-if="todos.length > 0">
<button @click="markAllCompleted" class="bulk-btn">
全部完成
</button>
<button @click="clearCompleted" class="bulk-btn" v-if="completedCount > 0">
清除已完成 ({{ completedCount }})
</button>
<button @click="clearAll" class="bulk-btn bulk-danger">
清空全部
</button>
</div>
</div>
</BuiltInApp>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, nextTick } from 'vue'
import BuiltInApp from '../components/BuiltInApp.vue'
interface Todo {
id: string
text: string
completed: boolean
createdAt: Date
completedAt?: Date
editing?: boolean
editText?: string
}
type FilterType = 'all' | 'pending' | 'completed'
const newTodoText = ref('')
const todos = ref<Todo[]>([])
const currentFilter = ref<FilterType>('all')
const editInputs = ref<HTMLInputElement[]>([])
const filters = [
{ key: 'all' as FilterType, label: '全部' },
{ key: 'pending' as FilterType, label: '待办' },
{ key: 'completed' as FilterType, label: '已完成' }
]
// 计算属性
const completedCount = computed(() =>
todos.value.filter(todo => todo.completed).length
)
const pendingCount = computed(() =>
todos.value.filter(todo => !todo.completed).length
)
const filteredTodos = computed(() => {
switch (currentFilter.value) {
case 'completed':
return todos.value.filter(todo => todo.completed)
case 'pending':
return todos.value.filter(todo => !todo.completed)
default:
return todos.value
}
})
// 方法
const addTodo = async () => {
const text = newTodoText.value.trim()
if (!text) return
const todo: Todo = {
id: Date.now().toString(),
text,
completed: false,
createdAt: new Date()
}
todos.value.unshift(todo)
newTodoText.value = ''
await saveTodos()
showNotification('✅ 待办事项已添加', `"${text}" 已添加到您的待办列表`)
}
const toggleTodo = async (id: string) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
todo.completedAt = todo.completed ? new Date() : undefined
await saveTodos()
if (todo.completed) {
showNotification('🎉 任务完成', `"${todo.text}" 已完成!`)
}
}
}
const startEdit = (todo: Todo) => {
todo.editing = true
todo.editText = todo.text
nextTick(() => {
const input = editInputs.value?.find(input =>
input && input.closest('.todo-item')?.getAttribute('data-id') === todo.id
)
input?.focus()
input?.select()
})
}
const saveEdit = async (todo: Todo) => {
if (todo.editText && todo.editText.trim() !== todo.text) {
todo.text = todo.editText.trim()
await saveTodos()
}
todo.editing = false
todo.editText = undefined
}
const cancelEdit = (todo: Todo) => {
todo.editing = false
todo.editText = undefined
}
const deleteTodo = async (id: string) => {
if (confirm('确定要删除这个待办事项吗?')) {
todos.value = todos.value.filter(t => t.id !== id)
await saveTodos()
}
}
const setFilter = (filter: FilterType) => {
currentFilter.value = filter
}
const markAllCompleted = async () => {
todos.value.forEach(todo => {
if (!todo.completed) {
todo.completed = true
todo.completedAt = new Date()
}
})
await saveTodos()
showNotification('✅ 全部完成', '所有待办事项已标记为完成')
}
const clearCompleted = async () => {
if (confirm(`确定要删除 ${completedCount.value} 个已完成的待办事项吗?`)) {
todos.value = todos.value.filter(todo => !todo.completed)
await saveTodos()
showNotification('🗑️ 清理完成', '已清除所有已完成的待办事项')
}
}
const clearAll = async () => {
if (confirm('确定要清空所有待办事项吗?此操作不可恢复。')) {
todos.value = []
await saveTodos()
showNotification('🗑️ 清空完成', '所有待办事项已清空')
}
}
const loadTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
const result = await window.SystemSDK.storage.get('todos')
if (result.success && result.data) {
const savedTodos = JSON.parse(result.data)
todos.value = savedTodos.map((todo: any) => ({
...todo,
createdAt: new Date(todo.createdAt),
completedAt: todo.completedAt ? new Date(todo.completedAt) : undefined
}))
}
}
} catch (error) {
console.error('加载待办事项失败:', error)
}
}
const saveTodos = async () => {
try {
if (window.SystemSDK && window.SystemSDK.initialized) {
await window.SystemSDK.storage.set('todos', JSON.stringify(todos.value))
}
} catch (error) {
console.error('保存待办事项失败:', error)
}
}
const showNotification = async (title: string, message: string) => {
try {
if (window.SystemSDK?.ui?.showNotification) {
await window.SystemSDK.ui.showNotification({
title,
body: message,
duration: 3000
})
}
} catch (error) {
console.error('显示通知失败:', error)
}
}
const formatDate = (date: Date) => {
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / (1000 * 60))
const diffHours = Math.floor(diffMs / (1000 * 60 * 60))
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24))
if (diffMins < 1) return '刚刚'
if (diffMins < 60) return `${diffMins}分钟前`
if (diffHours < 24) return `${diffHours}小时前`
if (diffDays < 7) return `${diffDays}天前`
return date.toLocaleDateString('zh-CN')
}
const getEmptyMessage = () => {
switch (currentFilter.value) {
case 'completed':
return '还没有已完成的任务'
case 'pending':
return '没有待办事项'
default:
return '还没有待办事项'
}
}
const getEmptyDescription = () => {
switch (currentFilter.value) {
case 'completed':
return '完成一些任务后,它们会出现在这里'
case 'pending':
return '所有任务都已完成,真棒!'
default:
return '添加一个新的待办事项开始管理您的任务吧!'
}
}
onMounted(async () => {
await loadTodos()
})
</script>
<style scoped>
.todo-app {
height: 100%;
display: flex;
flex-direction: column;
background: #f8f9fa;
}
.header {
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
text-align: center;
}
.header h1 {
font-size: 24px;
margin-bottom: 10px;
font-weight: 600;
}
.stats {
font-size: 14px;
opacity: 0.9;
}
.count {
font-weight: 600;
}
.add-todo {
padding: 20px;
background: white;
border-bottom: 1px solid #e9ecef;
}
.input-group {
display: flex;
gap: 10px;
}
.todo-input {
flex: 1;
padding: 12px 16px;
border: 2px solid #e9ecef;
border-radius: 8px;
font-size: 16px;
outline: none;
transition: border-color 0.3s;
}
.todo-input:focus {
border-color: #667eea;
}
.add-btn {
padding: 12px 24px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: background 0.3s;
font-weight: 500;
}
.add-btn:hover:not(:disabled) {
background: #5a67d8;
}
.add-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.filters {
padding: 0 20px 20px;
display: flex;
gap: 10px;
justify-content: center;
background: white;
}
.filter-btn {
padding: 8px 16px;
border: 1px solid #e9ecef;
background: white;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
font-size: 14px;
}
.filter-btn.active {
background: #667eea;
color: white;
border-color: #667eea;
}
.filter-btn:hover:not(.active) {
background: #f8f9fa;
}
.todo-list {
flex: 1;
overflow-y: auto;
padding: 0 20px;
background: white;
}
.todo-item {
display: flex;
align-items: center;
padding: 16px;
margin-bottom: 10px;
background: white;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: all 0.3s ease;
border-left: 4px solid #667eea;
}
.todo-item:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
.todo-item.completed {
opacity: 0.7;
border-left-color: #28a745;
}
.todo-checkbox {
margin-right: 12px;
width: 20px;
height: 20px;
cursor: pointer;
accent-color: #28a745;
}
.todo-content {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.todo-text {
font-size: 16px;
transition: color 0.3s;
line-height: 1.4;
}
.todo-text.completed {
text-decoration: line-through;
color: #6c757d;
}
.todo-date {
font-size: 12px;
color: #6c757d;
}
.todo-edit {
flex: 1;
margin-right: 12px;
}
.edit-input {
width: 100%;
padding: 8px 12px;
border: 2px solid #667eea;
border-radius: 4px;
font-size: 16px;
outline: none;
}
.todo-actions {
display: flex;
gap: 8px;
}
.edit-btn,
.delete-btn {
background: none;
border: none;
cursor: pointer;
font-size: 16px;
opacity: 0.7;
transition: opacity 0.2s;
padding: 4px;
}
.edit-btn:hover,
.delete-btn:hover {
opacity: 1;
}
.empty-state {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
color: #6c757d;
background: white;
padding: 40px;
}
.empty-icon {
font-size: 64px;
margin-bottom: 16px;
}
.empty-state h3 {
margin-bottom: 8px;
color: #495057;
}
.bulk-actions {
padding: 20px;
background: white;
border-top: 1px solid #e9ecef;
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}
.bulk-btn {
padding: 8px 16px;
border: 1px solid #dee2e6;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.bulk-btn:hover {
background: #f8f9fa;
}
.bulk-danger {
color: #dc3545;
border-color: #dc3545;
}
.bulk-danger:hover {
background: #dc3545;
color: white;
}
/* 动画 */
.todo-list-enter-active,
.todo-list-leave-active {
transition: all 0.3s ease;
}
.todo-list-enter-from {
opacity: 0;
transform: translateY(20px);
}
.todo-list-leave-to {
opacity: 0;
transform: translateX(-100%);
}
.todo-list-move {
transition: transform 0.3s ease;
}
/* 响应式设计 */
@media (max-width: 768px) {
.header {
padding: 15px;
}
.header h1 {
font-size: 20px;
}
.add-todo {
padding: 15px;
}
.input-group {
flex-direction: column;
}
.filters {
padding: 0 15px 15px;
flex-wrap: wrap;
}
.todo-list {
padding: 0 15px;
}
.todo-item {
padding: 12px;
}
.bulk-actions {
padding: 15px;
}
}
</style>

View File

@@ -0,0 +1,35 @@
/**
* 内置应用清单接口
*/
export interface InternalAppManifest {
id: string
name: string
version: string
description: string
author: string
icon: string
permissions: string[]
window: {
width: number
height: number
minWidth?: number
minHeight?: number
maxWidth?: number
maxHeight?: number
resizable?: boolean
minimizable?: boolean
maximizable?: boolean
closable?: boolean
}
category?: string
keywords?: string[]
}
/**
* 应用注册信息
*/
export interface AppRegistration {
manifest: InternalAppManifest
component: any // Vue组件
isBuiltIn: boolean
}

View File

@@ -1,34 +0,0 @@
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
import { NotificationService } from '@/core/service/services/NotificationService.ts'
import { SettingsService } from '@/core/service/services/SettingsService.ts'
import { WindowFormService } from '@/core/service/services/WindowFormService.ts'
import { UserService } from '@/core/service/services/UserService.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
export default class XSystem {
private static _instance: XSystem = new XSystem()
private _desktopRootDom: HTMLElement;
constructor() {
console.log('XSystem')
new NotificationService()
new SettingsService()
new WindowFormService()
new UserService()
}
public static get instance() {
return this._instance
}
public get desktopRootDom() {
return this._desktopRootDom
}
public async initialization(dom: HTMLDivElement) {
this._desktopRootDom = dom
await processManager.runProcess('basic-system', BasicSystemProcess)
await processManager.runProcess('desktop', DesktopProcess, dom)
}
}

View File

@@ -1,18 +0,0 @@
{
"name": "department",
"title": "部门",
"description": "部门",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "部门",
"icon": "iconfont icon-setting",
"width": 200,
"height": 100
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
部门页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "fileManage",
"title": "文件管理",
"description": "文件管理",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "文件管理",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
文件管理页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "music",
"title": "音乐",
"description": "音乐",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "音乐",
"icon": "iconfont icon-setting",
"width": 200,
"height": 100
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
音乐页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "personalCenter",
"title": "个人中心",
"description": "个人中心",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "个人中心",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
个人中心页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "photograph",
"title": "照片",
"description": "照片",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "照片",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
照片页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "recycleBin",
"title": "回收站",
"description": "回收站",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "回收站",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
回收站页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "setting",
"title": "设置",
"description": "设置",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "设置",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
设置APP页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "tv",
"title": "电视",
"description": "电视",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "电视",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
电视页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,18 +0,0 @@
{
"name": "video",
"title": "电影",
"description": "电影",
"icon": "iconfont icon-setting",
"startName": "main",
"singleton": true,
"isJustProcess": false,
"windowFormConfigs": [
{
"name": "main",
"title": "电影",
"icon": "iconfont icon-setting",
"width": 800,
"height": 600
}
]
}

View File

@@ -1,9 +0,0 @@
<template>
<div>
电影页面
</div>
</template>
<script setup lang="ts"></script>
<style scoped></style>

View File

@@ -1,87 +0,0 @@
import { reactive, onBeforeUnmount, type Reactive } from 'vue'
import type { IObservable } from '@/core/state/IObservable.ts'
/**
* Vue Hook: useObservable
* 支持深层解构赋值,直接修改触发 ObservableImpl 通知 + Vue 响应式更新
* @example
* interface AppState {
* count: number
* user: { name: string; age: number }
* items: number[]
* }
*
* // 创建 ObservableImpl
* const obs = new ObservableImpl<AppState>({
* count: 0,
* user: { name: 'Alice', age: 20 },
* items: []
* })
*
* export default defineComponent({
* setup() {
* // 深层解构 Hook
* const { count, user, items } = useObservable(obs)
*
* const increment = () => {
* count += 1 // 触发 ObservableImpl 通知 + Vue 更新
* }
*
* const changeAge = () => {
* user.age = 30 // 深层对象也能触发通知
* }
*
* const addItem = () => {
* items.push(42) // 数组方法拦截,触发通知
* }
*
* return { count, user, items, increment, changeAge, addItem }
* }
* })
*/
export function useObservable<T extends object>(observable: IObservable<T>): Reactive<T> {
// 创建 Vue 响应式对象
const state = reactive({} as T)
/**
* 将 ObservableImpl Proxy 映射到 Vue 响应式对象
* 递归支持深层对象
*/
function mapKeys(obj: any, proxy: any) {
(Object.keys(proxy) as (keyof typeof proxy)[]).forEach(key => {
const value = proxy[key]
if (typeof value === 'object' && value !== null) {
// 递归创建子对象 Proxy
obj[key] = reactive({} as typeof value)
mapKeys(obj[key], value)
} else {
// 基本类型通过 getter/setter 同步
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
return proxy[key]
},
set(val) {
proxy[key] = val
},
})
}
})
}
// 获取 ObservableImpl 的 Proxy
const refsProxy = observable.toRefsProxy()
mapKeys(state, refsProxy)
// 订阅 ObservableImpl保持响应式同步
const unsubscribe = observable.subscribe(() => {
// 空实现即可getter/setter 已同步
})
onBeforeUnmount(() => {
unsubscribe()
})
return state
}

View File

@@ -1,10 +0,0 @@
import {
create,
NButton,
NCard,
NConfigProvider,
} from 'naive-ui'
export const naiveUi = create({
components: [NButton, NCard, NConfigProvider]
})

View File

@@ -1,21 +0,0 @@
import { createDiscreteApi } from 'naive-ui'
import { configProviderProps } from './theme.ts'
const { message, notification, dialog, loadingBar, modal } = createDiscreteApi(
['message', 'dialog', 'notification', 'loadingBar', 'modal'],
{
configProviderProps: configProviderProps,
notificationProviderProps: {
placement: 'bottom-right',
max: 3
}
}
)
export const { messageApi, notificationApi, dialogApi, loadingBarApi, modalApi } = {
messageApi: message,
notificationApi: notification,
dialogApi: dialog,
loadingBarApi: loadingBar,
modalApi: modal
}

View File

@@ -1,15 +0,0 @@
import { type ConfigProviderProps, darkTheme, dateZhCN, type GlobalTheme, lightTheme, zhCN } from 'naive-ui'
const lTheme: GlobalTheme = {
...lightTheme,
common: {
...lightTheme.common,
primaryColor: '#0070f3'
}
}
export const configProviderProps: ConfigProviderProps = {
theme: lTheme,
dateLocale: dateZhCN,
locale: zhCN,
}

View File

@@ -1,8 +0,0 @@
/**
* 可销毁接口
* 销毁实例,清理副作用,让内存可以被回收
*/
export interface IDestroyable {
/** 销毁实例,清理副作用,让内存可以被回收 */
destroy(): void
}

View File

@@ -1,29 +0,0 @@
/**
* 版本信息
*/
export interface IVersion {
/**
* 公司名称
*/
company: string
/**
* 版本号
*/
major: number
/**
* 子版本号
*/
minor: number
/**
* 修订号
*/
build: number
/**
* 私有版本号
*/
private: number
}

View File

@@ -1,82 +0,0 @@
import ProcessImpl from '@/core/process/impl/ProcessImpl.ts'
import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts'
import { createApp, h } from 'vue'
import DesktopComponent from '@/core/desktop/ui/DesktopComponent.vue'
import { naiveUi } from '@/core/common/naive-ui/components.ts'
import { debounce } from 'lodash'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
import './ui/DesktopElement.ts'
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
interface IDesktopDataState {
/** 显示器宽度 */
monitorWidth: number;
/** 显示器高度 */
monitorHeight: number;
}
export class DesktopProcess extends ProcessImpl {
/** 桌面根dom类似显示器 */
private readonly _monitorDom: HTMLElement
private _isMounted: boolean = false
private _data = new ObservableImpl<IDesktopDataState>({
monitorWidth: 0,
monitorHeight: 0,
})
public get monitorDom() {
return this._monitorDom
}
public get isMounted() {
return this._isMounted
}
public get basicSystemProcess() {
return processManager.findProcessByName<BasicSystemProcess>('basic-system')
}
get data() {
return this._data
}
constructor(info: IProcessInfo, dom: HTMLDivElement) {
super(info)
console.log('DesktopProcess')
dom.style.position = 'relative'
dom.style.overflow = 'hidden'
dom.style.width = `${window.innerWidth}px`
dom.style.height = `${window.innerHeight}px`
this._monitorDom = dom
this._data.state.monitorWidth = window.innerWidth
this._data.state.monitorHeight = window.innerHeight
window.addEventListener('resize', this.onResize)
this.createDesktopUI()
}
private onResize = debounce(() => {
this._monitorDom.style.width = `${window.innerWidth}px`
this._monitorDom.style.height = `${window.innerHeight}px`
this._data.state.monitorWidth = window.innerWidth
this._data.state.monitorHeight = window.innerHeight
}, 300)
private createDesktopUI() {
if (this._isMounted) return
const app = createApp(DesktopComponent, { process: this })
app.use(naiveUi)
app.mount(this._monitorDom)
this._isMounted = true
}
private initDesktop(dom: HTMLDivElement) {
const d = document.createElement('desktop-element')
dom.appendChild(d)
}
override destroy() {
super.destroy()
window.removeEventListener('resize', this.onResize)
}
}

View File

@@ -1,15 +0,0 @@
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
export const DesktopProcessInfo = new ProcessInfoImpl({
name: 'desktop',
title: '桌面',
version: {
company: 'XZG',
major: 1,
minor: 0,
build: 0,
private: 0
},
singleton: true,
isJustProcess: true
})

View File

@@ -1,15 +0,0 @@
/**
* 桌面应用图标信息
*/
export interface IDesktopAppIcon {
/** 图标name */
name: string;
/** 图标 */
icon: string;
/** 图标路径 */
path: string;
/** 图标在grid布局中的列 */
x: number;
/** 图标在grid布局中的行 */
y: number;
}

View File

@@ -1,21 +0,0 @@
/**
* 桌面网格模板参数
*/
export interface IGridTemplateParams {
/** 单元格预设宽度 */
readonly cellExpectWidth: number
/** 单元格预设高度 */
readonly cellExpectHeight: number
/** 单元格实际宽度 */
cellRealWidth: number
/** 单元格实际高度 */
cellRealHeight: number
/** 列间距 */
gapX: number
/** 行间距 */
gapY: number
/** 总列数 */
colCount: number
/** 总行数 */
rowCount: number
}

View File

@@ -1,95 +0,0 @@
<template>
<n-config-provider
:config-provider-props="configProviderProps"
class="w-full h-full pos-relative"
>
<div class="desktop-root" @contextmenu="onContextMenu">
<div class="desktop-bg">
<div class="desktop-icons-container" :style="gridStyle">
<AppIcon
v-for="(appIcon, i) in appIconsRef"
:key="i"
:iconInfo="appIcon"
:gridTemplate="gridTemplate"
@dblclick="runApp(appIcon)"
/>
</div>
</div>
<div class="task-bar">
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">
测试
</div>
</div>
</div>
</n-config-provider>
</template>
<script setup lang="ts">
import type { DesktopProcess } from '@/core/desktop/DesktopProcess.ts'
import { notificationApi } from '@/core/common/naive-ui/discrete-api.ts'
import { configProviderProps } from '@/core/common/naive-ui/theme.ts'
import { useDesktopInit } from '@/core/desktop/ui/hooks/useDesktopInit.ts'
import AppIcon from '@/core/desktop/ui/components/AppIcon.vue'
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import { eventManager } from '@/core/events/EventManager.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
const props = defineProps<{ process: DesktopProcess }>()
props.process.data.subscribeKey(['monitorWidth', 'monitorHeight'], ({monitorWidth, monitorHeight}) => {
console.log('onDesktopRootDomResize', monitorWidth, monitorHeight)
notificationApi.create({
title: '桌面通知',
content: `桌面尺寸变化${monitorWidth}x${monitorHeight}}`,
duration: 2000,
})
})
// props.process.data.subscribe((data) => {
// console.log('desktopData', data.monitorWidth)
// })
const { appIconsRef, gridStyle, gridTemplate } = useDesktopInit('.desktop-icons-container')
// eventManager.addEventListener('onDesktopRootDomResize', (width, height) => {
// console.log(width, height)
// notificationApi.create({
// title: '桌面通知',
// content: `桌面尺寸变化${width}x${height}}`,
// duration: 2000,
// })
// })
const onContextMenu = (e: MouseEvent) => {
e.preventDefault()
}
const runApp = (appIcon: IDesktopAppIcon) => {
processManager.runProcess(appIcon.name)
}
</script>
<style lang="scss" scoped>
$taskBarHeight: 40px;
.desktop-root {
@apply w-full h-full flex flex-col;
.desktop-bg {
@apply w-full h-full flex-1 p-2 pos-relative;
background-image: url('imgs/desktop-bg-2.jpeg');
background-repeat: no-repeat;
background-size: cover;
height: calc(100% - #{$taskBarHeight});
}
.desktop-icons-container {
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
}
.task-bar {
@apply w-full bg-gray-200 flex justify-center items-center;
height: $taskBarHeight;
flex-shrink: 0;
}
}
</style>

View File

@@ -1,35 +0,0 @@
import { css, html, LitElement, unsafeCSS } from 'lit'
import { customElement } from 'lit/decorators.js'
import desktopStyle from './css/desktop.scss?inline'
@customElement('desktop-element')
export class DesktopElement extends LitElement {
static override styles = css`
${unsafeCSS(desktopStyle)}
`
private onContextMenu = (e: MouseEvent) => {
e.preventDefault()
e.stopPropagation()
console.log('contextmenu')
}
override render() {
return html`
<div class="desktop-root" @contextmenu=${this.onContextMenu}>
<div class="desktop-container">
<div class="desktop-icons-container"
:style="gridStyle">
<AppIcon v-for="(appIcon, i) in appIconsRef" :key="i"
:iconInfo="appIcon" :gridTemplate="gridTemplate"
@dblclick="runApp(appIcon)"
/>
</div>
</div>
<div class="task-bar">
<div id="taskbar" class="w-[80px] h-full flex justify-center items-center bg-blue">测试</div>
</div>
</div>
`
}
}

View File

@@ -1,55 +0,0 @@
<template>
<div
class="icon-container"
:style="`grid-column: ${iconInfo.x}/${iconInfo.x + 1};grid-row: ${iconInfo.y}/${iconInfo.y + 1};`"
draggable="true"
@dragstart="onDragStart"
@dragend="onDragEnd"
>
{{ iconInfo.name }}
</div>
</template>
<script setup lang="ts">
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
import { eventManager } from '@/core/events/EventManager.ts'
const { iconInfo, gridTemplate } = defineProps<{ iconInfo: IDesktopAppIcon, gridTemplate: IGridTemplateParams }>()
const onDragStart = (e: DragEvent) => {}
const onDragEnd = (e: DragEvent) => {
const el = e.target as HTMLElement | null
if (!el) return
// 鼠标所在位置已存在图标元素
const pointTarget = document.elementFromPoint(e.clientX, e.clientY)
if (!pointTarget) return
if (pointTarget.classList.contains('icon-container')) return
if (!pointTarget.classList.contains('desktop-icons-container')) return
// 获取容器边界
const rect = el.parentElement!.getBoundingClientRect()
// 鼠标相对容器左上角坐标
const mouseX = e.clientX - rect.left
const mouseY = e.clientY - rect.top
// 计算鼠标所在单元格坐标向上取整从1开始
const gridX = Math.ceil(mouseX / gridTemplate.cellRealWidth)
const gridY = Math.ceil(mouseY / gridTemplate.cellRealHeight)
iconInfo.x = gridX
iconInfo.y = gridY
eventManager.notifyEvent('onDesktopAppIconPos', iconInfo)
}
</script>
<style scoped lang="scss">
.icon-container {
width: 100%;
height: 100%;
@apply flex flex-col items-center justify-center bg-gray-200;
}
</style>

View File

@@ -1,17 +0,0 @@
import { css, html, LitElement } from 'lit'
export class DesktopAppIconElement extends LitElement {
static override styles = css`
:host {
width: 100%;
height: 100%;
@apply flex flex-col items-center justify-center bg-gray-200;
}
`
override render() {
return html`<div class="desktop-app-icon">
<slot></slot>
</div>`
}
}

View File

@@ -1,31 +0,0 @@
*,
*::before,
*::after {
box-sizing: border-box; /* 使用更直观的盒模型 */
margin: 0;
padding: 0;
}
$taskBarHeight: 40px;
.desktop-root {
@apply w-full h-full flex flex-col;
.desktop-container {
@apply w-full h-full flex-1 p-2 pos-relative;
background-image: url("../imgs/desktop-bg-2.jpeg");
background-repeat: no-repeat;
background-size: cover;
height: calc(100% - #{$taskBarHeight});
}
.desktop-icons-container {
@apply w-full h-full flex-1 grid grid-auto-flow-col pos-relative;
}
.task-bar {
@apply w-full bg-gray-200 flex justify-center items-center;
height: $taskBarHeight;
flex-shrink: 0;
}
}

View File

@@ -1,169 +0,0 @@
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
import {
computed,
nextTick,
onMounted,
onUnmounted,
reactive,
ref,
toRaw,
toRefs,
toValue,
useTemplateRef,
watch,
watchEffect,
} from 'vue'
import type { IGridTemplateParams } from '@/core/desktop/types/IGridTemplateParams.ts'
import { eventManager } from '@/core/events/EventManager.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
export function useDesktopInit(containerStr: string) {
let container:HTMLElement
// 初始值
const gridTemplate = reactive<IGridTemplateParams>({
cellExpectWidth: 90,
cellExpectHeight: 110,
cellRealWidth: 90,
cellRealHeight: 110,
gapX: 4,
gapY: 4,
colCount: 1,
rowCount: 1
})
const ro = new ResizeObserver(entries => {
const entry= entries[0]
const containerRect = entry.contentRect
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))
gridTemplate.cellRealWidth = Number((w / gridTemplate.colCount).toFixed(2))
gridTemplate.cellRealHeight = Number((h / gridTemplate.rowCount).toFixed(2))
})
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`
}))
onMounted(() => {
container = document.querySelector(containerStr)!
ro.observe(container)
})
onUnmounted(() => {
ro.unobserve(container)
ro.disconnect()
})
// 有桌面图标的app
const appInfos = processManager.processInfos.filter(processInfo => !processInfo.isJustProcess)
const oldAppIcons: IDesktopAppIcon[] = JSON.parse(localStorage.getItem('desktopAppIconInfo') || '[]')
const appIcons: IDesktopAppIcon[] = 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
return {
name: processInfo.name,
icon: processInfo.icon,
path: processInfo.startName,
x: oldAppIcon ? oldAppIcon.x : x,
y: oldAppIcon ? oldAppIcon.y : y
}
})
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
})
eventManager.addEventListener('onDesktopAppIconPos', (iconInfo) => {
localStorage.setItem('desktopAppIconInfo', JSON.stringify(toValue(appIconsRef.value)))
})
return {
gridTemplate,
appIconsRef,
gridStyle
}
}
/**
* 重新安排图标位置
* @param appIconInfos 图标信息
* @param maxCol 列数
* @param maxRow 行数
*/
function rearrangeIcons(
appIconInfos: IDesktopAppIcon[],
maxCol: number,
maxRow: number
): IRearrangeInfo {
const occupied = new Set<string>();
function key(x: number, y: number) {
return `${x},${y}`;
}
const appIcons: IDesktopAppIcon[] = []
const hideAppIcons: IDesktopAppIcon[] = []
const temp: IDesktopAppIcon[] = []
for (const appIcon of appIconInfos) {
const { x, y } = appIcon;
if (x <= maxCol && y <= maxRow) {
if (!occupied.has(key(x, y))) {
occupied.add(key(x, y))
appIcons.push({ ...appIcon, x, y })
}
} else {
temp.push(appIcon)
}
}
const max = maxCol * maxRow
for (const appIcon of temp) {
if (appIcons.length < max) {
// 最后格子也被占 → 从 (1,1) 开始找空位
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;
}
}
if (placed) break;
}
} else {
// 放不下了
hideAppIcons.push(appIcon)
}
}
return {
appIcons,
hideAppIcons
};
}
interface IRearrangeInfo {
/** 正常的桌面图标信息 */
appIcons: IDesktopAppIcon[];
/** 隐藏的桌面图标信息(超出屏幕显示的) */
hideAppIcons: IDesktopAppIcon[];
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

View File

@@ -1,64 +0,0 @@
import { useDraggable } from '@vueuse/core'
import { ref } from 'vue'
export function useIconDrag(el: HTMLElement, container: HTMLElement) {
let offsetX = 0
let offsetY = 0
let containerRect = container.getBoundingClientRect()
el.addEventListener('mousedown', (e) => {
el.classList.add('dragging')
let rect = el.getBoundingClientRect()
console.log(rect)
offsetX = e.clientX - rect.left
offsetY = e.clientY - rect.top
// 临时脱离 grid用绝对定位移动
el.style.position = "absolute";
el.style.left = rect.left - containerRect.left + "px";
el.style.top = rect.top - containerRect.top + "px";
el.style.gridRow = "auto";
el.style.gridColumn = "auto";
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", onMouseUp);
})
function onMouseMove(e: MouseEvent) {
if (!el) return;
el.style.left = e.clientX - containerRect.left - offsetX + "px";
el.style.top = e.clientY - containerRect.top - offsetY + "px";
}
function onMouseUp(e: MouseEvent) {
if (!el) return;
const cellWidth = 90 + 16; // 图标宽度 + gap
const cellHeight = 110 + 16;
// 计算所在行列
let col = Math.round((e.clientX - containerRect.left) / cellWidth) + 1;
let row = Math.round((e.clientY - containerRect.top) / cellHeight) + 1;
// 限制在 grid 内
const maxCols = Math.floor(containerRect.width / cellWidth);
const maxRows = Math.floor(containerRect.height / cellHeight);
col = Math.max(1, Math.min(maxCols, col));
row = Math.max(1, Math.min(maxRows, row));
console.log(col, row)
// 放回 grid
el.style.position = "relative";
el.style.left = "";
el.style.top = "";
el.style.gridRow = `${row}`;
el.style.gridColumn = `${col}`;
el.classList.remove("dragging");
document.removeEventListener("mousemove", onMouseMove);
document.removeEventListener("mouseup", onMouseUp);
}
}

View File

@@ -1,35 +0,0 @@
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
import type { IDesktopAppIcon } from '@/core/desktop/types/IDesktopAppIcon.ts'
export const eventManager = new EventBuilderImpl<IAllEvent>()
/**
* 系统进程的事件
* @description
* <p>onAuthChange - 认证状态改变</p>
* <p>onThemeChange - 主题改变</p>
*/
export interface IBasicSystemEvent extends IEventMap {
/** 认证状态改变 */
onAuthChange: () => {},
/** 主题改变 */
onThemeChange: (theme: string) => void
}
/**
* 桌面进程的事件
* @description
* <p>onDesktopRootDomResize - 桌面根dom尺寸改变</p>
* <p>onDesktopProcessInitialize - 桌面进程初始化完成</p>
*/
export interface IDesktopEvent extends IEventMap {
/** 桌面根dom尺寸改变 */
onDesktopRootDomResize: (width: number, height: number) => void
/** 桌面进程初始化完成 */
onDesktopProcessInitialize: () => void
/** 桌面应用图标位置改变 */
onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void
}
export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {}

View File

@@ -1,47 +0,0 @@
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
/**
* 事件定义
* @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
*/
export interface IEventMap {
[key: string]: (...args: any[]) => void
}
/**
* 事件管理器接口定义
*/
export interface IEventBuilder<Events extends IEventMap> extends IDestroyable {
/**
* 添加事件监听
* @param eventName 事件名称
* @param handler 事件处理函数
* @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
* @returns void
*/
addEventListener<E extends keyof Events, F extends Events[E]>(
eventName: E,
handler: F,
options?: {
immediate?: boolean
immediateArgs?: Parameters<F>
once?: boolean
},
): void
/**
* 移除事件监听
* @param eventName 事件名称
* @param handler 事件处理函数
* @returns void
*/
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F): void
/**
* 触发事件
* @param eventName 事件名称
* @param args 参数
* @returns void
*/
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>): void
}

View File

@@ -1,61 +0,0 @@
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
/**
* 窗口的事件
*/
export interface WindowFormEvent extends IEventMap {
/**
* 窗口最小化
* @param id 窗口id
*/
windowFormMinimize: (id: string) => void;
/**
* 窗口最大化
* @param id 窗口id
*/
windowFormMaximize: (id: string) => void;
/**
* 窗口还原
* @param id 窗口id
*/
windowFormRestore: (id: string) => void;
/**
* 窗口关闭
* @param id 窗口id
*/
windowFormClose: (id: string) => void;
/**
* 窗口聚焦
* @param id 窗口id
*/
windowFormFocus: (id: string) => void;
/**
* 窗口数据更新
* @param data 窗口数据
*/
windowFormDataUpdate: (data: IWindowFormDataUpdateParams) => void;
/**
* 窗口创建完成
*/
windowFormCreated: () => void;
}
interface IWindowFormDataUpdateParams {
/** 窗口id */
id: string;
/** 窗口状态 */
state: TWindowFormState,
/** 窗口宽度 */
width: number,
/** 窗口高度 */
height: number,
/** 窗口x坐标(左上角) */
x: number,
/** 窗口y坐标(左上角) */
y: number
}
/** 窗口事件管理器 */
export const wfem = new EventBuilderImpl<WindowFormEvent>()

View File

@@ -1,96 +0,0 @@
import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts'
interface HandlerWrapper<T extends (...args: any[]) => any> {
fn: T
once: boolean
}
export class EventBuilderImpl<Events extends IEventMap> implements IEventBuilder<Events> {
private _eventHandlers = new Map<keyof Events, Set<HandlerWrapper<Events[keyof Events]>>>()
/**
* 添加事件监听器
* @param eventName 事件名称
* @param handler 监听器
* @param options { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
* @example
* eventBus.addEventListener('noArgs', () => {})
* eventBus.addEventListener('greet', name => {}, { immediate: true, immediateArgs: ['abc'] })
* eventBus.addEventListener('onResize', (w, h) => {}, { immediate: true, immediateArgs: [1, 2] })
*/
addEventListener<E extends keyof Events, F extends Events[E]>(
eventName: E,
handler: F,
options?: {
immediate?: boolean
immediateArgs?: Parameters<F>
once?: boolean
},
) {
if (!handler) return
if (!this._eventHandlers.has(eventName)) {
this._eventHandlers.set(eventName, new Set<HandlerWrapper<F>>())
}
const set = this._eventHandlers.get(eventName)!
if (![...set].some((wrapper) => wrapper.fn === handler)) {
set.add({ fn: handler, once: options?.once ?? false })
}
if (options?.immediate) {
try {
handler(...(options.immediateArgs ?? []))
} catch (e) {
console.error(e)
}
}
}
/**
* 移除事件监听器
* @param eventName 事件名称
* @param handler 监听器
* @example
* eventBus.removeEventListener('noArgs', () => {})
*/
removeEventListener<E extends keyof Events, F extends Events[E]>(eventName: E, handler: F) {
const set = this._eventHandlers.get(eventName)
if (!set) return
for (const wrapper of set) {
if (wrapper.fn === handler) {
set.delete(wrapper)
}
}
}
/**
* 通知事件
* @param eventName 事件名称
* @param args 参数
* @example
* eventBus.notifyEvent('noArgs')
* eventBus.notifyEvent('greet', 'Alice')
* eventBus.notifyEvent('onResize', 1, 2)
*/
notifyEvent<E extends keyof Events, F extends Events[E]>(eventName: E, ...args: Parameters<F>) {
if (!this._eventHandlers.has(eventName)) return
const set = this._eventHandlers.get(eventName)!
for (const wrapper of set) {
try {
wrapper.fn(...args)
} catch (e) {
console.error(e)
}
if (wrapper.once) {
set.delete(wrapper)
}
}
}
destroy() {
this._eventHandlers.clear()
}
}

View File

@@ -1,28 +0,0 @@
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
/**
* 进程接口
*/
export interface IProcess extends IDestroyable {
/** 进程id */
get id(): string;
/** 进程信息 */
get processInfo(): IProcessInfo;
/** 进程的窗体列表 */
get windowForms(): Map<string, IWindowForm>;
get event(): IEventBuilder<IProcessEvent>;
/**
* 打开窗体
* @param startName 窗体启动名
*/
openWindowForm(startName: string): void;
/**
* 关闭窗体
* @param id 窗体id
*/
closeWindowForm(id: string): void;
}

View File

@@ -1,26 +0,0 @@
import type { IVersion } from '@/core/common/types/IVersion.ts'
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
/**
* 进程的描述信息
*/
export interface IProcessInfo {
/** 进程名称 - 唯一 */
get name(): string;
/** 进程标题 */
get title(): string;
/** 进程描述 */
get description(): string;
/** 进程图标 */
get icon(): string;
/** 启动窗体名称 */
get startName(): string;
/** 进程版本 */
get version(): IVersion;
/** 是否单例进程 */
get singleton(): boolean;
/** 是否仅进程 */
get isJustProcess(): boolean;
/** 进程的窗体配置 */
get windowFormConfigs(): IWindowFormConfig[];
}

View File

@@ -1,41 +0,0 @@
import type { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
/**
* 进程管理
*/
export interface IProcessManager {
/** 所有进程信息 */
get processInfos(): IProcessInfo[];
/**
* 注册进程
* @param process 进程
*/
registerProcess(process: IProcess): void;
/**
* 通过进程id查找进程
* @param id 进程id
*/
findProcessById(id: string): IProcess | undefined;
/**
* 通过进程名查找进程
* @param name 进程名
*/
findProcessByName<T extends IProcess = IProcess>(name: string): T | undefined;
/**
* 通过进程id删除进程
* @param id 进程id
*/
removeProcess(id: string): void;
/**
* 通过进程对象删除进程
* @param process 进程对象
*/
removeProcess(process: IProcess): void;
/**
* 通过进程名查找进程信息
* @param name 进程名
*/
findProcessInfoByName(name: string): IProcessInfo | undefined;
}

View File

@@ -1,3 +0,0 @@
import ProcessManagerImpl from '@/core/process/impl/ProcessManagerImpl.ts'
export const processManager = new ProcessManagerImpl();

View File

@@ -1,83 +0,0 @@
import { v4 as uuidV4 } from 'uuid';
import WindowFormImpl from '../../window/impl/WindowFormImpl.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
/**
* 进程
*/
export default class ProcessImpl implements IProcess {
private readonly _id: string = uuidV4();
private readonly _processInfo: IProcessInfo;
// 当前进程的窗体集合
private _windowForms: Map<string, IWindowForm> = new Map();
private _event: IEventBuilder<IProcessEvent> = new EventBuilderImpl<IProcessEvent>()
public get id() {
return this._id;
}
public get processInfo() {
return this._processInfo;
}
public get windowForms() {
return this._windowForms;
}
public get event() {
return this._event;
}
constructor(info: IProcessInfo) {
console.log(`AppProcess: ${info.name}`)
this._processInfo = info;
const startName = info.startName;
this.initEvent();
processManager.registerProcess(this);
// 通过设置 isJustProcess 为 true则不会创建窗体
if (!info.isJustProcess) {
this.openWindowForm(startName)
}
}
private initEvent() {
this.event.addEventListener('processWindowFormExit', (id: string) => {
this.windowForms.delete(id)
if(this.windowForms.size === 0) {
processManager.removeProcess(this)
}
})
}
public openWindowForm(startName: string) {
const info = this._processInfo.windowFormConfigs.find(item => item.name === startName);
if (!info) throw new Error(`未找到窗体:${startName}`);
const wf = new WindowFormImpl(this, info);
this._windowForms.set(wf.id, wf);
}
public closeWindowForm(id: string) {
try {
const wf = this._windowForms.get(id);
if (!wf) throw new Error(`未找到窗体:${id}`);
wf.destroy();
this.windowForms.delete(id)
if(this.windowForms.size === 0) {
this.destroy()
processManager.removeProcess(this)
}
} catch (e) {
console.log('关闭窗体失败', e)
}
}
public destroy() {
this._event.destroy()
}
}

View File

@@ -1,101 +0,0 @@
import type { IVersion } from '../../common/types/IVersion.ts'
import type { IAppProcessInfoParams } from '../types/IAppProcessInfoParams.ts'
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
export class ProcessInfoImpl implements IProcessInfo {
/**
* 应用进程名称
* @private
*/
private readonly _name: string;
/**
* 应用进程标题
* @private
*/
private readonly _title: string;
/**
* 应用进程描述
* @private
*/
private readonly _description: string;
/**
* 应用进程图标
* @private
*/
private readonly _icon: string;
/**
* 应用进程启动入口
* 对应windowFrom参数name
* @private
*/
private readonly _startName: string;
/**
* 应用版本信息
* @private
*/
private readonly _version: IVersion;
/**
* 应用是否只存在一个进程
* @private
*/
private readonly _singleton: boolean;
/**
* 是否只是一个进程
* @private
*/
private readonly _isJustProcess: boolean;
/**
* 进程所有的窗口配置信息
* @private
*/
private readonly _windowFormConfigs: Array<IWindowFormConfig>;
constructor(info: IAppProcessInfoParams) {
this._name = info.name;
this._title = info.title || '';
this._description = info.description || '';
this._icon = <string> info.icon;
this._startName = info.startName || '';
this._version = info.version || { company: 'XZG', major: 1, minor: 0, build: 0, private: 0 };
this._singleton = info.singleton;
this._isJustProcess = info.isJustProcess;
this._windowFormConfigs = info.windowFormConfigs || [];
}
public get name() {
return this._name;
}
public get title() {
return this._title;
}
public get description() {
return this._description;
}
public get icon() {
return this._icon;
}
public get startName() {
return this._startName;
}
public get version() {
return this._version;
}
public get singleton() {
return this._singleton;
}
public get isJustProcess() {
return this._isJustProcess;
}
public get windowFormConfigs() {
return this._windowFormConfigs;
}
}

View File

@@ -1,107 +0,0 @@
import ProcessImpl from './ProcessImpl.ts'
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
import { BasicSystemProcessInfo } from '@/core/system/BasicSystemProcessInfo.ts'
import { DesktopProcessInfo } from '@/core/desktop/DesktopProcessInfo.ts'
import type { IAppProcessInfoParams } from '@/core/process/types/IAppProcessInfoParams.ts'
import type { IProcessManager } from '@/core/process/IProcessManager.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import { processManager } from '@/core/process/ProcessManager.ts'
import { isUndefined } from 'lodash'
/**
* 进程管理
*/
export default class ProcessManagerImpl implements IProcessManager {
private _processPool: Map<string, IProcess> = new Map<string, IProcess>();
private _processInfos: IProcessInfo[] = new Array<ProcessInfoImpl>();
public get processInfos() {
return this._processInfos;
}
constructor() {
console.log('ProcessManageImpl')
this.loadAppProcessInfos();
}
// TODO 加载所有进程信息
private loadAppProcessInfos() {
console.log('加载所有进程信息')
// 添加内置进程
const apps = import.meta.glob<IAppProcessInfoParams>('../../apps/**/*.json', { eager: true })
const internalProcessInfos: ProcessInfoImpl[] = Object.values(apps).map(data => new ProcessInfoImpl(data))
this._processInfos.push(BasicSystemProcessInfo)
this._processInfos.push(DesktopProcessInfo)
this._processInfos.push(...internalProcessInfos)
}
public async runProcess<T extends IProcess = IProcess, A extends any[] = any[]>(
proc: string | IProcessInfo,
constructor?: new (info: IProcessInfo, ...args: A) => T,
...args: A
): Promise<T> {
let info = typeof proc === 'string' ? this.findProcessInfoByName(proc) : proc
if (isUndefined(info)) {
throw new Error(`未找到进程信息:${proc}`)
}
// 是单例应用
if (info.singleton) {
let process = this.findProcessByName(info.name)
if (process) {
return process as T
}
}
// 创建进程
let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info, ...args)
return process as T
}
// 添加进程
public registerProcess(process: ProcessImpl) {
this._processPool.set(process.id, process);
}
/**
* 通过进程id查找进程
* @param id 进程id
*/
public findProcessById(id: string) {
return this._processPool.get(id);
}
/**
* 通过进程名称查找进程
* @param name 进程名称
*/
public findProcessByName<T extends IProcess = IProcess>(name: string) {
const pools = [...this._processPool.values()];
return pools.find(proc => proc.processInfo.name === name) as T | undefined;
}
/**
* 根据进程id删除进程
* @param id 进程id
*/
public removeProcess(id: string): void;
/**
* 根据进程删除进程
* @param process 进程信息
*/
public removeProcess(process: IProcess): void;
public removeProcess(params: string | IProcess) {
const id = typeof params === 'string' ? params : params.id;
this._processPool.delete(id);
}
/**
* 通过进程名称查找进程信息
*/
public findProcessInfoByName(name: string) {
return this._processInfos.find(info => info.name === name);
}
}

View File

@@ -1,26 +0,0 @@
import type { IVersion } from '../../common/types/IVersion.ts'
import type { IWindowFormConfig } from '../../window/types/IWindowFormConfig.ts'
/**
* 应用进程入参信息
*/
export interface IAppProcessInfoParams {
/** 应用进程名称 */
name: string;
/** 应用进程标题 */
title?: string;
/** 应用进程描述 */
description?: string;
/** 应用进程图标 */
icon?: string;
/** 应用进程启动入口 */
startName?: string;
/** 应用版本信息 */
version?: IVersion;
/** 应用是否只存在一个进程 */
singleton: boolean;
/** 是否只是一个进程, 没有UI */
isJustProcess: boolean;
/** 进程所有的窗口配置信息 */
windowFormConfigs?: IWindowFormConfig[];
}

View File

@@ -1,24 +0,0 @@
import type { IEventMap } from '@/core/events/IEventBuilder.ts'
/**
* 进程的事件
* <p>onProcessExit - 进程退出</p>
* <p>onProcessWindowFormOpen - 进程的窗体打开</p>
* <p>onProcessWindowFormExit - 进程的窗体退出</p>
* <p>onProcessWindowFormFocus - 进程的窗体获取焦点</p>
*
*/
type TProcessEvent =
'onProcessExit' |
'onProcessWindowFormOpen' |
'onProcessWindowFormExit' |
'onProcessWindowFormFocus' |
'onProcessWindowFormBlur'
export interface IProcessEvent extends IEventMap {
/**
* 进程的窗体退出
* @param id 窗体id
*/
processWindowFormExit: (id: string) => void
}

View File

@@ -1,21 +0,0 @@
import { serviceManager, type ServiceManager } from '@/core/service/kernel/ServiceManager.ts'
/**
* 服务基类 - 抽象类
*/
export abstract class AService {
private readonly _id: string;
private _sm: ServiceManager = serviceManager;
get id() {
return this._id;
}
get sm() {
return this._sm;
}
protected constructor(id: string) {
this._id = id;
this._sm.registerService(this);
}
}

View File

@@ -1,41 +0,0 @@
import type { AService } from '@/core/service/kernel/AService.ts'
/**
* 服务管理
*/
export class ServiceManager {
private _services: Map<string, AService> = new Map<string, AService>();
get services(): Map<string, AService> {
return this._services
}
/**
* 注册服务
* @param service
*/
registerService(service: AService): void {
if (this._services.has(service.id)) {
throw new Error(`服务 ${service.id} 已存在`)
}
this._services.set(service.id, service)
}
/**
* 通过id获取服务
* @param id
*/
getService<T extends AService>(id: string): T | undefined {
return this._services.get(id) as T | undefined
}
/**
* 广播
* @param event
* @param data
*/
broadcast(event: string, data?: any): void {
}
}
export const serviceManager = new ServiceManager()

View File

@@ -1,8 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
export class NotificationService extends AService {
constructor() {
super('NotificationService');
console.log('NotificationService - 服务注册')
}
}

View File

@@ -1,8 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
export class SettingsService extends AService {
constructor() {
super('SettingsService')
console.log('SettingsService - 服务注册')
}
}

View File

@@ -1,20 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
import type { IObservable } from '@/core/state/IObservable.ts'
interface IUserInfo {
id: string;
name: string;
token: string;
}
export class UserService extends AService {
private _userInfo: IObservable<IUserInfo>;
get userInfo() {
return this._userInfo;
}
constructor() {
super("UserService");
console.log("UserService - 服务注册")
}
}

View File

@@ -1,64 +0,0 @@
import { AService } from '@/core/service/kernel/AService.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import WindowFormImpl from '@/core/window/impl/WindowFormImpl.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
interface IWindow {
id: string;
title: string;
x: number;
y: number;
width: number;
height: number;
zIndex: number;
minimized: boolean;
maximized: boolean;
}
export class WindowFormService extends AService {
private windows: Map<string, IWindowForm> = new Map();
constructor() {
super("WindowFormService");
console.log('WindowFormService - 服务注册')
}
public createWindow(proc: IProcess, info: IWindowFormConfig): IWindowForm {
const window = new WindowFormImpl(proc, info);
this.windows.set(window.id, window);
return window;
}
public closeWindow(id: string) {
if (this.windows.has(id)) {
this.windows.delete(id);
this.sm.broadcast("WindowFrom:closed", id);
}
}
public focusWindow(id: string) {
const win = this.windows.get(id);
if (win) {
this.sm.broadcast("WindowFrom:focused", win);
}
}
public minimizeWindow(id: string) {
const win = this.windows.get(id);
if (win) {
this.sm.broadcast("WindowFrom:minimized", win);
}
}
public maximizeWindow(id: string) {
const win = this.windows.get(id);
if (win) {
this.sm.broadcast("WindowFrom:maximized", win);
}
}
onMessage(event: string, data?: any) {
console.log(`[WindowService] 收到事件:`, event, data);
}
}

View File

@@ -1,57 +0,0 @@
// 订阅函数类型
export type TObservableListener<T> = (state: T) => void
// 字段订阅函数类型
export type TObservableKeyListener<T, K extends keyof T> = (values: Pick<T, K>) => void
// 工具类型:排除函数属性
export type TNonFunctionProperties<T> = {
[K in keyof T as T[K] extends Function ? never : K]: T[K]
}
// ObservableImpl 数据类型
export type TObservableState<T> = T & { [key: string]: any }
/**
* ObservableImpl 接口定义
*/
export interface IObservable<T extends TNonFunctionProperties<T>> {
/** ObservableImpl 状态对象,深层 Proxy */
readonly state: TObservableState<T>
/**
* 订阅整个状态变化
* @param fn 监听函数
* @param options immediate 是否立即触发一次
* @returns 取消订阅函数
*/
subscribe(fn: TObservableListener<T>, options?: { immediate?: boolean }): () => void
/**
* 订阅指定字段变化
* @param keys 单个或多个字段
* @param fn 字段变化回调
* @param options immediate 是否立即触发一次
* @returns 取消订阅函数
*/
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options?: { immediate?: boolean }
): () => void
/**
* 批量更新状态
* @param values Partial<T>
*/
patch(values: Partial<T>): void
/** 销毁 ObservableImpl 实例 */
dispose(): void
/**
* 语法糖:返回一个可解构赋值的 Proxy
* 用于直接赋值触发通知
*/
toRefsProxy(): { [K in keyof T]: T[K] }
}

View File

@@ -1,305 +0,0 @@
import type {
IObservable,
TNonFunctionProperties,
TObservableKeyListener,
TObservableListener,
TObservableState,
} from '@/core/state/IObservable.ts'
/**
* 创建一个可观察对象,用于管理状态和事件。
* @template T - 需要处理的状态类型
* @example
* interface Todos {
* id: number
* text: string
* done: boolean
* }
*
* interface AppState {
* count: number
* todos: Todos[]
* user: {
* name: string
* age: number
* }
* inc(): void
* }
*
* const obs = new ObservableImpl<AppState>({
* count: 0,
* todos: [],
* user: { name: "Alice", age: 20 },
* inc() {
* this.count++ // ✅ this 指向 obs.state
* },
* })
*
* // ================== 使用示例 ==================
*
* // 1. 订阅整个 state
* obs.subscribe(state => {
* console.log("[全量订阅] state 更新:", state)
* })
*
* // 2. 订阅单个字段
* obs.subscribeKey("count", ({ count }) => {
* console.log("[字段订阅] count 更新:", count)
* })
*
* // 3. 订阅多个字段
* obs.subscribeKey(["name", "age"] as (keyof AppState["user"])[], (user) => {
* console.log("[多字段订阅] user 更新:", user)
* })
*
* // 4. 批量更新
* obs.patch({ count: 10, user: { name: "Bob", age: 30 } })
*
* // 5. 方法里操作 state
* obs.state.inc() // this.count++ → 相当于 obs.state.count++
*
* // 6. 数组操作
* obs.subscribeKey("todos", ({ todos }) => {
* console.log("[数组订阅] todos 更新:", todos.map(t => t.text))
* })
*
* obs.state.todos.push({ id: 1, text: "Buy milk", done: false })
* obs.state.todos.push({ id: 2, text: "Read book", done: false })
* obs.state.todos[0].done = true
*
* // 7. 嵌套对象
* obs.subscribeKey("user", ({ user }) => {
* console.log("[嵌套订阅] user 更新:", user)
* })
*
* obs.state.user.age++
*/
export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObservable<T> {
/** Observable 状态对象,深层 Proxy */
public readonly state: TObservableState<T>
/** 全量订阅函数集合 */
private listeners: Set<TObservableListener<T>> = new Set()
/**
* 字段订阅函数集合
* 新结构:
* Map<TObservableKeyListener, Array<keyof T>>
* 记录每个回调订阅的字段数组,保证多字段订阅 always 返回所有订阅字段值
*/
private keyListeners: Map<TObservableKeyListener<T, keyof T>, Array<keyof T>> = new Map()
/** 待通知的字段集合 */
private pendingKeys: Set<keyof T> = new Set()
/** 是否已经安排通知 */
private notifyScheduled = false
/** 是否已销毁 */
private disposed = false
/** 缓存 Proxy避免重复包装 */
private proxyCache: WeakMap<object, TObservableState<unknown>> = new WeakMap()
constructor(initialState: TNonFunctionProperties<T>) {
// 创建深层响应式 Proxy
this.state = this.makeReactive(initialState) as TObservableState<T>
}
/** 创建深层 Proxy拦截 get/set/delete并自动缓存 */
private makeReactive<O extends object>(obj: O): TObservableState<O> {
// 非对象直接返回(包括 null 已被排除)
if (typeof obj !== "object" || obj === null) {
return obj as unknown as TObservableState<O>
}
// 如果已有 Proxy 缓存则直接返回
const cached = this.proxyCache.get(obj as object)
if (cached !== undefined) {
return cached as TObservableState<O>
}
const handler: ProxyHandler<O> = {
get: (target, prop, receiver) => {
const value = Reflect.get(target, prop, receiver) as unknown
// 不包装函数
if (typeof value === "function") {
return value
}
// 对对象/数组继续进行响应式包装(递归)
if (typeof value === "object" && value !== null) {
return this.makeReactive(value as object)
}
return value
},
set: (target, prop, value, receiver) => {
// 读取旧值(使用 Record 以便类型安全访问属性)
const oldValue = (target as Record<PropertyKey, unknown>)[prop as PropertyKey] as unknown
const result = Reflect.set(target, prop, value as unknown, receiver)
// 仅在值改变时触发通知(基于引用/原始值比较)
if (!this.disposed && oldValue !== (value as unknown)) {
this.pendingKeys.add(prop as keyof T)
this.scheduleNotify()
}
return result
},
deleteProperty: (target, prop) => {
if (prop in target) {
// 使用 Reflect.deleteProperty 以保持一致性
const deleted = Reflect.deleteProperty(target, prop)
if (deleted && !this.disposed) {
this.pendingKeys.add(prop as keyof T)
this.scheduleNotify()
}
return deleted
}
return false
}
}
const proxy = new Proxy(obj, handler) as TObservableState<O>
this.proxyCache.set(obj as object, proxy as TObservableState<unknown>)
return proxy
}
/** 安排下一次通知(微任务合并) */
private scheduleNotify(): void {
if (!this.notifyScheduled && !this.disposed && this.pendingKeys.size > 0) {
this.notifyScheduled = true
Promise.resolve().then(() => this.flushNotify())
}
}
/** 执行通知(聚合字段订阅并保证错误隔离) */
private flushNotify(): void {
if (this.disposed) return
this.pendingKeys.clear()
this.notifyScheduled = false
// 全量订阅 —— 每个订阅单独 try/catch避免一个错误阻塞其它订阅
for (const fn of this.listeners) {
try {
fn(this.state as unknown as T)
} catch (err) {
console.error("Observable listener error:", err)
}
}
// ================== 字段订阅 ==================
// 遍历所有回调,每个回调都返回它订阅的字段(即使只有部分字段变化)
this.keyListeners.forEach((subKeys, fn) => {
try {
// 构造 Pick<T, K> 风格的结果对象:结果类型为 Pick<T, (typeof subKeys)[number]>
const result = {} as Pick<T, (typeof subKeys)[number]>
subKeys.forEach(k => {
// 这里断言原因state 的索引访问返回 unknown但我们把它赋回到受限的 Pick 上
result[k] = (this.state as Record<keyof T, unknown>)[k] as T[(typeof k) & keyof T]
})
// 调用时类型上兼容 TObservableKeyListener<T, K>,因为我们传的是对应 key 的 Pick
fn(result as Pick<T, (typeof subKeys)[number]>)
} catch (err) {
console.error("Observable keyListener error:", err)
}
})
}
/** 订阅整个状态变化 */
public subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
this.listeners.add(fn)
if (options.immediate) {
try {
fn(this.state as unknown as T)
} catch (err) {
console.error("Observable subscribe immediate error:", err)
}
}
return () => {
this.listeners.delete(fn)
}
}
/** 订阅指定字段变化(多字段订阅 always 返回所有字段值) */
public subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {}
): () => void {
const keyArray = Array.isArray(keys) ? keys : [keys]
// ================== 存储回调和它订阅的字段数组 ==================
this.keyListeners.set(fn as TObservableKeyListener<T, keyof T>, keyArray as (keyof T)[])
// ================== 立即调用 ==================
if (options.immediate) {
const result = {} as Pick<T, K>
keyArray.forEach(k => {
result[k] = (this.state as Record<keyof T, unknown>)[k] as T[K]
})
try {
fn(result)
} catch (err) {
console.error("Observable subscribeKey immediate error:", err)
}
}
// ================== 返回取消订阅函数 ==================
return () => {
this.keyListeners.delete(fn as TObservableKeyListener<T, keyof T>)
}
}
/** 批量更新状态(避免重复 schedule */
public patch(values: Partial<T>): void {
let changed = false
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const typedKey = key as keyof T
const oldValue = (this.state as Record<keyof T, unknown>)[typedKey]
const newValue = values[typedKey] as unknown
if (oldValue !== newValue) {
(this.state as Record<keyof T, unknown>)[typedKey] = newValue
changed = true
}
}
}
// 如果至少有一处变化,安排一次通知(如果写入已由 set 调度过也不会重复安排)
if (changed) this.scheduleNotify()
}
/** 销毁 Observable 实例 */
public dispose(): void {
this.disposed = true
this.listeners.clear()
this.keyListeners.clear()
this.pendingKeys.clear()
this.proxyCache = new WeakMap()
Object.freeze(this.state)
}
/** 语法糖:返回一个可解构赋值的 Proxy */
public toRefsProxy(): { [K in keyof T]: T[K] } {
const self = this
return new Proxy({} as { [K in keyof T]: T[K] }, {
get(_, prop: string | symbol) {
const key = prop as keyof T
return (self.state as Record<keyof T, unknown>)[key] as T[typeof key]
},
set(_, prop: string | symbol, value) {
const key = prop as keyof T
;(self.state as Record<keyof T, unknown>)[key] = value as unknown
return true
},
ownKeys() {
return Reflect.ownKeys(self.state)
},
getOwnPropertyDescriptor(_, _prop: string | symbol) {
return { enumerable: true, configurable: true }
}
})
}
}

View File

@@ -1,297 +0,0 @@
import type {
IObservable,
TNonFunctionProperties,
TObservableKeyListener,
TObservableListener,
TObservableState,
} from '@/core/state/IObservable.ts'
/**
* 创建一个可观察对象,用于管理状态和事件。
* WeakRef 和垃圾回收功能
* @template T - 需要处理的状态类型
* @example
* interface AppState {
* count: number
* user: {
* name: string
* age: number
* }
* items: number[]
* }
*
* // 创建 ObservableImpl
* const obs = new ObservableImpl<AppState>({
* count: 0,
* user: { name: 'Alice', age: 20 },
* items: []
* })
*
* // 1⃣ 全量订阅
* const unsubscribeAll = obs.subscribe(state => {
* console.log('全量订阅', state)
* }, { immediate: true })
*
* // 2⃣ 单字段订阅
* const unsubscribeCount = obs.subscribeKey('count', ({ count }) => {
* console.log('count 字段变化:', count)
* })
*
* // 3⃣ 多字段订阅
* const unsubscribeUser = obs.subscribeKey(['user', 'count'], ({ user, count }) => {
* console.log('user 或 count 变化:', { user, count })
* })
*
* // 4⃣ 修改属性
* obs.state.count = 1 // ✅ 会触发 count 和全量订阅
* obs.state.user.age = 21 // ✅ 深层对象修改触发 user 订阅
* obs.state.user.name = 'Bob'
* // 语法糖:解构赋值直接赋值触发通知
* const { count, user, items } = obs.toRefsProxy()
* count = 1 // 触发 Proxy set
* user.age = 18 // 深层对象 Proxy 支持
* items.push(42) // 数组方法拦截触发通知
*
* // 5⃣ 数组方法触发
* obs.state.items.push(10) // ✅ push 会触发 items 的字段订阅
* obs.state.items.splice(0, 1)
*
* // 6⃣ 批量修改(同一事件循环只触发一次通知)
* obs.patch({
* count: 2,
* user: { name: 'Charlie', age: 30 }
* })
*
* // 7⃣ 解构赋值访问对象属性仍然触发订阅
* const { state } = obs
* state.user.age = 31 // ✅ 会触发 user 订阅
*
* // 8⃣ 取消订阅
* unsubscribeAll()
* unsubscribeCount()
* unsubscribeUser()
*
* // 9⃣ 销毁 ObservableImpl
* obs.dispose()
*/
export class ObservableWeakRefImpl<T extends TNonFunctionProperties<T>> implements IObservable<T> {
/** ObservableImpl 的状态对象,深层 Proxy */
public readonly state: TObservableState<T>
/** 全量订阅列表 */
private listeners: Set<WeakRef<TObservableListener<T>> | TObservableListener<T>> = new Set()
/** 字段订阅列表 */
private keyListeners: Map<keyof T, Set<WeakRef<Function> | Function>> = new Map()
/** FinalizationRegistry 用于自动清理 WeakRef */
private registry?: FinalizationRegistry<WeakRef<Function>>
/** 待通知的字段 */
private pendingKeys: Set<keyof T> = new Set()
/** 通知调度状态 */
private notifyScheduled = false
/** 已销毁标记 */
private disposed = false
constructor(initialState: TNonFunctionProperties<T>) {
if (typeof WeakRef !== 'undefined' && typeof FinalizationRegistry !== 'undefined') {
this.registry = new FinalizationRegistry((ref: WeakRef<Function>) => {
this.listeners.delete(ref as unknown as TObservableListener<T>)
this.keyListeners.forEach(set => set.delete(ref))
})
}
// 创建深层响应式 Proxy
this.state = this.makeReactive(initialState) as TObservableState<T>
}
/** 创建响应式对象,深层递归 Proxy + 数组方法拦截 */
private makeReactive(obj: TNonFunctionProperties<T>): TObservableState<T> {
const handler: ProxyHandler<any> = {
get: (target, prop: string | symbol, receiver) => {
const key = prop as keyof T // 类型断言
const value = Reflect.get(target, key, receiver)
if (Array.isArray(value)) return this.wrapArray(value, key)
if (typeof value === 'object' && value !== null) return this.makeReactive(value)
return value
},
set: (target, prop: string | symbol, value, receiver) => {
const key = prop as keyof T // 类型断言
const oldValue = target[key]
if (oldValue !== value) {
target[key] = value
this.pendingKeys.add(key)
this.scheduleNotify()
}
return true
},
}
return new Proxy(obj, handler) as TObservableState<T>
}
/** 包装数组方法,使 push/pop/splice 等触发通知 */
private wrapArray(arr: any[], parentKey: keyof T): any {
const self = this
const arrayMethods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'] as const
arrayMethods.forEach(method => {
const original = arr[method]
Object.defineProperty(arr, method, {
value: function (...args: any[]) {
const result = original.apply(this, args)
self.pendingKeys.add(parentKey)
self.scheduleNotify()
return result
},
writable: true,
configurable: true,
})
})
return arr
}
/** 调度异步通知 */
private scheduleNotify(): void {
if (!this.notifyScheduled && !this.disposed) {
this.notifyScheduled = true
Promise.resolve().then(() => this.flushNotify())
}
}
/** 执行通知逻辑 */
private flushNotify(): void {
if (this.disposed) return
const keys = Array.from(this.pendingKeys)
this.pendingKeys.clear()
this.notifyScheduled = false
// 全量订阅
for (const ref of this.listeners) {
const fn = this.deref(ref)
if (fn) fn(this.state)
else this.listeners.delete(ref as TObservableListener<T>)
}
// 字段订阅
const fnMap = new Map<Function, (keyof T)[]>()
for (const key of keys) {
const set = this.keyListeners.get(key)
if (!set) continue
for (const ref of set) {
const fn = this.deref(ref)
if (!fn) {
set.delete(ref)
continue
}
if (!fnMap.has(fn)) fnMap.set(fn, [])
fnMap.get(fn)!.push(key)
}
}
fnMap.forEach((subKeys, fn) => {
const result = {} as Pick<T, typeof subKeys[number]>
subKeys.forEach(k => (result[k] = this.state[k]))
fn(result)
})
}
/** 全量订阅 */
subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
const ref = this.makeRef(fn)
this.listeners.add(ref)
this.registry?.register(fn, ref as WeakRef<Function>)
if (options.immediate) fn(this.state)
return () => {
this.listeners.delete(ref)
this.registry?.unregister(fn)
}
}
/** 字段订阅 */
subscribeKey<K extends keyof T>(
keys: K | K[],
fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {}
): () => void {
const keyArray = Array.isArray(keys) ? keys : [keys]
const refs: (WeakRef<Function> | Function)[] = []
for (const key of keyArray) {
if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set())
const ref = this.makeRef(fn)
this.keyListeners.get(key)!.add(ref)
this.registry?.register(fn as unknown as Function, ref as WeakRef<Function>)
refs.push(ref)
}
if (options.immediate) {
const result = {} as Pick<T, K>
keyArray.forEach(k => (result[k] = this.state[k]))
fn(result)
}
return () => {
for (let i = 0; i < keyArray.length; i++) {
const set = this.keyListeners.get(keyArray[i])
if (set) set.delete(refs[i])
}
this.registry?.unregister(fn as unknown as Function)
}
}
/** 批量更新 */
patch(values: Partial<T>): void {
for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) {
const typedKey = key as keyof T
this.state[typedKey] = values[typedKey]!
this.pendingKeys.add(typedKey)
}
}
this.scheduleNotify()
}
/** 销毁 ObservableImpl */
dispose(): void {
this.disposed = true
this.listeners.clear()
this.keyListeners.clear()
this.pendingKeys.clear()
}
/** 语法糖:解构赋值直接赋值触发通知 */
toRefsProxy(): { [K in keyof T]: T[K] } {
const self = this
return new Proxy({} as T, {
get(_, prop: string | symbol) {
const key = prop as keyof T
return self.state[key]
},
set(_, prop: string | symbol, value) {
const key = prop as keyof T
self.state[key] = value
return true
},
ownKeys() {
return Reflect.ownKeys(self.state)
},
getOwnPropertyDescriptor(_, prop: string | symbol) {
return { enumerable: true, configurable: true }
}
})
}
/** WeakRef 创建 */
private makeRef<F extends Function>(fn: F): WeakRef<F> | F {
return typeof WeakRef !== 'undefined' ? new WeakRef(fn) : fn
}
/** WeakRef 解引用 */
private deref<F extends Function>(ref: WeakRef<F> | F): F | undefined {
return typeof WeakRef !== 'undefined' && ref instanceof WeakRef ? ref.deref() : (ref as F)
}
}

View File

@@ -1,14 +0,0 @@
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
interface IGlobalStoreParams {
/** 桌面根dom ID类似显示器 */
monitorDomId: string;
monitorWidth: number;
monitorHeight: number;
}
export const globalStore = new ObservableImpl<IGlobalStoreParams>({
monitorDomId: '#app',
monitorWidth: 0,
monitorHeight: 0
})

View File

@@ -1,19 +0,0 @@
import ProcessImpl from '../process/impl/ProcessImpl.ts'
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
/**
* 基础系统进程
*/
export class BasicSystemProcess extends ProcessImpl {
private _isMounted: boolean = false;
public get isMounted() {
return this._isMounted;
}
constructor(info: IProcessInfo) {
super(info)
console.log('BasicSystemProcess')
}
}

View File

@@ -1,18 +0,0 @@
import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts'
/**
* 基础系统进程信息
*/
export const BasicSystemProcessInfo = new ProcessInfoImpl({
name: 'basic-system',
title: '基础系统进程',
isJustProcess: true,
version: {
company: 'XZG',
major: 1,
minor: 0,
build: 0,
private: 0
},
singleton: true
});

View File

@@ -1,752 +0,0 @@
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
/** 拖拽移动开始的回调 */
type TDragStartCallback = (x: number, y: number) => void;
/** 拖拽移动中的回调 */
type TDragMoveCallback = (x: number, y: number) => void;
/** 拖拽移动结束的回调 */
type TDragEndCallback = (x: number, y: number) => void;
/** 拖拽调整尺寸的方向 */
type TResizeDirection =
| 'top'
| 'bottom'
| 'left'
| 'right'
| 'top-left'
| 'top-right'
| 'bottom-left'
| 'bottom-right';
/** 元素边界 */
interface IElementRect {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
}
/** 拖拽调整尺寸回调数据 */
interface IResizeCallbackData {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
/** 拖拽调整尺寸的方向 */
direction: TResizeDirection;
}
/** 拖拽参数 */
interface IDraggableResizableOptions {
/** 拖拽/调整尺寸目标元素 */
target: HTMLElement;
/** 拖拽句柄 */
handle?: HTMLElement;
/** 拖拽边界容器元素 */
boundaryElement?: HTMLElement;
/** 移动步进(网格吸附) */
snapGrid?: number;
/** 关键点吸附阈值 */
snapThreshold?: number;
/** 是否开启吸附动画 */
snapAnimation?: boolean;
/** 拖拽结束吸附动画时长 */
snapAnimationDuration?: number;
/** 是否允许超出边界 */
allowOverflow?: boolean;
/** 最小化任务栏位置的元素ID */
taskbarElementId: string;
/** 拖拽开始回调 */
onDragStart?: TDragStartCallback;
/** 拖拽移动中的回调 */
onDragMove?: TDragMoveCallback;
/** 拖拽结束回调 */
onDragEnd?: TDragEndCallback;
/** 调整尺寸的最小宽度 */
minWidth?: number;
/** 调整尺寸的最小高度 */
minHeight?: number;
/** 调整尺寸的最大宽度 */
maxWidth?: number;
/** 调整尺寸的最大高度 */
maxHeight?: number;
/** 拖拽调整尺寸中的回调 */
onResizeMove?: (data: IResizeCallbackData) => void;
/** 拖拽调整尺寸结束回调 */
onResizeEnd?: (data: IResizeCallbackData) => void;
/** 窗口状态改变回调 */
onWindowStateChange?: (state: TWindowFormState) => void;
}
/** 拖拽的范围边界 */
interface IBoundaryRect {
/** 最小 X 坐标 */
minX?: number;
/** 最大 X 坐标 */
maxX?: number;
/** 最小 Y 坐标 */
minY?: number;
/** 最大 Y 坐标 */
maxY?: number;
}
/**
* 拖拽 + 调整尺寸 + 最大最小化 通用类
* 统一使用 position: absolute + transform: translate 实现拖拽
*/
export class DraggableResizableWindow {
private handle?: HTMLElement;
private target: HTMLElement;
private boundaryElement: HTMLElement;
private snapGrid: number;
private snapThreshold: number;
private snapAnimation: boolean;
private snapAnimationDuration: number;
private allowOverflow: boolean;
private onDragStart?: TDragStartCallback;
private onDragMove?: TDragMoveCallback;
private onDragEnd?: TDragEndCallback;
private onResizeMove?: (data: IResizeCallbackData) => void;
private onResizeEnd?: (data: IResizeCallbackData) => void;
private onWindowStateChange?: (state: TWindowFormState) => void;
private isDragging = false;
private currentDirection: TResizeDirection | null = null;
private dragThreshold = 2; // 拖拽阈值 超过才开始真正的拖拽
private startX = 0;
private startY = 0;
private startWidth = 0;
private startHeight = 0;
private startTop = 0;
private startLeft = 0;
private offsetX = 0;
private offsetY = 0;
private currentX = 0;
private currentY = 0;
private pendingDrag = false;
private pendingResize = false;
private dragDX = 0;
private dragDY = 0;
private resizeDX = 0;
private resizeDY = 0;
private minWidth: number;
private minHeight: number;
private maxWidth: number;
private maxHeight: number;
private containerRect: DOMRect;
private resizeObserver?: ResizeObserver;
private mutationObserver: MutationObserver;
private animationFrame?: number;
private _windowFormState: TWindowFormState = 'default';
/** 元素信息 */
private targetBounds: IElementRect;
/** 最小化前的元素信息 */
private targetPreMinimizeBounds?: IElementRect;
/** 最大化前的元素信息 */
private targetPreMaximizedBounds?: IElementRect;
private taskbarElementId: string;
get windowFormState() {
return this._windowFormState;
}
constructor(options: IDraggableResizableOptions) {
this.handle = options.handle;
this.target = options.target;
this.boundaryElement = options.boundaryElement ?? document.body;
this.snapGrid = options.snapGrid ?? 1;
this.snapThreshold = options.snapThreshold ?? 0;
this.snapAnimation = options.snapAnimation ?? false;
this.snapAnimationDuration = options.snapAnimationDuration ?? 200;
this.allowOverflow = options.allowOverflow ?? true;
this.onDragStart = options.onDragStart;
this.onDragMove = options.onDragMove;
this.onDragEnd = options.onDragEnd;
this.minWidth = options.minWidth ?? 100;
this.minHeight = options.minHeight ?? 50;
this.maxWidth = options.maxWidth ?? window.innerWidth;
this.maxHeight = options.maxHeight ?? window.innerHeight;
this.onResizeMove = options.onResizeMove;
this.onResizeEnd = options.onResizeEnd;
this.onWindowStateChange = options.onWindowStateChange;
this.taskbarElementId = options.taskbarElementId;
this.target.style.position = "absolute";
this.target.style.left = '0px';
this.target.style.top = '0px';
this.target.style.transform = "translate(0px, 0px)";
this.init();
requestAnimationFrame(() => {
this.targetBounds = {
width: this.target.offsetWidth,
height: this.target.offsetHeight,
top: this.target.offsetTop,
left: this.target.offsetLeft,
};
this.containerRect = this.boundaryElement.getBoundingClientRect();
const x = this.containerRect.width / 2 - this.target.offsetWidth / 2;
const y = this.containerRect.height / 2 - this.target.offsetHeight / 2;
this.target.style.transform = `translate(${x}px, ${y}px)`;
});
}
private init() {
if (this.handle) {
this.handle.addEventListener('mousedown', this.onMouseDownDrag);
}
this.target.addEventListener('mousedown', this.onMouseDownResize);
this.target.addEventListener('mouseleave', this.onMouseLeave);
document.addEventListener('mousemove', this.onDocumentMouseMoveCursor);
this.observeResize(this.boundaryElement);
this.mutationObserver = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.removedNodes.forEach(node => {
if (node === this.target) this.destroy();
});
}
});
if (this.target.parentElement) {
this.mutationObserver.observe(this.target.parentElement, { childList: true });
}
}
private onMouseDownDrag = (e: MouseEvent) => {
e.preventDefault();
if (!this.handle?.contains(e.target as Node)) return;
const target = e.target as HTMLElement;
if (target.classList.contains('btn')) return;
if (this.getResizeDirection(e)) return;
this.startX = e.clientX;
this.startY = e.clientY;
document.addEventListener('mousemove', this.checkDragStart);
document.addEventListener('mouseup', this.cancelPendingDrag);
}
private checkDragStart = (e: MouseEvent) => {
const dx = e.clientX - this.startX;
const dy = e.clientY - this.startY;
if (Math.abs(dx) > this.dragThreshold || Math.abs(dy) > this.dragThreshold) {
// 超过阈值,真正开始拖拽
document.removeEventListener('mousemove', this.checkDragStart);
document.removeEventListener('mouseup', this.cancelPendingDrag);
if (this._windowFormState === 'maximized') {
const preRect = this.targetPreMaximizedBounds!;
const rect = this.target.getBoundingClientRect();
const relX = e.clientX / rect.width;
const relY = e.clientY / rect.height;
const newLeft = e.clientX - preRect.width * relX;
const newTop = e.clientY - preRect.height * relY;
this.targetPreMaximizedBounds = {
width: preRect.width,
height: preRect.height,
top: newTop,
left: newLeft,
};
this.restore(() => this.startDrag(e));
} else {
this.startDrag(e);
}
}
};
private cancelPendingDrag = () => {
document.removeEventListener('mousemove', this.checkDragStart);
document.removeEventListener('mouseup', this.cancelPendingDrag);
}
private startDrag = (e: MouseEvent) => {
this.isDragging = true;
this.startX = e.clientX;
this.startY = e.clientY;
const style = window.getComputedStyle(this.target);
const matrix = new DOMMatrixReadOnly(style.transform);
this.offsetX = matrix.m41;
this.offsetY = matrix.m42;
this.onDragStart?.(this.offsetX, this.offsetY);
document.addEventListener('mousemove', this.onMouseMoveDragRAF);
document.addEventListener('mouseup', this.onMouseUpDrag);
};
private onMouseMoveDragRAF = (e: MouseEvent) => {
e.stopPropagation();
this.dragDX = e.clientX - this.startX;
this.dragDY = e.clientY - this.startY;
if (!this.pendingDrag) {
this.pendingDrag = true;
requestAnimationFrame(() => {
this.pendingDrag = false;
this.applyDragFrame();
});
}
};
private applyDragFrame() {
if (!this.isDragging) return;
let newX = this.offsetX + this.dragDX;
let newY = this.offsetY + this.dragDY;
if (this.snapGrid > 1) {
newX = Math.round(newX / this.snapGrid) * this.snapGrid;
newY = Math.round(newY / this.snapGrid) * this.snapGrid;
}
this.applyPosition(newX, newY, false);
this.onDragMove?.(newX, newY);
}
private onMouseUpDrag = (e: MouseEvent) => {
e.stopPropagation();
if (!this.isDragging) return;
this.isDragging = false;
const snapped = this.applySnapping(this.currentX, this.currentY);
if (this.snapAnimation) {
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
this.onDragEnd?.(snapped.x, snapped.y);
this.updateTargetBounds(snapped.x, snapped.y);
});
} else {
this.applyPosition(snapped.x, snapped.y, true);
this.onDragEnd?.(snapped.x, snapped.y);
this.updateTargetBounds(snapped.x, snapped.y);
}
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
document.removeEventListener('mouseup', this.onMouseUpDrag);
};
private applyPosition(x: number, y: number, isFinal: boolean) {
this.currentX = x;
this.currentY = y;
this.target.style.transform = `translate(${x}px, ${y}px)`;
if (isFinal) this.applyBoundary();
}
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame);
const startX = this.currentX;
const startY = this.currentY;
const deltaX = targetX - startX;
const deltaY = targetY - startY;
const startTime = performance.now();
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
const x = startX + deltaX * ease;
const y = startY + deltaY * ease;
this.applyPosition(x, y, false);
this.onDragMove?.(x, y);
if (progress < 1) this.animationFrame = requestAnimationFrame(step);
else { this.applyPosition(targetX, targetY, true); onComplete?.(); }
};
this.animationFrame = requestAnimationFrame(step);
}
private applyBoundary() {
if (this.allowOverflow) return;
let { x, y } = { x: this.currentX, y: this.currentY };
const rect = this.target.getBoundingClientRect();
x = Math.min(Math.max(x, 0), this.containerRect.width - rect.width);
y = Math.min(Math.max(y, 0), this.containerRect.height - rect.height);
this.currentX = x;
this.currentY = y;
this.applyPosition(x, y, false);
}
private applySnapping(x: number, y: number) {
let snappedX = x, snappedY = y;
const containerSnap = this.getSnapPoints();
if (this.snapThreshold > 0) {
for (const sx of containerSnap.x) if (Math.abs(x - sx) <= this.snapThreshold) { snappedX = sx; break; }
for (const sy of containerSnap.y) if (Math.abs(y - sy) <= this.snapThreshold) { snappedY = sy; break; }
}
return { x: snappedX, y: snappedY };
}
private getSnapPoints() {
const snapPoints = { x: [] as number[], y: [] as number[] };
const rect = this.target.getBoundingClientRect();
snapPoints.x = [0, this.containerRect.width - rect.width];
snapPoints.y = [0, this.containerRect.height - rect.height];
return snapPoints;
}
private onMouseDownResize = (e: MouseEvent) => {
e.preventDefault();
e.stopPropagation();
const dir = this.getResizeDirection(e);
if (!dir) return;
this.startResize(e, dir);
};
private onMouseLeave = (e: MouseEvent) => {
e.stopPropagation();
this.updateCursor(null);
};
private startResize(e: MouseEvent, dir: TResizeDirection) {
this.currentDirection = dir;
const rect = this.target.getBoundingClientRect();
const style = window.getComputedStyle(this.target);
const matrix = new DOMMatrixReadOnly(style.transform);
this.offsetX = matrix.m41;
this.offsetY = matrix.m42;
this.startX = e.clientX;
this.startY = e.clientY;
this.startWidth = rect.width;
this.startHeight = rect.height;
this.startLeft = this.offsetX;
this.startTop = this.offsetY;
document.addEventListener('mousemove', this.onResizeDragRAF);
document.addEventListener('mouseup', this.onResizeEndHandler);
}
private onResizeDragRAF = (e: MouseEvent) => {
e.stopPropagation();
this.resizeDX = e.clientX - this.startX;
this.resizeDY = e.clientY - this.startY;
if (!this.pendingResize) {
this.pendingResize = true;
requestAnimationFrame(() => {
this.pendingResize = false;
this.applyResizeFrame();
});
}
};
private applyResizeFrame() {
if (!this.currentDirection) return;
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newX = this.startLeft;
let newY = this.startTop;
const dx = this.resizeDX;
const dy = this.resizeDY;
switch (this.currentDirection) {
case 'right': newWidth += dx; break;
case 'bottom': newHeight += dy; break;
case 'bottom-right': newWidth += dx; newHeight += dy; break;
case 'left': newWidth -= dx; newX += dx; break;
case 'top': newHeight -= dy; newY += dy; break;
case 'top-left': newWidth -= dx; newX += dx; newHeight -= dy; newY += dy; break;
case 'top-right': newWidth += dx; newHeight -= dy; newY += dy; break;
case 'bottom-left': newWidth -= dx; newX += dx; newHeight += dy; break;
}
const d = this.applyResizeBounds(newX, newY, newWidth, newHeight);
this.updateCursor(this.currentDirection);
this.onResizeMove?.({
width: d.width,
height: d.height,
left: d.left,
top: d.top,
direction: this.currentDirection,
});
}
// 应用尺寸调整边界
private applyResizeBounds(newX: number, newY: number, newWidth: number, newHeight: number): {
left: number;
top: number;
width: number;
height: number;
} {
// 最小/最大宽高限制
newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth));
newHeight = Math.max(this.minHeight, Math.min(this.maxHeight, newHeight));
// 边界限制
if (this.allowOverflow) {
this.currentX = newX;
this.currentY = newY;
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.applyPosition(newX, newY, false);
return {
left: newX,
top: newY,
width: newWidth,
height: newHeight,
}
}
newX = Math.min(Math.max(0, newX), this.containerRect.width - newWidth);
newY = Math.min(Math.max(0, newY), this.containerRect.height - newHeight);
this.currentX = newX;
this.currentY = newY;
this.target.style.width = `${newWidth}px`;
this.target.style.height = `${newHeight}px`;
this.applyPosition(newX, newY, false);
return {
left: newX,
top: newY,
width: newWidth,
height: newHeight,
}
}
private onResizeEndHandler = (e?: MouseEvent) => {
e?.stopPropagation();
if (!this.currentDirection) return;
this.onResizeEnd?.({
width: this.target.offsetWidth,
height: this.target.offsetHeight,
left: this.currentX,
top: this.currentY,
direction: this.currentDirection,
});
this.updateTargetBounds(this.currentX, this.currentY, this.target.offsetWidth, this.target.offsetHeight);
this.currentDirection = null;
this.updateCursor(null);
document.removeEventListener('mousemove', this.onResizeDragRAF);
document.removeEventListener('mouseup', this.onResizeEndHandler);
};
private getResizeDirection(e: MouseEvent): TResizeDirection | null {
const rect = this.target.getBoundingClientRect();
const offset = 4;
const x = e.clientX;
const y = e.clientY;
const top = y >= rect.top && y <= rect.top + offset;
const bottom = y >= rect.bottom - offset && y <= rect.bottom;
const left = x >= rect.left && x <= rect.left + offset;
const right = x >= rect.right - offset && x <= rect.right;
if (top && left) return 'top-left';
if (top && right) return 'top-right';
if (bottom && left) return 'bottom-left';
if (bottom && right) return 'bottom-right';
if (top) return 'top';
if (bottom) return 'bottom';
if (left) return 'left';
if (right) return 'right';
return null;
}
private updateCursor(direction: TResizeDirection | null) {
if (!direction) { this.target.style.cursor = 'default'; return; }
const cursorMap: Record<TResizeDirection, string> = {
top: 'ns-resize', bottom: 'ns-resize', left: 'ew-resize', right: 'ew-resize',
'top-left': 'nwse-resize', 'top-right': 'nesw-resize',
'bottom-left': 'nesw-resize', 'bottom-right': 'nwse-resize'
};
this.target.style.cursor = cursorMap[direction];
}
private onDocumentMouseMoveCursor = (e: MouseEvent) => {
e.stopPropagation();
if (this.currentDirection || this.isDragging) return;
const dir = this.getResizeDirection(e);
this.updateCursor(dir);
};
// 最小化到任务栏
public minimize() {
if (this._windowFormState === 'minimized') return;
this.targetPreMinimizeBounds = { ...this.targetBounds }
this._windowFormState = 'minimized';
const taskbar = document.querySelector(this.taskbarElementId);
if (!taskbar) throw new Error('任务栏元素未找到');
const rect = taskbar.getBoundingClientRect();
const startX = this.currentX;
const startY = this.currentY;
const startW = this.target.offsetWidth;
const startH = this.target.offsetHeight;
this.animateWindow(startX, startY, startW, startH, rect.left, rect.top, rect.width, rect.height, 400, () => {
this.target.style.display = 'none';
});
}
/** 最大化 */
public maximize() {
if (this._windowFormState === 'maximized') return;
this.targetPreMaximizedBounds = { ...this.targetBounds }
this._windowFormState = 'maximized';
const rect = this.target.getBoundingClientRect();
const startX = this.currentX;
const startY = this.currentY;
const startW = rect.width;
const startH = rect.height;
const targetX = 0;
const targetY = 0;
const targetW = this.containerRect?.width ?? window.innerWidth;
const targetH = this.containerRect?.height ?? window.innerHeight;
this.animateWindow(startX, startY, startW, startH, targetX, targetY, targetW, targetH, 300);
}
/** 恢复到默认窗体状态 */
public restore(onComplete?: () => void) {
if (this._windowFormState === 'default') return;
let b: IElementRect;
if ((this._windowFormState as TWindowFormState) === 'minimized' && this.targetPreMinimizeBounds) {
// 最小化恢复,恢复到最小化前的状态
b = this.targetPreMinimizeBounds;
} else if ((this._windowFormState as TWindowFormState) === 'maximized' && this.targetPreMaximizedBounds) {
// 最大化恢复,恢复到最大化前的默认状态
b = this.targetPreMaximizedBounds;
} else {
b = this.targetBounds;
}
this._windowFormState = 'default';
this.target.style.display = 'block';
const startX = this.currentX;
const startY = this.currentY;
const startW = this.target.offsetWidth;
const startH = this.target.offsetHeight;
this.animateWindow(startX, startY, startW, startH, b.left, b.top, b.width, b.height, 300, onComplete);
}
/**
* 窗体最大化、最小化和恢复默认 动画
* @param startX
* @param startY
* @param startW
* @param startH
* @param targetX
* @param targetY
* @param targetW
* @param targetH
* @param duration
* @param onComplete
* @private
*/
private animateWindow(
startX: number,
startY: number,
startW: number,
startH: number,
targetX: number,
targetY: number,
targetW: number,
targetH: number,
duration: number,
onComplete?: () => void
) {
const startTime = performance.now();
const step = (now: number) => {
const elapsed = now - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = 1 - Math.pow(1 - progress, 3);
const x = startX + (targetX - startX) * ease;
const y = startY + (targetY - startY) * ease;
const w = startW + (targetW - startW) * ease;
const h = startH + (targetH - startH) * ease;
this.target.style.width = `${w}px`;
this.target.style.height = `${h}px`;
this.applyPosition(x, y, false);
if (progress < 1) {
requestAnimationFrame(step);
} else {
this.target.style.width = `${targetW}px`;
this.target.style.height = `${targetH}px`;
this.applyPosition(targetX, targetY, true);
onComplete?.();
this.onWindowStateChange?.(this._windowFormState);
}
};
requestAnimationFrame(step);
}
private updateTargetBounds(left: number, top: number, width?: number, height?: number) {
this.targetBounds = {
left, top,
width: width ?? this.target.offsetWidth,
height: height ?? this.target.offsetHeight
};
}
/** 监听元素变化 */
private observeResize(element: HTMLElement) {
this.resizeObserver = new ResizeObserver(() => {
this.containerRect = element.getBoundingClientRect();
});
this.resizeObserver.observe(element);
}
/**
* 销毁实例
*/
public destroy() {
try {
if (this.handle) this.handle.removeEventListener('mousedown', this.onMouseDownDrag);
this.target.removeEventListener('mousedown', this.onMouseDownResize);
document.removeEventListener('mousemove', this.onMouseMoveDragRAF);
document.removeEventListener('mouseup', this.onMouseUpDrag);
document.removeEventListener('mousemove', this.onResizeDragRAF);
document.removeEventListener('mouseup', this.onResizeEndHandler);
document.removeEventListener('mousemove', this.onDocumentMouseMoveCursor);
document.removeEventListener('mousemove', this.checkDragStart);
document.removeEventListener('mouseup', this.cancelPendingDrag);
this.resizeObserver?.disconnect();
this.mutationObserver.disconnect();
cancelAnimationFrame(this.animationFrame ?? 0);
} catch (e) {}
}
}

View File

@@ -1,17 +0,0 @@
/** 单例模式
* 确保一个类只有一个实例,并提供一个全局访问点
* @param constructor
* @constructor
*/
export function Singleton<T extends { new (...args: any[]): any }>(constructor: T): T {
let instance: any;
return new Proxy(constructor, {
construct(target, argsList, newTarget) {
if (!instance) {
instance = Reflect.construct(target, argsList, newTarget);
}
return instance;
},
});
}

View File

@@ -1,14 +0,0 @@
import type { IProcess } from '@/core/process/IProcess.ts'
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
export interface IWindowForm extends IDestroyable {
/** 窗体id */
get id(): string;
/** 窗体所属的进程 */
get proc(): IProcess | undefined;
/** 窗体元素 */
get windowFormEle(): HTMLElement;
/** 窗体状态 */
get windowFormState(): TWindowFormState;
}

View File

@@ -1,114 +0,0 @@
import { v4 as uuidV4 } from 'uuid';
import XSystem from '../../XSystem.ts'
import type { IProcess } from '@/core/process/IProcess.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import type { IWindowFormConfig } from '@/core/window/types/IWindowFormConfig.ts'
import type { TWindowFormState, WindowFormPos } from '@/core/window/types/WindowFormTypes.ts'
import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.ts'
import '../ui/WindowFormElement.ts'
import { wfem } from '@/core/events/WindowFormEventManager.ts'
import type { IObservable } from '@/core/state/IObservable.ts'
import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts'
export interface IWindowFormDataState {
/** 窗体id */
id: string;
/** 窗体进程id */
procId: string;
/** 进程名称唯一 */
name: string;
/** 窗体标题 */
title: string;
/** 窗体位置x (左上角) */
x: number;
/** 窗体位置y (左上角) */
y: number;
/** 窗体宽度 */
width: number;
/** 窗体高度 */
height: number;
/** 窗体状态 'default' | 'minimized' | 'maximized' */
state: TWindowFormState;
/** 窗体是否已关闭 */
closed: boolean;
}
export default class WindowFormImpl implements IWindowForm {
private readonly _id: string = uuidV4()
private readonly _proc: IProcess
private readonly _data: IObservable<IWindowFormDataState>
private dom: HTMLElement
private drw: DraggableResizableWindow
public get id() {
return this._id
}
public get proc() {
return this._proc
}
private get desktopRootDom() {
return XSystem.instance.desktopRootDom
}
public get windowFormEle() {
return this.dom
}
public get windowFormState() {
return this.drw.windowFormState
}
constructor(proc: IProcess, config: IWindowFormConfig) {
this._proc = proc
console.log('WindowForm')
this._data = new ObservableImpl<IWindowFormDataState>({
id: this.id,
procId: proc.id,
name: proc.processInfo.name,
title: config.title ?? '未命名',
x: config.left ?? 0,
y: config.top ?? 0,
width: config.width ?? 200,
height: config.height ?? 100,
state: 'default',
closed: false,
})
this.initEvent()
this.createWindowFrom()
}
private initEvent() {
this._data.subscribeKey('closed', (state) => {
console.log('closed', state)
this.closeWindowForm()
this._proc.closeWindowForm(this.id)
})
}
private createWindowFrom() {
const wf = document.createElement('window-form-element')
wf.wid = this.id
wf.wfData = this._data
wf.title = this._data.state.title
wf.dragContainer = document.body
wf.snapDistance = 20
wf.taskbarElementId = '#taskbar'
this.dom = wf
this.desktopRootDom.appendChild(this.dom)
Promise.resolve().then(() => {
wfem.notifyEvent('windowFormCreated')
wfem.notifyEvent('windowFormFocus', this.id)
})
}
private closeWindowForm() {
this.desktopRootDom.removeChild(this.dom)
this._data.dispose()
}
public minimize() {}
public maximize() {}
public restore() {}
public destroy() {}
}

View File

@@ -1,60 +0,0 @@
/**
* 窗体配置信息
*/
export interface IWindowFormConfig {
/**
* 窗体名称
*/
name: string;
/**
* 窗体标题
*/
title?: string;
/**
* 窗体图标
*/
icon?: string;
top?: number;
left?: number;
/**
* 窗体宽度
*/
width?: number;
widthAuto?: boolean;
/**
* 窗体高度
*/
height?: number;
heightAuto?: boolean;
/**
* 窗体最小宽度
*/
minWidth?: number;
/**
* 窗体最小高度
*/
minHeight?: number;
/**
* 窗体最大宽度
*/
maxWidth?: number;
/**
* 窗体最大高度
*/
maxHeight?: number;
/**
* 窗体透明度
*/
opacity?: number;
windowStyle?: string;
windowState?: number;
resizeMode?: number;
topMost?: boolean;
/**
* 是否显示在任务栏
*/
showInTaskbar?: boolean;
showTitleBarIcon?: boolean;
showTitleBarText?: boolean;
hideTitleBar?: boolean;
}

View File

@@ -1,10 +0,0 @@
/**
* 窗体位置坐标 - 左上角
*/
export interface WindowFormPos {
x: number;
y: number;
}
/** 窗口状态 */
export type TWindowFormState = 'default' | 'minimized' | 'maximized';

View File

@@ -1,904 +0,0 @@
import { LitElement, html, css, unsafeCSS } from 'lit'
import { customElement, property } from 'lit/decorators.js';
import wfStyle from './css/wf.scss?inline'
import type { TWindowFormState } from '@/core/window/types/WindowFormTypes.ts'
import { wfem } from '@/core/events/WindowFormEventManager.ts'
import type { IObservable } from '@/core/state/IObservable.ts'
import type { IWindowFormDataState } from '@/core/window/impl/WindowFormImpl.ts'
/** 拖拽移动开始的回调 */
type TDragStartCallback = (x: number, y: number) => void;
/** 拖拽移动中的回调 */
type TDragMoveCallback = (x: number, y: number) => void;
/** 拖拽移动结束的回调 */
type TDragEndCallback = (x: number, y: number) => void;
/** 拖拽调整尺寸的方向 */
type TResizeDirection =
| 't' // 上
| 'b' // 下
| 'l' // 左
| 'r' // 右
| 'tl' // 左上
| 'tr' // 右上
| 'bl' // 左下
| 'br'; // 右下
/** 拖拽调整尺寸回调数据 */
interface IResizeCallbackData {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
/** 拖拽调整尺寸的方向 */
direction: TResizeDirection | null;
}
/** 元素边界 */
interface IElementRect {
/** 宽度 */
width: number;
/** 高度 */
height: number;
/** 顶点坐标(相对 offsetParent */
top: number;
/** 左点坐标(相对 offsetParent */
left: number;
}
export interface WindowFormEventMap extends HTMLElementEventMap {
'windowForm:dragStart': CustomEvent<TDragStartCallback>;
'windowForm:dragMove': CustomEvent<TDragMoveCallback>;
'windowForm:dragEnd': CustomEvent<TDragEndCallback>;
'windowForm:resizeStart': CustomEvent<IResizeCallbackData>;
'windowForm:resizeMove': CustomEvent<IResizeCallbackData>;
'windowForm:resizeEnd': CustomEvent<IResizeCallbackData>;
'windowForm:stateChange': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:minimize': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:maximize': CustomEvent<{ state: TWindowFormState }>;
'windowForm:stateChange:restore': CustomEvent<{ state: TWindowFormState }>;
'windowForm:close': CustomEvent;
}
@customElement('window-form-element')
export class WindowFormElement extends LitElement {
// ==== 公共属性 ====
@property({ type: String }) wid: string
@property({ type: String }) override title = 'Window'
@property({ type: Boolean }) resizable = true
@property({ type: Boolean }) minimizable = true
@property({ type: Boolean }) maximizable = true
@property({ type: Boolean }) closable = true
@property({ type: Boolean, reflect: true }) focused: boolean = true
@property({ type: String, reflect: true }) windowFormState: TWindowFormState = 'default'
@property({ type: Object }) dragContainer?: HTMLElement
@property({ type: Boolean }) allowOverflow = true // 允许窗口超出容器
@property({ type: Number }) snapDistance = 0 // 吸附距离
@property({ type: Boolean }) snapAnimation = true // 吸附动画
@property({ type: Number }) snapAnimationDuration = 300 // 吸附动画时长 ms
@property({ type: Number }) maxWidth?: number = Infinity
@property({ type: Number }) minWidth?: number = 0
@property({ type: Number }) maxHeight?: number = Infinity
@property({ type: Number }) minHeight?: number = 0
@property({ type: String }) taskbarElementId?: string
@property({ type: Object }) wfData: IObservable<IWindowFormDataState>;
private _listeners: Array<{ type: string; original: Function; wrapped: EventListener }> = []
// ==== 拖拽/缩放状态(内部变量,不触发渲染) ====
private dragging = false
private resizeDir: TResizeDirection | null = null
private startX = 0
private startY = 0
private startWidth = 0
private startHeight = 0
private startX_host = 0
private startY_host = 0
private x = 0
private y = 0
private preX = 0
private preY = 0
private width = 640
private height = 360
private animationFrame?: number
private resizing = false
// private get x() {
// return this.wfData.state.x
// }
// private set x(value: number) {
// this.wfData.patch({ x: value })
// }
// private get y() {
// return this.wfData.state.y
// }
// private set y(value: number) {
// this.wfData.patch({ y: value })
// }
// private windowFormState: TWindowFormState = 'default';
/** 元素信息 */
private targetBounds: IElementRect
/** 最小化前的元素信息 */
private targetPreMinimizeBounds?: IElementRect
/** 最大化前的元素信息 */
private targetPreMaximizedBounds?: IElementRect
static override styles = css`
${unsafeCSS(wfStyle)}
`
protected override createRenderRoot() {
const root = this.attachShadow({ mode: 'closed' })
const sheet = new CSSStyleSheet()
sheet.replaceSync(wfStyle)
root.adoptedStyleSheets = [sheet]
return root
}
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler: (this: WindowFormElement, ev: WindowFormEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler: (ev: WindowFormEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
): void
/**
* 添加受管理的事件监听
* @param type 事件类型
* @param handler 事件处理函数
*/
public addManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler:
| ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any)
| ((ev: WindowFormEventMap[K]) => any),
options?: boolean | AddEventListenerOptions
) {
const wrapped: EventListener = (ev: Event) => {
(handler as any).call(this, ev as WindowFormEventMap[K])
}
this.addEventListener(type, wrapped, options)
this._listeners.push({ type, original: handler, wrapped })
}
public removeManagedEventListener<K extends keyof WindowFormEventMap>(
type: K,
handler:
| ((this: WindowFormElement, ev: WindowFormEventMap[K]) => any)
| ((ev: WindowFormEventMap[K]) => any)
) {
const index = this._listeners.findIndex(
l => l.type === type && l.original === handler
)
if (index !== -1) {
const { type: t, wrapped } = this._listeners[index]
this.removeEventListener(t, wrapped)
this._listeners.splice(index, 1)
}
}
/**
* 移除所有受管理事件监听
*/
public removeAllManagedListeners() {
for (const { type, wrapped } of this._listeners) {
this.removeEventListener(type, wrapped)
}
this._listeners = []
}
override firstUpdated() {
wfem.addEventListener('windowFormFocus', this.windowFormFocusFun)
window.addEventListener('pointerup', this.onPointerUp)
window.addEventListener('pointermove', this.onPointerMove)
this.addEventListener('pointerdown', this.toggleFocus)
const container = this.dragContainer || document.body
const containerRect = container.getBoundingClientRect()
this.x = containerRect.width / 2 - this.width / 2
this.y = containerRect.height / 2 - this.height / 2
this.style.width = `${this.width}px`
this.style.height = `${this.height}px`
this.style.transform = `translate(${this.x}px, ${this.y}px)`
this.targetBounds = {
width: this.offsetWidth,
height: this.offsetHeight,
top: this.x,
left: this.y,
}
}
override disconnectedCallback() {
super.disconnectedCallback()
window.removeEventListener('pointerup', this.onPointerUp)
window.removeEventListener('pointermove', this.onPointerMove)
this.removeEventListener('pointerdown', this.toggleFocus)
wfem.removeEventListener('windowFormFocus', this.windowFormFocusFun)
this.removeAllManagedListeners()
}
private windowFormFocusFun = (id: string) => {
if (id === this.wid) {
this.focused = true
} else {
this.focused = false
}
}
private toggleFocus = () => {
this.focused = !this.focused
wfem.notifyEvent('windowFormFocus', this.wid)
this.dispatchEvent(
new CustomEvent('update:focused', {
detail: this.focused,
bubbles: true,
composed: true,
}),
)
}
// ====== 拖拽 ======
private onTitlePointerDown = (e: PointerEvent) => {
if (e.pointerType === 'mouse' && e.button !== 0) return
if ((e.target as HTMLElement).closest('.controls')) return
e.preventDefault()
this.dragging = true
this.startX = e.clientX
this.startY = e.clientY
this.preX = this.x
this.preY = this.y
this.setPointerCapture?.(e.pointerId)
this.dispatchEvent(
new CustomEvent('windowForm:dragStart', {
detail: { x: this.x, y: this.y },
bubbles: true,
composed: true,
}),
)
}
private onPointerMove = (e: PointerEvent) => {
if (this.dragging) {
const dx = e.clientX - this.startX
const dy = e.clientY - this.startY
const x = this.preX + dx
const y = this.preY + dy
this.applyPosition(x, y, false)
this.dispatchEvent(
new CustomEvent('windowForm:dragMove', {
detail: { x, y },
bubbles: true,
composed: true,
}),
)
} else if (this.resizeDir) {
this.performResize(e)
}
}
private onPointerUp = (e: PointerEvent) => {
if (this.dragging) {
this.dragUp(e)
}
if (this.resizeDir) {
this.resizeUp(e)
}
this.dragging = false
this.resizing = false
this.resizeDir = null
document.body.style.cursor = ''
try {
this.releasePointerCapture?.(e.pointerId)
} catch {}
}
/** 获取所有吸附点 */
private getSnapPoints() {
const snapPoints = { x: [] as number[], y: [] as number[] }
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
const rect = this.getBoundingClientRect()
snapPoints.x = [0, containerRect.width - rect.width]
snapPoints.y = [0, containerRect.height - rect.height]
return snapPoints
}
/**
* 获取最近的吸附点
* @param x 左上角起始点x
* @param y 左上角起始点y
*/
private applySnapping(x: number, y: number) {
let snappedX = x,
snappedY = y
const containerSnap = this.getSnapPoints()
if (this.snapDistance > 0) {
for (const sx of containerSnap.x)
if (Math.abs(x - sx) <= this.snapDistance) {
snappedX = sx
break
}
for (const sy of containerSnap.y)
if (Math.abs(y - sy) <= this.snapDistance) {
snappedY = sy
break
}
}
return { x: snappedX, y: snappedY }
}
private dragUp(e: PointerEvent) {
const snapped = this.applySnapping(this.x, this.y)
if (this.snapAnimation) {
this.animateTo(snapped.x, snapped.y, this.snapAnimationDuration, () => {
this.updateTargetBounds(snapped.x, snapped.y)
this.dispatchEvent(
new CustomEvent('windowForm:dragEnd', {
detail: { x: snapped.x, y: snapped.y },
bubbles: true,
composed: true,
}),
)
})
} else {
this.applyPosition(snapped.x, snapped.y, true)
this.updateTargetBounds(snapped.x, snapped.y)
this.dispatchEvent(
new CustomEvent('windowForm:dragEnd', {
detail: { x: snapped.x, y: snapped.y },
bubbles: true,
composed: true,
}),
)
}
}
private applyPosition(x: number, y: number, isFinal: boolean) {
this.x = x
this.y = y
this.style.transform = `translate(${x}px, ${y}px)`
if (isFinal) this.applyBoundary()
}
private applyBoundary() {
if (this.allowOverflow) return
let { x, y } = { x: this.x, y: this.y }
const rect = this.getBoundingClientRect()
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
x = Math.min(Math.max(x, 0), containerRect.width - rect.width)
y = Math.min(Math.max(y, 0), containerRect.height - rect.height)
this.applyPosition(x, y, false)
}
private animateTo(targetX: number, targetY: number, duration: number, onComplete?: () => void) {
if (this.animationFrame) cancelAnimationFrame(this.animationFrame)
const startX = this.x
const startY = this.y
const deltaX = targetX - startX
const deltaY = targetY - startY
const startTime = performance.now()
const step = (now: number) => {
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
const ease = 1 - Math.pow(1 - progress, 3)
const x = startX + deltaX * ease
const y = startY + deltaY * ease
this.applyPosition(x, y, false)
this.dispatchEvent(
new CustomEvent('windowForm:dragMove', {
detail: { x, y },
bubbles: true,
composed: true,
}),
)
if (progress < 1) {
this.animationFrame = requestAnimationFrame(step)
} else {
this.applyPosition(targetX, targetY, true)
onComplete?.()
}
}
this.animationFrame = requestAnimationFrame(step)
}
// ====== 缩放 ======
private startResize = (dir: TResizeDirection, e: PointerEvent) => {
if (!this.resizable) return
if (e.pointerType === 'mouse' && e.button !== 0) return
e.preventDefault()
e.stopPropagation()
this.resizing = true
this.resizeDir = dir
this.startX = e.clientX
this.startY = e.clientY
const rect = this.getBoundingClientRect()
this.startWidth = rect.width
this.startHeight = rect.height
this.startX_host = rect.left
this.startY_host = rect.top
const target = e.target as HTMLElement
document.body.style.cursor = target.style.cursor || window.getComputedStyle(target).cursor
this.setPointerCapture?.(e.pointerId)
this.dispatchEvent(
new CustomEvent('windowForm:resizeStart', {
detail: { x: this.x, y: this.y, width: this.width, height: this.height, dir },
bubbles: true,
composed: true,
}),
)
}
private performResize(e: PointerEvent) {
if (!this.resizeDir || !this.resizing) return
let newWidth = this.startWidth
let newHeight = this.startHeight
let newX = this.startX_host
let newY = this.startY_host
const dx = e.clientX - this.startX
const dy = e.clientY - this.startY
switch (this.resizeDir) {
case 'r':
newWidth += dx
break
case 'b':
newHeight += dy
break
case 'l':
newWidth -= dx
newX += dx
break
case 't':
newHeight -= dy
newY += dy
break
case 'tl':
newWidth -= dx
newX += dx
newHeight -= dy
newY += dy
break
case 'tr':
newWidth += dx
newHeight -= dy
newY += dy
break
case 'bl':
newWidth -= dx
newX += dx
newHeight += dy
break
case 'br':
newWidth += dx
newHeight += dy
break
}
const d = this.applyResizeBounds(newX, newY, newWidth, newHeight)
this.dispatchEvent(
new CustomEvent('windowForm:resizeMove', {
detail: { dir: this.resizeDir, width: d.width, height: d.height, left: d.left, top: d.top },
bubbles: true,
composed: true,
}),
)
}
/**
* 应用尺寸调整边界
* @param newX 新的X坐标
* @param newY 新的Y坐标
* @param newWidth 新的宽度
* @param newHeight 新的高度
* @private
*/
private applyResizeBounds(
newX: number,
newY: number,
newWidth: number,
newHeight: number,
): {
left: number
top: number
width: number
height: number
} {
// 最小/最大宽高限制
if (this.minWidth != null) newWidth = Math.max(this.minWidth, newWidth)
if (this.maxWidth != null) newWidth = Math.min(this.maxWidth, newWidth)
if (this.minHeight != null) newHeight = Math.max(this.minHeight, newHeight)
if (this.maxHeight != null) newHeight = Math.min(this.maxHeight, newHeight)
// 边界限制
if (this.allowOverflow) {
this.x = newX
this.y = newY
this.width = newWidth
this.height = newHeight
this.style.width = `${newWidth}px`
this.style.height = `${newHeight}px`
this.applyPosition(newX, newY, false)
return {
left: newX,
top: newY,
width: newWidth,
height: newHeight,
}
}
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
newX = Math.min(Math.max(0, newX), containerRect.width - newWidth)
newY = Math.min(Math.max(0, newY), containerRect.height - newHeight)
this.x = newX
this.y = newY
this.width = newWidth
this.height = newHeight
this.style.width = `${newWidth}px`
this.style.height = `${newHeight}px`
this.applyPosition(newX, newY, false)
return {
left: newX,
top: newY,
width: newWidth,
height: newHeight,
}
}
private resizeUp(e: PointerEvent) {
if (!this.resizable) return
this.updateTargetBounds(this.x, this.y, this.width, this.height)
this.dispatchEvent(
new CustomEvent('windowForm:resizeEnd', {
detail: {
dir: this.resizeDir,
width: this.width,
height: this.height,
left: this.x,
top: this.y,
},
bubbles: true,
composed: true,
}),
)
}
// ====== 窗口操作 ======
// 最小化到任务栏
private minimize() {
if (!this.taskbarElementId) return
if (this.windowFormState === 'minimized') return
this.targetPreMinimizeBounds = { ...this.targetBounds }
this.windowFormState = 'minimized'
const taskbar = document.querySelector(this.taskbarElementId)
if (!taskbar) throw new Error('任务栏元素未找到')
const rect = taskbar.getBoundingClientRect()
const startX = this.x
const startY = this.y
const startW = this.offsetWidth
const startH = this.offsetHeight
this.animateWindow(
startX,
startY,
startW,
startH,
rect.left,
rect.top,
rect.width,
rect.height,
400,
() => {
this.style.display = 'none'
this.dispatchEvent(
new CustomEvent('windowForm:stateChange:minimize', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
}),
)
},
)
}
/** 最大化 */
private maximize() {
if (this.windowFormState === 'maximized') {
this.restore()
return
}
this.targetPreMaximizedBounds = { ...this.targetBounds }
this.windowFormState = 'maximized'
const rect = this.getBoundingClientRect()
const startX = this.x
const startY = this.y
const startW = rect.width
const startH = rect.height
const targetX = 0
const targetY = 0
const containerRect = (this.dragContainer || document.body).getBoundingClientRect()
const targetW = containerRect?.width ?? window.innerWidth
const targetH = containerRect?.height ?? window.innerHeight
this.animateWindow(
startX,
startY,
startW,
startH,
targetX,
targetY,
targetW,
targetH,
300,
() => {
this.dispatchEvent(
new CustomEvent('windowForm:stateChange:maximize', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
}),
)
},
)
}
/** 恢复到默认窗体状态 */
private restore(onComplete?: () => void) {
if (this.windowFormState === 'default') return
let b: IElementRect
if (
(this.windowFormState as TWindowFormState) === 'minimized' &&
this.targetPreMinimizeBounds
) {
// 最小化恢复,恢复到最小化前的状态
b = this.targetPreMinimizeBounds
} else if (
(this.windowFormState as TWindowFormState) === 'maximized' &&
this.targetPreMaximizedBounds
) {
// 最大化恢复,恢复到最大化前的默认状态
b = this.targetPreMaximizedBounds
} else {
b = this.targetBounds
}
this.windowFormState = 'default'
this.style.display = 'block'
const startX = this.x
const startY = this.y
const startW = this.offsetWidth
const startH = this.offsetHeight
this.animateWindow(
startX,
startY,
startW,
startH,
b.left,
b.top,
b.width,
b.height,
300,
() => {
onComplete?.()
this.dispatchEvent(
new CustomEvent('windowForm:stateChange:restore', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
}),
)
},
)
}
private windowFormClose() {
this.dispatchEvent(
new CustomEvent('windowForm:close', {
bubbles: true,
composed: true,
}),
)
this.wfData.state.closed = true
}
/**
* 窗体最大化、最小化和恢复默认 动画
* @param startX
* @param startY
* @param startW
* @param startH
* @param targetX
* @param targetY
* @param targetW
* @param targetH
* @param duration
* @param onComplete
* @private
*/
private animateWindow(
startX: number,
startY: number,
startW: number,
startH: number,
targetX: number,
targetY: number,
targetW: number,
targetH: number,
duration: number,
onComplete?: () => void,
) {
const startTime = performance.now()
const step = (now: number) => {
const elapsed = now - startTime
const progress = Math.min(elapsed / duration, 1)
const ease = 1 - Math.pow(1 - progress, 3)
const x = startX + (targetX - startX) * ease
const y = startY + (targetY - startY) * ease
const w = startW + (targetW - startW) * ease
const h = startH + (targetH - startH) * ease
this.style.width = `${w}px`
this.style.height = `${h}px`
this.applyPosition(x, y, false)
if (progress < 1) {
requestAnimationFrame(step)
} else {
this.style.width = `${targetW}px`
this.style.height = `${targetH}px`
this.applyPosition(targetX, targetY, true)
onComplete?.()
this.dispatchEvent(
new CustomEvent('windowForm:stateChange', {
detail: { state: this.windowFormState },
bubbles: true,
composed: true,
}),
)
}
}
requestAnimationFrame(step)
}
private updateTargetBounds(left: number, top: number, width?: number, height?: number) {
this.targetBounds = {
left,
top,
width: width ?? this.offsetWidth,
height: height ?? this.offsetHeight,
}
}
// ====== 渲染 ======
override render() {
return html`
<div class="window">
<div class="titlebar" @pointerdown=${this.onTitlePointerDown}>
<div class="title" title=${this.title}>${this.title}</div>
<div class="controls">
${this.minimizable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.minimize()
}}
>
</button>`
: null}
${this.maximizable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.maximize()
}}
>
${this.windowFormState === 'maximized' ? '▣' : '▢'}
</button>`
: null}
${this.closable
? html`<button
class="ctrl"
@click=${(e: Event) => {
e.stopPropagation()
this.windowFormClose()
}}
>
</button>`
: null}
</div>
</div>
<div class="content"><slot></slot></div>
${this.resizable
? html`
<div
class="resizer t"
@pointerdown=${(e: PointerEvent) => this.startResize('t', e)}
></div>
<div
class="resizer b"
@pointerdown=${(e: PointerEvent) => this.startResize('b', e)}
></div>
<div
class="resizer r"
@pointerdown=${(e: PointerEvent) => this.startResize('r', e)}
></div>
<div
class="resizer l"
@pointerdown=${(e: PointerEvent) => this.startResize('l', e)}
></div>
<div
class="resizer tr"
@pointerdown=${(e: PointerEvent) => this.startResize('tr', e)}
></div>
<div
class="resizer tl"
@pointerdown=${(e: PointerEvent) => this.startResize('tl', e)}
></div>
<div
class="resizer br"
@pointerdown=${(e: PointerEvent) => this.startResize('br', e)}
></div>
<div
class="resizer bl"
@pointerdown=${(e: PointerEvent) => this.startResize('bl', e)}
></div>
`
: null}
</div>
`
}
}
declare global {
interface HTMLElementTagNameMap {
'window-form-element': WindowFormElement;
}
interface WindowFormElementEventMap extends WindowFormEventMap {}
}

View File

@@ -1,101 +0,0 @@
*,
*::before,
*::after {
box-sizing: border-box; /* 使用更直观的盒模型 */
margin: 0;
padding: 0;
}
:host {
position: absolute;
top: 0;
left: 0;
display: block;
z-index: 10;
user-select: none;
--titlebar-height: 32px;
--shadow: 0 10px 30px rgba(0,0,0,0.25);
font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial;
}
:host([focused]) {
z-index: 11;
.window {
border-color: #8338ec;
}
}
:host([windowFormState='maximized']) {
.window {
border-radius: 0;
box-shadow: none;
}
}
.window {
position: absolute;
box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.25));
background: linear-gradient(#ffffff, #f6f6f6);
border: 1px solid rgba(0,0,0,0.08);
border-radius: 6px;
overflow: hidden;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
&.focus {
border-color: #3a86ff;
}
}
.titlebar {
height: var(--titlebar-height);
display: flex;
align-items: center;
padding: 0 8px;
gap: 8px;
background: linear-gradient(#f2f2f2, #e9e9e9);
border-bottom: 1px solid rgba(0,0,0,0.06);
.title {
font-size: 13px;
font-weight: 600;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: #111;
}
.controls {
display: flex; gap: 6px;
button.ctrl {
width: 34px;
height: 24px;
border: none;
background: transparent;
border-radius: 4px;
cursor: pointer;
font-weight: 600;
font-size: 12px;
&:hover {
background: rgba(0,0,0,0.06);
}
}
}
}
.content { flex: 1; overflow: auto; padding: 12px; background: transparent; }
.resizer { position: absolute; z-index: 20; }
.resizer.t { height: 6px; left: 0; right: 0; top: -3px; cursor: ns-resize; }
.resizer.b { height: 6px; left: 0; right: 0; bottom: -3px; cursor: ns-resize; }
.resizer.r { width: 6px; top: 0; bottom: 0; right: -3px; cursor: ew-resize; }
.resizer.l { width: 6px; top: 0; bottom: 0; left: -3px; cursor: ew-resize; }
.resizer.tr { width: 12px; height: 12px; right: -6px; top: -6px; cursor: nesw-resize; }
.resizer.tl { width: 12px; height: 12px; left: -6px; top: -6px; cursor: nwse-resize; }
.resizer.br { width: 12px; height: 12px; right: -6px; bottom: -6px; cursor: nwse-resize; }
.resizer.bl { width: 12px; height: 12px; left: -6px; bottom: -6px; cursor: nesw-resize; }

View File

@@ -1,20 +0,0 @@
import type { WindowFormEventMap } from '@/core/window/ui/WindowFormElement.ts'
export function addWindowFormEventListener<K extends keyof WindowFormEventMap>(
el: HTMLElement,
type: K,
listener: (ev: WindowFormEventMap[K]) => any,
options?: boolean | AddEventListenerOptions
) {
// 强制类型转换,保证 TS 不报错
el.addEventListener(type, listener as EventListener, options);
}
export function removeWindowFormEventListener<K extends keyof WindowFormEventMap>(
el: HTMLElement,
type: K,
listener: (ev: WindowFormEventMap[K]) => any,
options?: boolean | EventListenerOptions
) {
el.removeEventListener(type, listener as EventListener, options);
}

View File

@@ -1,15 +1,62 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { naiveUi } from '@/common/naive-ui/components.ts'
import { SystemServiceIntegration } from '@/services/SystemServiceIntegration'
import { registerBuiltInApps } from '@/apps'
import 'virtual:uno.css'
import './css/basic.css'
import App from './ui/App.vue'
// 注册内置应用
registerBuiltInApps()
// 初始化系统服务
const systemService = new SystemServiceIntegration({
debug: import.meta.env.DEV,
enablePerformanceMonitoring: true,
enableSecurityAudit: true
})
// 创建应用实例
const app = createApp(App)
// 注册插件
app.use(createPinia())
app.use(naiveUi)
app.mount('#app')
// 提供系统服务给组件使用
app.provide('systemService', systemService)
// 初始化系统服务然后挂载应用
systemService.initialize()
.then(() => {
app.mount('#app')
console.log('桌面系统启动完成')
})
.catch((error) => {
console.error('系统启动失败:', error)
// 显示错误信息
document.body.innerHTML = `
<div style="display: flex; justify-content: center; align-items: center; height: 100vh; font-family: sans-serif;">
<div style="text-align: center; color: #e74c3c;">
<h1>系统启动失败</h1>
<p>错误信息: ${error.message}</p>
<button onclick="location.reload()" style="padding: 10px 20px; margin-top: 20px; cursor: pointer;">
重新加载
</button>
</div>
</div>
`
})
// 全局错误处理
app.config.errorHandler = (error, instance, info) => {
console.error('Vue应用错误:', error, info)
}
// 在页面卸载时清理系统服务
window.addEventListener('beforeunload', () => {
systemService.shutdown()
})

702
src/sdk/index.ts Normal file
View File

@@ -0,0 +1,702 @@
import type {
SystemDesktopSDK,
SDKConfig,
APIResponse,
WindowSDK,
StorageSDK,
NetworkSDK,
EventSDK,
UISDK,
SystemSDK,
WindowState,
WindowEvents,
StorageEvents,
NetworkRequestConfig,
NetworkResponse,
EventMessage,
EventSubscriptionConfig,
DialogOptions,
NotificationOptions,
FilePickerOptions,
SystemInfo,
AppInfo,
PermissionStatus,
} from './types'
/**
* SDK基础类
*/
abstract class SDKBase {
protected appId: string = ''
protected initialized: boolean = false
/**
* 发送消息到系统
*/
protected sendToSystem<T = any>(type: string, data?: any): Promise<T> {
return new Promise((resolve, reject) => {
const requestId = Date.now().toString() + Math.random().toString(36).substr(2, 9)
const handler = (event: MessageEvent) => {
if (event.data?.type === 'system:response' && event.data?.requestId === requestId) {
window.removeEventListener('message', handler)
if (event.data.success) {
resolve(event.data.data)
} else {
reject(new Error(event.data.error || '系统调用失败'))
}
}
}
window.addEventListener('message', handler)
// 发送消息到父窗口(系统)
window.parent.postMessage(
{
type: 'sdk:call',
requestId,
method: type,
data,
appId: this.appId,
},
'*',
)
// 设置超时
setTimeout(() => {
window.removeEventListener('message', handler)
reject(new Error('系统调用超时'))
}, 10000)
})
}
/**
* 包装API响应
*/
protected wrapResponse<T>(promise: Promise<T>): Promise<APIResponse<T>> {
return promise
.then((data) => ({ success: true, data }))
.catch((error) => ({
success: false,
error: error.message || '未知错误',
code: error.code || -1,
}))
}
}
/**
* 窗体SDK实现
*/
class WindowSDKImpl extends SDKBase implements WindowSDK {
private eventListeners = new Map<keyof WindowEvents, Set<Function>>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async setTitle(title: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.setTitle', { title }))
}
async resize(width: number, height: number): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.resize', { width, height }))
}
async move(x: number, y: number): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.move', { x, y }))
}
async minimize(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.minimize'))
}
async maximize(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.maximize'))
}
async restore(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.restore'))
}
async fullscreen(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.fullscreen'))
}
async close(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('window.close'))
}
async getState(): Promise<APIResponse<WindowState>> {
return this.wrapResponse(this.sendToSystem('window.getState'))
}
async getSize(): Promise<APIResponse<{ width: number; height: number }>> {
return this.wrapResponse(this.sendToSystem('window.getSize'))
}
async getPosition(): Promise<APIResponse<{ x: number; y: number }>> {
return this.wrapResponse(this.sendToSystem('window.getPosition'))
}
on<K extends keyof WindowEvents>(event: K, callback: WindowEvents[K]): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set())
}
this.eventListeners.get(event)!.add(callback)
}
off<K extends keyof WindowEvents>(event: K, callback?: WindowEvents[K]): void {
if (callback) {
this.eventListeners.get(event)?.delete(callback)
} else {
this.eventListeners.delete(event)
}
}
private setupEventListeners(): void {
window.addEventListener('message', (event) => {
if (event.data?.type?.startsWith('system:window:')) {
const eventType = event.data.type.replace('system:window:', '') as keyof WindowEvents
const listeners = this.eventListeners.get(eventType)
if (listeners) {
listeners.forEach((callback) => {
try {
callback(...(event.data.args || []))
} catch (error) {
console.error('窗体事件处理错误:', error)
}
})
}
}
})
}
}
/**
* 存储SDK实现
*/
class StorageSDKImpl extends SDKBase implements StorageSDK {
private eventListeners = new Map<keyof StorageEvents, Set<Function>>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async set(key: string, value: any): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.set', { key, value }))
}
async get<T = any>(key: string): Promise<APIResponse<T | null>> {
return this.wrapResponse(this.sendToSystem('storage.get', { key }))
}
async remove(key: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.remove', { key }))
}
async clear(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.clear'))
}
async keys(): Promise<APIResponse<string[]>> {
return this.wrapResponse(this.sendToSystem('storage.keys'))
}
async has(key: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('storage.has', { key }))
}
async getStats(): Promise<APIResponse<any>> {
return this.wrapResponse(this.sendToSystem('storage.getStats'))
}
on<K extends keyof StorageEvents>(event: K, callback: StorageEvents[K]): void {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, new Set())
}
this.eventListeners.get(event)!.add(callback)
}
off<K extends keyof StorageEvents>(event: K, callback?: StorageEvents[K]): void {
if (callback) {
this.eventListeners.get(event)?.delete(callback)
} else {
this.eventListeners.delete(event)
}
}
private setupEventListeners(): void {
window.addEventListener('message', (event) => {
if (event.data?.type?.startsWith('system:storage:')) {
const eventType = event.data.type.replace('system:storage:', '') as keyof StorageEvents
const listeners = this.eventListeners.get(eventType)
if (listeners) {
listeners.forEach((callback) => {
try {
callback(...(event.data.args || []))
} catch (error) {
console.error('存储事件处理错误:', error)
}
})
}
}
})
}
}
/**
* 网络SDK实现
*/
class NetworkSDKImpl extends SDKBase implements NetworkSDK {
constructor(appId: string) {
super()
this.appId = appId
}
async request<T = any>(
url: string,
config?: NetworkRequestConfig,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.wrapResponse(this.sendToSystem('network.request', { url, config }))
}
async get<T = any>(
url: string,
config?: Omit<NetworkRequestConfig, 'method'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'GET' })
}
async post<T = any>(
url: string,
data?: any,
config?: Omit<NetworkRequestConfig, 'method' | 'body'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'POST', body: data })
}
async put<T = any>(
url: string,
data?: any,
config?: Omit<NetworkRequestConfig, 'method' | 'body'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'PUT', body: data })
}
async delete<T = any>(
url: string,
config?: Omit<NetworkRequestConfig, 'method'>,
): Promise<APIResponse<NetworkResponse<T>>> {
return this.request(url, { ...config, method: 'DELETE' })
}
async upload(
url: string,
file: File | Blob,
onProgress?: (loaded: number, total: number) => void,
): Promise<APIResponse<NetworkResponse>> {
return this.wrapResponse(
this.sendToSystem('network.upload', { url, file, hasProgressCallback: !!onProgress }),
)
}
async download(
url: string,
filename?: string,
onProgress?: (loaded: number, total: number) => void,
): Promise<APIResponse<Blob>> {
return this.wrapResponse(
this.sendToSystem('network.download', { url, filename, hasProgressCallback: !!onProgress }),
)
}
async isOnline(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('network.isOnline'))
}
async getStats(): Promise<
APIResponse<{ requestCount: number; failureCount: number; averageTime: number }>
> {
return this.wrapResponse(this.sendToSystem('network.getStats'))
}
}
/**
* 事件SDK实现
*/
class EventSDKImpl extends SDKBase implements EventSDK {
private subscriptions = new Map<string, Function>()
constructor(appId: string) {
super()
this.appId = appId
this.setupEventListeners()
}
async emit<T = any>(channel: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.emit', { channel, data }))
}
async on<T = any>(
channel: string,
callback: (message: EventMessage<T>) => void,
config?: EventSubscriptionConfig,
): Promise<APIResponse<string>> {
const result = await this.wrapResponse(this.sendToSystem('events.on', { channel, config }))
if (result.success && result.data) {
this.subscriptions.set(result.data, callback)
}
return result
}
async off(subscriptionId: string): Promise<APIResponse<boolean>> {
const result = await this.wrapResponse(this.sendToSystem('events.off', { subscriptionId }))
if (result.success) {
this.subscriptions.delete(subscriptionId)
}
return result
}
async broadcast<T = any>(channel: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.broadcast', { channel, data }))
}
async sendTo<T = any>(targetAppId: string, data: T): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('events.sendTo', { targetAppId, data }))
}
async getSubscriberCount(channel: string): Promise<APIResponse<number>> {
return this.wrapResponse(this.sendToSystem('events.getSubscriberCount', { channel }))
}
async getChannels(): Promise<APIResponse<string[]>> {
return this.wrapResponse(this.sendToSystem('events.getChannels'))
}
private setupEventListeners(): void {
window.addEventListener('message', (event) => {
if (event.data?.type === 'system:event' && event.data?.subscriptionId) {
const callback = this.subscriptions.get(event.data.subscriptionId)
if (callback) {
try {
callback(event.data.message)
} catch (error) {
console.error('事件回调处理错误:', error)
}
}
}
})
}
}
/**
* UI SDK实现
*/
class UISDKImpl extends SDKBase implements UISDK {
constructor(appId: string) {
super()
this.appId = appId
}
async showDialog(
options: DialogOptions,
): Promise<APIResponse<{ buttonIndex: number; inputValue?: string }>> {
return this.wrapResponse(this.sendToSystem('ui.showDialog', options))
}
async showNotification(options: NotificationOptions): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showNotification', options))
}
async showFilePicker(options?: FilePickerOptions): Promise<APIResponse<FileList | null>> {
return this.wrapResponse(this.sendToSystem('ui.showFilePicker', options))
}
async showSaveDialog(defaultName?: string, accept?: string): Promise<APIResponse<string | null>> {
return this.wrapResponse(this.sendToSystem('ui.showSaveDialog', { defaultName, accept }))
}
async showToast(
message: string,
type?: 'info' | 'success' | 'warning' | 'error',
duration?: number,
): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showToast', { message, type, duration }))
}
async showLoading(message?: string): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showLoading', { message }))
}
async hideLoading(id: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('ui.hideLoading', { id }))
}
async showProgress(options: {
title?: string
message?: string
progress: number
}): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('ui.showProgress', options))
}
async updateProgress(
id: string,
progress: number,
message?: string,
): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('ui.updateProgress', { id, progress, message }))
}
async hideProgress(id: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('ui.hideProgress', { id }))
}
}
/**
* 系统SDK实现
*/
class SystemSDKImpl extends SDKBase implements SystemSDK {
constructor(appId: string) {
super()
this.appId = appId
}
async getSystemInfo(): Promise<APIResponse<SystemInfo>> {
return this.wrapResponse(this.sendToSystem('system.getSystemInfo'))
}
async getAppInfo(): Promise<APIResponse<AppInfo>> {
return this.wrapResponse(this.sendToSystem('system.getAppInfo'))
}
async requestPermission(
permission: string,
reason?: string,
): Promise<APIResponse<PermissionStatus>> {
return this.wrapResponse(this.sendToSystem('system.requestPermission', { permission, reason }))
}
async checkPermission(permission: string): Promise<APIResponse<PermissionStatus>> {
return this.wrapResponse(this.sendToSystem('system.checkPermission', { permission }))
}
async getClipboard(): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('system.getClipboard'))
}
async setClipboard(text: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('system.setClipboard', { text }))
}
async openExternal(url: string): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('system.openExternal', { url }))
}
async getCurrentTime(): Promise<APIResponse<Date>> {
const result = await this.wrapResponse(this.sendToSystem('system.getCurrentTime'))
if (result.success && result.data) {
result.data = new Date(result.data)
}
return result
}
async generateUUID(): Promise<APIResponse<string>> {
return this.wrapResponse(this.sendToSystem('system.generateUUID'))
}
async exit(): Promise<APIResponse<boolean>> {
return this.wrapResponse(this.sendToSystem('system.exit'))
}
}
/**
* 主SDK实现类
*/
class SystemDesktopSDKImpl implements SystemDesktopSDK {
readonly version: string = '1.0.0'
private _appId: string = ''
private _initialized: boolean = false
private _window!: WindowSDK
private _storage!: StorageSDK
private _network!: NetworkSDK
private _events!: EventSDK
private _ui!: UISDK
private _system!: SystemSDK
get appId(): string {
return this._appId
}
get initialized(): boolean {
return this._initialized
}
get window(): WindowSDK {
this.checkInitialized()
return this._window
}
get storage(): StorageSDK {
this.checkInitialized()
return this._storage
}
get network(): NetworkSDK {
this.checkInitialized()
return this._network
}
get events(): EventSDK {
this.checkInitialized()
return this._events
}
get ui(): UISDK {
this.checkInitialized()
return this._ui
}
get system(): SystemSDK {
this.checkInitialized()
return this._system
}
async init(config: SDKConfig): Promise<APIResponse<boolean>> {
try {
if (this._initialized) {
return { success: false, error: 'SDK已初始化' }
}
this._appId = config.appId
// 初始化各个子模块
this._window = new WindowSDKImpl(this._appId)
this._storage = new StorageSDKImpl(this._appId)
this._network = new NetworkSDKImpl(this._appId)
this._events = new EventSDKImpl(this._appId)
this._ui = new UISDKImpl(this._appId)
this._system = new SystemSDKImpl(this._appId)
// 向系统注册应用
const response = await this.sendToSystem('sdk.init', config)
if (response.success) {
this._initialized = true
console.log(`SDK已初始化应用ID: ${this._appId}`)
}
return response
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '初始化失败',
}
}
}
async destroy(): Promise<APIResponse<boolean>> {
try {
if (!this._initialized) {
return { success: false, error: 'SDK未初始化' }
}
const response = await this.sendToSystem('sdk.destroy', { appId: this._appId })
if (response.success) {
this._initialized = false
this._appId = ''
console.log('SDK已销毁')
}
return response
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '销毁失败',
}
}
}
async getStatus(): Promise<
APIResponse<{ initialized: boolean; connected: boolean; permissions: string[] }>
> {
try {
const response = await this.sendToSystem('sdk.getStatus', { appId: this._appId })
return {
success: true,
data: {
initialized: this._initialized,
connected: response.data.connected || false,
permissions: response.data.permissions || [],
},
}
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : '获取状态失败',
}
}
}
private checkInitialized(): void {
if (!this._initialized) {
throw new Error('SDK未初始化请先调用init()方法')
}
}
private sendToSystem<T = any>(
type: string,
data?: any,
): Promise<{ success: boolean; data?: T; error?: string }> {
return new Promise((resolve, reject) => {
const requestId = Date.now().toString() + Math.random().toString(36).substr(2, 9)
const handler = (event: MessageEvent) => {
if (event.data?.type === 'system:response' && event.data?.requestId === requestId) {
window.removeEventListener('message', handler)
resolve(event.data)
}
}
window.addEventListener('message', handler)
window.parent.postMessage(
{
type: 'sdk:call',
requestId,
method: type,
data,
appId: this._appId,
},
'*',
)
setTimeout(() => {
window.removeEventListener('message', handler)
reject(new Error('系统调用超时'))
}, 10000)
})
}
}
// 创建全局SDK实例
const SystemSDK = new SystemDesktopSDKImpl()
// 导出SDK实例
export default SystemSDK
// 在window对象上挂载SDK
if (typeof window !== 'undefined') {
window.SystemSDK = SystemSDK
}

638
src/sdk/types.ts Normal file
View File

@@ -0,0 +1,638 @@
/**
* 系统SDK主接口
* 为第三方应用提供统一的系统服务访问接口
*/
// =============================================================================
// 核心类型定义
// =============================================================================
/**
* SDK初始化配置
*/
export interface SDKConfig {
appId: string
appName: string
version: string
permissions: string[]
debug?: boolean
}
/**
* API响应结果包装器
*/
export interface APIResponse<T = any> {
success: boolean
data?: T
error?: string
code?: number
}
/**
* 事件回调函数类型
*/
export type EventCallback<T = any> = (data: T) => void | Promise<void>
/**
* 权限状态枚举
*/
export enum PermissionStatus {
GRANTED = 'granted',
DENIED = 'denied',
PROMPT = 'prompt'
}
// =============================================================================
// 窗体SDK接口
// =============================================================================
/**
* 窗体状态
*/
export enum WindowState {
NORMAL = 'normal',
MINIMIZED = 'minimized',
MAXIMIZED = 'maximized',
FULLSCREEN = 'fullscreen'
}
/**
* 窗体事件类型
*/
export interface WindowEvents {
onResize: (width: number, height: number) => void
onMove: (x: number, y: number) => void
onStateChange: (state: WindowState) => void
onFocus: () => void
onBlur: () => void
onClose: () => void
}
/**
* 窗体SDK接口
*/
export interface WindowSDK {
/**
* 设置窗体标题
*/
setTitle(title: string): Promise<APIResponse<boolean>>
/**
* 调整窗体尺寸
*/
resize(width: number, height: number): Promise<APIResponse<boolean>>
/**
* 移动窗体位置
*/
move(x: number, y: number): Promise<APIResponse<boolean>>
/**
* 最小化窗体
*/
minimize(): Promise<APIResponse<boolean>>
/**
* 最大化窗体
*/
maximize(): Promise<APIResponse<boolean>>
/**
* 还原窗体
*/
restore(): Promise<APIResponse<boolean>>
/**
* 全屏显示
*/
fullscreen(): Promise<APIResponse<boolean>>
/**
* 关闭窗体
*/
close(): Promise<APIResponse<boolean>>
/**
* 获取当前窗体状态
*/
getState(): Promise<APIResponse<WindowState>>
/**
* 获取窗体尺寸
*/
getSize(): Promise<APIResponse<{ width: number; height: number }>>
/**
* 获取窗体位置
*/
getPosition(): Promise<APIResponse<{ x: number; y: number }>>
/**
* 监听窗体事件
*/
on<K extends keyof WindowEvents>(event: K, callback: WindowEvents[K]): void
/**
* 移除事件监听器
*/
off<K extends keyof WindowEvents>(event: K, callback?: WindowEvents[K]): void
}
// =============================================================================
// 存储SDK接口
// =============================================================================
/**
* 存储事件类型
*/
export interface StorageEvents {
onChange: (key: string, newValue: any, oldValue: any) => void
onQuotaExceeded: (usedSpace: number, maxSpace: number) => void
}
/**
* 存储使用统计
*/
export interface StorageStats {
usedSpace: number // 已使用空间(MB)
maxSpace: number // 最大空间(MB)
keysCount: number // 键数量
lastAccessed: Date
}
/**
* 存储SDK接口
*/
export interface StorageSDK {
/**
* 存储数据
*/
set(key: string, value: any): Promise<APIResponse<boolean>>
/**
* 获取数据
*/
get<T = any>(key: string): Promise<APIResponse<T | null>>
/**
* 删除数据
*/
remove(key: string): Promise<APIResponse<boolean>>
/**
* 清空所有数据
*/
clear(): Promise<APIResponse<boolean>>
/**
* 获取所有键名
*/
keys(): Promise<APIResponse<string[]>>
/**
* 检查键是否存在
*/
has(key: string): Promise<APIResponse<boolean>>
/**
* 获取存储使用统计
*/
getStats(): Promise<APIResponse<StorageStats>>
/**
* 监听存储变化
*/
on<K extends keyof StorageEvents>(event: K, callback: StorageEvents[K]): void
/**
* 移除事件监听器
*/
off<K extends keyof StorageEvents>(event: K, callback?: StorageEvents[K]): void
}
// =============================================================================
// 网络SDK接口
// =============================================================================
/**
* HTTP方法
*/
export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS'
/**
* 网络请求配置
*/
export interface NetworkRequestConfig {
method?: HTTPMethod
headers?: Record<string, string>
body?: any
timeout?: number
responseType?: 'json' | 'text' | 'blob' | 'arrayBuffer'
}
/**
* 网络响应
*/
export interface NetworkResponse<T = any> {
data: T
status: number
statusText: string
headers: Record<string, string>
url: string
}
/**
* 上传进度回调
*/
export type UploadProgressCallback = (loaded: number, total: number) => void
/**
* 下载进度回调
*/
export type DownloadProgressCallback = (loaded: number, total: number) => void
/**
* 网络SDK接口
*/
export interface NetworkSDK {
/**
* 发送HTTP请求
*/
request<T = any>(url: string, config?: NetworkRequestConfig): Promise<APIResponse<NetworkResponse<T>>>
/**
* GET请求
*/
get<T = any>(url: string, config?: Omit<NetworkRequestConfig, 'method'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* POST请求
*/
post<T = any>(url: string, data?: any, config?: Omit<NetworkRequestConfig, 'method' | 'body'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* PUT请求
*/
put<T = any>(url: string, data?: any, config?: Omit<NetworkRequestConfig, 'method' | 'body'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* DELETE请求
*/
delete<T = any>(url: string, config?: Omit<NetworkRequestConfig, 'method'>): Promise<APIResponse<NetworkResponse<T>>>
/**
* 上传文件
*/
upload(url: string, file: File | Blob, onProgress?: UploadProgressCallback): Promise<APIResponse<NetworkResponse>>
/**
* 下载文件
*/
download(url: string, filename?: string, onProgress?: DownloadProgressCallback): Promise<APIResponse<Blob>>
/**
* 检查网络状态
*/
isOnline(): Promise<APIResponse<boolean>>
/**
* 获取网络请求统计
*/
getStats(): Promise<APIResponse<{ requestCount: number; failureCount: number; averageTime: number }>>
}
// =============================================================================
// 事件SDK接口
// =============================================================================
/**
* 事件消息
*/
export interface EventMessage<T = any> {
id: string
channel: string
data: T
senderId: string
timestamp: Date
}
/**
* 事件订阅配置
*/
export interface EventSubscriptionConfig {
filter?: (message: EventMessage) => boolean
once?: boolean // 只监听一次
}
/**
* 事件SDK接口
*/
export interface EventSDK {
/**
* 发送事件消息
*/
emit<T = any>(channel: string, data: T): Promise<APIResponse<string>>
/**
* 订阅事件频道
*/
on<T = any>(
channel: string,
callback: (message: EventMessage<T>) => void,
config?: EventSubscriptionConfig
): Promise<APIResponse<string>>
/**
* 取消订阅
*/
off(subscriptionId: string): Promise<APIResponse<boolean>>
/**
* 广播消息
*/
broadcast<T = any>(channel: string, data: T): Promise<APIResponse<string>>
/**
* 发送点对点消息
*/
sendTo<T = any>(targetAppId: string, data: T): Promise<APIResponse<string>>
/**
* 获取频道订阅者数量
*/
getSubscriberCount(channel: string): Promise<APIResponse<number>>
/**
* 获取可用频道列表
*/
getChannels(): Promise<APIResponse<string[]>>
}
// =============================================================================
// UI SDK接口
// =============================================================================
/**
* 对话框类型
*/
export enum DialogType {
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
ERROR = 'error',
CONFIRM = 'confirm'
}
/**
* 对话框选项
*/
export interface DialogOptions {
title?: string
message: string
type?: DialogType
buttons?: string[]
defaultButton?: number
cancelButton?: number
}
/**
* 通知选项
*/
export interface NotificationOptions {
title: string
body: string
icon?: string
duration?: number // 显示时长(毫秒)
actions?: Array<{ title: string; action: string }>
}
/**
* 文件选择选项
*/
export interface FilePickerOptions {
accept?: string // 文件类型过滤
multiple?: boolean // 是否多选
directory?: boolean // 是否选择目录
}
/**
* UI SDK接口
*/
export interface UISDK {
/**
* 显示对话框
*/
showDialog(options: DialogOptions): Promise<APIResponse<{ buttonIndex: number; inputValue?: string }>>
/**
* 显示通知
*/
showNotification(options: NotificationOptions): Promise<APIResponse<string>>
/**
* 显示文件选择器
*/
showFilePicker(options?: FilePickerOptions): Promise<APIResponse<FileList | null>>
/**
* 显示保存文件对话框
*/
showSaveDialog(defaultName?: string, accept?: string): Promise<APIResponse<string | null>>
/**
* 显示Toast消息
*/
showToast(message: string, type?: 'info' | 'success' | 'warning' | 'error', duration?: number): Promise<APIResponse<string>>
/**
* 显示加载指示器
*/
showLoading(message?: string): Promise<APIResponse<string>>
/**
* 隐藏加载指示器
*/
hideLoading(id: string): Promise<APIResponse<boolean>>
/**
* 显示进度条
*/
showProgress(options: { title?: string; message?: string; progress: number }): Promise<APIResponse<string>>
/**
* 更新进度条
*/
updateProgress(id: string, progress: number, message?: string): Promise<APIResponse<boolean>>
/**
* 隐藏进度条
*/
hideProgress(id: string): Promise<APIResponse<boolean>>
}
// =============================================================================
// 系统SDK接口
// =============================================================================
/**
* 系统信息
*/
export interface SystemInfo {
platform: string
userAgent: string
language: string
timezone: string
screenResolution: { width: number; height: number }
colorDepth: number
pixelRatio: number
}
/**
* 应用信息
*/
export interface AppInfo {
id: string
name: string
version: string
permissions: string[]
createdAt: Date
lastActiveAt: Date
}
/**
* 系统SDK接口
*/
export interface SystemSDK {
/**
* 获取系统信息
*/
getSystemInfo(): Promise<APIResponse<SystemInfo>>
/**
* 获取当前应用信息
*/
getAppInfo(): Promise<APIResponse<AppInfo>>
/**
* 请求权限
*/
requestPermission(permission: string, reason?: string): Promise<APIResponse<PermissionStatus>>
/**
* 检查权限状态
*/
checkPermission(permission: string): Promise<APIResponse<PermissionStatus>>
/**
* 获取剪贴板内容
*/
getClipboard(): Promise<APIResponse<string>>
/**
* 设置剪贴板内容
*/
setClipboard(text: string): Promise<APIResponse<boolean>>
/**
* 打开外部链接
*/
openExternal(url: string): Promise<APIResponse<boolean>>
/**
* 获取当前时间
*/
getCurrentTime(): Promise<APIResponse<Date>>
/**
* 生成UUID
*/
generateUUID(): Promise<APIResponse<string>>
/**
* 退出应用
*/
exit(): Promise<APIResponse<boolean>>
}
// =============================================================================
// 主SDK接口
// =============================================================================
/**
* 系统SDK主接口
* 整合所有子模块SDK
*/
export interface SystemDesktopSDK {
/**
* SDK版本
*/
readonly version: string
/**
* 当前应用ID
*/
readonly appId: string
/**
* 是否已初始化
*/
readonly initialized: boolean
/**
* 窗体操作SDK
*/
readonly window: WindowSDK
/**
* 存储操作SDK
*/
readonly storage: StorageSDK
/**
* 网络请求SDK
*/
readonly network: NetworkSDK
/**
* 事件通信SDK
*/
readonly events: EventSDK
/**
* UI操作SDK
*/
readonly ui: UISDK
/**
* 系统操作SDK
*/
readonly system: SystemSDK
/**
* 初始化SDK
*/
init(config: SDKConfig): Promise<APIResponse<boolean>>
/**
* 销毁SDK
*/
destroy(): Promise<APIResponse<boolean>>
/**
* 获取SDK状态
*/
getStatus(): Promise<APIResponse<{ initialized: boolean; connected: boolean; permissions: string[] }>>
}
// =============================================================================
// 全局类型声明
// =============================================================================
declare global {
interface Window {
/**
* 系统桌面SDK全局实例
*/
SystemSDK: SystemDesktopSDK
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,639 @@
import { reactive, ref } from 'vue'
import type { IEventBuilder } from '@/events/IEventBuilder'
import { v4 as uuidv4 } from 'uuid'
/**
* 消息类型枚举
*/
export enum MessageType {
SYSTEM = 'system',
APPLICATION = 'application',
USER_INTERACTION = 'user_interaction',
CROSS_APP = 'cross_app',
BROADCAST = 'broadcast'
}
/**
* 消息优先级枚举
*/
export enum MessagePriority {
LOW = 0,
NORMAL = 1,
HIGH = 2,
CRITICAL = 3
}
/**
* 消息状态枚举
*/
export enum MessageStatus {
PENDING = 'pending',
SENT = 'sent',
DELIVERED = 'delivered',
FAILED = 'failed',
EXPIRED = 'expired'
}
/**
* 事件消息接口
*/
export interface EventMessage {
id: string
type: MessageType
priority: MessagePriority
senderId: string
receiverId?: string // undefined表示广播消息
channel: string
payload: any
timestamp: Date
expiresAt?: Date
status: MessageStatus
retryCount: number
maxRetries: number
}
/**
* 事件订阅者接口
*/
export interface EventSubscriber {
id: string
appId: string
channel: string
handler: (message: EventMessage) => void | Promise<void>
filter?: (message: EventMessage) => boolean
priority: MessagePriority
createdAt: Date
active: boolean
}
/**
* 通信通道接口
*/
export interface CommunicationChannel {
name: string
description: string
restricted: boolean // 是否需要权限
allowedApps: string[] // 允许访问的应用ID列表
maxMessageSize: number // 最大消息大小(字节)
messageRetention: number // 消息保留时间(毫秒)
}
/**
* 事件统计信息
*/
export interface EventStatistics {
totalMessagesSent: number
totalMessagesReceived: number
totalBroadcasts: number
failedMessages: number
activeSubscribers: number
channelUsage: Map<string, number>
}
/**
* 事件通信服务类
*/
export class EventCommunicationService {
private subscribers = reactive(new Map<string, EventSubscriber>())
private messageQueue = reactive(new Map<string, EventMessage[]>()) // 按应用分组的消息队列
private messageHistory = reactive(new Map<string, EventMessage[]>()) // 消息历史记录
private channels = reactive(new Map<string, CommunicationChannel>())
private statistics = reactive<EventStatistics>({
totalMessagesSent: 0,
totalMessagesReceived: 0,
totalBroadcasts: 0,
failedMessages: 0,
activeSubscribers: 0,
channelUsage: new Map()
})
private processingInterval: number | null = null
private eventBus: IEventBuilder<any>
constructor(eventBus: IEventBuilder<any>) {
this.eventBus = eventBus
this.initializeDefaultChannels()
this.startMessageProcessing()
}
/**
* 订阅事件频道
*/
subscribe(
appId: string,
channel: string,
handler: (message: EventMessage) => void | Promise<void>,
options: {
filter?: (message: EventMessage) => boolean
priority?: MessagePriority
} = {}
): string {
// 检查通道权限
if (!this.canAccessChannel(appId, channel)) {
throw new Error(`应用 ${appId} 无权访问频道 ${channel}`)
}
const subscriberId = uuidv4()
const subscriber: EventSubscriber = {
id: subscriberId,
appId,
channel,
handler,
filter: options.filter,
priority: options.priority || MessagePriority.NORMAL,
createdAt: new Date(),
active: true
}
this.subscribers.set(subscriberId, subscriber)
this.updateActiveSubscribersCount()
console.log(`应用 ${appId} 订阅了频道 ${channel}`)
return subscriberId
}
/**
* 取消订阅
*/
unsubscribe(subscriberId: string): boolean {
const result = this.subscribers.delete(subscriberId)
if (result) {
this.updateActiveSubscribersCount()
console.log(`取消订阅: ${subscriberId}`)
}
return result
}
/**
* 发送消息
*/
async sendMessage(
senderId: string,
channel: string,
payload: any,
options: {
receiverId?: string
priority?: MessagePriority
type?: MessageType
expiresIn?: number // 过期时间(毫秒)
maxRetries?: number
} = {}
): Promise<string> {
// 检查发送者权限
if (!this.canAccessChannel(senderId, channel)) {
throw new Error(`应用 ${senderId} 无权向频道 ${channel} 发送消息`)
}
// 检查消息大小
const messageSize = JSON.stringify(payload).length
const channelConfig = this.channels.get(channel)
if (channelConfig && messageSize > channelConfig.maxMessageSize) {
throw new Error(`消息大小超出限制: ${messageSize} > ${channelConfig.maxMessageSize}`)
}
const messageId = uuidv4()
const now = new Date()
const message: EventMessage = {
id: messageId,
type: options.type || MessageType.APPLICATION,
priority: options.priority || MessagePriority.NORMAL,
senderId,
receiverId: options.receiverId,
channel,
payload,
timestamp: now,
expiresAt: options.expiresIn ? new Date(now.getTime() + options.expiresIn) : undefined,
status: MessageStatus.PENDING,
retryCount: 0,
maxRetries: options.maxRetries || 3
}
// 如果是点对点消息,直接发送
if (options.receiverId) {
await this.deliverMessage(message)
} else {
// 广播消息,加入队列处理
this.addToQueue(message)
}
// 更新统计信息
this.statistics.totalMessagesSent++
if (!options.receiverId) {
this.statistics.totalBroadcasts++
}
const channelUsage = this.statistics.channelUsage.get(channel) || 0
this.statistics.channelUsage.set(channel, channelUsage + 1)
// 记录消息历史
this.recordMessage(message)
console.log(`[EventCommunication] 消息 ${messageId} 已发送到频道 ${channel}[发送者: ${senderId}]`)
return messageId
}
/**
* 广播消息到所有订阅者
*/
async broadcast(
senderId: string,
channel: string,
payload: any,
options: {
priority?: MessagePriority
expiresIn?: number
} = {}
): Promise<string> {
return this.sendMessage(senderId, channel, payload, {
...options,
type: MessageType.BROADCAST
})
}
/**
* 发送跨应用消息
*/
async sendCrossAppMessage(
senderId: string,
receiverId: string,
payload: any,
options: {
priority?: MessagePriority
expiresIn?: number
} = {}
): Promise<string> {
const channel = 'cross-app'
return this.sendMessage(senderId, channel, payload, {
...options,
receiverId,
type: MessageType.CROSS_APP
})
}
/**
* 获取消息历史
*/
getMessageHistory(
appId: string,
options: {
channel?: string
limit?: number
since?: Date
} = {}
): EventMessage[] {
const history = this.messageHistory.get(appId) || []
let filtered = history.filter(msg =>
msg.senderId === appId || msg.receiverId === appId
)
if (options.channel) {
filtered = filtered.filter(msg => msg.channel === options.channel)
}
if (options.since) {
filtered = filtered.filter(msg => msg.timestamp >= options.since!)
}
if (options.limit) {
filtered = filtered.slice(-options.limit)
}
return filtered.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime())
}
/**
* 获取应用的订阅列表
*/
getAppSubscriptions(appId: string): EventSubscriber[] {
return Array.from(this.subscribers.values()).filter(sub => sub.appId === appId)
}
/**
* 获取频道订阅者数量
*/
getChannelSubscriberCount(channel: string): number {
return Array.from(this.subscribers.values()).filter(
sub => sub.channel === channel && sub.active
).length
}
/**
* 创建通信频道
*/
createChannel(
channel: string,
config: Omit<CommunicationChannel, 'name'>
): boolean {
if (this.channels.has(channel)) {
return false
}
this.channels.set(channel, {
name: channel,
...config
})
console.log(`创建通信频道: ${channel}`)
return true
}
/**
* 删除通信频道
*/
deleteChannel(channel: string): boolean {
// 移除所有相关订阅
const subscribersToRemove = Array.from(this.subscribers.entries())
.filter(([, sub]) => sub.channel === channel)
.map(([id]) => id)
subscribersToRemove.forEach(id => this.unsubscribe(id))
// 删除频道
const result = this.channels.delete(channel)
if (result) {
console.log(`删除通信频道: ${channel}`)
}
return result
}
/**
* 获取统计信息
*/
getStatistics(): EventStatistics {
return { ...this.statistics }
}
/**
* 清理过期消息和订阅
*/
cleanup(): void {
const now = new Date()
// 清理过期消息
for (const [appId, messages] of this.messageQueue.entries()) {
const validMessages = messages.filter(msg =>
!msg.expiresAt || msg.expiresAt > now
)
if (validMessages.length !== messages.length) {
this.messageQueue.set(appId, validMessages)
}
}
// 清理消息历史(保留最近7天)
const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
for (const [appId, history] of this.messageHistory.entries()) {
const recentHistory = history.filter(msg => msg.timestamp > sevenDaysAgo)
this.messageHistory.set(appId, recentHistory)
}
console.log('事件通信服务清理完成')
}
/**
* 销毁服务
*/
destroy(): void {
if (this.processingInterval) {
clearInterval(this.processingInterval)
this.processingInterval = null
}
this.subscribers.clear()
this.messageQueue.clear()
this.messageHistory.clear()
this.channels.clear()
console.log('事件通信服务已销毁')
}
// 私有方法
/**
* 初始化默认频道
*/
private initializeDefaultChannels(): void {
// 系统事件频道
this.createChannel('system', {
description: '系统级事件通信',
restricted: true,
allowedApps: ['system'],
maxMessageSize: 1024 * 10, // 10KB
messageRetention: 24 * 60 * 60 * 1000 // 24小时
})
// 应用间通信频道
this.createChannel('cross-app', {
description: '应用间通信',
restricted: false,
allowedApps: [],
maxMessageSize: 1024 * 100, // 100KB
messageRetention: 7 * 24 * 60 * 60 * 1000 // 7天
})
// 用户交互频道
this.createChannel('user-interaction', {
description: '用户交互事件',
restricted: false,
allowedApps: [],
maxMessageSize: 1024 * 5, // 5KB
messageRetention: 60 * 60 * 1000 // 1小时
})
// 广播频道
this.createChannel('broadcast', {
description: '系统广播',
restricted: true,
allowedApps: ['system'],
maxMessageSize: 1024 * 50, // 50KB
messageRetention: 24 * 60 * 60 * 1000 // 24小时
})
}
/**
* 检查应用是否可以访问频道
*/
private canAccessChannel(appId: string, channel: string): boolean {
const channelConfig = this.channels.get(channel)
if (!channelConfig) {
// 频道不存在,默认允许
return true
}
if (!channelConfig.restricted) {
return true
}
// 系统应用总是有权限
if (appId === 'system') {
return true
}
return channelConfig.allowedApps.includes(appId)
}
/**
* 添加消息到队列
*/
private addToQueue(message: EventMessage): void {
const queueKey = message.receiverId || 'broadcast'
if (!this.messageQueue.has(queueKey)) {
this.messageQueue.set(queueKey, [])
}
const queue = this.messageQueue.get(queueKey)!
// 按优先级插入
const insertIndex = queue.findIndex(msg => msg.priority < message.priority)
if (insertIndex === -1) {
queue.push(message)
} else {
queue.splice(insertIndex, 0, message)
}
}
/**
* 直接投递消息
*/
private async deliverMessage(message: EventMessage): Promise<void> {
try {
const subscribers = this.getRelevantSubscribers(message)
if (subscribers.length === 0) {
message.status = MessageStatus.FAILED
console.warn(`[EventCommunication] 没有找到频道 ${message.channel} 的订阅者[消息 ID: ${message.id}]`)
return
}
// 并行发送给所有订阅者
const deliveryPromises = subscribers.map(async (subscriber) => {
try {
// 应用过滤器
if (subscriber.filter && !subscriber.filter(message)) {
return
}
await subscriber.handler(message)
this.statistics.totalMessagesReceived++
console.log(`[EventCommunication] 消息 ${message.id} 已投递给订阅者 ${subscriber.id}[频道: ${message.channel}]`)
} catch (error) {
console.error(`向订阅者 ${subscriber.id} 发送消息失败:`, error)
throw error
}
})
await Promise.allSettled(deliveryPromises)
message.status = MessageStatus.DELIVERED
} catch (error) {
message.status = MessageStatus.FAILED
this.statistics.failedMessages++
console.error('消息投递失败:', error)
// 重试机制
if (message.retryCount < message.maxRetries) {
message.retryCount++
message.status = MessageStatus.PENDING
setTimeout(() => this.deliverMessage(message), 1000 * message.retryCount)
}
}
}
/**
* 获取相关订阅者
*/
private getRelevantSubscribers(message: EventMessage): EventSubscriber[] {
return Array.from(this.subscribers.values()).filter(subscriber => {
if (!subscriber.active) return false
if (subscriber.channel !== message.channel) return false
// 点对点消息检查接收者
if (message.receiverId && subscriber.appId !== message.receiverId) {
return false
}
return true
})
}
/**
* 开始消息处理循环
*/
private startMessageProcessing(): void {
this.processingInterval = setInterval(() => {
this.processMessageQueue()
this.cleanupExpiredMessages()
}, 100) // 每100ms处理一次
}
/**
* 处理消息队列
*/
private processMessageQueue(): void {
for (const [queueKey, messages] of this.messageQueue.entries()) {
if (messages.length === 0) continue
// 处理优先级最高的消息
const message = messages.shift()!
// 检查消息是否过期
if (message.expiresAt && message.expiresAt <= new Date()) {
message.status = MessageStatus.EXPIRED
continue
}
this.deliverMessage(message)
}
}
/**
* 清理过期消息
*/
private cleanupExpiredMessages(): void {
const now = new Date()
for (const [queueKey, messages] of this.messageQueue.entries()) {
const validMessages = messages.filter(msg =>
!msg.expiresAt || msg.expiresAt > now
)
if (validMessages.length !== messages.length) {
this.messageQueue.set(queueKey, validMessages)
}
}
}
/**
* 记录消息历史
*/
private recordMessage(message: EventMessage): void {
// 记录发送者历史
if (!this.messageHistory.has(message.senderId)) {
this.messageHistory.set(message.senderId, [])
}
this.messageHistory.get(message.senderId)!.push(message)
// 记录接收者历史
if (message.receiverId && message.receiverId !== message.senderId) {
if (!this.messageHistory.has(message.receiverId)) {
this.messageHistory.set(message.receiverId, [])
}
this.messageHistory.get(message.receiverId)!.push(message)
}
}
/**
* 更新活跃订阅者数量
*/
private updateActiveSubscribersCount(): void {
this.statistics.activeSubscribers = Array.from(this.subscribers.values())
.filter(sub => sub.active).length
}
}

View File

@@ -0,0 +1,629 @@
import { reactive } from 'vue'
import type { AppManifest } from './ApplicationLifecycleManager'
/**
* 外置应用信息
*/
export interface ExternalApp {
id: string
manifest: AppManifest
basePath: string
manifestPath: string
entryPath: string
discovered: boolean
lastScanned: Date
}
/**
* 外置应用发现服务
* 自动扫描 public/apps 目录下的外部应用
*
* 注意:
* - 仅处理外部应用,不扫描内置应用
* - 内置应用通过 AppRegistry 静态注册
* - 已排除内置应用: calculator, notepad, todo
*/
export class ExternalAppDiscovery {
private static instance: ExternalAppDiscovery | null = null
private discoveredApps = reactive(new Map<string, ExternalApp>())
private isScanning = false
private hasStarted = false // 添加标志防止重复启动
constructor() {
console.log('[ExternalAppDiscovery] 服务初始化')
}
/**
* 获取单例实例
*/
static getInstance(): ExternalAppDiscovery {
if (!ExternalAppDiscovery.instance) {
ExternalAppDiscovery.instance = new ExternalAppDiscovery()
}
return ExternalAppDiscovery.instance
}
/**
* 启动应用发现服务(只执行一次扫描,不设置定时器)
*/
async startDiscovery(): Promise<void> {
// 防止重复启动
if (this.hasStarted) {
console.log('[ExternalAppDiscovery] 服务已启动,跳过重复启动')
return
}
console.log('[ExternalAppDiscovery] 启动应用发现服务')
this.hasStarted = true
// 只执行一次扫描,不设置定时器
console.log('[ExternalAppDiscovery] 开始执行扫描...')
await this.scanExternalApps()
console.log('[ExternalAppDiscovery] 扫描完成')
}
/**
* 停止应用发现服务
*/
stopDiscovery(): void {
console.log('[ExternalAppDiscovery] 停止应用发现服务')
this.hasStarted = false
}
/**
* 扫描外置应用
*/
async scanExternalApps(): Promise<void> {
if (this.isScanning) {
console.log('[ExternalAppDiscovery] 正在扫描中,跳过本次扫描')
return
}
this.isScanning = true
console.log('[ExternalAppDiscovery] ==> 开始扫描外置应用')
try {
// 获取 public/apps 目录下的所有应用文件夹
const appDirs = await this.getAppDirectories()
console.log(`[ExternalAppDiscovery] 发现 ${appDirs.length} 个应用目录:`, appDirs)
const newApps = new Map<string, ExternalApp>()
// 扫描每个应用目录
for (const appDir of appDirs) {
try {
console.log(`[ExternalAppDiscovery] 扫描应用目录: ${appDir}`)
const app = await this.scanAppDirectory(appDir)
if (app) {
newApps.set(app.id, app)
console.log(`[ExternalAppDiscovery] ✓ 成功扫描应用: ${app.manifest.name} (${app.id})`)
} else {
console.log(`[ExternalAppDiscovery] ✗ 应用目录 ${appDir} 扫描失败或不存在`)
}
} catch (error) {
if (error instanceof SyntaxError && error.message.includes('Unexpected token')) {
console.warn(
`[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 格式错误或返回HTML页面`,
)
} else {
console.warn(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 失败:`, error)
}
}
}
// 更新发现的应用列表
this.updateDiscoveredApps(newApps)
console.log(`[ExternalAppDiscovery] ==> 扫描完成,发现 ${newApps.size} 个有效应用`)
console.log(`[ExternalAppDiscovery] 当前总共有 ${this.discoveredApps.size} 个已发现应用`)
} catch (error) {
console.error('[ExternalAppDiscovery] 扫描外置应用失败:', error)
} finally {
this.isScanning = false
}
}
/**
* 获取应用目录列表
*/
private async getAppDirectories(): Promise<string[]> {
try {
console.log('[ExternalAppDiscovery] 开始获取应用目录列表')
// 方案1使用Vite的glob功能推荐
console.log('[ExternalAppDiscovery] 尝试使用Vite glob功能')
const knownApps = await this.getKnownAppDirectories()
console.log('[ExternalAppDiscovery] Vite glob结果:', knownApps)
const validApps: string[] = []
// 验证已知应用是否真实存在
for (const appDir of knownApps) {
try {
const manifestPath = `/apps/${appDir}/manifest.json`
console.log(`[ExternalAppDiscovery] 检查应用 ${appDir} 的 manifest.json: ${manifestPath}`)
const response = await fetch(manifestPath, { method: 'HEAD' })
if (response.ok) {
const contentType = response.headers.get('content-type')
console.log(
`[ExternalAppDiscovery] 应用 ${appDir} 的响应状态: ${response.status}, 内容类型: ${contentType}`,
)
// 检查是否返回JSON内容
if (
contentType &&
(contentType.includes('application/json') || contentType.includes('text/json'))
) {
validApps.push(appDir)
console.log(`[ExternalAppDiscovery] 确认应用存在: ${appDir}`)
} else {
console.warn(`[ExternalAppDiscovery] 应用 ${appDir} 的 manifest.json 返回非JSON内容`)
}
} else {
console.warn(`[ExternalAppDiscovery] 应用不存在: ${appDir} (HTTP ${response.status})`)
}
} catch (error) {
console.warn(`[ExternalAppDiscovery] 检查应用 ${appDir} 时出错:`, error)
}
}
console.log('[ExternalAppDiscovery] 验证后的有效应用:', validApps)
// 如果Vite glob没有找到应用尝试其他方法
if (validApps.length === 0) {
console.log('[ExternalAppDiscovery] Vite glob未找到有效应用尝试网络请求方式')
// 方案2尝试目录列表扫描
try {
console.log('[ExternalAppDiscovery] 尝试目录列表扫描')
const additionalApps = await this.tryDirectoryListing()
console.log('[ExternalAppDiscovery] 目录列表扫描结果:', additionalApps)
// 合并去重
for (const app of additionalApps) {
if (!validApps.includes(app)) {
validApps.push(app)
}
}
} catch (error) {
console.log('[ExternalAppDiscovery] 目录列表扫描失败')
}
// 方案3尝试扫描常见应用名称
if (validApps.length === 0) {
try {
console.log('[ExternalAppDiscovery] 尝试扫描常见应用名称')
const commonApps = await this.tryCommonAppNames()
console.log('[ExternalAppDiscovery] 常见应用扫描结果:', commonApps)
for (const app of commonApps) {
if (!validApps.includes(app)) {
validApps.push(app)
}
}
} catch (error) {
console.log('[ExternalAppDiscovery] 常见应用扫描失败')
}
}
}
console.log(`[ExternalAppDiscovery] 最终发现 ${validApps.length} 个应用目录:`, validApps)
return validApps
} catch (error) {
console.warn('[ExternalAppDiscovery] 获取目录列表失败,使用静态列表:', error)
const fallbackList = [
'music-player', // 音乐播放器应用
// 可以在这里添加更多已知的外部应用
]
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
return fallbackList
}
}
/**
* 尝试通过 fetch 获取目录列表(开发环境可能失败)
*/
private async tryDirectoryListing(): Promise<string[]> {
try {
console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表')
const response = await fetch('/apps/')
console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const html = await response.text()
console.log('[ExternalAppDiscovery] 响应内容类型:', response.headers.get('content-type'))
console.log('[ExternalAppDiscovery] 响应内容长度:', html.length)
// 检查是否真的是目录列表还是index.html
if (html.includes('<!DOCTYPE html') || html.includes('<html')) {
console.log('[ExternalAppDiscovery] 响应是HTML页面不是目录列表')
throw new Error('服务器返回HTML页面而不是目录列表')
}
const directories = this.parseDirectoryListing(html)
if (directories.length === 0) {
throw new Error('未从目录列表中解析到任何应用目录')
}
return directories
} catch (error) {
console.warn('[ExternalAppDiscovery] 目录列表扫描失败:', error)
throw error
}
}
/**
* 尝试扫描常见的应用名称
*/
private async tryCommonAppNames(): Promise<string[]> {
// 排除内置应用,只扫描外部应用
const builtInApps = ['calculator', 'notepad', 'todo']
// 常见的外部应用名称列表
const commonNames = [
'file-manager',
'text-editor',
'image-viewer',
'video-player',
'chat-app',
'weather-app',
'calendar-app',
'email-client',
'web-browser',
'code-editor',
].filter((name) => !builtInApps.includes(name)) // 过滤掉内置应用
const validApps: string[] = []
// 检查每个常见应用是否实际存在
for (const appName of commonNames) {
try {
const manifestPath = `/apps/${appName}/manifest.json`
const response = await fetch(manifestPath, { method: 'HEAD' })
// 检查响应状态和内容类型
if (response.ok) {
const contentType = response.headers.get('content-type')
// 只有在返回JSON内容时才认为找到了有效应用
if (
contentType &&
(contentType.includes('application/json') || contentType.includes('text/json'))
) {
validApps.push(appName)
console.log(`[ExternalAppDiscovery] 发现常见应用: ${appName}`)
} else {
console.debug(
`[ExternalAppDiscovery] 应用 ${appName} 存在但 manifest.json 返回非JSON内容`,
)
}
} else {
console.debug(`[ExternalAppDiscovery] 应用 ${appName} 不存在 (HTTP ${response.status})`)
}
} catch (error) {
// 静默失败,不记录日志避免噪音
console.debug(`[ExternalAppDiscovery] 检查应用 ${appName} 时出现网络错误`)
}
}
return validApps
}
/**
* 解析目录列表HTML
*/
private parseDirectoryListing(html: string): string[] {
console.log('[ExternalAppDiscovery] 解析目录列表HTML (前1000字符):', html.substring(0, 1000)) // 调试输出
const directories: string[] = []
const builtInApps = ['calculator', 'notepad', 'todo'] // 内置应用列表
// 使用最简单有效的方法
// 查找所有形如 /apps/dirname/ 的路径
const pattern = /\/apps\/([^\/"'\s>]+)\//g
let match
while ((match = pattern.exec(html)) !== null) {
const dirName = match[1]
console.log(`[ExternalAppDiscovery] 匹配到目录: ${dirName}`)
// 确保目录名有效且不是内置应用
if (
dirName &&
dirName.length > 0 &&
!dirName.startsWith('.') &&
!builtInApps.includes(dirName) &&
!directories.includes(dirName)
) {
directories.push(dirName)
}
}
// 去重
const uniqueDirs = [...new Set(directories)]
console.log('[ExternalAppDiscovery] 最终解析结果:', uniqueDirs)
return uniqueDirs
}
/**
* 测试目录解析功能
*/
private testParseDirectoryListing(): void {
// 测试方法已移除
}
/**
* 获取已知的应用目录
*/
private async getKnownAppDirectories(): Promise<string[]> {
try {
console.log('[ExternalAppDiscovery] 使用Vite glob导入获取应用目录')
// 使用Vite的glob功能静态导入所有manifest.json文件
const manifestModules = import.meta.glob('/public/apps/*/manifest.json')
// 从文件路径中提取应用目录名
const appDirs: string[] = []
for (const path in manifestModules) {
// 路径格式: /public/apps/app-name/manifest.json
const match = path.match(/\/public\/apps\/([^\/]+)\/manifest\.json/)
if (match && match[1]) {
const appDir = match[1]
// 排除内置应用
if (!this.isBuiltInApp(appDir)) {
appDirs.push(appDir)
}
}
}
console.log(`[ExternalAppDiscovery] 通过Vite glob发现外部应用目录: ${appDirs.join(', ')}`)
return appDirs
} catch (error) {
console.warn('[ExternalAppDiscovery] 使用Vite glob读取应用目录失败:', error)
// 回退到静态列表
const fallbackList = [
'music-player', // 音乐播放器应用
// 可以在这里添加更多已知的外部应用
]
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
return fallbackList
}
}
/**
* 通过网络请求获取应用目录(备用方法)
*/
private async getKnownAppDirectoriesViaNetwork(): Promise<string[]> {
try {
console.log('[ExternalAppDiscovery] 尝试通过网络请求获取目录列表 /apps/')
// 尝试通过网络请求获取目录列表
const response = await fetch('/public/apps/')
console.log('[ExternalAppDiscovery] 目录列表响应状态:', response.status)
if (!response.ok) {
console.log('[ExternalAppDiscovery] 响应不成功,使用回退列表')
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const contentType = response.headers.get('content-type')
console.log('[ExternalAppDiscovery] 响应内容类型:', contentType)
const html = await response.text()
console.log(11111111, html)
console.log('[ExternalAppDiscovery] 目录列表HTML长度:', html.length)
const appDirs = this.parseDirectoryListing(html)
console.log('[ExternalAppDiscovery] 解析到的应用目录:', appDirs)
// 过滤掉内置应用
const externalApps = appDirs.filter((dir) => !this.isBuiltInApp(dir))
console.log(`[ExternalAppDiscovery] 通过目录列表发现外部应用目录: ${externalApps.join(', ')}`)
return externalApps
} catch (error) {
console.warn('[ExternalAppDiscovery] 获取目录列表失败:', error)
// 回退到静态列表
const fallbackList = [
'music-player', // 音乐播放器应用
// 可以在这里添加更多已知的外部应用
]
console.log('[ExternalAppDiscovery] 使用回退列表:', fallbackList)
return fallbackList
}
}
/**
* 扫描单个应用目录
*/
private async scanAppDirectory(appDir: string): Promise<ExternalApp | null> {
try {
// 首先检查是否为内置应用
if (this.isBuiltInApp(appDir)) {
console.log(`[ExternalAppDiscovery] 跳过内置应用: ${appDir}`)
return null
}
const basePath = `/apps/${appDir}`
const manifestPath = `${basePath}/manifest.json`
console.log(`[ExternalAppDiscovery] 扫描外部应用目录: ${appDir}`)
// 尝试获取 manifest.json
const manifestResponse = await fetch(manifestPath)
if (!manifestResponse.ok) {
console.warn(
`[ExternalAppDiscovery] 未找到 manifest.json: ${manifestPath} (HTTP ${manifestResponse.status})`,
)
return null
}
// 检查响应内容类型
const contentType = manifestResponse.headers.get('content-type')
if (!contentType || !contentType.includes('application/json')) {
console.warn(
`[ExternalAppDiscovery] manifest.json 返回了非JSON内容: ${manifestPath}, content-type: ${contentType}`,
)
return null
}
let manifest: AppManifest
try {
manifest = (await manifestResponse.json()) as AppManifest
} catch (parseError) {
console.warn(`[ExternalAppDiscovery] 解析 manifest.json 失败: ${manifestPath}`, parseError)
return null
}
// 验证 manifest 格式
if (!this.validateManifest(manifest)) {
console.warn(`[ExternalAppDiscovery] 无效的 manifest.json: ${manifestPath}`)
return null
}
// 再次检查 manifest.id 是否为内置应用
if (this.isBuiltInApp(manifest.id)) {
console.warn(`[ExternalAppDiscovery] 检测到内置应用 ID: ${manifest.id},跳过`)
return null
}
const entryPath = `${basePath}/${manifest.entryPoint}`
// 验证入口文件是否存在
const entryResponse = await fetch(entryPath, { method: 'HEAD' })
if (!entryResponse.ok) {
console.warn(`[ExternalAppDiscovery] 入口文件不存在: ${entryPath}`)
return null
}
const app: ExternalApp = {
id: manifest.id,
manifest,
basePath,
manifestPath,
entryPath,
discovered: true,
lastScanned: new Date(),
}
console.log(`[ExternalAppDiscovery] 发现有效外部应用: ${manifest.name} (${manifest.id})`)
return app
} catch (error) {
console.error(`[ExternalAppDiscovery] 扫描应用目录 ${appDir} 时出错:`, error)
return null
}
}
/**
* 检查是否为内置应用
*/
private isBuiltInApp(appId: string): boolean {
const builtInApps = ['calculator', 'notepad', 'todo']
return builtInApps.includes(appId)
}
/**
* 验证应用清单
*/
private validateManifest(manifest: any): manifest is AppManifest {
if (!manifest || typeof manifest !== 'object') {
return false
}
// 检查必需字段
const requiredFields = ['id', 'name', 'version', 'entryPoint']
for (const field of requiredFields) {
if (!manifest[field] || typeof manifest[field] !== 'string') {
console.warn(`[ExternalAppDiscovery] manifest 缺少必需字段: ${field}`)
return false
}
}
// 验证版本格式
if (!/^\d+\.\d+\.\d+/.test(manifest.version)) {
console.warn(`[ExternalAppDiscovery] 版本号格式不正确: ${manifest.version}`)
return false
}
// 验证应用ID格式
if (!/^[a-zA-Z0-9._-]+$/.test(manifest.id)) {
console.warn(`[ExternalAppDiscovery] 应用ID格式不正确: ${manifest.id}`)
return false
}
return true
}
/**
* 更新发现的应用列表
*/
private updateDiscoveredApps(newApps: Map<string, ExternalApp>): void {
// 移除不再存在的应用
for (const [appId] of this.discoveredApps) {
if (!newApps.has(appId)) {
console.log(`[ExternalAppDiscovery] 应用已移除: ${appId}`)
this.discoveredApps.delete(appId)
}
}
// 添加或更新应用
for (const [appId, app] of newApps) {
const existingApp = this.discoveredApps.get(appId)
if (!existingApp) {
console.log(`[ExternalAppDiscovery] 发现新应用: ${app.manifest.name} (${appId})`)
this.discoveredApps.set(appId, app)
} else if (existingApp.manifest.version !== app.manifest.version) {
console.log(
`[ExternalAppDiscovery] 应用版本更新: ${appId} ${existingApp.manifest.version} -> ${app.manifest.version}`,
)
this.discoveredApps.set(appId, app)
} else {
// 只更新扫描时间
existingApp.lastScanned = app.lastScanned
}
}
}
/**
* 获取所有发现的应用
*/
getDiscoveredApps(): ExternalApp[] {
return Array.from(this.discoveredApps.values())
}
/**
* 获取指定应用
*/
getApp(appId: string): ExternalApp | undefined {
return this.discoveredApps.get(appId)
}
/**
* 检查应用是否存在
*/
hasApp(appId: string): boolean {
return this.discoveredApps.has(appId)
}
/**
* 获取应用数量
*/
getAppCount(): number {
return this.discoveredApps.size
}
/**
* 手动刷新应用列表
*/
async refresh(): Promise<void> {
console.log('[ExternalAppDiscovery] 手动刷新应用列表')
await this.scanExternalApps()
}
}
// 导出单例实例
export const externalAppDiscovery = ExternalAppDiscovery.getInstance()

View File

@@ -0,0 +1,689 @@
import { reactive, ref } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
/**
* 资源类型枚举
*/
export enum ResourceType {
LOCAL_STORAGE = 'localStorage',
NETWORK = 'network',
FILE_SYSTEM = 'fileSystem',
NOTIFICATION = 'notification',
CLIPBOARD = 'clipboard',
MEDIA = 'media',
GEOLOCATION = 'geolocation',
}
/**
* 权限级别枚举
*/
export enum PermissionLevel {
DENIED = 'denied',
GRANTED = 'granted',
PROMPT = 'prompt',
}
/**
* 权限请求结果
*/
export interface PermissionRequest {
id: string
appId: string
resourceType: ResourceType
description: string
requestedAt: Date
status: PermissionLevel
approvedAt?: Date
deniedAt?: Date
expiresAt?: Date
}
/**
* 资源访问配置
*/
export interface ResourceAccessConfig {
maxStorageSize: number // 本地存储最大容量(MB)
allowedDomains: string[] // 允许访问的网络域名
maxNetworkRequests: number // 每分钟最大网络请求数
allowFileAccess: boolean // 是否允许文件系统访问
allowNotifications: boolean // 是否允许通知
allowClipboard: boolean // 是否允许剪贴板访问
allowMedia: boolean // 是否允许摄像头麦克风
allowGeolocation: boolean // 是否允许地理位置
}
/**
* 网络请求记录
*/
export interface NetworkRequest {
id: string
appId: string
url: string
method: string
timestamp: Date
status?: number
responseSize?: number
}
/**
* 存储使用情况
*/
export interface StorageUsage {
appId: string
usedSpace: number // 已使用空间(MB)
maxSpace: number // 最大空间(MB)
lastAccessed: Date
}
/**
* 资源事件接口
*/
export interface ResourceEvents extends IEventMap {
onPermissionRequest: (request: PermissionRequest) => void
onPermissionGranted: (appId: string, resourceType: ResourceType) => void
onPermissionDenied: (appId: string, resourceType: ResourceType) => void
onResourceQuotaExceeded: (appId: string, resourceType: ResourceType) => void
onNetworkRequest: (request: NetworkRequest) => void
onStorageChange: (appId: string, usage: StorageUsage) => void
}
/**
* 资源管理服务类
*/
export class ResourceService {
private permissions = reactive(new Map<string, Map<ResourceType, PermissionRequest>>())
private networkRequests = reactive(new Map<string, NetworkRequest[]>())
private storageUsage = reactive(new Map<string, StorageUsage>())
private defaultConfig: ResourceAccessConfig
private eventBus: IEventBuilder<ResourceEvents>
constructor(eventBus: IEventBuilder<ResourceEvents>) {
this.eventBus = eventBus
// 默认资源访问配置
this.defaultConfig = {
maxStorageSize: 10, // 10MB
allowedDomains: [],
maxNetworkRequests: 60, // 每分钟60次
allowFileAccess: false,
allowNotifications: false,
allowClipboard: false,
allowMedia: false,
allowGeolocation: false,
}
this.initializeStorageMonitoring()
}
/**
* 请求资源权限
*/
async requestPermission(
appId: string,
resourceType: ResourceType,
description: string,
): Promise<PermissionLevel> {
const requestId = `${appId}-${resourceType}-${Date.now()}`
const request: PermissionRequest = {
id: requestId,
appId,
resourceType,
description,
requestedAt: new Date(),
status: PermissionLevel.PROMPT,
}
// 检查是否已有权限
const existingPermission = this.getPermission(appId, resourceType)
if (existingPermission) {
if (existingPermission.status === PermissionLevel.GRANTED) {
// 检查权限是否过期
if (!existingPermission.expiresAt || existingPermission.expiresAt > new Date()) {
return PermissionLevel.GRANTED
}
} else if (existingPermission.status === PermissionLevel.DENIED) {
return PermissionLevel.DENIED
}
}
// 触发权限请求事件UI层处理用户确认
this.eventBus.notifyEvent('onPermissionRequest', request)
// 根据资源类型的默认策略处理
return this.handlePermissionRequest(request)
}
/**
* 授予权限
*/
grantPermission(appId: string, resourceType: ResourceType, expiresIn?: number): boolean {
try {
const request = this.getPermission(appId, resourceType)
if (!request) return false
request.status = PermissionLevel.GRANTED
request.approvedAt = new Date()
if (expiresIn) {
request.expiresAt = new Date(Date.now() + expiresIn)
}
this.setPermission(appId, resourceType, request)
this.eventBus.notifyEvent('onPermissionGranted', appId, resourceType)
return true
} catch (error) {
console.error('授予权限失败:', error)
return false
}
}
/**
* 拒绝权限
*/
denyPermission(appId: string, resourceType: ResourceType): boolean {
try {
const request = this.getPermission(appId, resourceType)
if (!request) return false
request.status = PermissionLevel.DENIED
request.deniedAt = new Date()
this.setPermission(appId, resourceType, request)
this.eventBus.notifyEvent('onPermissionDenied', appId, resourceType)
return true
} catch (error) {
console.error('拒绝权限失败:', error)
return false
}
}
/**
* 检查应用是否有指定资源权限
*/
hasPermission(appId: string, resourceType: ResourceType): boolean {
const permission = this.getPermission(appId, resourceType)
if (!permission || permission.status !== PermissionLevel.GRANTED) {
return false
}
// 检查权限是否过期
if (permission.expiresAt && permission.expiresAt <= new Date()) {
permission.status = PermissionLevel.DENIED
this.setPermission(appId, resourceType, permission)
return false
}
return true
}
/**
* 本地存储操作
*/
async setStorage(appId: string, key: string, value: any): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
const permission = await this.requestPermission(
appId,
ResourceType.LOCAL_STORAGE,
'应用需要访问本地存储来保存数据',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
const storageKey = `app-${appId}-${key}`
const serializedValue = JSON.stringify(value)
// 检查存储配额
const usage = this.getStorageUsage(appId)
const valueSize = new Blob([serializedValue]).size / (1024 * 1024) // MB
if (usage.usedSpace + valueSize > usage.maxSpace) {
this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.LOCAL_STORAGE)
return false
}
localStorage.setItem(storageKey, serializedValue)
// 更新存储使用情况
this.updateStorageUsage(appId)
return true
} catch (error) {
console.error('存储数据失败:', error)
return false
}
}
/**
* 获取本地存储数据
*/
async getStorage(appId: string, key: string): Promise<any> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return null
}
try {
const storageKey = `app-${appId}-${key}`
const value = localStorage.getItem(storageKey)
if (value === null) {
return null
}
// 更新最后访问时间
this.updateStorageUsage(appId)
return JSON.parse(value)
} catch (error) {
console.error('获取存储数据失败:', error)
return null
}
}
/**
* 删除本地存储数据
*/
async removeStorage(appId: string, key: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return false
}
try {
const storageKey = `app-${appId}-${key}`
localStorage.removeItem(storageKey)
// 更新存储使用情况
this.updateStorageUsage(appId)
return true
} catch (error) {
console.error('删除存储数据失败:', error)
return false
}
}
/**
* 清空应用存储
*/
async clearStorage(appId: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.LOCAL_STORAGE)) {
return false
}
try {
const prefix = `app-${appId}-`
const keysToRemove: string[] = []
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(prefix)) {
keysToRemove.push(key)
}
}
keysToRemove.forEach((key) => localStorage.removeItem(key))
// 重置存储使用情况
this.resetStorageUsage(appId)
return true
} catch (error) {
console.error('清空存储失败:', error)
return false
}
}
/**
* 网络请求
*/
async makeNetworkRequest(
appId: string,
url: string,
options: RequestInit = {},
): Promise<Response | null> {
if (!this.hasPermission(appId, ResourceType.NETWORK)) {
const permission = await this.requestPermission(
appId,
ResourceType.NETWORK,
`应用需要访问网络来请求数据: ${url}`,
)
if (permission !== PermissionLevel.GRANTED) {
return null
}
}
// 检查域名白名单
try {
const urlObj = new URL(url)
const config = this.getAppResourceConfig(appId)
if (
config.allowedDomains.length > 0 &&
!config.allowedDomains.some((domain) => urlObj.hostname.endsWith(domain))
) {
console.warn(`域名 ${urlObj.hostname} 不在白名单中`)
return null
}
// 检查请求频率限制
if (!this.checkNetworkRateLimit(appId)) {
this.eventBus.notifyEvent('onResourceQuotaExceeded', appId, ResourceType.NETWORK)
return null
}
// 记录网络请求
const requestRecord: NetworkRequest = {
id: `${appId}-${Date.now()}`,
appId,
url,
method: options.method || 'GET',
timestamp: new Date(),
}
const response = await fetch(url, options)
// 更新请求记录
requestRecord.status = response.status
requestRecord.responseSize = parseInt(response.headers.get('content-length') || '0')
this.recordNetworkRequest(requestRecord)
this.eventBus.notifyEvent('onNetworkRequest', requestRecord)
return response
} catch (error) {
console.error('网络请求失败:', error)
return null
}
}
/**
* 显示通知
*/
async showNotification(
appId: string,
title: string,
options?: NotificationOptions,
): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.NOTIFICATION)) {
const permission = await this.requestPermission(
appId,
ResourceType.NOTIFICATION,
'应用需要显示通知来提醒您重要信息',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
if ('Notification' in window) {
// 请求浏览器通知权限
if (Notification.permission === 'default') {
await Notification.requestPermission()
}
if (Notification.permission === 'granted') {
new Notification(`[${appId}] ${title}`, options)
return true
}
}
return false
} catch (error) {
console.error('显示通知失败:', error)
return false
}
}
/**
* 访问剪贴板
*/
async getClipboard(appId: string): Promise<string | null> {
if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) {
const permission = await this.requestPermission(
appId,
ResourceType.CLIPBOARD,
'应用需要访问剪贴板来读取您复制的内容',
)
if (permission !== PermissionLevel.GRANTED) {
return null
}
}
try {
if (navigator.clipboard && navigator.clipboard.readText) {
return await navigator.clipboard.readText()
}
return null
} catch (error) {
console.error('读取剪贴板失败:', error)
return null
}
}
/**
* 写入剪贴板
*/
async setClipboard(appId: string, text: string): Promise<boolean> {
if (!this.hasPermission(appId, ResourceType.CLIPBOARD)) {
const permission = await this.requestPermission(
appId,
ResourceType.CLIPBOARD,
'应用需要访问剪贴板来复制内容',
)
if (permission !== PermissionLevel.GRANTED) {
return false
}
}
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
return true
}
return false
} catch (error) {
console.error('写入剪贴板失败:', error)
return false
}
}
/**
* 获取应用权限列表
*/
getAppPermissions(appId: string): PermissionRequest[] {
const appPermissions = this.permissions.get(appId)
return appPermissions ? Array.from(appPermissions.values()) : []
}
/**
* 获取所有网络请求记录
*/
getNetworkRequests(appId: string): NetworkRequest[] {
return this.networkRequests.get(appId) || []
}
/**
* 获取存储使用情况
*/
getStorageUsage(appId: string): StorageUsage {
let usage = this.storageUsage.get(appId)
if (!usage) {
usage = {
appId,
usedSpace: 0,
maxSpace: this.defaultConfig.maxStorageSize,
lastAccessed: new Date(),
}
this.storageUsage.set(appId, usage)
}
return usage
}
/**
* 获取应用资源配置
*/
getAppResourceConfig(appId: string): ResourceAccessConfig {
// 这里可以从数据库或配置文件加载应用特定配置
// 目前返回默认配置
return { ...this.defaultConfig }
}
/**
* 撤销应用所有权限
*/
revokeAllPermissions(appId: string): boolean {
try {
this.permissions.delete(appId)
this.networkRequests.delete(appId)
this.clearStorage(appId)
return true
} catch (error) {
console.error('撤销权限失败:', error)
return false
}
}
// 私有方法
/**
* 处理权限请求
*/
private async handlePermissionRequest(request: PermissionRequest): Promise<PermissionLevel> {
// 对于本地存储,默认授权
if (request.resourceType === ResourceType.LOCAL_STORAGE) {
this.grantPermission(request.appId, request.resourceType)
return PermissionLevel.GRANTED
}
// 其他资源需要用户确认,这里模拟用户同意
// 实际实现中,这里应该显示权限确认对话框
return new Promise((resolve) => {
setTimeout(() => {
// 模拟用户操作
const userResponse = Math.random() > 0.3 // 70%的概率同意
if (userResponse) {
this.grantPermission(request.appId, request.resourceType, 24 * 60 * 60 * 1000) // 24小时有效
resolve(PermissionLevel.GRANTED)
} else {
this.denyPermission(request.appId, request.resourceType)
resolve(PermissionLevel.DENIED)
}
}, 1000)
})
}
/**
* 获取权限记录
*/
private getPermission(appId: string, resourceType: ResourceType): PermissionRequest | undefined {
const appPermissions = this.permissions.get(appId)
return appPermissions?.get(resourceType)
}
/**
* 设置权限记录
*/
private setPermission(
appId: string,
resourceType: ResourceType,
request: PermissionRequest,
): void {
if (!this.permissions.has(appId)) {
this.permissions.set(appId, new Map())
}
this.permissions.get(appId)!.set(resourceType, request)
}
/**
* 检查网络请求频率限制
*/
private checkNetworkRateLimit(appId: string): boolean {
const requests = this.networkRequests.get(appId) || []
const now = new Date()
const oneMinuteAgo = new Date(now.getTime() - 60 * 1000)
const recentRequests = requests.filter((req) => req.timestamp > oneMinuteAgo)
const config = this.getAppResourceConfig(appId)
return recentRequests.length < config.maxNetworkRequests
}
/**
* 记录网络请求
*/
private recordNetworkRequest(request: NetworkRequest): void {
if (!this.networkRequests.has(request.appId)) {
this.networkRequests.set(request.appId, [])
}
const requests = this.networkRequests.get(request.appId)!
requests.push(request)
// 保留最近1000条记录
if (requests.length > 1000) {
requests.splice(0, requests.length - 1000)
}
}
/**
* 更新存储使用情况
*/
private updateStorageUsage(appId: string): void {
const usage = this.getStorageUsage(appId)
usage.lastAccessed = new Date()
// 计算实际使用空间
let usedSpace = 0
const prefix = `app-${appId}-`
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i)
if (key && key.startsWith(prefix)) {
const value = localStorage.getItem(key)
if (value) {
usedSpace += new Blob([value]).size
}
}
}
usage.usedSpace = usedSpace / (1024 * 1024) // 转换为MB
this.eventBus.notifyEvent('onStorageChange', appId, usage)
}
/**
* 重置存储使用情况
*/
private resetStorageUsage(appId: string): void {
const usage = this.getStorageUsage(appId)
usage.usedSpace = 0
usage.lastAccessed = new Date()
this.eventBus.notifyEvent('onStorageChange', appId, usage)
}
/**
* 初始化存储监控
*/
private initializeStorageMonitoring(): void {
// 监听存储变化事件
window.addEventListener('storage', (e) => {
if (e.key && e.key.startsWith('app-')) {
const parts = e.key.split('-')
if (parts.length >= 2) {
const appId = parts[1]
this.updateStorageUsage(appId)
}
}
})
}
}

View File

@@ -0,0 +1,873 @@
import { reactive, ref } from 'vue'
import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl'
import type { IEventBuilder } from '@/events/IEventBuilder'
// 导入所有服务
import { WindowService } from './WindowService'
import { ResourceService } from './ResourceService'
import { EventCommunicationService } from './EventCommunicationService'
import { ApplicationSandboxEngine } from './ApplicationSandboxEngine'
import { ApplicationLifecycleManager } from './ApplicationLifecycleManager'
import { externalAppDiscovery } from './ExternalAppDiscovery'
/**
* 系统服务配置接口
*/
export interface SystemServiceConfig {
debug?: boolean
maxMemoryUsage?: number
maxCpuUsage?: number
enablePerformanceMonitoring?: boolean
enableSecurityAudit?: boolean
autoCleanup?: boolean
cleanupInterval?: number
}
/**
* 系统状态接口
*/
export interface SystemStatus {
initialized: boolean
running: boolean
servicesStatus: {
windowService: boolean
resourceService: boolean
eventService: boolean
sandboxEngine: boolean
lifecycleManager: boolean
}
performance: {
memoryUsage: number
cpuUsage: number
activeApps: number
activeWindows: number
}
uptime: number
lastError?: string
}
/**
* SDK调用接口
*/
export interface SDKCall {
requestId: string
method: string
data?: any
appId: string
}
/**
* 系统服务集成层
* 统一管理所有核心服务,提供统一的对外接口
*/
export class SystemServiceIntegration {
private initialized = ref(false)
private running = ref(false)
private config: SystemServiceConfig
private startTime: Date
// 核心服务实例
private eventBus: IEventBuilder<any>
private windowService!: WindowService
private resourceService!: ResourceService
private eventService!: EventCommunicationService
private sandboxEngine!: ApplicationSandboxEngine
private lifecycleManager!: ApplicationLifecycleManager
// 系统状态
private systemStatus = reactive<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,
})
// 性能监控
private cleanupInterval: number | null = null
private performanceInterval: number | null = null
constructor(config: SystemServiceConfig = {}) {
this.config = {
debug: false,
maxMemoryUsage: 1024, // 1GB
maxCpuUsage: 80, // 80%
enablePerformanceMonitoring: true,
enableSecurityAudit: true,
autoCleanup: true,
cleanupInterval: 5 * 60 * 1000, // 5分钟
...config,
}
this.startTime = new Date()
this.eventBus = new EventBuilderImpl()
this.setupGlobalErrorHandling()
}
/**
* 初始化系统服务
*/
async initialize(): Promise<void> {
if (this.initialized.value) {
throw new Error('系统服务已初始化')
}
try {
console.log('开始初始化系统服务...')
// 按依赖顺序初始化服务
await this.initializeServices()
// 设置服务间通信
this.setupServiceCommunication()
// 设置SDK消息处理
this.setupSDKMessageHandling()
// 启动性能监控
if (this.config.enablePerformanceMonitoring) {
this.startPerformanceMonitoring()
}
// 启动自动清理
if (this.config.autoCleanup) {
this.startAutoCleanup()
}
// 启动外置应用发现服务
// 注意:外置应用发现服务统一由 SystemServiceIntegration 管理,
// ApplicationLifecycleManager 只负责使用已发现的应用,避免重复启动
console.log('启动外置应用发现服务...')
await externalAppDiscovery.startDiscovery()
this.initialized.value = true
this.running.value = true
this.systemStatus.initialized = true
this.systemStatus.running = true
console.log('系统服务初始化完成')
// 发送系统就绪事件
this.eventService.sendMessage('system', 'system-ready', {
timestamp: new Date(),
services: Object.keys(this.systemStatus.servicesStatus),
})
} catch (error) {
console.error('系统服务初始化失败:', error)
this.systemStatus.lastError = error instanceof Error ? error.message : String(error)
throw error
}
}
/**
* 获取系统状态
*/
getSystemStatus(): SystemStatus {
this.updateSystemStatus()
return { ...this.systemStatus }
}
/**
* 获取窗体服务
*/
getWindowService(): WindowService {
this.checkInitialized()
return this.windowService
}
/**
* 获取资源服务
*/
getResourceService(): ResourceService {
this.checkInitialized()
return this.resourceService
}
/**
* 获取事件服务
*/
getEventService(): EventCommunicationService {
this.checkInitialized()
return this.eventService
}
/**
* 获取沙箱引擎
*/
getSandboxEngine(): ApplicationSandboxEngine {
this.checkInitialized()
return this.sandboxEngine
}
/**
* 获取生命周期管理器
*/
getLifecycleManager(): ApplicationLifecycleManager {
this.checkInitialized()
return this.lifecycleManager
}
/**
* 处理SDK调用
*/
async handleSDKCall(call: SDKCall): Promise<any> {
this.checkInitialized()
const { requestId, method, data, appId } = call
try {
this.debugLog(`处理SDK调用: ${method}`, { appId, data })
const result = await this.executeSDKMethod(method, data, appId)
return {
success: true,
data: result,
requestId,
}
} catch (error) {
console.error('SDK调用失败:', error)
return {
success: false,
error: error instanceof Error ? error.message : String(error),
requestId,
}
}
}
/**
* 重启系统服务
*/
async restart(): Promise<void> {
console.log('重启系统服务...')
await this.shutdown()
await new Promise((resolve) => setTimeout(resolve, 1000))
await this.initialize()
console.log('系统服务重启完成')
}
/**
* 关闭系统服务
*/
async shutdown(): Promise<void> {
console.log('关闭系统服务...')
this.running.value = false
this.systemStatus.running = false
// 停止定时器
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval)
this.cleanupInterval = null
}
if (this.performanceInterval) {
clearInterval(this.performanceInterval)
this.performanceInterval = null
}
// 停止外置应用发现服务(由 SystemServiceIntegration 统一管理)
externalAppDiscovery.stopDiscovery()
// 按相反顺序关闭服务
try {
if (this.lifecycleManager) {
// 停止所有运行中的应用
const runningApps = this.lifecycleManager.getRunningApps()
for (const app of runningApps) {
await this.lifecycleManager.stopApp(app.id)
}
}
if (this.sandboxEngine) {
this.sandboxEngine.destroy()
}
if (this.eventService) {
this.eventService.destroy()
}
if (this.windowService) {
// 关闭所有窗体
const windows = this.windowService.getAllWindows()
for (const window of windows) {
await this.windowService.destroyWindow(window.id)
}
}
} catch (error) {
console.error('关闭服务时发生错误:', error)
}
this.initialized.value = false
this.systemStatus.initialized = false
console.log('系统服务已关闭')
}
// 私有方法
/**
* 初始化所有服务
*/
private async initializeServices(): Promise<void> {
// 1. 初始化资源服务
console.log('初始化资源服务...')
this.resourceService = new ResourceService(this.eventBus)
this.systemStatus.servicesStatus.resourceService = true
// 2. 初始化事件通信服务
console.log('初始化事件通信服务...')
this.eventService = new EventCommunicationService(this.eventBus)
this.systemStatus.servicesStatus.eventService = true
// 3. 初始化窗体服务
console.log('初始化窗体服务...')
this.windowService = new WindowService(this.eventBus)
this.systemStatus.servicesStatus.windowService = true
// 4. 初始化沙箱引擎
console.log('初始化沙箱引擎...')
this.sandboxEngine = new ApplicationSandboxEngine(this.resourceService, this.eventService)
this.systemStatus.servicesStatus.sandboxEngine = true
// 5. 初始化生命周期管理器
console.log('初始化生命周期管理器...')
this.lifecycleManager = new ApplicationLifecycleManager(
this.windowService,
this.resourceService,
this.eventService,
this.sandboxEngine,
)
this.systemStatus.servicesStatus.lifecycleManager = true
}
/**
* 设置服务间通信
*/
private setupServiceCommunication(): void {
// 监听应用生命周期事件
this.eventService.subscribe('system', 'app-lifecycle', (message) => {
this.debugLog('[AppLifecycle] 应用生命周期事件:', message.payload)
})
// 监听窗口状态变化事件
this.eventService.subscribe('system', 'window-state-change', (message) => {
this.debugLog('[WindowState] 窗口状态变化消息已处理:', message.payload)
})
// 监听窗体状态变化(来自 WindowService 的 onStateChange 事件)
this.eventBus.addEventListener(
'onStateChange',
(windowId: string, newState: string, oldState: string) => {
console.log(
`[SystemIntegration] 接收到窗体状态变化事件: ${windowId} ${oldState} -> ${newState}`,
)
this.eventService.sendMessage('system', 'window-state-change', {
windowId,
newState,
oldState,
})
console.log(`[SystemIntegration] 已发送 window-state-change 消息到事件通信服务`)
},
)
// 监听窗体关闭事件,自动停止对应的应用
this.eventBus.addEventListener('onClose', async (windowId: string) => {
console.log(`[SystemIntegration] 接收到窗体关闭事件: ${windowId}`)
// 查找对应的应用
const runningApps = this.lifecycleManager.getRunningApps()
for (const app of runningApps) {
if (app.windowId === windowId) {
try {
console.log(`窗口关闭,自动停止应用: ${app.id}`)
await this.lifecycleManager.stopApp(app.id)
} catch (error) {
console.error(`停止应用 ${app.id} 失败:`, error)
}
break
}
}
})
// 监听资源配额超出
this.eventBus.addEventListener(
'onResourceQuotaExceeded',
(appId: string, resourceType: string) => {
console.log(`[SystemIntegration] 接收到资源配额超出事件: ${appId} - ${resourceType}`)
this.eventService.sendMessage('system', 'resource-quota-exceeded', {
appId,
resourceType,
})
},
)
}
/**
* 设置SDK消息处理
*/
private setupSDKMessageHandling(): void {
// 监听来自iframe的SDK调用
window.addEventListener('message', async (event) => {
const data = event.data
if (!data) return
// 处理安全存储消息
if (data.type?.startsWith('sdk:storage:')) {
await this.handleStorageMessage(event)
return
}
// 处理其他SDK调用
if (data.type === 'sdk:call') {
const call: SDKCall = data
const result = await this.handleSDKCall(call)
// 发送响应回iframe
const iframe = this.findIframeBySource(event.source as Window)
if (iframe) {
iframe.contentWindow?.postMessage(
{
type: 'system:response',
...result,
},
'*',
)
}
}
})
}
/**
* 处理安全存储消息
*/
private async handleStorageMessage(event: MessageEvent): Promise<void> {
const { type, requestId, appId, key, value } = event.data
if (!requestId || !appId) {
console.warn('存储消息缺少必需参数')
return
}
// 验证应用权限
const app = this.lifecycleManager.getApp(appId)
if (!app) {
console.warn(`未找到应用: ${appId}`)
return
}
let result: any = null
let success = false
try {
switch (type) {
case 'sdk:storage:get':
result = await this.resourceService.getStorage(appId, key)
success = true
break
case 'sdk:storage:set':
result = await this.resourceService.setStorage(appId, key, value)
success = result === true
break
case 'sdk:storage:remove':
result = await this.resourceService.removeStorage(appId, key)
success = result === true
break
default:
console.warn(`未知的存储操作: ${type}`)
return
}
} catch (error) {
console.error('存储操作失败:', error)
success = false
}
// 发送响应回iframe
const iframe = this.findIframeBySource(event.source as Window)
if (iframe?.contentWindow) {
iframe.contentWindow.postMessage(
{
type: 'system:storage-response',
requestId,
result,
success,
},
'*',
)
}
}
/**
* 执行SDK方法
*/
private async executeSDKMethod(method: string, data: any, appId: string): Promise<any> {
const [service, action] = method.split('.')
switch (service) {
case 'window':
return this.executeWindowMethod(action, data, appId)
case 'storage':
return this.executeStorageMethod(action, data, appId)
case 'network':
return this.executeNetworkMethod(action, data, appId)
case 'events':
return this.executeEventMethod(action, data, appId)
case 'ui':
return this.executeUIMethod(action, data, appId)
case 'system':
return this.executeSystemMethod(action, data, appId)
case 'sdk':
return this.executeSDKMethod(action, data, appId)
default:
throw new Error(`未知的服务: ${service}`)
}
}
/**
* 执行窗体相关方法
*/
private async executeWindowMethod(action: string, data: any, appId: string): Promise<any> {
// 查找应用的窗体
const app = this.lifecycleManager.getApp(appId)
if (!app?.windowId) {
throw new Error('应用窗体不存在')
}
const windowId = app.windowId
switch (action) {
case 'setTitle':
return this.windowService.setWindowTitle(windowId, data.title)
case 'resize':
return this.windowService.setWindowSize(windowId, data.width, data.height)
case 'move':
// 需要实现窗体移动功能
return true
case 'minimize':
return this.windowService.minimizeWindow(windowId)
case 'maximize':
return this.windowService.maximizeWindow(windowId)
case 'restore':
return this.windowService.restoreWindow(windowId)
case 'close':
return this.lifecycleManager.stopApp(appId)
case 'getState':
const window = this.windowService.getWindow(windowId)
return window?.state
case 'getSize':
const windowInfo = this.windowService.getWindow(windowId)
return {
width: windowInfo?.config.width,
height: windowInfo?.config.height,
}
default:
throw new Error(`未知的窗体操作: ${action}`)
}
}
/**
* 执行存储相关方法
*/
private async executeStorageMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'set':
return this.resourceService.setStorage(appId, data.key, data.value)
case 'get':
return this.resourceService.getStorage(appId, data.key)
case 'remove':
return this.resourceService.removeStorage(appId, data.key)
case 'clear':
return this.resourceService.clearStorage(appId)
case 'keys':
// 需要实现获取所有键的功能
return []
case 'has':
const value = await this.resourceService.getStorage(appId, data.key)
return value !== null
case 'getStats':
return this.resourceService.getStorageUsage(appId)
default:
throw new Error(`未知的存储操作: ${action}`)
}
}
/**
* 执行网络相关方法
*/
private async executeNetworkMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'request':
const response = await this.resourceService.makeNetworkRequest(appId, data.url, data.config)
return response
? {
data: await response.text(),
status: response.status,
statusText: response.statusText,
headers: {} as Record<string, string>, // 简化headers处理
url: response.url,
}
: null
case 'isOnline':
return navigator.onLine
case 'getStats':
const requests = this.resourceService.getNetworkRequests(appId)
return {
requestCount: requests.length,
failureCount: requests.filter((r) => r.status && r.status >= 400).length,
averageTime: 0, // 需要实现时间统计
}
default:
throw new Error(`未知的网络操作: ${action}`)
}
}
/**
* 执行事件相关方法
*/
private async executeEventMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'emit':
return this.eventService.sendMessage(appId, data.channel, data.data)
case 'on':
return this.eventService.subscribe(appId, data.channel, (message) => {
// 发送事件到应用
const app = this.lifecycleManager.getApp(appId)
if (app?.sandboxId) {
this.sandboxEngine.sendMessage(app.sandboxId, {
type: 'system:event',
subscriptionId: data.subscriptionId,
message,
})
}
})
case 'off':
return this.eventService.unsubscribe(data.subscriptionId)
case 'broadcast':
return this.eventService.broadcast(appId, data.channel, data.data)
case 'sendTo':
return this.eventService.sendCrossAppMessage(appId, data.targetAppId, data.data)
default:
throw new Error(`未知的事件操作: ${action}`)
}
}
/**
* 执行UI相关方法
*/
private async executeUIMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'showNotification':
return this.resourceService.showNotification(appId, data.title, data)
case 'showToast':
// 需要实现Toast功能
console.log(`[Toast] ${data.message}`)
return 'toast-' + Date.now()
default:
throw new Error(`未知的UI操作: ${action}`)
}
}
/**
* 执行系统相关方法
*/
private async executeSystemMethod(action: string, data: any, appId: string): Promise<any> {
switch (action) {
case 'getSystemInfo':
return {
platform: navigator.platform,
userAgent: navigator.userAgent,
language: navigator.language,
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
screenResolution: {
width: screen.width,
height: screen.height,
},
colorDepth: screen.colorDepth,
pixelRatio: window.devicePixelRatio,
}
case 'getAppInfo':
const app = this.lifecycleManager.getApp(appId)
return app
? {
id: app.id,
name: app.manifest.name,
version: app.version,
permissions: app.manifest.permissions,
createdAt: app.installedAt,
lastActiveAt: app.lastActiveAt,
}
: null
case 'getClipboard':
return this.resourceService.getClipboard(appId)
case 'setClipboard':
return this.resourceService.setClipboard(appId, data.text)
case 'getCurrentTime':
return new Date()
case 'generateUUID':
return crypto.randomUUID()
default:
throw new Error(`未知的系统操作: ${action}`)
}
}
/**
* 查找消息来源的iframe
*/
private findIframeBySource(source: Window): HTMLIFrameElement | null {
const iframes = Array.from(document.querySelectorAll('iframe'))
for (const iframe of iframes) {
if (iframe.contentWindow === source) {
return iframe
}
}
return null
}
/**
* 开始性能监控
*/
private startPerformanceMonitoring(): void {
this.performanceInterval = setInterval(() => {
this.updateSystemStatus()
// 检查性能阈值
if (this.systemStatus.performance.memoryUsage > this.config.maxMemoryUsage!) {
this.eventService.sendMessage('system', 'performance-alert', {
type: 'memory',
usage: this.systemStatus.performance.memoryUsage,
limit: this.config.maxMemoryUsage,
})
}
if (this.systemStatus.performance.cpuUsage > this.config.maxCpuUsage!) {
this.eventService.sendMessage('system', 'performance-alert', {
type: 'cpu',
usage: this.systemStatus.performance.cpuUsage,
limit: this.config.maxCpuUsage,
})
}
}, 10000) // 每10秒检查一次
}
/**
* 开始自动清理
*/
private startAutoCleanup(): void {
this.cleanupInterval = setInterval(() => {
this.debugLog('执行自动清理...')
// 清理事件服务
this.eventService.cleanup()
// 清理沙箱引擎缓存
// this.sandboxEngine.cleanup()
this.debugLog('自动清理完成')
}, this.config.cleanupInterval!)
}
/**
* 更新系统状态
*/
private updateSystemStatus(): void {
this.systemStatus.uptime = Date.now() - this.startTime.getTime()
this.systemStatus.performance.activeApps = this.lifecycleManager?.getRunningApps().length || 0
this.systemStatus.performance.activeWindows = this.windowService?.getAllWindows().length || 0
// 简化的内存和CPU使用率计算
this.systemStatus.performance.memoryUsage =
(performance as any).memory?.usedJSHeapSize / 1024 / 1024 || 0
this.systemStatus.performance.cpuUsage = Math.random() * 20 // 模拟CPU使用率
}
/**
* 检查是否已初始化
*/
private checkInitialized(): void {
if (!this.initialized.value) {
throw new Error('系统服务未初始化')
}
}
/**
* 设置全局错误处理
*/
private setupGlobalErrorHandling(): void {
window.addEventListener('error', (event) => {
console.error('全局错误:', event.error)
this.systemStatus.lastError = event.error?.message || '未知错误'
})
window.addEventListener('unhandledrejection', (event) => {
console.error('未处理的Promise拒绝:', event.reason)
this.systemStatus.lastError = event.reason?.message || '未处理的Promise拒绝'
})
}
/**
* 调试日志
*/
private debugLog(message: string, data?: any): void {
if (this.config.debug) {
console.log(`[SystemService] ${message}`, data)
}
}
}

View File

@@ -0,0 +1,651 @@
import { ref, reactive } from 'vue'
import type { IEventBuilder, IEventMap } from '@/events/IEventBuilder'
import { v4 as uuidv4 } from 'uuid'
/**
* 窗体状态枚举
*/
export enum WindowState {
CREATING = 'creating',
LOADING = 'loading',
ACTIVE = 'active',
MINIMIZED = 'minimized',
MAXIMIZED = 'maximized',
CLOSING = 'closing',
DESTROYED = 'destroyed',
ERROR = 'error',
}
/**
* 窗体配置接口
*/
export interface WindowConfig {
title: string
width: number
height: number
minWidth?: number
minHeight?: number
maxWidth?: number
maxHeight?: number
resizable?: boolean
movable?: boolean
closable?: boolean
minimizable?: boolean
maximizable?: boolean
modal?: boolean
alwaysOnTop?: boolean
x?: number
y?: number
}
/**
* 窗体实例接口
*/
export interface WindowInstance {
id: string
appId: string
config: WindowConfig
state: WindowState
element?: HTMLElement
iframe?: HTMLIFrameElement
zIndex: number
createdAt: Date
updatedAt: Date
}
/**
* 窗体事件接口
*/
export interface WindowEvents extends IEventMap {
onStateChange: (windowId: string, newState: WindowState, oldState: WindowState) => void
onResize: (windowId: string, width: number, height: number) => void
onMove: (windowId: string, x: number, y: number) => void
onFocus: (windowId: string) => void
onBlur: (windowId: string) => void
onClose: (windowId: string) => void
}
/**
* 窗体管理服务类
*/
export class WindowService {
private windows = reactive(new Map<string, WindowInstance>())
private activeWindowId = ref<string | null>(null)
private nextZIndex = 1000
private eventBus: IEventBuilder<WindowEvents>
constructor(eventBus: IEventBuilder<WindowEvents>) {
this.eventBus = eventBus
}
/**
* 创建新窗体
*/
async createWindow(appId: string, config: WindowConfig): Promise<WindowInstance> {
const windowId = uuidv4()
const now = new Date()
const windowInstance: WindowInstance = {
id: windowId,
appId,
config,
state: WindowState.CREATING,
zIndex: this.nextZIndex++,
createdAt: now,
updatedAt: now,
}
this.windows.set(windowId, windowInstance)
try {
// 创建窗体DOM元素
await this.createWindowElement(windowInstance)
// 更新状态为加载中
this.updateWindowState(windowId, WindowState.LOADING)
// 模拟应用加载过程
await this.loadApplication(windowInstance)
// 激活窗体
this.updateWindowState(windowId, WindowState.ACTIVE)
this.setActiveWindow(windowId)
return windowInstance
} catch (error) {
this.updateWindowState(windowId, WindowState.ERROR)
throw error
}
}
/**
* 销毁窗体
*/
async destroyWindow(windowId: string): Promise<boolean> {
const window = this.windows.get(windowId)
if (!window) return false
try {
this.updateWindowState(windowId, WindowState.CLOSING)
// 清理DOM元素
if (window.element) {
window.element.remove()
}
// 从集合中移除
this.windows.delete(windowId)
// 更新活跃窗体
if (this.activeWindowId.value === windowId) {
this.activeWindowId.value = null
// 激活最后一个窗体
const lastWindow = Array.from(this.windows.values()).pop()
if (lastWindow) {
this.setActiveWindow(lastWindow.id)
}
}
this.eventBus.notifyEvent('onClose', windowId)
return true
} catch (error) {
console.error('销毁窗体失败:', error)
return false
}
}
/**
* 最小化窗体
*/
minimizeWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window || window.state === WindowState.MINIMIZED) return false
this.updateWindowState(windowId, WindowState.MINIMIZED)
if (window.element) {
window.element.style.display = 'none'
}
return true
}
/**
* 最大化窗体
*/
maximizeWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window || window.state === WindowState.MAXIMIZED) return false
const oldState = window.state
this.updateWindowState(windowId, WindowState.MAXIMIZED)
if (window.element) {
// 保存原始尺寸和位置
window.element.dataset.originalWidth = window.config.width.toString()
window.element.dataset.originalHeight = window.config.height.toString()
window.element.dataset.originalX = (window.config.x || 0).toString()
window.element.dataset.originalY = (window.config.y || 0).toString()
// 设置最大化样式
Object.assign(window.element.style, {
position: 'fixed',
top: '0',
left: '0',
width: '100vw',
height: 'calc(100vh - 40px)', // 减去任务栏高度
display: 'block',
})
}
this.setActiveWindow(windowId)
return true
}
/**
* 还原窗体
*/
restoreWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
const targetState =
window.state === WindowState.MINIMIZED
? WindowState.ACTIVE
: window.state === WindowState.MAXIMIZED
? WindowState.ACTIVE
: window.state
this.updateWindowState(windowId, targetState)
if (window.element) {
if (window.state === WindowState.MINIMIZED) {
window.element.style.display = 'block'
} else if (window.state === WindowState.MAXIMIZED) {
// 恢复原始尺寸和位置
const originalWidth = window.element.dataset.originalWidth
const originalHeight = window.element.dataset.originalHeight
const originalX = window.element.dataset.originalX
const originalY = window.element.dataset.originalY
Object.assign(window.element.style, {
width: originalWidth ? `${originalWidth}px` : `${window.config.width}px`,
height: originalHeight ? `${originalHeight}px` : `${window.config.height}px`,
left: originalX ? `${originalX}px` : '50%',
top: originalY ? `${originalY}px` : '50%',
transform: originalX && originalY ? 'none' : 'translate(-50%, -50%)',
})
}
}
this.setActiveWindow(windowId)
return true
}
/**
* 设置窗体标题
*/
setWindowTitle(windowId: string, title: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
window.config.title = title
window.updatedAt = new Date()
// 更新DOM元素标题
if (window.element) {
const titleElement = window.element.querySelector('.window-title')
if (titleElement) {
titleElement.textContent = title
}
}
return true
}
/**
* 设置窗体尺寸
*/
setWindowSize(windowId: string, width: number, height: number): boolean {
const window = this.windows.get(windowId)
if (!window) return false
// 检查尺寸限制
const finalWidth = Math.max(
window.config.minWidth || 200,
Math.min(window.config.maxWidth || Infinity, width),
)
const finalHeight = Math.max(
window.config.minHeight || 150,
Math.min(window.config.maxHeight || Infinity, height),
)
window.config.width = finalWidth
window.config.height = finalHeight
window.updatedAt = new Date()
if (window.element) {
window.element.style.width = `${finalWidth}px`
window.element.style.height = `${finalHeight}px`
}
this.eventBus.notifyEvent('onResize', windowId, finalWidth, finalHeight)
return true
}
/**
* 获取窗体实例
*/
getWindow(windowId: string): WindowInstance | undefined {
return this.windows.get(windowId)
}
/**
* 获取所有窗体
*/
getAllWindows(): WindowInstance[] {
return Array.from(this.windows.values())
}
/**
* 获取活跃窗体ID
*/
getActiveWindowId(): string | null {
return this.activeWindowId.value
}
/**
* 设置活跃窗体
*/
setActiveWindow(windowId: string): boolean {
const window = this.windows.get(windowId)
if (!window) return false
this.activeWindowId.value = windowId
window.zIndex = this.nextZIndex++
if (window.element) {
window.element.style.zIndex = window.zIndex.toString()
}
this.eventBus.notifyEvent('onFocus', windowId)
return true
}
/**
* 创建窗体DOM元素
*/
private async createWindowElement(windowInstance: WindowInstance): Promise<void> {
const { id, config, appId } = windowInstance
// 检查是否为内置应用
let isBuiltInApp = false
try {
const { AppRegistry } = await import('../apps/AppRegistry')
const appRegistry = AppRegistry.getInstance()
isBuiltInApp = appRegistry.hasApp(appId)
} catch (error) {
console.warn('无法导入 AppRegistry')
}
// 创建窗体容器
const windowElement = document.createElement('div')
windowElement.className = 'system-window'
windowElement.id = `window-${id}`
// 设置基本样式
Object.assign(windowElement.style, {
position: 'fixed',
width: `${config.width}px`,
height: `${config.height}px`,
left: config.x ? `${config.x}px` : '50%',
top: config.y ? `${config.y}px` : '50%',
transform: config.x && config.y ? 'none' : 'translate(-50%, -50%)',
zIndex: windowInstance.zIndex.toString(),
backgroundColor: '#fff',
border: '1px solid #ccc',
borderRadius: '8px',
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
overflow: 'hidden',
})
// 创建窗体标题栏
const titleBar = this.createTitleBar(windowInstance)
windowElement.appendChild(titleBar)
// 创建窗体内容区域
const contentArea = document.createElement('div')
contentArea.className = 'window-content'
contentArea.style.cssText = `
width: 100%;
height: calc(100% - 40px);
overflow: hidden;
`
if (isBuiltInApp) {
// 内置应用:创建普通 div 容器AppRenderer 组件会在这里渲染内容
const appContainer = document.createElement('div')
appContainer.className = 'built-in-app-container'
appContainer.id = `app-container-${appId}`
appContainer.style.cssText = `
width: 100%;
height: 100%;
background: #fff;
`
contentArea.appendChild(appContainer)
console.log(`[WindowService] 为内置应用 ${appId} 创建了普通容器`)
} else {
// 外部应用:创建 iframe 容器
const iframe = document.createElement('iframe')
iframe.style.cssText = `
width: 100%;
height: 100%;
border: none;
background: #fff;
`
iframe.sandbox = 'allow-scripts allow-forms allow-popups' // 移除allow-same-origin以提高安全性
contentArea.appendChild(iframe)
// 保存 iframe 引用(仅对外部应用)
windowInstance.iframe = iframe
console.log(`[WindowService] 为外部应用 ${appId} 创建了 iframe 容器`)
}
windowElement.appendChild(contentArea)
// 添加到页面
document.body.appendChild(windowElement)
// 保存引用
windowInstance.element = windowElement
}
/**
* 创建窗体标题栏
*/
private createTitleBar(windowInstance: WindowInstance): HTMLElement {
const titleBar = document.createElement('div')
titleBar.className = 'window-title-bar'
titleBar.style.cssText = `
height: 40px;
background: linear-gradient(to bottom, #f8f9fa, #e9ecef);
border-bottom: 1px solid #dee2e6;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 12px;
cursor: move;
user-select: none;
`
// 窗体标题
const title = document.createElement('span')
title.className = 'window-title'
title.textContent = windowInstance.config.title
title.style.cssText = `
font-size: 14px;
font-weight: 500;
color: #333;
`
// 控制按钮组
const controls = document.createElement('div')
controls.className = 'window-controls'
controls.style.cssText = `
display: flex;
gap: 8px;
`
// 最小化按钮
if (windowInstance.config.minimizable !== false) {
const minimizeBtn = this.createControlButton('', () => {
this.minimizeWindow(windowInstance.id)
})
controls.appendChild(minimizeBtn)
}
// 最大化按钮
if (windowInstance.config.maximizable !== false) {
const maximizeBtn = this.createControlButton('□', () => {
if (windowInstance.state === WindowState.MAXIMIZED) {
this.restoreWindow(windowInstance.id)
} else {
this.maximizeWindow(windowInstance.id)
}
})
controls.appendChild(maximizeBtn)
}
// 关闭按钮
if (windowInstance.config.closable !== false) {
const closeBtn = this.createControlButton('×', () => {
this.destroyWindow(windowInstance.id)
})
closeBtn.style.color = '#dc3545'
controls.appendChild(closeBtn)
}
titleBar.appendChild(title)
titleBar.appendChild(controls)
// 添加拖拽功能
if (windowInstance.config.movable !== false) {
this.addDragFunctionality(titleBar, windowInstance)
}
return titleBar
}
/**
* 创建控制按钮
*/
private createControlButton(text: string, onClick: () => void): HTMLElement {
const button = document.createElement('button')
button.textContent = text
button.style.cssText = `
width: 24px;
height: 24px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
font-size: 16px;
line-height: 1;
`
button.addEventListener('click', onClick)
// 添加悬停效果
button.addEventListener('mouseenter', () => {
button.style.backgroundColor = '#e9ecef'
})
button.addEventListener('mouseleave', () => {
button.style.backgroundColor = 'transparent'
})
return button
}
/**
* 添加窗体拖拽功能
*/
private addDragFunctionality(titleBar: HTMLElement, windowInstance: WindowInstance): void {
let isDragging = false
let startX = 0
let startY = 0
let startLeft = 0
let startTop = 0
titleBar.addEventListener('mousedown', (e) => {
if (!windowInstance.element) return
isDragging = true
startX = e.clientX
startY = e.clientY
const rect = windowInstance.element.getBoundingClientRect()
startLeft = rect.left
startTop = rect.top
// 设置为活跃窗体
this.setActiveWindow(windowInstance.id)
e.preventDefault()
})
document.addEventListener('mousemove', (e) => {
if (!isDragging || !windowInstance.element) return
const deltaX = e.clientX - startX
const deltaY = e.clientY - startY
const newLeft = startLeft + deltaX
const newTop = startTop + deltaY
windowInstance.element.style.left = `${newLeft}px`
windowInstance.element.style.top = `${newTop}px`
windowInstance.element.style.transform = 'none'
// 更新配置
windowInstance.config.x = newLeft
windowInstance.config.y = newTop
this.eventBus.notifyEvent('onMove', windowInstance.id, newLeft, newTop)
})
document.addEventListener('mouseup', () => {
isDragging = false
})
}
/**
* 加载应用
*/
private async loadApplication(windowInstance: WindowInstance): Promise<void> {
// 动态导入 AppRegistry 检查是否为内置应用
try {
const { AppRegistry } = await import('../apps/AppRegistry')
const appRegistry = AppRegistry.getInstance()
// 如果是内置应用,直接返回,不需要等待
if (appRegistry.hasApp(windowInstance.appId)) {
console.log(`[WindowService] 内置应用 ${windowInstance.appId} 无需等待加载`)
return Promise.resolve()
}
} catch (error) {
console.warn('无法导入 AppRegistry使用传统加载方式')
}
// 对于外部应用,保持原有的加载逻辑
return new Promise((resolve) => {
console.log(`[WindowService] 开始加载外部应用 ${windowInstance.appId}`)
setTimeout(() => {
if (windowInstance.iframe) {
// 这里可以设置 iframe 的 src 来加载具体应用
windowInstance.iframe.src = 'about:blank'
// 添加一些示例内容
const doc = windowInstance.iframe.contentDocument
if (doc) {
doc.body.innerHTML = `
<div style="padding: 20px; font-family: sans-serif;">
<h2>应用: ${windowInstance.config.title}</h2>
<p>应用ID: ${windowInstance.appId}</p>
<p>窗体ID: ${windowInstance.id}</p>
<p>这是一个示例应用内容。</p>
</div>
`
}
}
console.log(`[WindowService] 外部应用 ${windowInstance.appId} 加载完成`)
resolve()
}, 200) // 改为200ms即使是外部应用也不需要这么长的时间
})
}
/**
* 更新窗体状态
*/
private updateWindowState(windowId: string, newState: WindowState): void {
const window = this.windows.get(windowId)
if (!window) return
const oldState = window.state
// 只有状态真正发生变化时才触发事件
if (oldState === newState) return
window.state = newState
window.updatedAt = new Date()
// 所有状态变化都应该触发事件,这是正常的系统行为
console.log(`[WindowService] 窗体状态变化: ${windowId} ${oldState} -> ${newState}`)
this.eventBus.notifyEvent('onStateChange', windowId, newState, oldState)
}
}

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
}