diff --git a/.claudeignore b/.claudeignore
new file mode 100644
index 0000000..dfca827
--- /dev/null
+++ b/.claudeignore
@@ -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
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 16284a5..6871a86 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,6 @@
+## 1.1.9
+- 新增数据模块
+
## 1.1.8
- 支持窗口和自定义组件动态注册 (用来兼容代码在bundle中,代码加载顺序导致的问题)
diff --git a/README.md b/README.md
index 3645391..da65a89 100644
--- a/README.md
+++ b/README.md
@@ -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)
diff --git a/demo/FguiCreator3.8/assets/Data/DataItem.xml b/demo/FguiCreator3.8/assets/Data/DataItem.xml
new file mode 100644
index 0000000..5ad9577
--- /dev/null
+++ b/demo/FguiCreator3.8/assets/Data/DataItem.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/FguiCreator3.8/assets/Data/DataWindow.xml b/demo/FguiCreator3.8/assets/Data/DataWindow.xml
new file mode 100644
index 0000000..0fabcc7
--- /dev/null
+++ b/demo/FguiCreator3.8/assets/Data/DataWindow.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/FguiCreator3.8/assets/Data/package.xml b/demo/FguiCreator3.8/assets/Data/package.xml
new file mode 100644
index 0000000..020a43b
--- /dev/null
+++ b/demo/FguiCreator3.8/assets/Data/package.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/FguiCreator3.8/assets/Home/HomeWindow.xml b/demo/FguiCreator3.8/assets/Home/HomeWindow.xml
index bb5e661..9a85ea3 100644
--- a/demo/FguiCreator3.8/assets/Home/HomeWindow.xml
+++ b/demo/FguiCreator3.8/assets/Home/HomeWindow.xml
@@ -30,6 +30,10 @@
+
+
+
+
diff --git a/demo/assets/resources/ui/Data.bin b/demo/assets/resources/ui/Data.bin
new file mode 100644
index 0000000..9517cc7
Binary files /dev/null and b/demo/assets/resources/ui/Data.bin differ
diff --git a/demo/assets/resources/ui/Data.bin.meta b/demo/assets/resources/ui/Data.bin.meta
new file mode 100644
index 0000000..2ddd975
--- /dev/null
+++ b/demo/assets/resources/ui/Data.bin.meta
@@ -0,0 +1,12 @@
+{
+ "ver": "1.0.3",
+ "importer": "buffer",
+ "imported": true,
+ "uuid": "db12f6e3-dc8e-45aa-ba9f-90edf99cd732",
+ "files": [
+ ".bin",
+ ".json"
+ ],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/resources/ui/manual/Home.bin b/demo/assets/resources/ui/manual/Home.bin
index 7e0f3fb..ef798e1 100644
Binary files a/demo/assets/resources/ui/manual/Home.bin and b/demo/assets/resources/ui/manual/Home.bin differ
diff --git a/demo/assets/script/Data/global.meta b/demo/assets/script/Data/global.meta
new file mode 100644
index 0000000..a9079a8
--- /dev/null
+++ b/demo/assets/script/Data/global.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "1.2.0",
+ "importer": "directory",
+ "imported": true,
+ "uuid": "1bd1aa0a-5c4d-4425-afaa-2a46c0f02a42",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/Data/global/Level.ts b/demo/assets/script/Data/global/Level.ts
new file mode 100644
index 0000000..4497caf
--- /dev/null
+++ b/demo/assets/script/Data/global/Level.ts
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/demo/assets/script/Data/global/Level.ts.meta b/demo/assets/script/Data/global/Level.ts.meta
new file mode 100644
index 0000000..8c57b4a
--- /dev/null
+++ b/demo/assets/script/Data/global/Level.ts.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "4.0.24",
+ "importer": "typescript",
+ "imported": true,
+ "uuid": "632daee7-7c2c-4baf-a4dd-d1a12ad91169",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/Data/runtime.meta b/demo/assets/script/Data/runtime.meta
new file mode 100644
index 0000000..1b9b56c
--- /dev/null
+++ b/demo/assets/script/Data/runtime.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "1.2.0",
+ "importer": "directory",
+ "imported": true,
+ "uuid": "3edce213-52a5-403f-b57f-52da9d7bcbce",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/Data/DataHelper.ts b/demo/assets/script/Helper/DataHelper.ts
similarity index 86%
rename from demo/assets/script/Data/DataHelper.ts
rename to demo/assets/script/Helper/DataHelper.ts
index bebfd9e..e4f4428 100644
--- a/demo/assets/script/Data/DataHelper.ts
+++ b/demo/assets/script/Helper/DataHelper.ts
@@ -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 = new Map();
public static getValue(key: string, defaultValue: T): T {
diff --git a/demo/assets/script/Data/DataHelper.ts.meta b/demo/assets/script/Helper/DataHelper.ts.meta
similarity index 70%
rename from demo/assets/script/Data/DataHelper.ts.meta
rename to demo/assets/script/Helper/DataHelper.ts.meta
index 6e597f5..89cd21a 100644
--- a/demo/assets/script/Data/DataHelper.ts.meta
+++ b/demo/assets/script/Helper/DataHelper.ts.meta
@@ -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": {}
diff --git a/demo/assets/script/UI/Condition/ConditionWindow.ts b/demo/assets/script/UI/Condition/ConditionWindow.ts
index c3c01fe..ef0b85d 100644
--- a/demo/assets/script/UI/Condition/ConditionWindow.ts
+++ b/demo/assets/script/UI/Condition/ConditionWindow.ts
@@ -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")
diff --git a/demo/assets/script/UI/Data.meta b/demo/assets/script/UI/Data.meta
new file mode 100644
index 0000000..3d13696
--- /dev/null
+++ b/demo/assets/script/UI/Data.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "1.2.0",
+ "importer": "directory",
+ "imported": true,
+ "uuid": "d0395b78-5d23-4c60-91ca-8851d9eddc27",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/UI/Data/DataWindow.ts b/demo/assets/script/UI/Data/DataWindow.ts
new file mode 100644
index 0000000..79e237c
--- /dev/null
+++ b/demo/assets/script/UI/Data/DataWindow.ts
@@ -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}`;
+ }
+}
diff --git a/demo/assets/script/UI/Data/DataWindow.ts.meta b/demo/assets/script/UI/Data/DataWindow.ts.meta
new file mode 100644
index 0000000..3031e16
--- /dev/null
+++ b/demo/assets/script/UI/Data/DataWindow.ts.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "4.0.24",
+ "importer": "typescript",
+ "imported": true,
+ "uuid": "09bc366d-d2c6-46d1-b5b1-1f7e75f2be09",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/UI/Data/Items.meta b/demo/assets/script/UI/Data/Items.meta
new file mode 100644
index 0000000..0418404
--- /dev/null
+++ b/demo/assets/script/UI/Data/Items.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "1.2.0",
+ "importer": "directory",
+ "imported": true,
+ "uuid": "6aabaf5c-aed0-45c8-acc4-9fc86646c3f7",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/UI/Data/Items/DataItem.ts b/demo/assets/script/UI/Data/Items/DataItem.ts
new file mode 100644
index 0000000..9bc6c5f
--- /dev/null
+++ b/demo/assets/script/UI/Data/Items/DataItem.ts
@@ -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 {
+ }
+}
diff --git a/demo/assets/script/UI/Data/Items/DataItem.ts.meta b/demo/assets/script/UI/Data/Items/DataItem.ts.meta
new file mode 100644
index 0000000..84a4191
--- /dev/null
+++ b/demo/assets/script/UI/Data/Items/DataItem.ts.meta
@@ -0,0 +1,9 @@
+{
+ "ver": "4.0.24",
+ "importer": "typescript",
+ "imported": true,
+ "uuid": "8eacd9e1-5ee5-4b18-9e66-4f7e70787268",
+ "files": [],
+ "subMetas": {},
+ "userData": {}
+}
diff --git a/demo/assets/script/UI/HomeWindow.ts b/demo/assets/script/UI/HomeWindow.ts
index 3428abd..efdde56 100644
--- a/demo/assets/script/UI/HomeWindow.ts
+++ b/demo/assets/script/UI/HomeWindow.ts
@@ -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");
}
diff --git a/demo/assets/script/UI/Window/Components/CustomComponents.ts b/demo/assets/script/UI/Window/Components/CustomComponents.ts
index 1d8e00e..0225d10 100644
--- a/demo/assets/script/UI/Window/Components/CustomComponents.ts
+++ b/demo/assets/script/UI/Window/Components/CustomComponents.ts
@@ -14,4 +14,8 @@ export class CustomComponents extends fgui.GComponent {
public onInit(): void {
kunpo.log("CustomComponents onInit");
}
+
+ public dispose(): void {
+ kunpo.log("CustomComponents dispose");
+ }
}
diff --git a/demo/assets/script/condition/Condition1.ts b/demo/assets/script/condition/Condition1.ts
index 78605f1..986e747 100644
--- a/demo/assets/script/condition/Condition1.ts
+++ b/demo/assets/script/condition/Condition1.ts
@@ -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;
diff --git a/demo/assets/script/condition/Condition2.ts b/demo/assets/script/condition/Condition2.ts
index 6fa3db0..1eceed4 100644
--- a/demo/assets/script/condition/Condition2.ts
+++ b/demo/assets/script/condition/Condition2.ts
@@ -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;
diff --git a/demo/assets/script/condition/Condition3.ts b/demo/assets/script/condition/Condition3.ts
index 14ab0e3..17344bd 100644
--- a/demo/assets/script/condition/Condition3.ts
+++ b/demo/assets/script/condition/Condition3.ts
@@ -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;
diff --git a/demo/assets/script/condition/Condition4.ts b/demo/assets/script/condition/Condition4.ts
index 1747681..b2005ff 100644
--- a/demo/assets/script/condition/Condition4.ts
+++ b/demo/assets/script/condition/Condition4.ts
@@ -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;
diff --git a/demo/assets/uiconfig/ui_config.json b/demo/assets/uiconfig/ui_config.json
index f4a886d..a093fdf 100644
--- a/demo/assets/uiconfig/ui_config.json
+++ b/demo/assets/uiconfig/ui_config.json
@@ -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":[]}}}
\ No newline at end of file
+{"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":[]}}}
\ No newline at end of file
diff --git a/demo/extensions-config/fgui/Data/DataItem.json b/demo/extensions-config/fgui/Data/DataItem.json
new file mode 100644
index 0000000..dcfd1aa
--- /dev/null
+++ b/demo/extensions-config/fgui/Data/DataItem.json
@@ -0,0 +1,13 @@
+{
+ "props": {
+ "lab_level": {
+ "name": "lab_level",
+ "idPath": "n1_c2y1",
+ "namePath": "lab_level"
+ }
+ },
+ "callbacks": {},
+ "controls": {},
+ "transitions": {},
+ "__version__": "0.0.1"
+}
\ No newline at end of file
diff --git a/demo/extensions-config/fgui/Data/DataWindow.json b/demo/extensions-config/fgui/Data/DataWindow.json
new file mode 100644
index 0000000..768724f
--- /dev/null
+++ b/demo/extensions-config/fgui/Data/DataWindow.json
@@ -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"
+}
\ No newline at end of file
diff --git a/demo/extensions-config/fgui/Home/HomeWindow.json b/demo/extensions-config/fgui/Home/HomeWindow.json
index b8325ac..8d3e400 100644
--- a/demo/extensions-config/fgui/Home/HomeWindow.json
+++ b/demo/extensions-config/fgui/Home/HomeWindow.json
@@ -251,6 +251,11 @@
"name": "btn_ui",
"idPath": "n22_sf8l",
"namePath": "btn_ui"
+ },
+ "onClickData": {
+ "name": "btn_data",
+ "idPath": "n23_rrvv",
+ "namePath": "btn_data"
}
},
"controls": {
diff --git a/docs/Data.md b/docs/Data.md
new file mode 100644
index 0000000..86eb17a
--- /dev/null
+++ b/docs/Data.md
@@ -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(
+ dataClass: new () => T,
+ selector: (d: T) => any,
+ callback: (item: any, value?: any, data?: T) => void,
+ immediate: boolean = false,
+)
+
+// 方法绑定(装饰到“UI方法”上)
+data.bindMethod(
+ 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`。
+
diff --git a/package.json b/package.json
index 3504f93..fc3627b 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/data/BatchUpdater.ts b/src/data/BatchUpdater.ts
new file mode 100644
index 0000000..d58a25f
--- /dev/null
+++ b/src/data/BatchUpdater.ts
@@ -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();
+ /** 是否已调度批量更新 */
+ private static isScheduled = false;
+ /** 立即更新的绑定器集合(防重复触发) */
+ private static immediateUpdates = new Set();
+
+ /**
+ * 通知所有匹配的绑定器
+ * @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}`;
+ }
+}
\ No newline at end of file
diff --git a/src/data/BindManager.ts b/src/data/BindManager.ts
new file mode 100644
index 0000000..f9a93be
--- /dev/null
+++ b/src/data/BindManager.ts
@@ -0,0 +1,90 @@
+import { BindInfo } from './types';
+
+export class BindManager {
+ /**
+ * 绑定器集合
+ * 键:路径
+ * 值:绑定器集合
+ */
+ private static _bindings = new Map>();
+
+ 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 {
+ // 直接通过路径获取绑定器集合,避免不必要的遍历
+ return this._bindings.get(path) || new Set();
+ }
+
+ 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 {
+ 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());
+ }
+ /************** 调试用 **************/
+}
\ No newline at end of file
diff --git a/src/data/DataBase.ts b/src/data/DataBase.ts
new file mode 100644
index 0000000..b4d08f6
--- /dev/null
+++ b/src/data/DataBase.ts
@@ -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;
+ /** 是否已销毁 */
+ 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__;
+ }
+}
\ No newline at end of file
diff --git a/src/data/DataDecorator.ts b/src/data/DataDecorator.ts
new file mode 100644
index 0000000..5727dd5
--- /dev/null
+++ b/src/data/DataDecorator.ts
@@ -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(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(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();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/data/ProxyHandler.ts b/src/data/ProxyHandler.ts
new file mode 100644
index 0000000..8bb1e88
--- /dev/null
+++ b/src/data/ProxyHandler.ts
@@ -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);
+}
\ No newline at end of file
diff --git a/src/data/types.ts b/src/data/types.ts
new file mode 100644
index 0000000..41fff3f
--- /dev/null
+++ b/src/data/types.ts
@@ -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;
+}
\ No newline at end of file
diff --git a/src/fgui/WindowBase.ts b/src/fgui/WindowBase.ts
index fe16e17..74b3c15 100644
--- a/src/fgui/WindowBase.ts
+++ b/src/fgui/WindowBase.ts
@@ -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();
}
diff --git a/src/kunpocc.ts b/src/kunpocc.ts
index b78161d..39413f6 100644
--- a/src/kunpocc.ts
+++ b/src/kunpocc.ts
@@ -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';
+
diff --git a/src/ui/ComponentExtendHelper.ts b/src/ui/ComponentExtendHelper.ts
index b0e3f8c..0d04898 100644
--- a/src/ui/ComponentExtendHelper.ts
+++ b/src/ui/ComponentExtendHelper.ts
@@ -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);
}
diff --git a/src/ui/UIDecorator.ts b/src/ui/UIDecorator.ts
index 677dcf6..955d2ff 100644
--- a/src/ui/UIDecorator.ts
+++ b/src/ui/UIDecorator.ts
@@ -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;
};