实现@提及功能,允许用户在阅读页中高亮并点击提及的用户,触发添加好友流程。更新内容解析逻辑以支持提及格式,调整页面展示以适应新功能,并优化样式以提升用户体验。
This commit is contained in:
@@ -2,12 +2,16 @@
|
||||
* Soul创业派对 - 阅读页(标准流程版)
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*
|
||||
*
|
||||
* 更新: 2026-02-04
|
||||
* - 引入权限管理器(chapterAccessManager)统一权限判断
|
||||
* - 引入阅读追踪器(readingTracker)记录阅读进度、时长、是否读完
|
||||
* - 使用状态机(accessState)规范权限流转
|
||||
* - 异常统一保守处理,避免误解锁
|
||||
*
|
||||
* 更新: 正文 @某人({{@userId:昵称}})
|
||||
* - contentSegments 解析每行,mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead
|
||||
* - 回归:无@ 时仍按段展示;未登录/无联系方式/重复点击均有提示或去重
|
||||
*/
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
@@ -16,6 +20,25 @@ const { parseScene } = require('../../utils/scene.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: {
|
||||
// 系统信息
|
||||
@@ -32,6 +55,7 @@ Page({
|
||||
content: '',
|
||||
previewContent: '',
|
||||
contentParagraphs: [],
|
||||
contentSegments: [], // 每行解析为 [{type:'text'|'mention', text?, userId?, nickname?}]
|
||||
previewParagraphs: [],
|
||||
loading: true,
|
||||
|
||||
@@ -214,6 +238,7 @@ Page({
|
||||
const updates = {
|
||||
content: res.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: lines.map(parseLineToSegments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
@@ -239,6 +264,7 @@ Page({
|
||||
this.setData({
|
||||
content: cached.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: lines.map(parseLineToSegments),
|
||||
previewParagraphs: lines.slice(0, previewCount)
|
||||
})
|
||||
console.log('[Read] 从本地缓存加载成功')
|
||||
@@ -343,6 +369,7 @@ Page({
|
||||
// 3. 都失败,显示加载中并持续重试
|
||||
this.setData({
|
||||
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
|
||||
contentSegments: [parseLineToSegments('章节内容加载中...'), parseLineToSegments('正在尝试连接服务器,请稍候...')],
|
||||
previewParagraphs: ['章节内容加载中...']
|
||||
})
|
||||
|
||||
@@ -387,6 +414,7 @@ Page({
|
||||
content: res.content,
|
||||
previewContent: lines.slice(0, previewCount).join('\n'),
|
||||
contentParagraphs: lines,
|
||||
contentSegments: lines.map(parseLineToSegments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
// 导航栏、分享等使用的文章标题,同样统一为 sectionTitle
|
||||
@@ -412,6 +440,7 @@ Page({
|
||||
if (currentRetry >= maxRetries) {
|
||||
this.setData({
|
||||
contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
|
||||
contentSegments: [parseLineToSegments('内容加载失败'), parseLineToSegments('请检查网络连接后下拉刷新重试')],
|
||||
previewParagraphs: ['内容加载失败']
|
||||
})
|
||||
return
|
||||
@@ -466,6 +495,95 @@ Page({
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
// 点击正文中的 @某人:确认弹窗 → 登录/资料校验 → 调用 ckb/lead 加好友留资
|
||||
onMentionTap(e) {
|
||||
const userId = e.currentTarget.dataset.userId
|
||||
const nickname = (e.currentTarget.dataset.nickname || '').trim() || 'TA'
|
||||
if (!userId) return
|
||||
wx.showModal({
|
||||
title: '添加好友',
|
||||
content: `是否添加 @${nickname} ?`,
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (!res.confirm) return
|
||||
this._doMentionAddFriend(userId, nickname)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 边界:未登录→去登录;无手机/微信号→去资料编辑;重复同一人→本地 key 去重
|
||||
async _doMentionAddFriend(targetUserId, targetNickname) {
|
||||
const app = getApp()
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再添加好友',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
phone = (profileRes.data.phone || '').trim()
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!phone && !wechatId) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号或微信号,以便对方联系您',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const leadKey = `mention_lead_${myUserId}_${targetUserId}`
|
||||
if (wx.getStorageSync(leadKey)) {
|
||||
wx.showToast({ title: '已提交过,对方会尽快联系您', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/ckb/lead',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: myUserId,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechatId || undefined,
|
||||
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
|
||||
targetUserId,
|
||||
targetNickname: targetNickname || undefined,
|
||||
source: 'article_mention'
|
||||
}
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync(leadKey, true)
|
||||
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 分享弹窗
|
||||
showShare() {
|
||||
this.setData({ showShareModal: true })
|
||||
|
||||
@@ -42,10 +42,13 @@
|
||||
<view class="skeleton skeleton-5"></view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 免费或已购买 -->
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ 高亮可点) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
<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>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
|
||||
@@ -182,6 +182,13 @@
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* 正文内 @某人 高亮可点 */
|
||||
.paragraph .mention {
|
||||
color: #00CED1;
|
||||
font-weight: 500;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user