diff --git a/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx b/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx
index 2ed6ba9..2807b2c 100644
--- a/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx
+++ b/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx
@@ -13,11 +13,16 @@ import {
/**
* 渠道账号白名单编辑器(主人 2026-10-05 要求):**一个平台一条**,该平台下用
* 「+ 添加账号 ID」加多行;平台是权威枚举**下拉**;账号 ID 旁给「从该平台已配置
- * 账号里选」的候选。改动经 `onChange` 回到草稿时**立即展平**为
+ * 账号里选」的候选。改动经 `onChange` 回到草稿时**写回扁平数组**
* `accounts:[{platform,accountId}…]` —— 后端契约与 `owner.yaml` 格式完全不变。
*
* 为什么不在这里直接保存:档案的读取/草稿/保存状态由 `Users.tsx` 的
- * `OwnerProfileSection` 统一持有(一次真实 PUT),本组件只管呈现与展平。
+ * `OwnerProfileSection` 统一持有(一次真实 PUT),本组件只管呈现与写回草稿。
+ *
+ * 为什么拆成两个函数:`OwnerAccountsEditor` 只留「分组/选项」这层 memo(同一表单里改
+ * 别的字段时不必重算),界面本体 `OwnerAccountsView` 是**无 hook** 的 —— 本项目没有
+ * jsdom,测试正是**直接调用**它、点它自己的按钮来钉住「手势 → 草稿 → 重渲染」这条往返
+ * (见 `ownerAccounts.test.ts` ⑤)。往 View 里加 hook 会让那条测试报 "Invalid hook call"。
*/
export function OwnerAccountsEditor({
accounts,
@@ -39,9 +44,37 @@ export function OwnerAccountsEditor({
() => platformOptions(platforms, groups.map((g) => g.platform)),
[platforms, groups]
);
+ return (
+
+ );
+}
- // 每次改动都「分组 → 展平 → 回草稿」:草稿里始终是后端要的扁平形态,
- // 界面上的分组只是它的投影,两者不会各说各话。
+export interface OwnerAccountsViewProps {
+ /** 界面上的分组(草稿的投影);手势产生的下一份**草稿**也由它算出来。 */
+ groups: OwnerAccountGroup[];
+ options: EnumItem[];
+ channels: AdminChannelRecord[] | null;
+ disabled: boolean;
+ /** 新草稿(扁平数组)。 */
+ onChange: (next: OwnerAccount[]) => void;
+}
+
+/** 界面本体(**无 hook**,见文件头):所有手势都走这里 → `onChange(新草稿)`。 */
+export function OwnerAccountsView({
+ groups,
+ options,
+ channels,
+ disabled,
+ onChange,
+}: OwnerAccountsViewProps): React.ReactNode {
+ // 每次改动都「分组 → 改 → 回草稿」:草稿里始终是扁平形态,界面上的分组只是它的投影,
+ // 两者不会各说各话。
const emit = (next: OwnerAccountGroup[]): void => onChange(flattenAccounts(next));
const patchGroup = (i: number, patch: Partial): void =>
@@ -156,6 +189,7 @@ function PlatformGroupRow(props: PlatformGroupRowProps): React.ReactNode {
{group.accountIds.map((accountId, j) => (
+