feat: 实时消息与管理端运营能力

新增群聊/私信与公共 WebSocket 总线,站长用户内容档案,并将通知与待审角标改为推送驱动;同步精简管理仪表盘并修复主题换肤 DOM 冲突。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-15 22:58:16 +08:00
parent 29db9ae9b5
commit 42c395a864
55 changed files with 7607 additions and 346 deletions

View File

@@ -190,10 +190,15 @@ export function deriveAccentPair(input: string): AccentDerivation {
}
// 生成可直接注入的 CSS 文本(覆盖 globals.css 默认 accent 令牌);
// 未配置或等于默认色时返回空串,完全使用内置样式
// 未配置或等于默认色时返回空串,完全使用内置样式(SSR 场景)
export function accentStyleCSS(input: string): string {
if (!input || input.toLowerCase() === DEFAULT_ACCENT) return "";
const { light, dark } = deriveAccentPair(input);
return buildAccentCSS(input);
}
/** 始终输出一套 accent 覆盖(含默认色),供客户端 runtime 盖住 SSR 自定义 style */
function buildAccentCSS(input: string): string {
const { light, dark } = deriveAccentPair(input || DEFAULT_ACCENT);
const hi = mix(light, "#000000", 0.1);
const darkHi = mix(dark, "#ffffff", 0.14);
return [
@@ -222,22 +227,44 @@ export function accentStyleCSS(input: string): string {
// 客户端:实时换肤(后台预览 / 保存后即时生效)
// ---------------------------------------------------------------
const STYLE_ELEMENT_ID = "j13-accent-vars";
/** SSR 注入的 style(React 托管,客户端禁止 remove/改写) */
const SSR_STYLE_ID = "j13-accent-vars";
/** 客户端专用覆盖节点,挂在 body 末尾以压过 SSR */
const RUNTIME_STYLE_ID = "j13-accent-runtime";
// 将主题色应用到当前文档;传 null 移除覆盖、恢复内置默认
function ensureRuntimeStyle(): HTMLStyleElement {
let el = document.getElementById(RUNTIME_STYLE_ID) as HTMLStyleElement | null;
if (el) return el;
el = document.createElement("style");
el.id = RUNTIME_STYLE_ID;
el.setAttribute("data-accent-source", "runtime");
document.body.appendChild(el);
return el;
}
function removeRuntimeStyle(): void {
const el = document.getElementById(RUNTIME_STYLE_ID);
// 只移除我们自己创建的节点,绝不碰 React 的 SSR <style>
el?.remove();
}
// 将主题色应用到当前文档;传 null/默认色时恢复内置令牌(不拆 SSR 节点)
export function applyAccentTheme(input: string | null): void {
if (typeof document === "undefined") return;
let el = document.getElementById(STYLE_ELEMENT_ID) as HTMLStyleElement | null;
const css = input === null ? "" : accentStyleCSS(input);
if (!css) {
el?.remove();
const raw = (input || "").trim().toLowerCase();
const isDefault = !raw || raw === DEFAULT_ACCENT.toLowerCase();
if (isDefault) {
const ssr = document.getElementById(SSR_STYLE_ID);
const ssrHasCustom = !!(ssr && ssr.textContent && ssr.textContent.trim());
if (ssrHasCustom) {
// SSR 仍挂着旧自定义色:用 runtime 写回默认覆盖,等 refresh 后再卸
ensureRuntimeStyle().textContent = buildAccentCSS(DEFAULT_ACCENT);
} else {
removeRuntimeStyle();
}
return;
}
if (!el) {
el = document.createElement("style");
el.id = STYLE_ELEMENT_ID;
document.head.appendChild(el);
}
el.setAttribute("data-accent-source", "runtime");
el.textContent = css;
ensureRuntimeStyle().textContent = buildAccentCSS(raw);
}