|
@@ -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) => {
|
|
|
|
|
- if (isHtmlContent(rawContent)) {
|
|
|
|
|
- const { content: htmlContent, styles } = sanitizeHtml(rawContent);
|
|
|
|
|
- setProcessedHtmlContent(htmlContent);
|
|
|
|
|
- setHtmlStyles(styles);
|
|
|
|
|
- } else {
|
|
|
|
|
- setProcessedHtmlContent('');
|
|
|
|
|
- setHtmlStyles('');
|
|
|
|
|
|
|
+ const htmlPayload = useMemo(() => {
|
|
|
|
|
+ if (!isHtmlContent(content)) {
|
|
|
|
|
+ return { content: '', styles: '' };
|
|
|
}
|
|
}
|
|
|
- };
|
|
|
|
|
|
|
+ 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>
|