chore: 以本地工作区为准全量快照同步 GitHub
包含小程序、管理端、soul-api、脚本与静态资源等当前本地全部已跟踪与新增文件(排除 .DS_Store 与 .obsidian)。 Made-with: Cursor
This commit is contained in:
@@ -10,13 +10,11 @@ const mpPagePopups = require('./utils/mpPagePopups.js')
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
// 线上正式域名(真机/体验版/正式版强制使用此地址)
|
||||
// const API_BASE_URL_PROD = 'https://soulapi.quwanzhi.com'
|
||||
const API_BASE_URL_PROD = 'https://souldev.quwanzhi.com'
|
||||
// 线上正式 API(真机/体验版/正式版)。永平仓库默认正式域;部署前核对见 .cursor/skills/security-server-ops/SKILL.md
|
||||
const API_BASE_URL_PROD = 'https://soulapi.quwanzhi.com'
|
||||
|
||||
// 本地开发地址(仅开发者工具模拟器生效;真机自动回退 PROD)
|
||||
// const API_BASE_URL_DEV = 'http://127.0.0.1:9100'
|
||||
const API_BASE_URL_DEV = 'https://souldev.quwanzhi.com'
|
||||
// 开发者工具默认与正式一致;若需连测服可改为 https://souldev.quwanzhi.com 或本机 http://127.0.0.1:9100
|
||||
const API_BASE_URL_DEV = 'https://soulapi.quwanzhi.com'
|
||||
|
||||
// 运行时自动判断:非开发工具(真机/体验版/正式版)强制用 PROD,避免 localhost 打进上传包导致白屏
|
||||
const _isDevTools = (typeof __wxConfig !== 'undefined' && __wxConfig.platform === 'devtools')
|
||||
@@ -62,7 +60,7 @@ App({
|
||||
// 购买记录
|
||||
purchasedSections: [],
|
||||
hasFullBook: false,
|
||||
// VIP 会员(365天,包含增值版免费;与 hasFullBook=9.9 买断不同)
|
||||
// VIP 会员(365天,包含增值版免费;与 hasFullBook=365读书会权益不同)
|
||||
isVip: false,
|
||||
vipExpireDate: '',
|
||||
|
||||
@@ -570,6 +568,11 @@ App({
|
||||
if (!base) return
|
||||
// 本机 API 无稳定 WSS 时跳过,减少控制台报错与无效重连
|
||||
if (/^(https?:\/\/)?(127\.0\.0\.1|localhost)(:\d+)?$/i.test(base)) return
|
||||
try {
|
||||
const h = new URL(base.startsWith('http') ? base : 'https://' + base).hostname
|
||||
// 开发/预览域名未部署 WSS 时跳过(避免 DevTools 反复报 WebSocket failed)
|
||||
if (h === 'souldev.quwanzhi.com' || h.endsWith('.local')) return
|
||||
} catch (_) {}
|
||||
const wsUrl = base.replace(/^http/, 'ws') + '/ws/miniprogram'
|
||||
if (this._wsHeartbeatTimer) {
|
||||
clearInterval(this._wsHeartbeatTimer)
|
||||
@@ -1023,10 +1026,8 @@ App({
|
||||
},
|
||||
success: (res) => {
|
||||
const data = res.data
|
||||
const rejectWithBody = (message, body) => {
|
||||
const err = new Error(message)
|
||||
if (body != null && typeof body === 'object') err.response = body
|
||||
reject(err)
|
||||
const rejectWithBody = (message) => {
|
||||
reject(new Error(message))
|
||||
}
|
||||
if (res.statusCode === 200) {
|
||||
if (data && data.success === false) {
|
||||
@@ -1035,7 +1036,7 @@ App({
|
||||
this.logout()
|
||||
}
|
||||
showError(msg)
|
||||
rejectWithBody(msg, data)
|
||||
rejectWithBody(msg)
|
||||
return
|
||||
}
|
||||
resolve(data)
|
||||
@@ -1044,14 +1045,12 @@ App({
|
||||
if (res.statusCode === 401) {
|
||||
this.logout()
|
||||
showError('未授权,请重新登录')
|
||||
const err = new Error('未授权')
|
||||
if (data != null && typeof data === 'object') err.response = data
|
||||
reject(err)
|
||||
reject(new Error('未授权'))
|
||||
return
|
||||
}
|
||||
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
|
||||
showError(msg)
|
||||
rejectWithBody(msg, data && typeof data === 'object' ? data : undefined)
|
||||
rejectWithBody(msg)
|
||||
},
|
||||
fail: (err) => {
|
||||
const msg = (err && err.errMsg)
|
||||
@@ -1090,11 +1089,9 @@ App({
|
||||
if (pending[dedupKey]) return pending[dedupKey].promise
|
||||
}
|
||||
|
||||
const promise = this._requestOnce(url, options, silent).catch(async (err) => {
|
||||
const promise = this._requestOnce(url, options, silent).catch((err) => {
|
||||
const msg = (err && err.message) ? err.message : '网络异常,请重试'
|
||||
const next = new Error(msg)
|
||||
if (err && err.response != null) next.response = err.response
|
||||
throw next
|
||||
throw new Error(msg)
|
||||
})
|
||||
|
||||
if (method === 'GET') {
|
||||
@@ -1408,6 +1405,22 @@ App({
|
||||
return (this.globalData.readSectionIds || []).length
|
||||
},
|
||||
|
||||
/** 最近打开过的不同章节数(含仅预览),与 getReadCount 取大用于「浏览 N 章 →365」规则 */
|
||||
getBrowseDistinctChapterCount() {
|
||||
try {
|
||||
let list = wx.getStorageSync('recent_section_opens')
|
||||
if (!Array.isArray(list)) return 0
|
||||
const ids = new Set()
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const x = list[i]
|
||||
if (x && x.id) ids.add(String(x.id))
|
||||
}
|
||||
return ids.size
|
||||
} catch (e) {
|
||||
return 0
|
||||
}
|
||||
},
|
||||
|
||||
// 获取章节总数
|
||||
getTotalSections() {
|
||||
return this.globalData.totalSections
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
"pages/addresses/edit",
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/wallet/wallet",
|
||||
"pages/super-crm/super-crm",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail",
|
||||
"pages/mentors/mentors",
|
||||
"pages/match-history/match-history",
|
||||
"pages/mentor-detail/mentor-detail",
|
||||
"pages/profile-show/profile-show",
|
||||
"pages/profile-edit/profile-edit",
|
||||
|
||||
@@ -27,7 +27,7 @@ Page({
|
||||
{ name: '真实的赚钱', chapters: 20 },
|
||||
{ name: '真实的社会', chapters: 9 }
|
||||
],
|
||||
price: 9.9
|
||||
price: 365
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
</view>
|
||||
<view class="book-stat">
|
||||
<text class="book-stat-value">¥{{bookInfo.price}}</text>
|
||||
<text class="book-stat-label">全书价格</text>
|
||||
<text class="book-stat-label">读书会价格</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="parts-list">
|
||||
|
||||
@@ -12,49 +12,44 @@ const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
isVip: false,
|
||||
purchasedSections: [],
|
||||
|
||||
// 懒加载:篇章列表(不含章节详情),展开时再请求 chapters-by-part
|
||||
// 多书架构
|
||||
bookList: [],
|
||||
expandedBookId: null,
|
||||
|
||||
// 当前展开书的篇章数据(按 bookId 索引)
|
||||
_bookPartsData: {},
|
||||
|
||||
totalSections: 0,
|
||||
bookData: [],
|
||||
|
||||
// 展开状态
|
||||
expandedPart: null,
|
||||
bookCollapsed: false,
|
||||
|
||||
// 已加载的篇章章节缓存 { partId: chapters }
|
||||
_loadedChapters: {},
|
||||
|
||||
// 小三角点击动画:当前触发的子章 id(与 chapter.id 比对)
|
||||
_triangleAnimating: '',
|
||||
|
||||
// 固定模块 id -> mid(序言/尾声/附录,供 goToRead 传 mid)
|
||||
fixedSectionsMap: {},
|
||||
|
||||
// 附录
|
||||
appendixList: [
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
||||
],
|
||||
|
||||
// book/parts 加载中
|
||||
partsLoading: true,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true,
|
||||
bookFreePreviewPercent: 100,
|
||||
|
||||
// mp_config.mpUi.chaptersPage
|
||||
chaptersBookTitle: '一场SOUL的创业实验场',
|
||||
chaptersBookSubtitle: '来自Soul派对房的真实商业故事',
|
||||
chaptersBookTitle: '一场Soul的创业实验',
|
||||
chaptersBookSubtitle: '来自派对房的真实故事',
|
||||
chaptersNewBadgeText: 'NEW'
|
||||
},
|
||||
|
||||
@@ -66,7 +61,7 @@ Page({
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadVipStatus()
|
||||
this.loadParts()
|
||||
this.loadBooks()
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
@@ -75,9 +70,9 @@ Page({
|
||||
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
|
||||
const newBadgeText = String(c.newBadgeText || c.sectionNewBadgeText || h.latestSectionTitle || 'NEW').trim() || 'NEW'
|
||||
this.setData({
|
||||
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
|
||||
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
|
||||
'来自Soul派对房的真实商业故事',
|
||||
chaptersBookTitle: String(c.bookTitle || '一场Soul的创业实验').trim() || '一场Soul的创业实验',
|
||||
chaptersBookSubtitle: String(c.bookSubtitle || '来自派对房的真实故事').trim() ||
|
||||
'来自派对房的真实故事',
|
||||
chaptersNewBadgeText: newBadgeText
|
||||
})
|
||||
},
|
||||
@@ -90,6 +85,8 @@ Page({
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
const pct = app.globalData.bookFreePreviewPercent
|
||||
if (typeof pct === 'number') this.setData({ bookFreePreviewPercent: pct })
|
||||
this._applyChaptersMpUi()
|
||||
return
|
||||
}
|
||||
@@ -98,7 +95,9 @@ Page({
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
const previewPct = (typeof res?.bookFreePreviewPercent === 'number') ? res.bookFreePreviewPercent : 100
|
||||
app.globalData.bookFreePreviewPercent = previewPct
|
||||
this.setData({ searchEnabled, bookFreePreviewPercent: previewPct })
|
||||
this._applyChaptersMpUi()
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
@@ -106,11 +105,56 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 懒加载:仅拉取篇章列表 + totalSections + fixedSections(book/parts,不再用 all-chapters)
|
||||
async loadParts() {
|
||||
async loadBooks() {
|
||||
this.setData({ partsLoading: true })
|
||||
let books = []
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/books', silent: true })
|
||||
if (res?.success && Array.isArray(res.books)) {
|
||||
books = res.books
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Chapters] 加载书列表失败,使用默认书:', e?.message || e)
|
||||
}
|
||||
if (books.length === 0) {
|
||||
books = [{
|
||||
id: 1, bookKey: 'main',
|
||||
title: this.data.chaptersBookTitle || '一场Soul的创业实验',
|
||||
subtitle: this.data.chaptersBookSubtitle || '来自派对房的真实故事',
|
||||
icon: '', iconEmoji: '📖', sortOrder: 0, sectionCount: 0,
|
||||
}]
|
||||
}
|
||||
this.setData({
|
||||
bookList: books.map(b => ({
|
||||
id: b.id,
|
||||
bookKey: b.bookKey || 'main',
|
||||
title: b.title,
|
||||
subtitle: b.subtitle || '',
|
||||
icon: b.icon || '',
|
||||
iconEmoji: b.iconEmoji || '',
|
||||
sortOrder: b.sortOrder || 0,
|
||||
sectionCount: b.sectionCount || 0,
|
||||
})),
|
||||
expandedBookId: null,
|
||||
partsLoading: false,
|
||||
})
|
||||
},
|
||||
|
||||
async loadPartsForBook(bookId) {
|
||||
const effectiveBookId = bookId > 0 ? bookId : 1
|
||||
if (effectiveBookId > 0 && this.data._bookPartsData[effectiveBookId]) {
|
||||
this.setData({
|
||||
bookData: this.data._bookPartsData[effectiveBookId].parts,
|
||||
totalSections: this.data._bookPartsData[effectiveBookId].total,
|
||||
fixedSectionsMap: this.data._bookPartsData[effectiveBookId].fixedMap,
|
||||
appendixList: this.data._bookPartsData[effectiveBookId].appendixList,
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setData({ partsLoading: true })
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/parts', silent: true })
|
||||
const url = `/api/miniprogram/book/parts${effectiveBookId > 0 ? '?bookId=' + effectiveBookId : ''}`
|
||||
const res = await app.request({ url, silent: true })
|
||||
let parts = []
|
||||
let totalSections = 0
|
||||
let fixedSections = []
|
||||
@@ -142,17 +186,23 @@ Page({
|
||||
subtitle: p.subtitle || '',
|
||||
chapterCount: p.chapterCount || 0,
|
||||
chapters: [],
|
||||
alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
|
||||
alwaysShow: false
|
||||
}
|
||||
})
|
||||
app.globalData.totalSections = totalSections
|
||||
if (effectiveBookId > 0) {
|
||||
const cached = { ...this.data._bookPartsData }
|
||||
cached[effectiveBookId] = { parts: bookData, total: totalSections, fixedMap, appendixList }
|
||||
this.setData({ _bookPartsData: cached })
|
||||
}
|
||||
this.setData({
|
||||
bookData,
|
||||
totalSections,
|
||||
fixedSectionsMap: fixedMap,
|
||||
appendixList,
|
||||
_loadedChapters: {},
|
||||
partsLoading: false
|
||||
partsLoading: false,
|
||||
expandedPart: null
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[Chapters] 加载篇章失败:', e)
|
||||
@@ -194,8 +244,20 @@ Page({
|
||||
})
|
||||
})
|
||||
const chapters = Array.from(chMap.values())
|
||||
chapters.forEach(ch => ch.sections.reverse())
|
||||
// 目录子章下列表:默认最多展示 5 条,点小三角每次再展开 5 条
|
||||
|
||||
// 按免费展示比例标记:超出比例的章节标记为需付费
|
||||
const freePercent = this.data.bookFreePreviewPercent ?? 100
|
||||
if (freePercent < 100) {
|
||||
let allSections = []
|
||||
chapters.forEach(ch => { allSections = allSections.concat(ch.sections) })
|
||||
const freeCount = Math.floor(allSections.length * freePercent / 100)
|
||||
allSections.forEach((s, idx) => {
|
||||
if (idx >= freeCount && !s.isFree) {
|
||||
s.needPay = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
chapters.forEach((ch) => {
|
||||
const n = ch.sections.length
|
||||
ch.sectionVisibleLimit = n === 0 ? 0 : Math.min(5, n)
|
||||
@@ -218,8 +280,28 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async toggleBook(e) {
|
||||
const bookId = Number(e.currentTarget.dataset.bookId)
|
||||
trackClick('chapters', 'tab_click', `book_${bookId}`)
|
||||
const isExpanding = this.data.expandedBookId !== bookId
|
||||
this.setData({
|
||||
expandedBookId: isExpanding ? bookId : null,
|
||||
expandedPart: null
|
||||
})
|
||||
if (isExpanding) {
|
||||
await this.loadPartsForBook(bookId)
|
||||
// 自动展开第一个篇章(二级默认展开)
|
||||
if (this.data.bookData.length > 0) {
|
||||
const firstPartId = this.data.bookData[0].id
|
||||
this.setData({ expandedPart: firstPartId })
|
||||
await this.loadChaptersByPart(firstPartId)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.loadParts()
|
||||
this.setData({ _bookPartsData: {} })
|
||||
this.loadBooks()
|
||||
.then(() => wx.stopPullDownRefresh())
|
||||
.catch(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
@@ -239,7 +321,7 @@ Page({
|
||||
this.loadVipStatus()
|
||||
},
|
||||
|
||||
// 拉取 VIP 状态(isVip=会员,hasFullBook=9.9 买断)
|
||||
// 拉取 VIP 状态(isVip=会员,hasFullBook=365读书会权益)
|
||||
async loadVipStatus() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!--pages/chapters/chapters.wxml-->
|
||||
<!--Soul创业实验 - 目录页 1:1还原Web版本-->
|
||||
<!--Soul创业实验 - 目录页(多书架构)-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
@@ -18,8 +18,8 @@
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 目录骨架屏:加载中时展示 -->
|
||||
<view class="parts-skeleton" wx:if="{{partsLoading}}">
|
||||
<view class="skeleton-book-card">
|
||||
<view class="parts-skeleton" wx:if="{{partsLoading && bookList.length === 0}}">
|
||||
<view class="skeleton-book-card" wx:for="{{[1,2,3]}}" wx:key="*this">
|
||||
<view class="skeleton-book-icon"></view>
|
||||
<view class="skeleton-book-info">
|
||||
<view class="skeleton-line skeleton-title"></view>
|
||||
@@ -27,129 +27,142 @@
|
||||
</view>
|
||||
<view class="skeleton-count"></view>
|
||||
</view>
|
||||
<view class="skeleton-part-list">
|
||||
<view class="skeleton-part-item" wx:for="{{[1,2,3,4,5]}}" wx:key="*this">
|
||||
<view class="skeleton-part-header"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书籍信息卡(点击折叠/展开除"每日派对干货"外的篇章) -->
|
||||
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}" bindtap="toggleBookCollapse">
|
||||
<view class="book-icon">
|
||||
<view class="book-icon-inner"><icon name="book-open" size="56" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="book-info">
|
||||
<text class="book-title">{{chaptersBookTitle}}</text>
|
||||
<text class="book-subtitle">{{chaptersBookSubtitle}}</text>
|
||||
</view>
|
||||
<view class="book-right-area">
|
||||
<view class="book-count">
|
||||
<text class="count-value brand-color">{{totalSections}}</text>
|
||||
<text class="count-label">章节</text>
|
||||
</view>
|
||||
<text class="book-collapse-hint">{{bookCollapsed ? '展开 ▸' : '折叠 ▾'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 多书列表 -->
|
||||
<view class="chapters-content" wx:if="{{bookList.length > 0 || !partsLoading}}">
|
||||
|
||||
<!-- 目录内容 -->
|
||||
<view class="chapters-content" wx:if="{{!partsLoading}}">
|
||||
<!-- 序言(优先传 mid,阅读页用 by-mid 请求) -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="preface" data-mid="{{fixedSectionsMap.preface}}">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand"><icon name="book-open" size="36" color="#00CED1"></icon></view>
|
||||
<text class="item-title">序言|为什么我每天早上6点在Soul开播?</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.4)" customClass="item-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 篇章列表 -->
|
||||
<view class="part-list">
|
||||
<view class="part-item" wx:for="{{bookData}}" wx:key="id" wx:if="{{!bookCollapsed || item.alwaysShow}}">
|
||||
<!-- 篇章标题 -->
|
||||
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
|
||||
<view class="part-left">
|
||||
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFill"/>
|
||||
<view wx:elif="{{item.iconText}}" class="part-icon part-icon-text">{{item.iconText}}</view>
|
||||
<view wx:elif="{{item.iconEmoji}}" class="part-icon part-icon-emoji">{{item.iconEmoji}}</view>
|
||||
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
|
||||
<view class="part-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle">{{item.subtitle}}</text>
|
||||
</view>
|
||||
<!-- 按书遍历 -->
|
||||
<view class="book-group {{expandedBookId === book.id ? 'book-group--expanded' : ''}}" wx:for="{{bookList}}" wx:key="id" wx:for-item="book">
|
||||
<!-- 书籍卡片(点击折叠/展开) -->
|
||||
<view class="book-card" bindtap="toggleBook" data-book-id="{{book.id}}">
|
||||
<view class="book-card-left">
|
||||
<view class="book-card-icon">
|
||||
<text class="book-card-emoji">{{book.iconEmoji || '📖'}}</text>
|
||||
</view>
|
||||
<view class="part-right">
|
||||
<text class="part-count">{{item.chapters.length || item.chapterCount}}章</text>
|
||||
<icon name="{{expandedPart === item.id ? 'chevron-down' : 'chevron-right'}}" size="28" color="rgba(255,255,255,0.4)" customClass="part-arrow"></icon>
|
||||
<view class="book-card-meta">
|
||||
<text class="book-card-title">{{book.title}}</text>
|
||||
<text class="book-card-subtitle">{{book.subtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="book-card-right">
|
||||
<view class="book-card-stats">
|
||||
<text class="book-card-count">{{book.sectionCount || totalSections}}</text>
|
||||
<text class="book-card-unit">章节</text>
|
||||
</view>
|
||||
<text class="book-card-toggle {{expandedBookId === book.id ? 'toggle-active' : ''}}">{{expandedBookId === book.id ? '收起 ▴' : '展开 ▾'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书展开时:序言 + 篇章 + 尾声 + 附录 -->
|
||||
<block wx:if="{{expandedBookId === book.id}}">
|
||||
|
||||
<!-- 序言(仅主书 bookKey=main 显示) -->
|
||||
<view class="chapter-item" wx:if="{{book.bookKey === 'main' && fixedSectionsMap.preface}}" bindtap="goToRead" data-id="preface" data-mid="{{fixedSectionsMap.preface}}">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand">
|
||||
<text class="item-emoji">📜</text>
|
||||
</view>
|
||||
<text class="item-title">序言|为什么我每天早上6点在Soul开播?</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.4)" customClass="item-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 章节列表 - 展开时显示,懒加载 -->
|
||||
<block wx:if="{{expandedPart === item.id}}">
|
||||
<view class="chapters-list">
|
||||
<view wx:if="{{item.chapters.length === 0}}" class="chapters-loading">加载中...</view>
|
||||
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
|
||||
<view class="chapter-header">{{chapter.title}}</view>
|
||||
<view class="section-list">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section" wx:for-index="secIdx">
|
||||
<view class="section-item" wx:if="{{secIdx < chapter.sectionVisibleLimit}}" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<view class="section-lock-wrap">
|
||||
<icon wx:if="{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" name="lock-open" size="24" color="#00CED1" customClass="section-lock lock-open"></icon>
|
||||
<icon wx:else name="lock" size="24" color="rgba(255,255,255,0.3)" customClass="section-lock lock-closed"></icon>
|
||||
<!-- 加载中提示 -->
|
||||
<view class="chapters-loading" wx:if="{{partsLoading && bookData.length === 0}}">加载中...</view>
|
||||
|
||||
<!-- 篇章列表 -->
|
||||
<view class="part-list">
|
||||
<view class="part-item" wx:for="{{bookData}}" wx:key="id">
|
||||
<!-- 篇章标题 -->
|
||||
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
|
||||
<view class="part-left">
|
||||
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFill"/>
|
||||
<view wx:elif="{{item.iconEmoji}}" class="part-icon part-icon-emoji">{{item.iconEmoji}}</view>
|
||||
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
|
||||
<view class="part-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle" wx:if="{{item.subtitle}}">{{item.subtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-right">
|
||||
<text class="part-count">{{item.chapters.length || item.chapterCount}}章</text>
|
||||
<icon name="{{expandedPart === item.id ? 'chevron-down' : 'chevron-right'}}" size="28" color="rgba(255,255,255,0.4)" customClass="part-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 章节列表 - 展开时显示,懒加载 -->
|
||||
<block wx:if="{{expandedPart === item.id}}">
|
||||
<view class="chapters-list">
|
||||
<view wx:if="{{item.chapters.length === 0}}" class="chapters-loading">加载中...</view>
|
||||
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
|
||||
<view class="chapter-header">{{chapter.title}}</view>
|
||||
<view class="section-list">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section" wx:for-index="secIdx">
|
||||
<view class="section-item" wx:if="{{secIdx < chapter.sectionVisibleLimit}}" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<view class="section-lock-wrap">
|
||||
<icon wx:if="{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" name="lock-open" size="24" color="#00CED1" customClass="section-lock lock-open"></icon>
|
||||
<icon wx:else name="lock" size="24" color="rgba(255,255,255,0.3)" customClass="section-lock lock-closed"></icon>
|
||||
</view>
|
||||
<text class="section-title {{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
|
||||
<text wx:if="{{section.isNew || section.newBadgeText}}" class="tag tag-new">{{section.newBadgeText || chaptersNewBadgeText}}</text>
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
<text wx:elif="{{!(isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1)}}" class="section-price">¥{{section.price}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="section-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<text class="section-title {{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
|
||||
<text wx:if="{{section.isNew || section.newBadgeText}}" class="tag tag-new">{{section.newBadgeText || chaptersNewBadgeText}}</text>
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
<text wx:elif="{{!(isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1)}}" class="section-price">¥{{section.price}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="section-arrow"></icon>
|
||||
</block>
|
||||
<view class="section-expand-trigger" wx:if="{{chapter.sections.length > chapter.sectionVisibleLimit}}" bindtap="expandSectionChapter" data-part-id="{{item.id}}" data-chapter-id="{{chapter.id}}">
|
||||
<view class="latest-expand-triangle {{_triangleAnimating === chapter.id ? 'tri-bounce' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="section-expand-trigger" wx:if="{{chapter.sections.length > chapter.sectionVisibleLimit}}" bindtap="expandSectionChapter" data-part-id="{{item.id}}" data-chapter-id="{{chapter.id}}">
|
||||
<view class="latest-expand-triangle {{_triangleAnimating === chapter.id ? 'tri-bounce' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 尾声(优先传 mid) -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="epilogue" data-mid="{{fixedSectionsMap.epilogue}}">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand"><icon name="book-open" size="36" color="#00CED1"></icon></view>
|
||||
<text class="item-title">尾声|这本书的真实目的</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.4)" customClass="item-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附录 -->
|
||||
<view class="appendix-card card">
|
||||
<text class="appendix-title">附录</text>
|
||||
<view class="appendix-list">
|
||||
<view
|
||||
class="appendix-item"
|
||||
wx:for="{{appendixList}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<text class="appendix-text">{{item.title}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="appendix-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 尾声(仅主书显示) -->
|
||||
<view class="chapter-item" wx:if="{{book.bookKey === 'main' && fixedSectionsMap.epilogue}}" bindtap="goToRead" data-id="epilogue" data-mid="{{fixedSectionsMap.epilogue}}">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand">
|
||||
<text class="item-emoji">🏁</text>
|
||||
</view>
|
||||
<text class="item-title">尾声|这本书的真实目的</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.4)" customClass="item-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附录(仅主书显示) -->
|
||||
<view class="appendix-card card" wx:if="{{book.bookKey === 'main' && appendixList.length > 0}}">
|
||||
<text class="appendix-title">📎 附录</text>
|
||||
<view class="appendix-list">
|
||||
<view
|
||||
class="appendix-item"
|
||||
wx:for="{{appendixList}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<text class="appendix-text">{{item.title}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="appendix-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</block>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Soul创业实验 - 目录页样式
|
||||
* 1:1还原Web版本UI
|
||||
* 三书同级卡片 + 折叠/展开
|
||||
*/
|
||||
|
||||
.page {
|
||||
@@ -45,7 +45,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 搜索按钮 */
|
||||
.search-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
@@ -67,33 +66,31 @@
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
.brand-color { color: #00CED1; }
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
.nav-placeholder { width: 100%; }
|
||||
|
||||
/* ===== 目录骨架屏 ===== */
|
||||
.parts-skeleton {
|
||||
padding: 32rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-book-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
padding: 36rpx 32rpx;
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-book-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
@@ -108,7 +105,7 @@
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 32rpx;
|
||||
height: 28rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
@@ -119,26 +116,12 @@
|
||||
.skeleton-subtitle { width: 50%; }
|
||||
|
||||
.skeleton-count {
|
||||
width: 80rpx;
|
||||
height: 64rpx;
|
||||
width: 72rpx;
|
||||
height: 56rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
|
||||
.skeleton-part-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.skeleton-part-item .skeleton-part-header {
|
||||
height: 100rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: 16rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
@@ -146,109 +129,191 @@
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ===== 书籍信息卡 ===== */
|
||||
.book-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin: 32rpx 32rpx 24rpx 32rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.card-gradient {
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-icon-inner {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.book-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.book-right-area { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
|
||||
.book-count { text-align: right; }
|
||||
.count-value { font-size: 40rpx; font-weight: 700; display: block; }
|
||||
.count-label { font-size: 20rpx; color: rgba(255, 255, 255, 0.4); }
|
||||
.book-collapse-hint { font-size: 20rpx; color: #00CED1; opacity: 0.7; }
|
||||
|
||||
/* ===== 目录内容 ===== */
|
||||
/* ===== 目录内容区 ===== */
|
||||
.chapters-content {
|
||||
padding: 0 32rpx;
|
||||
padding: 16rpx 24rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 章节项 ===== */
|
||||
/* ===== 书组 ===== */
|
||||
.book-group {
|
||||
margin-bottom: 20rpx;
|
||||
border-radius: 28rpx;
|
||||
overflow: hidden;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.book-group--expanded {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.12);
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 书卡片 ===== */
|
||||
.book-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 28rpx;
|
||||
background: linear-gradient(135deg, #141416 0%, #1e2024 50%, #1a1c20 100%);
|
||||
border-radius: 28rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.15);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.book-card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 206, 209, 0.3), transparent);
|
||||
}
|
||||
|
||||
.book-card:active {
|
||||
background: linear-gradient(135deg, #1a1c1e 0%, #252830 50%, #202428 100%);
|
||||
}
|
||||
|
||||
.book-card-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-card-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 22rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #008B8B 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 6rpx 20rpx rgba(0, 206, 209, 0.25);
|
||||
}
|
||||
|
||||
.book-card-emoji {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.book-card-meta {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6rpx;
|
||||
}
|
||||
|
||||
.book-card-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
|
||||
.book-card-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.book-card-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: 6rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.book-card-stats {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.book-card-count {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #00CED1;
|
||||
line-height: 1.1;
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
|
||||
.book-card-unit {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-left: 2rpx;
|
||||
}
|
||||
|
||||
.book-card-toggle {
|
||||
font-size: 20rpx;
|
||||
color: rgba(0, 206, 209, 0.6);
|
||||
transition: color 0.2s ease;
|
||||
}
|
||||
|
||||
.toggle-active {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 章节项(序言/尾声) ===== */
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: 24rpx;
|
||||
padding: 22rpx 28rpx;
|
||||
margin: 0 12rpx 16rpx 12rpx;
|
||||
background: rgba(28, 28, 30, 0.7);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.chapter-item:active {
|
||||
background: #2c2c2e;
|
||||
background: rgba(44, 44, 46, 0.8);
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 14rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-emoji {
|
||||
font-size: 28rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.icon-brand {
|
||||
background: rgba(0, 206, 209, 0.2);
|
||||
background: rgba(0, 206, 209, 0.12);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 28rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
@@ -259,12 +324,12 @@
|
||||
.item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
font-size: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
@@ -273,9 +338,9 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
min-width: 80rpx;
|
||||
font-size: 20rpx;
|
||||
padding: 4rpx 14rpx;
|
||||
min-width: 64rpx;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
@@ -289,7 +354,7 @@
|
||||
.tag-new {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
font-size: 20rpx;
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
@@ -297,153 +362,134 @@
|
||||
.tag-vip {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: #FFD700;
|
||||
font-size: 20rpx;
|
||||
font-size: 18rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.text-brand {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.text-brand { color: #00CED1; }
|
||||
.text-muted { color: rgba(255, 255, 255, 0.4); }
|
||||
.text-xs { font-size: 22rpx; }
|
||||
|
||||
/* ===== 篇章列表 ===== */
|
||||
.part-list {
|
||||
margin-bottom: 24rpx;
|
||||
margin: 0 12rpx 16rpx;
|
||||
}
|
||||
|
||||
.part-item {
|
||||
margin-bottom: 24rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.part-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 22rpx 24rpx;
|
||||
background: rgba(28, 28, 30, 0.7);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.part-header:active {
|
||||
background: #2c2c2e;
|
||||
background: rgba(44, 44, 46, 0.8);
|
||||
}
|
||||
|
||||
.part-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
gap: 20rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.part-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 14rpx;
|
||||
background: linear-gradient(135deg, #1a2e3e 0%, #0d1b2a 100%);
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 与管理端 ChapterTree 篇头 emoji 一致 */
|
||||
|
||||
.part-icon-emoji {
|
||||
font-size: 34rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #1e3a4a 0%, #0f172a 100%);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.35);
|
||||
background: linear-gradient(135deg, #1a2e3e 0%, #0d1b2a 100%);
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.25);
|
||||
}
|
||||
|
||||
.part-icon-img {
|
||||
width: 64rpx; height: 64rpx; border-radius: 16rpx; flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 14rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.part-title {
|
||||
font-size: 28rpx;
|
||||
font-size: 27rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.part-icon-text {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
letter-spacing: 1rpx;
|
||||
line-height: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.part-subtitle {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.part-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-count {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.part-arrow {
|
||||
font-size: 32rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ===== 章节组 ===== */
|
||||
.chapters-list {
|
||||
margin-top: 16rpx;
|
||||
margin-left: 16rpx;
|
||||
margin-top: 12rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.chapters-loading {
|
||||
padding: 24rpx;
|
||||
font-size: 26rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.chapter-group {
|
||||
background: rgba(28, 28, 30, 0.5);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
padding: 14rpx 24rpx;
|
||||
font-size: 23rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-list {
|
||||
/* 小节列表 */
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.section-item {
|
||||
@@ -451,55 +497,46 @@
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 18rpx 24rpx;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.section-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.section-item:last-child { border-bottom: none; }
|
||||
|
||||
.section-item:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.section-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
gap: 14rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 小节锁图标 */
|
||||
.section-lock {
|
||||
width: 32rpx;
|
||||
min-width: 32rpx;
|
||||
font-size: 24rpx;
|
||||
width: 28rpx;
|
||||
min-width: 28rpx;
|
||||
font-size: 22rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.section-lock-wrap {
|
||||
min-width: 32rpx;
|
||||
min-width: 28rpx;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lock-open {
|
||||
color: #00CED1;
|
||||
}
|
||||
.lock-open { color: #00CED1; }
|
||||
.lock-closed { color: rgba(255, 255, 255, 0.3); }
|
||||
|
||||
.lock-closed {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 小节标题 */
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
font-size: 25rpx;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -507,13 +544,11 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 小节价格 */
|
||||
.section-price {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* 已购标签 */
|
||||
.tag-purchased {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
@@ -522,68 +557,56 @@
|
||||
.section-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
gap: 12rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
.section-arrow {
|
||||
font-size: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 附录 ===== */
|
||||
.card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin: 0 0 24rpx 0;
|
||||
width: 100%;
|
||||
background: rgba(28, 28, 30, 0.7);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
margin: 0 12rpx 16rpx 12rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.appendix-card {
|
||||
padding: 24rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 0 24rpx 0;
|
||||
padding: 20rpx 24rpx;
|
||||
}
|
||||
|
||||
.appendix-title {
|
||||
font-size: 24rpx;
|
||||
font-size: 23rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.appendix-list {
|
||||
/* 附录列表 */
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.appendix-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 14rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.appendix-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.appendix-item:last-child { border-bottom: none; }
|
||||
|
||||
.appendix-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
.appendix-item:active { opacity: 0.7; }
|
||||
|
||||
.appendix-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 23rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.appendix-arrow {
|
||||
font-size: 28rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
@@ -592,15 +615,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 0 12rpx;
|
||||
padding: 16rpx 0 10rpx;
|
||||
}
|
||||
|
||||
.latest-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 18rpx solid transparent;
|
||||
border-right: 18rpx solid transparent;
|
||||
border-top: 14rpx solid rgba(0, 206, 209, 0.55);
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 12rpx solid rgba(0, 206, 209, 0.5);
|
||||
opacity: 0.85;
|
||||
transform-origin: 50% 0;
|
||||
transition: border-top-color 0.15s ease;
|
||||
@@ -611,19 +634,9 @@
|
||||
}
|
||||
|
||||
@keyframes catalog-tri-nudge {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(10rpx) scale(1.12);
|
||||
opacity: 1;
|
||||
border-top-color: #00CED1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
0% { transform: translateY(0) scale(1); opacity: 0.85; }
|
||||
40% { transform: translateY(10rpx) scale(1.12); opacity: 1; border-top-color: #00CED1; }
|
||||
100% { transform: translateY(0) scale(1); opacity: 0.85; }
|
||||
}
|
||||
|
||||
.latest-expand-triangle.tri-bounce {
|
||||
|
||||
@@ -39,6 +39,17 @@ function superAvatarLetter(displayName) {
|
||||
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
|
||||
}
|
||||
|
||||
/** 旧 OSS 存 16personalities .svg 时改为 png 目录,与后端 resolveAvatarURL 一致 */
|
||||
function normalizeLegacyMbtiAvatarUrl(u) {
|
||||
if (!u || typeof u !== 'string') return u
|
||||
const lower = u.toLowerCase()
|
||||
if (!lower.includes('mbti-avatar')) return u
|
||||
let s = u.replace(/mbti-avatars-16personalities-male/g, 'mbti-avatars-png-male')
|
||||
s = s.replace(/mbti-avatars-16personalities-female/g, 'mbti-avatars-png-female')
|
||||
if (/\.svg$/i.test(s)) s = s.replace(/\.svg$/i, '.png')
|
||||
return s
|
||||
}
|
||||
|
||||
function parseTimeToMinutes(t) {
|
||||
const m = String(t || '').trim().match(/^(\d{1,2}):(\d{2})$/)
|
||||
if (!m) return null
|
||||
@@ -196,6 +207,10 @@ Page({
|
||||
superMembers: [],
|
||||
superMembersLoading: true,
|
||||
|
||||
// 收益排行榜
|
||||
earningsBoard: [],
|
||||
earningsBoardEnabled: false,
|
||||
|
||||
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
|
||||
latestChapters: [],
|
||||
displayLatestChapters: [],
|
||||
@@ -298,6 +313,10 @@ Page({
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
this._refreshHomeEntryFlags()
|
||||
void this.loadHomePinnedPerson()
|
||||
// 首页 tab 再次展示时重拉超级个体(避免首进网络失败或仅 onLoad 拉一次后长期空白)
|
||||
if (!app.globalData.auditMode) {
|
||||
void this.loadSuperMembers()
|
||||
}
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
@@ -330,6 +349,7 @@ Page({
|
||||
this.loadBookData()
|
||||
this.loadFeaturedAndLatest()
|
||||
this.loadSuperMembers()
|
||||
this.loadEarningsBoard()
|
||||
},
|
||||
|
||||
async loadSuperMembers() {
|
||||
@@ -359,6 +379,26 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async loadEarningsBoard() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/earnings/leaderboard', silent: true }).catch(() => null)
|
||||
if (res && res.success && res.enabled && Array.isArray(res.data) && res.data.length > 0) {
|
||||
const board = res.data.map(item => ({
|
||||
rank: item.rank,
|
||||
nickname: item.nickname || '分享达人',
|
||||
avatar: normalizeLegacyMbtiAvatarUrl(item.avatar || ''),
|
||||
avatarLetter: superAvatarLetter(item.nickname || '创')
|
||||
}))
|
||||
this.setData({ earningsBoard: board, earningsBoardEnabled: true })
|
||||
console.log('[Index] 收益排行榜:', board.length, '人')
|
||||
} else {
|
||||
this.setData({ earningsBoardEnabled: res && res.enabled === false ? false : true })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Index] 加载收益排行榜失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法,latest=updated_at)
|
||||
async loadFeaturedAndLatest() {
|
||||
this.setData({ bannerLoading: true })
|
||||
|
||||
@@ -137,6 +137,35 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益排行榜:与超级个体同结构(名次+头像+昵称,无能力分标签与数值) -->
|
||||
<view class="section" wx:if="{{earningsBoard.length > 0 && earningsBoardEnabled}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">收益排行榜</text>
|
||||
</view>
|
||||
<scroll-view class="super-scroll" scroll-x enhanced show-scrollbar="false">
|
||||
<view class="super-scroll-inner">
|
||||
<view
|
||||
class="super-item-h"
|
||||
wx:for="{{earningsBoard}}"
|
||||
wx:key="rank"
|
||||
hover-class="super-item-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="super-item-stack">
|
||||
<view class="share-avatar-wrap">
|
||||
<view class="share-rank {{item.rank <= 3 ? 'share-rank-top' : ''}}">{{item.rank}}</view>
|
||||
<view class="super-avatar">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.avatarLetter}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="super-name">{{item.nickname}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
<!-- 最新新增(时间线样式;超过 5 条时在列表下方点小三角展开,展开后随页面滚动看全部) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header latest-header">
|
||||
|
||||
@@ -729,6 +729,114 @@
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 收益排行榜(纵向列表样式预留,当前首页用 super-scroll 横滑) ===== */
|
||||
.earnings-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.earnings-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 16rpx;
|
||||
transition: background 0.15s;
|
||||
}
|
||||
.earnings-item-hover {
|
||||
background: rgba(255,255,255,0.08);
|
||||
}
|
||||
.earnings-rank {
|
||||
width: 44rpx;
|
||||
height: 44rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(255,255,255,0.5);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.earnings-rank-top {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
.earnings-avatar {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: rgba(0,206,209,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
border: 2rpx solid rgba(255,215,0,0.3);
|
||||
}
|
||||
.earnings-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.earnings-avatar-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
}
|
||||
.earnings-name {
|
||||
flex: 1;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255,255,255,0.85);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.earnings-amount {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #FFD700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 收益排行榜横滑:与 .super-scroll 共用;名次标叠在头像上 */
|
||||
.super-scroll .share-avatar-wrap {
|
||||
width: 112rpx;
|
||||
}
|
||||
|
||||
.share-avatar-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.share-avatar-wrap .share-rank {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.1);
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
top: -6rpx;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.share-rank-top {
|
||||
background: linear-gradient(135deg, #FFD700, #FFA500);
|
||||
color: #1a1a2e;
|
||||
}
|
||||
|
||||
/* ===== 最新新增(时间线样式) ===== */
|
||||
.latest-header {
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
80
miniprogram/pages/match-history/match-history.js
Normal file
80
miniprogram/pages/match-history/match-history.js
Normal file
@@ -0,0 +1,80 @@
|
||||
const app = getApp()
|
||||
|
||||
function formatTime(dateStr) {
|
||||
if (!dateStr) return ''
|
||||
const d = new Date(dateStr)
|
||||
if (isNaN(d.getTime())) return ''
|
||||
const now = new Date()
|
||||
const diffMs = now - d
|
||||
const diffMin = Math.floor(diffMs / 60000)
|
||||
if (diffMin < 1) return '刚刚'
|
||||
if (diffMin < 60) return diffMin + '分钟前'
|
||||
const diffH = Math.floor(diffMin / 60)
|
||||
if (diffH < 24) return diffH + '小时前'
|
||||
const diffD = Math.floor(diffH / 24)
|
||||
if (diffD < 7) return diffD + '天前'
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(d.getDate()).padStart(2, '0')
|
||||
return mm + '-' + dd
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
records: [],
|
||||
loading: false,
|
||||
noMore: false,
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onReachBottom() {
|
||||
if (!this.data.loading && !this.data.noMore) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
onPullDownRefresh() {
|
||||
this.setData({ records: [], page: 1, noMore: false })
|
||||
this.loadData().then(() => wx.stopPullDownRefresh())
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
const uid = app.globalData.userInfo?.id
|
||||
if (!uid) {
|
||||
this.setData({ loading: false })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/match/history?userId=${uid}&page=${this.data.page}&pageSize=${this.data.pageSize}`,
|
||||
silent: true,
|
||||
})
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
const items = res.data.map(item => ({
|
||||
...item,
|
||||
timeText: formatTime(item.createdAt),
|
||||
}))
|
||||
const merged = this.data.records.concat(items)
|
||||
this.setData({
|
||||
records: merged,
|
||||
page: this.data.page + 1,
|
||||
noMore: items.length < this.data.pageSize,
|
||||
})
|
||||
} else {
|
||||
this.setData({ noMore: true })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[MatchHistory] load error:', e)
|
||||
}
|
||||
this.setData({ loading: false })
|
||||
},
|
||||
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
})
|
||||
7
miniprogram/pages/match-history/match-history.json
Normal file
7
miniprogram/pages/match-history/match-history.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"navigationBarTitleText": "匹配记录",
|
||||
"navigationBarBackgroundColor": "#0a0a1a",
|
||||
"navigationBarTextStyle": "white",
|
||||
"backgroundColor": "#0a0a1a",
|
||||
"usingComponents": {}
|
||||
}
|
||||
128
miniprogram/pages/match-history/match-history.wxss
Normal file
128
miniprogram/pages/match-history/match-history.wxss
Normal file
@@ -0,0 +1,128 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0a0a1a;
|
||||
padding: 24rpx 32rpx;
|
||||
}
|
||||
.list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.record-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 28rpx 24rpx;
|
||||
background: rgba(255,255,255,0.04);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(255,255,255,0.06);
|
||||
}
|
||||
.record-left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.record-avatar {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: rgba(0,206,209,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 2rpx solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.avatar-letter {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
}
|
||||
.record-center {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.record-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255,255,255,0.9);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.record-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.record-type {
|
||||
font-size: 22rpx;
|
||||
color: #00CED1;
|
||||
background: rgba(0,206,209,0.1);
|
||||
padding: 4rpx 14rpx;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.record-time {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,255,255,0.35);
|
||||
}
|
||||
.record-contact {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.contact-tag {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255,255,255,0.45);
|
||||
}
|
||||
.record-right {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.score-value {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #FFD700;
|
||||
}
|
||||
.score-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255,255,255,0.35);
|
||||
}
|
||||
.empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 200rpx;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.empty-icon {
|
||||
font-size: 80rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255,255,255,0.4);
|
||||
}
|
||||
.empty-btn {
|
||||
margin-top: 16rpx;
|
||||
padding: 16rpx 48rpx;
|
||||
background: rgba(0,206,209,0.15);
|
||||
color: #00CED1;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
.loading-more, .no-more {
|
||||
text-align: center;
|
||||
padding: 40rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.3);
|
||||
}
|
||||
@@ -604,6 +604,19 @@
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.btn-gold-outline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 28rpx;
|
||||
background: transparent;
|
||||
color: #FFD700;
|
||||
font-size: 30rpx;
|
||||
text-align: center;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.4);
|
||||
}
|
||||
.btn-secondary {
|
||||
padding: 32rpx;
|
||||
background: #1c1c1e;
|
||||
@@ -616,6 +629,32 @@
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.btn-phone {
|
||||
background: linear-gradient(135deg, #34C759, #28a745);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 8rpx;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.match-mbti {
|
||||
font-size: 22rpx;
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
display: inline-block;
|
||||
margin-top: 6rpx;
|
||||
}
|
||||
|
||||
.interest-emoji {
|
||||
font-size: 32rpx;
|
||||
margin-right: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
|
||||
@@ -86,6 +86,9 @@ Page({
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 超级个体:快捷入口「客资 / 链接与轨迹」
|
||||
showSuperCrmEntry: false,
|
||||
|
||||
// mp_config.mpUi.myPage(后台可改文案/跳转)
|
||||
mpUiCardLabel: '名片',
|
||||
mpUiVipLabelVip: '会员中心',
|
||||
@@ -213,9 +216,11 @@ Page({
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadSuperCrmEntry()
|
||||
this.loadWalletBalance()
|
||||
this._refreshMyAvatarDisplay(safeUser)
|
||||
} else {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
const guestReadCount = app.getReadCount()
|
||||
const guestRecent = this._mergeRecentChaptersFromLocal([])
|
||||
this.setData({
|
||||
@@ -900,6 +905,7 @@ Page({
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
wallet: '/pages/wallet/wallet',
|
||||
superCrm: '/pages/super-crm/super-crm',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
@@ -953,6 +959,11 @@ Page({
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
goToMatchHistory() {
|
||||
trackClick('my', 'nav_click', '匹配记录')
|
||||
wx.navigateTo({ url: '/pages/match-history/match-history' })
|
||||
},
|
||||
|
||||
// 跳转到推广中心(需登录)
|
||||
goToReferral(e) {
|
||||
const focus = e && e.currentTarget && e.currentTarget.dataset ? (e.currentTarget.dataset.focus || '') : ''
|
||||
@@ -982,7 +993,25 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// VIP状态查询(注意:hasFullBook=9.9 买断,不等同 VIP)
|
||||
// VIP状态查询(注意:hasFullBook=365读书会权益,不等同 VIP)
|
||||
async loadSuperCrmEntry() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/my/super-stats?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
const show = !!(res && res.success && res.data && res.data.isSuperIndividual === true)
|
||||
this.setData({ showSuperCrmEntry: show })
|
||||
} catch (_) {
|
||||
this.setData({ showSuperCrmEntry: false })
|
||||
}
|
||||
},
|
||||
|
||||
async loadVipStatus() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">快捷入口</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stats-grid {{showSuperCrmEntry ? 'stats-grid-4' : ''}}">
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="orders">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">订单</text>
|
||||
@@ -110,6 +110,11 @@
|
||||
<text class="stat-num">{{walletBalanceText}}</text>
|
||||
<text class="stat-label">我的余额</text>
|
||||
</view>
|
||||
<view class="stat-box" wx:if="{{showSuperCrmEntry}}" bindtap="handleMenuTap" data-id="superCrm">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">客资</text>
|
||||
<text class="stat-label">链接与轨迹</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -201,6 +201,9 @@
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24rpx;
|
||||
align-items: stretch;
|
||||
}
|
||||
.stats-grid-4 {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
.stat-box {
|
||||
background: #252525; border-radius: 20rpx; padding: 24rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
|
||||
@@ -18,7 +18,7 @@ const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine')
|
||||
const { checkAndExecute, executeRule, getChapterReadMarketingRule } = require('../../utils/ruleEngine')
|
||||
const soulBridge = require('../../utils/soulBridge.js')
|
||||
|
||||
const app = getApp()
|
||||
@@ -26,25 +26,28 @@ const mpPagePopups = require('../../utils/mpPagePopups.js')
|
||||
|
||||
/** 与后端 defaultReadPreviewUI / read_preview_ui 配置键一致;占位符 {percent} {price} 在拉章节后替换 */
|
||||
const READ_UI_DEFAULTS = {
|
||||
singlePageUnlockTitle: '解锁完整内容',
|
||||
/** 朋友圈单页简洁版主标题(与分润说明分行) */
|
||||
singlePageUnlockTitle: '解锁全文',
|
||||
singlePagePayButtonText: '支付 ¥{price} 解锁全文',
|
||||
/** 单页展开区首行:付款引导;其下为分隔线 + shareTipLine + fullPaywallTip */
|
||||
singlePageExpandedHint: '预览页不能直接付款,务必先点底栏「前往小程序」。',
|
||||
payTapModalTitle: '解锁说明',
|
||||
payTapModalContent:
|
||||
'全文 ¥{price}。预览里无法完成支付:请先点屏幕底部「前往小程序」进入完整版,登录后再付款解锁。',
|
||||
fullUnlockTitle: '加入365读书会',
|
||||
fullUnlockDesc: '可先上滑阅读预览;需要全文时,点下方「支付¥{price}」查看说明',
|
||||
fullLockedProgressText: '已阅读约 {percent}% ,购买后继续阅读',
|
||||
fullPaywallTip: '分享给好友,好友购买你可获得约 90% 收益',
|
||||
notLoginUnlockDesc: '已预览约 {percent}% 内容,登录并支付 ¥{price} 后阅读全文',
|
||||
notLoginPaywallTip: '分享给好友,好友购买你可获得约 90% 收益',
|
||||
shareTipLine: '好友经你分享购买,你可获得约 90% 收益',
|
||||
/** 已登录付费墙不再展示大标题/上滑说明,仅保留「购买本章」为主按钮;以下为兼容旧配置占位 */
|
||||
fullUnlockTitle: '',
|
||||
fullUnlockDesc: '',
|
||||
fullLockedProgressText: '',
|
||||
/** 底部一句营销(未登录/已登录/单页共用,与后台 read_preview_ui.shareTipLine 一致) */
|
||||
fullPaywallTip: '转发给需要的人,一起学习还能赚佣金',
|
||||
notLoginUnlockDesc: '',
|
||||
notLoginPaywallTip: '转发给需要的人,一起学习还能赚佣金',
|
||||
shareTipLine: '转发给需要的人,一起学习还能赚佣金',
|
||||
momentsModalTitle: '分享到朋友圈',
|
||||
momentsModalContent:
|
||||
'已复制发圈文案(非分享给好友)。\n\n请点击右上角「···」→「分享到朋友圈」粘贴发布。',
|
||||
momentsClipboardFooter: '\n\n—— 以上为正文预览约 {percent}% ,搜「卡若创业派对」小程序阅读全文 ——',
|
||||
timelineTitleSuffix: '(预览{percent}%)',
|
||||
timelineImageUrl: '',
|
||||
}
|
||||
|
||||
/** 章节未返回前:用默认比例与价格生成 readUi,避免云端 read_preview_ui 为空时付费墙/预览条不显示 */
|
||||
@@ -239,7 +242,20 @@ Page({
|
||||
hasFullBook: false,
|
||||
canAccess: false, // 保留兼容性,从 accessState 派生
|
||||
purchasedCount: 0,
|
||||
fullbookShowThreshold: 2,
|
||||
/** 购买满 N 小节后展示「加入读书会」;与后台 fullbook_show_threshold 一致,默认 3 */
|
||||
fullbookShowThreshold: 3,
|
||||
/** 是否展示「加入读书会」按钮:满 N 节,或已购≥2 且非本书第一篇(有上一篇) */
|
||||
showFullBookCta: false,
|
||||
/** 浏览不同章节数 ≥ 阈值且双按钮并存时:凸显「加入读书会」;「购买本章」仅视觉次要,仍可付 ¥1 */
|
||||
paywall365Primary: false,
|
||||
/** 付费墙「购买本章」按钮完整 class(主渐变 / 次要描边) */
|
||||
sectionPurchaseBtnClass: 'purchase-btn purchase-section purchase-section--primary',
|
||||
/** 付费墙「加入读书会」按钮完整 class(含弱化/主按钮) */
|
||||
fullbookPurchaseBtnClass: 'purchase-btn purchase-fullbook',
|
||||
/** 用户旅程规则:365 营销锚点(revealMode=anchor 时展示,点击后再弹窗/支付) */
|
||||
pay365AnchorVisible: false,
|
||||
pay365AnchorLabel: '加入 365 读书会,解锁全部内容',
|
||||
pay365RuleSnapshot: null,
|
||||
|
||||
// 阅读进度
|
||||
readingProgress: 0,
|
||||
@@ -248,7 +264,7 @@ Page({
|
||||
/** 未解锁时:按 previewPercent 裁切可见高度(px)。0 表示未启用/待测量 */
|
||||
previewMaxHeightPx: 0,
|
||||
/** 与 mpPagePopups READ_PAGE_FALLBACK 一致;getConfig 前首屏即有字 */
|
||||
readBeforeLoginHint: '试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。',
|
||||
readBeforeLoginHint: '',
|
||||
readSinglePageTitle: '解锁完整内容',
|
||||
readSinglePageHint:
|
||||
'当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。',
|
||||
@@ -260,7 +276,7 @@ Page({
|
||||
|
||||
// 价格
|
||||
sectionPrice: 1,
|
||||
fullBookPrice: 9.9,
|
||||
fullBookPrice: 365,
|
||||
totalSections: 62,
|
||||
|
||||
// 弹窗
|
||||
@@ -275,6 +291,9 @@ Page({
|
||||
showLoginModal: false,
|
||||
showPrivacyModal: false,
|
||||
showPosterModal: false,
|
||||
showMomentsModal: false,
|
||||
momentsCopied: false,
|
||||
shareRate: 90,
|
||||
isPaying: false,
|
||||
isGeneratingPoster: false,
|
||||
|
||||
@@ -292,8 +311,9 @@ Page({
|
||||
|
||||
// 朋友圈单页模式(scene 1154 / systemInfo.mode):无法登录与支付,仅引导「前往小程序」
|
||||
readSinglePageMode: false,
|
||||
// 朋友圈单页付费墙:说明默认收起,点「购买本章」后展开极简文案 + 底栏箭头(无长 Modal)
|
||||
momentsPaywallExpanded: false,
|
||||
/** 点「支付解锁全文」时:底部引导浮层(指向系统底栏「前往小程序」) */
|
||||
showMomentsLaunchGuideModal: false,
|
||||
|
||||
// 未付费预览比例(与后端 unpaid_preview_percent / 章节 preview_percent 一致)
|
||||
effectivePreviewPercent: 20,
|
||||
@@ -424,26 +444,15 @@ Page({
|
||||
this.setData({ effectivePreviewPercent: pct, readUi: filled })
|
||||
},
|
||||
|
||||
/** 单页模式:点支付解锁 → 弹窗说明 → 展开底栏引导与箭头 */
|
||||
/** 单页模式:点支付解锁 → 底部浮层 + 向下箭头动画,引导底栏「前往小程序」 */
|
||||
onUnlockTapInSinglePage() {
|
||||
trackClick('read', 'btn_click', '单页_解锁引导')
|
||||
try {
|
||||
wx.vibrateShort({ type: 'light' })
|
||||
} catch (e) {}
|
||||
const ui = this.data.readUi || {}
|
||||
wx.showModal({
|
||||
title: ui.payTapModalTitle || '解锁说明',
|
||||
content:
|
||||
ui.payTapModalContent ||
|
||||
'请先点屏幕底部「前往小程序」进入完整版,登录后再付款解锁。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了',
|
||||
success: () => {
|
||||
if (this._detectReadSinglePage() && typeof this.setData === 'function') {
|
||||
this.setData({ readSinglePageMode: true, momentsPaywallExpanded: true })
|
||||
}
|
||||
},
|
||||
})
|
||||
try { wx.vibrateShort({ type: 'medium' }) } catch (_) {}
|
||||
this.setData({ showMomentsLaunchGuideModal: true })
|
||||
},
|
||||
|
||||
closeMomentsLaunchGuideModal() {
|
||||
this.setData({ showMomentsLaunchGuideModal: false })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
@@ -451,8 +460,57 @@ Page({
|
||||
this.setData({
|
||||
auditMode: app.globalData.auditMode || false,
|
||||
readSinglePageMode: sp,
|
||||
...(sp ? {} : { momentsPaywallExpanded: false }),
|
||||
...(sp ? {} : { momentsPaywallExpanded: false, showMomentsLaunchGuideModal: false }),
|
||||
})
|
||||
this._refreshAccessOnShow()
|
||||
if (!sp) {
|
||||
this.refreshPay365MarketingFromRules()
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 从后台回到阅读页时重拉 VIP 与解锁态。
|
||||
* 修复:管理端取消超级个体后,若仍用订单兜底 VIP,需配合后端 isVipEffective;同时刷新端上 showPaywall/全文裁切。
|
||||
*/
|
||||
async _refreshAccessOnShow() {
|
||||
const id = this.data.sectionId
|
||||
if (!id || this.data.loading || this.data.readSinglePageMode) return
|
||||
const uid = app.globalData.userInfo?.id
|
||||
if (!uid) return
|
||||
try {
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
const vipRes = await app.request({
|
||||
url: `/api/miniprogram/vip/status?userId=${encodeURIComponent(uid)}`,
|
||||
silent: true,
|
||||
timeout: 8000
|
||||
})
|
||||
if (vipRes && vipRes.success && vipRes.data) {
|
||||
app.globalData.isVip = !!vipRes.data.isVip
|
||||
app.globalData.vipExpireDate = vipRes.data.expireDate || ''
|
||||
const u = { ...(app.globalData.userInfo || {}) }
|
||||
u.isVip = app.globalData.isVip
|
||||
u.vipExpireDate = app.globalData.vipExpireDate
|
||||
app.globalData.userInfo = u
|
||||
try {
|
||||
wx.setStorageSync('userInfo', u)
|
||||
} catch (_) {}
|
||||
}
|
||||
const mid = this.data.sectionMid
|
||||
const chapterRes = await app.request({ url: this._getChapterUrl({ id, mid }), silent: true })
|
||||
const accessState = await accessManager.determineAccessState(id, chapterRes)
|
||||
const canAccess = accessManager.canAccessFullContent(accessState)
|
||||
if (accessState === this.data.accessState && canAccess === this.data.canAccess) return
|
||||
this.setData({
|
||||
accessState,
|
||||
canAccess,
|
||||
showPaywall: !canAccess,
|
||||
isLoggedIn: true
|
||||
})
|
||||
await this.loadContent(id, accessState, chapterRes)
|
||||
this.refreshPay365MarketingFromRules()
|
||||
} catch (e) {
|
||||
console.warn('[Read] onShow 刷新权限失败', e)
|
||||
}
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
@@ -530,6 +588,7 @@ Page({
|
||||
pendingGiftRequestSn: giftRequestSn || '',
|
||||
readSinglePageMode: this._detectReadSinglePage(),
|
||||
momentsPaywallExpanded: false,
|
||||
showMomentsLaunchGuideModal: false,
|
||||
})
|
||||
|
||||
if (ref) {
|
||||
@@ -542,12 +601,13 @@ Page({
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const rawThreshold = config.fullbookShowThreshold ?? config.prices?.fullbookShowThreshold
|
||||
const threshold = (rawThreshold != null && Number(rawThreshold) > 0) ? Number(rawThreshold) : 2
|
||||
const threshold = (rawThreshold != null && Number(rawThreshold) > 0) ? Number(rawThreshold) : 3
|
||||
this.setData({
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9,
|
||||
fullBookPrice: config.prices?.fullbook ?? 365,
|
||||
purchasedCount: purchasedSections.length,
|
||||
fullbookShowThreshold: threshold,
|
||||
shareRate: config.shareRate ?? 90,
|
||||
})
|
||||
|
||||
// 统一:先拉章节数据,用 isFree/price===0 判断免费
|
||||
@@ -590,6 +650,8 @@ Page({
|
||||
|
||||
// 5. 导航:文章详情已带 prev/next
|
||||
this._applyPrevNext(chapterRes)
|
||||
this.computeShowFullBookCta()
|
||||
this.refreshPay365MarketingFromRules()
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Read] 初始化失败:', e)
|
||||
@@ -731,6 +793,15 @@ Page({
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
app.touchRecentSection(id)
|
||||
// 未解锁试读也需触发「浏览 N 章→365」modal;单页模式无法支付由 ruleEngine 侧不弹或用户点引导
|
||||
const spMode = this.data.readSinglePageMode
|
||||
if (
|
||||
!spMode &&
|
||||
(accessManager.canAccessFullContent(accessState) ||
|
||||
accessState === accessManager.accessStates.LOCKED_NOT_PURCHASED)
|
||||
) {
|
||||
checkAndExecute('chapter_read', this)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败,尝试本地缓存:', e)
|
||||
@@ -783,11 +854,11 @@ Page({
|
||||
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
|
||||
}
|
||||
|
||||
// 普通章节
|
||||
// 普通章节(免费状态以后端为准,不在前端硬编码)
|
||||
return {
|
||||
id: id,
|
||||
title: this.getSectionTitle(id),
|
||||
isFree: id === '1.1',
|
||||
isFree: false,
|
||||
price: 1
|
||||
}
|
||||
},
|
||||
@@ -936,6 +1007,88 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
/** 是否展示「加入读书会」:满 N 次单章购买后,或已购≥2 且当前不是全书第一篇(有上一篇) */
|
||||
computeShowFullBookCta() {
|
||||
const th = Number(this.data.fullbookShowThreshold) || 3
|
||||
const pc = Number(this.data.purchasedCount) || 0
|
||||
const hasPrev = !!this.data.prevSection
|
||||
let show = pc >= th || (pc >= 2 && hasPrev)
|
||||
if (this.data.pay365AnchorVisible && this.data.pay365RuleSnapshot) {
|
||||
show = false
|
||||
}
|
||||
const browseN =
|
||||
typeof app.getBrowseDistinctChapterCount === 'function' ? app.getBrowseDistinctChapterCount() : 0
|
||||
/** 双按钮且已浏览 ≥N 个不同章节:读书会为主 CTA;单章仅视觉弱化(描边),仍始终可付 ¥1 */
|
||||
const paywall365Primary = !!(show && browseN >= th)
|
||||
|
||||
let sectionCls = 'purchase-btn purchase-section'
|
||||
let fullbookCls = 'purchase-btn purchase-fullbook'
|
||||
if (!show) {
|
||||
sectionCls += ' purchase-section--primary'
|
||||
} else if (paywall365Primary) {
|
||||
sectionCls += ' purchase-section--secondary'
|
||||
} else {
|
||||
sectionCls += ' purchase-section--primary'
|
||||
fullbookCls += ' purchase-fullbook--secondary'
|
||||
}
|
||||
|
||||
this.setData({
|
||||
showFullBookCta: show,
|
||||
paywall365Primary,
|
||||
sectionPurchaseBtnClass: sectionCls,
|
||||
fullbookPurchaseBtnClass: fullbookCls,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 后台规则 revealMode=anchor 时:仅展示锚点条,避免与「加入读书会」双按钮重复
|
||||
*/
|
||||
onPay365MarketingAnchor(rule) {
|
||||
if (!rule || rule.action !== 'pay365' || rule.revealMode !== 'anchor') return
|
||||
const label = (rule.anchorLabel && String(rule.anchorLabel).trim()) || '加入 365 读书会,解锁全部内容'
|
||||
this.setData({
|
||||
pay365AnchorVisible: true,
|
||||
pay365RuleSnapshot: rule,
|
||||
pay365AnchorLabel: label,
|
||||
})
|
||||
this.computeShowFullBookCta()
|
||||
},
|
||||
|
||||
/** 用户点击锚点 → 与规则引擎 executeRule 一致(弹窗确认后支付) */
|
||||
onPay365AnchorTap() {
|
||||
const rule = this.data.pay365RuleSnapshot
|
||||
if (!rule) return
|
||||
executeRule(rule, this)
|
||||
},
|
||||
|
||||
/**
|
||||
* 进入阅读页/切章后同步:未登录或无规则时不展示;已购读书会/VIP 不展示
|
||||
*/
|
||||
async refreshPay365MarketingFromRules() {
|
||||
try {
|
||||
const uid = app.globalData.userInfo && app.globalData.userInfo.id
|
||||
if (!uid || app.globalData.hasFullBook || app.globalData.isVip) {
|
||||
this.setData({ pay365AnchorVisible: false, pay365RuleSnapshot: null })
|
||||
this.computeShowFullBookCta()
|
||||
return
|
||||
}
|
||||
const rule = await getChapterReadMarketingRule()
|
||||
if (rule && rule.revealMode === 'anchor' && rule.action === 'pay365') {
|
||||
const label = (rule.anchorLabel && String(rule.anchorLabel).trim()) || '加入 365 读书会,解锁全部内容'
|
||||
this.setData({
|
||||
pay365AnchorVisible: true,
|
||||
pay365RuleSnapshot: rule,
|
||||
pay365AnchorLabel: label,
|
||||
})
|
||||
} else {
|
||||
this.setData({ pay365AnchorVisible: false, pay365RuleSnapshot: null })
|
||||
}
|
||||
this.computeShowFullBookCta()
|
||||
} catch (e) {
|
||||
console.warn('[Read] refreshPay365MarketingFromRules', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 返回(从分享进入无栈时回首页)
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
@@ -1292,15 +1445,25 @@ Page({
|
||||
}
|
||||
|
||||
const path = ref ? `/pages/read/read?${q}&ref=${ref}` : `/pages/read/read?${q}`
|
||||
const title = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 卡若创业派对 - 真实商业故事'
|
||||
const ui = this.data.readUi || {}
|
||||
const shareTpl = ui.shareTitle || ''
|
||||
let title = ''
|
||||
if (shareTpl && section?.title) {
|
||||
title = shareTpl.replace(/\{title\}/g, section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title)
|
||||
} else {
|
||||
title = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 卡若创业派对 - 真实商业故事'
|
||||
}
|
||||
return { title, path }
|
||||
},
|
||||
|
||||
// 分享到朋友圈:复制摘要 + 引导用户用右上角「···」发圈(无 open-type=shareTimeline)
|
||||
shareToMoments() {
|
||||
trackClick('read', 'btn_click', '分享到朋友圈_' + (this.data.sectionId || ''))
|
||||
this.setData({ showMomentsModal: true, momentsCopied: false })
|
||||
},
|
||||
|
||||
onMomentsCopy() {
|
||||
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
|
||||
const ui = this.data.readUi || {}
|
||||
const paras = (this.data.previewParagraphs || []).filter(Boolean).join('\n\n')
|
||||
@@ -1313,26 +1476,19 @@ Page({
|
||||
wx.setClipboardData({
|
||||
data: clipboardText,
|
||||
success: () => {
|
||||
// 系统会在 setClipboardData 成功后自动 toast「内容已复制」,与下方引导弹窗重复,先关掉再弹窗
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: ui.momentsModalTitle || '分享到朋友圈',
|
||||
content:
|
||||
ui.momentsModalContent ||
|
||||
'已复制发圈文案。\n\n请点击右上角「···」→「分享到朋友圈」粘贴发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
this.setData({ momentsCopied: true })
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
|
||||
wx.showToast({ title: '复制失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
closeMomentsModal() {
|
||||
this.setData({ showMomentsModal: false })
|
||||
},
|
||||
|
||||
// 分享到朋友圈:带文章标题,过长时截断
|
||||
onShareTimeline() {
|
||||
const { section, sectionId, sectionMid, chapterTitle, readUi } = this.data
|
||||
@@ -1341,12 +1497,23 @@ Page({
|
||||
const query = ref ? `${q}&ref=${ref}` : q
|
||||
const articleTitle = (section?.title || chapterTitle || '').trim()
|
||||
const base = articleTitle || '卡若创业派对 - 真实商业故事'
|
||||
const suffix = (readUi && readUi.timelineTitleSuffix) || ''
|
||||
let full = base + suffix
|
||||
let full = base
|
||||
if (full.length > 30) {
|
||||
full = full.slice(0, 30) + '…'
|
||||
}
|
||||
return { title: full, query }
|
||||
const imageRaw = (readUi && readUi.timelineImageUrl) || ''
|
||||
let imageUrl = ''
|
||||
if (imageRaw) {
|
||||
if (/^https?:\/\//i.test(imageRaw)) {
|
||||
imageUrl = imageRaw
|
||||
} else {
|
||||
const baseUrl = String((app.globalData && app.globalData.baseUrl) || '').replace(/\/$/, '')
|
||||
if (baseUrl) {
|
||||
imageUrl = `${baseUrl}/${String(imageRaw).replace(/^\/+/, '')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
return imageUrl ? { title: full, query, imageUrl } : { title: full, query }
|
||||
},
|
||||
|
||||
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
|
||||
@@ -1459,13 +1626,13 @@ Page({
|
||||
await this.processPayment('section', this.data.sectionId, price)
|
||||
},
|
||||
|
||||
// 购买全书 - 直接调起支付
|
||||
// 365 读书会(product_type=fullbook)- 直接调起支付
|
||||
async handlePurchaseFullBook() {
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
console.log('[Pay] 点击购买全书按钮')
|
||||
console.log('[Pay] 点击加入读书会按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
|
||||
if (!this.data.isLoggedIn) {
|
||||
@@ -1475,7 +1642,7 @@ Page({
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[Pay] 开始支付流程: 全书', { price: this.data.fullBookPrice })
|
||||
console.log('[Pay] 开始支付流程: 365读书会', { price: this.data.fullBookPrice })
|
||||
wx.hideLoading()
|
||||
await this.processPayment('fullbook', null, this.data.fullBookPrice)
|
||||
},
|
||||
@@ -1493,24 +1660,21 @@ Page({
|
||||
}
|
||||
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
const avatar = userInfo.avatarUrl || ''
|
||||
const nickname = userInfo.nickname || userInfo.nickName || ''
|
||||
const needProfile = !avatar || avatar.includes('default') || avatar.includes('132') || !nickname || nickname === '微信用户'
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '设置头像与昵称',
|
||||
content: '支付订单会关联你的对外展示信息,请先设置头像与昵称,避免账单与对方看到默认占位。',
|
||||
confirmText: '去设置',
|
||||
cancelText: '关闭',
|
||||
success: resolve
|
||||
})
|
||||
// 与 app._needsAvatarNickname 一致:须同时读 avatar 与 avatarUrl(业务侧存 avatar,微信侧可能只有 avatarUrl)
|
||||
if (userInfo?.id && typeof app._needsAvatarNickname === 'function' && app._needsAvatarNickname(userInfo)) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '设置头像与昵称',
|
||||
content:
|
||||
'头像与昵称会出现在名片与匹配卡片上,方便伙伴认出你。支付前请先完善,避免订单展示信息不完整。',
|
||||
confirmText: '去设置',
|
||||
cancelText: '关闭',
|
||||
success: resolve
|
||||
})
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
return
|
||||
}
|
||||
})
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1544,7 +1708,7 @@ Page({
|
||||
|
||||
if (type === 'fullbook' && checkRes.data.hasFullBook) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已购买全书', icon: 'none' })
|
||||
wx.showToast({ title: '已加入读书会', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1626,8 +1790,8 @@ Page({
|
||||
try {
|
||||
// 获取章节完整名称用于支付描述
|
||||
const sectionTitle = this.data.section?.title || sectionId
|
||||
const description = type === 'fullbook'
|
||||
? '《一场Soul的创业实验》全书'
|
||||
const description = type === 'fullbook'
|
||||
? '加入读书会'
|
||||
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
|
||||
|
||||
// 邀请码:与 VIP/钱包一致,优先 storage 落地 ref,否则回落本人推荐码便于自购归因
|
||||
@@ -1744,6 +1908,10 @@ Page({
|
||||
|
||||
// 2. 刷新用户购买状态
|
||||
await accessManager.refreshUserPurchaseStatus()
|
||||
const pcAfter = (app.globalData.purchasedSections || []).length
|
||||
this.setData({ purchasedCount: pcAfter })
|
||||
this.computeShowFullBookCta()
|
||||
this.refreshPay365MarketingFromRules()
|
||||
|
||||
// 3. 重新拉取章节并判断权限(应为 unlocked_purchased)
|
||||
const chapterRes = await app.request({
|
||||
@@ -1820,6 +1988,8 @@ Page({
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
this.setData({ purchasedCount: (res.data.purchasedSections || []).length })
|
||||
this.computeShowFullBookCta()
|
||||
this.refreshPay365MarketingFromRules()
|
||||
console.log('[Pay] ✅ 购买状态已刷新:', {
|
||||
hasFullBook: res.data.hasFullBook,
|
||||
purchasedCount: res.data.purchasedSections.length
|
||||
@@ -2134,10 +2304,10 @@ Page({
|
||||
const config = await accessManager.fetchLatestConfig()
|
||||
const purchasedSections2 = app.globalData.purchasedSections || []
|
||||
const rawTh2 = config.fullbookShowThreshold ?? config.prices?.fullbookShowThreshold
|
||||
const th2 = (rawTh2 != null && Number(rawTh2) > 0) ? Number(rawTh2) : 2
|
||||
const th2 = (rawTh2 != null && Number(rawTh2) > 0) ? Number(rawTh2) : 3
|
||||
this.setData({
|
||||
sectionPrice: config.prices?.section ?? 1,
|
||||
fullBookPrice: config.prices?.fullbook ?? 9.9,
|
||||
fullBookPrice: config.prices?.fullbook ?? 365,
|
||||
purchasedCount: purchasedSections2.length,
|
||||
fullbookShowThreshold: th2,
|
||||
})
|
||||
@@ -2167,6 +2337,8 @@ Page({
|
||||
}
|
||||
|
||||
this._applyPrevNext(chapterRes)
|
||||
this.computeShowFullBookCta()
|
||||
this.refreshPay365MarketingFromRules()
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '加载成功', icon: 'success' })
|
||||
|
||||
@@ -132,7 +132,7 @@
|
||||
</button>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
<text class="share-tip-text">{{readUi.shareTipLine || ('好友经你分享购买,你可获得约 ' + (shareRate || 90) + '% 收益')}}</text>
|
||||
<text class="share-tip-text">{{readUi.shareTipLine}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -159,38 +159,44 @@
|
||||
<view class="fade-mask"></view>
|
||||
</view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<!-- 付费墙 - 未登录(营销主标题与「购买本章」同框体系) -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview paywall--compact-sp' : ''}}">
|
||||
<view class="paywall-icon paywall-icon--compact" wx:if="{{readSinglePageMode}}"><icon name="lock" size="64" color="#00CED1"></icon></view>
|
||||
<view class="paywall-icon" wx:elif="{{!readSinglePageMode}}"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<view class="paywall-title-row">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}%</text>
|
||||
<text class="paywall-title paywall-title--inline">收益</text>
|
||||
<view class="paywall-marketing-box">
|
||||
<view class="paywall-title-row paywall-title-row--boxed">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}</text>
|
||||
<text class="paywall-title paywall-title--inline">% 收益</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section purchase-btn--fulltext" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label btn-label--block">支付 ¥{{section && section.price != null ? section.price : sectionPrice}} 解锁全文</text>
|
||||
<view class="purchase-options paywall-sp-compact-btns" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section purchase-btn--fulltext purchase-btn--compact-sp" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label btn-label--block">{{readUi.singlePagePayButtonText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '加入365读书会'}}</text>
|
||||
<text class="paywall-desc paywall-desc--pre" wx:if="{{readBeforeLoginHint}}">{{readBeforeLoginHint}}</text>
|
||||
<text class="paywall-desc">{{readUi.notLoginUnlockDesc || '已预览部分内容,登录并支付后阅读全文'}}</text>
|
||||
<view class="paywall-marketing-box" wx:if="{{!auditMode}}">
|
||||
<view class="paywall-title-row paywall-title-row--boxed">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}</text>
|
||||
<text class="paywall-title paywall-title--inline">% 收益</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<view class="purchase-btn purchase-section purchase-section--primary" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<text class="btn-price">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="login-btn" bindtap="showLoginModal" style="margin-top:12px">
|
||||
<text class="login-btn-text">手机号登录后购买</text>
|
||||
</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.notLoginPaywallTip || ('分享给好友,好友购买你可获得约 ' + (shareRate || 90) + '% 收益')}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -245,47 +251,51 @@
|
||||
<view class="fade-mask"></view>
|
||||
</view>
|
||||
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<!-- 付费墙 - 已登录未购买:营销框 + 双按钮(浏览满 N 篇凸显读书会) -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview paywall--compact-sp' : ''}} {{(!readSinglePageMode && ((paywall365Primary && showFullBookCta) || pay365AnchorVisible)) ? 'paywall--pay365-focus' : ''}}">
|
||||
<view class="paywall-icon paywall-icon--compact" wx:if="{{readSinglePageMode}}"><icon name="lock" size="64" color="#00CED1"></icon></view>
|
||||
<view class="paywall-icon" wx:elif="{{!readSinglePageMode && !pay365AnchorVisible}}"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<view class="paywall-title-row">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}%</text>
|
||||
<text class="paywall-title paywall-title--inline">收益</text>
|
||||
<view class="paywall-marketing-box">
|
||||
<view class="paywall-title-row paywall-title-row--boxed">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}</text>
|
||||
<text class="paywall-title paywall-title--inline">% 收益</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section purchase-btn--fulltext" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label btn-label--block">支付 ¥{{section && section.price != null ? section.price : sectionPrice}} 解锁全文</text>
|
||||
<view class="purchase-options paywall-sp-compact-btns" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section purchase-btn--fulltext purchase-btn--compact-sp" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label btn-label--block">{{readUi.singlePagePayButtonText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<text class="paywall-title">{{readUi.fullUnlockTitle || '加入365读书会'}}</text>
|
||||
<text class="paywall-desc">{{readUi.fullUnlockDesc || '可先上滑阅读预览'}}</text>
|
||||
<text class="paywall-subdesc" wx:if="{{readUi.fullLockedProgressText}}">{{readUi.fullLockedProgressText}}</text>
|
||||
<text class="paywall-subdesc" wx:else>已阅读约 {{effectivePreviewPercent}}% ,购买后继续阅读</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
<view class="paywall-marketing-box" wx:if="{{!auditMode}}">
|
||||
<view class="paywall-title-row paywall-title-row--boxed">
|
||||
<text class="paywall-title paywall-title--inline">解锁完整内容,分享得到</text>
|
||||
<text class="paywall-highlight-pct">{{shareRate || 90}}</text>
|
||||
<text class="paywall-title paywall-title--inline">% 收益</text>
|
||||
</view>
|
||||
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= fullbookShowThreshold}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="pay365-anchor-row" wx:if="{{pay365AnchorVisible && !readSinglePageMode}}" id="pay365-marketing-anchor" bindtap="onPay365AnchorTap">
|
||||
<text class="pay365-anchor-row-badge">365</text>
|
||||
<text class="pay365-anchor-row-text">{{pay365AnchorLabel}}</text>
|
||||
<text class="pay365-anchor-row-chevron">›</text>
|
||||
</view>
|
||||
<view class="purchase-options {{paywall365Primary && showFullBookCta ? 'purchase-options--365-first' : ''}}" wx:if="{{!auditMode}}">
|
||||
<view class="{{sectionPurchaseBtnClass}}" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
<view class="{{fullbookPurchaseBtnClass}}" bindtap="handlePurchaseFullBook" wx:if="{{showFullBookCta}}">
|
||||
<text class="btn-label">加入读书会</text>
|
||||
<text class="btn-price">¥{{fullBookPrice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">{{readUi.fullPaywallTip || ('分享给好友,好友购买你可获得约 ' + (shareRate || 90) + '% 收益')}}</text>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -486,4 +496,14 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁阅读时:365 营销锚点(规则 revealMode=anchor),点击后再弹窗确认支付 -->
|
||||
<view
|
||||
class="pay365-anchor-float"
|
||||
wx:if="{{pay365AnchorVisible && !readSinglePageMode && (accessState === 'free' || accessState === 'unlocked_purchased')}}"
|
||||
bindtap="onPay365AnchorTap"
|
||||
>
|
||||
<text class="pay365-anchor-float-text">{{pay365AnchorLabel}}</text>
|
||||
<text class="pay365-anchor-float-chevron">›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -202,16 +202,73 @@
|
||||
|
||||
/* ===== 文章内容 ===== */
|
||||
.article {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 34rpx;
|
||||
line-height: 1.9;
|
||||
color: rgba(255, 255, 255, 0.82);
|
||||
font-size: 32rpx;
|
||||
line-height: 1.85;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin-bottom: 48rpx;
|
||||
margin-bottom: 36rpx;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* --- heading 层级 --- */
|
||||
.seg-heading {
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.seg-h2 {
|
||||
font-size: 40rpx;
|
||||
margin: 56rpx 0 28rpx;
|
||||
padding-bottom: 16rpx;
|
||||
border-bottom: 1rpx solid rgba(0, 206, 209, 0.18);
|
||||
}
|
||||
.seg-h3 {
|
||||
font-size: 36rpx;
|
||||
margin: 44rpx 0 20rpx;
|
||||
color: #00CED1;
|
||||
}
|
||||
.seg-h4, .seg-h5, .seg-h6 {
|
||||
font-size: 34rpx;
|
||||
margin: 36rpx 0 16rpx;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
}
|
||||
|
||||
/* --- quote 引用 --- */
|
||||
.seg-quote {
|
||||
margin: 28rpx 0;
|
||||
padding: 20rpx 28rpx;
|
||||
border-left: 6rpx solid rgba(0, 206, 209, 0.5);
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border-radius: 0 12rpx 12rpx 0;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
/* --- listItem 列表 --- */
|
||||
.seg-list-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
.seg-list-marker {
|
||||
flex-shrink: 0;
|
||||
width: 44rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(0, 206, 209, 0.7);
|
||||
line-height: 1.85;
|
||||
text-align: center;
|
||||
}
|
||||
.seg-list-text {
|
||||
flex: 1;
|
||||
font-size: 30rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.75;
|
||||
}
|
||||
|
||||
/* 正文内 @某人 高亮可点 */
|
||||
.paragraph .mention {
|
||||
color: #00CED1;
|
||||
@@ -228,11 +285,62 @@
|
||||
/* 正文内图片 */
|
||||
.content-image {
|
||||
width: 100%;
|
||||
border-radius: 8rpx;
|
||||
margin: 16rpx 0;
|
||||
border-radius: 12rpx;
|
||||
margin: 24rpx 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 正文内表格 */
|
||||
.table-scroll {
|
||||
margin: 24rpx 0 36rpx;
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-table {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
min-width: 100%;
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.25);
|
||||
border-radius: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.table-row {
|
||||
display: flex;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.table-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.table-header-row {
|
||||
background: rgba(0, 206, 209, 0.12);
|
||||
border-bottom: 1rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.table-cell {
|
||||
flex: 1;
|
||||
min-width: 160rpx;
|
||||
padding: 20rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
line-height: 1.5;
|
||||
border-right: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.table-cell:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.table-header-cell {
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
@@ -289,11 +397,85 @@
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.paywall-title-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
/* 与「购买本章」同款底框:营销主标题单独成框 */
|
||||
.paywall-marketing-box {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #2c2c2e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.paywall-title-row--boxed {
|
||||
margin-bottom: 0;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
.paywall-icon--compact {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
margin: 0 auto 24rpx;
|
||||
}
|
||||
.paywall-icon--compact + .paywall-marketing-box {
|
||||
margin-top: 0;
|
||||
}
|
||||
.paywall-title--inline {
|
||||
display: inline;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.paywall-highlight-pct {
|
||||
display: inline;
|
||||
font-size: 46rpx;
|
||||
font-weight: 800;
|
||||
color: #FFD700;
|
||||
margin: 0 6rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:与完整小程序同款购买行,留白略紧 */
|
||||
.paywall--single-preview {
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
/* 单页简洁版:去大锁图标、标题+分润+单按钮纵向 */
|
||||
.paywall--compact-sp {
|
||||
padding: 32rpx 28rpx 36rpx;
|
||||
}
|
||||
.paywall-sp-compact-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.paywall-sp-compact-share {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.52);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 28rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
.paywall-sp-compact-btns {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.purchase-btn--compact-sp {
|
||||
padding: 26rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.paywall--single-preview .paywall-icon {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
@@ -395,21 +577,6 @@
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.preview-percent-row {
|
||||
margin: 16rpx 0 8rpx;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.preview-percent-badge {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.35);
|
||||
border-radius: 999rpx;
|
||||
padding: 8rpx 24rpx;
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
}
|
||||
|
||||
.purchase-btn--fulltext {
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -420,6 +587,20 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.purchase-btn--animate {
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
.bounce-once {
|
||||
animation: payBtnBounce 0.5s ease;
|
||||
}
|
||||
@keyframes payBtnBounce {
|
||||
0% { transform: scale(1); }
|
||||
20% { transform: scale(0.92); }
|
||||
50% { transform: scale(1.08); }
|
||||
80% { transform: scale(0.97); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
/* ===== 购买选项 ===== */
|
||||
.purchase-options {
|
||||
display: flex;
|
||||
@@ -440,6 +621,61 @@
|
||||
background: #2c2c2e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
/* 365 锚点为主时:单章降为次要-outline,避免与 1 元主按钮视觉同级 */
|
||||
.purchase-section--secondary {
|
||||
background: transparent;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.22);
|
||||
padding: 22rpx 28rpx;
|
||||
}
|
||||
.purchase-section--secondary .btn-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
.purchase-section--secondary .btn-price {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 「购买本章」主按钮:与读书会同款渐变(默认双按钮时优先单章) */
|
||||
.purchase-section--primary {
|
||||
background: linear-gradient(135deg, #00ced1 0%, #20b2aa 100%);
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.3);
|
||||
}
|
||||
.purchase-section--primary .btn-label,
|
||||
.purchase-section--primary .btn-price {
|
||||
color: #ffffff;
|
||||
}
|
||||
.purchase-section--primary .btn-price {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 双按钮初期:读书会弱化(描边小一号) */
|
||||
.purchase-fullbook--secondary {
|
||||
background: transparent;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow: none;
|
||||
padding: 22rpx 28rpx;
|
||||
}
|
||||
.purchase-fullbook--secondary .btn-label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.88);
|
||||
font-weight: 500;
|
||||
}
|
||||
.purchase-fullbook--secondary .btn-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #00ced1;
|
||||
}
|
||||
|
||||
/* 凸显读书会时:大按钮在上 */
|
||||
.purchase-options--365-first .purchase-fullbook {
|
||||
order: -1;
|
||||
}
|
||||
.purchase-options--365-first .purchase-section {
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.purchase-fullbook {
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
@@ -532,6 +768,12 @@
|
||||
padding: 24rpx 0;
|
||||
}
|
||||
|
||||
/* 365 营销为主:收紧卡片、去掉顶锁视觉干扰 */
|
||||
.paywall--pay365-focus {
|
||||
padding-top: 36rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.28);
|
||||
}
|
||||
|
||||
/* ===== 章节导航 ===== */
|
||||
.chapter-nav {
|
||||
margin-top: 96rpx;
|
||||
@@ -1448,6 +1690,76 @@ button.action-share-native {
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
/* ===== 分享到朋友圈 弹窗 ===== */
|
||||
.moments-modal {
|
||||
padding: 40rpx;
|
||||
}
|
||||
.moments-earn-banner {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: center;
|
||||
gap: 6rpx;
|
||||
margin-bottom: 36rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, rgba(0, 206, 209, 0.12) 0%, rgba(255, 215, 0, 0.08) 100%);
|
||||
border-radius: 20rpx;
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
.moments-earn-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.75);
|
||||
}
|
||||
.moments-earn-pct {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #FFD700;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.moments-steps {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 36rpx;
|
||||
}
|
||||
.moments-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.moments-step-num {
|
||||
flex-shrink: 0;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.moments-step-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
line-height: 1.45;
|
||||
}
|
||||
.moments-copy-btn {
|
||||
width: 100%;
|
||||
padding: 28rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #000;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
}
|
||||
.moments-copy-btn--done {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.45);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:垂直向下箭头,贴近底栏「前往小程序」按钮区域 */
|
||||
.singlepage-launch-pointer {
|
||||
position: fixed;
|
||||
@@ -1479,3 +1791,138 @@ button.action-share-native {
|
||||
transform: translateY(10rpx) scale(1.04);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== 朋友圈单页:点「支付解锁全文」→ 遮罩 + 底栏提示 + 向下箭头(指向系统「前往小程序」) ===== */
|
||||
.moments-launch-guide-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.52);
|
||||
backdrop-filter: blur(6rpx);
|
||||
z-index: 10070;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: stretch;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.moments-launch-guide-sheet {
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
margin: 0 auto;
|
||||
padding: 36rpx 40rpx 28rpx;
|
||||
padding-bottom: calc(28rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(180deg, #252528 0%, #1c1c1e 100%);
|
||||
border-radius: 32rpx 32rpx 0 0;
|
||||
border-top: 2rpx solid rgba(0, 206, 209, 0.22);
|
||||
box-shadow: 0 -12rpx 48rpx rgba(0, 0, 0, 0.45);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.moments-launch-guide-text {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
line-height: 1.55;
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
text-align: center;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.moments-launch-guide-arrow-wrap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: flex-start;
|
||||
margin-top: 12rpx;
|
||||
padding-bottom: 8rpx;
|
||||
min-height: 120rpx;
|
||||
}
|
||||
|
||||
.moments-launch-guide-arrow {
|
||||
font-size: 96rpx;
|
||||
line-height: 1;
|
||||
color: #00ced1;
|
||||
text-shadow: 0 0 24rpx rgba(0, 206, 209, 0.45);
|
||||
animation: moments-launch-guide-arrow-bounce 0.85s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes moments-launch-guide-arrow-bounce {
|
||||
0%,
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.88;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(28rpx);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 365 营销锚点:付费墙内一行,点击后再弹窗 */
|
||||
.pay365-anchor-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: 16rpx;
|
||||
margin: 0 0 24rpx;
|
||||
padding: 24rpx 28rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, rgba(0, 120, 115, 0.45) 0%, rgba(0, 206, 209, 0.22) 100%);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.55);
|
||||
box-sizing: border-box;
|
||||
box-shadow: 0 8rpx 28rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.pay365-anchor-row-badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #062a2b;
|
||||
background: linear-gradient(180deg, #9fffea 0%, #38bdac 100%);
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 10rpx;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.pay365-anchor-row-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #e8fffc;
|
||||
line-height: 1.45;
|
||||
font-weight: 500;
|
||||
}
|
||||
.pay365-anchor-row-chevron {
|
||||
font-size: 40rpx;
|
||||
color: #9fffea;
|
||||
font-weight: 300;
|
||||
margin-left: auto;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* 已解锁全文时:底部固定锚点条 */
|
||||
.pay365-anchor-float {
|
||||
position: fixed;
|
||||
left: 24rpx;
|
||||
right: 24rpx;
|
||||
bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
z-index: 96;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 22rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(90deg, rgba(15, 33, 55, 0.96), rgba(10, 22, 40, 0.96));
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.45);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.35);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.pay365-anchor-float-text {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #e8fbfa;
|
||||
}
|
||||
.pay365-anchor-float-chevron {
|
||||
font-size: 36rpx;
|
||||
color: #38bdac;
|
||||
margin-left: 12rpx;
|
||||
}
|
||||
|
||||
@@ -945,7 +945,7 @@ Page({
|
||||
if (match) {
|
||||
return {
|
||||
bookTitle: match[1] || '未知书籍',
|
||||
chapterTitle: match[2] || (productType === 'fullbook' ? '全书购买' : '')
|
||||
chapterTitle: match[2] || (productType === 'fullbook' ? '加入读书会' : '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,5 +2,7 @@
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true
|
||||
"enableShareTimeline": true,
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "dark"
|
||||
}
|
||||
|
||||
179
miniprogram/pages/super-crm/super-crm.js
Normal file
179
miniprogram/pages/super-crm/super-crm.js
Normal file
@@ -0,0 +1,179 @@
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
function formatDateTime(iso) {
|
||||
if (!iso) return '--'
|
||||
const d = new Date(iso)
|
||||
if (isNaN(d.getTime())) return '--'
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
const h = d.getHours().toString().padStart(2, '0')
|
||||
const min = d.getMinutes().toString().padStart(2, '0')
|
||||
return `${m}-${day} ${h}:${min}`
|
||||
}
|
||||
|
||||
function shortUserId(id) {
|
||||
const s = String(id || '').trim()
|
||||
if (!s) return ''
|
||||
return s.length > 14 ? `${s.slice(0, 8)}…` : s
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
tabIndex: 0,
|
||||
personName: '',
|
||||
leads: [],
|
||||
leadPage: 1,
|
||||
leadHasMore: true,
|
||||
leadLoading: false,
|
||||
tracks: [],
|
||||
trackLoading: false,
|
||||
trackHasMore: true,
|
||||
trackBefore: '',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this._ensureSuperThenLoad()
|
||||
},
|
||||
|
||||
async _ensureSuperThenLoad() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!app.globalData.isLoggedIn || !userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/my/super-stats?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true,
|
||||
})
|
||||
const ok = res?.success && res.data?.isSuperIndividual === true
|
||||
if (!ok) {
|
||||
wx.showToast({ title: '仅超级个体可查看', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
} catch (_) {
|
||||
wx.showToast({ title: '无法验证身份', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
this.loadLeads(true)
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/my/my' }) })
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const idx = Number(e.currentTarget.dataset.index) || 0
|
||||
if (idx === this.data.tabIndex) return
|
||||
trackClick('super_crm', 'tab_click', idx === 0 ? '客资' : '轨迹')
|
||||
this.setData({ tabIndex: idx })
|
||||
if (idx === 0 && this.data.leads.length === 0) {
|
||||
this.loadLeads(true)
|
||||
}
|
||||
if (idx === 1 && this.data.tracks.length === 0) {
|
||||
this.loadTracks(true)
|
||||
}
|
||||
},
|
||||
|
||||
async loadLeads(reset) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
const page = reset ? 1 : this.data.leadPage
|
||||
if (!reset && !this.data.leadHasMore) return
|
||||
this.setData({ leadLoading: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/my/super-leads?userId=${encodeURIComponent(userId)}&page=${page}&pageSize=20`,
|
||||
silent: true,
|
||||
})
|
||||
if (res?.forbidden || (res && res.success === false && String(res.error || '').indexOf('超级个体') >= 0)) {
|
||||
wx.showToast({ title: '仅超级个体可查看', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
if (!res?.success) {
|
||||
this.setData({ leadLoading: false })
|
||||
return
|
||||
}
|
||||
const raw = Array.isArray(res.records) ? res.records : []
|
||||
const mapped = raw.map((r) => ({
|
||||
...r,
|
||||
createdAtText: formatDateTime(r.createdAt),
|
||||
}))
|
||||
const total = Number(res.total || 0)
|
||||
const nextList = reset ? mapped : (this.data.leads || []).concat(mapped)
|
||||
const hasMore = nextList.length < total
|
||||
this.setData({
|
||||
personName: res.personName || this.data.personName,
|
||||
leads: nextList,
|
||||
leadPage: page + 1,
|
||||
leadHasMore: hasMore,
|
||||
leadLoading: false,
|
||||
})
|
||||
} catch (e) {
|
||||
this.setData({ leadLoading: false })
|
||||
wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async loadTracks(reset) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
if (!reset && !this.data.trackHasMore) return
|
||||
const before = reset ? '' : this.data.trackBefore
|
||||
if (!reset && !before) {
|
||||
this.setData({ trackHasMore: false })
|
||||
return
|
||||
}
|
||||
this.setData({ trackLoading: true })
|
||||
try {
|
||||
let url = `/api/miniprogram/my/super-lead-tracks?userId=${encodeURIComponent(userId)}&limit=40`
|
||||
if (before) url += `&before=${encodeURIComponent(before)}`
|
||||
const res = await app.request({ url, silent: true })
|
||||
if (res?.forbidden || (res && res.success === false && String(res.error || '').indexOf('超级个体') >= 0)) {
|
||||
wx.showToast({ title: '仅超级个体可查看', icon: 'none' })
|
||||
setTimeout(() => wx.navigateBack(), 400)
|
||||
return
|
||||
}
|
||||
if (!res?.success) {
|
||||
this.setData({ trackLoading: false })
|
||||
return
|
||||
}
|
||||
const raw = Array.isArray(res.tracks) ? res.tracks : []
|
||||
const mapped = raw.map((t) => ({
|
||||
...t,
|
||||
userShort: shortUserId(t.userId),
|
||||
createdAtText: formatDateTime(t.createdAt),
|
||||
}))
|
||||
const nextList = reset ? mapped : (this.data.tracks || []).concat(mapped)
|
||||
const nextBefore = res.nextBefore || ''
|
||||
const hasMore = !!(nextBefore && mapped.length > 0)
|
||||
this.setData({
|
||||
personName: res.personName || this.data.personName,
|
||||
tracks: nextList,
|
||||
trackBefore: nextBefore,
|
||||
trackHasMore: hasMore,
|
||||
trackLoading: false,
|
||||
})
|
||||
} catch (e) {
|
||||
this.setData({ trackLoading: false })
|
||||
wx.showToast({ title: (e && e.message) || '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
onLeadReachBottom() {
|
||||
if (this.data.tabIndex !== 0) return
|
||||
this.loadLeads(false)
|
||||
},
|
||||
|
||||
onTrackReachBottom() {
|
||||
if (this.data.tabIndex !== 1) return
|
||||
this.loadTracks(false)
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/super-crm/super-crm.json
Normal file
4
miniprogram/pages/super-crm/super-crm.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "链接客资",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
71
miniprogram/pages/super-crm/super-crm.wxml
Normal file
71
miniprogram/pages/super-crm/super-crm.wxml
Normal file
@@ -0,0 +1,71 @@
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="rgba(255,255,255,0.8)" customClass="back-icon"></icon>
|
||||
</view>
|
||||
<text class="nav-title">链接客资</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="nav-placeholder-block" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="tip-card" wx:if="{{personName}}">
|
||||
<text class="tip-text">绑定人物:{{personName}}</text>
|
||||
<text class="tip-sub">客资按用户去重;行为轨迹与推送 Webhook 同源,近 1 小时同行为已合并展示</text>
|
||||
</view>
|
||||
|
||||
<view class="tabs">
|
||||
<view class="tab {{tabIndex === 0 ? 'tab-active' : ''}}" bindtap="switchTab" data-index="0">客资</view>
|
||||
<view class="tab {{tabIndex === 1 ? 'tab-active' : ''}}" bindtap="switchTab" data-index="1">行为轨迹</view>
|
||||
</view>
|
||||
|
||||
<!-- 客资 -->
|
||||
<scroll-view
|
||||
wx:if="{{tabIndex === 0}}"
|
||||
class="list-scroll"
|
||||
scroll-y
|
||||
bindscrolltolower="onLeadReachBottom"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
>
|
||||
<view class="empty" wx:if="{{!leadLoading && leads.length === 0}}">
|
||||
<text>暂无留资记录</text>
|
||||
</view>
|
||||
<view class="card" wx:for="{{leads}}" wx:key="id">
|
||||
<view class="card-head">
|
||||
<text class="name">{{item.name || item.nickname || '访客'}}</text>
|
||||
<text class="time">{{item.createdAtText}}</text>
|
||||
</view>
|
||||
<text class="source">来源:{{item.sourceLabel || item.source || '-'}}</text>
|
||||
<text class="row" wx:if="{{item.phone}}">手机:{{item.phone}}</text>
|
||||
<text class="row" wx:if="{{item.wechatId}}">微信:{{item.wechatId}}</text>
|
||||
<text class="row muted" wx:if="{{item.pushStatus}}">推送:{{item.pushStatus}}</text>
|
||||
</view>
|
||||
<view class="load-more" wx:if="{{leadLoading}}">加载中...</view>
|
||||
<view class="load-more" wx:elif="{{leadHasMore && leads.length > 0}}">上拉加载更多</view>
|
||||
<view class="load-more" wx:elif="{{!leadHasMore && leads.length > 0}}">没有更多了</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 轨迹 -->
|
||||
<scroll-view
|
||||
wx:if="{{tabIndex === 1}}"
|
||||
class="list-scroll"
|
||||
scroll-y
|
||||
bindscrolltolower="onTrackReachBottom"
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
>
|
||||
<view class="empty" wx:if="{{!trackLoading && tracks.length === 0}}">
|
||||
<text>暂无行为轨迹</text>
|
||||
</view>
|
||||
<view class="card track-card" wx:for="{{tracks}}" wx:key="id">
|
||||
<view class="card-head">
|
||||
<text class="name">{{item.userNickname || item.userShort}}</text>
|
||||
<text class="time">{{item.timeAgo || item.createdAtText}}</text>
|
||||
</view>
|
||||
<text class="action-line">{{item.actionLabel}}{{item.chapterTitle ? ' · ' + item.chapterTitle : ''}}{{item.target ? ' · ' + item.target : ''}}</text>
|
||||
<text class="row muted" wx:if="{{item.moduleLabel}}">{{item.moduleLabel}}</text>
|
||||
</view>
|
||||
<view class="load-more" wx:if="{{trackLoading}}">加载中...</view>
|
||||
<view class="load-more" wx:elif="{{trackHasMore && tracks.length > 0}}">上拉加载更多</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
151
miniprogram/pages/super-crm/super-crm.wxss
Normal file
151
miniprogram/pages/super-crm/super-crm.wxss
Normal file
@@ -0,0 +1,151 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #0a0a0a;
|
||||
padding-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(10, 10, 10, 0.95);
|
||||
backdrop-filter: blur(40rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
.nav-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
.nav-placeholder-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.tip-card {
|
||||
margin: 24rpx 24rpx 16rpx;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 20rpx;
|
||||
border: 2rpx solid rgba(79, 209, 197, 0.2);
|
||||
}
|
||||
.tip-text {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #e5e7eb;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.tip-sub {
|
||||
font-size: 22rpx;
|
||||
line-height: 1.6;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
margin: 0 24rpx 16rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 16rpx;
|
||||
padding: 6rpx;
|
||||
}
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
.tab-active {
|
||||
background: #252525;
|
||||
color: #4fd1c5;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.list-scroll {
|
||||
height: calc(100vh - 320rpx);
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #252525;
|
||||
border-radius: 20rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.track-card .action-line {
|
||||
font-size: 26rpx;
|
||||
color: #e5e7eb;
|
||||
line-height: 1.5;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.card-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.name {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-right: 16rpx;
|
||||
}
|
||||
.time {
|
||||
font-size: 22rpx;
|
||||
color: #6b7280;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.source {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #4fd1c5;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.row {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #d1d5db;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.row.muted {
|
||||
color: #6b7280;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
.empty {
|
||||
padding: 120rpx 32rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.load-more {
|
||||
text-align: center;
|
||||
padding: 24rpx;
|
||||
font-size: 24rpx;
|
||||
color: #6b7280;
|
||||
}
|
||||
@@ -20,7 +20,7 @@ Page({
|
||||
],
|
||||
socialRights: [
|
||||
{ key: 'cases', title: '案例宝库', desc: '100+赚钱案例库', icon: 'book-open' },
|
||||
{ key: 'fullbook', title: '全书解锁', desc: '365天全案精读', icon: 'folder' },
|
||||
{ key: 'fullbook', title: '加入读书会', desc: '全年可读', icon: 'folder' },
|
||||
{ key: 'daily', title: '每日总结', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ key: 'leads', title: '获取客资', desc: '文章@你即可获客', icon: 'link' }
|
||||
],
|
||||
|
||||
@@ -25,7 +25,7 @@ class ChapterAccessManager {
|
||||
const res = await app.getConfig()
|
||||
if (res && res.success && res.prices) {
|
||||
return {
|
||||
prices: res.prices || { section: 1, fullbook: 9.9 },
|
||||
prices: res.prices || { section: 1, fullbook: 365 },
|
||||
shareRate: res.shareRate != null ? res.shareRate : 90
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,7 @@ class ChapterAccessManager {
|
||||
console.warn('[AccessManager] 获取配置失败,使用默认配置:', e)
|
||||
}
|
||||
return {
|
||||
prices: { section: 1, fullbook: 9.9 },
|
||||
prices: { section: 1, fullbook: 365 },
|
||||
shareRate: 90
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,52 +174,189 @@ function parseBlockToSegments(block, config) {
|
||||
return segs
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 <table> HTML 为结构化数据 { headers: string[], rows: string[][] }
|
||||
*/
|
||||
function parseTableHtml(tableHtml) {
|
||||
const stripTags = s => decodeEntities((s || '').replace(/<[^>]+>/g, '').trim())
|
||||
const headers = []
|
||||
const rows = []
|
||||
|
||||
const headMatch = tableHtml.match(/<thead[^>]*>([\s\S]*?)<\/thead>/i)
|
||||
if (headMatch) {
|
||||
const thRe = /<th[^>]*>([\s\S]*?)<\/th>/gi
|
||||
let m
|
||||
while ((m = thRe.exec(headMatch[1])) !== null) {
|
||||
headers.push(stripTags(m[1]))
|
||||
}
|
||||
}
|
||||
|
||||
const bodyMatch = tableHtml.match(/<tbody[^>]*>([\s\S]*?)<\/tbody>/i)
|
||||
const bodyContent = bodyMatch ? bodyMatch[1] : tableHtml
|
||||
const trRe = /<tr[^>]*>([\s\S]*?)<\/tr>/gi
|
||||
let trM
|
||||
let isFirst = !headMatch
|
||||
while ((trM = trRe.exec(bodyContent)) !== null) {
|
||||
if (headMatch && trM[0] === (headMatch[0].match(/<tr[^>]*>[\s\S]*?<\/tr>/i) || [])[0]) continue
|
||||
const cells = []
|
||||
const cellRe = /<t[dh][^>]*>([\s\S]*?)<\/t[dh]>/gi
|
||||
let cM
|
||||
while ((cM = cellRe.exec(trM[1])) !== null) {
|
||||
cells.push(stripTags(cM[1]))
|
||||
}
|
||||
if (!cells.length) continue
|
||||
if (isFirst && !headers.length) {
|
||||
headers.push(...cells)
|
||||
isFirst = false
|
||||
} else {
|
||||
rows.push(cells)
|
||||
}
|
||||
}
|
||||
return { headers, rows }
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 HTML 中解析出 lines(纯文本行)和 segments(含富文本片段)
|
||||
* segment 新增类型:heading / quote / listItem
|
||||
* @param {string} html
|
||||
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string },用于对 text 段自动匹配 @人名 / #标签;assetBase 用于补全图片相对 URL
|
||||
* @param {object} [config] - { persons: [], linkTags: [], assetBase?: string }
|
||||
*/
|
||||
function parseHtmlToSegments(html, config) {
|
||||
const lines = []
|
||||
const segments = []
|
||||
|
||||
// 1. 块级标签换行,保留内联标签供后续解析
|
||||
let text = html
|
||||
// 0. 提取 <table> 块
|
||||
const tables = []
|
||||
let text = html.replace(/<table[^>]*>[\s\S]*?<\/table>/gi, (match) => {
|
||||
const idx = tables.length
|
||||
tables.push(parseTableHtml(match))
|
||||
return '\n__TABLE_' + idx + '__\n'
|
||||
})
|
||||
|
||||
// 1. 提取 <h2>/<h3> → heading 占位
|
||||
const headings = []
|
||||
text = text.replace(/<h([2-6])[^>]*>([\s\S]*?)<\/h\1>/gi, function (_, lvl, inner) {
|
||||
const idx = headings.length
|
||||
headings.push({ level: parseInt(lvl, 10), raw: inner.trim() })
|
||||
return '\n__H_' + idx + '__\n'
|
||||
})
|
||||
|
||||
// 2. 提取 <blockquote> → quote 占位
|
||||
const quotes = []
|
||||
text = text.replace(/<blockquote[^>]*>([\s\S]*?)<\/blockquote>/gi, function (_, inner) {
|
||||
const idx = quotes.length
|
||||
quotes.push(inner.trim())
|
||||
return '\n__Q_' + idx + '__\n'
|
||||
})
|
||||
|
||||
// 3. 列表 → listItem 占位(保留 ordered 标记)
|
||||
var olDepth = 0
|
||||
var olCounter = 0
|
||||
text = text.replace(/<ol[^>]*>/gi, function () { olDepth++; olCounter = 0; return '\n' })
|
||||
text = text.replace(/<\/ol>/gi, function () { olDepth = Math.max(0, olDepth - 1); return '\n' })
|
||||
text = text.replace(/<ul[^>]*>/gi, '\n')
|
||||
text = text.replace(/<\/ul>/gi, '\n')
|
||||
text = text.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, function (_, inner) {
|
||||
var cleaned = inner.replace(/<p[^>]*>/gi, '').replace(/<\/p>/gi, '').trim()
|
||||
if (!cleaned) return '\n'
|
||||
if (olDepth > 0) {
|
||||
olCounter++
|
||||
return '\n__LI_O_' + olCounter + '__ ' + cleaned + '\n'
|
||||
}
|
||||
return '\n__LI_U__ ' + cleaned + '\n'
|
||||
})
|
||||
|
||||
// 4. 块级标签换行
|
||||
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')
|
||||
text = text.replace(/<hr\s*\/?>/gi, '\n')
|
||||
|
||||
// 2. 逐段解析
|
||||
const blocks = text.split(/\n+/)
|
||||
for (const block of blocks) {
|
||||
// 5. 逐段解析
|
||||
var blocks = text.split(/\n+/)
|
||||
for (var bi = 0; bi < blocks.length; bi++) {
|
||||
var block = blocks[bi]
|
||||
if (!block.trim()) continue
|
||||
|
||||
let blockSegs = parseBlockToSegments(block, config)
|
||||
// table
|
||||
var tblM = block.trim().match(/^__TABLE_(\d+)__$/)
|
||||
if (tblM) {
|
||||
var tbl = tables[parseInt(tblM[1], 10)]
|
||||
if (tbl) {
|
||||
lines.push([tbl.headers.join(' | '), ...tbl.rows.map(function (r) { return r.join(' | ') })].join('\n'))
|
||||
segments.push([{ type: 'table', headers: tbl.headers, rows: tbl.rows }])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// heading
|
||||
var hM = block.trim().match(/^__H_(\d+)__$/)
|
||||
if (hM) {
|
||||
var h = headings[parseInt(hM[1], 10)]
|
||||
if (h) {
|
||||
var hText = decodeEntities(h.raw.replace(/<[^>]+>/g, '').trim())
|
||||
lines.push(hText)
|
||||
segments.push([{ type: 'heading', level: h.level, text: hText }])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// quote
|
||||
var qM = block.trim().match(/^__Q_(\d+)__$/)
|
||||
if (qM) {
|
||||
var qHtml = quotes[parseInt(qM[1], 10)]
|
||||
if (qHtml) {
|
||||
var qText = decodeEntities(
|
||||
qHtml.replace(/<\/?p[^>]*>/gi, '\n').replace(/<[^>]+>/g, '').trim()
|
||||
).replace(/\n{2,}/g, '\n')
|
||||
lines.push(qText)
|
||||
segments.push([{ type: 'quote', text: qText }])
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// ordered list item
|
||||
var liOM = block.match(/^__LI_O_(\d+)__\s*(.*)/)
|
||||
if (liOM) {
|
||||
var liNum = parseInt(liOM[1], 10)
|
||||
var liContent = liOM[2].trim()
|
||||
var liSegs = parseBlockToSegments(liContent, config)
|
||||
var liText = decodeEntities(liContent.replace(/<[^>]+>/g, '').trim())
|
||||
lines.push(liNum + '. ' + liText)
|
||||
segments.push([{ type: 'listItem', ordered: true, number: liNum, text: liText, segs: liSegs }])
|
||||
continue
|
||||
}
|
||||
|
||||
// unordered list item
|
||||
var liUM = block.match(/^__LI_U__\s*(.*)/)
|
||||
if (liUM) {
|
||||
var liContent2 = liUM[1].trim()
|
||||
var liSegs2 = parseBlockToSegments(liContent2, config)
|
||||
var liText2 = decodeEntities(liContent2.replace(/<[^>]+>/g, '').trim())
|
||||
lines.push(liText2)
|
||||
segments.push([{ type: 'listItem', ordered: false, text: liText2, segs: liSegs2 }])
|
||||
continue
|
||||
}
|
||||
|
||||
// 普通段落
|
||||
var blockSegs = parseBlockToSegments(block, config)
|
||||
if (!blockSegs.length) continue
|
||||
|
||||
// 纯图片行独立成段
|
||||
if (blockSegs.length === 1 && blockSegs[0].type === 'image') {
|
||||
lines.push('')
|
||||
segments.push(blockSegs)
|
||||
continue
|
||||
}
|
||||
|
||||
// 对 text 段再跑一遍 @人名 / #标签 自动匹配(处理未用 TipTap 插入而是手打的 @xxx)
|
||||
if (config && (config.persons?.length || config.linkTags?.length)) {
|
||||
const expanded = []
|
||||
for (const seg of blockSegs) {
|
||||
var expanded = []
|
||||
for (var si = 0; si < blockSegs.length; si++) {
|
||||
var seg = blockSegs[si]
|
||||
if (seg.type === 'text' && seg.text) {
|
||||
const sub = matchLineToSegments(seg.text, config)
|
||||
expanded.push(...sub)
|
||||
expanded.push.apply(expanded, matchLineToSegments(seg.text, config))
|
||||
} else {
|
||||
expanded.push(seg)
|
||||
}
|
||||
@@ -227,8 +364,7 @@ function parseHtmlToSegments(html, config) {
|
||||
blockSegs = expanded
|
||||
}
|
||||
|
||||
// 行纯文本用于 lines(previewParagraphs 降级展示)
|
||||
const lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
var lineText = decodeEntities(block.replace(/<[^>]+>/g, '')).trim()
|
||||
lines.push(lineText)
|
||||
segments.push(blockSegs)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,8 @@ const READ_PATH = '/pages/read/read'
|
||||
|
||||
/** 阅读页 mpUi 兜底(pagePopupItems / readPage 均无或为空时) */
|
||||
const READ_PAGE_FALLBACK = {
|
||||
beforeLoginHint: '试读进度与下方百分比以后台配置为准;登录后可购买解锁全文。',
|
||||
/** 已不在付费墙展示长说明;保留短句供兼容旧配置拉取 */
|
||||
beforeLoginHint: '',
|
||||
singlePageTitle: '解锁完整内容',
|
||||
singlePagePaywallHint:
|
||||
'当前为朋友圈单页预览,无法在此登录或付款。请点击底部「前往小程序」进入完整版后再解锁本章。',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* 与管理端 content/ChapterTree.tsx 的 PART_ICONS、正文篇序规则一致
|
||||
* 后台篇头用 emoji 轮询;小程序目录页与之对齐(无自定义图时)
|
||||
*/
|
||||
const PART_ICONS = ['📖', '📕', '📗', '📘', '📙', '📓', '📔', '📒', '📚', '📖']
|
||||
const PART_ICONS = ['🧭', '🌍', '⚡', '💎', '🏆', '🚀', '🎯', '🔑', '🌟', '🎪']
|
||||
|
||||
/** 正文篇在列表中的从 0 开始的序号 → emoji(与 ChapterTree bodyPartOrdinal 一致) */
|
||||
function partEmojiForBodyIndex(bodyIndex) {
|
||||
|
||||
@@ -412,6 +412,90 @@ function checkRule_Withdraw(rules) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 浏览 N 章后弹出 365 读书会支付引导
|
||||
* 规则匹配:triggerConditions 包含 view_n_chapters_pay365 或 browse_n_chapters
|
||||
* 阈值来自 actionConfig.chapterThreshold(默认 3)
|
||||
* actionConfig.revealMode:modal 进入章节即延时弹窗;anchor 仅展示锚点条,用户点击后再弹窗/支付
|
||||
* actionConfig.anchorLabel:锚点条文案(可选)
|
||||
*/
|
||||
function findRuleByTriggerCondition(rules, conditionKey) {
|
||||
return rules.find(r => {
|
||||
if (r.completed) return false
|
||||
const tc = r.triggerConditions
|
||||
if (Array.isArray(tc)) return tc.includes(conditionKey)
|
||||
if (typeof tc === 'string') {
|
||||
try {
|
||||
const parsed = JSON.parse(tc)
|
||||
if (Array.isArray(parsed)) return parsed.includes(conditionKey)
|
||||
} catch (_) {}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function parseActionConfig(rule) {
|
||||
if (!rule) return {}
|
||||
const ac = rule.actionConfig
|
||||
if (ac && typeof ac === 'object' && !Array.isArray(ac)) return ac
|
||||
if (typeof ac === 'string' && ac.trim()) {
|
||||
try {
|
||||
const p = JSON.parse(ac)
|
||||
if (p && typeof p === 'object') return p
|
||||
} catch (_) {}
|
||||
}
|
||||
return {}
|
||||
}
|
||||
|
||||
function checkRule_ViewNChaptersPay365(rules) {
|
||||
const rule = findRuleByTriggerCondition(rules, 'view_n_chapters_pay365')
|
||||
|| findRuleByTriggerCondition(rules, 'browse_n_chapters')
|
||||
if (!rule) return null
|
||||
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
|
||||
const app = getAppInstance()
|
||||
if (!app) return null
|
||||
if (app.globalData.hasFullBook || app.globalData.isVip) return null
|
||||
|
||||
const ac = parseActionConfig(rule)
|
||||
const threshold = (Number(ac.chapterThreshold) > 0) ? Number(ac.chapterThreshold) : 3
|
||||
const fullRead =
|
||||
typeof app.getReadCount === 'function' ? app.getReadCount() : app.globalData.readCount || 0
|
||||
const browseDistinct =
|
||||
typeof app.getBrowseDistinctChapterCount === 'function' ? app.getBrowseDistinctChapterCount() : 0
|
||||
const readCount = Math.max(Number(fullRead) || 0, Number(browseDistinct) || 0)
|
||||
if (readCount < threshold) return null
|
||||
|
||||
if (isInCooldown('view_n_pay365')) return null
|
||||
setCooldown('view_n_pay365')
|
||||
|
||||
const ctaKind = String(ac.ctaKind || 'pay365')
|
||||
const amount = ctaKind === 'pay365' ? 365 : null
|
||||
|
||||
const rm = ac.revealMode
|
||||
let revealMode = rm === 'modal' || rm === 'anchor' ? rm : null
|
||||
if (!revealMode) {
|
||||
revealMode = 'modal'
|
||||
}
|
||||
const anchorLabel = trimStr(ac.anchorLabel)
|
||||
|
||||
return {
|
||||
ruleId: 'view_n_pay365',
|
||||
serverRuleId: rule.id,
|
||||
title: rule.title || '加入 365 读书会',
|
||||
message: rule.description || `你已阅读 ${readCount} 个章节,加入 365 读书会可解锁全部内容,一年内不限次阅读。`,
|
||||
confirmText: amount ? `¥${amount} 立即加入` : '去了解',
|
||||
cancelText: '再看看',
|
||||
action: ctaKind === 'pay365' ? 'pay365' : 'navigate',
|
||||
target: null,
|
||||
amount,
|
||||
revealMode,
|
||||
anchorLabel,
|
||||
}
|
||||
}
|
||||
|
||||
function checkRulesSync(scene, rules) {
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
@@ -431,6 +515,8 @@ function checkRulesSync(scene, rules) {
|
||||
return checkAvatarNicknameGuides(rules) || checkRule_ShareAfter5Chapters(rules) || checkRule_BindWechat(rules) || checkRule_Withdraw(rules)
|
||||
case 'before_join_party':
|
||||
return checkRule_JoinParty(rules)
|
||||
case 'chapter_read':
|
||||
return checkRule_ViewNChaptersPay365(rules)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
@@ -452,6 +538,12 @@ function executeRule(rule, pageInstance) {
|
||||
if (typeof pageInstance.showPhoneBinding === 'function') {
|
||||
pageInstance.showPhoneBinding()
|
||||
}
|
||||
} else if (rule.action === 'pay365' && pageInstance) {
|
||||
if (typeof pageInstance.handlePurchaseFullBook === 'function') {
|
||||
pageInstance.handlePurchaseFullBook()
|
||||
} else if (typeof pageInstance.processPayment === 'function') {
|
||||
pageInstance.processPayment('fullbook', null, rule.amount || 365)
|
||||
}
|
||||
}
|
||||
if (rule.serverRuleId) {
|
||||
markRuleCompleted(rule.serverRuleId)
|
||||
@@ -485,8 +577,34 @@ async function checkAndExecute(scene, pageInstance) {
|
||||
rule = checkRulesSync(scene, rules)
|
||||
}
|
||||
if (rule) {
|
||||
if (
|
||||
scene === 'chapter_read' &&
|
||||
rule.ruleId === 'view_n_pay365' &&
|
||||
rule.revealMode === 'anchor' &&
|
||||
rule.action === 'pay365' &&
|
||||
pageInstance &&
|
||||
typeof pageInstance.onPay365MarketingAnchor === 'function'
|
||||
) {
|
||||
setTimeout(() => pageInstance.onPay365MarketingAnchor(rule), 800)
|
||||
return
|
||||
}
|
||||
setTimeout(() => executeRule(rule, pageInstance), 800)
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { checkRules: checkRulesSync, executeRule, checkAndExecute, loadRules, markRuleCompleted }
|
||||
/** 阅读页同步营销态:与 chapter_read 同一套条件,不自动弹窗 */
|
||||
async function getChapterReadMarketingRule() {
|
||||
const rules = await loadRules()
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
return checkRulesSync('chapter_read', rules)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkRules: checkRulesSync,
|
||||
executeRule,
|
||||
checkAndExecute,
|
||||
loadRules,
|
||||
markRuleCompleted,
|
||||
getChapterReadMarketingRule,
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ function getReferralCodeForPay(app) {
|
||||
return ''
|
||||
}
|
||||
|
||||
/** 章节 / 全书支付描述(与 read 页原逻辑一致) */
|
||||
/** 章节 / 365读书会(fullbook)支付描述(与 read 页一致) */
|
||||
function buildSectionPayDescription(productType, sectionId, sectionTitle) {
|
||||
if (productType === 'fullbook') return '《一场Soul的创业实验》全书'
|
||||
if (productType === 'fullbook') return '加入读书会'
|
||||
if (productType === 'section') {
|
||||
const t = sectionTitle || sectionId || ''
|
||||
const short = t.length > 20 ? t.slice(0, 20) + '...' : t
|
||||
|
||||
Reference in New Issue
Block a user