cdp-material-sdk 0.0.8 → 0.0.10

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 (19) hide show
  1. package/dist/hooks/useFieldRegistry.d.ts +36 -0
  2. package/dist/host-react.d.ts +2 -0
  3. package/dist/host-react.js +7 -6
  4. package/dist/index.js +14 -13
  5. package/dist/useFieldRegistry-CwjlA-ja.js +123 -0
  6. package/docs/component-development/README.md +10 -0
  7. package/docs/component-development/recipes//344/275/277/347/224/250Adapter/351/200/202/351/205/215/347/273/204/344/273/266API.md +121 -23
  8. package/docs/component-development/recipes//345/243/260/346/230/216props.md +313 -0
  9. package/docs/component-development/recipes//345/243/260/346/230/216/345/270/203/345/261/200/345/256/271/345/231/250/347/273/204/344/273/266.md +38 -14
  10. package/docs/component-development/recipes//345/243/260/346/230/216/346/217/222/346/247/275.md +4 -2
  11. package/docs/component-development/recipes//345/243/260/346/230/216/346/225/260/346/215/256/345/255/227/346/256/265/347/273/204/344/273/266.md +122 -12
  12. package/docs/component-development/recipes//345/243/260/346/230/216/346/225/260/346/215/256/345/256/271/345/231/250/347/273/204/344/273/266.md +10 -4
  13. package/docs/component-development/recipes//351/205/215/347/275/256Loading/347/255/226/347/225/245.md +96 -0
  14. package/docs/component-development/reference/Manifest/345/255/227/346/256/265/345/217/202/350/200/203.md +2 -1
  15. package/docs/component-development/reference/SDK/345/257/274/345/205/245/350/276/271/347/225/214.md +5 -4
  16. package/docs/component-development/reference/Traits/350/203/275/345/212/233/346/250/241/345/236/213.md +59 -14
  17. package/docs/component-development/reference//345/274/225/346/223/216/345/237/272/347/241/200/350/203/275/345/212/233/346/250/241/345/236/213.md +11 -9
  18. package/package.json +1 -1
  19. package/dist/DataScope-B7ffH5qN.js +0 -106
@@ -0,0 +1,36 @@
1
+ import type { FieldState } from '../context/DataScopeContext';
2
+ /**
3
+ * 字段注册表 Hook —— 数据容器组件的便利工具。
4
+ *
5
+ * 与 `DataScope` 配对使用:DataScope 通过 `registerField` / `unregisterField`
6
+ * 把后代字段的实时状态(隐藏、只读、必填等)上报到容器;本 Hook 提供 Ref 支撑的
7
+ * 注册表实现,避免频繁重渲染。
8
+ *
9
+ * 典型用法:
10
+ * ```tsx
11
+ * const { registerField, unregisterField, getAllFieldStates } = useFieldRegistry();
12
+ *
13
+ * return (
14
+ * <DataScope
15
+ * registerField={registerField}
16
+ * unregisterField={unregisterField}
17
+ * getRecord={getValue}
18
+ * >
19
+ * {children}
20
+ * </DataScope>
21
+ * );
22
+ * ```
23
+ */
24
+ export interface UseFieldRegistryResult {
25
+ /** 注册或更新字段状态。建议在子组件 useEffect 中调用。 */
26
+ registerField: (field: FieldState) => void;
27
+ /** 注销字段。建议在子组件卸载时调用。 */
28
+ unregisterField: (id: string) => void;
29
+ /** 读取单个字段状态。 */
30
+ getFieldState: (id: string) => FieldState | undefined;
31
+ /** 读取全部字段状态快照。 */
32
+ getAllFieldStates: () => FieldState[];
33
+ /** 清空注册表。 */
34
+ clearRegistry: () => void;
35
+ }
36
+ export declare function useFieldRegistry(): UseFieldRegistryResult;
@@ -13,3 +13,5 @@ export { DataScope } from './components/data-scope/DataScope';
13
13
  export type { DataScopeProps } from './components/data-scope/DataScope';
14
14
  export { DataScopeContext } from './context/DataScopeContext';
15
15
  export type { DataScopeContextValue, FieldState, LazyScope, PathResolver } from './context/DataScopeContext';
