feat: 交付官方 Docker 运行时,外观改背景图并下线自定义 CSS/JS
站点/后台分轨背景与用户列表排序一并落地;生产 CORS 改走 SITE_URL,健康检查带版本号。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user