feat: sync account freeze lifecycle

This commit is contained in:
A 2026-07-15 20:24:37 +08:00
parent 76bfc5100f
commit 47fcf0ea41
40 changed files with 1363 additions and 196 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>telesrv admin</title>
<script type="module" crossorigin src="/assets/index-BHnkZ_za.js"></script>
<script type="module" crossorigin src="/assets/index-DRWO_DgE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-W1-UPxwc.css">
</head>
<body>

View file

@ -127,15 +127,21 @@ const translations: Record<Language, Record<string, string>> = {
"account.waitingData": "Waiting for data",
"account.noUsername": "No username",
"account.noPhone": "No phone",
"account.sendFrozen": "Sending frozen",
"account.sendNormal": "Sending allowed",
"account.accountFrozen": "Account frozen",
"account.accountActive": "Account active",
"account.authorizationsTitle": "Authorized Devices",
"account.authorizationsCount": "{count} authorizations",
"account.recentAdminOps": "Recent Admin Actions",
"account.recent30Audit": "Last 30 audit rows",
"account.actionDock": "Account Actions",
"account.freezeSend": "Freeze sending",
"account.unfreezeSend": "Unfreeze sending",
"account.freezeAccount": "Freeze account",
"account.updateFreeze": "Update freeze",
"account.unfreezeAccount": "Unfreeze account",
"account.freezeSince": "Frozen since",
"account.freezeUntil": "Appeal deadline",
"account.freezeUntilAria": "Freeze appeal deadline",
"account.freezeAppealURL": "Appeal URL",
"account.freezeAppealURLAria": "Freeze appeal URL",
"account.premiumMonths": "Premium duration (months)",
"account.premiumMonthsAria": "Set premium duration in months",
"account.setPremium": "Set premium",
@ -392,15 +398,21 @@ const translations: Record<Language, Record<string, string>> = {
"account.waitingData": "等待数据",
"account.noUsername": "无用户名",
"account.noPhone": "无手机号",
"account.sendFrozen": "发消息冻结",
"account.sendNormal": "发送正常",
"account.accountFrozen": "账号已冻结",
"account.accountActive": "账号正常",
"account.authorizationsTitle": "授权设备",
"account.authorizationsCount": "共 {count} 个授权",
"account.recentAdminOps": "最近后台操作",
"account.recent30Audit": "最近 30 条审计",
"account.actionDock": "账号操作",
"account.freezeSend": "冻结发消息",
"account.unfreezeSend": "解冻发消息",
"account.freezeAccount": "冻结账号",
"account.updateFreeze": "更新冻结信息",
"account.unfreezeAccount": "解冻账号",
"account.freezeSince": "冻结开始时间",
"account.freezeUntil": "申诉截止时间",
"account.freezeUntilAria": "账号冻结申诉截止时间",
"account.freezeAppealURL": "申诉链接",
"account.freezeAppealURLAria": "账号冻结申诉链接",
"account.premiumMonths": "会员时长(月)",
"account.premiumMonthsAria": "设置会员时长,单位月",
"account.setPremium": "设置会员",

View file

@ -16,12 +16,21 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
const [busy, setBusy] = useState(false);
const [months, setMonths] = useState("1");
const [starsAmount, setStarsAmount] = useState("1000");
const [freezeUntil, setFreezeUntil] = useState(() => toDateTimeLocal(new Date(Date.now() + 7 * 86400_000)));
const [freezeAppealURL, setFreezeAppealURL] = useState("");
async function load() {
setBusy(true);
setError("");
try {
setDetail(await api.account(id));
const next = await api.account(id);
setDetail(next);
if (next.Restriction.Frozen) {
if (next.Restriction.Until) {
setFreezeUntil(toDateTimeLocal(new Date(next.Restriction.Until)));
}
setFreezeAppealURL(next.Restriction.AppealURL || "");
}
} catch (err) {
setError(errorMessage(err));
} finally {
@ -58,7 +67,7 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
<div className="entity-badges">
{account.PremiumUntil > 0 ? <Badge tone="good">{t("account.premium")}</Badge> : <Badge>{t("account.notPremium")}</Badge>}
{detail.Verified ? <Badge tone="good">{t("common.verified")}</Badge> : <Badge>{t("account.notVerified")}</Badge>}
{account.Frozen ? <Badge tone="danger">{t("account.sendFrozen")}</Badge> : <Badge>{t("account.sendNormal")}</Badge>}
{account.Frozen ? <Badge tone="danger">{t("account.accountFrozen")}</Badge> : <Badge>{t("account.accountActive")}</Badge>}
</div>
</section>
<div className="summary-grid">
@ -70,6 +79,9 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
<Summary label={t("account.activeSessions")} value={String(detail.Authorizations.length)} />
<Summary label={t("account.accountFlags")} value={`support=${detail.Support} bot=${detail.Bot}`} />
<Summary label={t("account.restriction")} value={detail.HasRestriction ? detail.Restriction.Reason || t("account.restricted") : t("common.none")} />
<Summary label={t("account.freezeSince")} value={detail.Restriction.Since ? formatDate(detail.Restriction.Since) : t("common.none")} />
<Summary label={t("account.freezeUntil")} value={detail.Restriction.Until ? formatDate(detail.Restriction.Until) : t("common.none")} />
<Summary label={t("account.freezeAppealURL")} value={detail.Restriction.AppealURL || t("common.none")} />
<Summary label={t("account.createdAt")} value={formatDate(account.CreatedAt) || "-"} />
</div>
{detail.About && <p className="about-text">{detail.About}</p>}
@ -86,13 +98,46 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
side={
<section className="action-dock">
<div className="dock-title">{t("account.actionDock")}</div>
<label className="duration-field">
<span>{t("account.freezeUntil")}</span>
<input
aria-label={t("account.freezeUntilAria")}
value={freezeUntil}
onChange={(event) => setFreezeUntil(event.target.value)}
type="datetime-local"
/>
</label>
<label className="duration-field">
<span>{t("account.freezeAppealURL")}</span>
<input
aria-label={t("account.freezeAppealURLAria")}
value={freezeAppealURL}
onChange={(event) => setFreezeAppealURL(event.target.value)}
type="url"
placeholder="https://..."
/>
</label>
<ActionButton
label={account.Frozen ? t("account.unfreezeSend") : t("account.freezeSend")}
label={account.Frozen ? t("account.updateFreeze") : t("account.freezeAccount")}
icon={<CircleAlert size={15} />}
path="/api/actions/freeze-send"
payload={() => ({ user_id: account.ID, frozen: !account.Frozen })}
path="/api/actions/set-frozen"
payload={() => ({
user_id: account.ID,
frozen: true,
freeze_until: new Date(freezeUntil).toISOString(),
freeze_appeal_url: freezeAppealURL.trim()
})}
onDone={load}
/>
{account.Frozen && (
<ActionButton
label={t("account.unfreezeAccount")}
icon={<CircleAlert size={15} />}
path="/api/actions/set-frozen"
payload={() => ({ user_id: account.ID, frozen: false })}
onDone={load}
/>
)}
<label className="duration-field">
<span>{t("account.premiumMonths")}</span>
<input
@ -155,3 +200,8 @@ export function AccountDetailPage({ id, navigate }: { id: number; navigate: Navi
</PageFrame>
);
}
function toDateTimeLocal(date: Date): string {
const local = new Date(date.getTime() - date.getTimezoneOffset() * 60_000);
return local.toISOString().slice(0, 16);
}

View file

@ -16,6 +16,9 @@ export type AccountRow = {
export type RestrictionRow = {
Frozen: boolean;
Since: string | null;
Until: string | null;
AppealURL: string;
Reason: string;
Actor: string;
CommandID: string;