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