- 新增徽章系统:badge 服务与 handler、管理端配置页、BadgeChip/LevelBadge 展示组件 - 新增等级模型:model/level 及前端 levels 元数据 - 管理端经济模块拆分为 points/badges/levels 独立页面,EconomySettings 迁入设置 - 积分、签到、评论等服务适配等级与徽章逻辑
138 lines
5.7 KiB
TypeScript
138 lines
5.7 KiB
TypeScript
// 用户等级展示映射:等级体系(数量/阈值/颜色)由后台配置、后端下发
|
||
// (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(";")}}`;
|
||
}
|