fix(chat): 修复暗黑模式下 Markdown 和滚动条样式 — 响应式 useIsDark hook + 动态主题切换

This commit is contained in:
2026-06-02 22:44:46 +08:00
parent ed97b30d51
commit 1f05f259d0
7 changed files with 73 additions and 12 deletions

View File

@@ -6,6 +6,7 @@ import "overlayscrollbars/styles/overlayscrollbars.css";
import { OverlayScrollbarsComponent, type OverlayScrollbarsComponentRef } from "overlayscrollbars-react";
import { useCallback, useRef, useState } from "react";
import { useIsDark } from "../../../../hooks/use-is-dark";
import { useChatScroll } from "./use-chat-scroll";
interface ChatScrollAreaProps {
@@ -18,6 +19,7 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
const scrollRef = useRef<HTMLDivElement>(null);
const osRef = useRef<OverlayScrollbarsComponentRef>(null);
const [viewportElement, setViewportElement] = useState<HTMLDivElement | null>(null);
const isDark = useIsDark();
const handleOsInitialized = useCallback(() => {
const os = osRef.current;
@@ -38,7 +40,10 @@ export function ChatScrollArea({ children, messages, status }: ChatScrollAreaPro
events={{ initialized: handleOsInitialized }}
options={{
overflow: { x: "hidden", y: "scroll" },
scrollbars: { autoHide: "move", theme: "os-theme-custom" },
scrollbars: {
autoHide: "move",
theme: isDark ? "os-theme-custom-dark" : "os-theme-custom",
},
}}
ref={osRef}
>

View File

@@ -2,6 +2,27 @@ import { CopyOutlined } from "@ant-design/icons";
import { CodeHighlighter } from "@ant-design/x";
import { App, Button, Flex, Typography } from "antd";
import React from "react";
import { oneDark, oneLight } from "react-syntax-highlighter/dist/esm/styles/prism";
import { useIsDark } from "../../../../../hooks/use-is-dark";
type SyntaxTheme = Record<string, Record<string, null | number | string>>;
const customOneDark: SyntaxTheme = {
...(oneDark as SyntaxTheme),
'pre[class*="language-"]': {
...(oneDark as SyntaxTheme)['pre[class*="language-"]'],
margin: 0,
},
};
const customOneLight: SyntaxTheme = {
...(oneLight as SyntaxTheme),
'pre[class*="language-"]': {
...(oneLight as SyntaxTheme)['pre[class*="language-"]'],
margin: 0,
},
};
interface CodeBlockWithCopyProps {
block?: boolean;
@@ -13,14 +34,14 @@ interface CodeBlockWithCopyProps {
export function CodeBlockWithCopy({ block, children, className, lang }: CodeBlockWithCopyProps) {
const { message } = App.useApp();
const isDark = useIsDark();
if (!block) {
return <code className={className}>{children}</code>;
}
const codeText = extractText(children);
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
const displayLang = lang || "plaintext";
const displayLang = lang ?? "plaintext";
const handleCopy = () => {
void navigator.clipboard.writeText(codeText).then(() => {
@@ -44,7 +65,11 @@ export function CodeBlockWithCopy({ block, children, className, lang }: CodeBloc
);
return (
<CodeHighlighter header={header} lang={displayLang}>
<CodeHighlighter
header={header}
highlightProps={{ style: (isDark ? customOneDark : customOneLight) as React.CSSProperties }}
lang={displayLang}
>
{codeText}
</CodeHighlighter>
);

View File

@@ -1,9 +1,11 @@
import { XMarkdown } from "@ant-design/x-markdown";
import "@ant-design/x-markdown/themes/dark.css";
import "@ant-design/x-markdown/themes/light.css";
import { Typography } from "antd";
import type { PartProps } from "./types";
import { useIsDark } from "../../../../../hooks/use-is-dark";
import { CodeBlockWithCopy } from "./CodeBlockWithCopy";
interface TextPartProps extends PartProps {
@@ -18,6 +20,7 @@ const xmarkdownComponents = {
export function TextPart({ isStreaming, part, role }: TextPartProps) {
const text = typeof part["text"] === "string" ? part["text"] : "";
const isDark = useIsDark();
return (
<div className="part-body">
@@ -25,7 +28,7 @@ export function TextPart({ isStreaming, part, role }: TextPartProps) {
<Typography.Paragraph className="message-body-text">{text}</Typography.Paragraph>
) : (
<XMarkdown
className="x-markdown-light"
className={isDark ? "x-markdown-dark" : "x-markdown-light"}
components={xmarkdownComponents}
content={text}
streaming={{ hasNextChunk: isStreaming }}

3
src/web/css.d.ts vendored
View File

@@ -1 +1,4 @@
declare module "*.css";
declare module "react-syntax-highlighter/dist/esm/styles/prism" {
export { oneDark, oneLight } from "react-syntax-highlighter";
}

View File

@@ -0,0 +1,6 @@
import { theme } from "antd";
export function useIsDark(): boolean {
const { token } = theme.useToken();
return token.colorBgBase === "#000";
}

View File

@@ -225,23 +225,42 @@ body {
--os-handle-interactive-area-offset: 4px;
}
.x-markdown-light table {
.os-theme-custom-dark {
--os-size: 8px;
--os-padding-perpendicular: 2px;
--os-padding-axis: 2px;
--os-track-border-radius: 10px;
--os-handle-border-radius: 10px;
--os-handle-bg: rgba(255, 255, 255, 0.15);
--os-handle-bg-hover: rgba(255, 255, 255, 0.25);
--os-handle-bg-active: rgba(255, 255, 255, 0.35);
--os-handle-min-size: 33px;
--os-handle-max-size: none;
--os-handle-interactive-area-offset: 4px;
}
.x-markdown-light table,
.x-markdown-dark table {
border-collapse: collapse;
width: 100%;
}
.x-markdown-light th,
.x-markdown-light td {
.x-markdown-light td,
.x-markdown-dark th,
.x-markdown-dark td {
border: 1px solid var(--ant-color-border);
padding: 6px 12px;
text-align: left;
}
.x-markdown-light th {
.x-markdown-light th,
.x-markdown-dark th {
background: var(--ant-color-fill-quaternary);
font-weight: 600;
}
.x-markdown-light .x-md-table-wrap {
.x-markdown-light .x-md-table-wrap,
.x-markdown-dark .x-md-table-wrap {
overflow-x: auto;
}