Files
hjc-web/app/components/RichText.vue
T
gxwebsoft 2b69686795 feat(app): 添加多模板关于我们页面及相关路由和404页面
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引
- 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理
- 实现/article、/case、/product及/page动态路由兼容列表与详情展示
- 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置
- 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持
- 模板增强支持CMS单页内容加载及SEO信息动态设置
- 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
2026-09-08 12:13:44 +08:00

194 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div ref="contentRef" v-html="enhancedHtml" class="prose prose-slate max-w-none" />
</template>
<script setup lang="ts">
/**
* 富文本内容渲染组件
* 用于渲染 CMS 返回的 HTML 内容
*
* 设计要点:
* 1. 未安装 @tailwindcss/typography,因此 .prose 仅作为命名空间,
* 实际排版样式由本组件自身提供。
* 2. 后台编辑器(Quill / WangEditor / Tinymce 等)常输出 ql-align-center、
* w-e-text-align-center 等 class,或内联 style="text-align:center"
* 本组件会识别这些标记并让图片真正居中,实现「所见即所得」。
*/
interface Props {
content?: string | null
}
const props = defineProps<Props>()
const contentRef = ref<HTMLElement | null>(null)
const CENTER_CLASSES = ['ql-align-center', 'w-e-text-align-center']
const RIGHT_CLASSES = ['ql-align-right', 'w-e-text-align-right']
function hasCenterMarker(el: Element): boolean {
const className = el.className || ''
const style = (el.getAttribute('style') || '').toLowerCase()
return CENTER_CLASSES.some(c => className.includes(c)) || style.includes('text-align:center') || style.includes('text-align: center')
}
/** 把新的 style 声明追加到已有 style 字符串,避免双分号 */
function appendStyle(existing: string, addition: string): string {
const trimmed = existing.trim()
return trimmed ? `${trimmed};${addition}` : addition
}
/**
* 服务端/无 DOM 环境下的简单正则增强:
* - 移除 script 标签
* - 给 align="center" 的图片追加居中样式
* - 确保图片默认 display:inline-block,使父级 text-align:center 可生效
*/
function enhanceHtmlString(html: string): string {
if (!html) return ''
let result = html.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
// 给 align="center" 的图片追加 block + auto margin
result = result.replace(/<img\b([^>]*?)align=["']center["']([^>]*)>/gi, (match, before, after) => {
const styleMatch = /style=["']([^"']*)["']/i.exec(match)
let style = styleMatch ? styleMatch[1] : ''
if (!/display\s*:/i.test(style)) style = appendStyle(style, 'display:block')
if (!/margin\s*:/i.test(style) && !/margin-left\s*:/i.test(style)) {
style = appendStyle(style, 'margin-left:auto;margin-right:auto')
}
if (styleMatch) {
return match.replace(/style=["'][^"']*["']/i, `style="${style}"`)
}
return `<img${before}align="center"${after} style="${style}">`
})
// 默认所有图片 inline-block,方便响应父级 text-align
result = result.replace(/<img\b([^>]*)>/gi, (match, attrs) => {
let a = attrs
// 懒加载 + 异步解码(性能/CLS 优化),避免重复加载阻塞渲染
if (!/\bloading\s*=/i.test(a)) a = ` loading="lazy"${a}`
if (!/\bdecoding\s*=/i.test(a)) a = ` decoding="async"${a}`
if (/display\s*:/i.test(a)) return `<img${a}>`
const styleMatch = /style=["']([^"']*)["']/i.exec(a)
if (styleMatch) {
const style = appendStyle(styleMatch[1], 'display:inline-block')
return `<img${a}`.replace(/style=["'][^"']*["']/i, `style="${style}"`)
}
return `<img${a} style="display:inline-block">`
})
return result
}
/**
* 客户端增强:基于真实 DOM 做更精确的处理
*/
function enhanceDom(root: HTMLElement) {
// 1. 给常见编辑器居中/右对齐 class 追加内联 style(确保 class 样式生效)
CENTER_CLASSES.forEach(cls => {
root.querySelectorAll(`.${cls}`).forEach(el => {
(el as HTMLElement).style.textAlign = 'center'
})
})
RIGHT_CLASSES.forEach(cls => {
root.querySelectorAll(`.${cls}`).forEach(el => {
(el as HTMLElement).style.textAlign = 'right'
})
})
// 2. 处理图片:让被父级标记为居中的图片真正居中
root.querySelectorAll('img').forEach(img => {
const parent = img.parentElement
const isCentered =
img.getAttribute('align') === 'center' ||
(parent ? hasCenterMarker(parent) || hasCenterMarker(img) : hasCenterMarker(img))
if (isCentered) {
img.style.display = 'block'
img.style.marginLeft = 'auto'
img.style.marginRight = 'auto'
} else if (!img.style.display) {
// 默认 inline-block:父级 text-align:center 时仍可居中,
// 同时保留 vertical-align 避免行高异常
img.style.display = 'inline-block'
}
if (!img.style.maxWidth) img.style.maxWidth = '100%'
if (!img.style.height) img.style.height = 'auto'
})
}
const enhancedHtml = computed(() => enhanceHtmlString(props.content || ''))
onMounted(() => {
if (contentRef.value) {
enhanceDom(contentRef.value)
}
})
// 内容变化时重新增强(例如路由切换复用组件)
watch(() => props.content, () => {
nextTick(() => {
if (contentRef.value) {
enhanceDom(contentRef.value)
}
})
})
</script>
<style>
.prose img {
max-width: 100%;
height: auto;
display: inline-block;
vertical-align: middle;
}
.prose h2, .prose h3, .prose h4 {
margin-top: 1.5em;
margin-bottom: 0.75em;
}
.prose p {
margin-bottom: 1em;
line-height: 1.75;
}
.prose ul, .prose ol {
margin-bottom: 1em;
padding-left: 1.5em;
}
.prose ul {
list-style-type: disc;
}
.prose ol {
list-style-type: decimal;
}
/* 常见富文本编辑器居中/右对齐 class */
.prose .ql-align-center,
.prose .w-e-text-align-center,
.prose [style*="text-align: center"],
.prose [style*="text-align:center"] {
text-align: center;
}
.prose .ql-align-right,
.prose .w-e-text-align-right,
.prose [style*="text-align: right"],
.prose [style*="text-align:right"] {
text-align: right;
}
/* 父级被标记居中时,内部图片也居中 */
.prose .ql-align-center img,
.prose .w-e-text-align-center img,
.prose [style*="text-align: center"] img,
.prose [style*="text-align:center"] img,
.prose img[align="center"] {
display: block;
margin-left: auto;
margin-right: auto;
}
</style>