Add popup & detail optimization #24
@ -68,7 +68,13 @@ class CommandBar extends Component<ICommandBarProps & IMixinSettingProps & IMixi
|
|||||||
{this.getRenderButton({ iconName: "Camera", i18NKey: "Command.Bar.Camera.Info" })}
|
{this.getRenderButton({ iconName: "Camera", i18NKey: "Command.Bar.Camera.Info" })}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{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;
|
||||||
|
}
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</Theme>
|
</Theme>
|
||||||
}
|
}
|
||||||
|
@ -2,7 +2,7 @@ import { Localization } from "@Component/Localization/Localization";
|
|||||||
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
|
import { Theme, BackgroundLevel, FontLevel } from "@Component/Theme/Theme";
|
||||||
import { Themes } from "@Context/Setting";
|
import { Themes } from "@Context/Setting";
|
||||||
import { DirectionalHint } from "@fluentui/react";
|
import { DirectionalHint } from "@fluentui/react";
|
||||||
import { ILayout, LayoutDirection } from "@Model/Layout";
|
import { ILayout, LayoutDirection } from "@Context/Layout";
|
||||||
import { Component, ReactNode, MouseEvent } from "react";
|
import { Component, ReactNode, MouseEvent } from "react";
|
||||||
import { getPanelById, getPanelInfoById } from "../../Panel/Panel";
|
import { getPanelById, getPanelInfoById } from "../../Panel/Panel";
|
||||||
import { LocalizationTooltipHost } from "../Localization/LocalizationTooltipHost";
|
import { LocalizationTooltipHost } from "../Localization/LocalizationTooltipHost";
|
||||||
|
@ -2,8 +2,8 @@ import { Component, ReactNode, DetailedHTMLProps, HTMLAttributes } from "react";
|
|||||||
import { useSetting, IMixinSettingProps, Language } from "@Context/Setting";
|
import { useSetting, IMixinSettingProps, Language } from "@Context/Setting";
|
||||||
import "./Localization.scss";
|
import "./Localization.scss";
|
||||||
|
|
||||||
import EN_US from "../../Localization/EN-US";
|
import EN_US from "@Localization/EN-US";
|
||||||
import ZH_CN from "../../Localization/ZH-CN";
|
import ZH_CN from "@Localization/ZH-CN";
|
||||||
|
|
||||||
const LanguageDataBase = {
|
const LanguageDataBase = {
|
||||||
EN_US, ZH_CN
|
EN_US, ZH_CN
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { AllI18nKeys, I18N } from "@Component/Localization/Localization";
|
import { AllI18nKeys, I18N } from "@Component/Localization/Localization";
|
||||||
import { useSetting, IMixinSettingProps, Themes, Language } from "@Context/Setting";
|
import { useSettingWithEvent, IMixinSettingProps, Themes, Language } from "@Context/Setting";
|
||||||
import { FunctionComponent } from "react";
|
import { FunctionComponent } from "react";
|
||||||
import "./Message.scss";
|
import "./Message.scss";
|
||||||
|
|
||||||
@ -38,5 +38,5 @@ const MessageView: FunctionComponent<IMessageProps & IMixinSettingProps> = (prop
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
const Message = useSetting(MessageView);
|
const Message = useSettingWithEvent("language", "themes")(MessageView);
|
||||||
export { Message };
|
export { Message };
|
114
source/Component/Popup/Popup.scss
Normal file
114
source/Component/Popup/Popup.scss
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
@import "../Theme/Theme.scss";
|
||||||
|
|
||||||
|
$header-height: 32px;
|
||||||
|
|
||||||
|
@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: $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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
div.dark.popup-mask {
|
||||||
|
background-color: rgba(0, 0, 0, 0.55);
|
||||||
|
}
|
||||||
|
|
||||||
|
div.light.popup-mask {
|
||||||
|
background-color: rgba(0, 0, 0, 0.15);
|
||||||
|
}
|
112
source/Component/Popup/Popup.tsx
Normal file
112
source/Component/Popup/Popup.tsx
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
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 {}
|
||||||
|
|
||||||
|
@useStatusWithEvent("popupChange")
|
||||||
|
class Popup extends Component<IPopupProps & IMixinStatusProps> {
|
||||||
|
|
||||||
|
public renderMask(index?: number, click?: () => void, key?: string): ReactNode {
|
||||||
|
const classList: string[] = ["popup-mask", "show-fade"];
|
||||||
|
return <Theme
|
||||||
|
key={key}
|
||||||
|
onClick={click}
|
||||||
|
className={classList.join(" ")}
|
||||||
|
style={{
|
||||||
|
zIndex: index,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 renderHeader(popup: PopupModel): ReactNode {
|
||||||
|
return <div className="popup-layer-header">
|
||||||
|
<div className="header-text">
|
||||||
|
{popup.onRenderHeader()}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="header-close-icon"
|
||||||
|
onClick={() => {
|
||||||
|
popup.onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="CalculatorMultiply"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
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 <Theme
|
||||||
|
style={{
|
||||||
|
width: popup.width,
|
||||||
|
height: popup.height,
|
||||||
|
zIndex: popup.zIndex(),
|
||||||
|
top: top,
|
||||||
|
left: left
|
||||||
|
}}
|
||||||
|
key={popup.id}
|
||||||
|
backgroundLevel={BackgroundLevel.Level4}
|
||||||
|
className="popup-layer show-scale"
|
||||||
|
>
|
||||||
|
{this.renderHeader(popup)}
|
||||||
|
</Theme>
|
||||||
|
}
|
||||||
|
|
||||||
|
public render(): ReactNode {
|
||||||
|
return <>
|
||||||
|
{this.renderRootMask()}
|
||||||
|
{this.renderMaskList()}
|
||||||
|
{this.props.status?.popup.popups.map((popup) => {
|
||||||
|
return this.renderLayer(popup);
|
||||||
|
})}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popup };
|
78
source/Context/Context.tsx
Normal file
78
source/Context/Context.tsx
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import { Emitter, EventType } from "@Model/Emitter";
|
||||||
|
import { Component, FunctionComponent, ReactNode, Consumer } from "react";
|
||||||
|
|
||||||
|
type RenderComponent = (new (...p: any) => Component<any, any, any>) | FunctionComponent<any>;
|
||||||
|
|
||||||
|
function superConnectWithEvent<C extends Emitter<E>, E extends Record<EventType, any>>(
|
||||||
|
consumer: Consumer<C>, keyName: string
|
||||||
|
) {
|
||||||
|
return (...events: Array<keyof E>) => {
|
||||||
|
return <R extends RenderComponent>(components: R): R => {
|
||||||
|
const Components = components as any;
|
||||||
|
const Consumer = consumer;
|
||||||
|
return class extends Component<R> {
|
||||||
|
|
||||||
|
private status: C | undefined;
|
||||||
|
private isEventMount: boolean = false;
|
||||||
|
private propsObject: Record<string, C> = {};
|
||||||
|
|
||||||
|
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 <Consumer>
|
||||||
|
{(status: C) => {
|
||||||
|
this.status = status;
|
||||||
|
this.propsObject[keyName] = status;
|
||||||
|
this.mountEvent();
|
||||||
|
return <Components {...this.props} {...this.propsObject}/>;
|
||||||
|
}}
|
||||||
|
</Consumer>
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount() {
|
||||||
|
this.unmountEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function superConnect<C extends Emitter<any>>(consumer: Consumer<C>, keyName: string) {
|
||||||
|
return <R extends RenderComponent>(components: R): R => {
|
||||||
|
return ((props: any) => {
|
||||||
|
|
||||||
|
const Components = components as any;
|
||||||
|
const Consumer = consumer;
|
||||||
|
|
||||||
|
return <Consumer>
|
||||||
|
{(status: C) => <Components
|
||||||
|
{...props}
|
||||||
|
{...{[keyName]: status}}
|
||||||
|
/>}
|
||||||
|
</Consumer>
|
||||||
|
}) as any;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { superConnectWithEvent, superConnect };
|
@ -1,4 +1,4 @@
|
|||||||
import { Emitter } from "./Emitter";
|
import { Emitter } from "@Model/Emitter";
|
||||||
|
|
||||||
enum LayoutDirection {
|
enum LayoutDirection {
|
||||||
X = 1,
|
X = 1,
|
175
source/Context/Popups.ts
Normal file
175
source/Context/Popups.ts
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
import { ReactNode, createElement } from "react";
|
||||||
|
import { Emitter } from "@Model/Emitter";
|
||||||
|
import { Localization } from "@Component/Localization/Localization";
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 唯一标识符
|
||||||
|
*/
|
||||||
|
public id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 控制器
|
||||||
|
*/
|
||||||
|
public controller: PopupController;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染层级
|
||||||
|
*/
|
||||||
|
public index: number = Infinity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* react 节点
|
||||||
|
*/
|
||||||
|
public reactNode: ReactNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染标题
|
||||||
|
*/
|
||||||
|
public onRenderHeader(): ReactNode {
|
||||||
|
return createElement(Localization, {i18nKey: "Popup.Title.Unnamed"});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染函数
|
||||||
|
*/
|
||||||
|
public onRender(p: Popup): ReactNode {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭回调
|
||||||
|
*/
|
||||||
|
public onClose(): void {
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染节点
|
||||||
|
*/
|
||||||
|
public render(): ReactNode {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IPopupControllerEvent {
|
||||||
|
popupChange: void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗模型
|
||||||
|
*/
|
||||||
|
class PopupController extends Emitter<IPopupControllerEvent> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ID 序列号
|
||||||
|
*/
|
||||||
|
private idIndex = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 最小弹窗 Index
|
||||||
|
*/
|
||||||
|
public zIndex = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗列表
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
this.emit("popupChange");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实例化并开启一个弹窗
|
||||||
|
*/
|
||||||
|
public showPopup<P extends IPopupConstructor>(popup?: P): Popup {
|
||||||
|
let newPopup = new (popup ?? Popup)(this, `P-${this.idIndex ++}`);
|
||||||
|
this.popups.push(newPopup);
|
||||||
|
this.sortPopup();
|
||||||
|
return newPopup;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 关闭一个弹窗
|
||||||
|
*/
|
||||||
|
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;
|
||||||
|
currentPopup.isClose = true;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (closePopup) {
|
||||||
|
this.sortPopup();
|
||||||
|
this.emit("popupChange");
|
||||||
|
}
|
||||||
|
return closePopup;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Popup, PopupController }
|
@ -1,6 +1,7 @@
|
|||||||
import { createContext, Component, FunctionComponent } from "react";
|
import { createContext } from "react";
|
||||||
|
import { superConnect, superConnectWithEvent } from "./Context";
|
||||||
import { Emitter } from "@Model/Emitter";
|
import { Emitter } from "@Model/Emitter";
|
||||||
import { Layout } from "@Model/Layout";
|
import { Layout } from "./Layout";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题模式
|
* 主题模式
|
||||||
@ -12,9 +13,11 @@ enum Themes {
|
|||||||
|
|
||||||
type Language = "ZH_CN" | "EN_US";
|
type Language = "ZH_CN" | "EN_US";
|
||||||
|
|
||||||
class Setting extends Emitter<
|
interface ISettingEvents extends Setting {
|
||||||
Setting & {change: keyof Setting}
|
attrChange: keyof Setting;
|
||||||
> {
|
}
|
||||||
|
|
||||||
|
class Setting extends Emitter<ISettingEvents> {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 主题
|
* 主题
|
||||||
@ -36,7 +39,7 @@ class Setting extends Emitter<
|
|||||||
*/
|
*/
|
||||||
public setProps<P extends keyof Setting>(key: P, value: Setting[P]) {
|
public setProps<P extends keyof Setting>(key: P, value: Setting[P]) {
|
||||||
this[key] = value as any;
|
this[key] = value as any;
|
||||||
this.emit("change", key);
|
this.emit("attrChange", key);
|
||||||
this.emit(key as any, value as any);
|
this.emit(key as any, value as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,21 +54,14 @@ SettingContext.displayName = "Setting";
|
|||||||
const SettingProvider = SettingContext.Provider;
|
const SettingProvider = SettingContext.Provider;
|
||||||
const SettingConsumer = SettingContext.Consumer;
|
const SettingConsumer = SettingContext.Consumer;
|
||||||
|
|
||||||
type RenderComponent = (new (...p: any) => Component<any, any, any>) | FunctionComponent<any>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修饰器
|
* 修饰器
|
||||||
*/
|
*/
|
||||||
function useSetting<R extends RenderComponent>(components: R): R {
|
const useSetting = superConnect<Setting>(SettingConsumer, "setting");
|
||||||
return ((props: any) => {
|
|
||||||
const C = components;
|
const useSettingWithEvent = superConnectWithEvent<Setting, ISettingEvents>(SettingConsumer, "setting");
|
||||||
return <SettingConsumer>
|
|
||||||
{(setting: Setting) => <C {...props} setting={setting}></C>}
|
|
||||||
</SettingConsumer>
|
|
||||||
}) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Themes, Setting, SettingContext, useSetting, Language,
|
Themes, Setting, SettingContext, useSetting, Language, useSettingWithEvent,
|
||||||
IMixinSettingProps, SettingProvider, SettingConsumer
|
IMixinSettingProps, SettingProvider, SettingConsumer
|
||||||
};
|
};
|
@ -1,4 +1,4 @@
|
|||||||
import { createContext, Component, FunctionComponent, useState, useEffect, ReactNode } from "react";
|
import { createContext } from "react";
|
||||||
import { Emitter } from "@Model/Emitter";
|
import { Emitter } from "@Model/Emitter";
|
||||||
import { Model, ObjectID } from "@Model/Model";
|
import { Model, ObjectID } from "@Model/Model";
|
||||||
import { Label } from "@Model/Label";
|
import { Label } from "@Model/Label";
|
||||||
@ -9,6 +9,8 @@ import { AbstractRenderer } from "@Model/Renderer";
|
|||||||
import { ClassicRenderer, MouseMod } from "@GLRender/ClassicRenderer";
|
import { ClassicRenderer, MouseMod } from "@GLRender/ClassicRenderer";
|
||||||
import { Setting } from "./Setting";
|
import { Setting } from "./Setting";
|
||||||
import { I18N } from "@Component/Localization/Localization";
|
import { I18N } from "@Component/Localization/Localization";
|
||||||
|
import { superConnectWithEvent, superConnect } from "./Context";
|
||||||
|
import { PopupController } from "./Popups";
|
||||||
|
|
||||||
function randomColor(unNormal: boolean = false) {
|
function randomColor(unNormal: boolean = false) {
|
||||||
const color = [
|
const color = [
|
||||||
@ -38,6 +40,7 @@ interface IStatusEvent {
|
|||||||
labelAttrChange: void;
|
labelAttrChange: void;
|
||||||
groupAttrChange: void;
|
groupAttrChange: void;
|
||||||
individualChange: void;
|
individualChange: void;
|
||||||
|
popupChange: void;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Status extends Emitter<IStatusEvent> {
|
class Status extends Emitter<IStatusEvent> {
|
||||||
@ -65,6 +68,11 @@ class Status extends Emitter<IStatusEvent> {
|
|||||||
*/
|
*/
|
||||||
public model: Model = new Model();
|
public model: Model = new Model();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 弹窗
|
||||||
|
*/
|
||||||
|
public popup: PopupController = new PopupController();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 焦点对象
|
* 焦点对象
|
||||||
*/
|
*/
|
||||||
@ -95,6 +103,9 @@ class Status extends Emitter<IStatusEvent> {
|
|||||||
this.model.on("objectChange", () => this.emit("objectChange"));
|
this.model.on("objectChange", () => this.emit("objectChange"));
|
||||||
this.model.on("labelChange", () => this.emit("labelChange"));
|
this.model.on("labelChange", () => this.emit("labelChange"));
|
||||||
|
|
||||||
|
// 弹窗事件
|
||||||
|
this.popup.on("popupChange", () => this.emit("popupChange"));
|
||||||
|
|
||||||
// 对象变换时执行渲染,更新渲染器数据
|
// 对象变换时执行渲染,更新渲染器数据
|
||||||
this.on("objectChange", this.delayDraw);
|
this.on("objectChange", this.delayDraw);
|
||||||
this.model.on("individualChange", this.delayDraw);
|
this.model.on("individualChange", this.delayDraw);
|
||||||
@ -258,67 +269,12 @@ StatusContext.displayName = "Status";
|
|||||||
const StatusProvider = StatusContext.Provider;
|
const StatusProvider = StatusContext.Provider;
|
||||||
const StatusConsumer = StatusContext.Consumer;
|
const StatusConsumer = StatusContext.Consumer;
|
||||||
|
|
||||||
type RenderComponent = (new (...p: any) => Component<any, any, any>) | FunctionComponent<any>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 修饰器
|
* 修饰器
|
||||||
*/
|
*/
|
||||||
function useStatus<R extends RenderComponent>(components: R): R {
|
const useStatus = superConnect<Status>(StatusConsumer, "status");
|
||||||
return ((props: any) => {
|
|
||||||
const C = components;
|
|
||||||
return <StatusConsumer>
|
|
||||||
{(status: Status) => <C {...props} status={status}></C>}
|
|
||||||
</StatusConsumer>
|
|
||||||
}) as any;
|
|
||||||
}
|
|
||||||
|
|
||||||
function useStatusWithEvent(...events: Array<keyof IStatusEvent>) {
|
const useStatusWithEvent = superConnectWithEvent<Status, IStatusEvent>(StatusConsumer, "status");
|
||||||
return <R extends RenderComponent>(components: R): R => {
|
|
||||||
const C = components as any;
|
|
||||||
return class extends Component<R> {
|
|
||||||
|
|
||||||
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 <StatusConsumer>
|
|
||||||
{(status: Status) => {
|
|
||||||
this.status = status;
|
|
||||||
this.mountEvent();
|
|
||||||
return <C {...this.props} status={status}></C>;
|
|
||||||
}}
|
|
||||||
</StatusConsumer>
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount() {
|
|
||||||
this.unmountEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
} as any;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
Status, StatusContext, useStatus, useStatusWithEvent,
|
Status, StatusContext, useStatus, useStatusWithEvent,
|
||||||
|
@ -44,6 +44,7 @@ const EN_US = {
|
|||||||
"Panel.Info.Label.Details.View": "Edit view label attributes",
|
"Panel.Info.Label.Details.View": "Edit view label attributes",
|
||||||
"Panel.Title.Group.Details.View": "Group",
|
"Panel.Title.Group.Details.View": "Group",
|
||||||
"Panel.Info.Group.Details.View": "Edit view group attributes",
|
"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.Group": "All group",
|
||||||
"Build.In.Label.Name.All.Range": "All range",
|
"Build.In.Label.Name.All.Range": "All range",
|
||||||
"Common.No.Data": "No Data",
|
"Common.No.Data": "No Data",
|
||||||
@ -51,6 +52,7 @@ const EN_US = {
|
|||||||
"Common.Attr.Title.Basic": "Basic properties",
|
"Common.Attr.Title.Basic": "Basic properties",
|
||||||
"Common.Attr.Title.Spatial": "Spatial property",
|
"Common.Attr.Title.Spatial": "Spatial property",
|
||||||
"Common.Attr.Title.Individual.Generation": "Individual generation",
|
"Common.Attr.Title.Individual.Generation": "Individual generation",
|
||||||
|
"Common.Attr.Title.Individual.kill": "Individual kill",
|
||||||
"Common.Attr.Key.Display.Name": "Display name",
|
"Common.Attr.Key.Display.Name": "Display name",
|
||||||
"Common.Attr.Key.Position.X": "Position X",
|
"Common.Attr.Key.Position.X": "Position X",
|
||||||
"Common.Attr.Key.Position.Y": "Position Y",
|
"Common.Attr.Key.Position.Y": "Position Y",
|
||||||
@ -79,6 +81,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.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.Range": "The specified scope object is invalid",
|
||||||
"Common.Attr.Key.Generation.Error.Invalid.Label": "The specified label has expired",
|
"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.Not.Range": "Object is not a Range",
|
||||||
"Panel.Info.Range.Details.Attr.Error.Unspecified": "Unspecified range object",
|
"Panel.Info.Range.Details.Attr.Error.Unspecified": "Unspecified range object",
|
||||||
"Panel.Info.Group.Details.Attr.Error.Not.Group": "Object is not a Group",
|
"Panel.Info.Group.Details.Attr.Error.Not.Group": "Object is not a Group",
|
||||||
|
@ -44,13 +44,15 @@ const ZH_CN = {
|
|||||||
"Panel.Info.Label.Details.View": "编辑查看标签属性",
|
"Panel.Info.Label.Details.View": "编辑查看标签属性",
|
||||||
"Panel.Title.Group.Details.View": "群",
|
"Panel.Title.Group.Details.View": "群",
|
||||||
"Panel.Info.Group.Details.View": "编辑查看群属性",
|
"Panel.Info.Group.Details.View": "编辑查看群属性",
|
||||||
|
"Popup.Title.Unnamed": "弹窗消息",
|
||||||
"Build.In.Label.Name.All.Group": "全部群",
|
"Build.In.Label.Name.All.Group": "全部群",
|
||||||
"Build.In.Label.Name.All.Range": "全部范围",
|
"Build.In.Label.Name.All.Range": "全部范围",
|
||||||
"Common.No.Data": "暂无数据",
|
"Common.No.Data": "暂无数据",
|
||||||
"Common.No.Unknown.Error": "未知错误",
|
"Common.No.Unknown.Error": "未知错误",
|
||||||
"Common.Attr.Title.Basic": "基础属性",
|
"Common.Attr.Title.Basic": "基础属性",
|
||||||
"Common.Attr.Title.Spatial": "空间属性",
|
"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.Display.Name": "显示名称",
|
||||||
"Common.Attr.Key.Position.X": "X 坐标",
|
"Common.Attr.Key.Position.X": "X 坐标",
|
||||||
"Common.Attr.Key.Position.Y": "Y 坐标",
|
"Common.Attr.Key.Position.Y": "Y 坐标",
|
||||||
@ -79,6 +81,8 @@ const ZH_CN = {
|
|||||||
"Common.Attr.Key.Generation.Error.Empty.Range.List": "指定的标签中没有包含任何范围对象",
|
"Common.Attr.Key.Generation.Error.Empty.Range.List": "指定的标签中没有包含任何范围对象",
|
||||||
"Common.Attr.Key.Generation.Error.Invalid.Range": "指定的范围对象已失效",
|
"Common.Attr.Key.Generation.Error.Invalid.Range": "指定的范围对象已失效",
|
||||||
"Common.Attr.Key.Generation.Error.Invalid.Label": "指定的标签已失效",
|
"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.Not.Range": "对象不是一个范围",
|
||||||
"Panel.Info.Range.Details.Attr.Error.Unspecified": "未指定范围对象",
|
"Panel.Info.Range.Details.Attr.Error.Unspecified": "未指定范围对象",
|
||||||
"Panel.Info.Group.Details.Attr.Error.Not.Group": "对象不是一个群",
|
"Panel.Info.Group.Details.Attr.Error.Not.Group": "对象不是一个群",
|
||||||
|
@ -49,6 +49,11 @@ class Group extends CtrlObject {
|
|||||||
*/
|
*/
|
||||||
public genErrorMessageShowCount: number = 0;
|
public genErrorMessageShowCount: number = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除个数
|
||||||
|
*/
|
||||||
|
public killCount: number = 1;
|
||||||
|
|
||||||
private genInSingleRange(count: number, range: Range) {
|
private genInSingleRange(count: number, range: Range) {
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
let individual = new Individual(this);
|
let individual = new Individual(this);
|
||||||
@ -188,6 +193,42 @@ class Group extends CtrlObject {
|
|||||||
return success;
|
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<number> = 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 创建数量
|
* @param count 创建数量
|
||||||
|
@ -46,7 +46,7 @@ class Label {
|
|||||||
* 判断是否为相同标签
|
* 判断是否为相同标签
|
||||||
*/
|
*/
|
||||||
public equal(label: Label): boolean {
|
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;
|
return this === label || this.id === label.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,9 +7,10 @@ import { StatusProvider, Status } from "@Context/Status";
|
|||||||
import { ClassicRenderer } from "@GLRender/ClassicRenderer";
|
import { ClassicRenderer } from "@GLRender/ClassicRenderer";
|
||||||
import { initializeIcons } from '@fluentui/font-icons-mdl2';
|
import { initializeIcons } from '@fluentui/font-icons-mdl2';
|
||||||
import { RootContainer } from "@Component/Container/RootContainer";
|
import { RootContainer } from "@Component/Container/RootContainer";
|
||||||
import { LayoutDirection } from "@Model/Layout";
|
import { LayoutDirection } from "@Context/Layout";
|
||||||
import "./SimulatorWeb.scss";
|
|
||||||
import { CommandBar } from "@Component/CommandBar/CommandBar";
|
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/");
|
initializeIcons("https://img.mrkbear.com/fabric-cdn-prod_20210407.001/");
|
||||||
|
|
||||||
@ -101,6 +102,7 @@ class SimulatorWeb extends Component {
|
|||||||
backgroundLevel={BackgroundLevel.Level5}
|
backgroundLevel={BackgroundLevel.Level5}
|
||||||
fontLevel={FontLevel.Level3}
|
fontLevel={FontLevel.Level3}
|
||||||
>
|
>
|
||||||
|
<Popup/>
|
||||||
<HeaderBar height={45}/>
|
<HeaderBar height={45}/>
|
||||||
<div className="app-root-space" style={{
|
<div className="app-root-space" style={{
|
||||||
height: `calc( 100% - ${45}px)`
|
height: `calc( 100% - ${45}px)`
|
||||||
|
@ -136,6 +136,24 @@ class GroupDetails extends Component<IGroupDetailsProps & IMixinStatusProps> {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Message i18nKey="Common.Attr.Title.Individual.kill" isTitle/>
|
||||||
|
|
||||||
|
<AttrInput
|
||||||
|
id={group.id} isNumber={true} step={1} keyI18n="Common.Attr.Key.Kill.Count"
|
||||||
|
value={group.killCount} min={1} max={1000}
|
||||||
|
valueChange={(val) => {
|
||||||
|
this.props.status?.changeGroupAttrib(group.id, "killCount", (val as any) / 1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TogglesInput
|
||||||
|
keyI18n="Common.Attr.Key.Generation"
|
||||||
|
onIconName="RemoveFilter" offIconName="RemoveFilter"
|
||||||
|
valueChange={() => {
|
||||||
|
group.killIndividuals()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,6 +31,12 @@
|
|||||||
],
|
],
|
||||||
"@Component/*": [
|
"@Component/*": [
|
||||||
"./source/Component/*"
|
"./source/Component/*"
|
||||||
|
],
|
||||||
|
"@Localization/*": [
|
||||||
|
"./source/Localization/*"
|
||||||
|
],
|
||||||
|
"@Panel/*": [
|
||||||
|
"./source/Panel/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user