): void =>
emit(groups.map((g, idx) => (idx === i ? { ...g, ...patch } : g)));
const addAccount = (i: number): void =>
patchGroup(i, { accountIds: [...groups[i].accountIds, ""] });
const removeAccount = (i: number, j: number): void =>
patchGroup(i, { accountIds: groups[i].accountIds.filter((_, k) => k !== j) });
const patchAccount = (i: number, j: number, value: string): void =>
patchGroup(i, {
accountIds: groups[i].accountIds.map((id, k) => (k === j ? value : id)),
});
const removeGroup = (i: number): void => emit(groups.filter((_, idx) => idx !== i));
const addGroup = (): void => emit([...groups, { platform: "", accountIds: [""] }]);
return (
{groups.length === 0 ? (
尚未配置:现状为「无人是主人」(默认拒绝,fail-safe)。
) : null}
{groups.map((g, i) => (
patchGroup(i, { platform: v })}
onAccount={(j, v) => patchAccount(i, j, v)}
onAddAccount={() => addAccount(i)}
onRemoveAccount={(j) => removeAccount(i, j)}
onRemoveGroup={() => removeGroup(i)}
/>
))}
);
}
/** 头部:标题 + 说明(写盘格式不变)+「+ 添加平台」,以及「一组都没有」的诚实提示。 */
function WhitelistHeader({
disabled,
onAddGroup,
}: {
disabled: boolean;
onAddGroup: () => void;
}): React.ReactNode {
return (
<>
渠道账号白名单(命中即走 owner 车道)· 一个平台一条,可加多个账号 ID
保存时仍是扁平数组{' '}
{'accounts: [{platform, accountId}, …]'}
(owner.yaml 格式不变);平台与账号 ID 都填了的条目才会写盘。
>
);
}
const inputCls =
"w-full rounded-[var(--mu-radius)] bg-[var(--mu-surface-2)] px-3 py-2 text-sm outline-none focus:ring-1 focus:ring-[var(--mu-accent)] disabled:opacity-60";
interface PlatformGroupRowProps {
group: OwnerAccountGroup;
index: number;
options: EnumItem[];
candidates: string[];
known: boolean;
disabled: boolean;
onPlatform: (v: string) => void;
onAccount: (j: number, v: string) => void;
onAddAccount: () => void;
onRemoveAccount: (j: number) => void;
onRemoveGroup: () => void;
}
/** 一个平台分组:平台下拉 + 该平台下的账号 ID 多行(每行带候选,可删)。 */
function PlatformGroupRow(props: PlatformGroupRowProps): React.ReactNode {
const { group, index, options, candidates, known, disabled } = props;
const emptyCount = group.accountIds.filter((id) => id.trim() === "").length;
return (
{group.accountIds.length === 0 ? (
该平台还没有账号 ID:保存时这一组会被忽略(不会写入 owner.yaml)。
) : null}
{group.accountIds.map((accountId, j) => (
props.onAccount(j, v)}
onRemove={() => props.onRemoveAccount(j)}
/>
))}
{emptyCount > 0 ? (
有 {emptyCount} 条空白账号 ID:保存时会被忽略。
) : null}
);
}
/** 分组头:平台下拉(权威枚举)+ 该平台的账号条数 + 「删除该平台」。 */
function PlatformPicker({
group,
options,
disabled,
onPlatform,
onRemoveGroup,
}: {
group: OwnerAccountGroup;
options: EnumItem[];
disabled: boolean;
onPlatform: (v: string) => void;
onRemoveGroup: () => void;
}): React.ReactNode {
return (
{group.platform === ""
? "未选平台:这组不会被写盘"
: `该平台下的账号 ID(${group.accountIds.length} 条)`}
);
}
interface AccountIdRowProps {
listId: string;
/** 在本分组里的行号(`0` 起):给行一个稳定的定位标记,浏览器里/测试里都指得到具体那一行。 */
row: number;
accountId: string;
candidates: string[];
platform: string;
known: boolean;
disabled: boolean;
onValue: (v: string) => void;
onRemove: () => void;
}
/** 一行账号 ID:手填 + 该平台已配置账号的候选(点一下就填入)。 */
function AccountIdRow({
listId,
row,
accountId,
candidates,
platform,
known,
disabled,
onValue,
onRemove,
}: AccountIdRowProps): React.ReactNode {
return (
{candidates.length ? (
) : (
)}
);
}
/** 候选账号:`