feat: 帖子收藏与用户关注
- 新增收藏功能:favorite 服务与 handler、FavoriteButton 组件 - 新增关注功能:follow 服务与 handler、FollowButton 组件 - 用户主页适配关注/粉丝与收藏展示 - flash 消息工具、广告与侧边栏组件微调
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
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> = {
|
||||
@@ -12,59 +13,92 @@ const ICONS: Record<RoleIconKey, LucideIcon> = {
|
||||
user: Users,
|
||||
};
|
||||
|
||||
/** 已知板块信息:名称 + 配色下标(0..15) */
|
||||
type BoardRef = { name: string; colorIdx: number | null };
|
||||
|
||||
/**
|
||||
* 角色徽章:图标 + 文案,配色走角色元数据中的 CSS token。
|
||||
* - board_admin 额外展示授权板块数(或板块名)
|
||||
* - variant=plain 时仅图标+文字不着底色(用于表头/文本行)
|
||||
* 角色徽章:图标 + 文案。
|
||||
* - 普通角色:配色走角色元数据中的 CSS token
|
||||
* - board_admin 提供板块名时逐板块渲染「{板块名} · 板块管理员」,配色对齐板块色
|
||||
* (av-N,深浅色自适应);truncate 窄列场景折叠为单枚「{首板块}等 N 个板块 · 板块管理员」
|
||||
* - board_admin 未提供板块名时退回「板块管理员 · N 个板块」
|
||||
*/
|
||||
export default function RoleBadge({
|
||||
role,
|
||||
boardIds,
|
||||
boardNames,
|
||||
size = 11,
|
||||
boardColors,
|
||||
size = 12,
|
||||
className = "",
|
||||
title,
|
||||
truncate = false,
|
||||
}: {
|
||||
role?: string | null;
|
||||
boardIds?: number[] | null;
|
||||
/** id -> 名称映射;提供时板块管理员展示板块名(多选时折叠为 N 个板块) */
|
||||
/** 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];
|
||||
|
||||
let label = meta.label;
|
||||
if (role === "board_admin") {
|
||||
if (boardNames) {
|
||||
const names = (boardIds ?? [])
|
||||
.map((id) => (boardNames instanceof Map ? boardNames.get(id) : boardNames[id]))
|
||||
.filter(Boolean) as string[];
|
||||
if (names.length === 1) {
|
||||
label = `${meta.label} · ${names[0]}`;
|
||||
} else if (names.length > 1) {
|
||||
label = `${meta.label} · ${names.length} 个板块`;
|
||||
}
|
||||
} else if (boardIds && boardIds.length > 0) {
|
||||
label = `${meta.label} · ${boardIds.length} 个板块`;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
// 单枚渲染:有板块配色下标时用 av-N 类(自带深浅色两套值),否则角色配色 token
|
||||
const renderBadge = (label: string, boardColorIdx: number | null, key?: number) => (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium leading-[1.6] select-none ${
|
||||
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"
|
||||
} ${className}`}
|
||||
style={{ background: `var(${meta.softToken})`, color: `var(${meta.colorToken})` }}
|
||||
} ${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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user