feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS

站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-18 05:27:33 +08:00
parent 5f7193042f
commit d784b0ea7a
68 changed files with 2588 additions and 768 deletions

View File

@@ -1,11 +1,15 @@
"use client";
import { useEffect, useState } from "react";
import { Ban, Loader2, RotateCcw, ShieldCheck, Users as UsersIcon } from "lucide-react";
import Link from "next/link";
import { Ban, ExternalLink, Loader2, RotateCcw, ShieldCheck, Users as UsersIcon } from "lucide-react";
import { apiAdminSetUserBan, apiAdminSetUserRole, type Board } from "@/lib/api";
import { ROLES, ROLE_META } from "@/lib/roles";
import { ROLES, ROLE_META, roleMeta } from "@/lib/roles";
import { formatDate } from "@/lib/format";
import { toast } from "@/lib/toast";
import Avatar from "@/components/Avatar";
import Modal from "@/components/Modal";
import RoleBadge from "@/components/RoleBadge";
/** 角色与账号弹窗所需的最小用户形状(后台列表项 / 公开资料均可) */
export interface RoleAccountTarget {
@@ -15,6 +19,10 @@ export interface RoleAccountTarget {
role: string;
board_ids?: number[];
banned: boolean;
avatar?: string;
email?: string;
points?: number;
created_at?: string;
}
interface RoleOption {
@@ -125,9 +133,7 @@ export default function RoleAccountModal({
if (willUnban) parts.push("已解封");
toast(parts.join(" · ") || "已保存", "ok");
onSaved({
id: target.id,
username: target.username,
nickname: target.nickname,
...target,
role: nextRole,
board_ids: nextBoards,
banned: nextBanned,
@@ -140,26 +146,20 @@ export default function RoleAccountModal({
};
const saveLabel = willBan ? "保存并封禁" : willUnban ? "保存并解封" : "保存";
const displayName = target.nickname || target.username;
const currentMeta = roleMeta(target.role);
return (
<Modal
open
title="角色与账号"
description={
<>
<span className="truncate block">
@{target.username}({target.nickname})
</span>
<span className="block mt-1">
角色决定后台入口与前台管理能力;封禁立即强制下线。站长内容档案仍走列表入口。
</span>
</>
}
header={<IdentityHeader target={target} displayName={displayName} currentMetaLabel={currentMeta.label} />}
onClose={onClose}
busy={acting}
maxWidthClass="max-w-[500px]"
panelClassName="p-6 max-h-[88vh] overflow-y-auto"
ariaLabel="角色与账号"
maxWidthClass="max-w-[540px]"
panelClassName="p-6 max-h-[88vh] flex flex-col overflow-hidden"
bodyClassName="overflow-y-auto flex-1 min-h-0 -mx-1 px-1"
ariaLabel={`设置 ${displayName} 的角色与账号`}
footer={
<>
<button type="button" className="btn btn-line" onClick={onClose} disabled={acting}>
@@ -177,7 +177,7 @@ export default function RoleAccountModal({
</>
}
>
<p className="mb-2 text-[12.5px]" style={{ color: "var(--ink)" }}>
<p className="mb-2.5 text-[12px] font-medium tracking-wide" style={{ color: "var(--ink-3)" }}>
管理角色
</p>
<div className="flex flex-col gap-2" role="radiogroup" aria-label="管理角色">
@@ -231,7 +231,7 @@ export default function RoleAccountModal({
{role === ROLES.BOARD_ADMIN && (
<div className="mt-4">
<p className="text-[12.5px] mb-2" style={{ color: "var(--ink)" }}>
<p className="text-[12px] font-medium tracking-wide mb-2" style={{ color: "var(--ink-3)" }}>
授权板块 <span style={{ color: "var(--danger)" }}>*</span>
</p>
{boards.length === 0 ? (
@@ -280,7 +280,7 @@ export default function RoleAccountModal({
)}
<div className="mt-5 pt-4" style={{ borderTop: "1px solid var(--line)" }}>
<p className="text-[12.5px] mb-2" style={{ color: "var(--ink)" }}>
<p className="text-[12px] font-medium tracking-wide mb-2.5" style={{ color: "var(--ink-3)" }}>
账号状态
</p>
<div className="grid grid-cols-2 gap-2" role="radiogroup" aria-label="账号状态">
@@ -289,17 +289,17 @@ export default function RoleAccountModal({
role="radio"
aria-checked={!banned}
onClick={() => setBanned(false)}
className="rounded-xl px-3 py-2.5 text-left transition-colors"
className="rounded-xl px-3.5 py-3 text-left transition-colors"
style={{
border: `1.5px solid ${!banned ? "var(--ok)" : "var(--line)"}`,
background: !banned ? "var(--ok-soft)" : "var(--panel-2)",
}}
>
<span
className="flex items-center gap-1.5 text-[13px] font-medium"
className="flex items-center gap-1.5 text-[13.5px] font-medium"
style={{ color: !banned ? "var(--ok)" : "var(--ink)" }}
>
<RotateCcw size={14} /> 正常
<RotateCcw size={15} /> 正常
</span>
<span className="block mt-1 text-[11.5px] leading-snug" style={{ color: "var(--ink-3)" }}>
可登录与互动
@@ -310,17 +310,17 @@ export default function RoleAccountModal({
role="radio"
aria-checked={banned}
onClick={() => setBanned(true)}
className="rounded-xl px-3 py-2.5 text-left transition-colors"
className="rounded-xl px-3.5 py-3 text-left transition-colors"
style={{
border: `1.5px solid ${banned ? "var(--danger)" : "var(--line)"}`,
background: banned ? "var(--danger-soft)" : "var(--panel-2)",
}}
>
<span
className="flex items-center gap-1.5 text-[13px] font-medium"
className="flex items-center gap-1.5 text-[13.5px] font-medium"
style={{ color: banned ? "var(--danger)" : "var(--ink)" }}
>
<Ban size={14} /> 封禁
<Ban size={15} /> 封禁
</span>
<span className="block mt-1 text-[11.5px] leading-snug" style={{ color: "var(--ink-3)" }}>
强制下线,禁止登录
@@ -328,7 +328,7 @@ export default function RoleAccountModal({
</button>
</div>
{willBan && (
<p className="mt-2 text-[12px] leading-relaxed" style={{ color: "var(--danger)" }}>
<p className="mt-2.5 text-[12px] leading-relaxed" style={{ color: "var(--danger)" }}>
封禁后立即强制下线,无法登录、发帖与回复;已发布内容保留。角色变更与封禁将一并保存。
</p>
)}
@@ -336,3 +336,89 @@ export default function RoleAccountModal({
</Modal>
);
}
function IdentityHeader({
target,
displayName,
currentMetaLabel,
}: {
target: RoleAccountTarget;
displayName: string;
currentMetaLabel: string;
}) {
const metaBits: string[] = [`ID ${target.id}`];
if (typeof target.points === "number") {
metaBits.push(`积分 ${target.points.toLocaleString("zh-CN")}`);
}
if (target.created_at) {
metaBits.push(`注册 ${formatDate(target.created_at)}`);
}
return (
<div>
<p
className="text-[11px] font-medium uppercase tracking-[0.14em] mb-3"
style={{ color: "var(--ink-3)" }}
>
角色与账号
</p>
<div
className="flex items-start gap-4 min-w-0 rounded-2xl p-3.5"
style={{ background: "var(--panel-2)", border: "1px solid var(--line)" }}
>
{/* 描边用 box-shadow,勿把实色垫在默认头像后面:.av-* 底是半透明,会透出垫色把字母对比打翻 */}
<span
className="inline-flex items-center justify-center shrink-0 rounded-full"
style={{
boxShadow: target.banned
? "0 0 0 3px var(--panel-2), 0 0 0 4.5px var(--danger)"
: "0 0 0 3px var(--panel-2), 0 0 0 4.5px color-mix(in srgb, var(--accent) 38%, transparent)",
}}
aria-hidden
>
<Avatar
name={displayName}
src={target.avatar || undefined}
size={68}
/>
</span>
<div className="min-w-0 flex-1 pt-0.5">
<p
className="text-[18px] sm:text-[20px] font-extrabold tracking-tight leading-tight truncate"
style={{ color: "var(--ink)" }}
>
{displayName}
</p>
<p className="mt-0.5 text-[13px] truncate" style={{ color: "var(--ink-2)" }}>
@{target.username}
{target.email ? ` · ${target.email}` : ""}
</p>
<div className="mt-2 flex items-center gap-1.5 flex-wrap">
<RoleBadge role={target.role} boardIds={target.board_ids} />
<span
className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium leading-[1.6]"
style={
target.banned
? { background: "var(--danger-soft)", color: "var(--danger)" }
: { background: "var(--ok-soft)", color: "var(--ok)" }
}
>
{target.banned ? "已封禁" : "正常"}
</span>
</div>
<p className="mt-2 text-[11.5px] tabular-nums leading-relaxed" style={{ color: "var(--ink-3)" }}>
{metaBits.join(" · ")}
<span className="sr-only">当前角色 {currentMetaLabel}</span>
</p>
<Link
href={`/u/${target.id}`}
className="mt-2.5 inline-flex items-center gap-1 text-[12.5px] font-medium hover:underline"
style={{ color: "var(--accent)" }}
>
查看个人主页 <ExternalLink size={12} aria-hidden />
</Link>
</div>
</div>
</div>
);
}