优化错误处理逻辑,增加用户不存在时的自动登出功能。更新阅读页内容解析,支持TipTap HTML格式,提升用户体验。

This commit is contained in:
Alex-larget
2026-03-10 14:32:20 +08:00
parent 3e22e54f75
commit e23eba5d3e
3 changed files with 109 additions and 30 deletions

View File

@@ -352,6 +352,11 @@ App({
// 业务失败success === falsesoul-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

View File

@@ -9,36 +9,17 @@
* - 使用状态机accessState规范权限流转
* - 异常统一保守处理,避免误解锁
*
* 更新: 正文 @某人({{@userId:昵称}}
* 更新: 正文 @某人(TipTap HTML <span data-type="mention">
* - 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

View File

@@ -0,0 +1,93 @@
/**
* Soul创业派对 - 内容解析工具
* 解析 TipTap HTML含 <span data-type="mention">)为阅读页可展示的 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: <span data-type="mention" data-id="..." data-label="...">@nickname</span>
*/
function parseHtmlToSegments(html) {
const lines = []
const segments = []
// 1. 块级元素拆成段落
let text = html
text = text.replace(/<\/p>\s*<p[^>]*>/gi, '\n\n')
text = text.replace(/<p[^>]*>/gi, '')
text = text.replace(/<\/p>/gi, '\n')
text = text.replace(/<div[^>]*>/gi, '')
text = text.replace(/<\/div>/gi, '\n')
text = text.replace(/<br\s*\/?>/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(/<li[^>]*>/gi, '• ')
text = text.replace(/<\/li>/gi, '\n')
// 2. 逐段解析:提取文本与 mention
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(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/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(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/g, '>')
if (after) blockSegments.push({ type: 'text', text: after })
const lineText = block.replace(/<[^>]+>/g, '').replace(/&nbsp;/g, ' ').replace(/&amp;/g, '&').replace(/&lt;/g, '<').replace(/&gt;/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<Array<{type, text?, userId?, nickname?}>> }}
*/
function parseContent(rawContent) {
if (!rawContent || typeof rawContent !== 'string') {
return { lines: [], segments: [] }
}
if (isHtmlContent(rawContent)) {
return parseHtmlToSegments(rawContent)
}
return parsePlainTextToSegments(rawContent)
}
module.exports = {
parseContent,
isHtmlContent
}