diff --git a/source/Behavior/Behavior.ts b/source/Behavior/Behavior.ts index 6c0346a..123f8b3 100644 --- a/source/Behavior/Behavior.ts +++ b/source/Behavior/Behavior.ts @@ -4,7 +4,52 @@ import { Template } from "./Template"; const AllBehaviors: IAnyBehaviorRecorder[] = new Array(4).fill(0).map((_, i) => { let behavior = new BehaviorRecorder(Template); behavior.behaviorId = behavior.behaviorId + i; + behavior.behaviorName = behavior.behaviorName + Math.random().toString(36).slice(-6); + behavior.category = "Category" + Math.floor(Math.random() * 3).toString(); return behavior; }); -export { AllBehaviors }; \ No newline at end of file +/** + * 分类词条 + */ +type ICategory = {key: string, category: Record, item: IAnyBehaviorRecorder[]}; + +const AllBehaviorsWithCategory: ICategory[] = categoryBehaviors(AllBehaviors); + +/** + * 将词条进行分类 + */ +function categoryBehaviors(behaviors: IAnyBehaviorRecorder[]): ICategory[] { + let res: ICategory[] = []; + for (let i = 0; i < behaviors.length; i++) { + + let category: ICategory | undefined = undefined; + for (let j = 0; j < res.length; j++) { + if (res[j].key === behaviors[i].category) { + category = res[j]; + } + } + + if (!category) { + category = { + key: behaviors[i].category, + category: {}, + item: [] + } + res.push(category); + } + + if (behaviors[i].category[0] === "$") { + let terms = behaviors[i].terms[behaviors[i].category]; + if (terms) { + category.category = {...category.category, ...terms} + } + } + + category.item.push(behaviors[i]); + } + + return res; +} + +export { AllBehaviors, AllBehaviorsWithCategory, ICategory as ICategoryBehavior }; \ No newline at end of file diff --git a/source/Behavior/Template.ts b/source/Behavior/Template.ts index ff3a17a..fca9fd2 100644 --- a/source/Behavior/Template.ts +++ b/source/Behavior/Template.ts @@ -10,11 +10,22 @@ class Template extends Behavior> = { + "$Title": { + "ZH_CN": "行为", + "EN_US": "Behavior" + }, + "$Intro": { + "ZH_CN": "这是一个模板行为", + "EN_US": "This is a template behavior" + } + }; } export { Template }; \ No newline at end of file diff --git a/source/Component/BehaviorList/BehaviorList.scss b/source/Component/BehaviorList/BehaviorList.scss index 952ad08..8d9baa0 100644 --- a/source/Component/BehaviorList/BehaviorList.scss +++ b/source/Component/BehaviorList/BehaviorList.scss @@ -92,6 +92,14 @@ div.behavior-list { } } } + + div.add-button { + width: 45px; + height: 45px; + display: flex; + justify-content: center; + align-items: center; + } } div.dark.behavior-list { diff --git a/source/Component/BehaviorList/BehaviorList.tsx b/source/Component/BehaviorList/BehaviorList.tsx index ce91bb9..e9624a7 100644 --- a/source/Component/BehaviorList/BehaviorList.tsx +++ b/source/Component/BehaviorList/BehaviorList.tsx @@ -1,8 +1,8 @@ import { Theme } from "@Component/Theme/Theme"; import { Component, ReactNode } from "react"; import { IRenderBehavior, Behavior, BehaviorRecorder } from "@Model/Behavior"; +import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting"; import { Icon } from "@fluentui/react"; -import { Localization } from "@Component/Localization/Localization"; import "./BehaviorList.scss"; interface IBehaviorListProps { @@ -10,10 +10,12 @@ interface IBehaviorListProps { focusBehaviors?: IRenderBehavior[]; click?: (behavior: IRenderBehavior) => void; action?: (behavior: IRenderBehavior) => void; + onAdd?: () => void; actionType?: "info" | "delete"; } -class BehaviorList extends Component { +@useSettingWithEvent("language") +class BehaviorList extends Component { private isFocus(behavior: IRenderBehavior): boolean { if (this.props.focusBehaviors) { @@ -57,10 +59,10 @@ class BehaviorList extends Component { } - private renderTerm(key: string, className: string, needLocal: boolean) { + private renderTerm(behavior: IRenderBehavior, key: string, className: string, needLocal: boolean) { if (needLocal) { return
- + {behavior.getTerms(key, this.props.setting?.language)}
; } else { return
@@ -117,8 +119,8 @@ class BehaviorList extends Component {
- {this.renderTerm(name, "title-view", needLocal)} - {this.renderTerm(info, "info-view", needLocal)} + {this.renderTerm(behavior, name, "title-view", needLocal)} + {this.renderTerm(behavior, info, "info-view", needLocal)}
{this.renderActionButton(behavior)} @@ -126,11 +128,18 @@ class BehaviorList extends Component {
} + private renderAddButton(add: () => void) { + return
+ +
+ } + public render(): ReactNode { return {this.props.behaviors.map((behavior) => { return this.renderBehavior(behavior); })} + {this.props.onAdd ? this.renderAddButton(this.props.onAdd) : null} } } diff --git a/source/Component/BehaviorPopup/BehaviorPopup.scss b/source/Component/BehaviorPopup/BehaviorPopup.scss index 8886442..f4039e7 100644 --- a/source/Component/BehaviorPopup/BehaviorPopup.scss +++ b/source/Component/BehaviorPopup/BehaviorPopup.scss @@ -5,7 +5,7 @@ div.behavior-popup { height: 100%; } -span.behavior-popup-select-counter { +span.behavior-popup-select-counter, div.behavior-popup-no-data { opacity: .75; } diff --git a/source/Component/BehaviorPopup/BehaviorPopup.tsx b/source/Component/BehaviorPopup/BehaviorPopup.tsx index b87154c..eb5691c 100644 --- a/source/Component/BehaviorPopup/BehaviorPopup.tsx +++ b/source/Component/BehaviorPopup/BehaviorPopup.tsx @@ -1,19 +1,19 @@ -import { Component, ReactNode } from "react"; +import { Component, ReactNode, Fragment } from "react"; import { Popup } from "@Context/Popups"; -import { Localization, I18N } from "@Component/Localization/Localization"; +import { Localization } from "@Component/Localization/Localization"; import { SearchBox } from "@Component/SearchBox/SearchBox"; import { ConfirmContent } from "@Component/ConfirmPopup/ConfirmPopup"; import { BehaviorList } from "@Component/BehaviorList/BehaviorList"; -import { AllBehaviors } from "@Behavior/Behavior"; +import { AllBehaviorsWithCategory, ICategoryBehavior } from "@Behavior/Behavior"; import { Message } from "@Component/Message/Message"; -import { IRenderBehavior } from "@Model/Behavior"; -import { useStatus, IMixinStatusProps } from "@Context/Status"; +import { IRenderBehavior, BehaviorRecorder } from "@Model/Behavior"; +import { useStatus, IMixinStatusProps, randomColor } from "@Context/Status"; import { useSettingWithEvent, IMixinSettingProps } from "@Context/Setting"; import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup"; import "./BehaviorPopup.scss"; interface IBehaviorPopupProps { - + onDismiss?: () => void; } interface IBehaviorPopupState { @@ -21,7 +21,7 @@ interface IBehaviorPopupState { focusBehavior: Set; } -class BehaviorPopup extends Popup { +class BehaviorPopup extends Popup { public minWidth: number = 400; public minHeight: number = 300; @@ -34,7 +34,9 @@ class BehaviorPopup extends Popup { } public render(): ReactNode { - return + return { + this.close(); + }}/> } } @@ -66,10 +68,14 @@ class BehaviorPopupComponent extends Component< if (this.props.status) { const status = this.props.status; status.popup.showPopup(ConfirmPopup, { - infoI18n: behavior.describe as any, + renderInfo: () => { + return + }, titleI18N: "Popup.Behavior.Info.Title", titleI18NOption: { - behavior: I18N(this.props, behavior.behaviorName as any) + behavior: behavior.getTerms(behavior.behaviorName, this.props.setting?.language) }, yesI18n: "Popup.Behavior.Info.Confirm", }) @@ -86,21 +92,28 @@ class BehaviorPopupComponent extends Component< /> } - public render(): ReactNode { - return - + private renderBehaviors = (behaviors: ICategoryBehavior, first: boolean) => { + + let language = this.props.setting?.language ?? "EN_US"; + let filterItem = behaviors.item.filter((item) => { + let name = item.getTerms(item.behaviorName, this.props.setting?.language); + if (this.state.searchValue) { + return name.includes(this.state.searchValue); + } else { + return true; + } + }) + + if (filterItem.length <= 0) return undefined; + + return + { if (this.state.focusBehavior.has(behavior)) { @@ -111,6 +124,56 @@ class BehaviorPopupComponent extends Component< this.forceUpdate(); }} /> + + } + + private addSelectBehavior = () => { + this.state.focusBehavior.forEach((recorder) => { + if (this.props.status && recorder instanceof BehaviorRecorder) { + let newBehavior = this.props.status.model.addBehavior(recorder); + + // 初始化名字 + newBehavior.name = recorder.getTerms( + recorder.behaviorName, this.props.setting?.language + ) + " " + (recorder.nameIndex - 1).toString(); + + // 赋予一个随机颜色 + let color = randomColor(true); + newBehavior.color = `rgb(${color[0]},${color[1]},${color[2]})`; + } + }); + this.props.onDismiss ? this.props.onDismiss() : undefined; + } + + public render(): ReactNode { + let first: boolean = true; + let behaviorNodes = AllBehaviorsWithCategory.map((behavior) => { + let renderItem = this.renderBehaviors(behavior, first); + if (renderItem) { + first = false; + } + return renderItem; + }).filter((x) => !!x); + + return + { + behaviorNodes.length ? behaviorNodes : + + } } } diff --git a/source/Component/ConfirmPopup/ConfirmPopup.tsx b/source/Component/ConfirmPopup/ConfirmPopup.tsx index 0fad186..fe75de2 100644 --- a/source/Component/ConfirmPopup/ConfirmPopup.tsx +++ b/source/Component/ConfirmPopup/ConfirmPopup.tsx @@ -11,6 +11,7 @@ interface IConfirmPopupProps { infoI18n?: AllI18nKeys; yesI18n?: AllI18nKeys; noI18n?: AllI18nKeys; + renderInfo?: () => ReactNode; red?: "yes" | "no"; yes?: () => any; no?: () => any; @@ -59,7 +60,13 @@ class ConfirmPopup extends Popup { return - {this.props.infoI18n ? : null} + { + this.props.renderInfo ? + this.props.renderInfo() : + this.props.infoI18n ? + : + null + } } } diff --git a/source/Component/Message/Message.tsx b/source/Component/Message/Message.tsx index d46f997..56384da 100644 --- a/source/Component/Message/Message.tsx +++ b/source/Component/Message/Message.tsx @@ -4,7 +4,8 @@ import { FunctionComponent } from "react"; import "./Message.scss"; interface IMessageProps { - i18nKey: AllI18nKeys; + i18nKey?: AllI18nKeys; + text?: string; options?: Record; className?: string; isTitle?: boolean; @@ -34,7 +35,15 @@ const MessageView: FunctionComponent = (prop } return
- {I18N(language, props.i18nKey, props.options)} + { + props.text ? + {props.text} : + props.i18nKey ? + { + I18N(language, props.i18nKey, props.options) + } : + null + }
} diff --git a/source/Context/Status.tsx b/source/Context/Status.tsx index 16f111f..b7d9069 100644 --- a/source/Context/Status.tsx +++ b/source/Context/Status.tsx @@ -11,6 +11,7 @@ import { Setting } from "./Setting"; import { I18N } from "@Component/Localization/Localization"; import { superConnectWithEvent, superConnect } from "./Context"; import { PopupController } from "./Popups"; +import { Behavior } from "@Model/Behavior"; function randomColor(unNormal: boolean = false) { const color = [ @@ -32,6 +33,7 @@ interface IStatusEvent { mouseModChange: void; focusObjectChange: void; focusLabelChange: void; + focusBehaviorChange: void; objectChange: void; rangeLabelChange: void; groupLabelChange: void; @@ -40,6 +42,7 @@ interface IStatusEvent { labelAttrChange: void; groupAttrChange: void; individualChange: void; + behaviorChange: void; popupChange: void; } @@ -83,6 +86,11 @@ class Status extends Emitter { */ public focusLabel?: Label; + /** + * 焦点行为 + */ + public focusBehavior?: Behavior; + private drawTimer?: NodeJS.Timeout; private delayDraw = () => { @@ -102,6 +110,7 @@ class Status extends Emitter { // 对象变化事件 this.model.on("objectChange", () => this.emit("objectChange")); this.model.on("labelChange", () => this.emit("labelChange")); + this.model.on("behaviorChange", () => this.emit("behaviorChange")); // 弹窗事件 this.popup.on("popupChange", () => this.emit("popupChange")); @@ -136,6 +145,14 @@ class Status extends Emitter { this.emit("focusLabelChange"); } + /** + * 更新焦点行为 + */ + public setBehaviorObject(focusBehavior?: Behavior) { + this.focusBehavior = focusBehavior; + this.emit("focusBehaviorChange"); + } + /** * 修改范围属性 */ @@ -277,6 +294,6 @@ const useStatus = superConnect(StatusConsumer, "status"); const useStatusWithEvent = superConnectWithEvent(StatusConsumer, "status"); export { - Status, StatusContext, useStatus, useStatusWithEvent, + Status, StatusContext, useStatus, useStatusWithEvent, randomColor, IMixinStatusProps, StatusProvider, StatusConsumer }; \ No newline at end of file diff --git a/source/Localization/EN-US.ts b/source/Localization/EN-US.ts index 124c4f1..ced4fa1 100644 --- a/source/Localization/EN-US.ts +++ b/source/Localization/EN-US.ts @@ -44,6 +44,8 @@ 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", + "Panel.Title.Behavior.List.View": "Behavior list", + "Panel.Info.Behavior.List.View": "Edit view behavior list", "Popup.Title.Unnamed": "Popup message", "Popup.Title.Confirm": "Confirm message", "Popup.Action.Yes": "Confirm", @@ -51,16 +53,16 @@ const EN_US = { "Popup.Action.Objects.Confirm.Title": "Confirm Delete", "Popup.Action.Objects.Confirm.Delete": "Delete", "Popup.Delete.Objects.Confirm": "Are you sure you want to delete this object(s)? The object is deleted and cannot be recalled.", + "Popup.Delete.Behavior.Confirm": "Are you sure you want to delete this behavior? The behavior is deleted and cannot be recalled.", "Popup.Setting.Title": "Preferences setting", "Popup.Add.Behavior.Title": "Add behavior", "Popup.Add.Behavior.Action.Add": "Add all select behavior", "Popup.Add.Behavior.Select.Counter": "Selected {count} behavior", + "Popup.Add.Behavior.Select.Nodata": "Could not find behavior named \"{name}\"", "Popup.Behavior.Info.Title": "Behavior details: {behavior}", "Popup.Behavior.Info.Confirm": "OK, I know it", "Build.In.Label.Name.All.Group": "All group", "Build.In.Label.Name.All.Range": "All range", - "Behavior.Template.Title": "Behavior", - "Behavior.Template.Intro": "This is a template behavior", "Common.Search.Placeholder": "Search in here...", "Common.No.Data": "No Data", "Common.No.Unknown.Error": "Unknown error", diff --git a/source/Localization/ZH-CN.ts b/source/Localization/ZH-CN.ts index 3495892..3e71c1a 100644 --- a/source/Localization/ZH-CN.ts +++ b/source/Localization/ZH-CN.ts @@ -44,6 +44,8 @@ const ZH_CN = { "Panel.Info.Label.Details.View": "编辑查看标签属性", "Panel.Title.Group.Details.View": "群", "Panel.Info.Group.Details.View": "编辑查看群属性", + "Panel.Title.Behavior.List.View": "行为列表", + "Panel.Info.Behavior.List.View": "编辑查看行为列表", "Popup.Title.Unnamed": "弹窗消息", "Popup.Title.Confirm": "确认消息", "Popup.Action.Yes": "确定", @@ -51,16 +53,16 @@ const ZH_CN = { "Popup.Action.Objects.Confirm.Title": "删除确认", "Popup.Action.Objects.Confirm.Delete": "删除", "Popup.Delete.Objects.Confirm": "你确定要删除这个(些)对象吗?对象被删除将无法撤回。", + "Popup.Delete.Behavior.Confirm": "你确定要删除这个行为吗?行为被删除将无法撤回。", "Popup.Setting.Title": "首选项设置", "Popup.Add.Behavior.Title": "添加行为", "Popup.Add.Behavior.Action.Add": "添加全部选中行为", - "Popup.Add.Behavior.Select.Counter": "已选择 {count} 个行为", + "Popup.Add.Behavior.Select.Counter": "找不到名为 \"{name}\" 的行为", + "Popup.Add.Behavior.Select.Nodata": "Could not find behavior named \"{name}\"", "Popup.Behavior.Info.Title": "行为详情: {behavior}", "Popup.Behavior.Info.Confirm": "好的, 我知道了", "Build.In.Label.Name.All.Group": "全部群", "Build.In.Label.Name.All.Range": "全部范围", - "Behavior.Template.Title": "行为", - "Behavior.Template.Intro": "这是一个模板行为", "Common.Search.Placeholder": "在此处搜索...", "Common.No.Data": "暂无数据", "Common.No.Unknown.Error": "未知错误", diff --git a/source/Model/Behavior.ts b/source/Model/Behavior.ts index ff8abf9..dbc7d58 100644 --- a/source/Model/Behavior.ts +++ b/source/Model/Behavior.ts @@ -132,6 +132,8 @@ type IBehaviorConstructor< type IAnyBehavior = Behavior; type IAnyBehaviorRecorder = BehaviorRecorder; +type Language = "ZH_CN" | "EN_US"; + /** * 行为的基础信息 */ @@ -156,6 +158,34 @@ class BehaviorInfo = {}> extends Emitter { * 行为描述 */ public describe: string = ""; + + /** + * 类别 + */ + public category: string = ""; + + /** + * 提条列表 + */ + public terms: Record> = {}; + + /** + * 获取词条翻译 + */ + public getTerms(key: string, language?: Language | string): string { + if (key[0] === "$" && this.terms[key]) { + let res: string = ""; + if (language) { + res = this.terms[key][language]; + } else { + res = this.terms[key]["EN_US"]; + } + if (res) { + return res; + } + } + return key; + } } class BehaviorRecorder< @@ -239,6 +269,7 @@ class BehaviorRecorder< this.behaviorId = this.behaviorInstance.behaviorId; this.behaviorName = this.behaviorInstance.behaviorName; this.describe = this.behaviorInstance.describe; + this.terms = this.behaviorInstance.terms; } } diff --git a/source/Page/SimulatorWeb/SimulatorWeb.tsx b/source/Page/SimulatorWeb/SimulatorWeb.tsx index 07587a5..d9913dc 100644 --- a/source/Page/SimulatorWeb/SimulatorWeb.tsx +++ b/source/Page/SimulatorWeb/SimulatorWeb.tsx @@ -66,7 +66,7 @@ class SimulatorWeb extends Component { items: [ {panels: ["RenderView", "Label Aa Bb", "Label aaa"]}, { - items: [{panels: ["Label b", "Label bbb"]}, {panels: ["LabelList"]}], + items: [{panels: ["BehaviorList", "Label bbb"]}, {panels: ["LabelList"]}], scale: 80, layout: LayoutDirection.X } diff --git a/source/Panel/BehaviorList/BehaviorList.scss b/source/Panel/BehaviorList/BehaviorList.scss new file mode 100644 index 0000000..a864441 --- /dev/null +++ b/source/Panel/BehaviorList/BehaviorList.scss @@ -0,0 +1,8 @@ +@import "../../Component/Theme/Theme.scss"; + +div.behavior-list-panel-root { + width: 100%; + min-height: 100%; + padding: 10px; + box-sizing: border-box; +} \ No newline at end of file diff --git a/source/Panel/BehaviorList/BehaviorList.tsx b/source/Panel/BehaviorList/BehaviorList.tsx new file mode 100644 index 0000000..780ffd8 --- /dev/null +++ b/source/Panel/BehaviorList/BehaviorList.tsx @@ -0,0 +1,77 @@ +import { BehaviorList as BehaviorListComponent } from "@Component/BehaviorList/BehaviorList"; +import { Component } from "react"; +import { useStatusWithEvent, IMixinStatusProps } from "@Context/Status"; +import { useSetting, IMixinSettingProps } from "@Context/Setting"; +import { Behavior } from "@Model/Behavior"; +import { Message } from "@Component/Message/Message"; +import { ConfirmPopup } from "@Component/ConfirmPopup/ConfirmPopup"; +import { BehaviorPopup } from "@Component/BehaviorPopup/BehaviorPopup"; +import "./BehaviorList.scss"; + +interface IBehaviorListProps { + +} + +@useSetting +@useStatusWithEvent("behaviorChange", "focusBehaviorChange") +class BehaviorList extends Component { + + private labelInnerClick: boolean = false; + + public render() { + let behaviors: Behavior[] = []; + if (this.props.status) { + behaviors = this.props.status.model.behaviorPool.concat([]); + } + return
{ + if (this.props.status && !this.labelInnerClick) { + this.props.status.setBehaviorObject(); + } + this.labelInnerClick = false; + }} + > + {behaviors.length <=0 ? + : null + } + { + if (this.props.status) { + this.props.status.setBehaviorObject(behavior as Behavior); + } + // if (this.props.setting) { + // this.props.setting.layout.focus("LabelDetails"); + // } + this.labelInnerClick = true; + }} + onAdd={() => { + this.props.status?.popup.showPopup(BehaviorPopup, {}); + }} + action={(behavior) => { + if (this.props.status && behavior instanceof Behavior) { + const status = this.props.status; + status.popup.showPopup(ConfirmPopup, { + infoI18n: "Popup.Delete.Behavior.Confirm", + titleI18N: "Popup.Action.Objects.Confirm.Title", + yesI18n: "Popup.Action.Objects.Confirm.Delete", + red: "yes", + yes: () => { + status.model.deleteBehavior(behavior); + } + }) + } + this.labelInnerClick = true; + }} + /> +
; + } +} + +export { BehaviorList }; \ No newline at end of file diff --git a/source/Panel/Panel.tsx b/source/Panel/Panel.tsx index 0a5e6ad..a032e83 100644 --- a/source/Panel/Panel.tsx +++ b/source/Panel/Panel.tsx @@ -8,6 +8,7 @@ import { RangeDetails } from "./RangeDetails/RangeDetails"; import { LabelList } from "./LabelList/LabelList"; import { LabelDetails } from "./LabelDetails/LabelDetails"; import { GroupDetails } from "./GroupDetails/GroupDetails"; +import { BehaviorList } from "./BehaviorList/BehaviorList"; interface IPanelInfo { nameKey: string; @@ -27,6 +28,7 @@ type PanelId = "" | "LabelList" // 标签列表 | "LabelDetails" // 标签属性 | "GroupDetails" // 群属性 +| "BehaviorList" // 行为列表 ; const PanelInfoMap = new Map(); @@ -54,6 +56,10 @@ PanelInfoMap.set("GroupDetails", { nameKey: "Panel.Title.Group.Details.View", introKay: "Panel.Info.Group.Details.View", class: GroupDetails }); +PanelInfoMap.set("BehaviorList", { + nameKey: "Panel.Title.Behavior.List.View", introKay: "Panel.Info.Behavior.List.View", + class: BehaviorList, hidePadding: true +}); function getPanelById(panelId: PanelId): ReactNode { switch (panelId) {