// 用户等级展示映射:等级体系(数量/阈值/颜色)由后台配置、后端下发 // (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 = { 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(...) / " */ 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(";")}}`; }