WindowFormElement

This commit is contained in:
2025-09-16 14:32:57 +08:00
parent 08e08043d7
commit 122fba228a
8 changed files with 737 additions and 386 deletions

View File

@@ -0,0 +1,57 @@
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;
}
interface IWindowFormDataUpdateParams {
/** 窗口id */
id: string;
/** 窗口状态 */
state: TWindowFormState,
/** 窗口宽度 */
width: number,
/** 窗口高度 */
height: number,
/** 窗口x坐标(左上角) */
x: number,
/** 窗口y坐标(左上角) */
y: number
}
/** 窗口事件管理器 */
export const wfem = new EventBuilderImpl<WindowFormEvent>()

View File

@@ -1,7 +1,7 @@
import type { IProcessInfo } from '@/core/process/IProcessInfo.ts' import type { IProcessInfo } from '@/core/process/IProcessInfo.ts'
import type { IWindowForm } from '@/core/window/IWindowForm.ts' import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts' import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
/** /**
* 进程接口 * 进程接口

View File

@@ -6,7 +6,7 @@ import type { IWindowForm } from '@/core/window/IWindowForm.ts'
import { processManager } from '@/core/process/ProcessManager.ts' import { processManager } from '@/core/process/ProcessManager.ts'
import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts' import { EventBuilderImpl } from '@/core/events/impl/EventBuilderImpl.ts'
import type { IEventBuilder } from '@/core/events/IEventBuilder.ts' import type { IEventBuilder } from '@/core/events/IEventBuilder.ts'
import type { IProcessEvent } from '@/core/process/types/ProcessEvent.ts' import type { IProcessEvent } from '@/core/process/types/ProcessEventTypes.ts'
/** /**
* 进程 * 进程

View File

@@ -180,7 +180,7 @@ export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObs
} }
/** 订阅整个状态变化 */ /** 订阅整个状态变化 */
subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void { public subscribe(fn: TObservableListener<T>, options: { immediate?: boolean } = {}): () => void {
this.listeners.add(fn) this.listeners.add(fn)
if (options.immediate) fn(this.state) if (options.immediate) fn(this.state)
return () => { return () => {
@@ -189,7 +189,7 @@ export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObs
} }
/** 订阅指定字段变化 */ /** 订阅指定字段变化 */
subscribeKey<K extends keyof T>( public subscribeKey<K extends keyof T>(
keys: K | K[], keys: K | K[],
fn: TObservableKeyListener<T, K>, fn: TObservableKeyListener<T, K>,
options: { immediate?: boolean } = {} options: { immediate?: boolean } = {}
@@ -214,7 +214,7 @@ export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObs
} }
/** 批量更新状态 */ /** 批量更新状态 */
patch(values: Partial<T>): void { public patch(values: Partial<T>): void {
for (const key in values) { for (const key in values) {
if (Object.prototype.hasOwnProperty.call(values, key)) { if (Object.prototype.hasOwnProperty.call(values, key)) {
const typedKey = key as keyof T const typedKey = key as keyof T
@@ -226,7 +226,7 @@ export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObs
} }
/** 销毁 Observable 实例 */ /** 销毁 Observable 实例 */
dispose(): void { public dispose(): void {
this.disposed = true this.disposed = true
this.listeners.clear() this.listeners.clear()
this.keyListeners.clear() this.keyListeners.clear()
@@ -234,7 +234,7 @@ export class ObservableImpl<T extends TNonFunctionProperties<T>> implements IObs
} }
/** 语法糖:返回一个可解构赋值的 Proxy */ /** 语法糖:返回一个可解构赋值的 Proxy */
toRefsProxy(): { [K in keyof T]: T[K] } { public toRefsProxy(): { [K in keyof T]: T[K] } {
const self = this const self = this
return new Proxy({} as T, { return new Proxy({} as T, {
get(_, prop: string | symbol) { get(_, prop: string | symbol) {

View File

@@ -9,91 +9,125 @@ import { DraggableResizableWindow } from '@/core/utils/DraggableResizableWindow.
import '../css/window-form.scss' import '../css/window-form.scss'
import { serviceManager } from '@/core/service/kernel/ServiceManager.ts' import { serviceManager } from '@/core/service/kernel/ServiceManager.ts'
import '../ui/WindowFormElement.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 default class WindowFormImpl implements IWindowForm { export default class WindowFormImpl implements IWindowForm {
private readonly _id: string = uuidV4(); private readonly _id: string = uuidV4()
private readonly _procId: string; private readonly _procId: string
private dom: HTMLElement; private dom: HTMLElement
private drw: DraggableResizableWindow; private drw: DraggableResizableWindow
private pos: WindowFormPos = { x: 0, y: 0 }; private pos: WindowFormPos = { x: 0, y: 0 }
private width: number; private width: number
private height: number; private height: number
private _state: IObservable<{ x: number, y: number }> = new ObservableImpl({
x: 0,
y: 0,
})
public get id() { public get id() {
return this._id; return this._id
} }
public get proc() { public get proc() {
return processManager.findProcessById(this._procId) return processManager.findProcessById(this._procId)
} }
private get desktopRootDom() { private get desktopRootDom() {
return XSystem.instance.desktopRootDom; return XSystem.instance.desktopRootDom
} }
private get sm() { private get sm() {
return serviceManager.getService('WindowForm') return serviceManager.getService('WindowForm')
} }
public get windowFormEle() { public get windowFormEle() {
return this.dom; return this.dom
} }
public get windowFormState() { public get windowFormState() {
return this.drw.windowFormState return this.drw.windowFormState
} }
constructor(proc: IProcess, config: IWindowFormConfig) { constructor(proc: IProcess, config: IWindowFormConfig) {
this._procId = proc.id; this._procId = proc.id
console.log('WindowForm') console.log('WindowForm')
this.pos = { this.pos = {
x: config.left ?? 0, x: config.left ?? 0,
y: config.top ?? 0 y: config.top ?? 0,
} }
this.width = config.width ?? 200; this.width = config.width ?? 200
this.height = config.height ?? 100; this.height = config.height ?? 100
this.createWindowFrom(); this.createWindowFrom()
this.initEvent()
}
private initEvent() {
wfem.addEventListener('windowFormClose', this.windowFormCloseFun)
}
private windowFormCloseFun = (id: string) => {
if (id === this.id) {
this.closeWindowForm()
}
} }
private createWindowFrom() { private createWindowFrom() {
this.dom = this.createWindowFormEle(); // this.dom = this.createWindowFormEle();
this.dom.style.position = 'absolute'; // this.dom.style.position = 'absolute';
this.dom.style.width = `${this.width}px`; // this.dom.style.width = `${this.width}px`;
this.dom.style.height = `${this.height}px`; // this.dom.style.height = `${this.height}px`;
this.dom.style.zIndex = '10'; // this.dom.style.zIndex = '10';
//
const header = this.dom.querySelector('.title-bar') as HTMLElement; // const header = this.dom.querySelector('.title-bar') as HTMLElement;
const content = this.dom.querySelector('.window-content') as HTMLElement; // const content = this.dom.querySelector('.window-content') as HTMLElement;
this.drw = new DraggableResizableWindow({ // this.drw = new DraggableResizableWindow({
target: this.dom, // target: this.dom,
handle: header, // handle: header,
snapAnimation: true, // snapAnimation: true,
snapThreshold: 20, // snapThreshold: 20,
boundaryElement: document.body, // boundaryElement: document.body,
taskbarElementId: '#taskbar', // taskbarElementId: '#taskbar',
onWindowStateChange: (state) => { // onWindowStateChange: (state) => {
if (state === 'maximized') { // if (state === 'maximized') {
this.dom.style.borderRadius = '0px'; // this.dom.style.borderRadius = '0px';
} else { // } else {
this.dom.style.borderRadius = '5px'; // this.dom.style.borderRadius = '5px';
} // }
}, // },
}) // })
// this.desktopRootDom.appendChild(this.dom); // this.desktopRootDom.appendChild(this.dom);
const wf = document.createElement('window-form-element') const wf = document.createElement('window-form-element')
wf.pos = this._state
wf.wid = this.id
wf.dragContainer = document.body wf.dragContainer = document.body
wf.snapDistance = 20 wf.snapDistance = 20
wf.addEventListener('windowForm:dragStart', (e) => { wf.taskbarElementId = '#taskbar'
console.log('dragstart', e) wf.addManagedEventListener('windowForm:stateChange:minimize', (e) => {
console.log('windowForm:stateChange:minimize', e)
}) })
this.desktopRootDom.appendChild(wf) wf.addManagedEventListener('windowForm:stateChange:maximize', (e) => {
console.log('windowForm:stateChange:maximize', e)
})
wf.addManagedEventListener('windowForm:stateChange:restore', (e) => {
console.log('windowForm:stateChange:restore', e)
})
wf.addManagedEventListener('windowForm:close', () => {
this.closeWindowForm()
})
this.dom = wf
this.desktopRootDom.appendChild(this.dom)
wfem.notifyEvent('windowFormFocus', this.id)
} }
public closeWindowForm() { public closeWindowForm() {
this.drw.destroy(); // this.drw.destroy();
this.desktopRootDom.removeChild(this.dom); this.desktopRootDom.removeChild(this.dom)
this.proc?.event.notifyEvent('onProcessWindowFormExit', this.id) this.proc?.event.notifyEvent('onProcessWindowFormExit', this.id)
wfem.removeEventListener('windowFormClose', this.windowFormCloseFun)
// wfem.notifyEvent('windowFormClose', this.id)
} }
private createWindowFormEle() { private createWindowFormEle() {
const template = document.createElement('template'); const template = document.createElement('template')
template.innerHTML = ` template.innerHTML = `
<div class="window"> <div class="window">
<div class="title-bar"> <div class="title-bar">
@@ -107,23 +141,24 @@ export default class WindowFormImpl implements IWindowForm {
<div class="window-content"></div> <div class="window-content"></div>
</div> </div>
` `
const fragment = template.content.cloneNode(true) as DocumentFragment; const fragment = template.content.cloneNode(true) as DocumentFragment
const windowElement = fragment.firstElementChild as HTMLElement const windowElement = fragment.firstElementChild as HTMLElement
windowElement.querySelector('.btn.minimize') windowElement
?.addEventListener('click', () => this.drw.minimize()); .querySelector('.btn.minimize')
?.addEventListener('click', () => this.drw.minimize())
windowElement.querySelector('.btn.maximize') windowElement.querySelector('.btn.maximize')?.addEventListener('click', () => {
?.addEventListener('click', () => { if (this.drw.windowFormState === 'maximized') {
if (this.drw.windowFormState === 'maximized') { this.drw.restore()
this.drw.restore() } else {
} else { this.drw.maximize()
this.drw.maximize() }
} })
});
windowElement.querySelector('.btn.close') windowElement
?.addEventListener('click', () => this.closeWindowForm()); .querySelector('.btn.close')
?.addEventListener('click', () => this.closeWindowForm())
return windowElement return windowElement
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,21 @@
--shadow: 0 10px 30px rgba(0,0,0,0.25); --shadow: 0 10px 30px rgba(0,0,0,0.25);
font-family: system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial; 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 { .window {
position: absolute; position: absolute;
box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.25)); box-shadow: var(--shadow, 0 10px 30px rgba(0,0,0,0.25));
@@ -20,7 +35,12 @@
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
&.focus {
border-color: #3a86ff;
}
} }
.titlebar { .titlebar {
height: var(--titlebar-height, 32px); height: var(--titlebar-height, 32px);
display: flex; display: flex;
@@ -30,6 +50,7 @@
background: linear-gradient(#f2f2f2, #e9e9e9); background: linear-gradient(#f2f2f2, #e9e9e9);
border-bottom: 1px solid rgba(0,0,0,0.06); border-bottom: 1px solid rgba(0,0,0,0.06);
} }
.title { .title {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
@@ -39,7 +60,9 @@
text-overflow: ellipsis; text-overflow: ellipsis;
color: #111; color: #111;
} }
.controls { display: flex; gap: 6px; } .controls { display: flex; gap: 6px; }
button.ctrl { button.ctrl {
width: 34px; width: 34px;
height: 24px; height: 24px;
@@ -50,7 +73,9 @@ button.ctrl {
font-weight: 600; font-weight: 600;
font-size: 12px; font-size: 12px;
} }
button.ctrl:hover { background: rgba(0,0,0,0.06); } button.ctrl:hover { background: rgba(0,0,0,0.06); }
.content { flex: 1; overflow: auto; padding: 12px; background: transparent; } .content { flex: 1; overflow: auto; padding: 12px; background: transparent; }
.resizer { position: absolute; z-index: 20; } .resizer { position: absolute; z-index: 20; }