fix: document render (#4153)
This commit is contained in:
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||||||
For commercial licensing, please contact support@quantumnous.com
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { API, showError } from '../../../helpers';
|
import { API, showError } from '../../../helpers';
|
||||||
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui';
|
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
const { Title } = Typography;
|
const { Title } = Typography;
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import MarkdownRenderer from '../markdown/MarkdownRenderer';
|
import MarkdownRenderer from '../markdown/MarkdownRenderer';
|
||||||
|
|
||||||
// 检查是否为 URL
|
// Check whether content is a URL.
|
||||||
const isUrl = (content) => {
|
const isUrl = (content) => {
|
||||||
try {
|
try {
|
||||||
new URL(content.trim());
|
new URL(content.trim());
|
||||||
@@ -38,27 +38,23 @@ const isUrl = (content) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 检查是否为 HTML 内容
|
// Check whether content contains HTML.
|
||||||
const isHtmlContent = (content) => {
|
const isHtmlContent = (content) => {
|
||||||
if (!content || typeof content !== 'string') return false;
|
if (!content || typeof content !== 'string') return false;
|
||||||
|
|
||||||
// 检查是否包含HTML标签
|
|
||||||
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
|
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i;
|
||||||
return htmlTagRegex.test(content);
|
return htmlTagRegex.test(content);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 安全地渲染HTML内容
|
// Parse HTML content and extract inline styles.
|
||||||
const sanitizeHtml = (html) => {
|
const sanitizeHtml = (html) => {
|
||||||
// 创建一个临时元素来解析HTML
|
|
||||||
const tempDiv = document.createElement('div');
|
const tempDiv = document.createElement('div');
|
||||||
tempDiv.innerHTML = html;
|
tempDiv.innerHTML = html;
|
||||||
|
|
||||||
// 提取样式
|
|
||||||
const styles = Array.from(tempDiv.querySelectorAll('style'))
|
const styles = Array.from(tempDiv.querySelectorAll('style'))
|
||||||
.map((style) => style.innerHTML)
|
.map((style) => style.innerHTML)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
// 提取body内容,如果没有body标签则使用全部内容
|
|
||||||
const bodyContent = tempDiv.querySelector('body');
|
const bodyContent = tempDiv.querySelector('body');
|
||||||
const content = bodyContent ? bodyContent.innerHTML : html;
|
const content = bodyContent ? bodyContent.innerHTML : html;
|
||||||
|
|
||||||
@@ -76,15 +72,11 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [content, setContent] = useState('');
|
const [content, setContent] = useState('');
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [htmlStyles, setHtmlStyles] = useState('');
|
|
||||||
const [processedHtmlContent, setProcessedHtmlContent] = useState('');
|
|
||||||
|
|
||||||
const loadContent = async () => {
|
const loadContent = async () => {
|
||||||
// 先从缓存中获取
|
|
||||||
const cachedContent = localStorage.getItem(cacheKey) || '';
|
const cachedContent = localStorage.getItem(cacheKey) || '';
|
||||||
if (cachedContent) {
|
if (cachedContent) {
|
||||||
setContent(cachedContent);
|
setContent(cachedContent);
|
||||||
processContent(cachedContent);
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +85,6 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success && data) {
|
if (success && data) {
|
||||||
setContent(data);
|
setContent(data);
|
||||||
processContent(data);
|
|
||||||
localStorage.setItem(cacheKey, data);
|
localStorage.setItem(cacheKey, data);
|
||||||
} else {
|
} else {
|
||||||
if (!cachedContent) {
|
if (!cachedContent) {
|
||||||
@@ -111,16 +102,12 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const processContent = (rawContent) => {
|
const htmlPayload = useMemo(() => {
|
||||||
if (isHtmlContent(rawContent)) {
|
if (!isHtmlContent(content)) {
|
||||||
const { content: htmlContent, styles } = sanitizeHtml(rawContent);
|
return { content: '', styles: '' };
|
||||||
setProcessedHtmlContent(htmlContent);
|
|
||||||
setHtmlStyles(styles);
|
|
||||||
} else {
|
|
||||||
setProcessedHtmlContent('');
|
|
||||||
setHtmlStyles('');
|
|
||||||
}
|
}
|
||||||
};
|
return sanitizeHtml(content);
|
||||||
|
}, [content]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadContent();
|
loadContent();
|
||||||
@@ -129,8 +116,9 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
// 处理HTML样式注入
|
// 处理HTML样式注入
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const styleId = `document-renderer-styles-${cacheKey}`;
|
const styleId = `document-renderer-styles-${cacheKey}`;
|
||||||
|
const { styles } = htmlPayload;
|
||||||
|
|
||||||
if (htmlStyles) {
|
if (styles) {
|
||||||
let styleEl = document.getElementById(styleId);
|
let styleEl = document.getElementById(styleId);
|
||||||
if (!styleEl) {
|
if (!styleEl) {
|
||||||
styleEl = document.createElement('style');
|
styleEl = document.createElement('style');
|
||||||
@@ -138,7 +126,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
styleEl.type = 'text/css';
|
styleEl.type = 'text/css';
|
||||||
document.head.appendChild(styleEl);
|
document.head.appendChild(styleEl);
|
||||||
}
|
}
|
||||||
styleEl.innerHTML = htmlStyles;
|
styleEl.innerHTML = styles;
|
||||||
} else {
|
} else {
|
||||||
const el = document.getElementById(styleId);
|
const el = document.getElementById(styleId);
|
||||||
if (el) el.remove();
|
if (el) el.remove();
|
||||||
@@ -148,7 +136,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
const el = document.getElementById(styleId);
|
const el = document.getElementById(styleId);
|
||||||
if (el) el.remove();
|
if (el) el.remove();
|
||||||
};
|
};
|
||||||
}, [htmlStyles, cacheKey]);
|
}, [cacheKey, htmlPayload]);
|
||||||
|
|
||||||
// 显示加载状态
|
// 显示加载状态
|
||||||
if (loading) {
|
if (loading) {
|
||||||
@@ -207,15 +195,6 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
|
|
||||||
// 如果是 HTML 内容,直接渲染
|
// 如果是 HTML 内容,直接渲染
|
||||||
if (isHtmlContent(content)) {
|
if (isHtmlContent(content)) {
|
||||||
const { content: htmlContent, styles } = sanitizeHtml(content);
|
|
||||||
|
|
||||||
// 设置样式(如果有的话)
|
|
||||||
useEffect(() => {
|
|
||||||
if (styles && styles !== htmlStyles) {
|
|
||||||
setHtmlStyles(styles);
|
|
||||||
}
|
|
||||||
}, [content, styles, htmlStyles]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='min-h-screen bg-gray-50'>
|
<div className='min-h-screen bg-gray-50'>
|
||||||
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
|
<div className='max-w-4xl mx-auto py-12 px-4 sm:px-6 lg:px-8'>
|
||||||
@@ -225,7 +204,7 @@ const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => {
|
|||||||
</Title>
|
</Title>
|
||||||
<div
|
<div
|
||||||
className='prose prose-lg max-w-none'
|
className='prose prose-lg max-w-none'
|
||||||
dangerouslySetInnerHTML={{ __html: htmlContent }}
|
dangerouslySetInnerHTML={{ __html: htmlPayload.content }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,13 +95,15 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
|
|||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Button
|
<span className='inline-flex'>
|
||||||
icon={currentButtonIcon}
|
<Button
|
||||||
aria-label={t('切换主题')}
|
icon={currentButtonIcon}
|
||||||
theme='borderless'
|
aria-label={t('切换主题')}
|
||||||
type='tertiary'
|
theme='borderless'
|
||||||
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
|
type='tertiary'
|
||||||
/>
|
className='!p-1.5 !text-current focus:!bg-semi-color-fill-1 !rounded-full !bg-semi-color-fill-0 hover:!bg-semi-color-fill-1'
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user