Files
jiang13-bbs/frontend/lib/levels.ts
freefire 4ae57d9392 feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件
- 新增等级模型:model/level 及前端 levels 元数据
- 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置
- 积分、签到、评论等服务适配等级与徽章逻辑
2026-09-25 16:38:29 +08:00

138 lines
5.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 用户等级展示映射:等级体系(数量/阈值/颜色)由后台配置、后端下发
// (PublicSettings.levels),本模块只负责把它翻译成 CSS 变量(--lv-N-fg/-bg),
// 不持有阈值逻辑;等级数值由后端按累计获得积分计算(user.level),前端零计算。
/** 单级配置:threshold 为该级累计积分下限(Lv1 恒为 0),color 为色板 key */
export interface LevelDef {
threshold: number;
color: string;
}
/** 单级色规格:bg 为完整 background 简写值(特效档为含高光段的流动渐变) */
export interface LevelColorSpec {
fg: string;
bg: string;
/** 内描边色(黑金档金边用),默认透明 */
ring?: string;
/** 文字渐变层(完整 background 值):存在时 fg 应为 transparent,
* 文字以流动渐变呈现(鎏金字,金属质感的来源),见 .lv-text */
text?: string;
}
/**
* 等级色板:低段淡雅、高段金属流光,等级专属固定色,不随明暗主题切换。
* 段位语义:danger=红牌、accent=曜黑(黑金流光)、purple=鎏紫、gold=鎏金(暗底金渐变字)。
* 流光不靠扫光伪元素,由渐变内嵌高光段随 background-position 流动自然扫过。
*/
export const LEVEL_COLOR_TOKENS: Record<string, LevelColorSpec> = {
neutral: { fg: "#9aa3b2", bg: "rgba(148,158,176,0.16)" },
info: { fg: "#4d8dff", bg: "rgba(59,118,240,0.15)" },
teal: { fg: "#12a89b", bg: "rgba(14,148,136,0.15)" },
ok: { fg: "#27a94f", bg: "rgba(31,156,71,0.15)" },
danger: {
fg: "#ffffff",
// 90deg 水平渐变且首尾同色;size 固定 200%(配 lv-flow 位移 0→200% 严格无缝循环)
bg: "linear-gradient(90deg, #b91c1c, #ef4444 25%, #fecaca 38%, #dc2626 52%, #991b1b 78%, #b91c1c) 0% 0% / 200% 100%",
},
accent: {
fg: "#e9c46a",
bg: "linear-gradient(90deg, #16171c, #23252c 22%, #4a3d1c 36%, #23252c 52%, #16171c 78%) 0% 0% / 200% 100%",
ring: "rgba(233, 196, 106, 0.5)",
},
purple: {
fg: "#ffffff",
bg: "linear-gradient(90deg, #6d28d9, #a855f7 24%, #e9d5ff 37%, #8b5cf6 52%, #6d28d9 78%) 0% 0% / 200% 100%",
},
gold: {
// 鎏金字:暗底衬托,亮金流动渐变只在文字上(金底小面积易显廉价);
// 高光占比放大让 11px 小字保持干净亮泽
fg: "transparent",
bg: "linear-gradient(180deg, #221b0f, #100d07)",
ring: "rgba(240, 200, 120, 0.45)",
text: "linear-gradient(90deg, #c9973f, #ffe9a8 30%, #fff6d8 46%, #f0c964 66%, #c9973f) 0% 0% / 200% 100%",
},
};
export const LEVEL_COLOR_KEYS = Object.keys(LEVEL_COLOR_TOKENS);
/**
* 等级徽章共享类:LevelBadge 与后台等级管理预览必须逐字同用,
* 保证前后台胶囊大小完全一致(尺寸基准:text-11px + 紧凑行高 + px-2 圆角胶囊)
*/
export const LEVEL_BADGE_CLASS =
"lv-badge inline-flex items-center justify-center rounded-full px-2 py-[2px] text-[11px] font-semibold leading-[1.35] whitespace-nowrap";
/**
* 把 background 简写值拆为长写属性(React inline style 不允许简写与
* backgroundClip 等长写混用)。支持三种形态:
* "none" / 纯色(rgba、#hex)/ "linear-gradient(...) <position> / <size>"
*/
export function splitBackground(value: string): {
image: string;
color?: string;
position: string;
size: string;
} {
const v = value.trim();
if (v === "" || v === "none") {
return { image: "none", position: "0% 0%", size: "auto" };
}
// 渐变内部 stops 均为 hex 色,无嵌套括号,取到首个右括号即渐变结束
const m = v.match(/^(linear-gradient\([^)]*\))\s*(.*)$/);
if (m) {
const rest = m[2].trim();
const [pos, sizePart] = rest ? rest.split("/").map((s) => s.trim()) : [];
return {
image: m[1],
position: pos || "0% 0%",
size: sizePart || "auto",
};
}
return { image: "none", color: v, position: "0% 0%", size: "auto" };
}
/** 等级数量上限(CSS 变量按此档数固定下发;与后端 model.MaxLevelCount 同步) */
export const MAX_LEVEL_COUNT = 12;
/** 默认 7 级(与后端 model.DefaultLevelDefs 保持同步,异常回落用) */
export const DEFAULT_LEVEL_DEFS: LevelDef[] = [
{ threshold: 0, color: "neutral" },
{ threshold: 50, color: "info" },
{ threshold: 200, color: "teal" },
{ threshold: 500, color: "ok" },
{ threshold: 1200, color: "accent" },
{ threshold: 3000, color: "purple" },
{ threshold: 8000, color: "gold" },
];
/** 等级号钳制到 1..MAX_LEVEL_COUNT(游客/异常数据回落 Lv1) */
export function clampLevel(level?: number | null): number {
return Math.min(Math.max(Math.trunc(level ?? 1), 1), MAX_LEVEL_COUNT);
}
function tokensOf(color: string) {
return LEVEL_COLOR_TOKENS[color] ?? LEVEL_COLOR_TOKENS.neutral;
}
/**
* 生成等级颜色 CSS(:root 下的 --lv-N-fg / --lv-N-bg)。
* 固定输出 1..MAX_LEVEL_COUNT 全部档位:超配置数量的档位重复末级颜色,
* 保证配置缩级后旧数据/越界场景徽章仍有着色。
*/
export function levelVarsCSS(levels?: LevelDef[] | null): string {
const defs = levels && levels.length > 0 ? levels : DEFAULT_LEVEL_DEFS;
const lines: string[] = [];
for (let n = 1; n <= MAX_LEVEL_COUNT; n++) {
const idx = Math.min(n, defs.length) - 1;
const t = tokensOf(defs[idx]?.color ?? "");
// 文字渐变拆成 image/position/size 三个变量下发,
// 供 inline style 全长写引用(避免 React 简写/长写混用警告)
const text = splitBackground(t.text ?? "none");
lines.push(
`--lv-${n}-fg:${t.fg};--lv-${n}-bg:${t.bg};--lv-${n}-ring:${t.ring ?? "transparent"};` +
`--lv-${n}-text-image:${text.image};--lv-${n}-text-position:${text.position};--lv-${n}-text-size:${text.size}`,
);
}
return `:root{${lines.join(";")}}`;
}