diff --git a/package-lock.json b/package-lock.json index 56d8326..25de6fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1181,12 +1181,44 @@ } } }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, "node_modules/grapheme-splitter": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", "license": "MIT" }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-to-jsx-runtime": { "version": "2.3.6", "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", @@ -1214,6 +1246,35 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-whitespace": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", @@ -1227,6 +1288,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/html-url-attributes": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", @@ -1441,6 +1511,21 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/mdast-util-from-markdown": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", @@ -2908,6 +2993,40 @@ "redux": "^5.0.0" } }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/remark-parse": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", @@ -3136,6 +3255,20 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unist-util-is": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", @@ -3309,6 +3442,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", + "highlight.js": "^11.11.1", "jszip": "^3.10.1", "next": "16.2.3", "react": "19.2.4", @@ -3316,6 +3450,8 @@ "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "recharts": "^3.8.1", + "rehype-highlight": "^7.0.2", + "rehype-slug": "^6.0.0", "reselect": "^5.2.0", "tailwind-merge": "^3.5.0" }, diff --git a/web/package.json b/web/package.json index e2f4988..b974b74 100644 --- a/web/package.json +++ b/web/package.json @@ -18,6 +18,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "framer-motion": "^12.38.0", + "highlight.js": "^11.11.1", "jszip": "^3.10.1", "next": "16.2.3", "react": "19.2.4", @@ -25,6 +26,8 @@ "react-markdown": "^10.1.0", "react-redux": "^9.3.0", "recharts": "^3.8.1", + "rehype-highlight": "^7.0.2", + "rehype-slug": "^6.0.0", "reselect": "^5.2.0", "tailwind-merge": "^3.5.0" }, diff --git a/web/src/app/admin/docs-view/page.tsx b/web/src/app/admin/docs-view/page.tsx index 20c4cde..9f9a727 100644 --- a/web/src/app/admin/docs-view/page.tsx +++ b/web/src/app/admin/docs-view/page.tsx @@ -5,10 +5,13 @@ import { Card, Drawer, Empty, + Input, Menu, - Spin, + Skeleton, Typography, Button, + Image, + Tooltip, type CardProps, } from "antd"; import { @@ -17,10 +20,23 @@ import { MenuOutlined, MenuFoldOutlined, MenuUnfoldOutlined, + SearchOutlined, + CopyOutlined, + LinkOutlined, } from "@ant-design/icons"; -import { useCallback, useEffect, useState, type ComponentType, type RefAttributes } from "react"; +import { + useCallback, + useEffect, + useState, + useRef, + useMemo, + type ComponentType, + type RefAttributes, +} from "react"; import type { MenuProps } from "antd"; import ReactMarkdown from "react-markdown"; +import rehypeHighlight from "rehype-highlight"; +import rehypeSlug from "rehype-slug"; import { useAuth } from "@/components/auth-provider"; import { useMobileDetection } from "@/hooks/use-mobile-detection"; @@ -30,17 +46,35 @@ import type { DocumentChapterTreeItem, } from "@/types/document"; -const { Title, Paragraph } = Typography; +const { Title, Paragraph, Text } = Typography; const AntCard = Card as unknown as ComponentType>; type MenuItem = Required["items"][number]; +function flattenDocuments(chapters: DocumentChapterTreeItem[]): { id: number; title: string; chapterPath: string }[] { + const result: { id: number; title: string; chapterPath: string }[] = []; + const walk = (items: DocumentChapterTreeItem[], path: string) => { + for (const chapter of items) { + const currentPath = path ? `${path} / ${chapter.name}` : chapter.name; + for (const doc of chapter.documents?.filter((d) => d.status === "published") ?? []) { + result.push({ id: doc.id, title: doc.title, chapterPath: currentPath }); + } + if (chapter.children) walk(chapter.children, currentPath); + } + }; + walk(chapters, ""); + return result; +} + export default function DocsViewPage() { const { user, fetchWithAuth, hasPermission } = useAuth(); const isMobile = useMobileDetection(); const [selectedDocumentId, setSelectedDocumentId] = useState(null); const [siderCollapsed, setSiderCollapsed] = useState(false); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [copiedCodeBlockId, setCopiedCodeBlockId] = useState(null); + const contentRef = useRef(null); const canRead = hasPermission("document.read") || true; @@ -65,46 +99,89 @@ export default function DocsViewPage() { enabled: !!selectedDocumentId && !!user && canRead, }); - const convertToMenuItems = useCallback((chapters: DocumentChapterTreeItem[]): MenuItem[] => { - const convert = (chapters: DocumentChapterTreeItem[]): MenuItem[] => { + const documentIndex = useMemo(() => (treeData ? flattenDocuments(treeData) : []), [treeData]); + + const filteredMenuItems = useMemo(() => { + if (!treeData) return []; + + const filterTree = (chapters: DocumentChapterTreeItem[]): MenuItem[] => { return chapters .filter((chapter) => { const hasPublishedDocs = chapter.documents?.some((doc) => doc.status === "published"); const hasPublishedChildren = chapter.children?.some((child) => - child.documents?.some((doc) => doc.status === "published") + child.documents?.some((doc) => doc.status === "published"), ); return hasPublishedDocs || hasPublishedChildren; }) .map((chapter) => { - const hasChildren = chapter.children && chapter.children.length > 0; const publishedDocs = chapter.documents?.filter((doc) => doc.status === "published") || []; - const docItems: MenuItem[] = publishedDocs.map((doc) => ({ + const matchedDocs = searchQuery + ? publishedDocs.filter( + (doc) => + doc.title.toLowerCase().includes(searchQuery.toLowerCase()) || + chapter.name.toLowerCase().includes(searchQuery.toLowerCase()), + ) + : publishedDocs; + + const childItems = chapter.children ? filterTree(chapter.children) : []; + + if (searchQuery && matchedDocs.length === 0 && childItems.length === 0) { + return null; + } + + const docItems: MenuItem[] = matchedDocs.map((doc) => ({ key: `doc-${doc.id}`, icon: , - label: doc.title, + label: ( + + {searchQuery ? highlightMatch(doc.title, searchQuery) : doc.title} + + ), })); - const childItems = hasChildren ? convert(chapter.children) : []; - return { key: `chapter-${chapter.id}`, icon: , - label: chapter.name, + label: ( + + {searchQuery ? highlightMatch(chapter.name, searchQuery) : chapter.name} + + ), children: [...docItems, ...childItems], }; - }); + }) + .filter(Boolean) as MenuItem[]; }; - return convert(chapters); - }, []); - const handleMenuClick: MenuProps["onClick"] = useCallback((e) => { - if (e.key.startsWith("doc-")) { - const docId = parseInt(e.key.replace("doc-", ""), 10); - setSelectedDocumentId(docId); - setMobileMenuOpen(false); - } - }, []); + return filterTree(treeData); + }, [treeData, searchQuery]); + + function highlightMatch(text: string, query: string): React.ReactNode { + const lower = text.toLowerCase(); + const q = query.toLowerCase(); + const idx = lower.indexOf(q); + if (idx === -1) return text; + return ( + <> + {text.slice(0, idx)} + {text.slice(idx, idx + q.length)} + {text.slice(idx + q.length)} + + ); + } + + const handleMenuClick: MenuProps["onClick"] = useCallback( + (e) => { + if (e.key.startsWith("doc-")) { + const docId = parseInt(e.key.replace("doc-", ""), 10); + setSelectedDocumentId(docId); + setMobileMenuOpen(false); + contentRef.current?.scrollTo({ top: 0, behavior: "smooth" }); + } + }, + [], + ); const selectFirstDocument = useCallback(() => { if (!treeData || treeData.length === 0 || selectedDocumentId) return; @@ -132,6 +209,16 @@ export default function DocsViewPage() { selectFirstDocument(); }, [selectFirstDocument]); + const handleCopyCode = async (code: string, blockId: string) => { + try { + await navigator.clipboard.writeText(code); + setCopiedCodeBlockId(blockId); + setTimeout(() => setCopiedCodeBlockId(null), 2000); + } catch { + // silent + } + }; + if (!user || !canRead) { return (
@@ -144,30 +231,64 @@ export default function DocsViewPage() { ); } + const skeletonMenu = ( +
+ {[0, 1, 2, 3, 4, 5].map((i) => ( +
+ +
+ ))} +
+ ); + const menuContent = ( <>
{!siderCollapsed && ( - - 操作文档 - + <> + + 操作文档 + + {!treeLoading && treeData && treeData.length > 0 && ( +
+ } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + allowClear + size="small" + /> +
+ )} + )}
{treeLoading ? ( -
- -
+ skeletonMenu ) : treeData && treeData.length > 0 ? ( -
- -
+ <> + {searchQuery && ( +
+ {`找到 ${documentIndex.filter((d) => d.title.toLowerCase().includes(searchQuery.toLowerCase())).length} 个结果`} +
+ )} +
+ +
+ ) : (
@@ -175,27 +296,41 @@ export default function DocsViewPage() { )} {!isMobile && (
-
)} ); + const skeletonContent = ( +
+ +
+ +
+
+ +
+
+ +
+
+ ); + return (
} - onClick={() => setMobileMenuOpen(true)} - > + ) : null @@ -208,12 +343,9 @@ export default function DocsViewPage() {
)} -
+
{documentLoading ? ( -
- - 加载文档中... -
+ skeletonContent ) : selectedDocument ? (
@@ -223,67 +355,106 @@ export default function DocsViewPage() {
( - + h1: ({ children, id }) => ( + <Title level={2} className="admin-docs-view-heading-anchor" id={id}> + <a href={`#${id}`} className="admin-docs-view-heading-link" aria-label="Heading anchor"> + <LinkOutlined /> + </a> {children} ), - h2: ({ children }) => ( - + h2: ({ children, id }) => ( + <Title level={3} className="admin-docs-view-heading-anchor" id={id}> + <a href={`#${id}`} className="admin-docs-view-heading-link" aria-label="Heading anchor"> + <LinkOutlined /> + </a> {children} ), - h3: ({ children }) => ( - + h3: ({ children, id }) => ( + <Title level={4} className="admin-docs-view-heading-anchor" id={id}> + <a href={`#${id}`} className="admin-docs-view-heading-link" aria-label="Heading anchor"> + <LinkOutlined /> + </a> {children} ), - h4: ({ children }) => ( - + h4: ({ children, id }) => ( + <Title level={5} className="admin-docs-view-heading-anchor" id={id}> + <a href={`#${id}`} className="admin-docs-view-heading-link" aria-label="Heading anchor"> + <LinkOutlined /> + </a> {children} ), p: ({ children }) => ( - - {children} - + {children} ), ul: ({ children }) => ( -
    - {children} -
+
    {children}
), ol: ({ children }) => ( -
    - {children} -
- ), - li: ({ children }) => ( -
  • - {children} -
  • +
      {children}
    ), + li: ({ children }) =>
  • {children}
  • , blockquote: ({ children }) => ( -
    - {children} -
    +
    {children}
    ), code: ({ children, className }) => { const isBlock = className?.includes("language-"); + const codeText = String(children).replace(/\n$/, ""); + const blockId = `code-${Math.random().toString(36).slice(2, 8)}`; return isBlock ? ( -
    -                            {children}
    -                          
    +
    +
    + + {className?.replace("language-", "") || "code"} + + +
    +
    +                              {children}
    +                            
    +
    ) : ( {children} ); }, table: ({ children }) => (
    - - {children} -
    + {children}
    +
    + ), + img: ({ src, alt }) => ( +
    + {alt + +
    + } + preview={{ + mask:
    点击预览
    , + }} + /> + {alt && ( + + {alt} + + )}
    ), }} @@ -308,16 +479,30 @@ export default function DocsViewPage() { title="文档目录" placement="left" open={isMobile && mobileMenuOpen} - width={280} + width={300} onClose={() => setMobileMenuOpen(false)} + styles={{ body: { padding: 0 } }} > - +
    + {!treeLoading && treeData && treeData.length > 0 && ( +
    + } + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + allowClear + /> +
    + )} + +
    ); diff --git a/web/src/app/globals.css b/web/src/app/globals.css index 6dfdfe2..4349f9b 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -1698,6 +1698,424 @@ body { padding: 8px 12px; font-size: 14px; } + + +/* ====================================== + docs-view 新增优化样式 + ====================================== */ + +/* 搜索框 */ +.admin-docs-view-search-wrapper { + margin-top: 12px; +} + +.admin-docs-view-search-input { + border-radius: var(--ant-border-radius); + transition: all 0.3s ease; +} + +.admin-docs-view-search-input:focus { + box-shadow: 0 0 0 2px color-mix(in srgb, var(--ant-color-primary) 20%, transparent); +} + +/* 搜索结果计数 */ +.admin-docs-view-search-results-count { + font-size: 12px; + color: var(--ant-color-text-secondary); + padding: 4px 16px 0; +} + +/* 搜索高亮 */ +.admin-docs-view-search-highlight { + background: color-mix(in srgb, var(--ant-color-primary) 30%, transparent); + color: var(--ant-color-primary); + padding: 0 2px; + border-radius: 2px; + font-weight: 600; +} + +/* 骨架屏目录 */ +.admin-docs-view-skeleton-menu { + padding: 16px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.admin-docs-view-skeleton-item { + height: 22px; +} + +/* 骨架屏内容 */ +.admin-docs-view-skeleton-content { + max-width: 900px; + margin: 0 auto; + padding: 24px; +} + +/* 菜单项文档标题(搜索模式) */ +.admin-docs-view-menu-doc-title, +.admin-docs-view-menu-chapter-name { + font-size: 14px; + transition: color 0.2s; +} + +/* 标题锚点 */ +.admin-docs-view-heading-anchor { + position: relative; +} + +.admin-docs-view-heading-link { + position: absolute; + left: -24px; + top: 50%; + transform: translateY(-50%); + opacity: 0; + color: var(--ant-color-text-secondary); + font-size: 14px; + transition: opacity 0.2s, color 0.2s; + display: inline-flex; + align-items: center; + justify-content: center; + width: 20px; + text-decoration: none; +} + +.admin-docs-view-heading-anchor:hover .admin-docs-view-heading-link { + opacity: 1; +} + +.admin-docs-view-heading-link:hover { + color: var(--ant-color-primary); +} + +/* 代码块包装(带标题栏) */ +.admin-docs-view-code-block-wrapper { + margin: 20px 0; + border-radius: var(--ant-border-radius); + overflow: hidden; + border: 1px solid var(--ant-color-border-secondary); +} + +.admin-docs-view-code-block-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 12px; + background: var(--fquiz-theme-table-header-bg); + border-bottom: 1px solid var(--ant-color-border-secondary); + font-size: 12px; +} + +.admin-docs-view-code-lang { + color: var(--ant-color-text-secondary); + font-family: 'Consolas', 'Monaco', 'Courier New', monospace; + text-transform: uppercase; + font-size: 11px; + font-weight: 600; + letter-spacing: 0.5px; +} + +.admin-docs-view-copy-btn { + font-size: 12px; + color: var(--ant-color-text-secondary); + transition: color 0.2s; +} + +.admin-docs-view-copy-btn:hover { + color: var(--ant-color-primary); +} + +/* 代码块(在包装器内调整) */ +.admin-docs-view-code-block-wrapper .admin-docs-view-code-block { + margin: 0; + border: none; + border-radius: 0; +} + +/* 图片包装 */ +.admin-docs-view-image-wrapper { + margin: 20px 0; + text-align: center; +} + +.admin-docs-view-content-image { + border-radius: var(--ant-border-radius); + max-width: 100%; + cursor: zoom-in; + transition: box-shadow 0.3s ease; +} + +.admin-docs-view-content-image:hover { + box-shadow: 0 4px 16px color-mix(in srgb, var(--fquiz-theme-text-primary) 12%, transparent); +} + +.admin-docs-view-image-placeholder { + min-height: 120px; + display: flex; + align-items: center; + justify-content: center; + background: var(--ant-color-fill-alter); + border-radius: var(--ant-border-radius); +} + +.admin-docs-view-image-preview-mask { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: color-mix(in srgb, black 30%, transparent); + color: white; + font-size: 14px; + border-radius: var(--ant-border-radius); + opacity: 0; + transition: opacity 0.3s ease; +} + +.admin-docs-view-image-wrapper:hover .admin-docs-view-image-preview-mask { + opacity: 1; +} + +.admin-docs-view-image-caption { + display: block; + margin-top: 8px; + font-size: 13px; + text-align: center; +} + +/* 表格增强:交替行背景 */ +.admin-docs-view-table tbody tr:nth-child(even) { + background: color-mix(in srgb, var(--ant-color-primary) 3%, transparent); +} + +.admin-docs-view-table tbody tr:hover { + background: color-mix(in srgb, var(--ant-color-primary) 8%, transparent); +} + +/* 内容区滚动条美化 */ +.admin-docs-view-content::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +.admin-docs-view-content::-webkit-scrollbar-track { + background: transparent; +} + +.admin-docs-view-content::-webkit-scrollbar-thumb { + background: var(--fquiz-scrollbar-thumb); + border-radius: 4px; + transition: background 0.2s; +} + +.admin-docs-view-content::-webkit-scrollbar-thumb:hover { + background: var(--fquiz-scrollbar-thumb-hover); +} + +/* 侧栏菜单滚动条美化 */ +.admin-docs-view-sider-menu::-webkit-scrollbar { + width: 6px; +} + +.admin-docs-view-sider-menu::-webkit-scrollbar-track { + background: transparent; +} + +.admin-docs-view-sider-menu::-webkit-scrollbar-thumb { + background: var(--fquiz-scrollbar-thumb); + border-radius: 3px; +} + +.admin-docs-view-sider-menu::-webkit-scrollbar-thumb:hover { + background: var(--fquiz-scrollbar-thumb-hover); +} + +/* 移动端 Drawer 内容 */ +.admin-docs-view-mobile-drawer-content { + display: flex; + flex-direction: column; + height: 100%; +} + +/* 引用块增强 */ +.admin-docs-view-blockquote { + margin: 20px 0; + padding: 16px 20px; + border-left: 4px solid var(--ant-color-primary); + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--ant-color-primary) 6%, transparent) 0%, + color-mix(in srgb, var(--ant-color-primary) 2%, transparent) 100% + ); + border-radius: 0 var(--ant-border-radius) var(--ant-border-radius) 0; + position: relative; +} + +.admin-docs-view-blockquote::before { + content: '"'; + position: absolute; + top: -4px; + left: 8px; + font-size: 32px; + color: color-mix(in srgb, var(--ant-color-primary) 20%, transparent); + font-family: Georgia, serif; + line-height: 1; +} + +/* 暗色主题适配 */ +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block-header { + background: color-mix(in srgb, var(--ant-color-bg-container) 80%, black); + border-bottom-color: color-mix(in srgb, var(--ant-color-border-secondary) 50%, transparent); +} + +:root[data-fquiz-theme="dark"] .admin-docs-view-table tbody tr:nth-child(even) { + background: color-mix(in srgb, black 15%, transparent); +} + +:root[data-fquiz-theme="dark"] .admin-docs-view-table tbody tr:hover { + background: color-mix(in srgb, var(--ant-color-primary) 12%, transparent); +} + +:root[data-fquiz-theme="dark"] .admin-docs-view-image-placeholder { + background: color-mix(in srgb, var(--ant-color-bg-container) 80%, black); +} + +:root[data-fquiz-theme="dark"] .admin-docs-view-blockquote { + background: linear-gradient( + 135deg, + color-mix(in srgb, var(--ant-color-primary) 10%, transparent) 0%, + color-mix(in srgb, var(--ant-color-primary) 4%, transparent) 100% + ); +} + +/* 移动端增强 */ +@media (max-width: 767px) { + .admin-docs-view-document-header { + padding-bottom: 12px; + margin-bottom: 20px; + } + + .admin-docs-view-markdown-content { + font-size: 15px; + line-height: 1.7; + } + + .admin-docs-view-image-caption { + font-size: 12px; + } + + .admin-docs-view-blockquote { + padding: 12px 16px; + margin: 16px 0; + } + + .admin-docs-view-heading-link { + left: -20px; + font-size: 12px; + } +} + +/* ====================================== + highlight.js 语法高亮主题 + ====================================== */ +.admin-docs-view-code-block code { + background: transparent; + padding: 0; + font-family: 'Consolas', 'Monaco', 'Courier New', 'JetBrains Mono', monospace; + font-size: 14px; + color: var(--ant-color-text); + line-height: 1.6; +} + +/* Light theme */ +.admin-docs-view-code-block .hljs { + color: #24292e; +} + +.admin-docs-view-code-block .hljs-keyword, +.admin-docs-view-code-block .hljs-selector-tag, +.admin-docs-view-code-block .hljs-type { color: #d73a49; } + +.admin-docs-view-code-block .hljs-string, +.admin-docs-view-code-block .hljs-addition { color: #032f62; } + +.admin-docs-view-code-block .hljs-number, +.admin-docs-view-code-block .hljs-literal { color: #005cc5; } + +.admin-docs-view-code-block .hljs-comment, +.admin-docs-view-code-block .hljs-quote { color: #6a737d; font-style: italic; } + +.admin-docs-view-code-block .hljs-built_in, +.admin-docs-view-code-block .hljs-title.class_ { color: #e36209; } + +.admin-docs-view-code-block .hljs-title.function_, +.admin-docs-view-code-block .hljs-title { color: #6f42c1; } + +.admin-docs-view-code-block .hljs-attr, +.admin-docs-view-code-block .hljs-attribute { color: #005cc5; } + +.admin-docs-view-code-block .hljs-variable, +.admin-docs-view-code-block .hljs-template-variable { color: #e36209; } + +.admin-docs-view-code-block .hljs-regexp { color: #032f62; } + +.admin-docs-view-code-block .hljs-tag { color: #22863a; } + +.admin-docs-view-code-block .hljs-name { color: #22863a; } + +.admin-docs-view-code-block .hljs-selector-class { color: #6f42c1; } + +.admin-docs-view-code-block .hljs-meta { color: #005cc5; } + +/* Dark theme */ +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs { + color: #e6e6e6; +} + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-keyword, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-selector-tag, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-type { color: #ff7b72; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-string, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-addition { color: #a5d6ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-number, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-literal { color: #79c0ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-comment, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-quote { color: #8b949e; font-style: italic; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-built_in, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-title.class_ { color: #ffa657; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-title.function_, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-title { color: #d2a8ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-attr, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-attribute { color: #79c0ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-variable, +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-template-variable { color: #ffa657; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-regexp { color: #a5d6ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-tag { color: #7ee787; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-name { color: #7ee787; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-selector-class { color: #d2a8ff; } + +:root[data-fquiz-theme="dark"] .admin-docs-view-code-block .hljs-meta { color: #79c0ff; } + +/* 内联引用:侧栏当前文档高亮增强 */ +.ant-menu-item-selected { + position: relative; +} + +.ant-menu-item-selected::after { + opacity: 1 !important; +} } /* 表格边框优化 - 仅外边框,上下圆角对齐 */