refactor(admin): 主人账号编辑器拆出无 hook 的 OwnerAccountsView
界面本体与「分组/选项 memo」分层:OwnerAccountsEditor 只做 memo 组装,OwnerAccountsView 无 hook。本项目没有 jsdom,拆开后测试可以**直接调用**视图、点它自己的按钮,从而钉住「手势 → 草稿 → 重渲染」这条往返(见下一个提交)。行为不变:emit 仍用 flattenAccounts;账号行加 data-account-row 定位标记。
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user