16
+ export { useFieldRegistry } from './hooks/useFieldRegistry';
17
+ export type { UseFieldRegistryResult } from './hooks/useFieldRegistry';
@@ -1,11 +1,12 @@
1
- import { D as e, a as n, b as o, P as s, r as i, s as r, u, c as C } from "./DataScope-B7ffH5qN.js";
1
+ import { D as t, a as s, b as n, P as o, r as i, s as r, u, c as C, d as D } from "./useFieldRegistry-CwjlA-ja.js";
2
2
  export {
3
- e as DataContainerRuntimeContext,
4
- n as DataScope,
5
- o as DataScopeContext,
6
- s as PageContext,
3
+ t as DataContainerRuntimeContext,
4
+ s as DataScope,
5
+ n as DataScopeContext,
6
+ o as PageContext,
7
7
  i as resolveDataContainerRuntimeValue,
8
8
  r as shouldUseDataContainerRuntimeFallback,
9
9
  u as useDataContainer,
10
- C as useDataContainerApi
10
+ C as useDataContainerApi,
11
+ D as useFieldRegistry
11
12
  };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { C as e, a as t, b as n, c as i, D as o, E, I as T, d as _, L as O, e as N, f as R, N as r, g as A, O as C, h as S, R as l, S as u, i as D, j as m, k as I, l as P, m as d, n as p, o as Y, p as M, q as c, u as g, r as L, v as G, s as v } from "./validateManifest-CYqjvQe_.js";
