特性:在内容展示中增加视频支持
- 在小程序阅读页面中实现了视频渲染,支持单个视频项目和内嵌视频片段。 - 更新了内容解析器,以处理视频标签并提取视频源。 - 增强了视频元素的样式,以确保其正确显示和响应性。 制造材料:Cursor
This commit is contained in:
@@ -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 管理端 TipTap:div.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
|
||||
|
||||
Reference in New Issue
Block a user