1
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -237,6 +237,15 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== 预览裁切容器(按百分比显示) ===== */
|
||||
.preview-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.preview-body {
|
||||
/* 裁切由内联 style(max-height + overflow)控制 */
|
||||
}
|
||||
|
||||
/* ===== 渐变遮罩 ===== */
|
||||
.fade-mask {
|
||||
position: absolute;
|
||||
|
||||
@@ -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 := ""
|
||||
|
||||
Reference in New Issue
Block a user