feat: 管理端板块管理与旧版数据导入,补充部署运营文档
- 新增管理端板块管理页面与后端接口(admin_board) - 新增旧版数据导入:legacyimport 服务、导入面板、importusers 命令行工具 - 聊天用户卡片、板块图标等 UI 组件与界面优化 - 补充 about/公告/1Panel 部署等文档 - gitignore 排除 dist/ 构建产物与 .agents/ 本地工具目录
This commit is contained in:
218
frontend/components/ChatUserCard.tsx
Normal file
218
frontend/components/ChatUserCard.tsx
Normal file
@@ -0,0 +1,218 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Calendar, Crown, MessageSquare, VolumeX } from "lucide-react";
|
||||
import Modal from "@/components/Modal";
|
||||
import Avatar from "@/components/Avatar";
|
||||
import RoleBadge from "@/components/RoleBadge";
|
||||
import ImageLightbox from "@/components/markdown/ImageLightbox";
|
||||
import { useAllowMessages } from "@/components/MessagesPolicyProvider";
|
||||
import { apiGetUserProfile, apiOpenDirectChat, type UserProfile, type User } from "@/lib/api";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
export type ChatUserCardTarget = {
|
||||
user: User;
|
||||
/** 群内身份(可从成员表查到时携带) */
|
||||
roomRole?: "owner" | "admin" | "member";
|
||||
muted?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* 聊天用户资料卡:头像(可进图箱)+ 基本信息 + 主页/私聊入口。
|
||||
* 公开资料拉取失败时降级为消息内已有的基础字段,不阻塞弹层。
|
||||
*/
|
||||
export default function ChatUserCard({
|
||||
target,
|
||||
meId,
|
||||
onClose,
|
||||
}: {
|
||||
target: ChatUserCardTarget;
|
||||
meId: number;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const allowMessages = useAllowMessages();
|
||||
const [profile, setProfile] = useState<UserProfile | null>(null);
|
||||
const [dmBusy, setDmBusy] = useState(false);
|
||||
const [avatarOpen, setAvatarOpen] = useState(false);
|
||||
|
||||
const { user } = target;
|
||||
const isSelf = user.id === meId;
|
||||
const name = user.nickname || user.username;
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
setProfile(null);
|
||||
apiGetUserProfile(user.id)
|
||||
.then((p) => {
|
||||
if (alive) setProfile(p);
|
||||
})
|
||||
.catch(() => {
|
||||
// 静默:资料卡仍展示消息携带的基础字段
|
||||
});
|
||||
return () => {
|
||||
alive = false;
|
||||
};
|
||||
}, [user.id]);
|
||||
|
||||
const startDM = async () => {
|
||||
setDmBusy(true);
|
||||
try {
|
||||
const { room } = await apiOpenDirectChat(user.id);
|
||||
onClose();
|
||||
router.push(`/chat/${room.id}`);
|
||||
} catch (e) {
|
||||
toast(e instanceof Error ? e.message : "无法发起私聊");
|
||||
} finally {
|
||||
setDmBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
const sig = (profile?.user.signature || "").trim();
|
||||
const canPreviewAvatar = !!user.avatar;
|
||||
const stats = profile?.stats;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
open
|
||||
title="用户资料"
|
||||
ariaLabel={`${name} 的资料卡`}
|
||||
onClose={onClose}
|
||||
maxWidthClass="max-w-sm"
|
||||
>
|
||||
{/* 头部:头像 + 昵称/角色 */}
|
||||
<div className="flex items-center gap-3.5">
|
||||
{canPreviewAvatar ? (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-zoom-in rounded-full outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)]"
|
||||
aria-label="查看头像"
|
||||
onClick={() => setAvatarOpen(true)}
|
||||
>
|
||||
<Avatar name={name} src={user.avatar || undefined} size={64} />
|
||||
</button>
|
||||
) : (
|
||||
<Avatar name={name} src={user.avatar || undefined} size={64} />
|
||||
)}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-1.5 flex-wrap">
|
||||
<p
|
||||
className="text-[16px] font-extrabold tracking-tight truncate"
|
||||
style={{ color: "var(--ink)" }}
|
||||
>
|
||||
{name}
|
||||
</p>
|
||||
{user.role && user.role !== "user" && (
|
||||
<RoleBadge role={user.role} boardIds={user.board_ids} />
|
||||
)}
|
||||
{profile?.user.banned === true && (
|
||||
<span
|
||||
className="inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-semibold"
|
||||
style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
|
||||
>
|
||||
已封禁
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="meta text-[12.5px] mt-0.5 truncate">@{user.username}</p>
|
||||
{target.roomRole === "owner" && (
|
||||
<p
|
||||
className="mt-1.5 inline-flex items-center gap-1 text-[11.5px] font-medium"
|
||||
style={{ color: "var(--gold)" }}
|
||||
>
|
||||
<Crown size={12} /> 群主
|
||||
</p>
|
||||
)}
|
||||
{target.roomRole === "admin" && (
|
||||
<p
|
||||
className="mt-1.5 inline-flex items-center gap-1 text-[11.5px] font-medium"
|
||||
style={{ color: "var(--accent)" }}
|
||||
>
|
||||
<Crown size={12} /> 群管理员
|
||||
</p>
|
||||
)}
|
||||
{target.muted && (
|
||||
<p
|
||||
className="mt-1.5 inline-flex items-center gap-1 text-[11.5px]"
|
||||
style={{ color: "var(--ink-3)" }}
|
||||
>
|
||||
<VolumeX size={12} /> 禁言中
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 签名 */}
|
||||
{sig && (
|
||||
<p
|
||||
className="mt-3 text-[13px] leading-relaxed"
|
||||
style={{ color: "var(--ink-2)" }}
|
||||
>
|
||||
{sig}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* 基本信息行 */}
|
||||
<div
|
||||
className="mt-3.5 pt-3 flex items-center gap-x-3 gap-y-1.5 flex-wrap text-[12.5px]"
|
||||
style={{ borderTop: "1px solid var(--line)", color: "var(--ink-2)" }}
|
||||
>
|
||||
<span>
|
||||
<span className="tabular-nums font-semibold" style={{ color: "var(--ink)" }}>
|
||||
{stats ? stats.post_count : "–"}
|
||||
</span>{" "}
|
||||
帖子
|
||||
</span>
|
||||
<span>
|
||||
<span className="tabular-nums font-semibold" style={{ color: "var(--ink)" }}>
|
||||
{stats ? stats.comment_count : "–"}
|
||||
</span>{" "}
|
||||
评论
|
||||
</span>
|
||||
<span>
|
||||
<span className="tabular-nums font-semibold" style={{ color: "var(--ink)" }}>
|
||||
{stats ? stats.points : "–"}
|
||||
</span>{" "}
|
||||
积分
|
||||
</span>
|
||||
{profile?.user.created_at && (
|
||||
<span className="inline-flex items-center gap-1 meta">
|
||||
<Calendar size={12} />
|
||||
{new Date(profile.user.created_at).toLocaleDateString("zh-CN")} 加入
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 操作区 */}
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Link href={`/u/${user.id}`} className="btn btn-line btn-sm flex-1 justify-center">
|
||||
用户主页
|
||||
</Link>
|
||||
{allowMessages && !isSelf && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={startDM}
|
||||
disabled={dmBusy}
|
||||
className="btn btn-accent btn-sm flex-1 justify-center gap-1.5"
|
||||
>
|
||||
<MessageSquare size={14} />
|
||||
{dmBusy ? "打开中…" : "发私信"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{avatarOpen && canPreviewAvatar && (
|
||||
<ImageLightbox
|
||||
items={[{ src: user.avatar, alt: `${name} 的头像` }]}
|
||||
index={0}
|
||||
onClose={() => setAvatarOpen(false)}
|
||||
onIndexChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user