refactor(admin): 主人账号编辑器拆出无 hook 的 OwnerAccountsView

界面本体与「分组/选项 memo」分层:OwnerAccountsEditor 只做 memo 组装,OwnerAccountsView 无 hook。本项目没有 jsdom,拆开后测试可以**直接调用**视图、点它自己的按钮,从而钉住「手势 → 草稿 → 重渲染」这条往返(见下一个提交)。行为不变:emit 仍用 flattenAccounts;账号行加 data-account-row 定位标记。
This commit is contained in:
AskaEth committed 2026-10-13 19:56:32 +08:00
1 parent ae68bb0241
commit b01e2ddb1a
1 file changed
+42 -5
@@ -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 (
<OwnerAccountsView
groups={groups}
options={options}
channels={channels}
disabled={disabled}
onChange={onChange}
/>
);
}
// 每次改动都「分组 → 展平 → 回草稿」:草稿里始终是后端要的扁平形态,
// 界面上的分组只是它的投影,两者不会各说各话。
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<OwnerAccountGroup>): void =>
@@ -156,6 +189,7 @@ function PlatformGroupRow(props: PlatformGroupRowProps): React.ReactNode {
{group.accountIds.map((accountId, j) => (
<AccountIdRow
key={`acct-${index}-${j}`}
row={j}
listId={`acct-${index}-${j}-candidates`}
accountId={accountId}
candidates={candidates}
@@ -227,6 +261,8 @@ function PlatformPicker({
interface AccountIdRowProps {
listId: string;
/** 在本分组里的行号(`0` 起):给行一个稳定的定位标记,浏览器里/测试里都指得到具体那一行。 */
row: number;
accountId: string;
candidates: string[];
platform: string;
@@ -239,6 +275,7 @@ interface AccountIdRowProps {
/** 一行账号 ID:手填 + 该平台已配置账号的候选(点一下就填入)。 */
function AccountIdRow({
listId,
row,
accountId,
candidates,
platform,
@@ -248,7 +285,7 @@ function AccountIdRow({
onRemove,
}: AccountIdRowProps): React.ReactNode {
return (
<div className="space-y-1">
<div className="space-y-1" data-account-row={row}>
<div className="flex flex-wrap items-end gap-2">
<label className="block min-w-56 flex-1 space-y-1">
<span className="text-xs text-[var(--mu-text-lo)]">账号 ID</span>