支持板块图标与主题色自定义,并优化列表加载与未保存离开提示。

新增后端图标校验与色槽规范化,前端展示 BoardBadge/骨架屏,发帖与板块管理页增加未保存确认。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-28 17:13:32 +08:00
parent e038d4f9e9
commit 55c256654b
63 changed files with 1966 additions and 714 deletions

View File

@@ -0,0 +1,71 @@
import {
Code2, Coffee, HelpCircle, MessageSquare, Lightbulb,
BookOpen, Gamepad2, Palette, Music, Camera, Heart, Zap,
Globe, Users, Briefcase, GraduationCap, ShoppingBag, MapPin,
Megaphone, Flame, Star, Folder, Wrench, Cpu, type LucideIcon,
} from 'lucide-react';
import type { Board } from '../api/types';
/** 板块主题色槽位数(与 global.css 中 board-badge--N 对应) */
export const BOARD_PALETTE_SIZE = 8;
/** 可选板块图标key 须与后端 AllowedBoardIcons 一致) */
export const BOARD_ICON_OPTIONS: { key: string; label: string; Icon: LucideIcon }[] = [
{ key: 'code-2', label: '代码', Icon: Code2 },
{ key: 'coffee', label: '咖啡', Icon: Coffee },
{ key: 'help-circle', label: '问答', Icon: HelpCircle },
{ key: 'message-square', label: '讨论', Icon: MessageSquare },
{ key: 'lightbulb', label: '灵感', Icon: Lightbulb },
{ key: 'book-open', label: '阅读', Icon: BookOpen },
{ key: 'gamepad-2', label: '游戏', Icon: Gamepad2 },
{ key: 'palette', label: '设计', Icon: Palette },
{ key: 'music', label: '音乐', Icon: Music },
{ key: 'camera', label: '摄影', Icon: Camera },
{ key: 'heart', label: '生活', Icon: Heart },
{ key: 'zap', label: '快讯', Icon: Zap },
{ key: 'globe', label: '综合', Icon: Globe },
{ key: 'users', label: '社区', Icon: Users },
{ key: 'briefcase', label: '职场', Icon: Briefcase },
{ key: 'graduation-cap', label: '学习', Icon: GraduationCap },
{ key: 'shopping-bag', label: '交易', Icon: ShoppingBag },
{ key: 'map-pin', label: '本地', Icon: MapPin },
{ key: 'megaphone', label: '公告', Icon: Megaphone },
{ key: 'flame', label: '热门', Icon: Flame },
{ key: 'star', label: '精华', Icon: Star },
{ key: 'folder', label: '资源', Icon: Folder },
{ key: 'wrench', label: '工具', Icon: Wrench },
{ key: 'cpu', label: '硬件', Icon: Cpu },
];
const ICON_MAP = Object.fromEntries(
BOARD_ICON_OPTIONS.map(o => [o.key, o.Icon]),
) as Record<string, LucideIcon>;
const DEFAULT_ICONS: LucideIcon[] = [
Code2, Coffee, HelpCircle, MessageSquare,
Lightbulb, BookOpen, Gamepad2, Palette,
];
export type BoardVisual = Pick<Board, 'id' | 'icon' | 'color_index'>;
/** 按板块 id / color_index 取稳定色槽索引 */
export function getBoardThemeIndex(board: BoardVisual | number): number {
if (typeof board === 'object' && board.color_index != null && board.color_index >= 0) {
return board.color_index % BOARD_PALETTE_SIZE;
}
const id = typeof board === 'number' ? board : board.id;
return ((id % BOARD_PALETTE_SIZE) + BOARD_PALETTE_SIZE) % BOARD_PALETTE_SIZE;
}
/** 解析板块图标:优先使用后台配置,否则按 id 回退 */
export function getBoardIcon(board: BoardVisual | number): LucideIcon {
if (typeof board === 'object' && board.icon && ICON_MAP[board.icon]) {
return ICON_MAP[board.icon];
}
return DEFAULT_ICONS[getBoardThemeIndex(board)];
}
export function getBoardIconOption(key: string | undefined) {
if (!key) return undefined;
return BOARD_ICON_OPTIONS.find(o => o.key === key);
}