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 }}