删除 Kbone 小程序开发技能相关文档,优化项目结构以提升可维护性。
This commit is contained in:
909
miniprogram/pages/read/read.js
Normal file
909
miniprogram/pages/read/read.js
Normal file
@@ -0,0 +1,909 @@
|
||||
/**
|
||||
* Soul创业派对 - 阅读页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 章节信息
|
||||
sectionId: '',
|
||||
section: null,
|
||||
partTitle: '',
|
||||
chapterTitle: '',
|
||||
|
||||
// 内容
|
||||
content: '',
|
||||
previewContent: '',
|
||||
contentParagraphs: [],
|
||||
previewParagraphs: [],
|
||||
loading: true,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
canAccess: false,
|
||||
purchasedCount: 0,
|
||||
|
||||
// 阅读进度
|
||||
readingProgress: 0,
|
||||
showPaywall: false,
|
||||
|
||||
// 上一篇/下一篇
|
||||
prevSection: null,
|
||||
nextSection: null,
|
||||
|
||||
// 价格
|
||||
sectionPrice: 1,
|
||||
fullBookPrice: 9.9,
|
||||
totalSections: 62,
|
||||
|
||||
// 弹窗
|
||||
showShareModal: false,
|
||||
showLoginModal: false,
|
||||
showPosterModal: false,
|
||||
isPaying: false,
|
||||
isGeneratingPoster: false,
|
||||
|
||||
// 免费章节
|
||||
freeIds: ['preface', 'epilogue', '1.1', 'appendix-1', 'appendix-2', 'appendix-3']
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const { id, ref } = options
|
||||
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight,
|
||||
sectionId: id
|
||||
})
|
||||
|
||||
// 处理推荐码绑定
|
||||
if (ref) {
|
||||
console.log('[Read] 检测到推荐码:', ref)
|
||||
wx.setStorageSync('referral_code', ref)
|
||||
app.handleReferralCode({ query: { ref } })
|
||||
}
|
||||
|
||||
// 加载免费章节配置
|
||||
this.loadFreeChaptersConfig()
|
||||
|
||||
this.initSection(id)
|
||||
},
|
||||
|
||||
// 从后端加载免费章节配置
|
||||
async loadFreeChaptersConfig() {
|
||||
try {
|
||||
const res = await app.request('/api/db/config')
|
||||
if (res.success && res.freeChapters) {
|
||||
this.setData({ freeIds: res.freeChapters })
|
||||
console.log('[Read] 加载免费章节配置:', res.freeChapters)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Read] 使用默认免费章节配置')
|
||||
}
|
||||
},
|
||||
|
||||
onPageScroll(e) {
|
||||
// 计算阅读进度
|
||||
const query = wx.createSelectorQuery()
|
||||
query.select('.page').boundingClientRect()
|
||||
query.exec((res) => {
|
||||
if (res[0]) {
|
||||
const scrollTop = e.scrollTop
|
||||
const pageHeight = res[0].height - this.data.statusBarHeight - 200
|
||||
const progress = pageHeight > 0 ? Math.min((scrollTop / pageHeight) * 100, 100) : 0
|
||||
this.setData({ readingProgress: progress })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 初始化章节
|
||||
async initSection(id) {
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
// 模拟获取章节数据
|
||||
const section = this.getSectionInfo(id)
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
|
||||
const isFree = this.data.freeIds.includes(id)
|
||||
const isPurchased = hasFullBook || (purchasedSections && purchasedSections.includes(id))
|
||||
const canAccess = isFree || isPurchased
|
||||
const purchasedCount = purchasedSections?.length || 0
|
||||
|
||||
this.setData({
|
||||
section,
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
canAccess,
|
||||
purchasedCount,
|
||||
showPaywall: !canAccess
|
||||
})
|
||||
|
||||
// 加载内容
|
||||
await this.loadContent(id)
|
||||
|
||||
// 获取上一篇/下一篇
|
||||
this.loadNavigation(id)
|
||||
|
||||
} catch (e) {
|
||||
console.error('初始化章节失败:', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 获取章节信息
|
||||
getSectionInfo(id) {
|
||||
// 特殊章节
|
||||
if (id === 'preface') {
|
||||
return { id: 'preface', title: '为什么我每天早上6点在Soul开播?', isFree: true, price: 0 }
|
||||
}
|
||||
if (id === 'epilogue') {
|
||||
return { id: 'epilogue', title: '这本书的真实目的', isFree: true, price: 0 }
|
||||
}
|
||||
if (id.startsWith('appendix')) {
|
||||
const appendixTitles = {
|
||||
'appendix-1': 'Soul派对房精选对话',
|
||||
'appendix-2': '创业者自检清单',
|
||||
'appendix-3': '本书提到的工具和资源'
|
||||
}
|
||||
return { id, title: appendixTitles[id] || '附录', isFree: true, price: 0 }
|
||||
}
|
||||
|
||||
// 普通章节
|
||||
return {
|
||||
id: id,
|
||||
title: this.getSectionTitle(id),
|
||||
isFree: id === '1.1',
|
||||
price: 1
|
||||
}
|
||||
},
|
||||
|
||||
// 获取章节标题
|
||||
getSectionTitle(id) {
|
||||
const titles = {
|
||||
'1.1': '荷包:电动车出租的被动收入模式',
|
||||
'1.2': '老墨:资源整合高手的社交方法',
|
||||
'1.3': '笑声背后的MBTI',
|
||||
'1.4': '人性的三角结构:利益、情感、价值观',
|
||||
'1.5': '沟通差的问题:为什么你说的别人听不懂',
|
||||
'2.1': '相亲故事:你以为找的是人,实际是在找模式',
|
||||
'2.2': '找工作迷茫者:为什么简历解决不了人生',
|
||||
'2.3': '撸运费险:小钱困住大脑的真实心理',
|
||||
'2.4': '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力',
|
||||
'2.5': '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒',
|
||||
'3.1': '3000万流水如何跑出来(退税模式解析)',
|
||||
'8.1': '流量杠杆:抖音、Soul、飞书',
|
||||
'9.14': '大健康私域:一个月150万的70后'
|
||||
}
|
||||
return titles[id] || `章节 ${id}`
|
||||
},
|
||||
|
||||
// 加载内容 - 三级降级方案:API → 本地缓存 → 备用API
|
||||
async loadContent(id) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
|
||||
// 1. 优先从API获取
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 5000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
// 成功后缓存到本地
|
||||
wx.setStorageSync(cacheKey, res)
|
||||
console.log('[Read] 从API加载成功:', id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] API加载失败,尝试本地缓存:', e.message)
|
||||
}
|
||||
|
||||
// 2. API失败,尝试从本地缓存读取
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
this.setChapterContent(cached)
|
||||
console.log('[Read] 从本地缓存加载成功:', id)
|
||||
// 后台静默刷新
|
||||
this.silentRefresh(id)
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 本地缓存读取失败')
|
||||
}
|
||||
|
||||
// 3. 都失败,显示加载中并持续重试
|
||||
this.setData({
|
||||
contentParagraphs: ['章节内容加载中...', '正在尝试连接服务器,请稍候...'],
|
||||
previewParagraphs: ['章节内容加载中...']
|
||||
})
|
||||
|
||||
// 延迟重试(最多3次)
|
||||
this.retryLoadContent(id, 3)
|
||||
},
|
||||
|
||||
// 带超时的章节请求
|
||||
fetchChapterWithTimeout(id, timeout = 5000) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
reject(new Error('请求超时'))
|
||||
}, timeout)
|
||||
|
||||
app.request(`/api/book/chapter/${id}`)
|
||||
.then(res => {
|
||||
clearTimeout(timer)
|
||||
resolve(res)
|
||||
})
|
||||
.catch(err => {
|
||||
clearTimeout(timer)
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 设置章节内容
|
||||
setChapterContent(res) {
|
||||
const lines = res.content.split('\n').filter(line => line.trim())
|
||||
const previewCount = Math.ceil(lines.length * 0.2)
|
||||
|
||||
this.setData({
|
||||
content: res.content,
|
||||
previewContent: lines.slice(0, previewCount).join('\n'),
|
||||
contentParagraphs: lines,
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 静默刷新(后台更新缓存)
|
||||
async silentRefresh(id) {
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 10000)
|
||||
if (res && res.content) {
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 后台缓存更新成功:', id)
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败不处理
|
||||
}
|
||||
},
|
||||
|
||||
// 重试加载
|
||||
retryLoadContent(id, maxRetries, currentRetry = 0) {
|
||||
if (currentRetry >= maxRetries) {
|
||||
this.setData({
|
||||
contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
|
||||
previewParagraphs: ['内容加载失败']
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 重试失败,继续重试:', currentRetry + 1)
|
||||
}
|
||||
this.retryLoadContent(id, maxRetries, currentRetry + 1)
|
||||
}, 2000 * (currentRetry + 1))
|
||||
},
|
||||
|
||||
|
||||
// 加载导航
|
||||
loadNavigation(id) {
|
||||
const sectionOrder = [
|
||||
'preface', '1.1', '1.2', '1.3', '1.4', '1.5',
|
||||
'2.1', '2.2', '2.3', '2.4', '2.5',
|
||||
'3.1', '3.2', '3.3', '3.4',
|
||||
'4.1', '4.2', '4.3', '4.4', '4.5',
|
||||
'5.1', '5.2', '5.3', '5.4', '5.5',
|
||||
'6.1', '6.2', '6.3', '6.4',
|
||||
'7.1', '7.2', '7.3', '7.4', '7.5',
|
||||
'8.1', '8.2', '8.3', '8.4', '8.5', '8.6',
|
||||
'9.1', '9.2', '9.3', '9.4', '9.5', '9.6', '9.7', '9.8', '9.9', '9.10', '9.11', '9.12', '9.13', '9.14',
|
||||
'10.1', '10.2', '10.3', '10.4',
|
||||
'11.1', '11.2', '11.3', '11.4', '11.5',
|
||||
'epilogue'
|
||||
]
|
||||
|
||||
const currentIndex = sectionOrder.indexOf(id)
|
||||
const prevId = currentIndex > 0 ? sectionOrder[currentIndex - 1] : null
|
||||
const nextId = currentIndex < sectionOrder.length - 1 ? sectionOrder[currentIndex + 1] : null
|
||||
|
||||
this.setData({
|
||||
prevSection: prevId ? { id: prevId, title: this.getSectionTitle(prevId) } : null,
|
||||
nextSection: nextId ? { id: nextId, title: this.getSectionTitle(nextId) } : null
|
||||
})
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
wx.navigateBack({
|
||||
fail: () => wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
})
|
||||
},
|
||||
|
||||
// 分享弹窗
|
||||
showShare() {
|
||||
this.setData({ showShareModal: true })
|
||||
},
|
||||
|
||||
closeShareModal() {
|
||||
this.setData({ showShareModal: false })
|
||||
},
|
||||
|
||||
// 复制链接
|
||||
copyLink() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
const shareUrl = `https://soul.quwanzhi.com/read/${this.data.sectionId}${referralCode ? '?ref=' + referralCode : ''}`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareUrl,
|
||||
success: () => {
|
||||
wx.showToast({ title: '链接已复制', icon: 'success' })
|
||||
this.setData({ showShareModal: false })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 复制分享文案(朋友圈风格)
|
||||
copyShareText() {
|
||||
const { section } = this.data
|
||||
|
||||
const shareText = `🔥 刚看完这篇《${section?.title || 'Soul创业派对'}》,太上头了!
|
||||
|
||||
62个真实商业案例,每个都是从0到1的实战经验。私域运营、资源整合、商业变现,干货满满。
|
||||
|
||||
推荐给正在创业或想创业的朋友,搜"Soul创业派对"小程序就能看!
|
||||
|
||||
#创业派对 #私域运营 #商业案例`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({ title: '文案已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 分享到微信 - 自动带分享人ID
|
||||
onShareAppMessage() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || wx.getStorageSync('referralCode') || ''
|
||||
|
||||
// 分享标题优化
|
||||
const shareTitle = section?.title
|
||||
? `📚 ${section.title.length > 20 ? section.title.slice(0, 20) + '...' : section.title}`
|
||||
: '📚 Soul创业派对 - 真实商业故事'
|
||||
|
||||
return {
|
||||
title: shareTitle,
|
||||
path: `/pages/read/read?id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`,
|
||||
imageUrl: '/assets/share-cover.png' // 可配置分享封面图
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
const { section, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const referralCode = userInfo?.referralCode || ''
|
||||
|
||||
return {
|
||||
title: `${section?.title || 'Soul创业派对'} - 来自派对房的真实故事`,
|
||||
query: `id=${sectionId}${referralCode ? '&ref=' + referralCode : ''}`
|
||||
}
|
||||
},
|
||||
|
||||
// 显示登录弹窗
|
||||
showLoginModal() {
|
||||
this.setData({ showLoginModal: true })
|
||||
},
|
||||
|
||||
closeLoginModal() {
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
// 微信登录
|
||||
async handleWechatLogin() {
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
this.setData({ showLoginModal: false })
|
||||
this.initSection(this.data.sectionId)
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号登录
|
||||
async handlePhoneLogin(e) {
|
||||
if (!e.detail.code) {
|
||||
return this.handleWechatLogin()
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (result) {
|
||||
this.setData({ showLoginModal: false })
|
||||
this.initSection(this.data.sectionId)
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '登录失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 购买章节 - 直接调起支付
|
||||
async handlePurchaseSection() {
|
||||
console.log('[Pay] 点击购买章节按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
|
||||
if (!this.data.isLoggedIn) {
|
||||
wx.hideLoading()
|
||||
console.log('[Pay] 用户未登录,显示登录弹窗')
|
||||
this.setData({ showLoginModal: true })
|
||||
return
|
||||
}
|
||||
|
||||
const price = this.data.section?.price || 1
|
||||
console.log('[Pay] 开始支付流程:', { sectionId: this.data.sectionId, price })
|
||||
wx.hideLoading()
|
||||
await this.processPayment('section', this.data.sectionId, price)
|
||||
},
|
||||
|
||||
// 购买全书 - 直接调起支付
|
||||
async handlePurchaseFullBook() {
|
||||
console.log('[Pay] 点击购买全书按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
|
||||
if (!this.data.isLoggedIn) {
|
||||
wx.hideLoading()
|
||||
console.log('[Pay] 用户未登录,显示登录弹窗')
|
||||
this.setData({ showLoginModal: true })
|
||||
return
|
||||
}
|
||||
|
||||
console.log('[Pay] 开始支付流程: 全书', { price: this.data.fullBookPrice })
|
||||
wx.hideLoading()
|
||||
await this.processPayment('fullbook', null, this.data.fullBookPrice)
|
||||
},
|
||||
|
||||
// 处理支付 - 调用真实微信支付接口
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
// 检查金额是否有效
|
||||
if (!amount || amount <= 0) {
|
||||
console.error('[Pay] 金额无效:', amount)
|
||||
wx.showToast({ title: '价格信息错误', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否已购买(避免重复购买)
|
||||
if (type === 'section' && sectionId) {
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
if (purchasedSections.includes(sectionId)) {
|
||||
wx.showToast({ title: '已购买过此章节', icon: 'none' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (type === 'fullbook' && app.globalData.hasFullBook) {
|
||||
wx.showToast({ title: '已购买全书', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ isPaying: true })
|
||||
wx.showLoading({ title: '正在发起支付...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 先获取openId (支付必需)
|
||||
let openId = app.globalData.openId || wx.getStorageSync('openId')
|
||||
|
||||
if (!openId) {
|
||||
console.log('[Pay] 需要先获取openId,尝试静默获取')
|
||||
wx.showLoading({ title: '获取支付凭证...', mask: true })
|
||||
openId = await app.getOpenId()
|
||||
|
||||
if (!openId) {
|
||||
// openId获取失败,但已登录用户可以使用用户ID替代
|
||||
if (app.globalData.isLoggedIn && app.globalData.userInfo?.id) {
|
||||
console.log('[Pay] 使用用户ID作为替代')
|
||||
openId = app.globalData.userInfo.id
|
||||
} else {
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要登录后才能支付,请先登录',
|
||||
showCancel: false
|
||||
})
|
||||
this.setData({ showLoginModal: true, isPaying: false })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('[Pay] 开始创建订单:', { type, sectionId, amount, openId: openId.slice(0, 10) + '...' })
|
||||
wx.showLoading({ title: '创建订单中...', mask: true })
|
||||
|
||||
// 2. 调用后端创建预支付订单
|
||||
let paymentData = null
|
||||
|
||||
try {
|
||||
// 获取章节完整名称用于支付描述
|
||||
const sectionTitle = this.data.section?.title || sectionId
|
||||
const description = type === 'fullbook'
|
||||
? '《一场Soul的创业实验》全书'
|
||||
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
|
||||
|
||||
const res = await app.request('/api/miniprogram/pay', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
openId,
|
||||
productType: type,
|
||||
productId: sectionId,
|
||||
amount,
|
||||
description,
|
||||
userId: app.globalData.userInfo?.id || ''
|
||||
}
|
||||
})
|
||||
|
||||
console.log('[Pay] 创建订单响应:', res)
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
paymentData = res.data.payParams
|
||||
console.log('[Pay] 获取支付参数成功:', paymentData)
|
||||
} else {
|
||||
throw new Error(res.error || res.message || '创建订单失败')
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.error('[Pay] API创建订单失败:', apiError)
|
||||
wx.hideLoading()
|
||||
// 支付接口失败时,显示客服联系方式
|
||||
wx.showModal({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
success: () => {
|
||||
wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
this.setData({ isPaying: false })
|
||||
return
|
||||
}
|
||||
|
||||
// 3. 调用微信支付
|
||||
wx.hideLoading()
|
||||
console.log('[Pay] 调起微信支付, paymentData:', paymentData)
|
||||
|
||||
try {
|
||||
await this.callWechatPay(paymentData)
|
||||
|
||||
// 4. 支付成功,更新本地数据
|
||||
console.log('[Pay] 微信支付成功!')
|
||||
this.mockPaymentSuccess(type, sectionId)
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
|
||||
// 5. 刷新页面
|
||||
this.initSection(this.data.sectionId)
|
||||
} catch (payErr) {
|
||||
console.error('[Pay] 微信支付调起失败:', payErr)
|
||||
if (payErr.errMsg && payErr.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消支付', icon: 'none' })
|
||||
} else if (payErr.errMsg && payErr.errMsg.includes('requestPayment:fail')) {
|
||||
// 支付失败,可能是参数错误或权限问题
|
||||
wx.showModal({
|
||||
title: '支付失败',
|
||||
content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: payErr.errMsg || '支付失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('[Pay] 支付流程异常:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '支付出错,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isPaying: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 模拟支付成功
|
||||
mockPaymentSuccess(type, sectionId) {
|
||||
if (type === 'fullbook') {
|
||||
app.globalData.hasFullBook = true
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.hasFullBook = true
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
} else if (sectionId) {
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
if (!purchasedSections.includes(sectionId)) {
|
||||
purchasedSections.push(sectionId)
|
||||
app.globalData.purchasedSections = purchasedSections
|
||||
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.purchasedSections = purchasedSections
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 调用微信支付
|
||||
callWechatPay(paymentData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
timeStamp: paymentData.timeStamp,
|
||||
nonceStr: paymentData.nonceStr,
|
||||
package: paymentData.package,
|
||||
signType: paymentData.signType || 'MD5',
|
||||
paySign: paymentData.paySign,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到上一篇
|
||||
goToPrev() {
|
||||
if (this.data.prevSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.prevSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到下一篇
|
||||
goToNext() {
|
||||
if (this.data.nextSection) {
|
||||
wx.redirectTo({ url: `/pages/read/read?id=${this.data.nextSection.id}` })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到推广中心
|
||||
goToReferral() {
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数)
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const scene = userId ? `id=${sectionId}&ref=${userId.slice(0,10)}` : `id=${sectionId}`
|
||||
const qrRes = await app.request('/api/miniprogram/qrcode', {
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
ctx.fillText('📚 Soul创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
// 内容摘要
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
y += 30
|
||||
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
|
||||
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
// 底部区域背景
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
// 绘制小程序码或占位符
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeImage) {
|
||||
// 下载base64图片并绘制
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
drawQRPlaceholder(ctx, width, height) {
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.beginPath()
|
||||
ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.setFontSize(9)
|
||||
ctx.fillText('扫码', width - 57, height - 52)
|
||||
ctx.fillText('阅读', width - 57, height - 40)
|
||||
},
|
||||
|
||||
// 文字换行处理
|
||||
wrapText(ctx, text, maxWidth, fontSize) {
|
||||
const lines = []
|
||||
let line = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
const testLine = line + text[i]
|
||||
const metrics = ctx.measureText(testLine)
|
||||
if (metrics.width > maxWidth && line) {
|
||||
lines.push(line)
|
||||
line = text[i]
|
||||
} else {
|
||||
line = testLine
|
||||
}
|
||||
}
|
||||
if (line) lines.push(line)
|
||||
return lines
|
||||
},
|
||||
|
||||
// 关闭海报弹窗
|
||||
closePosterModal() {
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
savePoster() {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {}
|
||||
})
|
||||
7
miniprogram/pages/read/read.json
Normal file
7
miniprogram/pages/read/read.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
232
miniprogram/pages/read/read.wxml
Normal file
232
miniprogram/pages/read/read.wxml
Normal file
@@ -0,0 +1,232 @@
|
||||
<!--pages/read/read.wxml-->
|
||||
<!--Soul创业派对 - 阅读页-->
|
||||
<view class="page">
|
||||
<!-- 阅读进度条 -->
|
||||
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
|
||||
<view class="progress-fill" style="width: {{readingProgress}}%;"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<text class="back-arrow">←</text>
|
||||
</view>
|
||||
<view class="nav-info">
|
||||
<text class="nav-part" wx:if="{{partTitle}}">{{partTitle}}</text>
|
||||
<text class="nav-chapter" wx:if="{{chapterTitle}}">{{chapterTitle}}</text>
|
||||
</view>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 阅读内容 -->
|
||||
<view class="read-content">
|
||||
<!-- 章节标题 -->
|
||||
<view class="chapter-header">
|
||||
<view class="chapter-meta">
|
||||
<text class="chapter-id">{{section.id}}</text>
|
||||
<text class="tag tag-free" wx:if="{{section.isFree}}">免费</text>
|
||||
</view>
|
||||
<text class="chapter-title">{{section.title}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" wx:if="{{loading}}">
|
||||
<view class="skeleton skeleton-1"></view>
|
||||
<view class="skeleton skeleton-2"></view>
|
||||
<view class="skeleton skeleton-3"></view>
|
||||
<view class="skeleton skeleton-4"></view>
|
||||
<view class="skeleton skeleton-5"></view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 有权限 -->
|
||||
<view class="article" wx:if="{{!loading && canAccess}}">
|
||||
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
<view class="chapter-nav">
|
||||
<view class="nav-buttons">
|
||||
<view
|
||||
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
|
||||
bindtap="goToPrev"
|
||||
wx:if="{{prevSection}}"
|
||||
>
|
||||
<text class="btn-label">上一篇</text>
|
||||
<text class="btn-title">{{prevSection.title}}</text>
|
||||
</view>
|
||||
<view class="nav-btn-placeholder" wx:else></view>
|
||||
|
||||
<view
|
||||
class="nav-btn nav-next"
|
||||
bindtap="goToNext"
|
||||
wx:if="{{nextSection}}"
|
||||
>
|
||||
<text class="btn-label">下一篇</text>
|
||||
<view class="btn-row">
|
||||
<text class="btn-title">{{nextSection.title}}</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-btn nav-end" wx:else>
|
||||
<text class="btn-end-text">已是最后一篇 🎉</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享操作区 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<button class="action-btn-inline btn-share-inline" open-type="share">
|
||||
<text class="action-icon-small">💬</text>
|
||||
<text class="action-text-small">推荐给好友</text>
|
||||
</button>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<text class="action-icon-small">🖼️</text>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预览内容 + 付费墙 - 无权限 -->
|
||||
<view class="article preview" wx:if="{{!loading && !canAccess}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 -->
|
||||
<view class="paywall" wx:if="{{showPaywall}}">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">
|
||||
已阅读20%,{{isLoggedIn ? '购买后继续阅读' : '登录并购买后继续阅读'}}
|
||||
</text>
|
||||
|
||||
<!-- 未登录时显示登录按钮 -->
|
||||
<view class="login-prompt" wx:if="{{!isLoggedIn}}">
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">请先登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已登录显示购买选项 -->
|
||||
<view class="purchase-options" wx:else>
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section.price}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<text class="btn-sparkle">✨</text>
|
||||
<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>
|
||||
|
||||
<text class="paywall-tip">分享给好友一起学习</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 - 付费内容也显示 -->
|
||||
<view class="chapter-nav chapter-nav-locked">
|
||||
<view class="nav-buttons">
|
||||
<view
|
||||
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
|
||||
bindtap="goToPrev"
|
||||
wx:if="{{prevSection}}"
|
||||
>
|
||||
<text class="btn-label">上一篇</text>
|
||||
<text class="btn-title">章节 {{prevSection.id}}</text>
|
||||
</view>
|
||||
<view class="nav-btn-placeholder" wx:else></view>
|
||||
|
||||
<view
|
||||
class="nav-btn nav-next"
|
||||
bindtap="goToNext"
|
||||
wx:if="{{nextSection}}"
|
||||
>
|
||||
<text class="btn-label">下一篇</text>
|
||||
<view class="btn-row">
|
||||
<text class="btn-title">{{nextSection.title}}</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-btn nav-end" wx:else>
|
||||
<text class="btn-end-text">已是最后一篇 🎉</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content poster-modal" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">生成海报</text>
|
||||
<view class="modal-close" bindtap="closePosterModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
|
||||
<view class="poster-actions">
|
||||
<view class="poster-btn btn-save" bindtap="savePoster">
|
||||
<text class="btn-icon">💾</text>
|
||||
<text>保存到相册</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="poster-tip">长按海报可直接分享到微信</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 - 只保留微信登录 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal">✕</view>
|
||||
<view class="login-icon">🔐</view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
|
||||
<button class="btn-wechat" bindtap="handleWechatLogin">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
</button>
|
||||
|
||||
<text class="login-notice">登录即表示同意《用户协议》和《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付中提示 -->
|
||||
<view class="modal-overlay" wx:if="{{isPaying}}" catchtap="">
|
||||
<view class="loading-box">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">支付处理中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮分享按钮 -->
|
||||
<button class="fab-share" open-type="share">
|
||||
<text class="fab-share-icon">↗</text>
|
||||
<text class="fab-share-text">分享</text>
|
||||
</button>
|
||||
</view>
|
||||
964
miniprogram/pages/read/read.wxss
Normal file
964
miniprogram/pages/read/read.wxss
Normal file
@@ -0,0 +1,964 @@
|
||||
/**
|
||||
* Soul创业实验 - 阅读页样式
|
||||
* 1:1还原Web版本UI
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
/* ===== 阅读进度条 ===== */
|
||||
.progress-bar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
background: #1c1c1e;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
|
||||
transition: width 0.15s ease;
|
||||
}
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back, .nav-right-placeholder {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
border-radius: 50%;
|
||||
background: #1c1c1e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-right-placeholder {
|
||||
/* 占位保持标题居中 */
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.nav-info {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.nav-part {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-chapter {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 阅读内容 ===== */
|
||||
.read-content {
|
||||
max-width: 750rpx;
|
||||
margin: 0 auto;
|
||||
padding: 48rpx 40rpx 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 章节标题 ===== */
|
||||
.chapter-header {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.chapter-id {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
min-width: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag-free {
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ===== 加载状态 ===== */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 32rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-1 { width: 75%; }
|
||||
.skeleton-2 { width: 90%; }
|
||||
.skeleton-3 { width: 65%; }
|
||||
.skeleton-4 { width: 85%; }
|
||||
.skeleton-5 { width: 70%; }
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ===== 文章内容 ===== */
|
||||
.article {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 34rpx;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin-bottom: 48rpx;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== 渐变遮罩 ===== */
|
||||
.fade-mask {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -40rpx;
|
||||
right: -40rpx;
|
||||
height: 300rpx;
|
||||
background: linear-gradient(to top, #000000 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== 付费墙 ===== */
|
||||
.paywall {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin-top: 48rpx;
|
||||
padding: 48rpx;
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.paywall-icon {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
margin: 0 auto 32rpx;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.paywall-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.paywall-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
/* ===== 购买选项 ===== */
|
||||
.purchase-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.purchase-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.purchase-section {
|
||||
background: #2c2c2e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.purchase-fullbook {
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.3);
|
||||
}
|
||||
|
||||
.purchase-section .btn-label {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.purchase-section .btn-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-sparkle {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-label {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-price {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-discount {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.paywall-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 章节导航 ===== */
|
||||
.chapter-nav {
|
||||
margin-top: 96rpx;
|
||||
padding-top: 64rpx;
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
.nav-btn-placeholder {
|
||||
flex: 1;
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
.nav-prev {
|
||||
background: #1c1c1e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-next {
|
||||
background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.1) 100%);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.nav-end {
|
||||
background: #1c1c1e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.nav-next .btn-label {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.btn-end-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 分享操作区 ===== */
|
||||
.action-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.action-row-inline {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn-inline {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn-inline::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-text-small {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 推广提示区 ===== */
|
||||
.promo-section {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.2);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.promo-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.promo-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.promo-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.promo-title {
|
||||
font-size: 30rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promo-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.promo-right {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.promo-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(20rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
padding: 48rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 分享弹窗 ===== */
|
||||
.share-link-box {
|
||||
padding: 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.link-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 26rpx;
|
||||
color: #00CED1;
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.link-tip {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.share-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 24rpx;
|
||||
border: none;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.share-btn-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.icon-copy {
|
||||
background: rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.icon-wechat {
|
||||
background: rgba(7, 193, 96, 0.2);
|
||||
color: #07C160;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.icon-poster {
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
.share-btn-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 支付弹窗 ===== */
|
||||
.payment-info {
|
||||
padding: 24rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.payment-type {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.method-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.method-active {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.method-check {
|
||||
color: #07C160;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.payment-notice {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 登录提示 ===== */
|
||||
.login-prompt {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 登录弹窗 ===== */
|
||||
.login-modal {
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
font-size: 80rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.btn-wechat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-wechat::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-wechat-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-phone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-phone::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-phone-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.login-notice {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin-top: 32rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 支付中加载 ===== */
|
||||
.loading-box {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #00CED1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* ===== 海报弹窗 ===== */
|
||||
.poster-modal {
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.poster-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 32rpx 0;
|
||||
padding: 24rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.poster-canvas {
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.poster-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.poster-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.poster-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 右下角悬浮分享按钮 ===== */
|
||||
.fab-share {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
z-index: 90;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.fab-share-icon {
|
||||
font-size: 40rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-share-text {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
margin-top: 4rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
Reference in New Issue
Block a user