drgame-cc 1.0.15 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/drscript/BtnExtra.ts +17 -0
  2. package/drscript/Define.ts +22 -0
  3. package/drscript/ExitWin.ts +39 -0
  4. package/drscript/Info.ts +75 -0
  5. package/drscript/Loader.ts +89 -0
  6. package/drscript/PlatformAdapter.ts +165 -0
  7. package/drscript/README - /345/211/257/346/234/254.md" +814 -0
  8. package/drscript/README.md +814 -0
  9. package/drscript/ad/AdManager.ts +174 -0
  10. package/drscript/ad/AdManager.ts.meta +10 -0
  11. package/drscript/ad/AdSlot.ts +44 -0
  12. package/drscript/ad/AdSlot.ts.meta +10 -0
  13. package/drscript/ad/AggregateAdProvider.ts +148 -0
  14. package/drscript/ad/AggregateAdProvider.ts.meta +10 -0
  15. package/drscript/ad/IAdProvider.ts +39 -0
  16. package/drscript/ad/IAdProvider.ts.meta +10 -0
  17. package/drscript/ad/MockAdProvider.ts +36 -0
  18. package/drscript/ad/MockAdProvider.ts.meta +10 -0
  19. package/drscript/ad/SelfAdProvider.ts +41 -0
  20. package/drscript/ad/SelfAdProvider.ts.meta +10 -0
  21. package/drscript/ad-sdk/README.md +344 -0
  22. package/drscript/ad-sdk/README.md.meta +6 -0
  23. package/drscript/ad-sdk/ad-sdk.d.ts +59 -0
  24. package/drscript/ad-sdk/ad-sdk.d.ts.meta +6 -0
  25. package/drscript/ad-sdk/ad-sdk.js +322 -0
  26. package/drscript/ad-sdk/ad-sdk.js.meta +10 -0
  27. package/drscript/ad-sdk/adapters/xiaomi-adapter.d.ts +29 -0
  28. package/drscript/ad-sdk/adapters/xiaomi-adapter.d.ts.meta +6 -0
  29. package/drscript/ad-sdk/adapters/xiaomi-adapter.js +449 -0
  30. package/drscript/ad-sdk/adapters/xiaomi-adapter.js.meta +10 -0
  31. package/drscript/ad-sdk/adapters.meta +13 -0
  32. package/drscript/ad-sdk/event-emitter.d.ts +12 -0
  33. package/drscript/ad-sdk/event-emitter.d.ts.meta +6 -0
  34. package/drscript/ad-sdk/event-emitter.js +65 -0
  35. package/drscript/ad-sdk/event-emitter.js.meta +10 -0
  36. package/drscript/ad-sdk/leaderboard/leaderboard.d.ts +23 -0
  37. package/drscript/ad-sdk/leaderboard/leaderboard.d.ts.meta +6 -0
  38. package/drscript/ad-sdk/leaderboard/leaderboard.js +193 -0
  39. package/drscript/ad-sdk/leaderboard/leaderboard.js.meta +10 -0
  40. package/drscript/ad-sdk/leaderboard.meta +13 -0
  41. package/drscript/ad-sdk/package.json +46 -0
  42. package/drscript/ad-sdk/package.json.meta +6 -0
  43. package/drscript/ad-sdk/reporters/reporter.d.ts +40 -0
  44. package/drscript/ad-sdk/reporters/reporter.d.ts.meta +6 -0
  45. package/drscript/ad-sdk/reporters/reporter.js +198 -0
  46. package/drscript/ad-sdk/reporters/reporter.js.meta +10 -0
  47. package/drscript/ad-sdk/reporters/superset-lewo.js +413 -0
  48. package/drscript/ad-sdk/reporters/superset-lewo.js.meta +10 -0
  49. package/drscript/ad-sdk/reporters.meta +13 -0
  50. package/drscript/ad-sdk/types/enums.d.ts +64 -0
  51. package/drscript/ad-sdk/types/enums.d.ts.meta +6 -0
  52. package/drscript/ad-sdk/types/enums.js +111 -0
  53. package/drscript/ad-sdk/types/enums.js.meta +10 -0
  54. package/drscript/ad-sdk/types/index.d.ts +6 -0
  55. package/drscript/ad-sdk/types/index.d.ts.meta +6 -0
  56. package/drscript/ad-sdk/types/index.js +15 -0
  57. package/drscript/ad-sdk/types/index.js.meta +10 -0
  58. package/drscript/ad-sdk/types/interfaces.d.ts +333 -0
  59. package/drscript/ad-sdk/types/interfaces.d.ts.meta +6 -0
  60. package/drscript/ad-sdk/types.meta +13 -0
  61. package/drscript/config/TvUiConfig.ts +155 -0
  62. package/drscript/config/TvUiConfig.ts.meta +10 -0
  63. package/drscript/config/TvUiConfigManager.ts +133 -0
  64. package/drscript/config/TvUiConfigManager.ts.meta +10 -0
  65. package/drscript/core/BackManager.ts +40 -0
  66. package/drscript/core/BackManager.ts.meta +10 -0
  67. package/drscript/core/NativeBridge.ts +137 -0
  68. package/drscript/core/NativeBridge.ts.meta +10 -0
  69. package/drscript/core/TvUi.ts +69 -0
  70. package/drscript/core/TvUi.ts.meta +10 -0
  71. package/drscript/core/TvUiRoot.ts +137 -0
  72. package/drscript/core/TvUiRoot.ts.meta +10 -0
  73. package/drscript/focus/FocusItem.ts +57 -0
  74. package/drscript/focus/FocusItem.ts.meta +10 -0
  75. package/drscript/focus/FocusManager.ts +538 -0
  76. package/drscript/focus/FocusManager.ts.meta +10 -0
  77. package/drscript/focus/FocusScope.ts +10 -0
  78. package/drscript/focus/FocusScope.ts.meta +10 -0
  79. package/drscript/focus/FocusVisualLayer.ts +238 -0
  80. package/drscript/focus/FocusVisualLayer.ts.meta +10 -0
  81. package/drscript/icon/IconConfig.ts +7 -0
  82. package/drscript/icon/IconConfig.ts.meta +10 -0
  83. package/drscript/icon/IconManager.ts +97 -0
  84. package/drscript/icon/IconManager.ts.meta +10 -0
  85. package/drscript/input/KeyCodeAdapter.ts +23 -0
  86. package/drscript/input/KeyCodeAdapter.ts.meta +10 -0
  87. package/drscript/input/RemoteInputManager.ts +105 -0
  88. package/drscript/input/RemoteInputManager.ts.meta +10 -0
  89. package/drscript/toast/ToastComponent.ts +35 -0
  90. package/drscript/toast/ToastComponent.ts.meta +10 -0
  91. package/drscript/toast/ToastManager.ts +66 -0
  92. package/drscript/toast/ToastManager.ts.meta +10 -0
  93. package/drscript/toast/TvExitConfirmDialog.ts +334 -0
  94. package/drscript/toast/TvExitConfirmDialog.ts.meta +10 -0
  95. package/drscript/toast/UIFunctions.ts +150 -0
  96. package/drscript/toast/UIFunctions.ts.meta +10 -0
  97. package/package.json +2 -1
