From 16b4b27352711583955808b204a73134a150752d Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Mon, 22 Sep 2025 13:23:12 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/core/XSystem.ts | 21 +--- src/core/common/types/IDestroyable.ts | 8 ++ src/core/desktop/DesktopProcess.ts | 114 ++++++++------------ src/core/desktop/ui/DesktopComponent.vue | 49 ++++++--- src/core/events/IEventBuilder.ts | 4 +- src/core/events/impl/EventBuilderImpl.ts | 14 +-- src/core/process/IProcess.ts | 3 +- src/core/process/impl/ProcessImpl.ts | 6 ++ src/core/process/impl/ProcessManagerImpl.ts | 7 +- src/core/state/impl/ObservableImpl.ts | 59 ++++------ src/core/state/store/GlobalStore.ts | 14 +++ src/core/utils/DraggableResizableWindow.ts | 26 ++--- src/core/window/IWindowForm.ts | 3 +- src/core/window/impl/WindowFormImpl.ts | 8 +- 14 files changed, 167 insertions(+), 169 deletions(-) create mode 100644 src/core/common/types/IDestroyable.ts diff --git a/src/core/XSystem.ts b/src/core/XSystem.ts index 6d0f853..f681c42 100644 --- a/src/core/XSystem.ts +++ b/src/core/XSystem.ts @@ -1,23 +1,14 @@ import { BasicSystemProcess } from '@/core/system/BasicSystemProcess.ts' import { DesktopProcess } from '@/core/desktop/DesktopProcess.ts' -import { ObservableWeakRefImpl } from '@/core/state/impl/ObservableWeakRefImpl.ts' -import type { IObservable } from '@/core/state/IObservable.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' -interface IGlobalState { - isLogin: boolean -} - export default class XSystem { private static _instance: XSystem = new XSystem() - private _globalState: IObservable = new ObservableWeakRefImpl({ - isLogin: false - }) private _desktopRootDom: HTMLElement; constructor() { @@ -31,19 +22,13 @@ export default class XSystem { public static get instance() { return this._instance } - public get globalState() { - return this._globalState - } public get desktopRootDom() { return this._desktopRootDom } - public initialization(dom: HTMLDivElement) { + public async initialization(dom: HTMLDivElement) { this._desktopRootDom = dom - processManager.runProcess('basic-system', BasicSystemProcess).then(() => { - processManager.runProcess('desktop', DesktopProcess).then((proc) => { - proc.mount(dom) - }) - }) + await processManager.runProcess('basic-system', BasicSystemProcess) + await processManager.runProcess('desktop', DesktopProcess, dom) } } diff --git a/src/core/common/types/IDestroyable.ts b/src/core/common/types/IDestroyable.ts new file mode 100644 index 0000000..f069e4d --- /dev/null +++ b/src/core/common/types/IDestroyable.ts @@ -0,0 +1,8 @@ +/** + * 可销毁接口 + * 销毁实例,清理副作用,让内存可以被回收 + */ +export interface IDestroyable { + /** 销毁实例,清理副作用,让内存可以被回收 */ + destroy(): void +} diff --git a/src/core/desktop/DesktopProcess.ts b/src/core/desktop/DesktopProcess.ts index 6dea86c..2a173c7 100644 --- a/src/core/desktop/DesktopProcess.ts +++ b/src/core/desktop/DesktopProcess.ts @@ -5,95 +5,68 @@ 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 { eventManager } from '@/core/events/EventManager.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 { - private _desktopRootDom: HTMLElement; - private _isMounted: boolean = false; - private _width: number = 0; - private _height: number = 0; - private _pendingResize: boolean = false; + /** 桌面根dom,类似显示器 */ + private readonly _monitorDom: HTMLElement + private _isMounted: boolean = false + private _data = new ObservableImpl({ + monitorWidth: 0, + monitorHeight: 0, + }) - public get desktopRootDom() { - return this._desktopRootDom; + public get monitorDom() { + return this._monitorDom } public get isMounted() { - return this._isMounted; + return this._isMounted } public get basicSystemProcess() { return processManager.findProcessByName('basic-system') } - public get width() { - return this._width; - } - public set width(value: number) { - if (this._height === value) return; - if (!this._isMounted) return; - this._width = value; - this._desktopRootDom.style.width = value >= 0 ? `${value}px` : '100%'; - - this.scheduleResizeEvent() - } - public get height() { - return this._height; - } - public set height(value: number) { - if (this._height === value) return; - if (!this._isMounted) return; - this._height = value; - this._desktopRootDom.style.height = value >= 0 ? `${value}px` : '100%'; - - this.scheduleResizeEvent() + get data() { + return this._data } - private scheduleResizeEvent() { - if (this._pendingResize) return; - - this._pendingResize = true; - - Promise.resolve().then(() => { - if (this._pendingResize) { - this._pendingResize = false; - console.log('onDesktopRootDomResize') - eventManager.notifyEvent('onDesktopRootDomResize', this._width, this._height); - } - }); - } - - constructor(info: IProcessInfo) { + constructor(info: IProcessInfo, dom: HTMLDivElement) { super(info) console.log('DesktopProcess') - } - - public mount(dom: HTMLDivElement) { - console.log('DesktopProcess: start mount') - if (this._isMounted) return - this._width = window.innerWidth - this._height = window.innerHeight - window.addEventListener( - 'resize', - debounce(() => { - this.width = window.innerWidth - this.height = window.innerHeight - }, 300) - ) - - dom.style.zIndex = '0'; - dom.style.width = `${this._width}px` - dom.style.height = `${this._height}px` dom.style.position = 'relative' dom.style.overflow = 'hidden' - this._desktopRootDom = dom + 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(dom) - - // this.initDesktop(dom) - + app.mount(this._monitorDom) this._isMounted = true } @@ -101,4 +74,9 @@ export class DesktopProcess extends ProcessImpl { const d = document.createElement('desktop-element') dom.appendChild(d) } + + override destroy() { + super.destroy() + window.removeEventListener('resize', this.onResize) + } } \ No newline at end of file diff --git a/src/core/desktop/ui/DesktopComponent.vue b/src/core/desktop/ui/DesktopComponent.vue index 0791d51..76f32b9 100644 --- a/src/core/desktop/ui/DesktopComponent.vue +++ b/src/core/desktop/ui/DesktopComponent.vue @@ -5,16 +5,20 @@ >
-
- +
-
测试
+
+ 测试 +
@@ -32,18 +36,29 @@ 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, - }) - }, -) +// eventManager.addEventListener('onDesktopRootDomResize', (width, height) => { +// console.log(width, height) +// notificationApi.create({ +// title: '桌面通知', +// content: `桌面尺寸变化${width}x${height}}`, +// duration: 2000, +// }) +// }) const onContextMenu = (e: MouseEvent) => { e.preventDefault() @@ -61,7 +76,7 @@ $taskBarHeight: 40px; .desktop-bg { @apply w-full h-full flex-1 p-2 pos-relative; - background-image: url("imgs/desktop-bg-2.jpeg"); + background-image: url('imgs/desktop-bg-2.jpeg'); background-repeat: no-repeat; background-size: cover; height: calc(100% - #{$taskBarHeight}); diff --git a/src/core/events/IEventBuilder.ts b/src/core/events/IEventBuilder.ts index 8db40ae..f29cbe0 100644 --- a/src/core/events/IEventBuilder.ts +++ b/src/core/events/IEventBuilder.ts @@ -1,3 +1,5 @@ +import type { IDestroyable } from '@/core/common/types/IDestroyable.ts' + /** * 事件定义 * @interface IEventMap 事件定义 键是事件名称,值是事件处理函数 @@ -9,7 +11,7 @@ export interface IEventMap { /** * 事件管理器接口定义 */ -export interface IEventBuilder { +export interface IEventBuilder extends IDestroyable { /** * 添加事件监听 * @param eventName 事件名称 diff --git a/src/core/events/impl/EventBuilderImpl.ts b/src/core/events/impl/EventBuilderImpl.ts index ad978b8..2c29425 100644 --- a/src/core/events/impl/EventBuilderImpl.ts +++ b/src/core/events/impl/EventBuilderImpl.ts @@ -5,9 +5,7 @@ interface HandlerWrapper any> { once: boolean } -export class EventBuilderImpl - implements IEventBuilder -{ +export class EventBuilderImpl implements IEventBuilder { private _eventHandlers = new Map>>() /** @@ -24,9 +22,9 @@ export class EventBuilderImpl eventName: E, handler: F, options?: { - immediate?: boolean; - immediateArgs?: Parameters; - once?: boolean; + immediate?: boolean + immediateArgs?: Parameters + once?: boolean }, ) { if (!handler) return @@ -91,4 +89,8 @@ export class EventBuilderImpl } } } + + destroy() { + this._eventHandlers.clear() + } } diff --git a/src/core/process/IProcess.ts b/src/core/process/IProcess.ts index 45f25c0..aed33f5 100644 --- a/src/core/process/IProcess.ts +++ b/src/core/process/IProcess.ts @@ -2,11 +2,12 @@ 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 { +export interface IProcess extends IDestroyable { /** 进程id */ get id(): string; /** 进程信息 */ diff --git a/src/core/process/impl/ProcessImpl.ts b/src/core/process/impl/ProcessImpl.ts index 0a513ee..75a3528 100644 --- a/src/core/process/impl/ProcessImpl.ts +++ b/src/core/process/impl/ProcessImpl.ts @@ -66,12 +66,18 @@ export default class ProcessImpl implements IProcess { 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() + } } \ No newline at end of file diff --git a/src/core/process/impl/ProcessManagerImpl.ts b/src/core/process/impl/ProcessManagerImpl.ts index 2bb8731..e87be6a 100644 --- a/src/core/process/impl/ProcessManagerImpl.ts +++ b/src/core/process/impl/ProcessManagerImpl.ts @@ -37,9 +37,10 @@ export default class ProcessManagerImpl implements IProcessManager { this._processInfos.push(...internalProcessInfos) } - public async runProcess( + public async runProcess( proc: string | IProcessInfo, - constructor?: new (info: IProcessInfo) => T, + constructor?: new (info: IProcessInfo, ...args: A) => T, + ...args: A ): Promise { let info = typeof proc === 'string' ? this.findProcessInfoByName(proc) : proc if (isUndefined(info)) { @@ -55,7 +56,7 @@ export default class ProcessManagerImpl implements IProcessManager { } // 创建进程 - let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info) + let process = isUndefined(constructor) ? new ProcessImpl(info) : new constructor(info, ...args) return process as T } diff --git a/src/core/state/impl/ObservableImpl.ts b/src/core/state/impl/ObservableImpl.ts index 3253b64..6a558f7 100644 --- a/src/core/state/impl/ObservableImpl.ts +++ b/src/core/state/impl/ObservableImpl.ts @@ -81,8 +81,13 @@ export class ObservableImpl> implements IObs /** 全量订阅函数集合 */ private listeners: Set> = new Set() - /** 字段订阅函数集合 */ - private keyListeners: Map>> = new Map() + /** + * 字段订阅函数集合 + * 新结构: + * Map> + * 记录每个回调订阅的字段数组,保证多字段订阅 always 返回所有订阅字段值 + */ + private keyListeners: Map, Array> = new Map() /** 待通知的字段集合 */ private pendingKeys: Set = new Set() @@ -161,7 +166,7 @@ export class ObservableImpl> implements IObs /** 安排下一次通知(微任务合并) */ private scheduleNotify(): void { - if (!this.notifyScheduled && !this.disposed) { + if (!this.notifyScheduled && !this.disposed && this.pendingKeys.size > 0) { this.notifyScheduled = true Promise.resolve().then(() => this.flushNotify()) } @@ -171,7 +176,6 @@ export class ObservableImpl> implements IObs private flushNotify(): void { if (this.disposed) return - const keys = Array.from(this.pendingKeys) this.pendingKeys.clear() this.notifyScheduled = false @@ -180,30 +184,13 @@ export class ObservableImpl> implements IObs try { fn(this.state as unknown as T) } catch (err) { - // 可以根据需要把错误上报或自定义处理 - // 这里简单打印以便调试 - // eslint-disable-next-line no-console console.error("Observable listener error:", err) } } - // 字段订阅:把同一个回调的多个 key 聚合到一次调用里 - const fnMap: Map, Array> = new Map() - for (const key of keys) { - const set = this.keyListeners.get(key) - if (!set) continue - for (const fn of set) { - const existing = fnMap.get(fn) - if (existing === undefined) { - fnMap.set(fn, [key]) - } else { - existing.push(key) - } - } - } - - // 调用每个字段订阅回调 - fnMap.forEach((subKeys, fn) => { + // ================== 字段订阅 ================== + // 遍历所有回调,每个回调都返回它订阅的字段(即使只有部分字段变化) + this.keyListeners.forEach((subKeys, fn) => { try { // 构造 Pick 风格的结果对象:结果类型为 Pick const result = {} as Pick @@ -214,7 +201,6 @@ export class ObservableImpl> implements IObs // 调用时类型上兼容 TObservableKeyListener,因为我们传的是对应 key 的 Pick fn(result as Pick) } catch (err) { - // eslint-disable-next-line no-console console.error("Observable keyListener error:", err) } }) @@ -227,7 +213,6 @@ export class ObservableImpl> implements IObs try { fn(this.state as unknown as T) } catch (err) { - // eslint-disable-next-line no-console console.error("Observable subscribe immediate error:", err) } } @@ -236,19 +221,18 @@ export class ObservableImpl> implements IObs } } - /** 订阅指定字段变化 */ + /** 订阅指定字段变化(多字段订阅 always 返回所有字段值) */ public subscribeKey( keys: K | K[], fn: TObservableKeyListener, options: { immediate?: boolean } = {} ): () => void { const keyArray = Array.isArray(keys) ? keys : [keys] - for (const key of keyArray) { - if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set()) - // 存储为 Set> - this.keyListeners.get(key)!.add(fn as TObservableKeyListener) - } + // ================== 存储回调和它订阅的字段数组 ================== + this.keyListeners.set(fn as TObservableKeyListener, keyArray as (keyof T)[]) + + // ================== 立即调用 ================== if (options.immediate) { const result = {} as Pick keyArray.forEach(k => { @@ -257,30 +241,26 @@ export class ObservableImpl> implements IObs try { fn(result) } catch (err) { - // eslint-disable-next-line no-console console.error("Observable subscribeKey immediate error:", err) } } + // ================== 返回取消订阅函数 ================== return () => { - for (const key of keyArray) { - this.keyListeners.get(key)?.delete(fn as TObservableKeyListener) - } + this.keyListeners.delete(fn as TObservableKeyListener) } } /** 批量更新状态(避免重复 schedule) */ public patch(values: Partial): void { let changed = false - // 用 for..in 保持和对象字面量兼容(跳过原型链) for (const key in values) { if (Object.prototype.hasOwnProperty.call(values, key)) { const typedKey = key as keyof T const oldValue = (this.state as Record)[typedKey] const newValue = values[typedKey] as unknown if (oldValue !== newValue) { - // 直接写入 state,会被 Proxy 的 set 捕获并安排通知 - ;(this.state as Record)[typedKey] = newValue + (this.state as Record)[typedKey] = newValue changed = true } } @@ -322,3 +302,4 @@ export class ObservableImpl> implements IObs } } + diff --git a/src/core/state/store/GlobalStore.ts b/src/core/state/store/GlobalStore.ts index e69de29..b777904 100644 --- a/src/core/state/store/GlobalStore.ts +++ b/src/core/state/store/GlobalStore.ts @@ -0,0 +1,14 @@ +import { ObservableImpl } from '@/core/state/impl/ObservableImpl.ts' + +interface IGlobalStoreParams { + /** 桌面根dom ID,类似显示器 */ + monitorDomId: string; + monitorWidth: number; + monitorHeight: number; +} + +export const globalStore = new ObservableImpl({ + monitorDomId: '#app', + monitorWidth: 0, + monitorHeight: 0 +}) diff --git a/src/core/utils/DraggableResizableWindow.ts b/src/core/utils/DraggableResizableWindow.ts index 2e2be6e..b77e744 100644 --- a/src/core/utils/DraggableResizableWindow.ts +++ b/src/core/utils/DraggableResizableWindow.ts @@ -734,17 +734,19 @@ export class DraggableResizableWindow { * 销毁实例 */ public destroy() { - 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); + 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) {} } } diff --git a/src/core/window/IWindowForm.ts b/src/core/window/IWindowForm.ts index d42b7d3..d832878 100644 --- a/src/core/window/IWindowForm.ts +++ b/src/core/window/IWindowForm.ts @@ -1,7 +1,8 @@ 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 { +export interface IWindowForm extends IDestroyable { /** 窗体id */ get id(): string; /** 窗体所属的进程 */ diff --git a/src/core/window/impl/WindowFormImpl.ts b/src/core/window/impl/WindowFormImpl.ts index b9f0f5e..e8d82f8 100644 --- a/src/core/window/impl/WindowFormImpl.ts +++ b/src/core/window/impl/WindowFormImpl.ts @@ -35,10 +35,10 @@ export interface IWindowFormDataState { export default class WindowFormImpl implements IWindowForm { private readonly _id: string = uuidV4() - private readonly _proc: IProcess; + private readonly _proc: IProcess + private readonly _data: IObservable private dom: HTMLElement private drw: DraggableResizableWindow - private _data: IObservable; public get id() { return this._id @@ -70,7 +70,7 @@ export default class WindowFormImpl implements IWindowForm { width: config.width ?? 200, height: config.height ?? 100, state: 'default', - closed: false + closed: false, }) this.initEvent() @@ -109,4 +109,6 @@ export default class WindowFormImpl implements IWindowForm { public minimize() {} public maximize() {} public restore() {} + + public destroy() {} }