feat(admin): 管理后台 - 统一桌面端信息架构与视觉规范
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { Modal, Tag } from "antd";
|
||||
import { Modal } from "antd";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { ScrollText } from "lucide-react";
|
||||
import { useState, type CSSProperties } from "react";
|
||||
import { useState, type CSSProperties, type ReactNode } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { aceternityMotion } from "@/lib/aceternity-motion";
|
||||
@@ -15,16 +15,18 @@ type AppChangelogButtonProps = {
|
||||
showLabel?: boolean;
|
||||
labelClassName?: string;
|
||||
versionClassName?: string;
|
||||
icon?: ReactNode;
|
||||
label?: ReactNode;
|
||||
};
|
||||
|
||||
export function AppChangelogButton({ className, style, showVersion = false, showLabel = false, labelClassName, versionClassName }: AppChangelogButtonProps) {
|
||||
export function AppChangelogButton({ className, style, showVersion = false, showLabel = false, labelClassName, versionClassName, icon, label = "更新日志" }: AppChangelogButtonProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" className={className} style={style} onClick={() => setOpen(true)} aria-label="查看更新日志" title="更新日志">
|
||||
<ScrollText className="size-4 shrink-0" />
|
||||
{showLabel ? <span className={`whitespace-nowrap ${labelClassName || ""}`}>更新日志</span> : null}
|
||||
{icon ?? <ScrollText className="size-4 shrink-0" />}
|
||||
{showLabel ? <span className={`whitespace-nowrap ${labelClassName || ""}`}>{label}</span> : null}
|
||||
{showVersion ? <span className={versionClassName}>v{APP_VERSION.replace(/^v/, "")}</span> : null}
|
||||
</button>
|
||||
<AppChangelogModal open={open} onClose={() => setOpen(false)} />
|
||||
@@ -34,32 +36,55 @@ export function AppChangelogButton({ className, style, showVersion = false, show
|
||||
|
||||
function AppChangelogModal({ open, onClose }: { open: boolean; onClose: () => void }) {
|
||||
const reducedMotion = useReducedMotion();
|
||||
const version = `v${APP_VERSION.replace(/^v/, "")}`;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
rootClassName="app-spatial-modal"
|
||||
title={<div className="flex min-w-0 items-center gap-3 pr-8"><span className="grid size-9 shrink-0 place-items-center rounded-full border border-border bg-muted/45"><ScrollText className="size-4" /></span><div className="min-w-0"><div className="flex items-center gap-2 text-base font-semibold">更新日志<Tag variant="filled">v{APP_VERSION.replace(/^v/, "")}</Tag></div><div className="mt-0.5 text-xs font-normal text-foreground/45">产品能力、交互与稳定性变化</div></div></div>}
|
||||
rootClassName="app-spatial-modal app-changelog-modal"
|
||||
title={
|
||||
<div className="app-changelog-heading">
|
||||
<span className="app-changelog-heading-icon">
|
||||
<ScrollText className="size-4" />
|
||||
</span>
|
||||
<div className="app-changelog-heading-copy">
|
||||
<div className="app-changelog-heading-title">更新日志</div>
|
||||
<div className="app-changelog-heading-description">按版本查看产品能力、交互与稳定性变化</div>
|
||||
</div>
|
||||
<span className="app-changelog-current-version">当前版本 {version}</span>
|
||||
</div>
|
||||
}
|
||||
open={open}
|
||||
width={760}
|
||||
width={820}
|
||||
footer={null}
|
||||
centered
|
||||
onCancel={onClose}
|
||||
styles={{ body: { maxHeight: "min(72vh, 760px)", overflowY: "auto", overscrollBehavior: "contain" } }}
|
||||
styles={{ body: { padding: 0 } }}
|
||||
modalRender={(node) => (
|
||||
<motion.div initial={reducedMotion ? false : { opacity: 0, y: 14, scale: 0.975 }} animate={{ opacity: 1, y: 0, scale: 1 }} transition={{ duration: aceternityMotion.duration.panel, ease: aceternityMotion.easing.enter }}>
|
||||
{node}
|
||||
</motion.div>
|
||||
)}
|
||||
>
|
||||
<div className="thin-scrollbar pr-2 text-sm leading-6 text-foreground/75">
|
||||
<div className="app-changelog-scroll thin-scrollbar">
|
||||
<ReactMarkdown
|
||||
components={{
|
||||
h1: ({ children }) => <h2 className="mb-4 text-xl font-semibold text-foreground">{children}</h2>,
|
||||
h2: ({ children }) => <h3 className="mb-2 mt-6 border-b border-border pb-2 text-base font-semibold text-foreground first:mt-0">{children}</h3>,
|
||||
ul: ({ children }) => <ul className="space-y-2 pl-5">{children}</ul>,
|
||||
li: ({ children }) => <li className="list-disc pl-1 marker:text-foreground/35">{children}</li>,
|
||||
p: ({ children }) => <p className="my-2">{children}</p>,
|
||||
code: ({ children }) => <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">{children}</code>,
|
||||
h1: () => null,
|
||||
h2: ({ children }) => {
|
||||
const label = String(children);
|
||||
const latest = label === "Unreleased";
|
||||
|
||||
return (
|
||||
<h3 className={`app-changelog-section-heading${latest ? " is-latest" : ""}`}>
|
||||
<span className="app-changelog-section-marker" aria-hidden="true" />
|
||||
<span>{latest ? "开发中" : label}</span>
|
||||
{latest ? <span className="app-changelog-latest-badge">最新</span> : null}
|
||||
</h3>
|
||||
);
|
||||
},
|
||||
ul: ({ children }) => <ul className="app-changelog-list">{children}</ul>,
|
||||
li: ({ children }) => <li>{children}</li>,
|
||||
p: ({ children }) => <p className="app-changelog-paragraph">{children}</p>,
|
||||
code: ({ children }) => <code className="app-changelog-code">{children}</code>,
|
||||
}}
|
||||
>
|
||||
{__APP_CHANGELOG__}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export const WORKSPACE_SIDEBAR_STORAGE_KEY = "infinite-canvas:workspace-sidebar-collapsed";
|
||||
export const WORKSPACE_SIDEBAR_CHANGE_EVENT = "workspace:sidebar-collapsed-change";
|
||||
|
||||
type WorkspaceSidebarStorage = Pick<Storage, "getItem" | "setItem">;
|
||||
|
||||
@@ -27,3 +28,21 @@ export function writeWorkspaceSidebarCollapsed(collapsed: boolean, storage?: Wor
|
||||
// localStorage 不可用时保留当前内存状态,不能阻断侧栏交互。
|
||||
}
|
||||
}
|
||||
|
||||
export function publishWorkspaceSidebarCollapsed(collapsed: boolean) {
|
||||
writeWorkspaceSidebarCollapsed(collapsed);
|
||||
window.dispatchEvent(new CustomEvent(WORKSPACE_SIDEBAR_CHANGE_EVENT, { detail: { collapsed } }));
|
||||
}
|
||||
|
||||
export function subscribeWorkspaceSidebarCollapsed(onChange: (collapsed: boolean) => void) {
|
||||
const handleChange = (event: Event) => onChange(Boolean((event as CustomEvent<{ collapsed?: boolean }>).detail?.collapsed));
|
||||
const handleStorage = (event: StorageEvent) => {
|
||||
if (event.key === WORKSPACE_SIDEBAR_STORAGE_KEY) onChange(event.newValue === "1");
|
||||
};
|
||||
window.addEventListener(WORKSPACE_SIDEBAR_CHANGE_EVENT, handleChange);
|
||||
window.addEventListener("storage", handleStorage);
|
||||
return () => {
|
||||
window.removeEventListener(WORKSPACE_SIDEBAR_CHANGE_EVENT, handleChange);
|
||||
window.removeEventListener("storage", handleStorage);
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user