feat: 用户徽章与等级体系,积分经济管理重构
- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
This commit is contained in:
137
frontend/lib/levels.ts
Normal file
137
frontend/lib/levels.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
// 用户等级展示映射:等级体系(数量/阈值/颜色)由后台配置、后端下发
|
||||
// (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(";")}}`;
|
||||
}
|
||||
Reference in New Issue
Block a user