UI模块添加数据绑定装饰器

1.添加数据基类,子类自动添加代理,数据变化自动通知
 2.支持同属性多装饰器
This commit is contained in:
gongxh 2025-08-29 15:25:10 +08:00
parent b62a4af8db
commit e48011d941
45 changed files with 1354 additions and 21 deletions

55
.claudeignore Normal file
View File

@ -0,0 +1,55 @@
# Claude Code ignore rules for kunpolibrary
# 昆坡库项目过滤规则,减少上下文消耗
# Demo and example files - 示例和演示文件
demo/
**/demo/
# Dependencies - 依赖包
node_modules/
**/node_modules/
# Generated/build output - 构建产物
dist/
**/dist/
# Type definitions - 类型定义文件
libs/
**/libs/
# Images and assets - 图片和静态资源
image/
**/image/
*.jpg
*.jpeg
*.png
*.gif
*.ico
# Other common build artifacts - 其他常见构建产物
build/
**/build/
temp/
**/temp/
.temp/
**/.temp/
# Cache directories - 缓存目录
.cache/
**/.cache/
# Log files - 日志文件
*.log
logs/
**/logs/
# IDE and editor files - IDE和编辑器文件
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files - 操作系统生成的文件
.DS_Store
Thumbs.db

View File

@ -1,3 +1,6 @@
## 1.1.9
- 新增数据模块
## 1.1.8
- 支持窗口和自定义组件动态注册 (用来兼容代码在bundle中代码加载顺序导致的问题)

View File

