perf(admin-web): 长列表虚拟滚动 + 专属页只读配置卡(裁决 16.4.3/16.4.6)+ 修限流读写成对的数据丢失
- 16.4.3(效率最高):AdminTable 超阈值走窗口化(DOM 行数与数据量解耦,上下占位行撑滚动条);
新增可复用 VirtualList;适配器消息日志换成窗口化版
- 16.4.6(操作更便利):保留列表页内联编辑,同时把「专属页只读展示该平台完整字段」真正实现
(AdapterConfigSummary,此前专属页一个配置字段都不显示)
- 🔴 顺带修一条静默数据丢失:列表页限流三项原从 acct.config.* 读,而后端把它们存**账号顶层**
⇒ 表单永远回显空、保存时 undefined→0 会把已设限流清空。现改从顶层读、留空则不发该键、非法输入报错中止;
并顺手改正用户可见的错误文案(0 = 保护性默认 6条/分,负值才是关闭)
- 复核:tsc 0 错 + vite build 成功;channel limits 27 / admin virtual scroll 8023 项断言;3 条变异反证
This commit is contained in:
1 parent
b480b04dd0
commit
c1a98d7c01
9 files changed
+976
-22
No files matched your search
@@ -47,14 +47,61 @@ export function buildCfgTarget(name: string, records: AdminChannelRecord[]): Cfg
|
||||
denyGroups: (acct?.denyGroups ?? []).join(","),
|
||||
allowUsers: (acct?.allowUsers ?? []).join(","),
|
||||
denyUsers: acct?.denyUsers ? (Array.isArray(acct.denyUsers) ? acct.denyUsers.join(",") : "") : "",
|
||||
groupMsgPerMin: acctCfg.groupMsgPerMin == null ? "" : String(acctCfg.groupMsgPerMin),
|
||||
msgPerMin: acctCfg.msgPerMin == null ? "" : String(acctCfg.msgPerMin),
|
||||
maxMessages: acctCfg.maxMessages == null ? "" : String(acctCfg.maxMessages),
|
||||
// 限流三个字段读**账号顶层**,不是 `config`(见 LIMIT_KEYS 的说明)。
|
||||
groupMsgPerMin: limitInitialOf(acct?.groupMsgPerMin),
|
||||
msgPerMin: limitInitialOf(acct?.msgPerMin),
|
||||
maxMessages: limitInitialOf(acct?.maxMessages),
|
||||
personaId: channelPersonaOf(acct),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** 限流 / 会话额度的三个字段名(design/12 §6 策略三/四)。回显、写回与表单共用这一份,
|
||||
* 避免两处清单漂移。
|
||||
*
|
||||
* ⚠️ 线上它们是**账号顶层**字段(`AdminChannelAccount.GroupMsgPerMin/MsgPerMin/
|
||||
* MaxMessages`,见 `internal/services/gateway/admin_write_channel_config.go` 的
|
||||
* `adminChannelAccountOf`),**不在 `config` 里** —— 从 `config` 读永远读不到:回显空
|
||||
* ⇒ 保存时按「未设置」写回 0 ⇒ **静默清掉主人已设的限流**。 */
|
||||
export const LIMIT_KEYS = ["groupMsgPerMin", "msgPerMin", "maxMessages"] as const;
|
||||
|
||||
/** 限流字段名。 */
|
||||
export type LimitKey = (typeof LIMIT_KEYS)[number];
|
||||
|
||||
/** 已设限额 → 表单回显:缺省与 `0` 都渲染成空(后端 `omitempty` ⇒ 两者在盘上等价)。
|
||||
* 非零值**原样回传**,这样「没动过的字段」保存时原样送回,不会把已存限额写没。 */
|
||||
function limitInitialOf(v: number | undefined): string {
|
||||
return v === undefined || v === 0 ? "" : String(v);
|
||||
}
|
||||
|
||||
/** 表单限流值 → 请求体取值:
|
||||
* - 留空 → `undefined`:该键**不进请求体**(后端 int 契约下省略 = 0 = 未设置);
|
||||
* - 整数(含负值)→ 该数值;
|
||||
* - 其它(小数 / 非数字)→ `null`,调用方报错并**中止保存** —— `Number("x")` 是 NaN、
|
||||
* 序列化成 `null`,后端 int 收到 `null` 静默落 0,同样会清空已设限额。
|
||||
*
|
||||
* 数值语义以 `internal/services/channelbridge/flowgate.go` 的 `flowOrDefault` 为准:
|
||||
* **0 = 保护性默认**(群 6 条/分、待处理上限 50)、**负值 = 关闭限制**;`msgPerMin` 只认正值。 */
|
||||
export function limitFieldValue(raw: string): number | undefined | null {
|
||||
const t = raw.trim();
|
||||
if (!t) return undefined;
|
||||
return /^-?\d+$/.test(t) ? Number(t) : null;
|
||||
}
|
||||
|
||||
/** 表单三个限流字段 → PUT 请求体片段。任一字段非法即整单拒绝(绝不发半份请求体,
|
||||
* 也绝不用 0 代替「未设置」)。 */
|
||||
export function limitRequestBody(
|
||||
fields: Record<LimitKey, string>
|
||||
): { ok: true; body: Partial<Record<LimitKey, number>> } | { ok: false; error: string } {
|
||||
const body: Partial<Record<LimitKey, number>> = {};
|
||||
for (const key of LIMIT_KEYS) {
|
||||
const value = limitFieldValue(fields[key]);
|
||||
if (value === null) return { ok: false, error: `${key} 需为整数(留空 = 未设置,负值 = 关闭限制)` };
|
||||
if (value !== undefined) body[key] = value;
|
||||
}
|
||||
return { ok: true, body };
|
||||
}
|
||||
|
||||
/** The set of platforms that have at least one configured account. */
|
||||
export function configuredPlatforms(records: AdminChannelRecord[]): AdminChannelRecord[] {
|
||||
return records.filter((c) => (c.accounts?.length ?? 0) > 0);
|
||||
|
||||
Reference in new issue
Block a user