更新管理端迁移Mycontent-temp的菜单与布局规范,确保主导航收敛并优化隐藏页面入口。新增相关会议记录与文档,反映团队讨论的最新决策与实施建议。
This commit is contained in:
@@ -111,31 +111,26 @@ Page({
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
|
||||
if (isLoggedIn && userInfo) {
|
||||
const readIds = app.globalData.readSectionIds || []
|
||||
const recentList = readIds.slice(-5).reverse().map(id => ({
|
||||
id,
|
||||
mid: app.getSectionMid(id),
|
||||
title: `章节 ${id}`
|
||||
}))
|
||||
|
||||
const userId = userInfo.id || ''
|
||||
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
|
||||
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
|
||||
|
||||
// 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
|
||||
// 先设基础信息;阅读统计与收益再分别从后端刷新
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
userInfo,
|
||||
userIdShort,
|
||||
userWechat,
|
||||
readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
|
||||
readCount: 0,
|
||||
referralCount: userInfo.referralCount || 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: true,
|
||||
recentChapters: recentList,
|
||||
totalReadTime: Math.floor(Math.random() * 200) + 50
|
||||
recentChapters: [],
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0
|
||||
})
|
||||
this.loadDashboardStats()
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
@@ -149,11 +144,48 @@ Page({
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: []
|
||||
recentChapters: [],
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
async loadDashboardStats() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/user/dashboard-stats?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
|
||||
if (!res?.success || !res.data) return
|
||||
|
||||
const readSectionIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds : []
|
||||
app.globalData.readSectionIds = readSectionIds
|
||||
wx.setStorageSync('readSectionIds', readSectionIds)
|
||||
|
||||
const recentChapters = Array.isArray(res.data.recentChapters)
|
||||
? res.data.recentChapters.map((item) => ({
|
||||
id: item.id,
|
||||
mid: item.mid || app.getSectionMid(item.id),
|
||||
title: item.title || `章节 ${item.id}`
|
||||
}))
|
||||
: []
|
||||
|
||||
this.setData({
|
||||
readCount: Number(res.data.readCount || 0),
|
||||
totalReadTime: Number(res.data.totalReadMinutes || 0),
|
||||
matchHistory: Number(res.data.matchHistory || 0),
|
||||
recentChapters
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[My] 拉取阅读统计失败:', e && e.message)
|
||||
}
|
||||
},
|
||||
|
||||
// 拉取待确认收款列表(用于「确认收款」按钮)
|
||||
async loadPendingConfirm() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
|
||||
@@ -77,6 +77,16 @@ Page({
|
||||
|
||||
async onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
|
||||
// 预加载 linkTags 配置(供 onLinkTagTap 旧格式降级匹配 type 用)
|
||||
if (!app.globalData.linkTagsConfig) {
|
||||
app.request({ url: '/api/miniprogram/config', silent: true }).then(cfg => {
|
||||
if (cfg && Array.isArray(cfg.linkTags)) {
|
||||
app.globalData.linkTagsConfig = cfg.linkTags
|
||||
}
|
||||
}).catch(() => {})
|
||||
}
|
||||
|
||||
// 支持 scene(扫码)、mid、id、ref
|
||||
const sceneStr = (options && options.scene) || ''
|
||||
const parsed = parseScene(sceneStr)
|
||||
@@ -196,9 +206,9 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 【重构】加载章节内容(专注于内容加载,权限判断已在 onLoad 中由 accessManager 完成)
|
||||
// prefetchedChapter:若已有章节数据(含 content)则复用,避免二次请求
|
||||
// 加载章节内容:优先复用 prefetchedChapter 避免二次请求,失败时降级本地缓存
|
||||
async loadContent(id, accessState, prefetchedChapter) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
try {
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
let res = prefetchedChapter
|
||||
@@ -212,7 +222,7 @@ Page({
|
||||
price: res.price ?? sectionPrice
|
||||
}
|
||||
this.setData({ section })
|
||||
|
||||
|
||||
if (res && res.content) {
|
||||
const { lines, segments } = contentParser.parseContent(res.content)
|
||||
const previewCount = Math.ceil(lines.length * 0.2)
|
||||
@@ -226,29 +236,29 @@ Page({
|
||||
}
|
||||
if (res.mid) updates.sectionMid = res.mid
|
||||
this.setData(updates)
|
||||
|
||||
// 如果有权限,标记为已读
|
||||
// 写入本地缓存,供离线/重试降级使用
|
||||
try { wx.setStorageSync(cacheKey, res) } catch (_) {}
|
||||
if (accessManager.canAccessFullContent(accessState)) {
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败:', e)
|
||||
// 尝试从本地缓存加载
|
||||
const cacheKey = `chapter_${id}`
|
||||
console.error('[Read] 加载内容失败,尝试本地缓存:', e)
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
const { lines, segments } = contentParser.parseContent(cached.content)
|
||||
const previewCount = Math.ceil(lines.length * 0.2)
|
||||
|
||||
this.setData({
|
||||
content: cached.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
previewParagraphs: lines.slice(0, previewCount)
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
})
|
||||
console.log('[Read] 从本地缓存加载成功')
|
||||
return
|
||||
}
|
||||
} catch (cacheErr) {
|
||||
console.warn('[Read] 本地缓存也失败:', cacheErr)
|
||||
@@ -315,48 +325,6 @@ Page({
|
||||
return `/api/miniprogram/book/chapter/${finalId}`
|
||||
},
|
||||
|
||||
// 加载内容 - 三级降级方案:API → 本地缓存 → 备用API
|
||||
async loadContent(id) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
|
||||
// 1. 优先从API获取
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 5000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
// 成功后缓存到本地
|
||||
wx.setStorageSync(cacheKey, res)
|
||||
console.log('[Read] 从API加载成功:', id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] API加载失败,尝试本地缓存:', e.message)
|
||||
}
|
||||
|
||||
// 2. API失败,尝试从本地缓存读取
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
this.setChapterContent(cached)
|
||||
console.log('[Read] 从本地缓存加载成功:', id)
|
||||
// 后台静默刷新
|
||||
this.silentRefresh(id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 本地缓存读取失败')
|
||||
}
|
||||
|
||||
// 3. 都失败,显示加载中并持续重试
|
||||
this.setData({
|
||||
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
|
||||
contentSegments: contentParser.parseContent('章节内容加载中...\n正在尝试连接服务器,请稍候...').segments,
|
||||
previewParagraphs: ['章节内容加载中...']
|
||||
})
|
||||
|
||||
// 延迟重试(最多3次)
|
||||
this.retryLoadContent(id, 3)
|
||||
},
|
||||
|
||||
// 带超时的章节请求
|
||||
fetchChapterWithTimeout(id, timeout = 5000) {
|
||||
@@ -476,6 +444,69 @@ Page({
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
// 点击正文中的 #链接标签:外链复制到剪贴板,小程序内页直接跳转
|
||||
onLinkTagTap(e) {
|
||||
let url = (e.currentTarget.dataset.url || '').trim()
|
||||
const label = (e.currentTarget.dataset.label || '').trim()
|
||||
let tagType = (e.currentTarget.dataset.tagType || '').trim()
|
||||
let pagePath = (e.currentTarget.dataset.pagePath || '').trim()
|
||||
|
||||
// 旧格式(<a href>)tagType 为空 → 按 label 从缓存 linkTags 补充类型信息
|
||||
if (!tagType && label) {
|
||||
const cached = (app.globalData.linkTagsConfig || []).find(t => t.label === label)
|
||||
if (cached) {
|
||||
tagType = cached.type || 'url'
|
||||
pagePath = cached.pagePath || ''
|
||||
if (!url) url = cached.url || ''
|
||||
}
|
||||
}
|
||||
|
||||
// CKB 类型:复用 @mention 加好友流程,弹出留资表单
|
||||
if (tagType === 'ckb') {
|
||||
// 触发通用加好友(无特定 personId,使用全局 CKB Key)
|
||||
this.onMentionTap({ currentTarget: { dataset: { userId: '', nickname: label } } })
|
||||
return
|
||||
}
|
||||
|
||||
// 小程序内部路径(pagePath 或 url 以 /pages/ 开头)
|
||||
const internalPath = pagePath || (url.startsWith('/pages/') ? url : '')
|
||||
if (internalPath) {
|
||||
wx.navigateTo({ url: internalPath, fail: () => wx.switchTab({ url: internalPath }) })
|
||||
return
|
||||
}
|
||||
|
||||
// 外部 URL:优先用 wx.openLink 在浏览器打开,旧版微信降级复制
|
||||
if (url) {
|
||||
if (typeof wx.openLink === 'function') {
|
||||
wx.openLink({
|
||||
url,
|
||||
fail: () => {
|
||||
// openLink 不支持(如不在微信内),降级复制
|
||||
wx.setClipboardData({
|
||||
data: url,
|
||||
success: () => wx.showToast({ title: `链接已复制`, icon: 'none', duration: 2000 })
|
||||
})
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.setClipboardData({
|
||||
data: url,
|
||||
success: () => wx.showToast({ title: `链接已复制`, icon: 'none', duration: 2000 })
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
wx.showToast({ title: '暂无跳转地址', icon: 'none' })
|
||||
},
|
||||
|
||||
// 点击正文图片 → 全屏预览
|
||||
onImageTap(e) {
|
||||
const src = e.currentTarget.dataset.src
|
||||
if (!src) return
|
||||
wx.previewImage({ current: src, urls: [src] })
|
||||
},
|
||||
|
||||
// 点击正文中的 @某人:确认弹窗 → 登录/资料校验 → 调用 ckb/lead 加好友留资
|
||||
onMentionTap(e) {
|
||||
const userId = e.currentTarget.dataset.userId
|
||||
|
||||
@@ -42,12 +42,14 @@
|
||||
<view class="skeleton skeleton-5"></view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ 高亮可点) -->
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text>
|
||||
<text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">@{{seg.nickname}}</text>
|
||||
<text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}">#{{seg.label}}</text>
|
||||
<image wx:elif="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -189,6 +189,21 @@
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
/* 正文内 #链接标签 高亮可点 */
|
||||
.paragraph .link-tag {
|
||||
color: #FFD700;
|
||||
font-weight: 500;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
/* 正文内图片 */
|
||||
.content-image {
|
||||
width: 100%;
|
||||
border-radius: 8rpx;
|
||||
margin: 16rpx 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
@@ -43,8 +43,18 @@ Page({
|
||||
const dt = new Date(d.expireDate)
|
||||
expStr = `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`
|
||||
}
|
||||
// 同步 VIP 状态到全局(与「我的」页保持一致)
|
||||
const isVip = !!d.isVip
|
||||
app.globalData.isVip = isVip
|
||||
app.globalData.vipExpireDate = d.expireDate || expStr || ''
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.isVip = isVip
|
||||
userInfo.vipExpireDate = app.globalData.vipExpireDate
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
this.setData({
|
||||
isVip: d.isVip,
|
||||
isVip,
|
||||
daysRemaining: d.daysRemaining,
|
||||
expireDateStr: expStr,
|
||||
price: d.price || 1980
|
||||
@@ -109,12 +119,8 @@ Page({
|
||||
try {
|
||||
await new Promise(r => setTimeout(r, 1500))
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
// 重新拉取 VIP 状态并同步到全局
|
||||
await this.loadVipInfo()
|
||||
app.globalData.hasFullBook = true
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = true
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
const pages = getCurrentPages()
|
||||
pages.forEach(p => {
|
||||
if (typeof p.initUserStatus === 'function') p.initUserStatus()
|
||||
|
||||
@@ -125,6 +125,15 @@ class ChapterAccessManager {
|
||||
app.globalData.hasFullBook = true
|
||||
}
|
||||
|
||||
// VIP 会员:标记全局 isVip,供超级个体解锁、前端展示等使用
|
||||
if (purchaseData.reason === 'has_vip') {
|
||||
app.globalData.isVip = true
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.isVip = true
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
|
||||
if (!app.globalData.purchasedSections.includes(sectionId)) {
|
||||
app.globalData.purchasedSections = [...app.globalData.purchasedSections, sectionId]
|
||||
}
|
||||
|
||||
@@ -1,26 +1,107 @@
|
||||
/**
|
||||
* Soul创业派对 - 内容解析工具
|
||||
* 解析 TipTap HTML(含 <span data-type="mention">)为阅读页可展示的 segments
|
||||
* 解析 TipTap HTML 为阅读页可展示的 segments
|
||||
*
|
||||
* segment 类型:
|
||||
* { type: 'text', text }
|
||||
* { type: 'mention', userId, nickname } — @某人,点击加好友
|
||||
* { type: 'linkTag', label, url } — #链接标签,点击跳转
|
||||
* { type: 'image', src, alt } — 图片
|
||||
*/
|
||||
|
||||
/**
|
||||
* 判断内容是否为 HTML(含标签)
|
||||
*/
|
||||
/** 判断内容是否为 HTML */
|
||||
function isHtmlContent(content) {
|
||||
if (!content || typeof content !== 'string') return false
|
||||
const trimmed = content.trim()
|
||||
return trimmed.includes('<') && trimmed.includes('>') && /<[a-z][^>]*>/i.test(trimmed)
|
||||
}
|
||||
|
||||
/** 解码常见 HTML 实体 */
|
||||
function decodeEntities(str) {
|
||||
return str
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 HTML 中解析出段落与 mention 片段
|
||||
* TipTap mention: <span data-type="mention" data-id="..." data-label="...">@nickname</span>
|
||||
* 将一个 HTML block 字符串解析为 segments 数组
|
||||
* 处理三种内联元素:mention / linkTag(span) / linkTag(a) / img
|
||||
*/
|
||||
function parseBlockToSegments(block) {
|
||||
const segs = []
|
||||
// 合并匹配所有内联元素
|
||||
const tokenRe = /<span[^>]*data-type="mention"[^>]*>[\s\S]*?<\/span>|<span[^>]*data-type="linkTag"[^>]*>[\s\S]*?<\/span>|<a[^>]*href="([^"]*)"[^>]*>(#[^<]*)<\/a>|<img[^>]*\/?>/gi
|
||||
let lastEnd = 0
|
||||
let m
|
||||
|
||||
while ((m = tokenRe.exec(block)) !== null) {
|
||||
// 前置纯文本
|
||||
const before = decodeEntities(block.slice(lastEnd, m.index).replace(/<[^>]+>/g, ''))
|
||||
if (before.trim()) segs.push({ type: 'text', text: before })
|
||||
|
||||
const tag = m[0]
|
||||
|
||||
if (/data-type="mention"/i.test(tag)) {
|
||||
// @mention — TipTap mention span
|
||||
const idMatch = tag.match(/data-id="([^"]*)"/)
|
||||
const labelMatch = tag.match(/data-label="([^"]*)"/)
|
||||
const innerText = tag.replace(/<[^>]+>/g, '')
|
||||
const userId = idMatch ? idMatch[1].trim() : ''
|
||||
const nickname = labelMatch ? labelMatch[1].trim() : innerText.replace(/^@/, '').trim()
|
||||
if (userId || nickname) segs.push({ type: 'mention', userId, nickname })
|
||||
|
||||
} else if (/data-type="linkTag"/i.test(tag)) {
|
||||
// #linkTag — 自定义 span 格式(data-type="linkTag" data-url="..." data-tag-type="..." data-page-path="...")
|
||||
const urlMatch = tag.match(/data-url="([^"]*)"/)
|
||||
const tagTypeMatch = tag.match(/data-tag-type="([^"]*)"/)
|
||||
const pagePathMatch = tag.match(/data-page-path="([^"]*)"/)
|
||||
const tagIdMatch = tag.match(/data-tag-id="([^"]*)"/)
|
||||
const innerText = tag.replace(/<[^>]+>/g, '').replace(/^#/, '').trim()
|
||||
const url = urlMatch ? urlMatch[1] : ''
|
||||
const tagType = tagTypeMatch ? tagTypeMatch[1] : 'url'
|
||||
const pagePath = pagePathMatch ? pagePathMatch[1] : ''
|
||||
const tagId = tagIdMatch ? tagIdMatch[1] : ''
|
||||
segs.push({ type: 'linkTag', label: innerText || '#', url, tagType, pagePath, tagId })
|
||||
|
||||
} else if (/^<a /i.test(tag)) {
|
||||
// #linkTag — 旧格式 <a href>(insertLinkTag 旧版产生,url 可能为空)
|
||||
// m[1] = href, m[2] = innerText(以 # 开头)
|
||||
const url = m[1] || ''
|
||||
const label = (m[2] || '').replace(/^#/, '').trim()
|
||||
// 旧格式没有 tagType,在 onLinkTagTap 中会按 label 匹配缓存的 linkTags 配置降级处理
|
||||
segs.push({ type: 'linkTag', label: label || '#', url, tagType: '', pagePath: '', tagId: '' })
|
||||
|
||||
} else if (/^<img /i.test(tag)) {
|
||||
// 图片
|
||||
const srcMatch = tag.match(/src="([^"]*)"/)
|
||||
const altMatch = tag.match(/alt="([^"]*)"/)
|
||||
if (srcMatch) {
|
||||
segs.push({ type: 'image', src: srcMatch[1], alt: altMatch ? altMatch[1] : '' })
|
||||
}
|
||||
}
|
||||
|
||||
lastEnd = m.index + tag.length
|
||||
}
|
||||
|
||||
// 尾部纯文本
|
||||
const after = decodeEntities(block.slice(lastEnd).replace(/<[^>]+>/g, ''))
|
||||
if (after.trim()) segs.push({ type: 'text', text: after })
|
||||
|
||||
return segs
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 HTML 中解析出 lines(纯文本行)和 segments(含富文本片段)
|
||||
*/
|
||||
function parseHtmlToSegments(html) {
|
||||
const lines = []
|
||||
const segments = []
|
||||
|
||||
// 1. 块级元素拆成段落
|
||||
// 1. 块级标签换行,保留内联标签供后续解析
|
||||
let text = html
|
||||
text = text.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
|
||||
text = text.replace(/<p[^>]*>/gi, '')
|
||||
@@ -35,37 +116,31 @@ function parseHtmlToSegments(html) {
|
||||
text = text.replace(/<li[^>]*>/gi, '• ')
|
||||
text = text.replace(/<\/li>/gi, '\n')
|
||||
|
||||
// 2. 逐段解析:提取文本与 mention
|
||||
// 2. 逐段解析
|
||||
const blocks = text.split(/\n+/)
|
||||
for (const block of blocks) {
|
||||
const blockSegments = []
|
||||
const mentionRe = /<span[^>]*data-type="mention"[^>]*>([^<]*)<\/span>/gi
|
||||
let lastEnd = 0
|
||||
let m
|
||||
while ((m = mentionRe.exec(block)) !== null) {
|
||||
const before = block.slice(lastEnd, m.index).replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
if (before) blockSegments.push({ type: 'text', text: before })
|
||||
const idMatch = m[0].match(/data-id="([^"]*)"/)
|
||||
const labelMatch = m[0].match(/data-label="([^"]*)"/)
|
||||
const userId = idMatch ? idMatch[1].trim() : ''
|
||||
const nickname = labelMatch ? labelMatch[1].trim() : (m[1] || '').replace(/^@/, '').trim()
|
||||
blockSegments.push({ type: 'mention', userId, nickname })
|
||||
lastEnd = m.index + m[0].length
|
||||
}
|
||||
const after = block.slice(lastEnd).replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
if (after) blockSegments.push({ type: 'text', text: after })
|
||||
const lineText = block.replace(/<[^>]+>/g, '').replace(/ /g, ' ').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').trim()
|
||||
if (lineText) {
|
||||
lines.push(lineText)
|
||||
segments.push(blockSegments.length ? blockSegments : [{ type: 'text', text: lineText }])
|
||||
if (!block.trim()) continue
|
||||
|
||||
const blockSegs = parseBlockToSegments(block)
|
||||
if (!blockSegs.length) continue
|
||||
|
||||
// 纯图片行独立成段
|
||||
if (blockSegs.length === 1 && blockSegs[0].type === 'image') {
|
||||
lines.push('')
|
||||
segments.push(blockSegs)
|
||||
continue
|
||||
}
|
||||
|
||||
// 行纯文本用于 lines(previewParagraphs 降级展示)
|
||||
const lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
lines.push(lineText)
|
||||
segments.push(blockSegs)
|
||||
}
|
||||
|
||||
return { lines, segments }
|
||||
}
|
||||
|
||||
/**
|
||||
* 纯文本按行解析(无 mention)
|
||||
*/
|
||||
/** 纯文本按行解析(无 HTML 标签) */
|
||||
function parsePlainTextToSegments(text) {
|
||||
const lines = text.split('\n').map(l => l.trim()).filter(l => l.length > 0)
|
||||
const segments = lines.map(line => [{ type: 'text', text: line }])
|
||||
@@ -74,8 +149,8 @@ function parsePlainTextToSegments(text) {
|
||||
|
||||
/**
|
||||
* 将原始内容解析为 contentSegments(用于阅读页展示)
|
||||
* @param {string} rawContent - 原始内容(TipTap HTML 或纯文本)
|
||||
* @returns {{ lines: string[], segments: Array<Array<{type, text?, userId?, nickname?}>> }}
|
||||
* @param {string} rawContent
|
||||
* @returns {{ lines: string[], segments: Array<Array<segment>> }}
|
||||
*/
|
||||
function parseContent(rawContent) {
|
||||
if (!rawContent || typeof rawContent !== 'string') {
|
||||
|
||||
Reference in New Issue
Block a user