diff --git a/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx b/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx index 2ed6ba9..82c78c5 100644 --- a/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx +++ b/frontend/web/src/admin/modules/OwnerAccountsEditor.tsx @@ -3,7 +3,7 @@ import type { AdminChannelRecord, EnumItem, OwnerAccount } from "../../lib/types import { Button } from "../../components/primitives"; import { accountCandidates, - flattenAccounts, + draftAccounts, groupAccounts, platformIsConfigured, platformOptions, @@ -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,10 +44,39 @@ export function OwnerAccountsEditor({ () => platformOptions(platforms, groups.map((g) => g.platform)), [platforms, groups] ); + return ( + + ); +} - // 每次改动都「分组 → 展平 → 回草稿」:草稿里始终是后端要的扁平形态, - // 界面上的分组只是它的投影,两者不会各说各话。 - const emit = (next: OwnerAccountGroup[]): void => onChange(flattenAccounts(next)); +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 { + // 每次改动都「分组 → 改 → 回草稿」:草稿里始终是扁平形态,界面上的分组只是它的投影, + // 两者不会各说各话。写回必须**无损**(`draftAccounts`):一旦写成有损的写盘形态,刚点出来的 + // 「+ 添加账号 ID」空行当场蒸发 —— 点了等于没点。空行只在保存时丢(见 `accountsForSave`)。 + const emit = (next: OwnerAccountGroup[]): void => onChange(draftAccounts(next)); const patchGroup = (i: number, patch: Partial): void => emit(groups.map((g, idx) => (idx === i ? { ...g, ...patch } : g))); @@ -156,6 +190,7 @@ function PlatformGroupRow(props: PlatformGroupRowProps): React.ReactNode { {group.accountIds.map((accountId, j) => ( +