Skip to content

Commit bf31e88

Browse files
authored
feat: 添加更新详情弹窗 (#996)
* feat: 添加更新详情弹窗 * feat: 使用 marked 库增强 Markdown 渲染并添加 HTML 清理功能
1 parent 9176a99 commit bf31e88

File tree

4 files changed

+420
-23
lines changed

4 files changed

+420
-23
lines changed
Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import { NButton, NModal, NModalProvider, NSpin, NTag } from 'naive-ui'
2+
import { defineComponent, ref, watchEffect } from 'vue'
3+
import { marked } from 'marked'
4+
import { getReleaseDetails } from '../../external/api/github-check-update'
5+
import './markdown-styles.css'
6+
7+
interface ReleaseDetails {
8+
name: string
9+
body: string
10+
html_url: string
11+
published_at: string
12+
tag_name: string
13+
}
14+
15+
export const UpdateDetailModal = defineComponent({
16+
props: {
17+
show: Boolean,
18+
version: String,
19+
repo: {
20+
type: String as () => 'mx-server' | 'mx-admin',
21+
required: true,
22+
},
23+
title: String,
24+
},
25+
emits: ['update:show'],
26+
setup(props, { emit }) {
27+
const loading = ref(false)
28+
const releaseDetails = ref<ReleaseDetails | null>(null)
29+
30+
const fetchReleaseDetails = async () => {
31+
if (!props.version) return
32+
33+
loading.value = true
34+
try {
35+
const details = await getReleaseDetails(props.repo, props.version)
36+
releaseDetails.value = details
37+
} catch (error) {
38+
console.error('获取发布详情失败:', error)
39+
} finally {
40+
loading.value = false
41+
}
42+
}
43+
44+
watchEffect(() => {
45+
if (props.show && props.version) {
46+
fetchReleaseDetails()
47+
}
48+
})
49+
50+
const handleClose = () => {
51+
emit('update:show', false)
52+
}
53+
54+
const openGitHub = () => {
55+
if (releaseDetails.value?.html_url) {
56+
window.open(releaseDetails.value.html_url, '_blank')
57+
}
58+
}
59+
60+
const formatDate = (dateString: string) => {
61+
return new Date(dateString).toLocaleString('zh-CN')
62+
}
63+
64+
// 简单的 HTML 清理函数,移除潜在的危险标签和属性
65+
const sanitizeHtml = (html: string): string => {
66+
// 允许的标签和属性
67+
const allowedTags = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'br', 'strong', 'b', 'em', 'i', 'u', 'code', 'pre', 'blockquote', 'ul', 'ol', 'li', 'a', 'hr', 'table', 'thead', 'tbody', 'tr', 'th', 'td']
68+
const allowedAttributes = ['href', 'title', 'target', 'rel']
69+
70+
// 移除 script 标签和 javascript: 协议
71+
return html
72+
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
73+
.replace(/javascript:/gi, '')
74+
.replace(/on\w+\s*=/gi, '') // 移除事件处理器
75+
}
76+
77+
const formatMarkdown = (markdown: string): string => {
78+
if (!markdown) return ''
79+
80+
try {
81+
// 使用 marked 库进行专业的 markdown 渲染
82+
const result = marked.parse(markdown, {
83+
breaks: true, // 支持换行符转换为 <br>
84+
gfm: true, // 支持 GitHub Flavored Markdown
85+
})
86+
87+
// 确保返回字符串并进行安全清理
88+
const htmlString = typeof result === 'string' ? result : markdown.replace(/\n/g, '<br>')
89+
return sanitizeHtml(htmlString)
90+
} catch (error) {
91+
console.error('Markdown 渲染失败:', error)
92+
// 降级到简单的文本显示
93+
return markdown.replace(/\n/g, '<br>')
94+
}
95+
}
96+
97+
return () => (
98+
<NModal
99+
show={props.show}
100+
onUpdateShow={handleClose}
101+
preset="card"
102+
style={{ width: '600px', maxWidth: '90vw' }}
103+
title={props.title || '更新详情'}
104+
bordered={false}
105+
closable
106+
>
107+
<NSpin show={loading.value}>
108+
{releaseDetails.value ? (
109+
<div class="space-y-4">
110+
<div class="flex items-center justify-between">
111+
<div>
112+
<h3 class="text-lg font-semibold mb-2">
113+
{releaseDetails.value.name || releaseDetails.value.tag_name}
114+
</h3>
115+
<div class="flex items-center gap-2">
116+
<NTag type="info">{releaseDetails.value.tag_name}</NTag>
117+
<span class="text-sm text-gray-500">
118+
发布于 {formatDate(releaseDetails.value.published_at)}
119+
</span>
120+
</div>
121+
</div>
122+
<NButton type="primary" onClick={openGitHub}>
123+
在 GitHub 查看
124+
</NButton>
125+
</div>
126+
127+
{releaseDetails.value.body && (
128+
<div class="mt-4">
129+
<h4 class="font-medium mb-2">更新内容:</h4>
130+
<div
131+
class="prose prose-sm max-w-none p-4 bg-gray-50 rounded-lg dark:bg-gray-800 markdown-content leading-relaxed"
132+
innerHTML={formatMarkdown(releaseDetails.value.body)}
133+
/>
134+
</div>
135+
)}
136+
</div>
137+
) : !loading.value ? (
138+
<div class="text-center py-8 text-gray-500">
139+
无法获取更新详情
140+
</div>
141+
) : null}
142+
</NSpin>
143+
</NModal>
144+
)
145+
},
146+
})
147+
148+
export const useUpdateDetailModal = () => {
149+
const showModal = ref(false)
150+
const version = ref('')
151+
const repo = ref<'mx-server' | 'mx-admin'>('mx-server')
152+
const title = ref('')
153+
154+
const openModal = (params: {
155+
version: string
156+
repo: 'mx-server' | 'mx-admin'
157+
title?: string
158+
}) => {
159+
version.value = params.version
160+
repo.value = params.repo
161+
title.value = params.title || '更新详情'
162+
showModal.value = true
163+
}
164+
165+
const closeModal = () => {
166+
showModal.value = false
167+
}
168+
169+
const Modal = () => (
170+
<UpdateDetailModal
171+
show={showModal.value}
172+
onUpdate:show={(val: boolean) => showModal.value = val}
173+
version={version.value}
174+
repo={repo.value}
175+
title={title.value}
176+
/>
177+
)
178+
179+
return {
180+
openModal,
181+
closeModal,
182+
Modal,
183+
}
184+
}
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
/* Markdown 内容样式优化 */
2+
.markdown-content {
3+
color: var(--text-color);
4+
}
5+
6+
.markdown-content h1,
7+
.markdown-content h2,
8+
.markdown-content h3,
9+
.markdown-content h4,
10+
.markdown-content h5,
11+
.markdown-content h6 {
12+
margin-top: 1.5em;
13+
margin-bottom: 0.5em;
14+
font-weight: 600;
15+
line-height: 1.3;
16+
}
17+
18+
.markdown-content h1 {
19+
font-size: 1.5em;
20+
border-bottom: 2px solid #e5e7eb;
21+
padding-bottom: 0.3em;
22+
}
23+
24+
.markdown-content h2 {
25+
font-size: 1.3em;
26+
border-bottom: 1px solid #e5e7eb;
27+
padding-bottom: 0.2em;
28+
}
29+
30+
.markdown-content h3 {
31+
font-size: 1.1em;
32+
}
33+
34+
.markdown-content p {
35+
margin-bottom: 1em;
36+
line-height: 1.6;
37+
}
38+
39+
.markdown-content ul,
40+
.markdown-content ol {
41+
margin: 1em 0;
42+
padding-left: 1.5em;
43+
}
44+
45+
.markdown-content li {
46+
margin-bottom: 0.5em;
47+
line-height: 1.6;
48+
}
49+
50+
.markdown-content code {
51+
background-color: rgba(175, 184, 193, 0.2);
52+
padding: 0.2em 0.4em;
53+
border-radius: 3px;
54+
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
55+
font-size: 0.85em;
56+
}
57+
58+
.markdown-content pre {
59+
background-color: #f6f8fa;
60+
border-radius: 6px;
61+
padding: 1em;
62+
overflow-x: auto;
63+
margin: 1em 0;
64+
}
65+
66+
.markdown-content pre code {
67+
background-color: transparent;
68+
padding: 0;
69+
border-radius: 0;
70+
font-size: 0.9em;
71+
}
72+
73+
.markdown-content blockquote {
74+
border-left: 4px solid #ddd;
75+
padding-left: 1em;
76+
margin: 1em 0;
77+
color: #666;
78+
font-style: italic;
79+
}
80+
81+
.markdown-content a {
82+
color: #0969da;
83+
text-decoration: none;
84+
}
85+
86+
.markdown-content a:hover {
87+
text-decoration: underline;
88+
}
89+
90+
.markdown-content strong {
91+
font-weight: 600;
92+
}
93+
94+
.markdown-content em {
95+
font-style: italic;
96+
}
97+
98+
.markdown-content hr {
99+
border: none;
100+
border-top: 1px solid #e5e7eb;
101+
margin: 2em 0;
102+
}
103+
104+
.markdown-content table {
105+
border-collapse: collapse;
106+
width: 100%;
107+
margin: 1em 0;
108+
}
109+
110+
.markdown-content th,
111+
.markdown-content td {
112+
border: 1px solid #ddd;
113+
padding: 0.5em;
114+
text-align: left;
115+
}
116+
117+
.markdown-content th {
118+
background-color: #f6f8fa;
119+
font-weight: 600;
120+
}
121+
122+
/* 暗色主题适配 */
123+
.dark .markdown-content h1,
124+
.dark .markdown-content h2 {
125+
border-bottom-color: #374151;
126+
}
127+
128+
.dark .markdown-content pre {
129+
background-color: #374151;
130+
}
131+
132+
.dark .markdown-content blockquote {
133+
border-left-color: #6b7280;
134+
color: #9ca3af;
135+
}
136+
137+
.dark .markdown-content a {
138+
color: #60a5fa;
139+
}
140+
141+
.dark .markdown-content th {
142+
background-color: #374151;
143+
}
144+
145+
.dark .markdown-content th,
146+
.dark .markdown-content td {
147+
border-color: #4b5563;
148+
}
149+
150+
.dark .markdown-content hr {
151+
border-top-color: #374151;
152+
}
153+
154+
.dark .markdown-content code {
155+
background-color: rgba(156, 163, 175, 0.2);
156+
}

src/external/api/github-check-update.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,23 @@ export const checkUpdateFromGitHub = async () => {
1515
return {
1616
system: system.tag_name.replace(/^v/, ''),
1717
dashboard: dashboard.tag_name.replace(/^v/, ''),
18+
systemRelease: system,
19+
dashboardRelease: dashboard,
20+
}
21+
}
22+
23+
export const getReleaseDetails = async (repo: 'mx-server' | 'mx-admin', tagName: string) => {
24+
const { data } = await octokit.rest.repos.getReleaseByTag({
25+
owner: 'mx-space',
26+
repo,
27+
tag: `v${tagName}`,
28+
})
29+
30+
return {
31+
name: data.name,
32+
body: data.body,
33+
html_url: data.html_url,
34+
published_at: data.published_at,
35+
tag_name: data.tag_name,
1836
}
1937
}

0 commit comments

Comments
 (0)