@@ -0,0 +1,814 @@
1
+ # TV UI Framework 接入指南
2
+
3
+ 本文档面向 Cocos Creator 2.4.x 项目开发人员,说明如何在 TV 项目中接入 `drres` 框架,并完成焦点、图标、遥控输入、返回键和聚合广告能力。
4
+
5
+ 当前框架已经接入聚合广告 SDK:真实广告统一走 `AggregateAdProvider + vendor/ad-sdk`。业务代码只调用 slot 或项目封装,不直接关心具体广告渠道。
6
+
7
+ ## 适用场景
8
+
9
+ - Cocos Creator 2.4.x + TypeScript。
10
+ - 小米 TV / Android TV / 1920x1080 横屏项目。
11
+ - 遥控器方向键、确认键、返回键交互。
12
+ - 需要复用全局焦点、图标跟随、弹窗 scope、返回键和广告 slot 的小游戏项目。
13
+
14
+ 核心原则:
15
+
16
+ - 焦点状态只由 `FocusManager` 管理。
17
+ - 图标是全局焦点视觉的一部分,不挂在业务按钮上。
18
+ - 广告只暴露 slot,不让业务代码关心具体渠道。
19
+ - 真实广告优先扩展 `vendor/ad-sdk/adapters/`,业务调用保持不变。
20
+ - 后续项目只认 `scripts/drres/core/TvUi`,不再兼容旧 TV 工具链。
21
+
22
+ ## 目录结构
23
+
24
+ 复制框架时至少带上两部分:
25
+
26
+ ```text
27
+ assets/scripts/drres/
28
+ config/
29
+ focus/
30
+ icon/
31
+ ad/
32
+ input/
33
+ core/
34
+ vendor/ad-sdk/
35
+
36
+ assets/resources/drres/
37
+ config/default.json
38
+ icons/pointer-yellow-60.png
39
+ icons/pointer-blue-60.png
40
+ icons/pointer-pink-60.png
41
+ ```
42
+
43
+ 模块职责:
44
+
45
+ - `core/TvUi.ts`:唯一公共入口,导出 `root/config/focus/icon/ads/input/api/back`。
46
+ - `core/TvUiRoot.ts`:创建全局常驻节点,初始化配置、焦点视觉、广告和遥控输入。
47
+ - `focus/`:焦点注册、移动、确认、scope、全局视觉层。
48
+ - `icon/`:根据配置加载本地或远程图标并缓存。
49
+ - `ad/`:slot 广告、多渠道 provider、mock fallback。
50
+ - `vendor/ad-sdk/`:聚合广告 SDK 和小米 adapter。
51
+ - `input/`:遥控器 keyCode 适配和输入分发。
52
+ - `config/`:默认配置、项目覆盖配置、本地配置合并。
53
+
54
+ ## 接入顺序
55
+
56
+ ### 1. 复制框架目录
57
+
58
+ 把 `assets/scripts/drres` 和 `assets/resources/drres` 复制到目标项目,并确保 `.meta` 文件一起提交。Cocos 项目中 `.meta` 不可缺,否则资源 UUID 会变化。
59
+
60
+ ### 2. 初始化 TvUi
61
+
62
+ 在项目入口或平台初始化位置引入:
63
+
64
+ ```ts
65
+ import TvUi from "../scripts/drres/core/TvUi";
66
+ ```
67
+
68
+ 推荐封装一个项目级初始化方法,避免重复初始化:
69
+
70
+ ```ts
71
+ static tvUiInited = false;
72
+
73
+ static initTvUi() {
74
+ if (Platform.tvUiInited) return;
75
+ Platform.tvUiInited = true;
76
+
77
+ TvUi.init({
78
+ cpId: "ppl",
79
+ version: Consts.GAME_VERSION,
80
+ designWidth: 1920,
81
+ designHeight: 1080,
82
+ enableCursor: true,
83
+ configPath: "drres/config/default",
84
+ focus: {
85
+ enabled: true,
86
+ defaultIconKey: "default"
87
+ },
88
+ adSdk: {
89
+ platform: "xiaomi"
90
+ },
91
+ ads: {
92
+ reward: {
93
+ enabled: true,
94
+ mode: "reward",
95
+ channels: [
96
+ { type: "aggregate", priority: 1, reward_type: "reward" },
97
+ { type: "mock", priority: 100 }
98
+ ]
99
+ },
100
+ popup: {
101
+ enabled: true,
102
+ mode: "popup",
103
+ channels: [
104
+ { type: "aggregate", priority: 1 },
105
+ { type: "mock", priority: 100 }
106
+ ]
107
+ },
108
+ game: {
109
+ enabled: true,
110
+ mode: "popup",
111
+ channels: [
112
+ { type: "aggregate", priority: 1 },
113
+ { type: "mock", priority: 100 }
114
+ ]
115
+ }
116
+ }
117
+ });
118
+ }
119
+ ```
120
+
121
+ `ppl` 项目参考:`ppl/assets/framework/Platform.ts` 的 `Platform.initTvUi()`。
122
+
123
+ `TvUi.init()` 会自动:
124
+
125
+ - 创建并常驻 `TvUiRoot`。
126
+ - 加载 `resources/drres/config/default.json`。
127
+ - 初始化焦点层、图标、广告、遥控输入。
128
+ - 暴露 `window.TvUi` 和 `window.drres`。
129
+ - 注册生命周期回调到原生 bridge。
130
+
131
+ ### 3. 准备默认配置
132
+
133
+ 默认配置位于:
134
+
135
+ ```text
136
+ assets/resources/drres/config/default.json
137
+ ```
138
+
139
+ 项目可以在 `TvUi.init()` 里覆盖配置,也可以修改默认配置。推荐把公共默认值放在 `default.json`,项目差异放在初始化参数里。
140
+
141
+ ## 焦点接入
142
+
143
+ 焦点接入分三种场景:普通页面、复杂页面、弹窗。
144
+
145
+ ### 普通页面:自动注册按钮
146
+
147
+ 如果页面上的可操作元素都是 Cocos `cc.Button`,直接注册整个页面根节点:
148
+
149
+ ```ts
150
+ onShown() {
151
+ Platform.initTvUi();
152
+ TvUi.focus.reset();
153
+ TvUi.focus.pushScope("main", this.node);
154
+ TvUi.focus.registerButtons(this.node);
155
+ TvUi.focus.focusNode(this.findButtonByHandler("click_play"));
156
+ }
157
+
158
+ onHidden() {
159
+ TvUi.focus.unregisterByRoot(this.node);
160
+ TvUi.focus.popScope("main");
161
+ }
162
+ ```
163
+
164
+ 注册后:
165
+
166
+ - 方向键会按节点位置自动寻找下一个焦点。
167
+ - 确认键会触发按钮 `clickEvents`。
168
+ - 焦点节点会按默认 `focusScale` 放大。
169
+ - 全局图标会跟随当前焦点。
170
+
171
+ `ppl` 主界面参考:
172
+
173
+ ```ts
174
+ setupTvControls() {
175
+ Platform.initTvUi();
176
+ TvUi.focus.reset();
177
+ TvUi.focus.pushScope("main", this.node);
178
+ TvUi.focus.registerButtons(this.node);
179
+ this.focusPlayButton();
180
+ TvUi.back.setFallback(() => {
181
+ TvGlobalExit.showConfirm();
182
+ return true;
183
+ });
184
+ }
185
+ ```
186
+
187
+ ### 复杂页面:手动注册焦点
188
+
189
+ 如果页面布局复杂,或者要指定方向关系、图标、点击逻辑,用 `register` 手动注册:
190
+
191
+ ```ts
192
+ TvUi.focus.register(startButton, {
193
+ root: this.node,
194
+ focusId: "main_start",
195
+ iconKey: "yellow",
196
+ up: "main_rank",
197
+ down: "main_sign",
198
+ left: "main_lottery",
199
+ right: "main_shop",
200
+ focusScale: 1.08,
201
+ onClick: () => {
202
+ this.click_play();
203
+ },
204
+ onFocus: () => {
205
+ // 可选:业务高亮
206
+ },
207
+ onBlur: () => {
208
+ // 可选:取消业务高亮
209
+ }
210
+ });
211
+ ```
212
+
213
+ 指定焦点:
214
+
215
+ ```ts
216
+ TvUi.focus.focusNode(startButton);
217
+ TvUi.focus.focus("main_start");
218
+ ```
219
+
220
+ 清理页面焦点:
221
+
222
+ ```ts
223
+ TvUi.focus.unregisterByRoot(this.node);
224
+ ```
225
+
226
+ 复杂 TV 页面推荐显式配置 `up/down/left/right`,减少自动坐标推断跳错。
227
+
228
+ ### FocusItem 组件
229
+
230
+ 需要在编辑器里声明方向关系时,可以给节点挂 `FocusItem`:
231
+
232
+ ```ts
233
+ @property
234
+ focusId: string = "";
235
+
236
+ @property
237
+ iconKey: string = "";
238
+
239
+ @property
240
+ up: string = "";
241
+ @property
242
+ down: string = "";
243
+ @property
244
+ left: string = "";
245
+ @property
246
+ right: string = "";
247
+ ```
248
+
249
+ `FocusItem` 只声明焦点关系和图标 key,不负责画焦点框,也不负责画图标。
250
+
251
+ ### 弹窗:独立 scope
252
+
253
+ 弹窗打开时要把焦点限制在弹窗内,关闭后恢复上一层焦点:
254
+
255
+ ```ts
256
+ onShown() {
257
+ TvUi.focus.pushScope("dialog:shop", this.node);
258
+ TvUi.focus.registerButtons(this.node);
259
+ TvUi.focus.focusNode(this.closeButton.node);
260
+ TvUi.back.push(this.onBack);
261
+ }
262
+
263
+ onHidden() {
264
+ TvUi.focus.unregisterByRoot(this.node);
265
+ TvUi.focus.popScope("dialog:shop");
266
+ TvUi.back.remove(this.onBack);
267
+ }
268
+
269
+ private onBack = () => {
270
+ this.click_close();
271
+ return true;
272
+ };
273
+ ```
274
+
275
+ `ppl` 已封装弹窗焦点和返回键:
276
+
277
+ ```ts
278
+ this._tvCleanup = bindTvDialogControls(this, this.click_close, {
279
+ firstFocusNode: this.closeButton.node
280
+ });
281
+ ```
282
+
283
+ 关闭弹窗时调用:
284
+
285
+ ```ts
286
+ if (this._tvCleanup) {
287
+ this._tvCleanup();
288
+ this._tvCleanup = null;
289
+ }
290
+ ```
291
+
292
+ `ppl` 参考:
293
+
294
+ - `ppl/assets/Game/Scripts/Info.ts` 的 `bindTvDialogControls()`。
295
+ - `LoseDialog`、`ShopDialog`、`SignDialog`、`WinDialog` 的 `onShown()`。
296
+
297
+ ### 返回键
298
+
299
+ 主界面 fallback:
300
+
301
+ ```ts
302
+ TvUi.back.setFallback(() => {
303
+ TvGlobalExit.showConfirm();
304
+ return true;
305
+ });
306
+ ```
307
+
308
+ 弹窗返回:
309
+
310
+ ```ts
311
+ const backHandler = () => {
312
+ this.click_close();
313
+ return true;
314
+ };
315
+
316
+ TvUi.back.push(backHandler);
317
+ TvUi.back.remove(backHandler);
318
+ ```
319
+
320
+ `return true` 表示已处理返回键;`return false` 表示继续交给下一层 handler 或 fallback。
321
+
322
+ ## 图标配置
323
+
324
+ 默认图标位于:
325
+
326
+ ```text
327
+ assets/resources/drres/icons/
328
+ pointer-yellow-60.png
329
+ pointer-blue-60.png
330
+ pointer-pink-60.png
331
+ ```
332
+
333
+ 默认配置在 `assets/resources/drres/config/default.json`:
334
+
335
+ ```json
336
+ {
337
+ "focus": {
338
+ "enabled": true,
339
+ "frameEnabled": false,
340
+ "defaultIconKey": "default",
341
+ "iconPlacement": "bottomRight",
342
+ "iconOffsetX": -20,
343
+ "iconOffsetY": 20
344
+ },
345
+ "icons": {
346
+ "default": {
347
+ "path": "drres/icons/pointer-yellow-60",
348
+ "width": 60,
349
+ "height": 60,
350
+ "offsetX": -20,
351
+ "offsetY": 20
352
+ }
353
+ }
354
+ }
355
+ ```
356
+
357
+ 配置说明:
358
+
359
+ - `path`:`resources` 下的本地图标路径,不带扩展名。
360
+ - `url`:远程图标 URL。
361
+ - `width/height`:图标显示尺寸。
362
+ - `offsetX/offsetY`:当前图标自己的偏移,优先级高于 `focus.iconOffsetX/Y`。
363
+ - `iconPlacement`:支持 `topRight`、`bottomRight`。
364
+ - `frameEnabled`:是否绘制全局焦点框。当前默认 `false`,只显示图标和按钮自身缩放。
365
+
366
+ 切换默认图标:
367
+
368
+ ```ts
369
+ TvUi.icon.setIconTheme("blue");
370
+ ```
371
+
372
+ 运行时更新配置并保存到本地:
373
+
374
+ ```ts
375
+ TvUi.config.updateConfig({
376
+ focus: {
377
+ defaultIconKey: "pink"
378
+ }
379
+ }, true);
380
+ ```
381
+
382
+ ## 广告接入
383
+
384
+ ### 调用原则
385
+
386
+ 业务代码不要直接调原生广告,也不要直接调具体平台 adapter。推荐优先级:
387
+
388
+ 1. 项目已有封装:`showRewardVideo()`、`showTvDialogAd()`、`hideTvDialogAd()`。
389
+ 2. 框架 API:`TvUi.ads.showReward()`、`TvUi.ads.showPopup()`、`TvUi.ads.showSlot()`。
390
+ 3. 新平台能力:扩展 `vendor/ad-sdk/adapters/`。
391
+
392
+ 当前真实广告由 `AggregateAdProvider` 接入 `vendor/ad-sdk`,默认渠道链路:
393
+
394
+ ```text
395
+ aggregate(1) -> mock(100)
396
+ ```
397
+
398
+ 规则:
399
+
400
+ - `enabled === false` 的渠道会跳过。
401
+ - provider 不存在或不可用会尝试下一渠道。
402
+ - provider 返回 `failed` 或 Promise reject 会尝试下一渠道。
403
+ - 奖励广告用户关闭返回 `closed`,触发 `onClose`,不发奖励,不切到下一渠道。
404
+ - 全部真实渠道失败后进入 `mock`。
405
+
406
+ ### Reward 广告
407
+
408
+ 直接调用框架:
409
+
410
+ ```ts
411
+ TvUi.ads.showReward({
412
+ trigger_scene: "shop",
413
+ reward_type: "shop",
414
+ onSuccess: data => {
415
+ giveCoin();
416
+ },
417
+ onClose: data => {
418
+ Toast.make("必须看完广告才能获得奖励");
419
+ },
420
+ onFail: error => {
421
+ Toast.make("广告暂不可用,请稍后再试");
422
+ }
423
+ });
424
+ ```
425
+
426
+ 项目内推荐封装:
427
+
428
+ ```ts
429
+ showRewardVideo("shop", "shop", this.share_succ, this);
430
+ ```
431
+
432
+ `ppl` 封装参考:`ppl/assets/Game/Scripts/Info.ts` 的 `showRewardVideo()`。
433
+
434
+ ### 看视频复活示例
435
+
436
+ 按钮回调:
437
+
438
+ ```ts
439
+ click_revive() {
440
+ this.refreshReviveRound();
441
+ if (this.revive_count >= this.max_revive_count) {
442
+ return;
443
+ }
444
+ showRewardVideo("revive_pop", "extra_life", this.revive, this);
445
+ }
446
+ ```
447
+
448
+ 奖励成功后执行业务复活:
449
+
450
+ ```ts
451
+ revive() {
452
+ this.refreshReviveRound();
453
+ if (this.revive_count >= this.max_revive_count) {
454
+ return;
455
+ }
456
+ LoseDialog.reviveUsedCount++;
457
+ this.revive_count = LoseDialog.reviveUsedCount;
458
+ this.getComponent(View).hide();
459
+ Game.instance.revive();
460
+ }
461
+ ```
462
+
463
+ 完整链路:
464
+
465
+ ```text
466
+ 按钮 click_revive
467
+ -> showRewardVideo("revive_pop", "extra_life", this.revive, this)
468
+ -> TvUi.ads.showReward
469
+ -> AggregateAdProvider
470
+ -> vendor/ad-sdk
471
+ -> 小米 adapter
472
+ -> 原生 requestAd
473
+ -> onReward 后调用 revive
474
+ ```
475
+
476
+ `ppl` 参考:`ppl/assets/Game/Scripts/ui/LoseDialog.ts`。
477
+
478
+ ### Popup 广告
479
+
480
+ 直接调用框架:
481
+
482
+ ```ts
483
+ TvUi.ads.showPopup({
484
+ scene: "home_popup",
485
+ onLoad: data => {
486
+ console.log("popup loaded", data);
487
+ },
488
+ onFail: error => {
489
+ console.log("popup failed", error);
490
+ }
491
+ });
492
+ ```
493
+
494
+ 隐藏 popup:
495
+
496
+ ```ts
497
+ TvUi.ads.hidePopup("home_popup");
498
+ ```
499
+
500
+ 项目内推荐封装:
501
+
502
+ ```ts
503
+ showTvDialogAd("lose");
504
+ hideTvDialogAd("lose");
505
+ ```
506
+
507
+ `ppl` 参考:
508
+
509
+ - `LoseDialog.onShown()` 调 `showTvDialogAd("lose")`。
510
+ - `ShopDialog.onShown()` 调 `showTvDialogAd("shop")`。
511
+ - `WinDialog.onShown()` 调 `showTvDialogAd("win")`。
512
+
513
+ ### 自定义 slot
514
+
515
+ 配置:
516
+
517
+ ```json
518
+ {
519
+ "ads": {
520
+ "home_popup": {
521
+ "enabled": true,
522
+ "mode": "popup",
523
+ "channels": [
524
+ { "type": "aggregate", "priority": 1, "placementId": "home_popup" },
525
+ { "type": "mock", "priority": 100 }
526
+ ]
527
+ }
528
+ }
529
+ }
530
+ ```
531
+
532
+ 调用:
533
+
534
+ ```ts
535
+ TvUi.ads.showSlot("home_popup", {
536
+ scene: "home_popup",
537
+ onLoad: data => {},
538
+ onFail: error => {}
539
+ });
540
+ ```
541
+
542
+ ### 聚合 SDK 配置
543
+
544
+ `AggregateAdProvider` 通过 `vendor/ad-sdk` 统一接入真实广告。当前 SDK 内置小米 adapter。
545
+
546
+ SDK 默认原生 action:
547
+
548
+ ```text
549
+ requestAd
550
+ requestPopupAd
551
+ hidePopupAd
552
+ ```
553
+
554
+ SDK 配置:
555
+
556
+ ```json
557
+ {
558
+ "adSdk": {
559
+ "platform": "xiaomi",
560
+ "adapterConfig": {
561
+ "cpId": "ppl"
562
+ }
563
+ }
564
+ }
565
+ ```
566
+
567
+ channel 配置:
568
+
569
+ ```json
570
+ {
571
+ "type": "aggregate",
572
+ "priority": 1,
573
+ "placementId": "home_popup"
574
+ }
575
+ ```
576
+
577
+ 支持回调:
578
+
579
+ ```text
580
+ onReward
581
+ onAdClosed
582
+ onAdFailed
583
+ onPopupAdLoaded
584
+ onPopupAdClosed
585
+ onPopupAdFailed
586
+ ```
587
+
588
+ 后续新增平台时优先扩展:
589
+
590
+ ```text
591
+ assets/scripts/drres/vendor/ad-sdk/adapters/
592
+ ```
593
+
594
+ 业务和 TV UI slot 调用保持不变。
595
+
596
+ ### 自营 Provider
597
+
598
+ `SelfAdProvider` 适合本地兜底图片:
599
+
600
+ ```json
601
+ {
602
+ "type": "self",
603
+ "priority": 50,
604
+ "imagePath": "drres/ads/home-popup"
605
+ }
606
+ ```
607
+
608
+ 调用时传入 `container`:
609
+
610
+ ```ts
611
+ TvUi.ads.showSlot("home_popup", {
612
+ container: adNode,
613
+ scene: "home_popup"
614
+ });
615
+ ```
616
+
617
+ 说明:当前自营广告主要支持本地 `imagePath`。远程 `imageUrl` 是后续增强项。
618
+
619
+ ### 自定义 Provider
620
+
621
+ 新增 provider:
622
+
623
+ ```ts
624
+ import IAdProvider, { AdRuntimeData, AdShowResult } from "./IAdProvider";
625
+
626
+ export default class CustomAdProvider implements IAdProvider {
627
+ public type: string = "custom";
628
+
629
+ public isAvailable(): boolean {
630
+ return true;
631
+ }
632
+
633
+ public load(data: AdRuntimeData): Promise<AdRuntimeData> {
634
+ return Promise.resolve(data);
635
+ }
636
+
637
+ public show(data: AdRuntimeData): Promise<AdShowResult> {
638
+ return Promise.resolve({
639
+ status: "loaded",
640
+ data: {}
641
+ });
642
+ }
643
+
644
+ public hide(slotId: string): void {
645
+ }
646
+ }
647
+ ```
648
+
649
+ 在 `AdManager.init()` 注册:
650
+
651
+ ```ts
652
+ this.registerProvider(new CustomAdProvider());
653
+ ```
654
+
655
+ 配置:
656
+
657
+ ```json
658
+ { "type": "custom", "priority": 3, "placementId": "custom_slot" }
659
+ ```
660
+
661
+ ## ppl 接入实例
662
+
663
+ `ppl` 项目的接入方式可以直接作为新项目模板:
664
+
665
+ 1. `Platform.login()` 或项目入口调用 `Platform.initTvUi()`。
666
+ 2. 主界面 `Main.setupTvControls()`:
667
+ - 隐藏分享入口。
668
+ - `TvUi.focus.reset()`。
669
+ - `TvUi.focus.pushScope("main", this.node)`。
670
+ - `TvUi.focus.registerButtons(this.node)`。
671
+ - 默认聚焦开始按钮。
672
+ - `TvUi.back.setFallback()` 打开退出确认。
673
+ 3. 打开子弹窗前调用 `suspendTvControlsForChildDialog()`,避免主界面焦点抢回。
674
+ 4. 弹窗内调用 `bindTvDialogControls()` 注册按钮焦点和返回键。
675
+ 5. 弹窗关闭后调用 cleanup,并恢复主界面焦点。
676
+ 6. 奖励广告统一调用 `showRewardVideo(trigger_scene, reward_type, callback, target)`。
677
+ 7. 插屏/浮层广告统一调用 `showTvDialogAd(scene)` 和 `hideTvDialogAd(scene)`。
678
+
679
+ 主界面最小示例:
680
+
681
+ ```ts
682
+ setupTvControls() {
683
+ Platform.initTvUi();
684
+ TvUi.focus.reset();
685
+ TvUi.focus.pushScope("main", this.node);
686
+ TvUi.focus.registerButtons(this.node);
687
+ this.focusPlayButton();
688
+ TvUi.back.setFallback(() => {
689
+ TvGlobalExit.showConfirm();
690
+ return true;
691
+ });
692
+ }
693
+ ```
694
+
695
+ 弹窗最小示例:
696
+
697
+ ```ts
698
+ onShown() {
699
+ showTvDialogAd("shop");
700
+ this._tvCleanup = bindTvDialogControls(this, this.click_close, {
701
+ firstFocusNode: this.closeButton.node
702
+ });
703
+ }
704
+
705
+ onHidden() {
706
+ hideTvDialogAd("shop");
707
+ if (this._tvCleanup) {
708
+ this._tvCleanup();
709
+ this._tvCleanup = null;
710
+ }
711
+ }
712
+ ```
713
+
714
+ 奖励按钮最小示例:
715
+
716
+ ```ts
717
+ click_free() {
718
+ showRewardVideo("shop", "shop", this.share_succ, this);
719
+ }
720
+ ```
721
+
722
+ ## 项目迁移规则
723
+
724
+ 新项目和后续项目只允许使用:
725
+
726
+ ```ts
727
+ import TvUi from "../scripts/drres/core/TvUi";
728
+ ```
729
+
730
+ 禁止新代码使用:
731
+
732
+ ```text
733
+ TvKit
734
+ tvKit
735
+ code/tvkit
736
+ MiAPI
737
+ ```
738
+
739
+ 迁移检查:
740
+
741
+ ```bash
742
+ rg "code/tvkit|TvKit|tvKit|MiAPI" ppl/assets
743
+ rg "scripts/drres/core/TvUi" ppl/assets -g "*.ts"
744
+ ```
745
+
746
+ `ppl/assets/code` 应不存在。所有 TV UI 能力都从 `TvUi` facade 进入。
747
+
748
+ ## 验收清单
749
+
750
+ 基础焦点:
751
+
752
+ - 方向键可以在主界面按钮之间移动。
753
+ - 确认键触发当前焦点按钮点击。
754
+ - 当前焦点图标跟随按钮移动。
755
+ - 图标靠近屏幕边缘时不会超出屏幕。
756
+ - 不显示黄色全局焦点框时,按钮自身缩放/高亮仍正常。
757
+
758
+ 弹窗和返回:
759
+
760
+ - 打开弹窗后焦点限制在弹窗 scope 内。
761
+ - 关闭弹窗后恢复主界面焦点。
762
+ - 返回键在弹窗内关闭弹窗。
763
+ - 主界面返回键打开退出确认或执行项目 fallback。
764
+
765
+ 广告:
766
+
767
+ - 无 `AppBridge` 时真实渠道不可用,最终进入 `mock`。
768
+ - 聚合 SDK 成功时不进入 mock。
769
+ - 聚合 SDK 失败时进入 mock/fallback。
770
+ - 奖励广告用户关闭时触发 `onClose`,不发奖励,不切下一渠道。
771
+
772
+ 静态检查:
773
+
774
+ ```bash
775
+ rg "code/tvkit|TvKit|tvKit|MiAPI" ppl/assets
776
+ rg "XiaomiAdProvide[r]|type: \"xiaom[i]\"|requestAggregateA[d]|requestAggregatePopupA[d]" ppl/assets/scripts/drres ppl/assets/resources/drres/config/default.json ppl/assets/framework/Platform.ts
777
+ git diff --check
778
+ ```
779
+
780
+ ## 常见问题
781
+
782
+ ### 遥控器确认键没有触发按钮
783
+
784
+ 确认按钮是否是 `cc.Button`,并且已经调用 `TvUi.focus.registerButtons(root)` 或 `TvUi.focus.register(node, { onClick })`。
785
+
786
+ ### 焦点跳转不符合预期
787
+
788
+ 复杂布局不要只依赖坐标推断,给关键节点配置 `up/down/left/right`。
789
+
790
+ ### 弹窗打开后主界面按钮还能响应
791
+
792
+ 打开子弹窗前先暂停主界面焦点,弹窗内使用独立 scope,关闭后再恢复主界面焦点。`ppl` 中使用 `suspendTvControlsForChildDialog()` 和 `bindTvDialogControls()`。
793
+
794
+ ### 看视频没有发奖励
795
+
796
+ 确认业务奖励只写在 `onSuccess` 或 `showRewardVideo` 的成功回调里。用户关闭广告只会触发 `onClose`,不会发奖励。
797
+
798
+ ### 真实广告不可用
799
+
800
+ 检查:
801
+
802
+ - `TvUi.api.hasBridge()` 是否为 `true`。
803
+ - 原生是否支持 `requestAd`、`requestPopupAd`、`hidePopupAd`。
804
+ - `adSdk.platform` 是否配置为当前 adapter 支持的平台。
805
+ - 渠道失败后是否已经进入 `mock` fallback。
806
+
807
+ ## 后续增强
808
+
809
+ 当前框架已覆盖 v1 接入主流程,以下能力可按项目需要继续补:
810
+
811
+ - 远程配置拉取和合并。
812
+ - 正式设置页,用于切换图标、调整偏移、开关广告渠道。
813
+ - 远程自营广告图片。
814
+ - 广告曝光、点击、关闭统一埋点上报。