import { _decorator, Component, Label, Sprite } from 'cc'; const { ccclass, property } = _decorator; /** * PlayerInfoView: 响应式视图组件(自包含,便于 Cocos 编辑器挂载)。 * * 设计取舍:不 import framework/store-binder,让本组件保持"纯 cc.Component 子类 + * 极简内联 binder"的单文件结构,方便通过 funplay-cocos-mcp 的 mount 流程挂到 prefab * (挂脚本路径与持久化限制见 memory `prefab-persist-mount-script` / * `cocos-mcp-mount-script-path`)。 * * 实际订阅契约与 framework/ui/store-binder 一致: * - bindTo(binder) 接收外部 binder 实例 * - 调用方持有 binder,内部 subscribe + 自动刷新 Label.string * * 后续如需让 framework/store-binder 与本组件复用同一份逻辑,可把 subscribe * 抽到一个 cc-agnostic 的 pure module;目前以"单文件自包含"落地,优先确保编辑器挂载稳定。 */ /** 内联极简 binder(语义与 framework/ui/store-binder 对齐) */ interface ViewBinder { bind(store: ViewStore, slot: string, cb: (value: T) => void): void; unbindAll(): void; } /** 极简 store 契约:暴露 subscribe 与 value */ interface ViewStore { subscribe(cb: (value: T, prev: T) => void): () => void; value: T; } function createViewBinder(): ViewBinder { const unsubs: Array<() => void> = []; return { bind(store: ViewStore, _slot: string, cb: (value: T) => void) { // 首次同步:bind 时立即用当前 value 跑一次 callback try { cb(store.value); } catch (e) { console.error('[PlayerInfoView] initial callback error:', e); } const unsub = store.subscribe((value) => cb(value)); unsubs.push(unsub); }, unbindAll() { while (unsubs.length > 0) { const u = unsubs.pop(); try { u?.(); } catch { /* 退订隔离 */ } } }, }; } @ccclass('PlayerInfoView') export class PlayerInfoView extends Component { @property(Label) nicknameLabel: Label = null!; @property(Label) beanLabel: Label = null!; @property(Sprite) avatarSprite: Sprite = null!; private binder: ViewBinder | null = null; private session: { player: { state: ViewStore } } | null = null; /** * 由场景初始化代码调用,把视图绑定到 session.player.state。 * 调用方负责传入 session(典型用法:PlatformSession)。 * * 兼容性:session.player.state 需有 subscribe/value 两个成员——framework * 的 ReadonlyReactive 已具备。 */ bindTo(session: { player: { state: ViewStore } }): void { this.unbindIfNeeded(); this.session = session; this.binder = createViewBinder(); this.binder.bind(session.player.state, 'state', (p: any) => this.applyPlayer(p)); } onDestroy(): void { this.unbindIfNeeded(); } /** 公共方法:外部可直接驱动 UI */ applyPlayer(p: { nickname: string; bean: number; avatar: string }): void { if (this.nicknameLabel) this.nicknameLabel.string = p.nickname ?? ''; if (this.beanLabel) this.beanLabel.string = String(p.bean ?? 0); // avatar 加载留给业务侧扩展 } private unbindIfNeeded(): void { this.binder?.unbindAll(); this.binder = null; this.session = null; } }