()
+
+/**
+ * 系统进程的事件
+ * @description
+ * onAuthChange - 认证状态改变
+ * onThemeChange - 主题改变
+ */
+export interface IBasicSystemEvent extends IEventMap {
+ /** 认证状态改变 */
+ onAuthChange: () => {},
+ /** 主题改变 */
+ onThemeChange: (theme: string) => void
+}
+
+/**
+ * 桌面进程的事件
+ * @description
+ * onDesktopRootDomResize - 桌面根dom尺寸改变
+ * onDesktopProcessInitialize - 桌面进程初始化完成
+ */
+export interface IDesktopEvent extends IEventMap {
+ /** 桌面根dom尺寸改变 */
+ onDesktopRootDomResize: (width: number, height: number) => void
+ /** 桌面进程初始化完成 */
+ onDesktopProcessInitialize: () => void
+ /** 桌面应用图标位置改变 */
+ onDesktopAppIconPos: (iconInfo: IDesktopAppIcon) => void
+}
+
+export interface IAllEvent extends IDesktopEvent, IBasicSystemEvent {}
diff --git a/src/events/IEventBuilder.ts b/src/events/IEventBuilder.ts
new file mode 100644
index 0000000..f29cbe0
--- /dev/null
+++ b/src/events/IEventBuilder.ts
@@ -0,0 +1,47 @@
+import type { IDestroyable } from '@/core/common/types/IDestroyable.ts'
+
+/**
+ * 事件定义
+ * @interface IEventMap 事件定义 键是事件名称,值是事件处理函数
+ */
+export interface IEventMap {
+ [key: string]: (...args: any[]) => void
+}
+
+/**
+ * 事件管理器接口定义
+ */
+export interface IEventBuilder extends IDestroyable {
+ /**
+ * 添加事件监听
+ * @param eventName 事件名称
+ * @param handler 事件处理函数
+ * @param options 配置项 { immediate: 立即执行一次 immediateArgs: 立即执行的参数 once: 只监听一次 }
+ * @returns void
+ */
+ addEventListener(
+ eventName: E,
+ handler: F,
+ options?: {
+ immediate?: boolean
+ immediateArgs?: Parameters
+ once?: boolean
+ },
+ ): void
+
+ /**
+ * 移除事件监听
+ * @param eventName 事件名称
+ * @param handler 事件处理函数
+ * @returns void
+ */
+ removeEventListener(eventName: E, handler: F): void
+
+ /**
+ * 触发事件
+ * @param eventName 事件名称
+ * @param args 参数
+ * @returns void
+ */
+ notifyEvent(eventName: E, ...args: Parameters): void
+}
\ No newline at end of file
diff --git a/src/events/WindowFormEventManager.ts b/src/events/WindowFormEventManager.ts
new file mode 100644
index 0000000..0da2044
--- /dev/null
+++ b/src/events/WindowFormEventManager.ts
@@ -0,0 +1,61 @@
+import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl.ts'
+import type { IEventMap } from '@/events/IEventBuilder.ts'
+import type { TWindowFormState } from '@/ui/types/WindowFormTypes.ts'
+
+/**
+ * 窗口的事件
+ */
+export interface IWindowFormEvent 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()
diff --git a/src/events/impl/EventBuilderImpl.ts b/src/events/impl/EventBuilderImpl.ts
new file mode 100644
index 0000000..2c29425
--- /dev/null
+++ b/src/events/impl/EventBuilderImpl.ts
@@ -0,0 +1,96 @@
+import type { IEventBuilder, IEventMap } from '@/core/events/IEventBuilder.ts'
+
+interface HandlerWrapper any> {
+ fn: T
+ once: boolean
+}
+
+export class EventBuilderImpl implements IEventBuilder {
+ private _eventHandlers = new Map>>()
+
+ /**
+ * 添加事件监听器
+ * @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(
+ eventName: E,
+ handler: F,
+ options?: {
+ immediate?: boolean
+ immediateArgs?: Parameters
+ once?: boolean
+ },
+ ) {
+ if (!handler) return
+ if (!this._eventHandlers.has(eventName)) {
+ this._eventHandlers.set(eventName, new Set>())
+ }
+
+ 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(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(eventName: E, ...args: Parameters) {
+ 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()
+ }
+}
diff --git a/src/main.ts b/src/main.ts
index a54b5e3..98ef18d 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,16 +1,15 @@
import { createApp } from 'vue'
import { createPinia } from 'pinia'
+import { naiveUi } from '@/common/naive-ui/components.ts'
import 'virtual:uno.css'
import './css/basic.css'
-// import App from './App.vue'
+import App from './ui/App.vue'
-// const app = createApp(App)
-//
-// app.use(createPinia())
-//
-// app.mount('#app')
+const app = createApp(App)
-import XSystem from '@/core/XSystem.ts'
-XSystem.instance.initialization(document.querySelector('#app')!);
\ No newline at end of file
+app.use(createPinia())
+app.use(naiveUi)
+
+app.mount('#app')
diff --git a/src/ui/App.vue b/src/ui/App.vue
new file mode 100644
index 0000000..b150f92
--- /dev/null
+++ b/src/ui/App.vue
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/ui/desktop-container/AppIcon.vue b/src/ui/desktop-container/AppIcon.vue
new file mode 100644
index 0000000..89ce8a2
--- /dev/null
+++ b/src/ui/desktop-container/AppIcon.vue
@@ -0,0 +1,52 @@
+
+
+ {{ iconInfo.name }}
+
+
+
+
+
+
diff --git a/src/ui/desktop-container/DesktopContainer.vue b/src/ui/desktop-container/DesktopContainer.vue
new file mode 100644
index 0000000..de63f7a
--- /dev/null
+++ b/src/ui/desktop-container/DesktopContainer.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
diff --git a/src/ui/desktop-container/useDesktopContainerInit.ts b/src/ui/desktop-container/useDesktopContainerInit.ts
new file mode 100644
index 0000000..defe929
--- /dev/null
+++ b/src/ui/desktop-container/useDesktopContainerInit.ts
@@ -0,0 +1,164 @@
+import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts'
+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
+ // 初始值
+ const gridTemplate = reactive({
+ cellExpectWidth: 90,
+ cellExpectHeight: 110,
+ cellRealWidth: 90,
+ cellRealHeight: 110,
+ gapX: 4,
+ gapY: 4,
+ colCount: 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`
+ }))
+
+ 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));
+
+ 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))
+ })
+
+ onMounted(() => {
+ container = document.querySelector(containerStr)!
+ ro.observe(container)
+ })
+ onUnmounted(() => {
+ ro.unobserve(container)
+ ro.disconnect()
+ })
+
+ // 有桌面图标的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)
+
+ // 左上角坐标原点,从上到下从左到右 索引从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([])
+
+ 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))
+ })
+
+ return {
+ gridTemplate,
+ appIconsRef,
+ gridStyle
+ }
+}
+
+/**
+ * 重新安排图标位置
+ * @param appIconInfos 图标信息
+ * @param maxCol 列数
+ * @param maxRow 行数
+ */
+function rearrangeIcons(
+ appIconInfos: IDesktopAppIcon[],
+ maxCol: number,
+ maxRow: number
+): IRearrangeInfo {
+ const occupied = new Set();
+
+ 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[];
+}
diff --git a/src/ui/imgs/desktop-bg-1.jpeg b/src/ui/imgs/desktop-bg-1.jpeg
new file mode 100644
index 0000000..719e22c
Binary files /dev/null and b/src/ui/imgs/desktop-bg-1.jpeg differ
diff --git a/src/ui/imgs/desktop-bg-2.jpeg b/src/ui/imgs/desktop-bg-2.jpeg
new file mode 100644
index 0000000..4e0e791
Binary files /dev/null and b/src/ui/imgs/desktop-bg-2.jpeg differ
diff --git a/src/ui/types/IDesktopAppIcon.ts b/src/ui/types/IDesktopAppIcon.ts
new file mode 100644
index 0000000..5550409
--- /dev/null
+++ b/src/ui/types/IDesktopAppIcon.ts
@@ -0,0 +1,15 @@
+/**
+ * 桌面应用图标信息
+ */
+export interface IDesktopAppIcon {
+ /** 图标name */
+ name: string;
+ /** 图标 */
+ icon: string;
+ /** 图标路径 */
+ path: string;
+ /** 图标在grid布局中的列 */
+ x: number;
+ /** 图标在grid布局中的行 */
+ y: number;
+}
diff --git a/src/ui/types/IGridTemplateParams.ts b/src/ui/types/IGridTemplateParams.ts
new file mode 100644
index 0000000..ccda108
--- /dev/null
+++ b/src/ui/types/IGridTemplateParams.ts
@@ -0,0 +1,21 @@
+/**
+ * 桌面网格模板参数
+ */
+export interface IGridTemplateParams {
+ /** 单元格预设宽度 */
+ readonly cellExpectWidth: number
+ /** 单元格预设高度 */
+ readonly cellExpectHeight: number
+ /** 单元格实际宽度 */
+ cellRealWidth: number
+ /** 单元格实际高度 */
+ cellRealHeight: number
+ /** 列间距 */
+ gapX: number
+ /** 行间距 */
+ gapY: number
+ /** 总列数 */
+ colCount: number
+ /** 总行数 */
+ rowCount: number
+}
\ No newline at end of file
diff --git a/src/ui/types/WindowFormTypes.ts b/src/ui/types/WindowFormTypes.ts
new file mode 100644
index 0000000..494c7f8
--- /dev/null
+++ b/src/ui/types/WindowFormTypes.ts
@@ -0,0 +1,10 @@
+/**
+ * 窗体位置坐标 - 左上角
+ */
+export interface WindowFormPos {
+ x: number;
+ y: number;
+}
+
+/** 窗口状态 */
+export type TWindowFormState = 'default' | 'minimized' | 'maximized';