diff --git a/src/App.vue b/src/App.vue deleted file mode 100644 index ae64768..0000000 --- a/src/App.vue +++ /dev/null @@ -1,18 +0,0 @@ - - - - - diff --git a/src/common/hooks/useClickFocus.ts b/src/common/hooks/useClickFocus.ts new file mode 100644 index 0000000..1c7d0f0 --- /dev/null +++ b/src/common/hooks/useClickFocus.ts @@ -0,0 +1,65 @@ +import { onMounted, onBeforeUnmount, type Ref, watch } from "vue"; + +interface ClickFocusOptions { + once?: boolean; // 点击一次后解绑 +} + +/** + * Vue3 Hook:点击自身聚焦 / 点击外部失焦 + * @param targetRef 元素的 ref + * @param onFocus 点击自身回调 + * @param onBlur 点击外部回调 + * @param options 配置项 { once } + */ +export function useClickFocus( + targetRef: Ref, + onFocus?: () => void, + onBlur?: () => void, + options: ClickFocusOptions = {} +) { + let cleanupFn: (() => void) | null = null; + + const bindEvents = (el: HTMLElement) => { + if (!el.hasAttribute("tabindex")) el.setAttribute("tabindex", "0"); + + const handleClick = (e: MouseEvent) => { + if (!el.isConnected) return cleanup(); + + if (el.contains(e.target as Node)) { + el.focus(); + onFocus?.(); + } else { + el.blur(); + onBlur?.(); + } + + if (options.once) cleanup(); + }; + + document.addEventListener("click", handleClick); + + cleanupFn = () => { + document.removeEventListener("click", handleClick); + cleanupFn = null; + }; + }; + + const cleanup = () => { + cleanupFn?.(); + }; + + onMounted(() => { + const el = targetRef.value; + if (el) bindEvents(el); + }); + + onBeforeUnmount(() => { + cleanup(); + }); + + // 支持动态 ref 变化 + watch(targetRef, (newEl, oldEl) => { + if (oldEl) cleanup(); + if (newEl) bindEvents(newEl); + }); +} diff --git a/src/common/hooks/useObservableVue.ts b/src/common/hooks/useObservableVue.ts new file mode 100644 index 0000000..9ca6458 --- /dev/null +++ b/src/common/hooks/useObservableVue.ts @@ -0,0 +1,87 @@ +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({ + * 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(observable: IObservable): Reactive { + // 创建 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 +} diff --git a/src/common/naive-ui/components.ts b/src/common/naive-ui/components.ts new file mode 100644 index 0000000..3623052 --- /dev/null +++ b/src/common/naive-ui/components.ts @@ -0,0 +1,10 @@ +import { + create, + NButton, + NCard, + NConfigProvider, +} from 'naive-ui' + +export const naiveUi = create({ + components: [NButton, NCard, NConfigProvider] +}) diff --git a/src/common/naive-ui/discrete-api.ts b/src/common/naive-ui/discrete-api.ts new file mode 100644 index 0000000..6365dd7 --- /dev/null +++ b/src/common/naive-ui/discrete-api.ts @@ -0,0 +1,21 @@ +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 +} diff --git a/src/common/naive-ui/theme.ts b/src/common/naive-ui/theme.ts new file mode 100644 index 0000000..ab270f6 --- /dev/null +++ b/src/common/naive-ui/theme.ts @@ -0,0 +1,15 @@ +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, +} diff --git a/src/common/types/IDestroyable.ts b/src/common/types/IDestroyable.ts new file mode 100644 index 0000000..f069e4d --- /dev/null +++ b/src/common/types/IDestroyable.ts @@ -0,0 +1,8 @@ +/** + * 可销毁接口 + * 销毁实例,清理副作用,让内存可以被回收 + */ +export interface IDestroyable { + /** 销毁实例,清理副作用,让内存可以被回收 */ + destroy(): void +} diff --git a/src/common/types/IVersion.ts b/src/common/types/IVersion.ts new file mode 100644 index 0000000..dfc88d3 --- /dev/null +++ b/src/common/types/IVersion.ts @@ -0,0 +1,29 @@ +/** + * 版本信息 + */ +export interface IVersion { + /** + * 公司名称 + */ + company: string + + /** + * 版本号 + */ + major: number + + /** + * 子版本号 + */ + minor: number + + /** + * 修订号 + */ + build: number + + /** + * 私有版本号 + */ + private: number +} diff --git a/src/events/DesktopEventManager.ts b/src/events/DesktopEventManager.ts new file mode 100644 index 0000000..0153e7f --- /dev/null +++ b/src/events/DesktopEventManager.ts @@ -0,0 +1,16 @@ +import { EventBuilderImpl } from '@/events/impl/EventBuilderImpl.ts' +import type { IEventMap } from '@/events/IEventBuilder.ts' +import type { IDesktopAppIcon } from '@/ui/types/IDesktopAppIcon.ts' + +/** + * 桌面相关的事件 + */ +export interface IDesktopEvent extends IEventMap { + /** + * 桌面应用位置改变 + */ + desktopAppPosChange: (info: IDesktopAppIcon) => void; +} + +/** 窗口事件管理器 */ +export const desktopEM = new EventBuilderImpl() diff --git a/src/events/EventManager.ts b/src/events/EventManager.ts new file mode 100644 index 0000000..6b63a22 --- /dev/null +++ b/src/events/EventManager.ts @@ -0,0 +1,35 @@ +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() + +/** + * 系统进程的事件 + * @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 @@ + + + + + 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';