特性:在内容展示中增加视频支持

- 在小程序阅读页面中实现了视频渲染,支持单个视频项目和内嵌视频片段。
- 更新了内容解析器,以处理视频标签并提取视频源。
- 增强了视频元素的样式,以确保其正确显示和响应性。

制造材料:Cursor
This commit is contained in:
乘风
2026-04-14 16:14:21 +08:00
parent 35e18a7ccb
commit 5010e2dfba
4 changed files with 347 additions and 98 deletions

View File

@@ -7,6 +7,7 @@
* { type: 'mention', userId, nickname } — @某人,点击加好友(提交存客宝见 utils/soulBridge.submitCkbLead
* { type: 'linkTag', label, url, ... } — #链接标签,点击跳转(阅读页 onLinkTagTap外链→link-preview、小程序→navigateToMiniProgram
* { type: 'image', src, alt } — 图片
* { type: 'video', src } — 内嵌视频(管理端 rich-video-wrap / <video>
*/
/** 判断内容是否为 HTML */
@@ -56,6 +57,24 @@ function extractImgSrcFromTag(tag) {
return ''
}
/** 从 <video ...> 或包含 video 的 HTML 片段取出首个 src */
function extractVideoSrcFromTag(tag) {
const vm = tag.match(/<video[^>]*>/i)
const chunk = vm ? vm[0] : tag
return extractImgSrcFromTag(chunk)
}
function pushResolvedVideoSrc(videos, rawSrc, config) {
if (!rawSrc || typeof rawSrc !== 'string') return
const decoded = decodeEntities(rawSrc.trim())
if (!decoded) return
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
videos.push({ src })
}
/**
* 单行展示用:昵称、#标签文案、章节外标题类字段 — 合并换行、<br>、连续空白(避免 TipTap/粘贴带入异常断行)
*/
@@ -81,7 +100,7 @@ function stripTrailingAtForMention(before) {
/**
* 将一个 HTML block 字符串解析为 segments 数组
* 处理三种内联元素mention / linkTag(span) / linkTag(a) / img
* 处理内联元素mention / linkTag(span) / linkTag(a) / img / video
*/
function parseBlockToSegments(block, config) {
const segs = []
@@ -92,7 +111,7 @@ function parseBlockToSegments(block, config) {
if (token) personTokenSet.add(token)
}
// 合并匹配所有内联元素
const tokenRe = /<span[^>]*data-type="mention"[^>]*>[\s\S]*?<\/span>|<span[^>]*data-type="linkTag"[^>]*>[\s\S]*?<\/span>|<a[^>]*href="([^"]*)"[^>]*>(#[^<]*)<\/a>|<img[^>]*\/?>/gi
const tokenRe = /<span[^>]*data-type="mention"[^>]*>[\s\S]*?<\/span>|<span[^>]*data-type="linkTag"[^>]*>[\s\S]*?<\/span>|<a[^>]*href="([^"]*)"[^>]*>(#[^<]*)<\/a>|<video[^>]*>[\s\S]*?<\/video>|<video[^>]*\/>|<img[^>]*\/?>/gi
let lastEnd = 0
let m
@@ -150,6 +169,16 @@ function parseBlockToSegments(block, config) {
// 旧格式没有 tagType在 onLinkTagTap 中会按 label 匹配缓存的 linkTags 配置降级处理
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
} else if (/^<video/i.test(tag)) {
const rawSrc = extractVideoSrcFromTag(tag)
if (rawSrc) {
const decoded = decodeEntities(rawSrc)
const src =
config && config.assetBase
? resolveArticleImageSrc(decoded, config.assetBase)
: resolveArticleImageSrc(decoded, '')
segs.push({ type: 'video', src })
}
} else if (/^<img /i.test(tag)) {
// 图片src 可能为相对路径,需结合 config.assetBase
const rawSrc = extractImgSrcFromTag(tag)
@@ -233,6 +262,33 @@ function parseHtmlToSegments(html, config) {
return '\n__TABLE_' + idx + '__\n'
})
// 0.5 管理端 TipTapdiv.rich-video-wrap > video +可选caption须在剥离 div 前整体替换
const videos = []
const videoWrapRe =
/<div[^>]*(?:class="[^"]*rich-video-wrap[^"]*"|class='[^']*rich-video-wrap[^']*')[^>]*>\s*<video[^>]*>\s*<\/video>\s*(?:<div[^>]*(?:class="[^"]*rich-video-caption[^"]*"|class='[^']*rich-video-caption[^']*')[^>]*>[\s\S]*?<\/div>)?\s*<\/div>/gi
text = text.replace(videoWrapRe, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
// 未包在 rich-video-wrap 内的裸 <video>(兼容粘贴或其它导出)
text = text.replace(/<video[^>]*\/>/gi, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
text = text.replace(/<video[^>]*>\s*<\/video>/gi, (match) => {
const rawSrc = extractVideoSrcFromTag(match)
if (!rawSrc) return match
const idx = videos.length
pushResolvedVideoSrc(videos, rawSrc, config)
return '\n__VIDEO_' + idx + '__\n'
})
// 1. 提取 <h2>/<h3> → heading 占位
const headings = []
text = text.replace(/<h([2-6])[^>]*>([\s\S]*?)<\/h\1>/gi, function (_, lvl, inner) {
@@ -292,6 +348,17 @@ function parseHtmlToSegments(html, config) {
continue
}
// video整块占位
var vidM = block.trim().match(/^__VIDEO_(\d+)__$/)
if (vidM) {
var v = videos[parseInt(vidM[1], 10)]
if (v && v.src) {
lines.push('')
segments.push([{ type: 'video', src: v.src }])
}
continue
}
// heading
var hM = block.trim().match(/^__H_(\d+)__$/)
if (hM) {
@@ -345,7 +412,10 @@ function parseHtmlToSegments(html, config) {
var blockSegs = parseBlockToSegments(block, config)
if (!blockSegs.length) continue
if (blockSegs.length === 1 && blockSegs[0].type === 'image') {
if (
blockSegs.length === 1 &&
(blockSegs[0].type === 'image' || blockSegs[0].type === 'video')
) {
lines.push('')
segments.push(blockSegs)
continue