From e23eba5d3e13af3e6776b468985a14cf437fc945 Mon Sep 17 00:00:00 2001 From: Alex-larget <33240357+Alex-larget@users.noreply.github.com> Date: Tue, 10 Mar 2026 14:32:20 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E9=94=99=E8=AF=AF=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91=EF=BC=8C=E5=A2=9E=E5=8A=A0=E7=94=A8?= =?UTF-8?q?=E6=88=B7=E4=B8=8D=E5=AD=98=E5=9C=A8=E6=97=B6=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=99=BB=E5=87=BA=E5=8A=9F=E8=83=BD=E3=80=82=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E9=98=85=E8=AF=BB=E9=A1=B5=E5=86=85=E5=AE=B9=E8=A7=A3?= =?UTF-8?q?=E6=9E=90=EF=BC=8C=E6=94=AF=E6=8C=81TipTap=20HTML=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=EF=BC=8C=E6=8F=90=E5=8D=87=E7=94=A8=E6=88=B7=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- miniprogram/app.js | 5 ++ miniprogram/pages/read/read.js | 41 ++++--------- miniprogram/utils/contentParser.js | 93 ++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 miniprogram/utils/contentParser.js diff --git a/miniprogram/app.js b/miniprogram/app.js index d564eb03..4528dd53 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -352,6 +352,11 @@ App({ // 业务失败:success === false,soul-api 用 message 或 error 返回原因 if (data && data.success === false) { const msg = this._getApiErrorMsg(data, '操作失败') + // 登录态不一致:本地有 token/userInfo,但后端查不到该用户 + // 典型原因:切换环境(baseUrl)、换库/清库、用户被删除、token 与用户不匹配 + if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) { + this.logout() + } showError(msg) reject(new Error(msg)) return diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index a2c34075..456a9c23 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -9,36 +9,17 @@ * - 使用状态机(accessState)规范权限流转 * - 异常统一保守处理,避免误解锁 * - * 更新: 正文 @某人({{@userId:昵称}}) + * 更新: 正文 @某人(TipTap HTML ) * - contentSegments 解析每行,mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead - * - 回归:无@ 时仍按段展示;未登录/无联系方式/重复点击均有提示或去重 */ import accessManager from '../../utils/chapterAccessManager' import readingTracker from '../../utils/readingTracker' const { parseScene } = require('../../utils/scene.js') +const contentParser = require('../../utils/contentParser.js') const app = getApp() -// 解析单行中的 {{@userId:昵称}} 为片段数组,用于阅读页 @ 高亮与点击 -function parseLineToSegments(line) { - const segments = [] - const re = /\{\{@([^:]+):(.*?)\}\}/g - let lastEnd = 0 - let m - while ((m = re.exec(line)) !== null) { - if (m.index > lastEnd) { - segments.push({ type: 'text', text: line.slice(lastEnd, m.index) }) - } - segments.push({ type: 'mention', userId: String(m[1]).trim(), nickname: String(m[2] || '').trim() }) - lastEnd = re.lastIndex - } - if (lastEnd < line.length) { - segments.push({ type: 'text', text: line.slice(lastEnd) }) - } - return segments.length ? segments : [{ type: 'text', text: line }] -} - Page({ data: { // 系统信息 @@ -233,12 +214,12 @@ Page({ this.setData({ section }) if (res && res.content) { - const lines = res.content.split('\n').filter(line => line.trim()) + const { lines, segments } = contentParser.parseContent(res.content) const previewCount = Math.ceil(lines.length * 0.2) const updates = { content: res.content, contentParagraphs: lines, - contentSegments: lines.map(parseLineToSegments), + contentSegments: segments, previewParagraphs: lines.slice(0, previewCount), partTitle: res.partTitle || '', chapterTitle: res.chapterTitle || '' @@ -258,13 +239,13 @@ Page({ try { const cached = wx.getStorageSync(cacheKey) if (cached && cached.content) { - const lines = cached.content.split('\n').filter(line => line.trim()) + const { lines, segments } = contentParser.parseContent(cached.content) const previewCount = Math.ceil(lines.length * 0.2) this.setData({ content: cached.content, contentParagraphs: lines, - contentSegments: lines.map(parseLineToSegments), + contentSegments: segments, previewParagraphs: lines.slice(0, previewCount) }) console.log('[Read] 从本地缓存加载成功') @@ -369,7 +350,7 @@ Page({ // 3. 都失败,显示加载中并持续重试 this.setData({ contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'], - contentSegments: [parseLineToSegments('章节内容加载中...'), parseLineToSegments('正在尝试连接服务器,请稍候...')], + contentSegments: contentParser.parseContent('章节内容加载中...\n正在尝试连接服务器,请稍候...').segments, previewParagraphs: ['章节内容加载中...'] }) @@ -396,9 +377,9 @@ Page({ }) }, - // 设置章节内容 + // 设置章节内容(兼容纯文本/Markdown 与 TipTap HTML) setChapterContent(res) { - const lines = res.content.split('\n').filter(line => line.trim()) + const { lines, segments } = contentParser.parseContent(res.content) const previewCount = Math.ceil(lines.length * 0.2) const sectionPrice = this.data.sectionPrice ?? 1 const sectionTitle = (res.sectionTitle || res.title || '').trim() @@ -414,7 +395,7 @@ Page({ content: res.content, previewContent: lines.slice(0, previewCount).join('\n'), contentParagraphs: lines, - contentSegments: lines.map(parseLineToSegments), + contentSegments: segments, previewParagraphs: lines.slice(0, previewCount), partTitle: res.partTitle || '', // 导航栏、分享等使用的文章标题,同样统一为 sectionTitle @@ -440,7 +421,7 @@ Page({ if (currentRetry >= maxRetries) { this.setData({ contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'], - contentSegments: [parseLineToSegments('内容加载失败'), parseLineToSegments('请检查网络连接后下拉刷新重试')], + contentSegments: contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments, previewParagraphs: ['内容加载失败'] }) return diff --git a/miniprogram/utils/contentParser.js b/miniprogram/utils/contentParser.js new file mode 100644 index 00000000..b88a5503 --- /dev/null +++ b/miniprogram/utils/contentParser.js @@ -0,0 +1,93 @@ +/** + * Soul创业派对 - 内容解析工具 + * 解析 TipTap HTML(含 )为阅读页可展示的 segments + */ + +/** + * 判断内容是否为 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 中解析出段落与 mention 片段 + * TipTap mention: @nickname + */ +function parseHtmlToSegments(html) { + const lines = [] + const segments = [] + + // 1. 块级元素拆成段落 + let text = html + text = text.replace(/<\/p>\s*]*>/gi, '\n\n') + text = text.replace(/]*>/gi, '') + text = text.replace(/<\/p>/gi, '\n') + text = text.replace(/]*>/gi, '') + text = text.replace(/<\/div>/gi, '\n') + text = text.replace(//gi, '\n') + text = text.replace(/<\/?h[1-6][^>]*>/gi, '\n') + text = text.replace(/<\/?blockquote[^>]*>/gi, '\n') + text = text.replace(/<\/?ul[^>]*>/gi, '\n') + text = text.replace(/<\/?ol[^>]*>/gi, '\n') + text = text.replace(/]*>/gi, '• ') + text = text.replace(/<\/li>/gi, '\n') + + // 2. 逐段解析:提取文本与 mention + const blocks = text.split(/\n+/) + for (const block of blocks) { + const blockSegments = [] + const mentionRe = /]*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 }]) + } + } + return { lines, segments } +} + +/** + * 纯文本按行解析(无 mention) + */ +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 }]) + return { lines, segments } +} + +/** + * 将原始内容解析为 contentSegments(用于阅读页展示) + * @param {string} rawContent - 原始内容(TipTap HTML 或纯文本) + * @returns {{ lines: string[], segments: Array> }} + */ +function parseContent(rawContent) { + if (!rawContent || typeof rawContent !== 'string') { + return { lines: [], segments: [] } + } + if (isHtmlContent(rawContent)) { + return parseHtmlToSegments(rawContent) + } + return parsePlainTextToSegments(rawContent) +} + +module.exports = { + parseContent, + isHtmlContent +}