This commit is contained in:
Alex-larget
2026-03-26 21:29:18 +08:00
parent 7bf301a9c8
commit 159ce035f2
5 changed files with 109 additions and 18 deletions

View File

@@ -10,8 +10,8 @@ const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
// baseUrl 手动切换(注释方式):
const API_BASE_URL = 'http://localhost:8080'
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
// const API_BASE_URL = 'http://localhost:8080'
const API_BASE_URL = 'https://soulapi.quwanzhi.com'
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version正式版否则用本字段
const APP_DISPLAY_VERSION = '1.7.2'

View File

@@ -136,6 +136,8 @@ Page({
readingProgress: 0,
/** 未解锁付费墙:合并 data.previewPercent章节与顶层 previewPercent全局 */
previewPercent: 20,
/** 未解锁时:按 previewPercent 裁切可见高度px。0 表示未启用/待测量 */
previewMaxHeightPx: 0,
/** mpUi.readPage.beforeLoginHint未登录付费墙上方说明文案 */
readBeforeLoginHint: '',
/** 朋友圈单页标题与说明mpUi.readPage.singlePageTitle / singlePagePaywallHint */
@@ -185,6 +187,53 @@ Page({
momentsPaywallExpanded: false,
},
_isLockedState(state) {
return state === 'locked_not_login' || state === 'locked_not_purchased'
},
/**
* 未解锁时:先渲染完整正文,再测量其高度,按 previewPercent 计算可见高度并裁切。
* 这样后端无需截断,避免 HTML/段落被截断导致的渲染失败。
*/
_updatePreviewClipHeight() {
const state = this.data.accessState
if (!this._isLockedState(state)) {
if (this.data.previewMaxHeightPx !== 0) this.setData({ previewMaxHeightPx: 0 })
return
}
const percent = Number(this.data.previewPercent) || 20
// 先关闭裁切,确保测到“完整高度”
if (this.data.previewMaxHeightPx !== 0) {
this.setData({ previewMaxHeightPx: 0 })
}
const measure = () =>
new Promise((resolve) => {
wx.createSelectorQuery()
.in(this)
.select('#previewContentMeasure')
.boundingClientRect((rect) => resolve(rect || null))
.exec()
})
const apply = async () => {
// nextTick 让 setData 的 DOM 更新生效
await new Promise((r) => (typeof wx.nextTick === 'function' ? wx.nextTick(r) : setTimeout(r, 0)))
const rect = await measure()
const fullH = rect && rect.height ? rect.height : 0
if (!fullH || fullH < 20) return
const clipped = Math.max(120, Math.floor((fullH * Math.min(Math.max(percent, 1), 100)) / 100))
// 只在仍处于锁定态时应用,避免切换状态后误裁切
if (this._isLockedState(this.data.accessState)) {
this.setData({ previewMaxHeightPx: clipped })
}
}
apply().catch(() => {})
},
/**
* 是否处于朋友圈等「单页预览」环境。
* 兼容:部分机型/基础库首帧 getSystemInfoSync().mode 未就绪,需结合 launch/enter scene 1154、getWindowInfo。
@@ -480,11 +529,12 @@ Page({
}
this.setData({ section })
// 已解锁用 data.content完整内容未解锁用 content预览先 determineAccessState 再 loadContent 保证顺序正确
const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content
// 规则更新:小程序端始终使用“完整正文”渲染;未解锁时通过前端裁切可见比例实现预览
// 兼容老返回:完整正文可能在 res.data.content老链路只有 res.content
const displayContent = (res.data?.content ?? res.content)
if (res && displayContent) {
const { lines, segments } = contentParser.parseContent(displayContent, parseCfg)
// 预览内容由后端统一截取比例,这里展示全部预览内容
// 预览比例由前端按高度裁切,这里渲染完整内容(避免后端截断导致渲染失败)
const previewCount = lines.length
const updates = {
content: displayContent,
@@ -496,7 +546,9 @@ Page({
previewPercent: normalizePreviewPercent(res),
}
if (res.mid) updates.sectionMid = res.mid
this.setData(updates)
// 先关闭裁切,等 DOM 更新后再测量并按比例裁切
this.setData({ ...updates, previewMaxHeightPx: 0 })
this._updatePreviewClipHeight()
// 写入本地缓存(存 displayContent供离线/重试降级使用)
try { wx.setStorageSync(cacheKey, { ...res, content: displayContent }) } catch (_) {}
if (accessManager.canAccessFullContent(accessState)) {
@@ -511,7 +563,7 @@ Page({
if (cached && cached.content) {
await app.getReadExtras()
const { lines, segments } = contentParser.parseContent(cached.content, getContentParseConfig())
// 预览内容由后端统一截取比例,这里展示全部预览内容
// 预览比例由前端按高度裁切,这里渲染完整内容
const previewCount = lines.length
this.setData({
content: cached.content,
@@ -521,7 +573,9 @@ Page({
partTitle: cached.partTitle || '',
chapterTitle: cached.chapterTitle || '',
previewPercent: normalizePreviewPercent(cached),
previewMaxHeightPx: 0,
})
this._updatePreviewClipHeight()
app.touchRecentSection(id)
console.log('[Read] 从本地缓存加载成功')
return

View File

@@ -113,15 +113,25 @@
</view>
</view>
<!-- 预览内容 + 付费墙 - 未登录 -->
<!-- 预览内容 + 付费墙 - 未登录(规则更新:前端按百分比裁切显示,后端不截断正文) -->
<view class="article preview" wx:if="{{accessState === 'locked_not_login'}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
<text user-select>{{item}}</text>
<view class="preview-wrap">
<view
id="previewContentMeasure"
class="preview-body"
style="{{previewMaxHeightPx > 0 ? ('max-height:' + previewMaxHeightPx + 'px; overflow:hidden;') : ''}}"
>
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
<text user-select wx:if="{{!(item.length === 1 && item[0].type === 'image')}}"><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.mentionDisplay}}</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}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
</block>
</view>
</view>
<!-- 渐变遮罩(定位在裁切容器底部) -->
<view class="fade-mask"></view>
</view>
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 付费墙 - 未登录:完整小程序登录+价;朋友圈单页与正文同款「购买本章 ¥1」点后再展开极简说明 -->
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
@@ -189,13 +199,23 @@
<!-- 预览内容 + 付费墙 - 已登录未购买 -->
<view class="article preview" wx:if="{{accessState === 'locked_not_purchased'}}">
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
<text user-select>{{item}}</text>
<view class="preview-wrap">
<view
id="previewContentMeasure"
class="preview-body"
style="{{previewMaxHeightPx > 0 ? ('max-height:' + previewMaxHeightPx + 'px; overflow:hidden;') : ''}}"
>
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
<text user-select wx:if="{{!(item.length === 1 && item[0].type === 'image')}}"><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.mentionDisplay}}</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}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
</block>
</view>
</view>
<!-- 渐变遮罩(定位在裁切容器底部) -->
<view class="fade-mask"></view>
</view>
<!-- 渐变遮罩 -->
<view class="fade-mask"></view>
<!-- 付费墙 - 已登录未购买 -->
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>

View File

@@ -237,6 +237,15 @@
position: relative;
}
/* ===== 预览裁切容器(按百分比显示) ===== */
.preview-wrap {
position: relative;
}
.preview-body {
/* 裁切由内联 stylemax-height + overflow控制 */
}
/* ===== 渐变遮罩 ===== */
.fade-mask {
position: absolute;

View File

@@ -114,9 +114,17 @@ func DBCKBLeadList(c *gin.Context) {
if p := personMap[r.TargetPersonID]; p != nil {
personName = p.Name
ckbPlanId = p.CkbPlanID
// 兜底:迁移后计划 key 多写在 persons.ckb_api_key但 lead_records.plan_api_key 可能仍为空
// 此处仅用于管理端回显(前端会做掩码展示),不改库
if planKey == "" && strings.TrimSpace(p.CkbApiKey) != "" {
planKey = strings.TrimSpace(p.CkbApiKey)
}
} else if strings.TrimSpace(r.TargetPersonID) == "" && r.Source == "index_link_button" && indexLinkFallback != nil {
personName = indexLinkFallback.Name
ckbPlanId = indexLinkFallback.CkbPlanID
if planKey == "" && strings.TrimSpace(indexLinkFallback.CkbApiKey) != "" {
planKey = strings.TrimSpace(indexLinkFallback.CkbApiKey)
}
}
displayNick := r.Nickname
userAvatar := ""