Files
template-10586/app/components/RichText.vue
gxwebsoft 90f3e999e2 feat(page): 添加文章详情和栏目列表页面
- 创建了 article/[id].vue 页面用于显示栏目下文章列表
- 实现了 item/[id].vue 页面用于展示文章详情内容
- 开发了 page/[id].vue 页面用于单页内容展示
- 集成了 RichText 组件用于安全渲染富文本内容
- 实现了面包屑导航和分页功能
- 添加了搜索和刷新功能
- 完善了 SEO 元数据设置
2026-01-21 15:31:59 +08:00

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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;')
}
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>