- 创建了 article/[id].vue 页面用于显示栏目下文章列表 - 实现了 item/[id].vue 页面用于展示文章详情内容 - 开发了 page/[id].vue 页面用于单页内容展示 - 集成了 RichText 组件用于安全渲染富文本内容 - 实现了面包屑导航和分页功能 - 添加了搜索和刷新功能 - 完善了 SEO 元数据设置
135 lines
2.9 KiB
Vue
135 lines
2.9 KiB
Vue
<template>
|
|
<div v-if="mode === 'html'" class="rich-text" v-html="normalizedHtml" />
|
|
<div v-else class="rich-text whitespace-pre-wrap break-words">
|
|
{{ text }}
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
const props = defineProps<{
|
|
content?: string | null
|
|
}>()
|
|
|
|
const text = computed(() => (typeof props.content === 'string' ? props.content : ''))
|
|
|
|
// Heuristic: treat as HTML only when it looks like it contains tags.
|
|
const mode = computed(() => (/<[a-z][\s\S]*>/i.test(text.value) ? 'html' : 'text'))
|
|
|
|
function escapeHtml(input: string) {
|
|
return input
|
|
.replaceAll('&', '&')
|
|
.replaceAll('<', '<')
|
|
.replaceAll('>', '>')
|
|
.replaceAll('"', '"')
|
|
.replaceAll("'", ''')
|
|
}
|
|
|
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
return !!value && typeof value === 'object' && !Array.isArray(value)
|
|
}
|
|
|
|
const normalizedHtml = computed(() => {
|
|
const raw = text.value.trim()
|
|
if (!raw) return ''
|
|
|
|
// Some CMS fields store JSON; try to pull out a common HTML field.
|
|
if (
|
|
(raw.startsWith('{') && raw.endsWith('}')) ||
|
|
(raw.startsWith('[') && raw.endsWith(']'))
|
|
) {
|
|
try {
|
|
const parsed: unknown = JSON.parse(raw)
|
|
|
|
let candidate = ''
|
|
if (typeof parsed === 'string') candidate = parsed
|
|
else if (isRecord(parsed)) {
|
|
const html = parsed.html
|
|
const content = parsed.content
|
|
const body = parsed.body
|
|
if (typeof html === 'string') candidate = html
|
|
else if (typeof content === 'string') candidate = content
|
|
else if (typeof body === 'string') candidate = body
|
|
}
|
|
|
|
if (candidate && /<[a-z][\s\S]*>/i.test(candidate)) return candidate
|
|
|
|
// Fallback: render JSON as <pre>.
|
|
return `<pre class="rich-pre">${escapeHtml(JSON.stringify(parsed, null, 2))}</pre>`
|
|
} catch {
|
|
// ignore JSON parse errors
|
|
}
|
|
}
|
|
|
|
return raw
|
|
})
|
|
</script>
|
|
|
|
<style scoped>
|
|
.rich-text {
|
|
color: rgba(0, 0, 0, 0.88);
|
|
line-height: 1.75;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.rich-text :deep(h1) {
|
|
font-size: 28px;
|
|
font-weight: 700;
|
|
margin: 18px 0 12px;
|
|
}
|
|
|
|
.rich-text :deep(h2) {
|
|
font-size: 22px;
|
|
font-weight: 700;
|
|
margin: 16px 0 10px;
|
|
}
|
|
|
|
.rich-text :deep(h3) {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
margin: 14px 0 8px;
|
|
}
|
|
|
|
.rich-text :deep(p) {
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.rich-text :deep(a) {
|
|
color: #1677ff;
|
|
text-decoration: underline;
|
|
}
|
|
|
|
.rich-text :deep(ul),
|
|
.rich-text :deep(ol) {
|
|
padding-left: 20px;
|
|
margin: 10px 0;
|
|
}
|
|
|
|
.rich-text :deep(li) {
|
|
margin: 6px 0;
|
|
}
|
|
|
|
.rich-text :deep(img) {
|
|
max-width: 100%;
|
|
height: auto;
|
|
display: block;
|
|
margin: 12px auto;
|
|
}
|
|
|
|
.rich-text :deep(blockquote) {
|
|
margin: 12px 0;
|
|
padding: 8px 12px;
|
|
border-left: 4px solid rgba(0, 0, 0, 0.12);
|
|
background: rgba(0, 0, 0, 0.02);
|
|
}
|
|
|
|
.rich-pre {
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
background: rgba(0, 0, 0, 0.04);
|
|
font-size: 13px;
|
|
line-height: 1.6;
|
|
}
|
|
</style>
|