diff --git a/README.md b/README.md index 79387f3..249a6e4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # vue-desktop -This template should help get you started developing with Vue 3 in Vite. +浏览器:Chrome 84+、Edge 84+、Firefox 79+、Safari 14+ + +Node.js:v14+ + +不支持IE ## Recommended IDE Setup diff --git a/src/core/XSystem.ts b/src/core/XSystem.ts index 6282f18..f84db82 100644 --- a/src/core/XSystem.ts +++ b/src/core/XSystem.ts @@ -9,6 +9,7 @@ import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' import type { IProcessManage } from '@/core/process/IProcessManage.ts' import type { IProcess } from '@/core/process/IProcess.ts' +import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' export default class XSystem { private static _instance: XSystem = new XSystem() @@ -40,10 +41,10 @@ export default class XSystem { // 运行进程 public async run( - proc: string | ProcessInfoImpl, - constructor?: new (info: ProcessInfoImpl) => T, + proc: string | IProcessInfo, + constructor?: new (info: IProcessInfo) => T, ): Promise { - let info = typeof proc === 'string' ? this._processManage.findProcessInfoByName(proc) : proc + let info = typeof proc === 'string' ? this._processManage.findProcessInfoByName(proc)! : proc if (isUndefined(info)) { throw new Error(`未找到进程信息:${proc}`) } diff --git a/src/core/desktop/DesktopProcess.ts b/src/core/desktop/DesktopProcess.ts index 01f389b..389120b 100644 --- a/src/core/desktop/DesktopProcess.ts +++ b/src/core/desktop/DesktopProcess.ts @@ -7,6 +7,7 @@ import DesktopComponent from '@/core/desktop/ui/DesktopComponent.vue' import { naiveUi } from '@/core/common/naive-ui/components.ts' import { DesktopEventEnum } from '@/core/events/EventTypes.ts' import { debounce } from 'lodash' +import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' export class DesktopProcess extends ProcessImpl { private _desktopRootDom: HTMLElement; @@ -66,7 +67,7 @@ export class DesktopProcess extends ProcessImpl { return XSystem.instance.eventManage; } - constructor(info: ProcessInfoImpl) { + constructor(info: IProcessInfo) { super(info) console.log('DesktopProcess') } diff --git a/src/core/desktop/ui/hooks/useDesktopInit.ts b/src/core/desktop/ui/hooks/useDesktopInit.ts index 146a602..388f7db 100644 --- a/src/core/desktop/ui/hooks/useDesktopInit.ts +++ b/src/core/desktop/ui/hooks/useDesktopInit.ts @@ -79,10 +79,14 @@ export function useDesktopInit(containerStr: string) { }) const appIconsRef = ref(appIcons) + const exceedApp = ref([]) - watch(() => [gridTemplate.rowCount, gridTemplate.colCount], ([nRows, nCols], [oRows, oCols]) => { - if (oCols == 1 && oRows == 1) return - appIconsRef.value = rearrangeIcons(toRaw(appIconsRef.value), nCols, nRows, oCols, oRows) + 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 }) XSystem.instance.eventManage.addEventListener(DesktopEventEnum.onDesktopAppIconPos, (iconInfo) => { @@ -98,59 +102,69 @@ export function useDesktopInit(containerStr: string) { /** * 重新安排图标位置 - * @param appIcons 图标信息 - * @param newCols 新的列数 - * @param newRows 新的行数 - * @param oldCols 旧的列数 - * @param oldRows 旧的行数 + * @param appIconInfos 图标信息 + * @param maxCol 列数 + * @param maxRow 行数 */ function rearrangeIcons( - appIcons: IDesktopAppIcon[], - newCols: number, - newRows: number, - oldCols: number, - oldRows: number -): IDesktopAppIcon[] { - if (oldCols === newCols && oldRows === newRows) { - return appIcons; - } + appIconInfos: IDesktopAppIcon[], + maxCol: number, + maxRow: number +): IRearrangeInfo { const occupied = new Set(); function key(x: number, y: number) { return `${x},${y}`; } - const result: IDesktopAppIcon[] = [] - const exceed: IDesktopAppIcon[] = [] + const appIcons: IDesktopAppIcon[] = [] + const hideAppIcons: IDesktopAppIcon[] = [] + const temp: IDesktopAppIcon[] = [] - for (const appIcon of appIcons) { + for (const appIcon of appIconInfos) { const { x, y } = appIcon; - if (x <= newCols && y <= newRows) { + if (x <= maxCol && y <= maxRow) { if (!occupied.has(key(x, y))) { occupied.add(key(x, y)) - result.push({ ...appIcon, x, y }) + appIcons.push({ ...appIcon, x, y }) } } else { - exceed.push(appIcon) + temp.push(appIcon) } } - for (const appIcon of exceed) { - // 最后格子也被占 → 从 (1,1) 开始找空位 - let placed = false; - for (let c = 1; c <= newCols; c++) { - for (let r = 1; r <= newRows; r++) { - if (!occupied.has(key(c, r))) { - occupied.add(key(c, r)); - result.push({ ...appIcon, x: c, y: r }); - placed = true; - break; + 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; } - if (placed) break; + } else { + // 放不下了 + hideAppIcons.push(appIcon) } } - return result; + return { + appIcons, + hideAppIcons + }; +} + +interface IRearrangeInfo { + /** 正常的桌面图标信息 */ + appIcons: IDesktopAppIcon[]; + /** 隐藏的桌面图标信息(超出屏幕显示的) */ + hideAppIcons: IDesktopAppIcon[]; } diff --git a/src/core/state/Observable.ts b/src/core/state/Observable.ts new file mode 100644 index 0000000..6d053e6 --- /dev/null +++ b/src/core/state/Observable.ts @@ -0,0 +1,158 @@ +type Listener = (state: T) => void +type KeyListener = (changed: Pick) => void + +/** + * 从给定类型 T 中排除所有函数类型的属性,只保留非函数类型的属性 + * @template T - 需要处理的原始类型 + * @returns 一个新的类型,该类型只包含原始类型中非函数类型的属性 + */ +type NonFunctionProperties = { + [K in keyof T]: T[K] extends Function ? never : T[K] +} + +/** + * 创建一个可观察对象,用于管理状态和事件。 + * @template T - 需要处理的状态类型 + * @example + * interface AppState { + * count: number + * isOpen: boolean + * title: string + * } + * + * const app = new Observable({ + * count: 0, + * isOpen: false, + * title: "Demo" + * }) + * + * // 全量订阅 + * app.subscribe(state => console.log("全量:", state)) + * + * // 单字段订阅 + * app.subscribeKey("count", changes => console.log("count 变化:", changes)) + * + * // 多字段订阅 + * app.subscribeKey(["count", "isOpen"], changes => + * console.log("count/isOpen 回调:", changes) + * ) + * + * // 直接修改属性 + * app.count = 1 + * app.isOpen = true + * app.title = "New Title" + * + * // 输出示例: + * // 全量: { count: 1, isOpen: true, title: 'New Title' } + * // count 变化: { count: 1 } + * // count/isOpen 回调: { count: 1, isOpen: true } + */ +export class Observable { + private listeners = new Set>>() + private keyListeners = new Map>>() + private registry = new FinalizationRegistry((ref: WeakRef) => { + this.listeners.delete(ref) + this.keyListeners.forEach(set => set.delete(ref)) + }) + + private pendingKeys = new Set() + private notifyScheduled = false + + constructor(initialState: NonFunctionProperties) { + Object.assign(this, initialState) + + // Proxy 拦截属性修改 + return new Proxy(this, { + set: (target, prop: string, value) => { + const key = prop as keyof T + (target as any)[key] = value + + // 每次赋值都加入 pendingKeys + this.pendingKeys.add(key) + this.scheduleNotify() + return true + }, + get: (target, prop: string) => (target as any)[prop] + }) + } + + /** 安排微任务通知 */ + private scheduleNotify() { + if (!this.notifyScheduled) { + this.notifyScheduled = true + Promise.resolve().then(() => this.flushNotify()) + } + } + + /** 执行通知:全量 + 单/多字段通知 */ + private flushNotify() { + const keys = Array.from(this.pendingKeys) + this.pendingKeys.clear() + this.notifyScheduled = false + + // 全量通知一次 + for (const ref of this.listeners) { + const fn = ref.deref() + if (fn) fn(this as unknown as T) + else this.listeners.delete(ref) + } + + // 单/多字段通知(合并函数) + const fnMap = new Map() + + for (const key of keys) { + const set = this.keyListeners.get(key) + if (!set) continue + for (const ref of set) { + const fn = ref.deref() + if (!fn) { + set.delete(ref) + continue + } + if (!fnMap.has(fn)) fnMap.set(fn, []) + const arr = fnMap.get(fn)! + if (!arr.includes(key)) arr.push(key) + } + } + + // 调用每个函数一次,并返回订阅字段的当前值 + fnMap.forEach((subKeys, fn) => { + const result: Partial = {} + subKeys.forEach(k => (result[k] = (this as any)[k])) + fn(result) + }) + } + + /** 全量订阅 */ + subscribe(fn: Listener) { + const ref = new WeakRef(fn) + this.listeners.add(ref) + this.registry.register(fn, ref) + return () => { + this.listeners.delete(ref) + this.registry.unregister(fn) + } + } + + /** 单字段或多字段订阅 */ + subscribeKey(keys: K | K[], fn: KeyListener) { + const keyArray = Array.isArray(keys) ? keys : [keys] + const refs: WeakRef[] = [] + + for (const key of keyArray) { + if (!this.keyListeners.has(key)) this.keyListeners.set(key, new Set()) + const ref = new WeakRef(fn) + this.keyListeners.get(key)!.add(ref) + this.registry.register(fn, ref) + refs.push(ref) + } + + 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) + } + } +} diff --git a/src/core/state/useObservableReact.ts b/src/core/state/useObservableReact.ts new file mode 100644 index 0000000..df73fd2 --- /dev/null +++ b/src/core/state/useObservableReact.ts @@ -0,0 +1,32 @@ +// import { useEffect, useState } from "react" +// import { Observable } from "./Observable" +// +// export function useObservable( +// observable: Observable, +// keys?: K | K[] +// ): Pick | T { +// const keyArray = keys ? (Array.isArray(keys) ? keys : [keys]) : null +// +// const [state, setState] = useState>(() => { +// if (keyArray) { +// const init: Partial = {} +// keyArray.forEach(k => (init[k] = (observable as any)[k])) +// return init +// } +// return { ...(observable as any) } +// }) +// +// useEffect(() => { +// const unsubscribe = keyArray +// ? observable.subscribeKey(keyArray as K[], changed => { +// setState(prev => ({ ...prev, ...changed })) +// }) +// : observable.subscribe(s => setState({ ...s })) +// +// return () => { +// unsubscribe() +// } +// }, [observable, ...(keyArray || [])]) +// +// return state as any +// } diff --git a/src/core/state/useObservableVue.ts b/src/core/state/useObservableVue.ts new file mode 100644 index 0000000..fd17452 --- /dev/null +++ b/src/core/state/useObservableVue.ts @@ -0,0 +1,38 @@ +import type { Observable } from '@/core/state/Observable.ts' +import { onUnmounted, ref, type Ref } from 'vue' + +/** + * vue使用自定义的 observable + * @param observable + * @param keys + * @returns + * @example + * const app = new Observable({ count: 0, isOpen: false, title: "Demo" }) + * // 多字段订阅 + * const state = useObservable(app, ["count", "isOpen"]) + * // state.value.count / state.value.isOpen 响应式 + */ +export function useObservable( + observable: Observable, + keys?: K[] | K +): Ref | T> { + const state = ref({} as any) // 响应式 + + const keyArray = keys ? (Array.isArray(keys) ? keys : [keys]) : null + + // 订阅回调 + const unsubscribe = keyArray + ? observable.subscribeKey(keyArray as K[], changed => { + // 更新响应式 state + Object.assign(state.value, changed) + }) + : observable.subscribe(s => { + state.value = { ...s } + }) + + onUnmounted(() => { + unsubscribe() + }) + + return state +} diff --git a/src/core/system/BasicSystemProcess.ts b/src/core/system/BasicSystemProcess.ts index 1cb2074..60c9a6c 100644 --- a/src/core/system/BasicSystemProcess.ts +++ b/src/core/system/BasicSystemProcess.ts @@ -1,5 +1,6 @@ import ProcessImpl from '../process/impl/ProcessImpl.ts' import { ProcessInfoImpl } from '@/core/process/impl/ProcessInfoImpl.ts' +import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' /** * 基础系统进程 @@ -11,7 +12,7 @@ export class BasicSystemProcess extends ProcessImpl{ return this._isMounted; } - constructor(info: ProcessInfoImpl) { + constructor(info: IProcessInfo) { super(info) console.log('BasicSystemProcess') } diff --git a/tsconfig.app.json b/tsconfig.app.json index 60fcea2..60a4dae 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -9,7 +9,8 @@ "@/*": ["./src/*"] }, "experimentalDecorators": true, - "target": "ES6", + "target": "es2021", + "lib": ["es2021", "dom"], "module": "ESNext", "strictPropertyInitialization": false, // 严格属性初始化检查 "noUnusedLocals": false, // 检查未使用的局部变量