|
|
@@ -1,8 +1,9 @@
|
|
|
-import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
+import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react';
|
|
|
import { useNavigate } from 'react-router-dom';
|
|
|
import { BulbOutlined, InfoCircleOutlined, CheckCircleOutlined, ExclamationCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|
|
import ReactMarkdown from 'react-markdown';
|
|
|
import rehypeRaw from 'rehype-raw';
|
|
|
+import ImagePreview from './ImagePreview';
|
|
|
|
|
|
export type TocItem = { id: string; text: string; level: number };
|
|
|
|
|
|
@@ -21,9 +22,18 @@ interface MarkdownViewerProps {
|
|
|
|
|
|
const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, onToc }) => {
|
|
|
const [toc, setToc] = useState<TocItem[]>([]);
|
|
|
+ const [previewImage, setPreviewImage] = useState<string | null>(null);
|
|
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
|
const navigate = useNavigate();
|
|
|
|
|
|
+ const handleImageClick = useCallback((src: string) => {
|
|
|
+ setPreviewImage(src);
|
|
|
+ }, []);
|
|
|
+
|
|
|
+ const handleClosePreview = useCallback(() => {
|
|
|
+ setPreviewImage(null);
|
|
|
+ }, []);
|
|
|
+
|
|
|
const components = useMemo(() => {
|
|
|
const getNodeText = (node: any): string => {
|
|
|
if (node == null) return '';
|
|
|
@@ -90,12 +100,17 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, onToc }) => {
|
|
|
/>
|
|
|
);
|
|
|
},
|
|
|
- img: (props: any) => (
|
|
|
- <img
|
|
|
- style={{ maxWidth: '100%', height: 'auto', display: 'block', margin: '12px auto' }}
|
|
|
- {...props}
|
|
|
- />
|
|
|
- ),
|
|
|
+ img: (props: any) => {
|
|
|
+ const src = props.src?.startsWith('/') ? props.src : `/${props.src || ''}`;
|
|
|
+ return (
|
|
|
+ <img
|
|
|
+ style={{ maxWidth: '100%', height: 'auto', display: 'block', margin: '12px auto', cursor: 'pointer' }}
|
|
|
+ {...props}
|
|
|
+ src={src}
|
|
|
+ onClick={() => handleImageClick(src)}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
// Custom: <Tip type="info|warn|success">...children...</Tip>
|
|
|
tip: (props: any) => {
|
|
|
const type = props?.node?.properties?.type || 'info';
|
|
|
@@ -206,7 +221,7 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, onToc }) => {
|
|
|
);
|
|
|
},
|
|
|
} as any;
|
|
|
- }, []);
|
|
|
+ }, [handleImageClick, navigate]);
|
|
|
|
|
|
useEffect(() => {
|
|
|
// build toc after render
|
|
|
@@ -241,10 +256,85 @@ const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ content, onToc }) => {
|
|
|
.help-tip-success{border-radius:14px;padding:16px 18px;border-color:#d1fadf;background:#f6fef9;box-shadow:0 2px 8px rgba(4,108,78,0.04) inset, 0 1px 2px rgba(0,0,0,0.02)}
|
|
|
.help-tip-success a{color:#0f766e}
|
|
|
.help-tip-success img{border-radius:12px;box-shadow:0 10px 24px rgba(2,44,34,0.08);}
|
|
|
+ /* Update item style - same as UpdateNotification */
|
|
|
+ .help-markdown .update-item{
|
|
|
+ display:flex;
|
|
|
+ align-items:flex-start;
|
|
|
+ gap:0;
|
|
|
+ margin:20px 0;
|
|
|
+ padding:20px 24px;
|
|
|
+ background:transparent;
|
|
|
+ border-radius:0;
|
|
|
+ border:none;
|
|
|
+ transition:all 0.3s ease;
|
|
|
+ position:relative;
|
|
|
+ }
|
|
|
+ .help-markdown .update-item:first-of-type{margin-top:16px;}
|
|
|
+ .help-markdown .update-item:last-of-type{margin-bottom:16px;}
|
|
|
+ .help-markdown .update-item-image{
|
|
|
+ flex-shrink:0;
|
|
|
+ width:120px;
|
|
|
+ height:120px;
|
|
|
+ display:flex;
|
|
|
+ align-items:center;
|
|
|
+ justify-content:center;
|
|
|
+ background:transparent;
|
|
|
+ border-radius:4px;
|
|
|
+ overflow:hidden;
|
|
|
+ padding-right:28px;
|
|
|
+ position:relative;
|
|
|
+ margin-right:4px;
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-image::after{
|
|
|
+ content:'';
|
|
|
+ position:absolute;
|
|
|
+ right:0;
|
|
|
+ top:50%;
|
|
|
+ transform:translateY(-50%);
|
|
|
+ width:1px;
|
|
|
+ height:70px;
|
|
|
+ background:linear-gradient(to bottom, rgba(232,232,232,0) 0%, rgba(232,232,232,0.6) 20%, #d9d9d9 50%, rgba(232,232,232,0.6) 80%, rgba(232,232,232,0) 100%);
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-image img{
|
|
|
+ width:100%;
|
|
|
+ height:100%;
|
|
|
+ object-fit:contain;
|
|
|
+ margin:0;
|
|
|
+ border-radius:0;
|
|
|
+ padding:0;
|
|
|
+ transition:transform 0.3s ease;
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-image img:hover{
|
|
|
+ transform:scale(1.08);
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-content{
|
|
|
+ flex:1;
|
|
|
+ min-width:0;
|
|
|
+ padding-left:28px;
|
|
|
+ padding-top:2px;
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-content h4{
|
|
|
+ margin-top:0;
|
|
|
+ margin-bottom:10px;
|
|
|
+ font-size:16px;
|
|
|
+ font-weight:600;
|
|
|
+ color:#262626;
|
|
|
+ line-height:1.5;
|
|
|
+ letter-spacing:0.2px;
|
|
|
+ }
|
|
|
+ .help-markdown .update-item-content p{
|
|
|
+ margin:0;
|
|
|
+ font-size:14px;
|
|
|
+ line-height:1.8;
|
|
|
+ color:#595959;
|
|
|
+ }
|
|
|
`}</style>
|
|
|
<ReactMarkdown rehypePlugins={[rehypeRaw]} components={components}>
|
|
|
{content}
|
|
|
</ReactMarkdown>
|
|
|
+
|
|
|
+ {/* 图片预览模态框 */}
|
|
|
+ <ImagePreview src={previewImage} onClose={handleClosePreview} />
|
|
|
</div>
|
|
|
);
|
|
|
};
|