增加公告图片编辑, 增加取消归档

This commit is contained in:
yml
2026-06-15 20:37:13 +08:00
parent 85f2d9b3b3
commit 9271f47553
16 changed files with 765 additions and 26 deletions
@@ -0,0 +1,60 @@
import { Marked } from 'marked'
const announcementMarkdown = new Marked({
breaks: true,
gfm: true,
renderer: {
html() {
return ''
},
link({ href, title, tokens }) {
const text = this.parser.parseInline(tokens)
if (!isSafeMarkdownURL(href)) return text
const safeTitle = title ? ` title="${escapeHTMLAttribute(title)}"` : ''
return `<a href="${escapeHTMLAttribute(href)}"${safeTitle} target="_blank" rel="noopener noreferrer">${text}</a>`
},
image({ href, title, text }) {
if (!isSafeMarkdownURL(href)) return ''
const imageText = parseImageText(text)
const safeTitle = title ? ` title="${escapeHTMLAttribute(title)}"` : ''
const widthStyle = imageText.width
? ` style="width:${imageText.width}px;max-width:100%;height:auto;"`
: ''
return `<img src="${escapeHTMLAttribute(href)}" alt="${escapeHTMLAttribute(imageText.alt)}"${safeTitle}${widthStyle}>`
},
},
})
export function renderAnnouncementMarkdown(content: string) {
return announcementMarkdown.parse(content) as string
}
function isSafeMarkdownURL(url: string) {
const value = url.trim().toLowerCase()
return (
value.startsWith('/') ||
value.startsWith('http://') ||
value.startsWith('https://') ||
value.startsWith('mailto:')
)
}
function escapeHTMLAttribute(value: string) {
return value
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
function parseImageText(text: string) {
const matched = text.match(/^(.*?)(?:\|w=(\d{2,4}))$/)
if (!matched) return { alt: text }
const width = Number(matched[2])
if (!Number.isFinite(width) || width < 120 || width > 1200) return { alt: text }
return {
alt: matched[1] || '',
width,
}
}
@@ -11,25 +11,19 @@ import {
Warning,
} from '@element-plus/icons-vue'
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
import { renderAnnouncementMarkdown } from '@/features/announcement/utils/markdown'
import { debugError } from '@/shared/utils/debug'
import { formatDateTime } from '@/shared/utils/time'
import { marked } from 'marked'
const route = useRoute()
const router = useRouter()
const loading = ref(false)
const announcement = ref<Announcement | null>(null)
// 配置 marked 选项
marked.setOptions({
breaks: true, // 支持换行
gfm: true, // GitHub Flavored Markdown
})
// 计算属性:将 Markdown 转换为 HTML
const renderedContent = computed(() => {
if (!announcement.value?.content) return ''
return marked.parse(announcement.value.content)
return renderAnnouncementMarkdown(announcement.value.content)
})
const categories = [
@@ -2,8 +2,8 @@
import { computed, onMounted, ref } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { showToast } from 'vant'
import { marked } from 'marked'
import { fetchAnnouncementDetail, type Announcement } from '@/features/announcement'
import { renderAnnouncementMarkdown } from '@/features/announcement/utils/markdown'
import { formatDateTime } from '@/shared/utils/time'
const route = useRoute()
@@ -11,11 +11,6 @@ const router = useRouter()
const loading = ref(false)
const announcement = ref<Announcement | null>(null)
marked.setOptions({
breaks: true,
gfm: true,
})
const categories = [
{ value: 'notice', label: '通知公告' },
{ value: 'tutorial', label: '使用教程' },
@@ -25,7 +20,7 @@ const categories = [
const renderedContent = computed(() => {
if (!announcement.value?.content) return ''
return marked.parse(announcement.value.content)
return renderAnnouncementMarkdown(announcement.value.content)
})
onMounted(loadAnnouncement)