diff --git a/miniprogram/app.js b/miniprogram/app.js index aa854e05..086642aa 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -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' diff --git a/miniprogram/pages/read/read.js b/miniprogram/pages/read/read.js index 4f9409b4..2e1d714b 100644 --- a/miniprogram/pages/read/read.js +++ b/miniprogram/pages/read/read.js @@ -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 diff --git a/miniprogram/pages/read/read.wxml b/miniprogram/pages/read/read.wxml index beff9bb8..baa547a2 100644 --- a/miniprogram/pages/read/read.wxml +++ b/miniprogram/pages/read/read.wxml @@ -113,15 +113,25 @@ - + - - {{item}} + + + + {{seg.text}}{{seg.mentionDisplay}}#{{seg.label}} + + + + + + + - - - @@ -189,13 +199,23 @@ - - {{item}} + + + + {{seg.text}}{{seg.mentionDisplay}}#{{seg.label}} + + + + + + + - - - diff --git a/miniprogram/pages/read/read.wxss b/miniprogram/pages/read/read.wxss index 2fe7a67a..a011982f 100644 --- a/miniprogram/pages/read/read.wxss +++ b/miniprogram/pages/read/read.wxss @@ -237,6 +237,15 @@ position: relative; } +/* ===== 预览裁切容器(按百分比显示) ===== */ +.preview-wrap { + position: relative; +} + +.preview-body { + /* 裁切由内联 style(max-height + overflow)控制 */ +} + /* ===== 渐变遮罩 ===== */ .fade-mask { position: absolute; diff --git a/soul-api/internal/handler/db_ckb_leads.go b/soul-api/internal/handler/db_ckb_leads.go index e673cd27..18610207 100644 --- a/soul-api/internal/handler/db_ckb_leads.go +++ b/soul-api/internal/handler/db_ckb_leads.go @@ -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 := ""