Commit 2816ddfe by lijiabin

【需求 25171】refactor: 抽取富文本样式与图片预览能力

parent 8557ea8c
...@@ -299,8 +299,7 @@ import ActionMore from '@/components/common/ActionMore/index.vue' ...@@ -299,8 +299,7 @@ import ActionMore from '@/components/common/ActionMore/index.vue'
import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue' import SendMessageDialog from '@/components/common/SendMessageDialog/index.vue'
import { useUserStore } from '@/stores/user' import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
import type { Directive } from 'vue' import { useImagePreview, useNavigation } from '@/hooks'
import { useNavigation } from '@/hooks'
const userStore = useUserStore() const userStore = useUserStore()
const { userInfo } = storeToRefs(userStore) const { userInfo } = storeToRefs(userStore)
...@@ -386,116 +385,5 @@ const showBottomVideo = computed( ...@@ -386,116 +385,5 @@ const showBottomVideo = computed(
articleDetail.articleVideoUrl, articleDetail.articleVideoUrl,
) )
const showPreview = ref(false) const { showPreview, currentPreviewIndex, srcList, vImagePreview } = useImagePreview()
const currentPreviewIndex = ref(0)
const srcList = ref<string[]>([])
// 图片预览的指令
const vImagePreview: Directive = {
mounted(el: HTMLElement) {
const imgs = el.querySelectorAll('img')
imgs.forEach((item, index) => {
item.style.cursor = 'pointer'
if (item.src) {
srcList.value.push(item.src)
item.addEventListener('click', () => {
currentPreviewIndex.value = index
showPreview.value = true
})
}
})
},
}
</script> </script>
<style scoped>
.html-content {
font-size: 18px;
line-height: 1.8;
color: #374151;
word-break: break-word;
}
.html-content :deep(p) {
margin: 1em 0;
}
.html-content :deep(h1),
.html-content :deep(h2),
.html-content :deep(h3),
.html-content :deep(h4),
.html-content :deep(h5),
.html-content :deep(h6) {
margin: 1.2em 0 0.6em;
line-height: 1.4;
font-weight: 600;
color: #111827;
}
.html-content :deep(h1) {
font-size: 1.7em;
}
.html-content :deep(h2) {
font-size: 1.5em;
}
.html-content :deep(h3) {
font-size: 1.3em;
}
.html-content :deep(ul),
.html-content :deep(ol) {
margin: 1em 0;
padding-left: 1.5em;
}
.html-content :deep(ul) {
list-style: disc;
}
.html-content :deep(ol) {
list-style: decimal;
}
.html-content :deep(li) {
margin: 0.35em 0;
}
.html-content :deep(blockquote) {
margin: 1em 0;
padding: 0.6em 1em;
border-left: 4px solid #d1d5db;
color: #6b7280;
background: #f9fafb;
}
.html-content :deep(pre),
.html-content :deep(code) {
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
}
.html-content :deep(pre) {
margin: 1em 0;
padding: 0.8em 1em;
border-radius: 6px;
overflow-x: auto;
background: #f3f4f6;
}
.html-content :deep(code) {
padding: 0.1em 0.35em;
border-radius: 4px;
background: #f3f4f6;
}
.html-content :deep(pre code) {
padding: 0;
background: transparent;
}
.html-content :deep(a) {
color: #2563eb;
text-decoration: underline;
}
</style>
...@@ -6,3 +6,4 @@ export * from './useUploadImg' ...@@ -6,3 +6,4 @@ export * from './useUploadImg'
export * from './useNavigation' export * from './useNavigation'
export * from './useMessageBox.tsx' export * from './useMessageBox.tsx'
export * from './useMediaInfo' export * from './useMediaInfo'
export * from './useImagePreview'
import { ref, type Directive } from 'vue'
// 页面内共用一个预览弹窗,指令只挂在需要预览的正文 DOM 上。
export function useImagePreview() {
const showPreview = ref(false)
const currentPreviewIndex = ref(0)
const srcList = ref<string[]>([])
const imageHandlers = new WeakMap<HTMLElement, EventListener>()
// 使用捕获阶段的事件委托,动态富文本更新后无需重复扫描和绑定图片事件。
const vImagePreview: Directive = {
mounted(el: HTMLElement) {
const handler: EventListener = (event) => {
const target = event.target
if (!(target instanceof HTMLImageElement)) return
// 点击时只读取当前正文的图片,不跨问题收集,也不在更新钩子中写入响应式状态。
const imgs = Array.from(el.querySelectorAll('img')).filter((item) => item.src)
const index = imgs.indexOf(target)
if (index < 0) return
// 图片预览属于当前内容的独立操作,不能继续触发问吧卡片跳转。
event.stopPropagation()
event.preventDefault()
srcList.value = imgs.map((item) => item.src)
currentPreviewIndex.value = index
showPreview.value = true
}
imageHandlers.set(el, handler)
el.addEventListener('click', handler, true)
},
beforeUnmount(el: HTMLElement) {
const handler = imageHandlers.get(el)
if (handler) {
el.removeEventListener('click', handler, true)
imageHandlers.delete(el)
}
},
}
return {
showPreview,
currentPreviewIndex,
srcList,
vImagePreview,
}
}
@import './reset.css'; @import './reset.css';
@import './uiLib.scss'; @import './uiLib.scss';
@import './rich-content.css';
/* 解决弹窗滚轮 */ /* 解决弹窗滚轮 */
/* 统一文章与问吧富文本内容的排版规则,保持已验证的 ArticleContent 样式不变。 */
.html-content {
font-size: 18px;
line-height: 1.8;
color: #374151;
word-break: break-word;
}
.html-content img {
cursor: pointer;
}
.html-content p {
margin: 1em 0;
}
.html-content h1,
.html-content h2,
.html-content h3,
.html-content h4,
.html-content h5,
.html-content h6 {
margin: 1.2em 0 0.6em;
line-height: 1.4;
font-weight: 600;
color: #111827;
}
.html-content h1 {
font-size: 1.7em;
}
.html-content h2 {
font-size: 1.5em;
}
.html-content h3 {
font-size: 1.3em;
}
.html-content ul,
.html-content ol {
margin: 1em 0;
padding-left: 1.5em;
}
.html-content ul {
list-style: disc;
}
.html-content ol {
list-style: decimal;
}
.html-content li {
margin: 0.35em 0;
}
.html-content blockquote {
margin: 1em 0;
padding: 0.6em 1em;
border-left: 4px solid #d1d5db;
color: #6b7280;
background: #f9fafb;
}
.html-content pre,
.html-content code {
font-family: SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
}
.html-content pre {
margin: 1em 0;
padding: 0.8em 1em;
border-radius: 6px;
overflow-x: auto;
background: #f3f4f6;
}
.html-content code {
padding: 0.1em 0.35em;
border-radius: 4px;
background: #f3f4f6;
}
.html-content pre code {
padding: 0;
background: transparent;
}
.html-content a {
color: #2563eb;
text-decoration: underline;
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment