mirror of
https://github.com/Gongxh0901/kunpolibrary
synced 2025-09-13 09:58:46 +00:00
UI模块添加数据绑定装饰器
1.添加数据基类,子类自动添加代理,数据变化自动通知 2.支持同属性多装饰器
This commit is contained in:
parent
b62a4af8db
commit
e48011d941
55
.claudeignore
Normal file
55
.claudeignore
Normal 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
|
@ -1,3 +1,6 @@
|
||||
## 1.1.9
|
||||
- 新增数据模块
|
||||
|
||||
## 1.1.8
|
||||
- 支持窗口和自定义组件动态注册 (用来兼容代码在bundle中,代码加载顺序导致的问题)
|
||||
|
||||
|
@ -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)
|
||||
|
11
demo/FguiCreator3.8/assets/Data/DataItem.xml
Normal file
11
demo/FguiCreator3.8/assets/Data/DataItem.xml
Normal 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>
|
60
demo/FguiCreator3.8/assets/Data/DataWindow.xml
Normal file
60
demo/FguiCreator3.8/assets/Data/DataWindow.xml
Normal 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="对象属性min:1">
|
||||
<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="对象属性max:2">
|
||||
<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>
|
8
demo/FguiCreator3.8/assets/Data/package.xml
Normal file
8
demo/FguiCreator3.8/assets/Data/package.xml
Normal 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>
|
@ -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"/>
|
||||
|
BIN
demo/assets/resources/ui/Data.bin
Normal file
BIN
demo/assets/resources/ui/Data.bin
Normal file
Binary file not shown.
12
demo/assets/resources/ui/Data.bin.meta
Normal file
12
demo/assets/resources/ui/Data.bin.meta
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"ver": "1.0.3",
|
||||
"importer": "buffer",
|
||||
"imported": true,
|
||||
"uuid": "db12f6e3-dc8e-45aa-ba9f-90edf99cd732",
|
||||
"files": [
|
||||
".bin",
|
||||
".json"
|
||||
],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
Binary file not shown.
9
demo/assets/script/Data/global.meta
Normal file
9
demo/assets/script/Data/global.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "1bd1aa0a-5c4d-4425-afaa-2a46c0f02a42",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
62
demo/assets/script/Data/global/Level.ts
Normal file
62
demo/assets/script/Data/global/Level.ts
Normal 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;
|
||||
}
|
||||
}
|
9
demo/assets/script/Data/global/Level.ts.meta
Normal file
9
demo/assets/script/Data/global/Level.ts.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "632daee7-7c2c-4baf-a4dd-d1a12ad91169",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
9
demo/assets/script/Data/runtime.meta
Normal file
9
demo/assets/script/Data/runtime.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "3edce213-52a5-403f-b57f-52da9d7bcbce",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
@ -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 {
|
@ -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": {}
|
@ -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")
|
||||
|
9
demo/assets/script/UI/Data.meta
Normal file
9
demo/assets/script/UI/Data.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "d0395b78-5d23-4c60-91ca-8851d9eddc27",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
127
demo/assets/script/UI/Data/DataWindow.ts
Normal file
127
demo/assets/script/UI/Data/DataWindow.ts
Normal 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}`;
|
||||
}
|
||||
}
|
9
demo/assets/script/UI/Data/DataWindow.ts.meta
Normal file
9
demo/assets/script/UI/Data/DataWindow.ts.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "09bc366d-d2c6-46d1-b5b1-1f7e75f2be09",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
9
demo/assets/script/UI/Data/Items.meta
Normal file
9
demo/assets/script/UI/Data/Items.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.2.0",
|
||||
"importer": "directory",
|
||||
"imported": true,
|
||||
"uuid": "6aabaf5c-aed0-45c8-acc4-9fc86646c3f7",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
26
demo/assets/script/UI/Data/Items/DataItem.ts
Normal file
26
demo/assets/script/UI/Data/Items/DataItem.ts
Normal 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 {
|
||||
}
|
||||
}
|
9
demo/assets/script/UI/Data/Items/DataItem.ts.meta
Normal file
9
demo/assets/script/UI/Data/Items/DataItem.ts.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "4.0.24",
|
||||
"importer": "typescript",
|
||||
"imported": true,
|
||||
"uuid": "8eacd9e1-5ee5-4b18-9e66-4f7e70787268",
|
||||
"files": [],
|
||||
"subMetas": {},
|
||||
"userData": {}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
@ -14,4 +14,8 @@ export class CustomComponents extends fgui.GComponent {
|
||||
public onInit(): void {
|
||||
kunpo.log("CustomComponents onInit");
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
kunpo.log("CustomComponents dispose");
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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":[]}}}
|
13
demo/extensions-config/fgui/Data/DataItem.json
Normal file
13
demo/extensions-config/fgui/Data/DataItem.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"props": {
|
||||
"lab_level": {
|
||||
"name": "lab_level",
|
||||
"idPath": "n1_c2y1",
|
||||
"namePath": "lab_level"
|
||||
}
|
||||
},
|
||||
"callbacks": {},
|
||||
"controls": {},
|
||||
"transitions": {},
|
||||
"__version__": "0.0.1"
|
||||
}
|
74
demo/extensions-config/fgui/Data/DataWindow.json
Normal file
74
demo/extensions-config/fgui/Data/DataWindow.json
Normal 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"
|
||||
}
|
@ -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
188
docs/Data.md
Normal 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`。
|
||||
|
@ -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
125
src/data/BatchUpdater.ts
Normal 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
90
src/data/BindManager.ts
Normal 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
45
src/data/DataBase.ts
Normal 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
125
src/data/DataDecorator.ts
Normal 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
167
src/data/ProxyHandler.ts
Normal 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
31
src/data/types.ts
Normal 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;
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user