2b69686795
- 新增404页面,优化未找到页面体验,避免被搜索引擎索引 - 增加文件代理接口,隐藏真实文件服务器地址,支持文件请求代理 - 实现/article、/case、/product及/page动态路由兼容列表与详情展示 - 添加动态CMS页面兼容入口处理旧式路径,统一路由与SEO设置 - 新增模板1、模板7、模板2、模板3关于我们页面,实现多模板支持 - 模板增强支持CMS单页内容加载及SEO信息动态设置 - 配置环境变量及Git忽略文件规则辅助开发和构建环境管理
194 lines
5.8 KiB
Vue
194 lines
5.8 KiB
Vue
<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>
|