From 30a785eabbd060aa228f26e68486123351daca52 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Fri, 18 Mar 2022 17:31:13 +0800 Subject: [PATCH 01/11] Add kill individual api --- source/Localization/EN-US.ts | 3 ++ source/Localization/ZH-CN.ts | 5 ++- source/Model/Group.ts | 41 ++++++++++++++++++++++ source/Panel/GroupDetails/GroupDetails.tsx | 18 ++++++++++ 4 files changed, 66 insertions(+), 1 deletion(-) diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index b18fb44..a7d5b06 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -51,6 +51,7 @@ const EN_US = { "Common.Attr.Title.Basic": "Basic properties", "Common.Attr.Title.Spatial": "Spatial property", "Common.Attr.Title.Individual.Generation": "Individual generation", + "Common.Attr.Title.Individual.kill": "Individual kill", "Common.Attr.Key.Display.Name": "Display name", "Common.Attr.Key.Position.X": "Position X", "Common.Attr.Key.Position.Y": "Position Y", @@ -79,6 +80,8 @@ const EN_US = { "Common.Attr.Key.Generation.Error.Empty.Range.List": "The specified label does not contain any scope objects", "Common.Attr.Key.Generation.Error.Invalid.Range": "The specified scope object is invalid", "Common.Attr.Key.Generation.Error.Invalid.Label": "The specified label has expired", + "Common.Attr.Key.Kill.Random": "Random kill", + "Common.Attr.Key.Kill.Count": "Kill count", "Panel.Info.Range.Details.Attr.Error.Not.Range": "Object is not a Range", "Panel.Info.Range.Details.Attr.Error.Unspecified": "Unspecified range object", "Panel.Info.Group.Details.Attr.Error.Not.Group": "Object is not a Group", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index e880dd5..8f2b46a 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -50,7 +50,8 @@ const ZH_CN = { "Common.No.Unknown.Error": "未知错误", "Common.Attr.Title.Basic": "基础属性", "Common.Attr.Title.Spatial": "空间属性", - "Common.Attr.Title.Individual.Generation": "个体生成", + "Common.Attr.Title.Individual.Generation": "生成个体", + "Common.Attr.Title.Individual.kill": "消除个体", "Common.Attr.Key.Display.Name": "显示名称", "Common.Attr.Key.Position.X": "X 坐标", "Common.Attr.Key.Position.Y": "Y 坐标", @@ -79,6 +80,8 @@ const ZH_CN = { "Common.Attr.Key.Generation.Error.Empty.Range.List": "指定的标签中没有包含任何范围对象", "Common.Attr.Key.Generation.Error.Invalid.Range": "指定的范围对象已失效", "Common.Attr.Key.Generation.Error.Invalid.Label": "指定的标签已失效", + "Common.Attr.Key.Kill.Random": "随机消除", + "Common.Attr.Key.Kill.Count": "消除数量", "Panel.Info.Range.Details.Attr.Error.Not.Range": "对象不是一个范围", "Panel.Info.Range.Details.Attr.Error.Unspecified": "未指定范围对象", "Panel.Info.Group.Details.Attr.Error.Not.Group": "对象不是一个群", diff --git a/source/Model/Group.ts b/source/Model/Group.ts index 831083f..b536ae5 100644 --- a/source/Model/Group.ts +++ b/source/Model/Group.ts @@ -49,6 +49,11 @@ class Group extends CtrlObject { */ public genErrorMessageShowCount: number = 0; + /** + * 删除个数 + */ + public killCount: number = 1; + private genInSingleRange(count: number, range: Range) { for (let i = 0; i < count; i++) { let individual = new Individual(this); @@ -188,6 +193,42 @@ class Group extends CtrlObject { return success; } + /** + * 随机杀死个体 + */ + public killIndividuals(): boolean { + let success = false; + let killCount = this.killCount; + if (killCount > this.individuals.size) { + killCount = this.individuals.size; + } + + // 生成索引数组 + const allIndex = new Array(this.individuals.size).fill(0).map((_, i) => i); + const deleteIndex: Set = new Set(); + + for (let i = 0; i < killCount; i++) { + let randomIndex = Math.floor(Math.random() * allIndex.length); + deleteIndex.add(allIndex[randomIndex]); + allIndex.splice(randomIndex, 1); + } + + let j = 0; + this.individuals.forEach((individual) => { + if (deleteIndex.has(j)) { + this.remove(individual); + success = true; + } + j++; + }); + + if (success) { + this.model.emit("individualChange", this); + } + + return success + } + /** * 创建个体 * @param count 创建数量 diff --git a/source/Panel/GroupDetails/GroupDetails.tsx b/source/Panel/GroupDetails/GroupDetails.tsx index f38596e..b37221a 100644 --- a/source/Panel/GroupDetails/GroupDetails.tsx +++ b/source/Panel/GroupDetails/GroupDetails.tsx @@ -136,6 +136,24 @@ class GroupDetails extends Component { }} /> + + + { + this.props.status?.changeGroupAttrib(group.id, "killCount", (val as any) / 1); + }} + /> + + { + group.killIndividuals() + }} + /> + } From 8d5f2a7dfb167e035dc4999390d961d988ae73ac Mon Sep 17 00:00:00 2001 From: MrKBear Date: Sat, 19 Mar 2022 21:01:33 +0800 Subject: [PATCH 02/11] Add super connect function --- source/Context/Context.tsx | 59 ++++++++++++++++++++++++++++++++++++++ source/Context/Status.tsx | 51 ++------------------------------ source/Model/Label.ts | 2 +- 3 files changed, 63 insertions(+), 49 deletions(-) create mode 100644 source/Context/Context.tsx diff --git a/source/Context/Context.tsx b/source/Context/Context.tsx new file mode 100644 index 0000000..c5cd4c7 --- /dev/null +++ b/source/Context/Context.tsx @@ -0,0 +1,59 @@ +import { Emitter, EventType } from "@Model/Emitter"; +import { Component, FunctionComponent, ReactNode, Consumer } from "react"; + +type RenderComponent = (new (...p: any) => Component) | FunctionComponent; + +function superConnect, E extends Record>( + consumer: Consumer +) { + return (...events: Array) => { + return (components: R): R => { + const C = components as any; + return class extends Component { + + private status: C | undefined; + private isEventMount: boolean = false; + + private handelChange = () => { + this.forceUpdate(); + } + + private mountEvent() { + if (this.status && !this.isEventMount) { + this.isEventMount = true; + console.log("Component dep event mount: " + events.join(", ")); + for (let i = 0; i < events.length; i++) { + this.status.on(events[i], this.handelChange); + } + } + } + + private unmountEvent() { + if (this.status) { + for (let i = 0; i < events.length; i++) { + this.status.off(events[i], this.handelChange); + } + } + } + + public render(): ReactNode { + const Consumer = consumer; + return + {(status: C) => { + this.status = status; + this.mountEvent(); + return ; + }} + + } + + public componentWillUnmount() { + this.unmountEvent(); + } + + } as any; + } + } +} + +export { superConnect }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 3e0ce63..c9b6b2f 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -1,4 +1,4 @@ -import { createContext, Component, FunctionComponent, useState, useEffect, ReactNode } from "react"; +import { createContext, Component, FunctionComponent, ReactNode } from "react"; import { Emitter } from "@Model/Emitter"; import { Model, ObjectID } from "@Model/Model"; import { Label } from "@Model/Label"; @@ -9,6 +9,7 @@ import { AbstractRenderer } from "@Model/Renderer"; import { ClassicRenderer, MouseMod } from "@GLRender/ClassicRenderer"; import { Setting } from "./Setting"; import { I18N } from "@Component/Localization/Localization"; +import { superConnect } from "./Context"; function randomColor(unNormal: boolean = false) { const color = [ @@ -272,53 +273,7 @@ function useStatus(components: R): R { }) as any; } -function useStatusWithEvent(...events: Array) { - return (components: R): R => { - const C = components as any; - return class extends Component { - - private status: Status | undefined; - private isEventMount: boolean = false; - - private handelChange = () => { - this.forceUpdate(); - } - - private mountEvent() { - if (this.status && !this.isEventMount) { - this.isEventMount = true; - console.log("Component dep event mount: " + events.join(", ")); - for (let i = 0; i < events.length; i++) { - this.status.on(events[i], this.handelChange); - } - } - } - - private unmountEvent() { - if (this.status) { - for (let i = 0; i < events.length; i++) { - this.status.off(events[i], this.handelChange); - } - } - } - - public render(): ReactNode { - return - {(status: Status) => { - this.status = status; - this.mountEvent(); - return ; - }} - - } - - public componentWillUnmount() { - this.unmountEvent(); - } - - } as any; - } -} +const useStatusWithEvent = superConnect(StatusConsumer); export { Status, StatusContext, useStatus, useStatusWithEvent, diff --git a/source/Model/Label.ts b/source/Model/Label.ts index 81c1d2b..3cdf609 100644 --- a/source/Model/Label.ts +++ b/source/Model/Label.ts @@ -46,7 +46,7 @@ class Label { * 判断是否为相同标签 */ public equal(label: Label): boolean { - if (this.isDeleted() || label.isDeleted()) return false; + // if (this.isDeleted() || label.isDeleted()) return false; return this === label || this.id === label.id; } From 3f4318e1931e0d5bc355deb5461ee464a75934a4 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Sun, 20 Mar 2022 14:52:05 +0800 Subject: [PATCH 03/11] Detach connect function --- source/Context/Context.tsx | 31 +++++++++++++++++++++++++------ source/Context/Setting.tsx | 26 +++++++++++--------------- source/Context/Status.tsx | 15 +++------------ 3 files changed, 39 insertions(+), 33 deletions(-) diff --git a/source/Context/Context.tsx b/source/Context/Context.tsx index c5cd4c7..72c052f 100644 --- a/source/Context/Context.tsx +++ b/source/Context/Context.tsx @@ -3,16 +3,18 @@ import { Component, FunctionComponent, ReactNode, Consumer } from "react"; type RenderComponent = (new (...p: any) => Component) | FunctionComponent; -function superConnect, E extends Record>( - consumer: Consumer +function superConnectWithEvent, E extends Record>( + consumer: Consumer, keyName: string ) { return (...events: Array) => { return (components: R): R => { - const C = components as any; + const Components = components as any; + const Consumer = consumer; return class extends Component { private status: C | undefined; private isEventMount: boolean = false; + private propsObject: Record = {}; private handelChange = () => { this.forceUpdate(); @@ -37,12 +39,12 @@ function superConnect, E extends Record>( } public render(): ReactNode { - const Consumer = consumer; return {(status: C) => { this.status = status; + this.propsObject[keyName] = status; this.mountEvent(); - return ; + return ; }} } @@ -56,4 +58,21 @@ function superConnect, E extends Record>( } } -export { superConnect }; \ No newline at end of file +function superConnect>(consumer: Consumer, keyName: string) { + return (components: R): R => { + return ((props: any) => { + + const Components = components as any; + const Consumer = consumer; + + return + {(status: C) => } + + }) as any; + } +} + +export { superConnectWithEvent, superConnect }; \ No newline at end of file diff --git a/source/Context/Setting.tsx b/source/Context/Setting.tsx index eacc522..3665667 100644 --- a/source/Context/Setting.tsx +++ b/source/Context/Setting.tsx @@ -1,4 +1,5 @@ -import { createContext, Component, FunctionComponent } from "react"; +import { createContext } from "react"; +import { superConnect, superConnectWithEvent } from "./Context"; import { Emitter } from "@Model/Emitter"; import { Layout } from "@Model/Layout"; @@ -12,9 +13,11 @@ enum Themes { type Language = "ZH_CN" | "EN_US"; -class Setting extends Emitter< - Setting & {change: keyof Setting} -> { +interface ISettingEvents extends Setting { + change: keyof Setting; +} + +class Setting extends Emitter { /** * 主题 @@ -51,21 +54,14 @@ SettingContext.displayName = "Setting"; const SettingProvider = SettingContext.Provider; const SettingConsumer = SettingContext.Consumer; -type RenderComponent = (new (...p: any) => Component) | FunctionComponent; - /** * 修饰器 */ -function useSetting(components: R): R { - return ((props: any) => { - const C = components; - return - {(setting: Setting) => } - - }) as any; -} +const useSetting = superConnect(SettingConsumer, "setting"); + +const useStatusWithEvent = superConnectWithEvent(SettingConsumer, "setting"); export { - Themes, Setting, SettingContext, useSetting, Language, + Themes, Setting, SettingContext, useSetting, Language, useStatusWithEvent, IMixinSettingProps, SettingProvider, SettingConsumer }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index c9b6b2f..62a19c5 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -9,7 +9,7 @@ import { AbstractRenderer } from "@Model/Renderer"; import { ClassicRenderer, MouseMod } from "@GLRender/ClassicRenderer"; import { Setting } from "./Setting"; import { I18N } from "@Component/Localization/Localization"; -import { superConnect } from "./Context"; +import { superConnectWithEvent, superConnect } from "./Context"; function randomColor(unNormal: boolean = false) { const color = [ @@ -259,21 +259,12 @@ StatusContext.displayName = "Status"; const StatusProvider = StatusContext.Provider; const StatusConsumer = StatusContext.Consumer; -type RenderComponent = (new (...p: any) => Component) | FunctionComponent; - /** * 修饰器 */ -function useStatus(components: R): R { - return ((props: any) => { - const C = components; - return - {(status: Status) => } - - }) as any; -} +const useStatus = superConnect(StatusConsumer, "status"); -const useStatusWithEvent = superConnect(StatusConsumer); +const useStatusWithEvent = superConnectWithEvent(StatusConsumer, "status"); export { Status, StatusContext, useStatus, useStatusWithEvent, From 7d8a2161152e6bcfd2eb1a5f5949b5452d1c5949 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Sun, 20 Mar 2022 15:04:58 +0800 Subject: [PATCH 04/11] Move layout into context --- source/Component/Container/Container.tsx | 2 +- source/{Model => Context}/Layout.ts | 2 +- source/Context/Setting.tsx | 2 +- source/Page/SimulatorWeb/SimulatorWeb.tsx | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) rename source/{Model => Context}/Layout.ts (98%) diff --git a/source/Component/Container/Container.tsx b/source/Component/Container/Container.tsx index b60e1d5..a520149 100644 --- a/source/Component/Container/Container.tsx +++ b/source/Component/Container/Container.tsx @@ -2,7 +2,7 @@ import { Localization } from "@Component/Localization/Localization"; import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme"; import { Themes } from "@Context/Setting"; import { DirectionalHint } from "@fluentui/react"; -import { ILayout, LayoutDirection } from "@Model/Layout"; +import { ILayout, LayoutDirection } from "@Context/Layout"; import { Component, ReactNode, MouseEvent } from "react"; import { getPanelById, getPanelInfoById } from "../../Panel/Panel"; import { LocalizationTooltipHost } from "../Localization/LocalizationTooltipHost"; diff --git a/source/Model/Layout.ts b/source/Context/Layout.ts similarity index 98% rename from source/Model/Layout.ts rename to source/Context/Layout.ts index 9e5229a..2cfc84e 100644 --- a/source/Model/Layout.ts +++ b/source/Context/Layout.ts @@ -1,4 +1,4 @@ -import { Emitter } from "./Emitter"; +import { Emitter } from "@Model/Emitter"; enum LayoutDirection { X = 1, diff --git a/source/Context/Setting.tsx b/source/Context/Setting.tsx index 3665667..898df5d 100644 --- a/source/Context/Setting.tsx +++ b/source/Context/Setting.tsx @@ -1,7 +1,7 @@ import { createContext } from "react"; import { superConnect, superConnectWithEvent } from "./Context"; import { Emitter } from "@Model/Emitter"; -import { Layout } from "@Model/Layout"; +import { Layout } from "./Layout"; /** * 主题模式 diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 77c76f7..d4612bf 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -7,9 +7,9 @@ import { StatusProvider, Status } from "@Context/Status"; import { ClassicRenderer } from "@GLRender/ClassicRenderer"; import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; -import { LayoutDirection } from "@Model/Layout"; -import "./SimulatorWeb.scss"; +import { LayoutDirection } from "@Context/Layout"; import { CommandBar } from "@Component/CommandBar/CommandBar"; +import "./SimulatorWeb.scss"; initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/"); From 41a2618b73c55974361e6ca8cf0570efb7dd56a7 Mon Sep 17 00:00:00 2001 From: MrKBear Date: Sun, 20 Mar 2022 15:19:07 +0800 Subject: [PATCH 05/11] Fix message update --- source/Component/Message/Message.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/Component/Message/Message.tsx b/source/Component/Message/Message.tsx index 185266a..f8ab927 100644 --- a/source/Component/Message/Message.tsx +++ b/source/Component/Message/Message.tsx @@ -1,5 +1,5 @@ import { AllI18nKeys, I18N } from "@Component/Localization/Localization"; -import { useSetting, IMixinSettingProps, Themes, Language } from "@Context/Setting"; +import { useStatusWithEvent, IMixinSettingProps, Themes, Language } from "@Context/Setting"; import { FunctionComponent } from "react"; import "./Message.scss"; @@ -38,5 +38,5 @@ const MessageView: FunctionComponent = (prop } -const Message = useSetting(MessageView); +const Message = useStatusWithEvent("language", "themes")(MessageView); export { Message }; \ No newline at end of file From 7570e965cf55c03dbdd55eab98a9c74f1730f67d Mon Sep 17 00:00:00 2001 From: MrKBear Date: Mon, 21 Mar 2022 17:33:10 +0800 Subject: [PATCH 06/11] Add popup model --- source/Context/Popups.ts | 85 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 source/Context/Popups.ts diff --git a/source/Context/Popups.ts b/source/Context/Popups.ts new file mode 100644 index 0000000..30cc1fb --- /dev/null +++ b/source/Context/Popups.ts @@ -0,0 +1,85 @@ +import { ReactNode } from "react"; + +/** + * 弹窗类型 + */ +class Popup { + + /** + * 是否关闭 + */ + public isClose: boolean = false; + + /** + * 唯一标识符 + */ + public id: string; + + /** + * 控制器 + */ + public controller: PopupController; + + /** + * 渲染层级 + */ + public index: number = 0; + + /** + * react 节点 + */ + public reactNode: ReactNode; + + /** + * 渲染函数 + */ + public rendererFunction: undefined | ((p: Popup) => ReactNode); + + /** + * 渲染节点 + */ + public render(): ReactNode { + if (this.rendererFunction) { + this.reactNode = this.rendererFunction(this); + } + return this.reactNode; + }; + + public constructor(controller: PopupController, id: string) { + this.controller = controller; + this.id = id; + } +} + +/** + * 弹窗模型 + */ +class PopupController { + + /** + * ID 序列号 + */ + private idIndex = 0; + + /** + * 弹窗列表 + */ + public popups: Popup[] = []; + + public sortPopup() { + this.popups = this.popups.sort((a, b) => a.index - b.index); + } + + public newPopup(): Popup { + let newPopup = new Popup(this, `P-${this.idIndex ++}`); + this.popups.push(newPopup); + return newPopup; + } + + public closePopup(popup: Popup | string) { + + } + +} + +export { Popup, PopupController } \ No newline at end of file From 04060902e0d7f2d47c484616ed301d175b8e870e Mon Sep 17 00:00:00 2001 From: MrKBear Date: Mon, 21 Mar 2022 23:29:46 +0800 Subject: [PATCH 07/11] Add popup model --- source/Context/Popups.ts | 55 +++++++++++++++++++++++++++++++++++----- 1 file changed, 48 insertions(+), 7 deletions(-) diff --git a/source/Context/Popups.ts b/source/Context/Popups.ts index 30cc1fb..ef88393 100644 --- a/source/Context/Popups.ts +++ b/source/Context/Popups.ts @@ -1,4 +1,7 @@ import { ReactNode } from "react"; +import { Emitter } from "@Model/Emitter"; + +type IPopupConstructor = new (controller: PopupController, id: string) => Popup; /** * 弹窗类型 @@ -23,7 +26,7 @@ class Popup { /** * 渲染层级 */ - public index: number = 0; + public index: number = Infinity; /** * react 节点 @@ -51,10 +54,14 @@ class Popup { } } +interface IPopupControllerEvent { + popupChange: void; +} + /** * 弹窗模型 */ -class PopupController { +class PopupController extends Emitter { /** * ID 序列号 @@ -66,20 +73,54 @@ class PopupController { */ public popups: Popup[] = []; + /** + * 排序并重置序号 + */ public sortPopup() { this.popups = this.popups.sort((a, b) => a.index - b.index); + this.popups = this.popups.map((popup, index) => { + popup.index = (index + 1); + return popup; + }); } - public newPopup(): Popup { - let newPopup = new Popup(this, `P-${this.idIndex ++}`); + /** + * 实例化并开启一个弹窗 + */ + public showPopup

(popup?: P): Popup { + let newPopup = new (popup ?? Popup)(this, `P-${this.idIndex ++}`); this.popups.push(newPopup); + this.emit("popupChange"); return newPopup; } - public closePopup(popup: Popup | string) { - + /** + * 关闭一个弹窗 + */ + public closePopup(popup: Popup | string): Popup | undefined { + let id: string; + if (popup instanceof Popup) { + id = popup.id; + } else { + id = popup; + } + let closePopup: Popup | undefined; + this.popups = this.popups.filter( + currentPopup => { + let isDelete = currentPopup.id === id; + if (isDelete) { + closePopup = currentPopup; + return false; + } else { + return true; + } + } + ); + if (closePopup) { + this.emit("popupChange"); + } + return closePopup; } - } export { Popup, PopupController } \ No newline at end of file From 3357960f6115ded254fe36933094e39fe971067c Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 22 Mar 2022 13:55:20 +0800 Subject: [PATCH 08/11] Add popup model --- source/Component/Message/Message.tsx | 4 ++-- source/Context/Popups.ts | 23 ++++++++++++++++++----- source/Context/Setting.tsx | 8 ++++---- source/Context/Status.tsx | 12 +++++++++++- 4 files changed, 35 insertions(+), 12 deletions(-) diff --git a/source/Component/Message/Message.tsx b/source/Component/Message/Message.tsx index f8ab927..d46f997 100644 --- a/source/Component/Message/Message.tsx +++ b/source/Component/Message/Message.tsx @@ -1,5 +1,5 @@ import { AllI18nKeys, I18N } from "@Component/Localization/Localization"; -import { useStatusWithEvent, IMixinSettingProps, Themes, Language } from "@Context/Setting"; +import { useSettingWithEvent, IMixinSettingProps, Themes, Language } from "@Context/Setting"; import { FunctionComponent } from "react"; import "./Message.scss"; @@ -38,5 +38,5 @@ const MessageView: FunctionComponent = (prop } -const Message = useStatusWithEvent("language", "themes")(MessageView); +const Message = useSettingWithEvent("language", "themes")(MessageView); export { Message }; \ No newline at end of file diff --git a/source/Context/Popups.ts b/source/Context/Popups.ts index ef88393..c3ae5d5 100644 --- a/source/Context/Popups.ts +++ b/source/Context/Popups.ts @@ -36,18 +36,27 @@ class Popup { /** * 渲染函数 */ - public rendererFunction: undefined | ((p: Popup) => ReactNode); + public onRender(p: Popup): ReactNode { + return null; + } + + /** + * 关闭回调 + */ + public onClose(): void {}; /** * 渲染节点 */ public render(): ReactNode { - if (this.rendererFunction) { - this.reactNode = this.rendererFunction(this); - } + this.reactNode = this.onRender(this); return this.reactNode; }; + public close() { + return this.controller.closePopup(this); + } + public constructor(controller: PopupController, id: string) { this.controller = controller; this.id = id; @@ -82,6 +91,7 @@ class PopupController extends Emitter { popup.index = (index + 1); return popup; }); + this.emit("popupChange"); } /** @@ -90,7 +100,7 @@ class PopupController extends Emitter { public showPopup

(popup?: P): Popup { let newPopup = new (popup ?? Popup)(this, `P-${this.idIndex ++}`); this.popups.push(newPopup); - this.emit("popupChange"); + this.sortPopup(); return newPopup; } @@ -110,6 +120,8 @@ class PopupController extends Emitter { let isDelete = currentPopup.id === id; if (isDelete) { closePopup = currentPopup; + currentPopup.isClose = true; + currentPopup.onClose(); return false; } else { return true; @@ -117,6 +129,7 @@ class PopupController extends Emitter { } ); if (closePopup) { + this.sortPopup(); this.emit("popupChange"); } return closePopup; diff --git a/source/Context/Setting.tsx b/source/Context/Setting.tsx index 898df5d..f6ee558 100644 --- a/source/Context/Setting.tsx +++ b/source/Context/Setting.tsx @@ -14,7 +14,7 @@ enum Themes { type Language = "ZH_CN" | "EN_US"; interface ISettingEvents extends Setting { - change: keyof Setting; + attrChange: keyof Setting; } class Setting extends Emitter { @@ -39,7 +39,7 @@ class Setting extends Emitter { */ public setProps

(key: P, value: Setting[P]) { this[key] = value as any; - this.emit("change", key); + this.emit("attrChange", key); this.emit(key as any, value as any); } } @@ -59,9 +59,9 @@ const SettingConsumer = SettingContext.Consumer; */ const useSetting = superConnect(SettingConsumer, "setting"); -const useStatusWithEvent = superConnectWithEvent(SettingConsumer, "setting"); +const useSettingWithEvent = superConnectWithEvent(SettingConsumer, "setting"); export { - Themes, Setting, SettingContext, useSetting, Language, useStatusWithEvent, + Themes, Setting, SettingContext, useSetting, Language, useSettingWithEvent, IMixinSettingProps, SettingProvider, SettingConsumer }; \ No newline at end of file diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 62a19c5..b74e557 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -1,4 +1,4 @@ -import { createContext, Component, FunctionComponent, ReactNode } from "react"; +import { createContext } from "react"; import { Emitter } from "@Model/Emitter"; import { Model, ObjectID } from "@Model/Model"; import { Label } from "@Model/Label"; @@ -10,6 +10,7 @@ import { ClassicRenderer, MouseMod } from "@GLRender/ClassicRenderer"; import { Setting } from "./Setting"; import { I18N } from "@Component/Localization/Localization"; import { superConnectWithEvent, superConnect } from "./Context"; +import { PopupController } from "./Popups"; function randomColor(unNormal: boolean = false) { const color = [ @@ -39,6 +40,7 @@ interface IStatusEvent { labelAttrChange: void; groupAttrChange: void; individualChange: void; + popupChange: void; } class Status extends Emitter { @@ -66,6 +68,11 @@ class Status extends Emitter { */ public model: Model = new Model(); + /** + * 弹窗 + */ + public popup: PopupController = new PopupController(); + /** * 焦点对象 */ @@ -96,6 +103,9 @@ class Status extends Emitter { this.model.on("objectChange", () => this.emit("objectChange")); this.model.on("labelChange", () => this.emit("labelChange")); + // 弹窗事件 + this.popup.on("popupChange", () => this.emit("popupChange")); + // 对象变换时执行渲染,更新渲染器数据 this.on("objectChange", this.delayDraw); this.model.on("individualChange", this.delayDraw); From 93e1a8d3ef733ba9763a7e9e7a68e2492ed8ed4e Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 22 Mar 2022 14:02:53 +0800 Subject: [PATCH 09/11] Update ts config for map url --- source/Component/Localization/Localization.tsx | 4 ++-- tsconfig.json | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/source/Component/Localization/Localization.tsx b/source/Component/Localization/Localization.tsx index 77d2410..1a16ee7 100644 --- a/source/Component/Localization/Localization.tsx +++ b/source/Component/Localization/Localization.tsx @@ -2,8 +2,8 @@ import { Component, ReactNode, DetailedHTMLProps, HTMLAttributes } from "react"; import { useSetting, IMixinSettingProps, Language } from "@Context/Setting"; import "./Localization.scss"; -import EN_US from "../../Localization/EN-US"; -import ZH_CN from "../../Localization/ZH-CN"; +import EN_US from "@Localization/EN-US"; +import ZH_CN from "@Localization/ZH-CN"; const LanguageDataBase = { EN_US, ZH_CN diff --git a/tsconfig.json b/tsconfig.json index 8df4d3a..e8bd41b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -31,6 +31,12 @@ ], "@Component/*": [ "./source/Component/*" + ], + "@Localization/*": [ + "./source/Localization/*" + ], + "@Panel/*": [ + "./source/Panel/*" ] } }, From 90398b593e6f9f54794023e198aab60ddd9ad87f Mon Sep 17 00:00:00 2001 From: MrKBear Date: Tue, 22 Mar 2022 17:31:49 +0800 Subject: [PATCH 10/11] Add popup renderer --- source/Component/CommandBar/CommandBar.tsx | 8 +- source/Component/Popup/Popup.scss | 71 ++++++++++++++++ source/Component/Popup/Popup.tsx | 97 ++++++++++++++++++++++ source/Context/Popups.ts | 32 ++++++- source/Page/SimulatorWeb/SimulatorWeb.tsx | 2 + 5 files changed, 207 insertions(+), 3 deletions(-) create mode 100644 source/Component/Popup/Popup.scss create mode 100644 source/Component/Popup/Popup.tsx diff --git a/source/Component/CommandBar/CommandBar.tsx b/source/Component/CommandBar/CommandBar.tsx index 1b25b6a..bd21180 100644 --- a/source/Component/CommandBar/CommandBar.tsx +++ b/source/Component/CommandBar/CommandBar.tsx @@ -68,7 +68,13 @@ class CommandBar extends Component

- {this.getRenderButton({ iconName: "Settings", i18NKey: "Command.Bar.Setting.Info" })} + {this.getRenderButton({ + iconName: "Settings", + i18NKey: "Command.Bar.Setting.Info", + click: () => { + this.props.status ? this.props.status.popup.showPopup() : undefined; + } + })}
} diff --git a/source/Component/Popup/Popup.scss b/source/Component/Popup/Popup.scss new file mode 100644 index 0000000..6c217fa --- /dev/null +++ b/source/Component/Popup/Popup.scss @@ -0,0 +1,71 @@ +@import "../Theme/Theme.scss"; + +@keyframes show-scale{ + from { + transform: scale3d(1.15, 1.15, 1); + } + to { + transform: scale3d(1, 1, 1); + } +} + +@keyframes show-fade{ + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +div.popup-mask.show-fade { + animation: show-fade .1s cubic-bezier(0, 0, 1, 1) both; + opacity: 1; +} + +div.popup-layer.show-scale { + animation: show-scale .3s cubic-bezier(.1, .9, .2, 1) both, + show-fade .1s cubic-bezier(0, 0, 1, 1) both; + transform: scale3d(1, 1, 1); + opacity: 1; +} + +div.popup-mask { + position: absolute; + cursor: pointer; + width: 100%; + height: 100%; +} + +div.popup-layer { + position: absolute; + border-radius: 3px; + overflow: hidden; + + div.popup-layer-header { + min-height: 32px; + max-height: 32px; + } +} + +div.popup-layer.dark { + + div.popup-layer-header { + background-color: $lt-bg-color-lvl3-dark; + } +} + +div.popup-layer.light { + + div.popup-layer-header { + background-color: $lt-bg-color-lvl3-light; + } +} + +div.dark.popup-mask { + background-color: rgba(0, 0, 0, 0.55); +} + +div.light.popup-mask { + background-color: rgba(0, 0, 0, 0.15); +} \ No newline at end of file diff --git a/source/Component/Popup/Popup.tsx b/source/Component/Popup/Popup.tsx new file mode 100644 index 0000000..4737249 --- /dev/null +++ b/source/Component/Popup/Popup.tsx @@ -0,0 +1,97 @@ +import { Component, ReactNode } from "react"; +import { IMixinStatusProps, useStatusWithEvent } from "@Context/Status"; +import { BackgroundLevel, Theme } from "@Component/Theme/Theme"; +import { Popup as PopupModel } from "@Context/Popups"; +import "./Popup.scss"; + +interface IPopupProps {} + +@useStatusWithEvent("popupChange") +class Popup extends Component { + + public renderMask(index?: number, click?: () => void, key?: string): ReactNode { + const classList: string[] = ["popup-mask", "show-fade"]; + return + } + + public renderRootMask(): ReactNode { + if (this.props.status) { + const needMask = this.props.status.popup.popups.some(popup => popup.needMask); + if (!needMask) return null; + return this.renderMask(this.props.status.popup.zIndex, + () => { + this.props.status?.popup.popups.forEach( + popup => popup.onClose() + ) + } + ); + } else { + return null; + } + } + + public renderMaskList(): ReactNode { + if (this.props.status) { + return this.props.status.popup.popups + .filter((popup) => { + return popup.needMask && popup.maskForSelf; + }) + .filter((_, index) => { + if (index === 0) return false; + return true; + }) + .map((popup) => { + return this.renderMask(popup.zIndex() - 1, + () => { + popup.onClose(); + }, popup.id + ); + }) + } else { + return null; + } + } + + public renderLayer(popup: PopupModel) { + const pageWidth = document.documentElement.clientWidth; + const pageHeight = document.documentElement.clientHeight; + const top = (pageHeight - popup.height) / 2; + const left = (pageWidth - popup.width) / 2; + + return +
+ +
+
+ } + + public render(): ReactNode { + return <> + {this.renderRootMask()} + {this.renderMaskList()} + {this.props.status?.popup.popups.map((popup) => { + return this.renderLayer(popup); + })} + ; + } +} + +export { Popup }; \ No newline at end of file diff --git a/source/Context/Popups.ts b/source/Context/Popups.ts index c3ae5d5..1d66303 100644 --- a/source/Context/Popups.ts +++ b/source/Context/Popups.ts @@ -8,11 +8,33 @@ type IPopupConstructor = new (controller: PopupController, id: string) => Popup; */ class Popup { + public zIndex() { + return this.index * 2 + this.controller.zIndex; + } + + public width: number = 300; + + public height: number = 200; + + public top: number = 0; + + public left: number = 0; + /** * 是否关闭 */ public isClose: boolean = false; + /** + * 需要蒙版 + */ + public needMask: boolean = true; + + /** + * 单独遮挡下层的蒙版 + */ + public maskForSelf: boolean = false; + /** * 唯一标识符 */ @@ -43,7 +65,9 @@ class Popup { /** * 关闭回调 */ - public onClose(): void {}; + public onClose(): void { + this.close(); + }; /** * 渲染节点 @@ -77,6 +101,11 @@ class PopupController extends Emitter { */ private idIndex = 0; + /** + * 最小弹窗 Index + */ + public zIndex = 100; + /** * 弹窗列表 */ @@ -121,7 +150,6 @@ class PopupController extends Emitter { if (isDelete) { closePopup = currentPopup; currentPopup.isClose = true; - currentPopup.onClose(); return false; } else { return true; diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index d4612bf..07587a5 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -9,6 +9,7 @@ import { initializeIcons } from '@fluentui/font-icons-mdl2'; import { RootContainer } from "@Component/Container/RootContainer"; import { LayoutDirection } from "@Context/Layout"; import { CommandBar } from "@Component/CommandBar/CommandBar"; +import { Popup } from "@Component/Popup/Popup"; import "./SimulatorWeb.scss"; initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/"); @@ -101,6 +102,7 @@ class SimulatorWeb extends Component { backgroundLevel={BackgroundLevel.Level5} fontLevel={FontLevel.Level3} > +
Date: Tue, 22 Mar 2022 22:16:11 +0800 Subject: [PATCH 11/11] Add popup header --- source/Component/Popup/Popup.scss | 47 +++++++++++++++++++++++++++++-- source/Component/Popup/Popup.tsx | 21 ++++++++++++-- source/Context/Popups.ts | 10 ++++++- source/Localization/EN-US.ts | 1 + source/Localization/ZH-CN.ts | 1 + 5 files changed, 74 insertions(+), 6 deletions(-) diff --git a/source/Component/Popup/Popup.scss b/source/Component/Popup/Popup.scss index 6c217fa..a39b8d6 100644 --- a/source/Component/Popup/Popup.scss +++ b/source/Component/Popup/Popup.scss @@ -1,5 +1,7 @@ @import "../Theme/Theme.scss"; +$header-height: 32px; + @keyframes show-scale{ from { transform: scale3d(1.15, 1.15, 1); @@ -43,8 +45,39 @@ div.popup-layer { overflow: hidden; div.popup-layer-header { - min-height: 32px; - max-height: 32px; + min-height: $header-height; + max-height: $header-height; + height: $header-height; + display: flex; + width: 100%; + + div.header-text { + width: calc( 100% - 32px ); + flex-shrink: 1; + display: flex; + align-items: center; + user-select: none; + + span { + padding-left: 8px; + display: inline-block; + vertical-align: middle; + white-space: nowrap; + text-overflow: ellipsis; + overflow: hidden; + } + } + + div.header-close-icon { + width: $header-height; + height: $header-height; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: center; + user-select: none; + cursor: pointer; + } } } @@ -52,6 +85,11 @@ div.popup-layer.dark { div.popup-layer-header { background-color: $lt-bg-color-lvl3-dark; + + div.header-close-icon:hover { + background-color: $lt-bg-color-lvl2-dark; + color: $lt-red; + } } } @@ -59,6 +97,11 @@ div.popup-layer.light { div.popup-layer-header { background-color: $lt-bg-color-lvl3-light; + + div.header-close-icon:hover { + background-color: $lt-bg-color-lvl2-light; + color: $lt-red; + } } } diff --git a/source/Component/Popup/Popup.tsx b/source/Component/Popup/Popup.tsx index 4737249..2bfaefe 100644 --- a/source/Component/Popup/Popup.tsx +++ b/source/Component/Popup/Popup.tsx @@ -2,6 +2,7 @@ import { Component, ReactNode } from "react"; import { IMixinStatusProps, useStatusWithEvent } from "@Context/Status"; import { BackgroundLevel, Theme } from "@Component/Theme/Theme"; import { Popup as PopupModel } from "@Context/Popups"; +import { Icon } from "@fluentui/react"; import "./Popup.scss"; interface IPopupProps {} @@ -59,6 +60,22 @@ class Popup extends Component { } } + public renderHeader(popup: PopupModel): ReactNode { + return
+
+ {popup.onRenderHeader()} +
+
{ + popup.onClose(); + }} + > + +
+
+ } + public renderLayer(popup: PopupModel) { const pageWidth = document.documentElement.clientWidth; const pageHeight = document.documentElement.clientHeight; @@ -77,9 +94,7 @@ class Popup extends Component { backgroundLevel={BackgroundLevel.Level4} className="popup-layer show-scale" > -
- -
+ {this.renderHeader(popup)} } diff --git a/source/Context/Popups.ts b/source/Context/Popups.ts index 1d66303..0b75ccd 100644 --- a/source/Context/Popups.ts +++ b/source/Context/Popups.ts @@ -1,5 +1,6 @@ -import { ReactNode } from "react"; +import { ReactNode, createElement } from "react"; import { Emitter } from "@Model/Emitter"; +import { Localization } from "@Component/Localization/Localization"; type IPopupConstructor = new (controller: PopupController, id: string) => Popup; @@ -55,6 +56,13 @@ class Popup { */ public reactNode: ReactNode; + /** + * 渲染标题 + */ + public onRenderHeader(): ReactNode { + return createElement(Localization, {i18nKey: "Popup.Title.Unnamed"}); + } + /** * 渲染函数 */ diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index a7d5b06..8e4ceb1 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -44,6 +44,7 @@ const EN_US = { "Panel.Info.Label.Details.View": "Edit view label attributes", "Panel.Title.Group.Details.View": "Group", "Panel.Info.Group.Details.View": "Edit view group attributes", + "Popup.Title.Unnamed": "Popup message", "Build.In.Label.Name.All.Group": "All group", "Build.In.Label.Name.All.Range": "All range", "Common.No.Data": "No Data", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 8f2b46a..784e522 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -44,6 +44,7 @@ const ZH_CN = { "Panel.Info.Label.Details.View": "编辑查看标签属性", "Panel.Title.Group.Details.View": "群", "Panel.Info.Group.Details.View": "编辑查看群属性", + "Popup.Title.Unnamed": "弹窗消息", "Build.In.Label.Name.All.Group": "全部群", "Build.In.Label.Name.All.Range": "全部范围", "Common.No.Data": "暂无数据",