|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import React, { useEffect, useState } from 'react'; |
|
|
import { API, showError } from '../../../helpers'; |
|
|
import { Empty, Card, Spin, Typography } from '@douyinfe/semi-ui'; |
|
|
const { Title } = Typography; |
|
|
import { |
|
|
IllustrationConstruction, |
|
|
IllustrationConstructionDark, |
|
|
} from '@douyinfe/semi-illustrations'; |
|
|
import { useTranslation } from 'react-i18next'; |
|
|
import MarkdownRenderer from '../markdown/MarkdownRenderer'; |
|
|
|
|
|
|
|
|
const isUrl = (content) => { |
|
|
try { |
|
|
new URL(content.trim()); |
|
|
return true; |
|
|
} catch { |
|
|
return false; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const isHtmlContent = (content) => { |
|
|
if (!content || typeof content !== 'string') return false; |
|
|
|
|
|
|
|
|
const htmlTagRegex = /<\/?[a-z][\s\S]*>/i; |
|
|
return htmlTagRegex.test(content); |
|
|
}; |
|
|
|
|
|
|
|
|
const sanitizeHtml = (html) => { |
|
|
|
|
|
const tempDiv = document.createElement('div'); |
|
|
tempDiv.innerHTML = html; |
|
|
|
|
|
|
|
|
const styles = Array.from(tempDiv.querySelectorAll('style')) |
|
|
.map(style => style.innerHTML) |
|
|
.join('\n'); |
|
|
|
|
|
|
|
|
const bodyContent = tempDiv.querySelector('body'); |
|
|
const content = bodyContent ? bodyContent.innerHTML : html; |
|
|
|
|
|
return { content, styles }; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const DocumentRenderer = ({ apiEndpoint, title, cacheKey, emptyMessage }) => { |
|
|
const { t } = useTranslation(); |
|
|
const [content, setContent] = useState(''); |
|
|
const [loading, setLoading] = useState(true); |
|
|
const [htmlStyles, setHtmlStyles] = useState(''); |
|
|
const [processedHtmlContent, setProcessedHtmlContent] = useState(''); |
|
|
|
|
|
const loadContent = async () => { |
|
|
|
|
|
const cachedContent = localStorage.getItem(cacheKey) || ''; |
|
|
if (cachedContent) { |
|
|
setContent(cachedContent); |
|
|
processContent(cachedContent); |
|
|
setLoading(false); |
|
|
} |
|
|
|
|
|
try { |
|
|
const res = await API.get(apiEndpoint); |
|
|
const { success, message, data } = res.data; |
|
|
if (success && data) { |
|
|
setContent(data); |
|
|
processContent(data); |
|
|
localStorage.setItem(cacheKey, data); |
|
|
} else { |
|
|
if (!cachedContent) { |
|
|
showError(message || emptyMessage); |
|
|
setContent(''); |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
if (!cachedContent) { |
|
|
showError(emptyMessage); |
|
|
setContent(''); |
|
|
} |
|
|
} finally { |
|
|
setLoading(false); |
|
|
} |
|
|
}; |
|
|
|
|
|
const processContent = (rawContent) => { |
|
|
if (isHtmlContent(rawContent)) { |
|
|
const { content: htmlContent, styles } = sanitizeHtml(rawContent); |
|
|
setProcessedHtmlContent(htmlContent); |
|
|
setHtmlStyles(styles); |
|
|
} else { |
|
|
setProcessedHtmlContent(''); |
|
|
setHtmlStyles(''); |
|
|
} |
|
|
}; |
|
|
|
|
|
useEffect(() => { |
|
|
loadContent(); |
|
|
}, []); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
const styleId = `document-renderer-styles-${cacheKey}`; |
|
|
|
|
|
if (htmlStyles) { |
|
|
let styleEl = document.getElementById(styleId); |
|
|
if (!styleEl) { |
|
|
styleEl = document.createElement('style'); |
|
|
styleEl.id = styleId; |
|
|
styleEl.type = 'text/css'; |
|
|
document.head.appendChild(styleEl); |
|
|
} |
|
|
styleEl.innerHTML = htmlStyles; |
|
|
} else { |
|
|
const el = document.getElementById(styleId); |
|
|
if (el) el.remove(); |
|
|
} |
|
|
|
|
|
return () => { |
|
|
const el = document.getElementById(styleId); |
|
|
if (el) el.remove(); |
|
|
}; |
|
|
}, [htmlStyles, cacheKey]); |
|
|
|
|
|
|
|
|
if (loading) { |
|
|
return ( |
|
|
<div className='flex justify-center items-center min-h-screen'> |
|
|
<Spin size='large' /> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (!content || content.trim() === '') { |
|
|
return ( |
|
|
<div className='flex justify-center items-center min-h-screen bg-gray-50'> |
|
|
<Empty |
|
|
title={t('管理员未设置' + title + '内容')} |
|
|
image={<IllustrationConstruction style={{ width: 150, height: 150 }} />} |
|
|
darkModeImage={<IllustrationConstructionDark style={{ width: 150, height: 150 }} />} |
|
|
className='p-8' |
|
|
/> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (isUrl(content)) { |
|
|
return ( |
|
|
<div className='flex justify-center items-center min-h-screen bg-gray-50 p-4'> |
|
|
<Card className='max-w-md w-full'> |
|
|
<div className='text-center'> |
|
|
<Title heading={4} className='mb-4'>{title}</Title> |
|
|
<p className='text-gray-600 mb-4'> |
|
|
{t('管理员设置了外部链接,点击下方按钮访问')} |
|
|
</p> |
|
|
<a |
|
|
href={content.trim()} |
|
|
target='_blank' |
|
|
rel='noopener noreferrer' |
|
|
title={content.trim()} |
|
|
aria-label={`${t('访问' + title)}: ${content.trim()}`} |
|
|
className='inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors' |
|
|
> |
|
|
{t('访问' + title)} |
|
|
</a> |
|
|
</div> |
|
|
</Card> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
if (isHtmlContent(content)) { |
|
|
const { content: htmlContent, styles } = sanitizeHtml(content); |
|
|
|
|
|
|
|
|
useEffect(() => { |
|
|
if (styles && styles !== htmlStyles) { |
|
|
setHtmlStyles(styles); |
|
|
} |
|
|
}, [content, styles, htmlStyles]); |
|
|
|
|
|
return ( |
|
|
<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='bg-white rounded-lg shadow-sm p-8'> |
|
|
<Title heading={2} className='text-center mb-8'>{title}</Title> |
|
|
<div |
|
|
className='prose prose-lg max-w-none' |
|
|
dangerouslySetInnerHTML={{ __html: htmlContent }} |
|
|
/> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
} |
|
|
|
|
|
|
|
|
return ( |
|
|
<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='bg-white rounded-lg shadow-sm p-8'> |
|
|
<Title heading={2} className='text-center mb-8'>{title}</Title> |
|
|
<div className='prose prose-lg max-w-none'> |
|
|
<MarkdownRenderer content={content} /> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
); |
|
|
}; |
|
|
|
|
|
export default DocumentRenderer; |