2
- import { D as K, a as h, b as x, P as V, r as b, s as y, u as F, c as k } from "./DataScope-B7ffH5qN.js";
1
+ import { C as e, a as t, b as n, c as i, D as o, E, I as T, d as _, L as O, e as N, f as R, N as r, g as A, O as C, h as l, R as S, S as u, i as D, j as d, k as m, l as I, m as P, n as p, o as Y, p as M, q as g, u as c, r as L, v as G, s as v } from "./validateManifest-CYqjvQe_.js";
2
+ import { D as K, a as h, b as x, P as V, r as y, s as F, u as b, c as k, d as z } from "./useFieldRegistry-CwjlA-ja.js";
3
3
  export {
4
4
  e as CATEGORY_LABELS,
5
5
  t as COMPONENT_CATEGORY,
@@ -18,25 +18,26 @@ export {
18
18
  r as NOTIFY_ERROR_STRATEGY,
19
19
  A as NOTIFY_TIMING,
20
20
  C as OptionItemSchema,
21
- S as OptionsSchema,
21
+ l as OptionsSchema,
22
22
  V as PageContext,
23
- l as RemoteOptionConfigSchema,
23
+ S as RemoteOptionConfigSchema,
24
24
  u as SLOT_KEY,
25
25
  D as STANDARD_EVENT_KEYS,
26
- m as diagnoseMissingActionImpls,
27
- I as diagnoseMissingStateKeys,
28
- P as isCustomEventKey,
29
- d as isStandardEventKey,
26
+ d as diagnoseMissingActionImpls,
27
+ m as diagnoseMissingStateKeys,
28
+ I as isCustomEventKey,
29
+ P as isStandardEventKey,
30
30
  p as normalizeAdapterEvents,
31
31
  Y as normalizeManifestEvents,
32
32
  M as printSingleValidationResult,
33
- c as printValidationResult,
34
- b as resolveDataContainerRuntimeValue,
35
- y as shouldUseDataContainerRuntimeFallback,
36
- g as useConcurrentLoading,
37
- F as useDataContainer,
33
+ g as printValidationResult,
34
+ y as resolveDataContainerRuntimeValue,
35
+ F as shouldUseDataContainerRuntimeFallback,
36
+ c as useConcurrentLoading,
37
+ b as useDataContainer,
38
38
  k as useDataContainerApi,
39
39
  L as useDualLoading,
40
+ z as useFieldRegistry,
40
41
  G as validateManifest,
41
42
  v as validateManifests
42
43
  };
@@ -0,0 +1,123 @@
1
+ import { createContext as C, useContext as l, useCallback as i, useRef as g, useMemo as j } from "react";
2
+ import { useStore as E } from "zustand";
3
+ import { cloneDeep as f, has as M, set as O } from "lodash-es";
4
+ import { jsx as F } from "react/jsx-runtime";
5
+ const R = C(null), y = C(null), k = "value", m = (t) => t !== null && typeof t == "object" && !Array.isArray(t), p = (t) => Array.isArray(t) && t.every((e) => e === void 0 || m(e)), d = (t, e, n = [k]) => {
6
+ if (!M(t, n)) {
7
+ O(t, n, f(e));
8
+ return;
9
+ }
10
+ if (p(e)) {
11
+ e.forEach((o, r) => {
12
+ d(t, o, [...n, r]);
13
+ });
14
+ return;
15
+ }
16
+ if (m(e))
17
+ for (const [o, r] of Object.entries(e))
18
+ d(
19
+ t,
20
+ r,
21
+ [...n, o]
22
+ );
23
+ }, x = (t, e) => {
24
+ if (e === void 0)
25
+ return t;
26
+ if (t === void 0)
27
+ return f(e);
28
+ const n = p(e), o = p(t), r = Array.isArray(e), a = Array.isArray(t), s = m(e), c = m(t);
29
+ if (r !== a || s !== c)
30
+ return f(e);
31
+ if ((r || a) && (!n || !o))
32
+ return f(e);
33
+ if (n !== o || s !== c)
34
+ return f(e);
35
+ if (!n && !s)
36
+ return f(e);
37
+ const u = { value: f(e) };
38
+ return d(u, t), u.value;
39
+ }, $ = (t) => t === void 0 || t !== null && typeof t == "object", P = (t, e, n) => !t || !e || t !== e.componentId || e.hasInitializedRef.current || e.initialDefaultValueRef.current === void 0 ? !1 : $(n), b = (t, e, n) => P(t, e, n) ? x(n, e?.initialDefaultValueRef.current) : n;
40
+ function w(t) {
41
+ const e = l(R), n = l(y), o = e?.store ?? null, r = t ?? n?.componentId, a = i(() => {
42
+ const u = o && r ? o.getState().components[r]?.value : void 0;
43
+ return b(r, n, u);
44
+ }, [r, n, o]), s = i((u) => {
45
+ !o || !r || o.getState().setComponentData(r, u);
46
+ }, [r, o]), c = i(() => {
47
+ !o || !r || o.getState().resetComponentData(r);
48
+ }, [r, o]);
49
+ return { getContainerData: a, setContainerData: s, resetContainerData: c };
50
+ }
51
+ function T(t) {
52
+ const e = w(t), n = l(R), o = l(y), r = n?.store ?? null, a = t ?? o?.componentId;
53
+ return {
54
+ containerData: r ? b(
55
+ a,
56
+ o,
57
+ E(r, (c) => a ? c.components[a]?.value : void 0)
58
+ ) : void 0,
59
+ ...e
60
+ };
61
+ }
62
+ const K = C(null), U = "components", Y = "value", _ = (t, e) => `${U}.${t}.${Y}.${e}`, q = ({
63
+ relativePath: t,
64
+ record: e,
65
+ getRecord: n,
66
+ index: o,
67
+ registerField: r,
68
+ unregisterField: a,
69
+ children: s
70
+ }) => {
71
+ const c = l(y);
72
+ if (!c?.componentId)
73
+ throw new Error(
74
+ "[cdp-material-sdk] DataScope 必须在数据容器组件内部使用,请确保上层提供 DataContainerRuntimeContext。"
75
+ );
76
+ const u = c.componentId, D = i(
77
+ (v) => {
78
+ const S = t ? t(v) : v;
79
+ return _(u, S);
80
+ },
81
+ [u, t]
82
+ ), A = g(e);
83
+ A.current = e;
84
+ const I = j(
85
+ () => ({
86
+ resolvePath: D,
87
+ inScope: !0,
88
+ getRecord: n ?? (() => A.current),
89
+ index: o,
90
+ registerField: r,
91
+ unregisterField: a
92
+ }),
93
+ [D, n, o, r, a]
94
+ );
95
+ return /* @__PURE__ */ F(K.Provider, { value: I, children: s });
96
+ };
97
+ function B() {
98
+ const t = g(/* @__PURE__ */ new Map()), e = i((s) => {
99
+ t.current.set(s.id, s);
100
+ }, []), n = i((s) => {
101
+ t.current.delete(s);
102
+ }, []), o = i((s) => t.current.get(s), []), r = i(() => Array.from(t.current.values()), []), a = i(() => {
103
+ t.current.clear();
104
+ }, []);
105
+ return {
106
+ registerField: e,
107
+ unregisterField: n,
108
+ getFieldState: o,
109
+ getAllFieldStates: r,
110
+ clearRegistry: a
111
+ };
112
+ }
113
+ export {
114
+ y as D,
115
+ R as P,
116
+ q as a,
117
+ K as b,
118
+ w as c,
119
+ B as d,
120
+ b as r,
121
+ P as s,
122
+ T as u
123
+ };
@@ -57,6 +57,16 @@
57
57
  - [声明插槽](./recipes/声明插槽.md)
58
58
  - [Traits 能力模型](./reference/Traits能力模型.md)
59
59
 
60
+ ### 我要声明组件对外的可配置项(props)
61
+
62
+ `props` 是组件向设计器、表达式和 AI 工具暴露的可配置项契约(标准 JSON Schema)。
63
+
64
+ 阅读:
65
+
66
+ - [声明 props](./recipes/声明props.md)
67
+ - [Manifest 字段参考](./reference/Manifest字段参考.md)
68
+ - [Traits 能力模型](./reference/Traits能力模型.md)(理解 trait 自动注入 props 的边界)
69
+
60
70
  ### 我要调整组件在设计器中的展示
61
71
 
62
72
  阅读:
@@ -12,19 +12,53 @@ Adapter 用于把组件自身 API 映射到 CDP 约定。它适合接入第三
12
12
 
13
13
  ---
14
14
 
15
- ## 适用场景
15
+ ## wrapper vs adapter 决策框架
16
16
 
17
- - 第三方组件的 prop 名与 CDP 默认名不同。
18
- - 第三方组件的事件 prop 名不同。
19
- - 需要对传入 props 做轻量转换。
17
+ 接入第三方组件、或组件 prop / 事件名不符合 CDP 约定时,有两条路径:用 manifest 的 **adapter** 在引擎层声明映射、或在 React **wrapper** 内手写转换。**按"层"做选择,不要按"默认优先谁"**——adapter 与 wrapper 各自有原生擅长的层。
18
+
19
+ ### 三层视角
20
+
21
+ | 层 | 适配诉求 | adapter 原生能力 | 推荐 |
22
+ |----|---------|-----------------|------|
23
+ | 事件层 | 改 prop 名(`onClick` → `onPress`) | `events[K].propName` / `customEvents[K].propName` | **adapter** |
24
+ | 事件层 | reshape payload 到 CDP 标准 payload | `events[K].transform`(类型化到 `EngineEventProtocol[K]`)/ `customEvents[K].transform` | **adapter** |
25
+ | 事件层 | 提取作用域 record / index | `events[K].toScope` / `customEvents[K].toScope` | **adapter** |
26
+ | Props 层 | 改 prop 名(`value` → `selectedValue`) | `propMapping` | **adapter** |
27
+ | Props 层 | 值转换 / 默认值 / 受控-非受控调谐 | 仅 `mapProps`(**逃生舱**) | **wrapper** |
28
+ | 结构层 | children / slots / ref 转发 / 副作用 | — | **wrapper** |
29
+
30
+ ### 事件层为什么是 adapter 的主场
31
+
32
+ `StandardEventBinding<K>` / `CustomEventBinding` 的能力是 **propName + transform + toScope**,三者合起来覆盖了"换名 + 重塑 payload + 提取作用域"——CDP 标准事件的全部适配诉求。`transform` 的返回类型还约束到 `EngineEventProtocol[K]`,是**类型化**的转换。这正是它的设计目标,**不是逃生舱**。
33
+
34
+ 写在 wrapper 里反而要重新构造一份相同的 payload,丢失类型约束,并把协议形状渗透到组件源码。
35
+
36
+ ### Props 层的分界线在"是否需要值变换"
37
+
38
+ - **只是改名** → `adapter.propMapping` 一行配置,组件保留原生 API。
39
+ - **要做值变换、补默认值、做受控转换、加副作用、需要 ref**:adapter 仅有 `mapProps` 这条路,而 `mapProps` 是真正的逃生舱(无类型、不可静态分析、复杂逻辑藏在 manifest)。这种场景**回到 wrapper**。
40
+
41
+ ### 结构层永远是 wrapper
42
+
43
+ children/slots 渲染、ref 转发、`useEffect` 副作用、错误边界——这些都不在 adapter 的能力范围。
44
+
45
+ ### 协议解耦
46
+
47
+ - **wrapper**:把第三方原生 API 锁在内部,对外暴露 CDP 形状。**适配层在 React 里**,组件依赖 `BaseUIProps` / `slotProps` / `cdp-material-sdk/portable` 类型。
48
+ - **adapter**:组件保留原生 API,CDP 形状的转换写在 manifest。**适配层在 manifest 里**,组件不引入 CDP 类型依赖,可被非 CDP 项目复用。
49
+
50
+ 两者方向相反,按上面三层选择,不必把协议解耦当作单一论据。
51
+
52
+ ### 同一个组件的混合策略
53
+
54
+ 事件层用 adapter、props 值层用 wrapper 是常见组合——只要 wrapper 输出的 props 能让 adapter 在引擎层补上事件映射即可。**不要在两层都做同样的事**,否则会重复甚至冲突。
20
55
 
21
56
  ---
22
57
 
23
- ## 可以跳过的情况
58
+ ## 可以跳过 adapter 的情况
24
59
 
25
- - 你可以在 wrapper 内直接转换 API。
26
- - 组件本身已经使用 `value`、`onChange`、`onClick` CDP 约定。
27
- - 适配逻辑复杂,需要在组件代码中处理。
60
+ - 组件本身已经使用 CDP 约定(`value` / `onChange` / `onClick` / 标准 payload)。
61
+ - 全部适配诉求都在结构层或 props 值层 走 wrapper 即可。
28
62
 
29
63
  ---
30
64
 
@@ -47,7 +81,15 @@ adapter: {
47
81
 
48
82
  ## events 映射
49
83
 
50
- 标准事件映射:
84
+ `StandardEventBinding<K>` / `CustomEventBinding` 提供三个原语:
85
+
86
+ | 字段 | 作用 |
87
+ |------|------|
88
+ | `propName` | 第三方组件实际触发回调的 prop 名(如 `onPress`、`onSelectedValueChange`) |
89
+ | `transform(...args)` | 把第三方回调参数 reshape 成 CDP 标准 payload;标准事件返回类型受 `EngineEventProtocol[K]` 约束 |
90
+ | `toScope(...args)` | 从回调参数中提取 `{ record, index }`,用于 List/Table 行项事件的作用域 |
91
+
92
+ ### 标准事件 — 仅改名
51
93
 
52
94
  ```ts
53
95
  events: {
@@ -62,29 +104,75 @@ adapter: {
62
104
  },
63
105
  ```
64
106
 
65
- 自定义事件映射:
107
+ ### 标准事件 — 改名 + payload 重塑
108
+
109
+ 第三方组件回调签名是 `(event, value) => void`,但 CDP `valueChange` 的 payload 形状是 `{ value }`:
110
+
111
+ ```ts
112
+ events: {
113
+ valueChange: { title: '值变化' },
114
+ },
115
+ adapter: {
116
+ events: {
117
+ valueChange: {
118
+ propName: 'onSelectedValueChange',
119
+ transform: (event, value) => ({ value }),
120
+ },
121
+ },
122
+ },
123
+ ```
124
+
125
+ `transform` 的返回类型由 `EngineEventProtocol['valueChange']` 自动约束,少写错。
126
+
127
+ ### 自定义事件 — 改名 + payload + scope
128
+
129
+ 行项点击常见形态:第三方 List 的 `onItemTap(item, index)`,CDP 侧声明为带作用域的自定义事件:
66
130
 
67
131
  ```ts
68
132
  customEvents: {
69
133
  'acme:rowClick': {
70
134
  title: '行点击',
71
- payloadSchema: { type: 'object' },
135
+ payloadSchema: {
136
+ type: 'object',
137
+ properties: {
138
+ rowId: { type: 'string' },
139
+ },
140
+ },
72
141
  },
73
142
  },
74
143
  adapter: {
75
144
  customEvents: {
76
145
  'acme:rowClick': {
77
- propName: 'onRowClick',
146
+ propName: 'onItemTap',
147
+ transform: (item) => ({ rowId: item.id }),
148
+ toScope: (item, index) => ({ record: item, index }),
78
149
  },
79
150
  },
80
151
  },
81
152
  ```
82
153
 
154
+ `toScope` 让事件编排能基于"哪一行"组织上下文,无需在 wrapper 中手写作用域注入。
155
+
83
156
  ---
84
157
 
85
- ## mapProps
158
+ ## propMapping
86
159
 
87
- 当静态映射不够时,可以使用 `mapProps`:
160
+ CDP prop 名映射为组件实际 prop 名(**只重命名,不变换值**):
161
+
162
+ ```ts
163
+ adapter: {
164
+ propMapping: {
165
+ loading: 'spinning',
166
+ value: 'selectedValue',
167
+ },
168
+ },
169
+ ```
170
+
171
+ ---
172
+
173
+ ## mapProps(逃生舱)
174
+
175
+ `propMapping` 不能做值变换、默认值、受控调谐等。adapter 在 props 层只剩 `mapProps` 这条路:
88
176
 
89
177
  ```ts
90
178
  adapter: {
@@ -96,29 +184,39 @@ adapter: {
96
184
  },
97
185
  ```
98
186
 
99
- 注意:`mapProps` 是逃生舱。优先使用 wrapper `propMapping`,避免把复杂逻辑藏在 manifest 中。
187
+ `mapProps` 无类型约束、不可静态分析,是真正的逃生舱。需要在 props 层做超出"改名"的事情时,**优先回到 wrapper**,不要把复杂逻辑藏在 manifest 中。
100
188
 
101
189
  ---
102
190
 
103
191
  ## 自检
104
192
 
105
- - [ ] adapter 标准事件已经先在 `events` 声明。
106
- - [ ] adapter 自定义事件已经先在 `customEvents` 声明。
107
- - [ ] 每个 adapter event 都有 `propName`。
108
- - [ ] `propMapping` 没有和 wrapper 内转换重复冲突。
193
+ - [ ] adapter 标准事件已经先在 `events` 声明,自定义事件已经先在 `customEvents` 声明。
194
+ - [ ] adapter event `propName` 与第三方组件实际回调 prop 名一致。
195
+ - [ ] 需要 reshape payload 时使用 `transform`,类型对齐到 `EngineEventProtocol[K]`(标准事件)或 `payloadSchema`(自定义事件)。
196
+ - [ ] 需要作用域的事件(List / Table 行项)使用 `toScope` 提取 `{ record, index }`。
197
+ - [ ] `propMapping` 只用于纯重命名;值变换 / 默认值 / 受控转换走 wrapper。
198
+ - [ ] `mapProps` 仅在没有 wrapper 控制权且必须保留 manifest 即真相时使用。
109
199
  - [ ] `validateManifest()` 没有 error。
110
200
 
111
201
  ---
112
202
 
113
203
  ## 常见错误
114
204
 
115
- ### Adapter 引用了未声明事件
205
+ ### Adapter 引用了未声明的事件
206
+
207
+ `adapter.events` / `adapter.customEvents` 中的 key,必须先在 manifest 的 `events` / `customEvents` 中声明,否则 `validateManifest()` 会报错。
208
+
209
+ ### 在 wrapper 里再 reshape 一次 payload
210
+
211
+ 如果已经用 `adapter.events.transform` 做了 payload 重塑,wrapper 不要再做一次——会出现两层映射叠加,或者 wrapper 输出的 payload 与 `EngineEventProtocol[K]` 形状不匹配。事件层适配只在一处做。
212
+
213
+ ### 用 `mapProps` 做改名
116
214
 
117
- 先声明事件,再用 adapter 映射。否则 `validateManifest()` 会报错。
215
+ 改名用 `propMapping`;`mapProps` 留给真正需要值变换的场景。把改名塞进 `mapProps` 会让校验和阅读都变难。
118
216
 
119
- ### 把复杂业务逻辑写进 mapProps
217
+ ### `mapProps` 当主力
120
218
 
121
- `mapProps` 只适合轻量适配。复杂逻辑应该写在 React wrapper 中。
219
+ `mapProps` 无类型、藏逻辑,应该是逃生舱而不是主力。出现复杂值变换、默认值或受控-非受控转换时,回到 wrapper,让组件作者用 TS 类型表达约束。
122
220
 
123
221
  ---
124
222