@ -47,6 +47,7 @@ npm set registry https://npm.aliyun.com
9. [小游戏接口封装](./docs/MiniGame.md)
10. [热更新](./docs/HotUpdate.md)
11. [条件显示节点 (一般用于UI上的红点)](./docs/Condition.md)
12. [数据模块](./docs/Data.md)
# 独立模块目录
1. [ec模块](https://github.com/Gongxh0901/kunpo-ec)

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<component size="150,150">
<displayList>
<graph id="n0_c2y1" name="n0" xy="0,0" size="150,150" type="rect">
<relation target="" sidePair="width-width,height-height"/>
</graph>
<text id="n1_c2y1" name="lab_level" xy="0,0" size="150,150" fontSize="36" align="center" vAlign="middle" autoSize="shrink" bold="true" text="组件">
<relation target="" sidePair="center-center,middle-middle"/>
</text>
</displayList>
</component>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<component size="750,1334">
<displayList>
<graph id="n0_73ie" name="n0" xy="-1,0" size="750,1334" type="rect" lineSize="5" lineColor="#ffff0000" fillColor="#ff003399" corner="20">
<relation target="" sidePair="width-width,height-height,center-center,middle-middle"/>
</graph>
<component id="n22_sf8l" name="btn_close" src="ukhni" fileName="btns/btn_close.xml" pkg="mkwn34a7" xy="323,1154">
<relation target="" sidePair="center-center,bottom-bottom"/>
</component>
<component id="n1_zmnj" name="btn_refresh_level" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="55,758" size="222,73">
<relation target="" sidePair="left-left,bottom-bottom"/>
<Button title="更新关卡" titleFontSize="26"/>
</component>
<component id="n23_rrvv" name="btn_refresh_storey" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="55,841" size="222,73">
<relation target="" sidePair="left-left,bottom-bottom"/>
<Button title="更新层数" titleFontSize="26"/>
</component>
<component id="n24_rrvv" name="btn_refresh_bool" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="55,924" size="222,73">
<relation target="" sidePair="left-left,bottom-bottom"/>
<Button title="更新bool" titleFontSize="26"/>
</component>
<component id="n25_rrvv" name="btn_refresh_data" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="496,758" size="222,73">
<relation target="" sidePair="right-right,bottom-bottom"/>
<Button title="更新对象" titleFontSize="26"/>
</component>
<component id="n26_rrvv" name="btn_refresh_min" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="496,841" size="222,73">
<relation target="" sidePair="right-right,bottom-bottom"/>
<Button title="更新min" titleFontSize="26"/>
</component>
<component id="n33_gsjf" name="btn_refresh_max" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="496,924" size="222,73">
<relation target="" sidePair="right-right,bottom-bottom"/>
<Button title="更新max" titleFontSize="26"/>
</component>
<component id="n27_rrvv" name="btn_refresh_all" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="264,1048" size="222,73">
<relation target="" sidePair="center-center,bottom-bottom"/>
<Button title="更新全部" titleFontSize="26"/>
</component>
<text id="n28_rrvv" name="lab_level" xy="60,105" size="138,49" fontSize="36" color="#ffffff" align="center" vAlign="middle" bold="true" text="关卡1">
<relation target="" sidePair="left-left,top-top"/>
</text>
<text id="n29_rrvv" name="lab_storey" xy="60,166" size="102,49" fontSize="36" color="#ffffff" align="center" vAlign="middle" bold="true" text="层1">
<relation target="" sidePair="left-left,top-top"/>
</text>
<text id="n30_rrvv" name="lab_bool" xy="60,227" size="189,49" fontSize="36" color="#ffffff" align="center" vAlign="middle" bold="true" text="bool: true">
<relation target="" sidePair="left-left,top-top"/>
</text>
<text id="n31_rrvv" name="lab_datamin" xy="60,288" size="280,49" fontSize="36" color="#ffffff" align="center" vAlign="middle" bold="true" text="对象属性min1">
<relation target="" sidePair="left-left,top-top"/>
</text>
<text id="n32_rrvv" name="lab_datamax" xy="60,349" size="291,49" fontSize="36" color="#ffffff" align="center" vAlign="middle" bold="true" text="对象属性max2">
<relation target="" sidePair="left-left,top-top"/>
</text>
<component id="n34_c2y1" name="n34" src="c2y12" fileName="DataItem.xml" xy="544,49">
<relation target="" sidePair="right-right,top-top"/>
</component>
<component id="n35_c2y1" name="n35" src="c2y12" fileName="DataItem.xml" xy="544,230">
<relation target="" sidePair="right-right,top-top"/>
</component>
</displayList>
</component>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<packageDescription id="87hfwn3d">
<resources>
<component id="rrvv1" name="DataWindow.xml" path="/" exported="true"/>
<component id="c2y12" name="DataItem.xml" path="/"/>
</resources>
<publish name=""/>
</packageDescription>

View File

@ -30,6 +30,10 @@
<relation target="" sidePair="center-center,top-top"/>
<Button title="界面功能" titleFontSize="26"/>
</component>
<component id="n23_rrvv" name="btn_data" src="kofe0" fileName="btns/button1.xml" pkg="mkwn34a7" xy="215,738" size="320,73">
<relation target="" sidePair="center-center,top-top"/>
<Button title="数据更新" titleFontSize="26"/>
</component>
</displayList>
<transition name="t0"/>
<transition name="t1"/>

Binary file not shown.

View File

@ -0,0 +1,12 @@
{
"ver": "1.0.3",
"importer": "buffer",
"imported": true,
"uuid": "db12f6e3-dc8e-45aa-ba9f-90edf99cd732",
"files": [
".bin",
".json"
],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "1bd1aa0a-5c4d-4425-afaa-2a46c0f02a42",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,62 @@
/**
* @Author: Gongxh
* @Date: 2025-08-19
* @Description:
*/
import { kunpo } from "../../header";
export class Level extends kunpo.DataBase {
private _levelid: number = 1;
private _storey: number = 0;
private _ispassed: boolean = false;
private _data: { min: number, max: number } = { min: 1, max: 100 };
constructor() {
super();
}
public get levelid() { return this._levelid; }
public set levelid(lv: number) { this._levelid = lv; }
public get storey() { return this._storey; }
public set storey(storey: number) { this._storey = storey; }
public get ispassed() { return this._ispassed; }
public set ispassed(bool: boolean) { this._ispassed = bool; }
public get data(): { min: number, max: number } { return this._data; }
public set data(data: { min: number, max: number }) { this._data = data; }
public init(data: any) {
this.data = { min: data.min, max: data.max };
this.refreshMin(data.min);
this.refreshMax(data.max);
this.ispassed = data.ispassed;
this.levelid = data.levelid;
this.storey = data.storey;
}
public refreshLevel(lv: number) {
this.levelid = lv;
}
public refreshStorey(storey: number) {
this.storey = storey;
}
public refreshBool(bool: boolean) {
this.ispassed = bool;
}
public refreshMin(min: number) {
this.data.min = min;
}
public refreshMax(max: number) {
this.data.max = max;
}
}

View File

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "632daee7-7c2c-4baf-a4dd-d1a12ad91169",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "3edce213-52a5-403f-b57f-52da9d7bcbce",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -5,8 +5,11 @@
*/
import { GlobalEvent } from "kunpocc-event";
import { Level } from "../Data/global/Level";
export class DataHelper {
public static level: Level = new Level();
private static _data: Map<string, any> = new Map();
public static getValue<T>(key: string, defaultValue: T): T {

View File

@ -2,7 +2,7 @@
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "7340bacc-d167-47e8-a83b-2224c46b7fd0",
"uuid": "e786f26b-332e-4bb2-88c4-4d1c25b675a5",
"files": [],
"subMetas": {},
"userData": {}

View File

@ -5,8 +5,8 @@
*/
import { ConditionType } from "../../condition/ConditionType";
import { DataHelper } from "../../Data/DataHelper";
import { fgui, kunpo } from "../../header";
import { DataHelper } from "../../Helper/DataHelper";
const { uiclass, uiprop, uiclick } = kunpo._uidecorator;
@uiclass("Window", "Condition", "ConditionWindow")

View File

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "d0395b78-5d23-4c60-91ca-8851d9eddc27",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,127 @@
/**
* @Author: Gongxh
* @Date: 2025-08-19
* @Description:
*/
import { Level } from "../../Data/global/Level";
import { fgui, kunpo } from "../../header";
import { DataHelper } from "../../Helper/DataHelper";
const { bindMethod, bindProp } = kunpo.data;
const { uiclass, uiprop, uiclick } = kunpo._uidecorator;
@uiclass("Window", "Data", "DataWindow")
export class DataWindow extends kunpo.Window {
@uiprop
@bindProp(Level, data => data.storey, (item: fgui.GTextField, value: number, data: Level) => {
item.text = `关卡:${data.levelid} 层数:${value}`;
})
@bindProp(Level, data => data.levelid, (item: fgui.GTextField, value: number, data: Level) => {
item.text = `关卡:${value} 层数:${data.storey}`;
})
private lab_level: fgui.GTextField;
@uiprop
@bindProp(Level, data => data.storey, (item: fgui.GTextField, value: number, data: Level) => {
item.text = `层数:${value}`;
})
private lab_storey: fgui.GTextField;
@uiprop
@bindProp(Level, data => data.refreshMin, (item: fgui.GTextField) => {
item.text = `最小值:${DataHelper.level.data.min}`;
})
private lab_min: fgui.GTextField;
@uiprop
@bindProp(Level, data => data.refreshMax, (item: fgui.GTextField) => {
item.text = `最大值:${DataHelper.level.data.max}`;
})
private lab_max: fgui.GTextField;
@uiprop
@bindProp(Level, data => data.ispassed, (item: fgui.GTextField) => {
item.text = `是否通过:${DataHelper.level.ispassed ? '是' : '否'}`;
})
private lab_ispassed: fgui.GTextField;
protected onInit(): void {
this.adapterType = kunpo.AdapterType.Bang;
this.type = kunpo.WindowType.Normal;
}
protected onShow(userdata?: any): void {
}
protected onClose(): void {
}
@uiclick
private onRefreshLevel(): void {
DataHelper.level.refreshLevel(DataHelper.level.levelid + 1);
}
@uiclick
private onRefreshStorey(): void {
DataHelper.level.refreshStorey(DataHelper.level.storey + 1);
}
@uiclick
private onRefreshBool(): void {
DataHelper.level.refreshBool(!DataHelper.level.ispassed);
}
@uiclick
private onRefreshData(): void {
DataHelper.level.data = { min: 1, max: 100 };
}
@uiclick
private onRefreshMin(): void {
DataHelper.level.refreshMin(DataHelper.level.data.min + 1);
}
@uiclick
private onRefreshMax(): void {
DataHelper.level.refreshMax(DataHelper.level.data.max - 1);
}
@uiclick
private onRefreshAll(): void {
DataHelper.level.init({ min: 1, max: 100, ispassed: true, levelid: 1, storey: 1 });
}
@uiclick
private onTouchClose(): void {
kunpo.WindowManager.closeWindow(this.name);
}
@bindMethod(Level, data => data.ispassed)
private refreshBool(level: Level): void {
this.lab_ispassed.text = `是否通过:${level.ispassed ? '是' : '否'}`;
}
@bindMethod(Level, data => data.refreshMin)
private refreshMin(level: Level): void {
this.lab_min.text = `对象属性min${level.data.min}`;
}
@bindMethod(Level, data => data.refreshMax)
private refreshMax(level: Level): void {
this.lab_max.text = `对象属性max${level.data.max}`;
}
@bindMethod(Level, data => data.refreshMax)
@bindMethod(Level, data => data.refreshMin)
@bindMethod(Level, data => data.init)
@bindMethod(Level, data => data.data)
private refreshData(level: Level): void {
console.log('触发回调了');
this.lab_min.text = `对象属性min${level.data.min}`;
this.lab_max.text = `对象属性max${level.data.max}`;
}
}

View File

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "09bc366d-d2c6-46d1-b5b1-1f7e75f2be09",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,9 @@
{
"ver": "1.2.0",
"importer": "directory",
"imported": true,
"uuid": "6aabaf5c-aed0-45c8-acc4-9fc86646c3f7",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -0,0 +1,26 @@
/**
* @Author: Gongxh
* @Date: 2025-08-30
* @Description:
*/
import { Level } from "../../../Data/global/Level";
import { fgui, kunpo } from "../../../header";
const { uiheader, uiprop, uicom, uiclick } = kunpo._uidecorator;
const { bindMethod, bindProp } = kunpo.data;
@uicom("Data", "DataItem")
export class DataItem extends fgui.GComponent {
@uiprop
@bindProp(Level, data => data.levelid, (item: fgui.GTextField, value: number, data: Level) => {
item.text = `关卡回调\n关卡:${value}\n层数:${data.storey}`;
})
@bindProp(Level, data => data.storey, (item: fgui.GTextField, value: number, data: Level) => {
item.text = `层数回调\n关卡:${data.levelid}\n层数:${value}`;
})
private lab_level: fgui.GTextField;
public onInit(): void {
}
}

View File

@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8eacd9e1-5ee5-4b18-9e66-4f7e70787268",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@ -97,6 +97,11 @@ export class HomeWindow extends kunpo.Window {
});
}
@uiclick
private onClickData(): void {
kunpo.WindowManager.showWindow("DataWindow");
}
public getHeaderInfo(): kunpo.WindowHeaderInfo {
return kunpo.WindowHeaderInfo.create("WindowHeader", "aaa");
}

View File

@ -14,4 +14,8 @@ export class CustomComponents extends fgui.GComponent {
public onInit(): void {
kunpo.log("CustomComponents onInit");
}
public dispose(): void {
kunpo.log("CustomComponents dispose");
}
}

View File

@ -4,7 +4,7 @@
* @Description: 1 conditon1
*/
import { GlobalEvent } from 'kunpocc-event';
import { DataHelper } from '../Data/DataHelper';
import { DataHelper } from '../Helper/DataHelper';
import { kunpo } from '../header';
import { ConditionType } from './ConditionType';
const { conditionClass } = kunpo._conditionDecorator;

View File

@ -4,7 +4,7 @@
* @Description: 2 condition2
*/
import { GlobalEvent } from 'kunpocc-event';
import { DataHelper } from '../Data/DataHelper';
import { DataHelper } from '../Helper/DataHelper';
import { kunpo } from '../header';
import { ConditionType } from './ConditionType';
const { conditionClass } = kunpo._conditionDecorator;

View File

@ -4,7 +4,7 @@
* @Description: 3 condition3
*/
import { GlobalEvent } from 'kunpocc-event';
import { DataHelper } from '../Data/DataHelper';
import { DataHelper } from '../Helper/DataHelper';
import { kunpo } from '../header';
import { ConditionType } from './ConditionType';
const { conditionClass } = kunpo._conditionDecorator;

View File

@ -4,7 +4,7 @@
* @Description: 4 condition4
*/
import { GlobalEvent } from 'kunpocc-event';
import { DataHelper } from '../Data/DataHelper';
import { DataHelper } from '../Helper/DataHelper';
import { kunpo } from '../header';
import { ConditionType } from './ConditionType';
const { conditionClass } = kunpo._conditionDecorator;

View File

@ -1 +1 @@
{"Basics":{"AlertWindow":{"props":["bg",1,0,"lab_title",1,4,"lab_content",1,5,"btn_close",1,1,"btn_ok",1,3,"btn_cancel",1,2],"callbacks":["onClickBtnClose",1,1,"onClickBtnOk",1,3,"onClickBtnCancel",1,2],"controls":[],"transitions":[]},"ToastWindow":{"props":["toast",1,1,"labTips",2,1,1,"bgMask",1,0],"callbacks":[],"controls":[],"transitions":[]},"WindowHeader":{"props":["btn_close",1,0],"callbacks":[],"controls":[],"transitions":[]},"WindowHeader2":{"props":["btn_close",1,0],"callbacks":[],"controls":[],"transitions":[]}},"Condition":{"ConditionWindow":{"props":["reddot1",1,4,"reddot2",1,5,"btn_condition1",1,6,"btn_condition2",1,7,"btn_condition3",1,8,"btn_condition4",1,9],"callbacks":["onClickBtnClose",1,1,"onClickBtnCondition1",1,6,"onClickBtnCondition2",1,7,"onClickBtnCondition3",1,8,"onClickBtnCondition4",1,9],"controls":[],"transitions":[]}},"Home":{"HomeWindow":{"props":[],"callbacks":["onClickUI",1,6,"onSocketWindow",1,3,"onClickBtnCondition",1,1,"onClickMiniGame",1,4,"onClickBtnHotUpdate",1,5,"onClickLoadBuffer",1,2],"controls":["sta2","sta2","status","status"],"transitions":["t0","t0","t1","t1"]}},"HotUpdate":{"HotUpdateWindow":{"props":["lab_version",1,4,"lab_desc",1,5],"callbacks":["onClickClose",1,3,"onCheckUpdate",1,1,"onStartUpdate",1,2],"controls":[],"transitions":[]}},"MiniGame":{"MiniGameWindow":{"props":["btn_close",1,5,"lab_adid",1,8,"lab_payQuantity",1,9],"callbacks":["onClickBtnClose",1,5,"onClickBtnInitAds",1,2,"onClickBtnPay",1,3],"controls":[],"transitions":[]}},"Socket":{"SocketTestWindow":{"props":["text_input",1,7,"text_input_message",1,9],"callbacks":["onCloseWindow",1,1,"onConnection",1,3,"onDisconnect",1,2,"onSendText",1,4,"onSendBinary",1,5],"controls":[],"transitions":[]}},"Window":{"CloseAllWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"CloseOneWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"CustomComponents":{"props":["n1",1,1],"callbacks":[],"controls":[],"transitions":[]},"HideAllWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"HideOneWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"PopWindow":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"PopWindowHeader1":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"PopWindowHeader2":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"UIBaseWindow":{"props":[],"callbacks":["onClickBtnClose",1,8,"onClickBtnHeader1",1,1,"onClickBtnHeader2",1,2,"onClickBtnEmpty",1,3,"onClickBtnCloseOne",1,4,"onClickBtnCloseAll",1,5,"onClickBtnHideOne",1,6,"onClickBtnHideAll",1,7],"controls":[],"transitions":[]}}}
{"Basics":{"AlertWindow":{"props":["bg",1,0,"lab_title",1,4,"lab_content",1,5,"btn_close",1,1,"btn_ok",1,3,"btn_cancel",1,2],"callbacks":["onClickBtnClose",1,1,"onClickBtnOk",1,3,"onClickBtnCancel",1,2],"controls":[],"transitions":[]},"ToastWindow":{"props":["toast",1,1,"labTips",2,1,1,"bgMask",1,0],"callbacks":[],"controls":[],"transitions":[]},"WindowHeader":{"props":["btn_close",1,0],"callbacks":[],"controls":[],"transitions":[]},"WindowHeader2":{"props":["btn_close",1,0],"callbacks":[],"controls":[],"transitions":[]}},"Condition":{"ConditionWindow":{"props":["reddot1",1,4,"reddot2",1,5,"btn_condition1",1,6,"btn_condition2",1,7,"btn_condition3",1,8,"btn_condition4",1,9],"callbacks":["onClickBtnClose",1,1,"onClickBtnCondition1",1,6,"onClickBtnCondition2",1,7,"onClickBtnCondition3",1,8,"onClickBtnCondition4",1,9],"controls":[],"transitions":[]}},"Data":{"DataItem":{"props":["lab_level",1,1],"callbacks":[],"controls":[],"transitions":[]},"DataWindow":{"props":["lab_level",1,9,"lab_storey",1,10,"lab_min",1,12,"lab_max",1,13,"lab_ispassed",1,11],"callbacks":["onRefreshLevel",1,2,"onRefreshStorey",1,3,"onRefreshBool",1,4,"onRefreshData",1,5,"onRefreshMin",1,6,"onRefreshMax",1,7,"onRefreshAll",1,8,"onTouchClose",1,1],"controls":[],"transitions":[]}},"Home":{"HomeWindow":{"props":[],"callbacks":["onClickUI",1,6,"onSocketWindow",1,3,"onClickBtnCondition",1,1,"onClickMiniGame",1,4,"onClickBtnHotUpdate",1,5,"onClickLoadBuffer",1,2,"onClickData",1,7],"controls":["sta2","sta2","status","status"],"transitions":["t0","t0","t1","t1"]}},"HotUpdate":{"HotUpdateWindow":{"props":["lab_version",1,4,"lab_desc",1,5],"callbacks":["onClickClose",1,3,"onCheckUpdate",1,1,"onStartUpdate",1,2],"controls":[],"transitions":[]}},"MiniGame":{"MiniGameWindow":{"props":["btn_close",1,5,"lab_adid",1,8,"lab_payQuantity",1,9],"callbacks":["onClickBtnClose",1,5,"onClickBtnInitAds",1,2,"onClickBtnPay",1,3],"controls":[],"transitions":[]}},"Socket":{"SocketTestWindow":{"props":["text_input",1,7,"text_input_message",1,9],"callbacks":["onCloseWindow",1,1,"onConnection",1,3,"onDisconnect",1,2,"onSendText",1,4,"onSendBinary",1,5],"controls":[],"transitions":[]}},"Window":{"CloseAllWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"CloseOneWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"CustomComponents":{"props":["n1",1,1],"callbacks":[],"controls":[],"transitions":[]},"HideAllWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"HideOneWindow":{"props":["btn_close",1,2],"callbacks":["onClickBtnClose",1,2],"controls":[],"transitions":[]},"PopWindow":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"PopWindowHeader1":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"PopWindowHeader2":{"props":["btn_close",1,2],"callbacks":["onCloseWindow",1,2],"controls":[],"transitions":[]},"UIBaseWindow":{"props":[],"callbacks":["onClickBtnClose",1,8,"onClickBtnHeader1",1,1,"onClickBtnHeader2",1,2,"onClickBtnEmpty",1,3,"onClickBtnCloseOne",1,4,"onClickBtnCloseAll",1,5,"onClickBtnHideOne",1,6,"onClickBtnHideAll",1,7],"controls":[],"transitions":[]}}}

View File

@ -0,0 +1,13 @@
{
"props": {
"lab_level": {
"name": "lab_level",
"idPath": "n1_c2y1",
"namePath": "lab_level"
}
},
"callbacks": {},
"controls": {},
"transitions": {},
"__version__": "0.0.1"
}

View File

@ -0,0 +1,74 @@
{
"props": {
"lab_level": {
"name": "lab_level",
"idPath": "n28_rrvv",
"namePath": "lab_level"
},
"lab_storey": {
"name": "lab_storey",
"idPath": "n29_rrvv",
"namePath": "lab_storey"
},
"lab_ispassed": {
"name": "lab_bool",
"idPath": "n30_rrvv",
"namePath": "lab_bool"
},
"lab_min": {
"name": "lab_datamin",
"idPath": "n31_rrvv",
"namePath": "lab_datamin"
},
"lab_max": {
"name": "lab_datamax",
"idPath": "n32_rrvv",
"namePath": "lab_datamax"
}
},
"callbacks": {
"onRefreshLevel": {
"name": "btn_refresh_level",
"idPath": "n1_zmnj",
"namePath": "btn_refresh_level"
},
"onRefreshStorey": {
"name": "btn_refresh_storey",
"idPath": "n23_rrvv",
"namePath": "btn_refresh_storey"
},
"onRefreshBool": {
"name": "btn_refresh_bool",
"idPath": "n24_rrvv",
"namePath": "btn_refresh_bool"
},
"onRefreshData": {
"name": "btn_refresh_data",
"idPath": "n25_rrvv",
"namePath": "btn_refresh_data"
},
"onRefreshMin": {
"name": "btn_refresh_min",
"idPath": "n26_rrvv",
"namePath": "btn_refresh_min"
},
"onRefreshMax": {
"name": "btn_refresh_max",
"idPath": "n33_gsjf",
"namePath": "btn_refresh_max"
},
"onRefreshAll": {
"name": "btn_refresh_all",
"idPath": "n27_rrvv",
"namePath": "btn_refresh_all"
},
"onTouchClose": {
"name": "btn_close",
"idPath": "n22_sf8l",
"namePath": "btn_close"
}
},
"controls": {},
"transitions": {},
"__version__": "0.0.1"
}

View File

@ -251,6 +251,11 @@
"name": "btn_ui",
"idPath": "n22_sf8l",
"namePath": "btn_ui"
},
"onClickData": {
"name": "btn_data",
"idPath": "n23_rrvv",
"namePath": "btn_data"
}
},
"controls": {

188
docs/Data.md Normal file
View File

@ -0,0 +1,188 @@
## 数据绑定Data Binding
本库的数据绑定面向“简单直接”的 UI 同步:当数据类的属性被设置或数据类上的公开方法被调用时,触发与之匹配的装饰器回调,用最少样板代码完成 UI 更新。复杂交互(动画、跨模块协作、节流/并发控制)仍建议使用事件系统或显式逻辑处理。
- 优势
- 精简:无需注册/注销一堆事件监听,几行装饰器即可完成 UI 同步
- 类型安全:通过 selector 函数选择数据路径,配合 TS 编译期检查
- 零侵入:数据类只需继承 `DataBase`,代理自动拦截属性设置与方法调用
- 高性能:同一帧内变更合并批量分发(`BatchUpdater`
- 适用边界
- 简单、直接的属性展示或方法触发的轻量级反馈
- 复杂动画链路、跨系统通信、长时流程控制 → 使用事件系统更明确可控
### 基本概念
- 数据类:继承 `DataBase`。属性赋值和公开方法调用都会发出变更通知
- 绑定路径:`ClassName:memberName`,由装饰器通过 selector 推导
- selector 约束:仅支持 `d => d.xxx.yyy``function(d){ return d.xxx.yyy; }` 的直链式访问;不支持动态表达式/可选链/解构
- 触发时机:
- 属性变更:设置新值且与旧值不同才触发(以 `_` 开头的属性被忽略)
- 方法调用:公开方法被调用即触发(`constructor` 与以 `_` 开头的方法被忽略)
- 生命周期:
- 初始化:`data.initializeBindings(this)`FGUI/窗口基类已内置自动调用)
- 清理:`data.cleanupBindings(this)`FGUI 组件 `dispose` 已内置调用)
- 即时更新:装饰器 `immediate` 参数,默认 false为 true 时属性变更会立刻执行一次回调
### API 速览
```ts
import { data } from "kunpocc"; // 实际从 src/data/DataDecorator 导出
import { DataBase } from "kunpocc";
// 属性绑定装饰到“UI字段”上
data.bindProp<T extends DataBase>(
dataClass: new () => T,
selector: (d: T) => any,
callback: (item: any, value?: any, data?: T) => void,
immediate: boolean = false,
)
// 方法绑定装饰到“UI方法”上
data.bindMethod<T extends DataBase>(
dataClass: new () => T,
selector: (d: T) => any, // 选择数据类上的某个“公开方法”
immediate: boolean = false,
)
```
回调参数说明(属性绑定):
- `item`:被装饰的 UI 字段(例如某 `Label`
- `value`:属性新值(仅属性变更时有值)
- `data`:当前数据类实例(便于读取其它字段)
方法绑定回调时机:当目标数据方法被调用后触发,默认不传 `value`,你可以在回调内读取 `data` 的当前状态。
---
### 使用方式(从简到难)
#### 1) 属性装饰器(同一窗口类中合并展示“单个/多个装饰器”)
```ts
import * as fgui from "fairygui-cc";
import { DataBase, data, Window, _uidecorator } from "kunpocc";
const { uiclass, uiprop } = _uidecorator;
// 示例数据类
class Level extends DataBase {
public levelid = 1;
public storey = 1;
}
// 使用窗口(继承 kunpo.Window同一类中分别演示
// A) 单个装饰器 B) 多个装饰器(同一 UI 属性上堆叠多个装饰器)
@uiclass("Window", "Data", "DataWindow")
export class DataWindow extends Window {
// --- A) 单个装饰器:仅响应 storey ---
@uiprop
@data.bindProp(Level, d => d.storey, (item: fgui.GTextField, value: number) => {
item.text = `层数:${value}`;
})
private lab_storey!: fgui.GTextField;
// --- B) 多个装饰器:同一属性上堆叠,分别响应 storey 与 levelid ---
@uiprop
@data.bindProp(Level, d => d.storey, (item: fgui.GTextField, value: number, model: Level) => {
item.text = `关卡:${model.levelid} 层数:${value}`;
})
@data.bindProp(Level, d => d.levelid, (item: fgui.GTextField, value: number, model: Level) => {
item.text = `关卡:${value} 层数:${model.storey}`;
})
private lab_level!: fgui.GTextField;
}
// 触发(示意)
const level = new Level();
level.storey = 2; // 触发 lab_storey 与 lab_level 的第一个装饰器
level.levelid = 10; // 触发 lab_level 的第二个装饰器
```
要点:
- A/B 两段示例均在“窗口类”中,满足“属性装饰器使用窗口”的要求
- “多个装饰器”强调“同一个 UI 属性”上堆叠多个 `@bindProp`,参考 demo 的 `DataWindow.ts``DataItem.ts`
- `immediate` 可按需加在装饰器末参数,设为 true 时“属性变更会立即执行一次回调”(首帧对齐)
#### 2) 方法装饰器(同一自定义组件中合并展示“单个/多个装饰器”)
```ts
import * as fgui from "fairygui-cc";
import { DataBase, data, _uidecorator } from "kunpocc";
const { uicom } = _uidecorator;
// 示例数据类
class Inventory extends DataBase {
public addItem(it: string) { /* ... */ }
public reset() { /* ... */ }
}
// 使用自定义组件(继承 fgui.GComponent同一类中分别演示
// A) 单个方法装饰器 B) 多个方法装饰器(同一 UI 方法上堆叠多个装饰器)
@uicom("Data", "DataItem")
export class DataItem extends fgui.GComponent {
// --- A) 单个方法装饰器:仅响应 addItem ---
@data.bindMethod(Inventory, d => d.addItem)
onAddItem(inv: Inventory) {
// 轻量反馈:刷新一次列表/播放提示
// this.refresh(inv);
}
// --- B) 多个方法装饰器:同一 UI 方法上堆叠,响应 reset 与 addItem ---
@data.bindMethod(Inventory, d => d.reset)
@data.bindMethod(Inventory, d => d.addItem)
onAny(inv: Inventory) {
// 统一刷新
// this.refresh(inv);
}
}
// 触发(示意)
const inv = new Inventory();
inv.addItem("Potion"); // 触发 onAddItem 与 onAny
inv.reset(); // 仅触发 onAny
```
要点:
- 示例放在“自定义组件”中,满足“方法装饰器使用自定义组件”的要求
- “多个装饰器”强调“同一个 UI 方法”上堆叠多个 `@bindMethod`,参考 demo 的 `DataWindow.ts``refreshData`
- 方法绑定不关心入参与返回值,调用即触发;复杂动画仍建议事件系统编排
---
### 生命周期与集成
- FGUI 自定义组件与窗口基类已内置:
- 构造onConstruct`data.initializeBindings(this)`
- `dispose()` 时:`data.cleanupBindings(this)`
- 非集成场景(如普通类):
- 初始化后手动调用 `data.initializeBindings(this)`
- 销毁前调用 `data.cleanupBindings(this)`
参考代码位置:
- `src/ui/ComponentExtendHelper.ts` 内在构造与 `dispose` 中已自动处理
- `src/fgui/WindowBase.ts` 同样在生命周期中调用了初始化
---
### 最佳实践与注意事项
- 关注点内聚:属性绑定仅做“值到视图”的即时映射;复杂动作用事件系统
- 避免回调里递归触发同一数据的写操作,防止无意义的级联更新
- 以 `_``__` 开头的属性/方法不会触发绑定,不要用作绑定目标
- `selector` 必须是直链式访问:`d => d.foo.bar`,不要写表达式/调用/条件语句
- `immediate` 用于是否立即触发更新,默认关闭以避免不必要开销
---
### 调试小贴士
```ts
import { BindManager } from "kunpocc";
BindManager.getAllPaths(); // 查看已注册的所有路径
BindManager.getTotalBindingCount(); // 总绑定数量
```
如果绑定无效:检查数据类是否继承了 `DataBase``selector` 是否为直链式、是否在生命周期内完成了 `initializeBindings`

View File

@ -1,6 +1,6 @@
{
"name": "kunpocc",
"version": "1.1.8",
"version": "1.1.9",
"description": "基于creator3.0+的kunpocc库",
"main": "./dist/kunpocc.cjs",
"module": "./dist/kunpocc.mjs",

125
src/data/BatchUpdater.ts Normal file
View File

@ -0,0 +1,125 @@
import { BindManager } from './BindManager';
import { BindInfo, IDataEvent } from './types';
/**
*
*/
interface PendingUpdate {
/** 绑定器信息 */
info: BindInfo;
/** 路径变化事件 */
event: IDataEvent;
}
/**
*
*
*/
export class BatchUpdater {
/** 挂起的更新任务集合 */
private static pendingUpdates = new Map<string, PendingUpdate>();
/** 是否已调度批量更新 */
private static isScheduled = false;
/** 立即更新的绑定器集合(防重复触发) */
private static immediateUpdates = new Set<string>();
/**
*
* @param event
*/
public static notifyBindings(event: IDataEvent): void {
const bindInfos = BindManager.getMatchingBindings(event.path);
for (const info of bindInfos) {
if (info.immediate) {
// 立即更新模式
this.executeImmediateUpdate(info, event);
} else {
// 批量更新模式
this.scheduleBatchUpdate(info, event);
}
}
}
/**
*
* @param info
* @param event
*/
private static executeImmediateUpdate(info: BindInfo, event: IDataEvent): void {
const key = this.getBindingKey(info);
// 防止同一帧内重复执行
if (this.immediateUpdates.has(key)) {
return;
}
this.immediateUpdates.add(key);
try {
info.callback.call(info.target, event);
} catch (error) {
console.error(`绑定器回调执行失败,路径:${event.path}`, error);
} finally {
// 下一帧清理标记
setTimeout(() => {
this.immediateUpdates.delete(key);
}, 0);
}
}
/**
*
* @param info
* @param event
*/
private static scheduleBatchUpdate(info: BindInfo, event: IDataEvent): void {
const key = this.getBindingKey(info);
// 同一绑定器在一帧内只保留最后一次更新
this.pendingUpdates.set(key, { info, event });
// 如果还未调度,则调度一次批量更新
if (!this.isScheduled) {
this.isScheduled = true;
setTimeout(() => this.flush(), 0);
}
}
/**
*
*/
private static flush(): void {
// 先复制当前状态
// 清理原始状态
// 安全处理复制的数据
const updates = Array.from(this.pendingUpdates.values());
this.pendingUpdates.clear();
this.isScheduled = false;
for (const { info, event } of updates) {
try {
let target = info.target;
if (info.isMethod) {
info.callback.call(target, event.target);
} else {
info.callback.call(target, target[info.prop], event.isProp ? event.value : undefined, event.target);
}
} catch (error) {
// 单个绑定器异常不影响其他绑定器的执行
console.error(`绑定器回调执行失败,路径:${event.path}`, error);
}
}
}
/**
*
* @param binding
*/
private static getBindingKey(info: BindInfo): string {
if (info.isMethod) {
return `${info.target.__data_id__}:${info.prop.toString()}`;
}
return `${info.target.__data_id__}:${info.prop.toString()}:${info.path}`;
}
}

90
src/data/BindManager.ts Normal file
View File

@ -0,0 +1,90 @@
import { BindInfo } from './types';
export class BindManager {
/**
*
*
*
*/
private static _bindings = new Map<string, Set<BindInfo>>();
static addBinding(info: BindInfo): void {
// 延迟初始化:在第一次添加绑定时确保实例已正确初始化
this._ensureInstanceInitialized(info.target);
if (!this._bindings.has(info.path)) {
this._bindings.set(info.path, new Set());
}
this._bindings.get(info.path)!.add(info);
}
/**
*
* 使@uicom@uiclass
*/
private static _ensureInstanceInitialized(instance: any): void {
// 如果已经初始化过,直接返回
if (instance.__bindings_initialized__) {
return;
}
const ctor = instance.constructor as any;
// 生成唯一ID
if (!instance.__data_id__) {
instance.__data_id__ = `${ctor.name}-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
}
// 标记已初始化
instance.__bindings_initialized__ = true;
}
static removeBinding(info: BindInfo): void {
const pathBindings = this._bindings.get(info.path);
if (pathBindings) {
pathBindings.delete(info);
if (pathBindings.size === 0) {
this._bindings.delete(info.path);
}
}
}
static getMatchingBindings(path: string): Set<BindInfo> {
// 直接通过路径获取绑定器集合,避免不必要的遍历
return this._bindings.get(path) || new Set<BindInfo>();
}
static cleanup(target: any): void {
const toRemove: BindInfo[] = [];
for (const bindingSet of this._bindings.values()) {
bindingSet.forEach(binding => {
if (binding.target === target) {
toRemove.push(binding);
}
});
}
toRemove.forEach(binding => this.removeBinding(binding));
}
static clearAll(): void {
this._bindings.clear();
}
/************** 调试用 **************/
static getBindingsForPath(path: string): Set<BindInfo> {
return this._bindings.get(path) || new Set();
}
static getTotalBindingCount(): number {
let count = 0;
for (const bindingSet of this._bindings.values()) {
count += bindingSet.size;
}
return count;
}
static getAllPaths(): string[] {
return Array.from(this._bindings.keys());
}
/************** 调试用 **************/
}

45
src/data/DataBase.ts Normal file
View File

@ -0,0 +1,45 @@
import { BindManager } from './BindManager';
import { ProxyObject } from './ProxyHandler';
import { BindInfo } from './types';
/**
*
* Proxy 访
*/
export class DataBase {
/** 响应式对象唯一标识 */
private __data_id__: string;
/** 绑定器集合 */
private __watchers__: Set<BindInfo>;
/** 是否已销毁 */
private __destroyed__: boolean = false;
constructor() {
// 返回包装后的对象,自动使用 constructor.name
return ProxyObject(this);
}
/**
*
*/
public destroy(): void {
this.__destroyed__ = true;
this.__watchers__.clear();
BindManager.cleanup(this);
}
/**
* ID
*/
public getDataId(): string {
return this.__data_id__;
}
/**
*
*/
public isDestroyed(): boolean {
return this.__destroyed__;
}
}

125
src/data/DataDecorator.ts Normal file
View File

@ -0,0 +1,125 @@
import { BindManager } from './BindManager';
import { DataBase } from './DataBase';
import { BindInfo } from './types';
export namespace data {
/**
* @bindAPI
*/
const BIND_METADATA_KEY = Symbol('__bind_metadata__');
/**
*
* @param instance
*/
export function initializeBindings(instance: any) {
const ctor = instance.constructor as any;
const binds = ctor[BIND_METADATA_KEY] || [];
for (const info of binds) {
const bindInfo: BindInfo = {
target: instance,
prop: info.prop,
callback: info.isMethod ? instance[info.prop].bind(instance) : info.callback.bind(instance),
path: info.path,
immediate: info.immediate,
isMethod: info.isMethod
};
// 注册到全局绑定器管理器BindManager 会自动处理延迟初始化)
BindManager.addBinding(bindInfo);
}
}
/**
*
*
* @param dataClass
* @param selector
* @param callback.item: 当前装饰的类属性
* @param callback.value: 如果绑定的是数据属性value为数据属性值undefined
* @param callback.data: 数据类实例
* @param immediate true
*/
export function bindProp<T extends DataBase>(dataClass: new () => T, selector: (data: T) => any, callback: (item: any, value?: any, data?: T) => void, immediate: boolean = false) {
return function (target: any, prop: string | symbol) {
// 解析路径
const path = `${dataClass.name}:${extractPathFromSelector(selector)}`;
// console.log('绑定属性的监听路径', path);
let ctor = target.constructor;
// 存储绑定元数据
ctor[BIND_METADATA_KEY] = ctor[BIND_METADATA_KEY] || [];
ctor[BIND_METADATA_KEY].push({
target: null,
prop,
callback,
path: path,
immediate,
isMethod: false
});
};
}
/**
*
*
*
* @param dataClass
* @param selector
* @param immediate false
*/
export function bindMethod<T extends DataBase>(dataClass: new () => T, selector: (data: T) => any, immediate: boolean = false) {
return function (target: any, method: string | symbol, descriptor?: PropertyDescriptor) {
// 解析路径
const path = `${dataClass.name}:${extractPathFromSelector(selector)}`;
// console.log('绑定方法的监听路径', path);
// 存储绑定元数据
let ctor = target.constructor;
ctor[BIND_METADATA_KEY] = ctor[BIND_METADATA_KEY] || [];
ctor[BIND_METADATA_KEY].push({
target: null,
prop: method,
callback: descriptor!.value,
path: path,
immediate,
isMethod: true
});
return descriptor;
};
}
/**
*
* TypeScript编译期检查使用
*/
function extractPathFromSelector(selector: Function): string {
const fnString = selector.toString();
// 匹配箭头函数: data => data.property.path
let match = fnString.match(/\w+\s*=>\s*\w+\.(.+)/);
if (!match) {
// 匹配普通函数: function(data) { return data.property.path; }
match = fnString.match(/return\s+\w+\.(.+);?\s*}/);
}
if (!match) {
throw new Error('无效的路径选择器函数,请使用 data => data.property.path 或 function(data) { return data.property.path; } 的形式');
}
return match[1].trim();
}
/**
*
* @param target
*/
export function cleanupBindings(target: any): void {
BindManager.cleanup(target);
if (target.__watchers__) {
target.__watchers__.clear();
}
}
}

167
src/data/ProxyHandler.ts Normal file
View File

@ -0,0 +1,167 @@
/**
* @Author: Gongxh
* @Date: 2025-08-26
* @Description:
*/
import { BatchUpdater } from "./BatchUpdater";
import { IDataEvent } from "./types";
// 全局唯一ID生成器
let nextId = 1;
function notifyChange(path: string, dataInstance: any, value?: any, isProp: boolean = false) {
const event: IDataEvent = {
path,
target: dataInstance,
value,
isProp
};
// console.log('发出的通知路径', path);
BatchUpdater.notifyBindings(event);
}
/**
*
*/
function handlePropertySet(dataInstance: any, target: any, prop: string | symbol, value: any): boolean {
let oldValue = Reflect.get(target, prop);
if (oldValue === value) {
// 数据不变 无需通知 无需修改
return true;
}
let propname = prop.toString();
// 排除以_和__开头的方法
if (propname.startsWith('_')) {
Reflect.set(target, prop, value);
return true;
}
const result = Reflect.set(target, prop, value);
if (!dataInstance.__destroyed__) {
const path = `${dataInstance.constructor.name}:${propname}`;
notifyChange(path, dataInstance, value, true);
}
return result;
}
/**
* constructor
*/
function handleMethodGet(dataInstance: any, target: any, prop: string | symbol, receiver: any): any {
const value = Reflect.get(target, prop, receiver);
const propname = prop.toString();
// 如果不是函数,直接返回
if (typeof value !== 'function') {
return value;
}
// 排除constructor方法
if (propname === 'constructor') {
return value;
}
// 排除以_和__开头的方法
if (propname.startsWith('_')) {
return value;
}
// 如果已经包装过,直接返回
if (value.__kunpo_wrapped__) {
return value;
}
const wrappedFunc = new Proxy(value, {
apply: function (target: any, thisArg: any, args: any[]): any {
// console.log('拦截到函数调用:', propname, args);
let result = Reflect.apply(target, thisArg, args);
const path = `${dataInstance.constructor.name}:${propname}`;
notifyChange(path, dataInstance);
return result;
}
});
// 标记已包装,避免重复包装
Object.defineProperty(wrappedFunc, '__kunpo_wrapped__', {
value: true,
writable: false,
enumerable: false,
configurable: false
});
// 缓存包装后的函数
Reflect.set(target, prop, wrappedFunc);
return wrappedFunc;
}
/**
*
*/
function setupInternalProperties(dataInstance: any): void {
// 使用构造函数名作为类名,与装饰器保持一致
const className = dataInstance.constructor.name;
dataInstance.__data_id__ = `${className}-${nextId++}`;
dataInstance.__watchers__ = new Set();
// 定义不可枚举的内部属性,防止代码混淆问题
Object.defineProperty(dataInstance, '__data_id__', {
value: dataInstance.__data_id__,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(dataInstance, '__watchers__', {
value: dataInstance.__watchers__,
writable: false,
enumerable: false,
configurable: false
});
Object.defineProperty(dataInstance, '__destroyed__', {
value: false,
writable: true,
enumerable: false,
configurable: false
});
}
/**
* _和__开头的属性
*/
function initializeDirectProperties(dataInstance: any): void {
for (const key in dataInstance) {
// 跳过以_和__开头的属性和函数
if (key.startsWith('_') || typeof dataInstance[key] === 'function') {
continue;
}
const value = dataInstance[key];
if (typeof value === 'object' && value !== null && !(value as any).__data_id__) {
// 简单标记为已包装,但不创建深层代理
Object.defineProperty(value, '__data_id__', {
value: `${dataInstance.__data_id__}:${key}`,
writable: false,
enumerable: false,
configurable: false
});
}
}
}
export function ProxyObject(dataInstance: any) {
const handler = {
set: (target: any, prop: string | symbol, value: any): boolean => {
return handlePropertySet(dataInstance, target, prop, value)
},
get: (target: any, prop: string | symbol, receiver: any): any => {
return handleMethodGet(dataInstance, target, prop, receiver)
}
};
setupInternalProperties(dataInstance);
initializeDirectProperties(dataInstance);
return new Proxy(dataInstance, handler);
}

31
src/data/types.ts Normal file
View File

@ -0,0 +1,31 @@
/**
*
*/
export interface BindInfo {
/** 监听目标对象 */
target: any;
/** 属性或方法名 */
prop: string | symbol;
/** 监听的路径 */
path: string;
/** 回调函数 */
callback: Function;
/** 是否立即更新 */
immediate: boolean;
/** 是否为方法监听 */
isMethod: boolean;
}
/**
*
*/
export interface IDataEvent {
/** 变化的属性路径 */
path: string;
/** 目标对象 */
target: any;
/** 是否是属性变化 */
isProp?: boolean;
/** 变化后的值 */
value?: any;
}

View File

@ -5,6 +5,7 @@
*/
import { GComponent } from "fairygui-cc";
import { data } from "../data/DataDecorator";
import { Screen } from "../global/Screen";
import { AdapterType, WindowType } from "../ui/header";
import { IWindow } from "../ui/IWindow";
@ -49,6 +50,8 @@ export abstract class WindowBase extends GComponent implements IWindow {
// 窗口自身也要设置是否吞噬触摸
this.opaque = swallowTouch;
this.bgAlpha = bgAlpha;
// 初始化数据绑定(如果有 @dataclass 装饰器)
data.initializeBindings(this);
this.onInit();
}
@ -79,6 +82,8 @@ export abstract class WindowBase extends GComponent implements IWindow {
* @internal
*/
public _close(): void {
// 窗口关闭时 清理绑定信息
data.cleanupBindings(this);
this.onClose();
this.dispose();
}

View File

@ -44,3 +44,7 @@ export { BytedanceCommon } from "./minigame/bytedance/BytedanceCommon";
export { MiniHelper } from "./minigame/MiniHelper";
export { WechatCommon } from "./minigame/wechat/WechatCommon";
/** 数据绑定相关 - 强类型数据绑定系统 */
export { DataBase } from './data/DataBase';
export { data } from './data/DataDecorator';

View File

@ -4,6 +4,7 @@
* @Description:
*/
import { UIObjectFactory } from "fairygui-cc";
import { data } from "../data/DataDecorator";
import { debug } from "../tool/log";
import { PropsHelper } from "./PropsHelper";
import { _uidecorator } from "./UIDecorator";
@ -51,9 +52,19 @@ export class ComponentExtendHelper {
// 自定义组件扩展
const onConstruct = function (this: any): void {
PropsHelper.serializeProps(this, pkg, name);
// 初始化数据绑定(如果有 @dataclass 装饰器)
data.initializeBindings(this);
this.onInit && this.onInit();
};
ctor.prototype.onConstruct = onConstruct;
const dispose = ctor.prototype.dispose
const newDispose = function (this: any): void {
data.cleanupBindings(this);
dispose.call(this);
};
ctor.prototype.dispose = newDispose;
// 自定义组件扩展
UIObjectFactory.setExtension(`ui://${pkg}/${name}`, ctor);
}

View File

@ -62,11 +62,11 @@ export namespace _uidecorator {
*/
export function uiclass(groupName: string, pkgName: string, name: string, bundle?: string): Function {
/** target 类的构造函数 */
return function (ctor: any): void {
// debug(`uiclass >${JSON.stringify(res)}<`);
// debug(`uiclass prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uiclassMap.set(ctor, {
ctor: ctor,
return function (ctor: any): any {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
uiclassMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@ -78,8 +78,9 @@ export namespace _uidecorator {
bundle: bundle || "",
},
});
// 首次引擎注册完成后 动态注册窗口
// 首次引擎注册完成后 动态注册窗口,使用实际的构造函数
_registerFinish && WindowManager.dynamicRegisterWindow(ctor, groupName, pkgName, name, bundle || "");
return ctor;
};
}
@ -110,10 +111,12 @@ export namespace _uidecorator {
* @param {string} name
*/
export function uicom(pkg: string, name: string): Function {
return function (ctor: any): void {
return function (ctor: any): any {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
// log(`pkg:【${pkg}】 uicom prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uicomponentMap.set(ctor, {
ctor: ctor,
uicomponentMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@ -123,8 +126,9 @@ export namespace _uidecorator {
name: name,
}
});
// 首次引擎注册完成后 动态注册自定义组件
// 首次引擎注册完成后 动态注册自定义组件,使用实际的构造函数
_registerFinish && ComponentExtendHelper.dynamicRegister(ctor, pkg, name);
return ctor;
};
}
@ -155,9 +159,11 @@ export namespace _uidecorator {
*/
export function uiheader(pkg: string, name: string, bundle?: string): Function {
return function (ctor: any): void {
// 检查是否有原始构造函数引用(由其他装饰器如 @dataclass 提供)
const originalCtor = ctor;
// log(`pkg:【${pkg}】 uiheader prop >${JSON.stringify(ctor[UIPropMeta] || {})}<`);
uiheaderMap.set(ctor, {
ctor: ctor,
uiheaderMap.set(originalCtor, {
ctor: ctor, // 存储实际的构造函数(可能被包装过)
props: ctor[UIPropMeta] || null,
callbacks: ctor[UICBMeta] || null,
controls: ctor[UIControlMeta] || null,
@ -168,7 +174,7 @@ export namespace _uidecorator {
bundle: bundle || "",
}
});
// 首次引擎注册完成后 动态注册窗口header
// 首次引擎注册完成后 动态注册窗口header,使用实际的构造函数
_registerFinish && WindowManager.dynamicRegisterHeader(ctor, pkg, name, bundle || "");
return ctor;
};