feat: 公开页背景色支持 iframe 外部配置 + CM助手旁排行榜入口
- 新增 usePublicBackground:公开页(排行榜/分享/会商分享/计划任务)支持 嵌套方通过 ?bg=/?text= 查询参数或 postMessage 覆盖背景/文字色 - Header 左上角 CM助手 旁新增「排行榜」按钮,新标签打开公开排行榜页 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
28098b3155
commit
da677f87d5
6 files changed
+136
-14
No files matched your search
@@ -0,0 +1,88 @@
|
|||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公开页(被第三方 iframe 嵌套)的可配置背景色。
|
||||||
|
*
|
||||||
|
* 这些 `/public/*`、`/share/*`、`/roundtable-share/*` 页面会被别的系统用
|
||||||
|
* `<iframe>` 嵌进去。嵌套方希望背景色跟自己的站点保持一致,所以我们把
|
||||||
|
* 「背景色 / 前景文字色」抽成可外部传入的参数,两种传法都支持:
|
||||||
|
*
|
||||||
|
* 1. URL 查询参数(嵌套时一次性指定,最常用):
|
||||||
|
* `<iframe src=".../#/public/leaderboard?bg=%23ffffff&text=%23111111">`
|
||||||
|
* —— `bg`/`background` 设背景,`text`/`fg` 设文字色。
|
||||||
|
* 2. `postMessage`(嵌套后动态切换,比如宿主切主题时同步):
|
||||||
|
* `iframe.contentWindow.postMessage({ type: "deerflow:set-bg", bg: "#fff", text: "#111" }, "*")`
|
||||||
|
*
|
||||||
|
* 不传时返回空对象,页面沿用自带的默认配色(Tailwind 类),互不影响。
|
||||||
|
*/
|
||||||
|
export type PublicBackground = {
|
||||||
|
backgroundColor?: string;
|
||||||
|
color?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const HEX_RE = /^#?(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
||||||
|
// 允许的非 hex 颜色:rgb()/rgba()/hsl()/hsla() 与简单命名色。限定安全字符集,
|
||||||
|
// 既能覆盖常见写法,又能挡掉任何想塞进内联样式的奇怪内容。
|
||||||
|
const SAFE_COLOR_RE = /^[a-zA-Z]+$|^(?:rgb|rgba|hsl|hsla)\(\s*[0-9.,%\s/]+\)$/;
|
||||||
|
|
||||||
|
/** 把外部传入的颜色串规整成可用的 CSS 颜色;非法/为空时返回 undefined。 */
|
||||||
|
export function normalizeCssColor(raw: string | null | undefined): string | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const value = raw.trim();
|
||||||
|
if (!value) return undefined;
|
||||||
|
if (HEX_RE.test(value)) return value.startsWith("#") ? value : `#${value}`;
|
||||||
|
if (SAFE_COLOR_RE.test(value)) return value;
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function readFromSearch(search: string): PublicBackground {
|
||||||
|
const params = new URLSearchParams(search.startsWith("?") ? search : `?${search}`);
|
||||||
|
const backgroundColor = normalizeCssColor(params.get("bg") ?? params.get("background"));
|
||||||
|
const color = normalizeCssColor(params.get("text") ?? params.get("fg"));
|
||||||
|
const result: PublicBackground = {};
|
||||||
|
if (backgroundColor) result.backgroundColor = backgroundColor;
|
||||||
|
if (color) result.color = color;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取公开页背景配置,可直接 `style={...}` 展开到页面根容器。
|
||||||
|
*
|
||||||
|
* 合并两处来源:HashRouter 路由内的查询串(`?bg=` 跟在 `#/路由` 后面)以及
|
||||||
|
* `window.location.search`(`?bg=` 在 `#` 前面);路由内的更贴近当前页,优先级更高。
|
||||||
|
* 同时监听 `postMessage` 以支持嵌套后动态改色。
|
||||||
|
*/
|
||||||
|
export function usePublicBackground(): PublicBackground {
|
||||||
|
const location = useLocation();
|
||||||
|
|
||||||
|
const fromUrl = useMemo<PublicBackground>(() => {
|
||||||
|
const outer = readFromSearch(typeof window !== "undefined" ? window.location.search : "");
|
||||||
|
const inner = readFromSearch(location.search);
|
||||||
|
return { ...outer, ...inner };
|
||||||
|
}, [location.search]);
|
||||||
|
|
||||||
|
const [override, setOverride] = useState<PublicBackground>({});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function onMessage(event: MessageEvent) {
|
||||||
|
const data = event.data as
|
||||||
|
| { type?: string; bg?: string; background?: string; text?: string; fg?: string }
|
||||||
|
| null;
|
||||||
|
if (!data || typeof data !== "object") return;
|
||||||
|
if (data.type !== "deerflow:set-bg" && data.type !== "deerflow:set-background") return;
|
||||||
|
setOverride((prev) => {
|
||||||
|
const next: PublicBackground = { ...prev };
|
||||||
|
const bg = normalizeCssColor(data.bg ?? data.background);
|
||||||
|
const text = normalizeCssColor(data.text ?? data.fg);
|
||||||
|
if (bg) next.backgroundColor = bg;
|
||||||
|
if (text) next.color = text;
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.addEventListener("message", onMessage);
|
||||||
|
return () => window.removeEventListener("message", onMessage);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return useMemo(() => ({ ...fromUrl, ...override }), [fromUrl, override]);
|
||||||
|
}
|
||||||
@@ -44,6 +44,7 @@ import type {
|
|||||||
AdminLeaderboardUser,
|
AdminLeaderboardUser,
|
||||||
} from "@/core/admin/api";
|
} from "@/core/admin/api";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
import { usePublicBackground } from "@/core/embed/background";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type UserRankTab = "activity" | "questions" | "tokens" | "skills" | "efficiency";
|
type UserRankTab = "activity" | "questions" | "tokens" | "skills" | "efficiency";
|
||||||
@@ -151,6 +152,8 @@ export default function PublicLeaderboardPage() {
|
|||||||
const [query, setQuery] = useState("");
|
const [query, setQuery] = useState("");
|
||||||
const [userPage, setUserPage] = useState(1);
|
const [userPage, setUserPage] = useState(1);
|
||||||
const days = RANGE_DAYS;
|
const days = RANGE_DAYS;
|
||||||
|
// 被第三方 iframe 嵌套时,允许嵌套方通过 ?bg=/?text= 或 postMessage 改背景/文字色。
|
||||||
|
const pageBg = usePublicBackground();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
@@ -222,7 +225,7 @@ export default function PublicLeaderboardPage() {
|
|||||||
}, [userPageCount]);
|
}, [userPageCount]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen w-full bg-[#FFF7F7] text-[#231815] dark:bg-[#011226] dark:text-white">
|
<div className="min-h-screen w-full bg-[#FFF7F7] text-[#231815] dark:bg-[#011226] dark:text-white" style={pageBg}>
|
||||||
<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 sm:py-10">
|
<div className="mx-auto w-full max-w-6xl px-4 py-8 sm:px-6 sm:py-10">
|
||||||
{/* 标题区:仅展示,不含任何操作按钮 */}
|
{/* 标题区:仅展示,不含任何操作按钮 */}
|
||||||
<header className="mb-6">
|
<header className="mb-6">
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
getPublicDraftShareSnapshot,
|
getPublicDraftShareSnapshot,
|
||||||
type PublicDraftSeatDelivery,
|
type PublicDraftSeatDelivery,
|
||||||
} from "@/core/roundtable-draft-shares/api";
|
} from "@/core/roundtable-draft-shares/api";
|
||||||
|
import { usePublicBackground } from "@/core/embed/background";
|
||||||
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
import { markdownComponents } from "@/roundtable-planning/components/markdown-components";
|
||||||
|
|
||||||
/** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */
|
/** 各席位最终交付 —— 折叠手风琴(仿 HighFidelityReport 第二区块)。 */
|
||||||
@@ -96,6 +97,8 @@ export default function PublicRoundtableSharePage({
|
|||||||
report?: boolean;
|
report?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { shareCode } = useParams<{ shareCode: string }>();
|
const { shareCode } = useParams<{ shareCode: string }>();
|
||||||
|
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||||
|
const pageBg = usePublicBackground();
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["public-roundtable-share", shareCode],
|
queryKey: ["public-roundtable-share", shareCode],
|
||||||
@@ -107,7 +110,7 @@ export default function PublicRoundtableSharePage({
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm">
|
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm" style={pageBg}>
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
正在加载分享内容…
|
正在加载分享内容…
|
||||||
</div>
|
</div>
|
||||||
@@ -116,7 +119,7 @@ export default function PublicRoundtableSharePage({
|
|||||||
|
|
||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-col items-center justify-center gap-2 px-6 text-center">
|
<div className="bg-background flex h-screen flex-col items-center justify-center gap-2 px-6 text-center" style={pageBg}>
|
||||||
<p className="text-lg font-medium">分享不可用</p>
|
<p className="text-lg font-medium">分享不可用</p>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
该分享链接不存在、已被撤销,或不是只读分享。
|
该分享链接不存在、已被撤销,或不是只读分享。
|
||||||
@@ -128,7 +131,7 @@ export default function PublicRoundtableSharePage({
|
|||||||
// ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
|
// ── 报告页:整页沙箱 iframe 展示第 3 步结果绘制 ──────────────────────────────
|
||||||
if (report) {
|
if (report) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-col">
|
<div className="bg-background flex h-screen flex-col" style={pageBg}>
|
||||||
<header className="flex shrink-0 items-center gap-2 border-b px-6 py-3">
|
<header className="flex shrink-0 items-center gap-2 border-b px-6 py-3">
|
||||||
<h1 className="truncate text-base font-semibold">
|
<h1 className="truncate text-base font-semibold">
|
||||||
{data.title || "未命名会商"}
|
{data.title || "未命名会商"}
|
||||||
@@ -162,7 +165,7 @@ export default function PublicRoundtableSharePage({
|
|||||||
const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
|
const reportUrl = `${window.location.origin}${window.location.pathname}#/roundtable-share/${shareCode}/report`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-[#fef6f6] flex h-screen flex-col text-slate-800">
|
<div className="bg-[#fef6f6] flex h-screen flex-col text-slate-800" style={pageBg}>
|
||||||
<header className="flex shrink-0 flex-col gap-1 border-b border-slate-100 bg-white px-6 py-3">
|
<header className="flex shrink-0 flex-col gap-1 border-b border-slate-100 bg-white px-6 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="truncate text-base font-semibold">
|
<h1 className="truncate text-base font-semibold">
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import {
|
|||||||
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
import { MarkdownContent } from "@/components/workspace/messages/markdown-content";
|
||||||
import { HtmlPreviewFrame } from "@/components/html-preview-frame";
|
import { HtmlPreviewFrame } from "@/components/html-preview-frame";
|
||||||
import { getBackendBaseURL } from "@/core/config";
|
import { getBackendBaseURL } from "@/core/config";
|
||||||
|
import { usePublicBackground } from "@/core/embed/background";
|
||||||
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
import { downloadMarkdownAsDocx } from "@/core/scheduled-tasks/markdown-docx";
|
||||||
import { reasoningPlugins } from "@/core/streamdown";
|
import { reasoningPlugins } from "@/core/streamdown";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
@@ -289,6 +290,8 @@ export default function PublicScheduledTaskPage() {
|
|||||||
const [runListWidth, setRunListWidth] = useState(RUN_LIST_DEFAULT_WIDTH);
|
const [runListWidth, setRunListWidth] = useState(RUN_LIST_DEFAULT_WIDTH);
|
||||||
const dragStateRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
const dragStateRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||||
|
const pageBg = usePublicBackground();
|
||||||
|
|
||||||
const { data, isLoading, isError, error } = useQuery({
|
const { data, isLoading, isError, error } = useQuery({
|
||||||
queryKey: ["public-scheduled-task", taskId],
|
queryKey: ["public-scheduled-task", taskId],
|
||||||
@@ -405,7 +408,7 @@ export default function PublicScheduledTaskPage() {
|
|||||||
|
|
||||||
if (!taskId) {
|
if (!taskId) {
|
||||||
return (
|
return (
|
||||||
<div className="text-muted-foreground flex h-screen items-center justify-center text-sm">
|
<div className="text-muted-foreground flex h-screen items-center justify-center text-sm" style={pageBg}>
|
||||||
缺少任务 ID。请使用 <code className="mx-1">/public/scheduled-tasks/<task_id></code>。
|
缺少任务 ID。请使用 <code className="mx-1">/public/scheduled-tasks/<task_id></code>。
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -413,7 +416,7 @@ export default function PublicScheduledTaskPage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm">
|
<div className="text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm" style={pageBg}>
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
正在加载任务记录...
|
正在加载任务记录...
|
||||||
</div>
|
</div>
|
||||||
@@ -422,7 +425,7 @@ export default function PublicScheduledTaskPage() {
|
|||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
return (
|
return (
|
||||||
<div className="text-destructive flex h-screen items-center justify-center text-sm">
|
<div className="text-destructive flex h-screen items-center justify-center text-sm" style={pageBg}>
|
||||||
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
加载失败:{error instanceof Error ? error.message : "未知错误"}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -430,7 +433,7 @@ export default function PublicScheduledTaskPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={150}>
|
<TooltipProvider delayDuration={150}>
|
||||||
<div className="bg-muted/20 flex h-screen min-h-0 overflow-hidden p-4">
|
<div className="bg-muted/20 flex h-screen min-h-0 overflow-hidden p-4" style={pageBg}>
|
||||||
<div
|
<div
|
||||||
className="relative flex min-h-0 shrink-0 transition-[width] duration-200 ease-out"
|
className="relative flex min-h-0 shrink-0 transition-[width] duration-200 ease-out"
|
||||||
style={{ width: visibleRunListWidth }}
|
style={{ width: visibleRunListWidth }}
|
||||||
@@ -1139,6 +1142,8 @@ function RunDetailPane({ run, taskId }: { run: PublicRun; taskId: string }) {
|
|||||||
|
|
||||||
export function PublicScheduledTaskFullscreenPage() {
|
export function PublicScheduledTaskFullscreenPage() {
|
||||||
const { taskId, runId } = useParams<{ taskId: string; runId: string }>();
|
const { taskId, runId } = useParams<{ taskId: string; runId: string }>();
|
||||||
|
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||||
|
const pageBg = usePublicBackground();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const requestedTab = searchParams.get("tab");
|
const requestedTab = searchParams.get("tab");
|
||||||
const tabKind: ContentTab["kind"] =
|
const tabKind: ContentTab["kind"] =
|
||||||
@@ -1194,7 +1199,7 @@ export function PublicScheduledTaskFullscreenPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider delayDuration={150}>
|
<TooltipProvider delayDuration={150}>
|
||||||
<div className="bg-background flex h-screen min-h-0 flex-col">
|
<div className="bg-background flex h-screen min-h-0 flex-col" style={pageBg}>
|
||||||
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
<div className="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||||
<Button asChild variant="ghost" size="sm" className="h-8 px-2 text-xs">
|
<Button asChild variant="ghost" size="sm" className="h-8 px-2 text-xs">
|
||||||
<a href={backHref}>
|
<a href={backHref}>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
import { MessageList } from "@/components/workspace/messages";
|
import { MessageList } from "@/components/workspace/messages";
|
||||||
import { ThreadContext } from "@/components/workspace/messages/context";
|
import { ThreadContext } from "@/components/workspace/messages/context";
|
||||||
import { SubtasksProvider } from "@/core/tasks/context";
|
import { SubtasksProvider } from "@/core/tasks/context";
|
||||||
|
import { usePublicBackground } from "@/core/embed/background";
|
||||||
import { getPublicShareSnapshot } from "@/core/thread-shares/api";
|
import { getPublicShareSnapshot } from "@/core/thread-shares/api";
|
||||||
import type { AgentThreadState } from "@/core/threads";
|
import type { AgentThreadState } from "@/core/threads";
|
||||||
|
|
||||||
@@ -71,6 +72,8 @@ function PublicShareBody({
|
|||||||
*/
|
*/
|
||||||
export default function PublicSharePage() {
|
export default function PublicSharePage() {
|
||||||
const { shareCode } = useParams<{ shareCode: string }>();
|
const { shareCode } = useParams<{ shareCode: string }>();
|
||||||
|
// 嵌套方可用 ?bg=/?text= 或 postMessage 覆盖背景/文字色。
|
||||||
|
const pageBg = usePublicBackground();
|
||||||
|
|
||||||
const { data, isLoading, isError } = useQuery({
|
const { data, isLoading, isError } = useQuery({
|
||||||
queryKey: ["public-share", shareCode],
|
queryKey: ["public-share", shareCode],
|
||||||
@@ -101,7 +104,7 @@ export default function PublicSharePage() {
|
|||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm">
|
<div className="bg-background text-muted-foreground flex h-screen items-center justify-center gap-2 text-sm" style={pageBg}>
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
正在加载分享内容…
|
正在加载分享内容…
|
||||||
</div>
|
</div>
|
||||||
@@ -110,7 +113,7 @@ export default function PublicSharePage() {
|
|||||||
|
|
||||||
if (isError || !data) {
|
if (isError || !data) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-col items-center justify-center gap-2 px-6 text-center">
|
<div className="bg-background flex h-screen flex-col items-center justify-center gap-2 px-6 text-center" style={pageBg}>
|
||||||
<p className="text-lg font-medium">分享不可用</p>
|
<p className="text-lg font-medium">分享不可用</p>
|
||||||
<p className="text-muted-foreground text-sm">
|
<p className="text-muted-foreground text-sm">
|
||||||
该分享链接不存在、已被撤销,或不是只读分享。
|
该分享链接不存在、已被撤销,或不是只读分享。
|
||||||
@@ -120,7 +123,7 @@ export default function PublicSharePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-background flex h-screen flex-col">
|
<div className="bg-background flex h-screen flex-col" style={pageBg}>
|
||||||
<header className="flex shrink-0 flex-col gap-1 border-b px-6 py-3">
|
<header className="flex shrink-0 flex-col gap-1 border-b px-6 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="truncate text-base font-semibold">
|
<h1 className="truncate text-base font-semibold">
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Bell, ChevronDown, FileText, Moon, Palette, User } from "lucide-react";
|
import { Bell, ChevronDown, FileText, Moon, Palette, Trophy, User } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
import { NotificationDialog } from "@/components/workspace/notification-dialog";
|
||||||
@@ -54,6 +54,26 @@ const Header = ({ collapsed }: HeaderProps) => {
|
|||||||
CM助手
|
CM助手
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* 用户排行榜入口:紧挨 CM助手,点击在新标签打开免登录的公开排行榜页。 */}
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() =>
|
||||||
|
window.open("#/public/leaderboard", "_blank", "noopener")
|
||||||
|
}
|
||||||
|
title="打开用户排行榜公开页"
|
||||||
|
className={cn(
|
||||||
|
"h-8 shrink-0 gap-1.5 rounded-sm px-2 text-xs font-medium",
|
||||||
|
isDark
|
||||||
|
? "text-slate-200 hover:bg-slate-800/50 hover:text-white"
|
||||||
|
: "text-white hover:bg-[var(--brand-950)] active:bg-[#8a0618]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Trophy className="size-4" />
|
||||||
|
{!collapsed && <span>排行榜</span>}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex shrink-0 items-center gap-2">
|
<div className="flex shrink-0 items-center gap-2">
|
||||||
|
|||||||
Reference in new issue
Block a user