| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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; |