- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件 - 新增关注功能:follow 服务与 handler、FollowButton 组件 - 用户主页适配关注/粉丝与收藏展示 - flash 消息工具、广告与侧边栏组件微调
105 lines
3.9 KiB
TypeScript
105 lines
3.9 KiB
TypeScript
"use client";
|
||
|
||
import { Crown, BadgeCheck, ShieldCheck, Shield, Users, type LucideIcon } from "lucide-react";
|
||
import { roleMeta, type RoleIconKey } from "@/lib/roles";
|
||
import { BOARD_COLOR_COUNT } from "@/components/BoardIcon";
|
||
|
||
// 角色图标集中映射(图标库不含 ShieldStar,超管使用 BadgeCheck 表达"全权限认证")
|
||
const ICONS: Record<RoleIconKey, LucideIcon> = {
|
||
crown: Crown,
|
||
"shield-star": BadgeCheck,
|
||
shield: ShieldCheck,
|
||
"shield-board": Shield,
|
||
user: Users,
|
||
};
|
||
|
||
/** 已知板块信息:名称 + 配色下标(0..15) */
|
||
type BoardRef = { name: string; colorIdx: number | null };
|
||
|
||
/**
|
||
* 角色徽章:图标 + 文案。
|
||
* - 普通角色:配色走角色元数据中的 CSS token
|
||
* - board_admin 提供板块名时逐板块渲染「{板块名} · 板块管理员」,配色对齐板块色
|
||
* (av-N,深浅色自适应);truncate 窄列场景折叠为单枚「{首板块}等 N 个板块 · 板块管理员」
|
||
* - board_admin 未提供板块名时退回「板块管理员 · N 个板块」
|
||
*/
|
||
export default function RoleBadge({
|
||
role,
|
||
boardIds,
|
||
boardNames,
|
||
boardColors,
|
||
size = 12,
|
||
className = "",
|
||
title,
|
||
truncate = false,
|
||
}: {
|
||
role?: string | null;
|
||
boardIds?: number[] | null;
|
||
/** id -> 名称映射;提供时板块管理员按板块逐枚展示 */
|
||
boardNames?: Map<number, string> | Record<number, string>;
|
||
/** id -> 配色下标(boardColor 计算);提供时徽章配色对齐板块色 */
|
||
boardColors?: Map<number, number> | Record<number, number>;
|
||
size?: number;
|
||
className?: string;
|
||
title?: string;
|
||
/** 窄列场景:限制最大宽度并省略过长文案;多板块折叠为单枚 */
|
||
truncate?: boolean;
|
||
}) {
|
||
const meta = roleMeta(role);
|
||
const Icon = ICONS[meta.icon];
|
||
|
||
// 单枚渲染:有板块配色下标时用 av-N 类(自带深浅色两套值),否则角色配色 token
|
||
const renderBadge = (label: string, boardColorIdx: number | null, key?: number) => (
|
||
<span
|
||
key={key}
|
||
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-[11.5px] font-medium leading-[1.5] select-none transition-transform duration-200 hover:-translate-y-[1px] ${
|
||
truncate ? "max-w-full whitespace-nowrap" : "whitespace-nowrap"
|
||
} ${boardColorIdx != null ? `av-${boardColorIdx}` : ""} ${className}`}
|
||
style={
|
||
boardColorIdx != null
|
||
? undefined
|
||
: { background: `var(${meta.softToken})`, color: `var(${meta.colorToken})` }
|
||
}
|
||
data-tip={title ?? label}
|
||
>
|
||
<Icon size={size} className="shrink-0" />
|
||
{truncate ? <span className="overflow-hidden text-ellipsis">{label}</span> : label}
|
||
</span>
|
||
);
|
||
|
||
if (role === "board_admin" && boardNames) {
|
||
const boards = (boardIds ?? [])
|
||
.map((id): BoardRef | null => {
|
||
const name = boardNames instanceof Map ? boardNames.get(id) : boardNames[id];
|
||
if (!name) return null;
|
||
const c = boardColors
|
||
? boardColors instanceof Map
|
||
? boardColors.get(id)
|
||
: boardColors[id]
|
||
: undefined;
|
||
return { name, colorIdx: typeof c === "number" ? c % BOARD_COLOR_COUNT : null };
|
||
})
|
||
.filter((b): b is BoardRef => b !== null);
|
||
|
||
if (boards.length > 0) {
|
||
if (truncate) {
|
||
// 窄列:折叠为单枚,避免多枚徽章撑破布局
|
||
const first = boards[0];
|
||
const label =
|
||
boards.length === 1
|
||
? `${first.name} · ${meta.label}`
|
||
: `${first.name} 等 ${boards.length} 个板块 · ${meta.label}`;
|
||
return renderBadge(label, first.colorIdx);
|
||
}
|
||
return <>{boards.map((b, i) => renderBadge(`${b.name} · ${meta.label}`, b.colorIdx, i))}</>;
|
||
}
|
||
// 板块名缺失(板块被删等):回落通用渲染
|
||
}
|
||
|
||
let label = meta.label;
|
||
if (role === "board_admin" && boardIds && boardIds.length > 0) {
|
||
label = `${meta.label} · ${boardIds.length} 个板块`;
|
||
}
|
||
return renderBadge(label, null);
|
||
}
|