Skip to content
Toggle navigation
P
Projects
G
Groups
S
Snippets
Help
phsl
/
new-api
This project
Loading...
Sign in
Toggle navigation
Go to a project
Project
Repository
Issues
0
Merge Requests
0
Pipelines
Wiki
Snippets
Members
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Unverified
Commit
c7cf2039
authored
Apr 09, 2026
by
Seefs
Committed by
GitHub
Apr 09, 2026
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix: document render (#4153)
parent
b07f0b96
Hide whitespace changes
Inline
Side-by-side
Showing
2 changed files
with
23 additions
and
42 deletions
+23
-42
web/src/components/common/DocumentRenderer/index.jsx
+14
-35
web/src/components/layout/headerbar/ThemeToggle.jsx
+9
-7
No files found.
web/src/components/common/DocumentRenderer/index.jsx
View file @
c7cf2039
...
@@ -17,7 +17,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
...
@@ -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
,
use
Memo
,
use
State
}
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 {
...
@@ -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) => {
...
@@ -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 }) => {
...
@@ -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 }) => {
...
@@ -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 }) => {
...
@@ -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 }) => {
...
@@ -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
(
htmlS
tyles
)
{
if
(
s
tyles
)
{
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 }) => {
...
@@ -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
=
htmlS
tyles
;
styleEl
.
innerHTML
=
s
tyles
;
}
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 }) => {
...
@@ -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 }) => {
...
@@ -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 }) => {
...
@@ -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
:
html
C
ontent
}
}
dangerouslySetInnerHTML=
{
{
__html
:
html
Payload
.
c
ontent
}
}
/>
/>
</
div
>
</
div
>
</
div
>
</
div
>
...
...
web/src/components/layout/headerbar/ThemeToggle.jsx
View file @
c7cf2039
...
@@ -95,13 +95,15 @@ const ThemeToggle = ({ theme, onThemeToggle, t }) => {
...
@@ -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
>
);
);
};
};
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment