chore: 清理敏感与开发文档,仅同步代码
- 永久忽略并从仓库移除 开发文档/ - 移除并忽略 .env 与小程序私有配置 - 同步小程序/管理端/API与脚本改动 Made-with: Cursor
This commit is contained in:
@@ -35,8 +35,7 @@ miniprogram/
|
||||
│ ├── purchases/ # 订单页
|
||||
│ └── settings/ # 设置页
|
||||
├── utils/
|
||||
│ ├── util.js # 工具函数
|
||||
│ └── payment.js # 支付工具
|
||||
│ └── util.js # 工具函数
|
||||
├── assets/
|
||||
│ └── icons/ # 图标资源
|
||||
├── project.config.json # 项目配置
|
||||
|
||||
@@ -5,22 +5,43 @@
|
||||
|
||||
const { parseScene } = require('./utils/scene.js')
|
||||
|
||||
const DEFAULT_BASE_URL = 'https://soulapi.quwanzhi.com'
|
||||
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
|
||||
const DEFAULT_MCH_ID = '1318592501'
|
||||
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
|
||||
|
||||
function getRuntimeBootstrapConfig() {
|
||||
try {
|
||||
const extCfg = wx.getExtConfigSync ? (wx.getExtConfigSync() || {}) : {}
|
||||
return {
|
||||
baseUrl: extCfg.apiBaseUrl || wx.getStorageSync('apiBaseUrl') || DEFAULT_BASE_URL,
|
||||
appId: extCfg.appId || DEFAULT_APP_ID,
|
||||
mchId: extCfg.mchId || DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: extCfg.withdrawSubscribeTmplId || DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
} catch (_) {
|
||||
return {
|
||||
baseUrl: DEFAULT_BASE_URL,
|
||||
appId: DEFAULT_APP_ID,
|
||||
mchId: DEFAULT_MCH_ID,
|
||||
withdrawSubscribeTmplId: DEFAULT_WITHDRAW_TMPL_ID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const bootstrapConfig = getRuntimeBootstrapConfig()
|
||||
|
||||
App({
|
||||
globalData: {
|
||||
// API基础地址 - 连接真实后端
|
||||
baseUrl: 'https://soulapi.quwanzhi.com',
|
||||
// baseUrl: 'https://souldev.quwanzhi.com',
|
||||
// baseUrl: 'http://localhost:8080',
|
||||
|
||||
|
||||
// 小程序配置 - 真实AppID
|
||||
appId: 'wxb8bbb2b10dec74aa',
|
||||
// 运行配置:优先外部配置/缓存,其次默认值
|
||||
baseUrl: bootstrapConfig.baseUrl,
|
||||
appId: bootstrapConfig.appId,
|
||||
|
||||
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
|
||||
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
|
||||
withdrawSubscribeTmplId: bootstrapConfig.withdrawSubscribeTmplId,
|
||||
|
||||
// 微信支付配置
|
||||
mchId: '1318592501', // 商户号
|
||||
mchId: bootstrapConfig.mchId,
|
||||
|
||||
// 用户信息
|
||||
userInfo: null,
|
||||
@@ -29,11 +50,15 @@ App({
|
||||
|
||||
// 书籍数据
|
||||
bookData: null,
|
||||
totalSections: 62,
|
||||
totalSections: 0,
|
||||
supportWechat: '',
|
||||
|
||||
// 购买记录
|
||||
purchasedSections: [],
|
||||
hasFullBook: false,
|
||||
// VIP 会员(365天,包含增值版免费;与 hasFullBook=9.9 买断不同)
|
||||
isVip: false,
|
||||
vipExpireDate: '',
|
||||
|
||||
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
|
||||
readSectionIds: [],
|
||||
@@ -58,17 +83,35 @@ App({
|
||||
// TabBar相关
|
||||
currentTab: 0,
|
||||
|
||||
// 是否处于「单页模式」(如朋友圈文章里的单页预览)
|
||||
// 用于在受限环境下给出引导文案,提示用户点击底部「前往小程序」进入完整体验
|
||||
isSinglePageMode: false,
|
||||
|
||||
// 更新检测:上次检测时间戳,避免频繁请求
|
||||
lastUpdateCheck: 0
|
||||
lastUpdateCheck: 0,
|
||||
|
||||
// 审核模式:后端 /api/miniprogram/config 返回 auditMode=true 时隐藏所有支付相关UI
|
||||
auditMode: false
|
||||
},
|
||||
|
||||
onLaunch(options) {
|
||||
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
||||
// 获取系统信息
|
||||
this.getSystemInfo()
|
||||
|
||||
// 场景值兜底:1154 为「朋友圈单页模式」进入
|
||||
try {
|
||||
const launchOpts = wx.getLaunchOptionsSync ? wx.getLaunchOptionsSync() : null
|
||||
if (launchOpts && launchOpts.scene === 1154) {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[App] 读取 LaunchOptions 失败:', e)
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
this.loadRuntimeConfig()
|
||||
|
||||
// 加载书籍数据
|
||||
this.loadBookData()
|
||||
@@ -190,6 +233,31 @@ App({
|
||||
return code.replace(/[\s\-_]/g, '').toUpperCase().trim()
|
||||
},
|
||||
|
||||
// 判断用户资料是否完善(昵称 + 头像)
|
||||
_isProfileIncomplete(user) {
|
||||
if (!user) return true
|
||||
const nickname = (user.nickname || '').trim()
|
||||
const avatar = (user.avatar || '').trim()
|
||||
const isDefaultNickname = !nickname || nickname === '微信用户'
|
||||
const noAvatar = !avatar
|
||||
return isDefaultNickname || noAvatar
|
||||
},
|
||||
|
||||
// 登录后若资料未完善,引导跳转到资料编辑页
|
||||
_ensureProfileCompletedAfterLogin(user) {
|
||||
try {
|
||||
if (!user || !this._isProfileIncomplete(user)) return
|
||||
const pages = getCurrentPages()
|
||||
const current = pages[pages.length - 1]
|
||||
// 避免在资料页内重复跳转
|
||||
if (current && current.route === 'pages/profile-edit/profile-edit') return
|
||||
wx.showToast({ title: '请先完善头像和昵称', icon: 'none', duration: 2000 })
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
} catch (e) {
|
||||
console.warn('[App] 跳转资料编辑页失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 根据业务 id 从 bookData 查 mid(用于跳转)
|
||||
getSectionMid(sectionId) {
|
||||
const list = this.globalData.bookData || []
|
||||
@@ -224,6 +292,11 @@ App({
|
||||
const systemInfo = wx.getSystemInfoSync()
|
||||
this.globalData.systemInfo = systemInfo
|
||||
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
|
||||
|
||||
// 微信在单页模式下会在 systemInfo.mode 标记 singlePage
|
||||
if (systemInfo.mode === 'singlePage') {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
|
||||
// 计算导航栏高度
|
||||
const menuButton = wx.getMenuButtonBoundingClientRect()
|
||||
@@ -235,6 +308,33 @@ App({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 若当前处于朋友圈等「单页模式」,在尝试登录/购买前给用户友好提示,
|
||||
* 引导用户点击底部「前往小程序」进入完整小程序再操作。
|
||||
* 返回 false 表示应中断当前操作。
|
||||
*/
|
||||
ensureFullAppForAuth() {
|
||||
// 每次调用时再做一次兜底检测,避免全局标记遗漏
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
if (sys && sys.mode === 'singlePage') {
|
||||
this.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[App] ensureFullAppForAuth getSystemInfoSync error:', e)
|
||||
}
|
||||
|
||||
if (!this.globalData.isSinglePageMode) return true
|
||||
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为朋友圈单页,仅支持部分浏览。如需登录和解锁内容,请点击底部「前往小程序」后再操作。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
try {
|
||||
@@ -246,12 +346,32 @@ App({
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = userInfo.purchasedSections || []
|
||||
this.globalData.hasFullBook = userInfo.hasFullBook || false
|
||||
this.globalData.isVip = userInfo.isVip || false
|
||||
this.globalData.vipExpireDate = userInfo.vipExpireDate || ''
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('检查登录状态失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadRuntimeConfig() {
|
||||
try {
|
||||
const res = await this.request({ url: '/api/miniprogram/config', silent: true, timeout: 5000 })
|
||||
const mpConfig = res?.mpConfig || {}
|
||||
this.globalData.baseUrl = mpConfig.apiDomain || this.globalData.baseUrl
|
||||
this.globalData.appId = mpConfig.appId || this.globalData.appId
|
||||
this.globalData.mchId = mpConfig.mchId || this.globalData.mchId
|
||||
this.globalData.withdrawSubscribeTmplId = mpConfig.withdrawSubscribeTmplId || this.globalData.withdrawSubscribeTmplId
|
||||
this.globalData.supportWechat = mpConfig.supportWechat || mpConfig.customerWechat || mpConfig.serviceWechat || ''
|
||||
this.globalData.auditMode = mpConfig.auditMode || false
|
||||
try {
|
||||
wx.setStorageSync('apiBaseUrl', this.globalData.baseUrl)
|
||||
} catch (_) {}
|
||||
} catch (e) {
|
||||
console.warn('[App] 加载运行配置失败,继续使用默认配置:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载书籍数据
|
||||
async loadBookData() {
|
||||
try {
|
||||
@@ -266,6 +386,7 @@ App({
|
||||
if (res && (res.data || res.chapters)) {
|
||||
const chapters = res.data || res.chapters || []
|
||||
this.globalData.bookData = chapters
|
||||
this.globalData.totalSections = res.total || chapters.length || 0
|
||||
wx.setStorageSync('bookData', chapters)
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -355,6 +476,7 @@ App({
|
||||
url: this.globalData.baseUrl + url,
|
||||
method: options.method || 'GET',
|
||||
data: options.data || {},
|
||||
timeout: options.timeout || 15000,
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': token ? `Bearer ${token}` : '',
|
||||
@@ -366,6 +488,11 @@ App({
|
||||
// 业务失败:success === false,soul-api 用 message 或 error 返回原因
|
||||
if (data && data.success === false) {
|
||||
const msg = this._getApiErrorMsg(data, '操作失败')
|
||||
// 登录态不一致:本地有 token/userInfo,但后端查不到该用户
|
||||
// 典型原因:切换环境(baseUrl)、换库/清库、用户被删除、token 与用户不匹配
|
||||
if (msg && (msg.includes('用户不存在') || msg.toLowerCase().includes('user not found'))) {
|
||||
this.logout()
|
||||
}
|
||||
showError(msg)
|
||||
reject(new Error(msg))
|
||||
return
|
||||
@@ -386,7 +513,7 @@ App({
|
||||
},
|
||||
fail: (err) => {
|
||||
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
|
||||
showError(msg)
|
||||
console.warn('[App] request fail:', url, msg)
|
||||
reject(new Error(msg))
|
||||
}
|
||||
})
|
||||
@@ -395,13 +522,15 @@ App({
|
||||
|
||||
// 登录方法 - 获取openId用于支付(加固错误处理,避免审核报“登录报错”)
|
||||
async login() {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes || !loginRes.code) {
|
||||
console.warn('[App] wx.login 未返回 code')
|
||||
wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
|
||||
return null
|
||||
}
|
||||
try {
|
||||
@@ -420,12 +549,13 @@ App({
|
||||
|
||||
// 保存用户信息
|
||||
if (res.data.user) {
|
||||
this.globalData.userInfo = res.data.user
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
||||
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
|
||||
wx.setStorageSync('userInfo', res.data.user)
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token || '')
|
||||
|
||||
// 登录成功后,检查待绑定的推荐码并执行绑定
|
||||
@@ -434,27 +564,30 @@ App({
|
||||
console.log('[App] 登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
}
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (apiError) {
|
||||
console.log('[App] API登录失败:', apiError.message)
|
||||
// 不使用模拟登录,提示用户网络问题
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
console.warn('[App] API登录失败:', apiError.message)
|
||||
return null
|
||||
}
|
||||
|
||||
return null
|
||||
} catch (e) {
|
||||
console.error('[App] 登录失败:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
console.warn('[App] 登录失败:', e)
|
||||
return null
|
||||
}
|
||||
},
|
||||
|
||||
// 获取openId (支付必需)
|
||||
async getOpenId() {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
// 先检查缓存
|
||||
const cachedOpenId = wx.getStorageSync('openId')
|
||||
if (cachedOpenId) {
|
||||
@@ -478,42 +611,42 @@ App({
|
||||
wx.setStorageSync('openId', res.data.openId)
|
||||
// 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
|
||||
if (res.data.user) {
|
||||
this.globalData.userInfo = res.data.user
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
||||
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
||||
wx.setStorageSync('userInfo', res.data.user)
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token || '')
|
||||
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
|
||||
if (pendingRef) {
|
||||
console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
}
|
||||
return res.data.openId
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[App] 获取openId失败:', e)
|
||||
console.warn('[App] 获取openId失败:', e)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
// 模拟登录已废弃 - 不再使用
|
||||
// 现在必须使用真实的微信登录获取openId作为唯一标识
|
||||
mockLogin() {
|
||||
console.warn('[App] mockLogin已废弃,请使用真实登录')
|
||||
return null
|
||||
},
|
||||
|
||||
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
|
||||
async loginWithPhone(phoneCode) {
|
||||
if (!this.ensureFullAppForAuth()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const loginRes = await new Promise((resolve, reject) => {
|
||||
wx.login({ success: resolve, fail: reject })
|
||||
})
|
||||
if (!loginRes.code) {
|
||||
wx.showToast({ title: '获取登录态失败', icon: 'none' })
|
||||
console.warn('[App] loginWithPhone: wx.login 未返回 code')
|
||||
return null
|
||||
}
|
||||
const res = await this.request('/api/miniprogram/phone-login', {
|
||||
@@ -522,12 +655,13 @@ App({
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
this.globalData.userInfo = res.data.user
|
||||
const user = res.data.user
|
||||
this.globalData.userInfo = user
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = res.data.user.purchasedSections || []
|
||||
this.globalData.hasFullBook = res.data.user.hasFullBook || false
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
|
||||
wx.setStorageSync('userInfo', res.data.user)
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
|
||||
// 登录成功后绑定推荐码
|
||||
@@ -536,12 +670,14 @@ App({
|
||||
console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef)
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料
|
||||
this._ensureProfileCompletedAfterLogin(user)
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[App] 手机号登录失败:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
console.warn('[App] 手机号登录失败:', e)
|
||||
}
|
||||
|
||||
return null
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
"pages/match/match",
|
||||
"pages/my/my",
|
||||
"pages/read/read",
|
||||
"pages/about/about",
|
||||
"pages/link-preview/link-preview",
|
||||
"pages/agreement/agreement",
|
||||
"pages/privacy/privacy",
|
||||
"pages/referral/referral",
|
||||
@@ -15,8 +15,16 @@
|
||||
"pages/addresses/addresses",
|
||||
"pages/addresses/edit",
|
||||
"pages/withdraw-records/withdraw-records",
|
||||
"pages/wallet/wallet",
|
||||
"pages/vip/vip",
|
||||
"pages/member-detail/member-detail","pages/mentors/mentors","pages/mentor-detail/mentor-detail","pages/profile-show/profile-show","pages/profile-edit/profile-edit"
|
||||
"pages/member-detail/member-detail",
|
||||
"pages/mentors/mentors",
|
||||
"pages/mentor-detail/mentor-detail",
|
||||
"pages/profile-show/profile-show",
|
||||
"pages/profile-edit/profile-edit",
|
||||
"pages/avatar-nickname/avatar-nickname",
|
||||
"pages/gift-pay/detail",
|
||||
"pages/gift-pay/list"
|
||||
],
|
||||
"window": {
|
||||
"backgroundTextStyle": "light",
|
||||
@@ -52,6 +60,10 @@
|
||||
]
|
||||
},
|
||||
"usingComponents": {},
|
||||
"navigateToMiniProgramAppIdList": [
|
||||
"wx6489c26045912fe1",
|
||||
"wx3d15ed02e98b04e3"
|
||||
],
|
||||
"__usePrivacyCheck__": true,
|
||||
"lazyCodeLoading": "requiredComponents",
|
||||
"style": "v2",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
|
||||
|
||||
<text class="doc-section">七、联系我们</text>
|
||||
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有疑问,请通过 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
155
miniprogram/pages/avatar-nickname/avatar-nickname.js
Normal file
155
miniprogram/pages/avatar-nickname/avatar-nickname.js
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* Soul创业派对 - 头像昵称引导页
|
||||
* 登录后资料未完善时引导用户修改默认头像和昵称,仅包含头像+昵称两项
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
saving: false,
|
||||
showAvatarModal: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadFromUser()
|
||||
},
|
||||
|
||||
loadFromUser() {
|
||||
const user = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !user?.id) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => getApp().goBackOrToHome(), 1500)
|
||||
return
|
||||
}
|
||||
const nickname = (user.nickname || user.nickName || '').trim()
|
||||
const avatar = user.avatar || user.avatarUrl || ''
|
||||
this.setData({ nickname, avatar })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onNicknameInput(e) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
onNicknameChange(e) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
},
|
||||
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
await this.uploadAndSaveAvatar(tempPath)
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
await this.uploadAndSaveAvatar(tempAvatarUrl)
|
||||
},
|
||||
|
||||
async uploadAndSaveAvatar(tempPath) {
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch {
|
||||
reject(new Error('解析失败'))
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const nickname = (this.data.nickname || '').trim()
|
||||
const avatar = (this.data.avatar || '').trim()
|
||||
if (!nickname) {
|
||||
wx.showToast({ title: '请输入昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId, avatar: avatar || undefined, nickname },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
if (nickname) app.globalData.userInfo.nickname = nickname
|
||||
if (avatar) app.globalData.userInfo.avatar = avatar
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
setTimeout(() => getApp().goBackOrToHome(), 800)
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ saving: false })
|
||||
},
|
||||
|
||||
goToFullProfile() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/avatar-nickname/avatar-nickname.json
Normal file
4
miniprogram/pages/avatar-nickname/avatar-nickname.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "完善资料",
|
||||
"usingComponents": {}
|
||||
}
|
||||
67
miniprogram/pages/avatar-nickname/avatar-nickname.wxml
Normal file
67
miniprogram/pages/avatar-nickname/avatar-nickname.wxml
Normal file
@@ -0,0 +1,67 @@
|
||||
<!--Soul创业派对 - 头像昵称引导页,仅头像+昵称-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><text class="back-icon">‹</text></view>
|
||||
<text class="nav-title">完善资料</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<!-- 引导文案 -->
|
||||
<view class="guide-card">
|
||||
<text class="guide-icon">👋</text>
|
||||
<text class="guide-title">完善头像和昵称</text>
|
||||
<text class="guide-desc">让他人更好地认识你,展示更专业的形象</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera">📷</view>
|
||||
</view>
|
||||
<text class="avatar-change">点击更换头像</text>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<view class="form-section">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击输入框自动填充昵称,或手动输入</text>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '完成'}}
|
||||
</view>
|
||||
|
||||
<view class="link-row" bindtap="goToFullProfile">
|
||||
<text class="link-text">完善更多资料</text>
|
||||
<text class="link-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:使用微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal">✕</view>
|
||||
<text class="avatar-modal-title">使用微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮,一键同步当前微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
271
miniprogram/pages/avatar-nickname/avatar-nickname.wxss
Normal file
271
miniprogram/pages/avatar-nickname/avatar-nickname.wxss
Normal file
@@ -0,0 +1,271 @@
|
||||
/* Soul创业派对 - 头像昵称引导页 */
|
||||
.page {
|
||||
background: #050B14;
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 44px;
|
||||
padding: 0 24rpx;
|
||||
background: rgba(5, 11, 20, 0.9);
|
||||
backdrop-filter: blur(8rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.nav-back {
|
||||
width: 60rpx;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
.back-icon {
|
||||
font-size: 44rpx;
|
||||
color: #5EEAD4;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 60rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 32rpx 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.guide-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
padding: 48rpx 32rpx;
|
||||
background: rgba(94, 234, 212, 0.08);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.25);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
.guide-icon {
|
||||
font-size: 64rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.guide-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #5EEAD4;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.guide-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(148, 163, 184, 0.95);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 192rpx;
|
||||
height: 192rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid #5EEAD4;
|
||||
box-shadow: 0 0 30rpx rgba(94, 234, 212, 0.3);
|
||||
}
|
||||
.avatar-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.avatar-placeholder {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 72rpx;
|
||||
font-weight: bold;
|
||||
color: #5EEAD4;
|
||||
background: rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
.avatar-camera {
|
||||
position: absolute;
|
||||
bottom: -8rpx;
|
||||
right: -8rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
background: #5EEAD4;
|
||||
color: #000;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
border: 4rpx solid #050B14;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.avatar-change {
|
||||
font-size: 28rpx;
|
||||
color: #5EEAD4;
|
||||
font-weight: 500;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.form-label {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #94A3B8;
|
||||
margin-bottom: 12rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
.form-input-wrap {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #17212F;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
border-radius: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.form-input-inner {
|
||||
width: 100%;
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
background: transparent;
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
}
|
||||
.input-tip {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
margin-left: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.save-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
text-align: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
border-radius: 24rpx;
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.save-btn[disabled] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.link-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx;
|
||||
}
|
||||
.link-text {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
.link-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #5EEAD4;
|
||||
}
|
||||
|
||||
/* 头像弹窗 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(16rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
background: #0b1220;
|
||||
border-radius: 32rpx;
|
||||
padding: 48rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
.avatar-modal-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.avatar-modal-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.btn-choose-avatar {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -16,6 +17,7 @@ Page({
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
isVip: false,
|
||||
purchasedSections: [],
|
||||
|
||||
// 书籍数据:以后台内容管理为准,仅用接口 /api/miniprogram/book/all-chapters 返回的数据
|
||||
@@ -26,14 +28,13 @@ Page({
|
||||
expandedPart: null,
|
||||
|
||||
// 附录
|
||||
appendixList: [
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
||||
],
|
||||
appendixList: [],
|
||||
|
||||
// 每日新增章节
|
||||
dailyChapters: []
|
||||
dailyChapters: [],
|
||||
|
||||
// 审核模式
|
||||
auditMode: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -43,6 +44,7 @@ Page({
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadVipStatus()
|
||||
this.loadChaptersOnce()
|
||||
},
|
||||
|
||||
@@ -74,6 +76,7 @@ Page({
|
||||
|
||||
const totalSections = res.total ?? rows.length
|
||||
app.globalData.bookData = rows
|
||||
app.globalData.totalSections = totalSections
|
||||
wx.setStorageSync('bookData', rows)
|
||||
|
||||
// bookData:过滤序言/尾声/附录,按 part 聚合,篇章顺序按 sort_order 与后台一致(含「2026每日派对干货」等)
|
||||
@@ -105,13 +108,19 @@ Page({
|
||||
})
|
||||
}
|
||||
const ch = part.chapters.get(cid)
|
||||
const isPremium =
|
||||
r.editionPremium === true ||
|
||||
r.edition_premium === true ||
|
||||
r.edition_premium === 1 ||
|
||||
r.edition_premium === '1'
|
||||
ch.sections.push({
|
||||
id: r.id,
|
||||
mid: r.mid ?? r.MID ?? 0,
|
||||
title: r.sectionTitle || r.section_title || r.title || '',
|
||||
isFree: r.isFree === true || (r.price !== undefined && r.price === 0),
|
||||
price: r.price ?? 1,
|
||||
isNew: r.isNew === true || r.is_new === true
|
||||
isNew: r.isNew === true || r.is_new === true,
|
||||
isPremium
|
||||
})
|
||||
})
|
||||
const partList = Array.from(partMap.values())
|
||||
@@ -125,6 +134,16 @@ Page({
|
||||
}))
|
||||
|
||||
const baseSort = 62
|
||||
const appendixList = rows
|
||||
.filter(r => {
|
||||
const partTitle = String(r.partTitle || r.part_title || '')
|
||||
return partTitle.includes('附录')
|
||||
})
|
||||
.sort((a, b) => (a.sort_order ?? a.sectionOrder ?? 999999) - (b.sort_order ?? b.sectionOrder ?? 999999))
|
||||
.map(c => ({
|
||||
id: c.id,
|
||||
title: c.section_title || c.sectionTitle || c.title || c.chapterTitle || '附录'
|
||||
}))
|
||||
const daily = rows
|
||||
.filter(r => (r.sectionOrder ?? r.sort_order ?? 0) > baseSort)
|
||||
.sort((a, b) => new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0))
|
||||
@@ -143,6 +162,7 @@ Page({
|
||||
this.setData({
|
||||
bookData,
|
||||
totalSections,
|
||||
appendixList,
|
||||
dailyChapters: daily,
|
||||
expandedPart: this.data.expandedPart
|
||||
})
|
||||
@@ -166,17 +186,40 @@ Page({
|
||||
tabBar.setData({ selected: 1 })
|
||||
}
|
||||
}
|
||||
this.setData({ auditMode: app.globalData.auditMode })
|
||||
this.updateUserStatus()
|
||||
this.loadVipStatus()
|
||||
},
|
||||
|
||||
// 拉取 VIP 状态(isVip=会员,hasFullBook=9.9 买断)
|
||||
async loadVipStatus() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true, timeout: 3000 })
|
||||
if (res?.success) {
|
||||
app.globalData.isVip = !!res.data?.isVip
|
||||
app.globalData.vipExpireDate = res.data?.expireDate || ''
|
||||
this.setData({ isVip: app.globalData.isVip })
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.isVip = app.globalData.isVip
|
||||
userInfo.vipExpireDate = app.globalData.vipExpireDate
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
} catch (e) {
|
||||
// 静默失败不影响目录展示
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
this.setData({ isLoggedIn, hasFullBook, purchasedSections })
|
||||
const { isLoggedIn, hasFullBook, purchasedSections, isVip } = app.globalData
|
||||
this.setData({ isLoggedIn, hasFullBook, purchasedSections, isVip })
|
||||
},
|
||||
|
||||
// 切换展开状态
|
||||
togglePart(e) {
|
||||
trackClick('chapters', 'tab_click', e.currentTarget.dataset.id || '篇章')
|
||||
const partId = e.currentTarget.dataset.id
|
||||
this.setData({
|
||||
expandedPart: this.data.expandedPart === partId ? null : partId
|
||||
@@ -187,13 +230,15 @@ Page({
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
trackClick('chapters', 'card_click', id || '章节')
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 检查是否已购买
|
||||
hasPurchased(sectionId) {
|
||||
if (this.data.hasFullBook) return true
|
||||
hasPurchased(sectionId, isPremium) {
|
||||
if (this.data.isVip) return true
|
||||
if (!isPremium && this.data.hasFullBook) return true
|
||||
return this.data.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
@@ -204,6 +249,7 @@ Page({
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
trackClick('chapters', 'nav_click', '搜索')
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left">
|
||||
<view class="search-btn" bindtap="goToSearch">
|
||||
<view class="search-btn" wx:if="{{searchEnabled}}" bindtap="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -17,8 +17,14 @@
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 目录加载中 -->
|
||||
<view class="parts-loading" wx:if="{{partsLoading}}">
|
||||
<view class="parts-loading-spinner"></view>
|
||||
<text class="parts-loading-text">加载目录中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 书籍信息卡 -->
|
||||
<view class="book-info-card card-gradient">
|
||||
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}">
|
||||
<view class="book-icon">
|
||||
<view class="book-icon-inner">📚</view>
|
||||
</view>
|
||||
@@ -33,9 +39,34 @@
|
||||
</view>
|
||||
|
||||
<!-- 目录内容 -->
|
||||
<view class="chapters-content">
|
||||
<!-- 序言 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="preface">
|
||||
<view class="chapters-content" wx:if="{{!partsLoading}}">
|
||||
<!-- 每日新增(最近更新章节快捷入口) -->
|
||||
<view class="daily-section" wx:if="{{dailyChapters.length > 0}}">
|
||||
<view class="daily-header">
|
||||
<text class="daily-title">每日新增</text>
|
||||
<text class="daily-badge">+{{dailyChapters.length}}</text>
|
||||
</view>
|
||||
<view class="daily-list">
|
||||
<view
|
||||
class="daily-item"
|
||||
wx:for="{{dailyChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="daily-dot"></view>
|
||||
<view class="daily-content">
|
||||
<text class="daily-item-title">{{item.title}}</text>
|
||||
<text class="daily-item-meta">{{item.dateStr}} · ¥{{item.price}}</text>
|
||||
</view>
|
||||
<text class="daily-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 序言(优先传 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">📖</view>
|
||||
<text class="item-title">序言|为什么我每天早上6点在Soul开播?</text>
|
||||
@@ -59,27 +90,30 @@
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-right">
|
||||
<text class="part-count">{{item.chapters.length}}章</text>
|
||||
<text class="part-count">{{item.chapters.length || item.chapterCount}}章</text>
|
||||
<text class="part-arrow {{expandedPart === item.id ? 'arrow-down' : ''}}">→</text>
|
||||
</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">
|
||||
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<text class="section-lock {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? 'lock-open' : 'lock-closed'}}">{{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '○' : '●'}}</text>
|
||||
<text class="section-title {{section.isFree || hasFullBook || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
|
||||
<text class="section-lock {{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? 'lock-open' : 'lock-closed'}}">{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? '○' : '●'}}</text>
|
||||
<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}}" class="tag tag-new">NEW</text>
|
||||
<text wx:if="{{section.isPremium}}" class="tag tag-vip">增值</text>
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
<text wx:elif="{{hasFullBook || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已购</text>
|
||||
<text wx:elif="{{isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已解锁</text>
|
||||
<text wx:elif="{{auditMode}}" class="tag tag-purchased">待开放</text>
|
||||
<text wx:else class="section-price">¥{{section.price}}</text>
|
||||
<text class="section-arrow">›</text>
|
||||
</view>
|
||||
@@ -92,8 +126,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 尾声 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="epilogue">
|
||||
<!-- 尾声(优先传 mid) -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="epilogue" data-mid="{{fixedSectionsMap.epilogue}}">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand">📖</view>
|
||||
<text class="item-title">尾声|这本书的真实目的</text>
|
||||
@@ -114,6 +148,7 @@
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<text class="appendix-text">{{item.title}}</text>
|
||||
<text class="appendix-arrow">→</text>
|
||||
|
||||
@@ -75,6 +75,34 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 目录加载中 ===== */
|
||||
.parts-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.parts-loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-top-color: #00CED1;
|
||||
border-radius: 50%;
|
||||
animation: parts-spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
.parts-loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
@keyframes parts-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ===== 书籍信息卡 ===== */
|
||||
.book-info-card {
|
||||
display: flex;
|
||||
@@ -146,6 +174,89 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 每日新增 ===== */
|
||||
.daily-section {
|
||||
margin-bottom: 32rpx;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.daily-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.daily-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.daily-badge {
|
||||
font-size: 22rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
background: #F6AD55;
|
||||
color: #ffffff;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
.daily-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.daily-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.daily-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.daily-dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 206, 209, 0.6);
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.daily-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4rpx;
|
||||
}
|
||||
|
||||
.daily-item-title {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.daily-item-meta {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.daily-arrow {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 章节项 ===== */
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
@@ -232,6 +343,14 @@
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.tag-vip {
|
||||
background: rgba(255, 215, 0, 0.12);
|
||||
color: #FFD700;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.text-brand {
|
||||
color: #00CED1;
|
||||
}
|
||||
@@ -331,6 +450,12 @@
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.chapters-loading {
|
||||
padding: 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.chapter-group {
|
||||
background: rgba(28, 28, 30, 0.5);
|
||||
border-radius: 16rpx;
|
||||
|
||||
117
miniprogram/pages/gift-pay/detail.js
Normal file
117
miniprogram/pages/gift-pay/detail.js
Normal file
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Soul创业派对 - 代付详情页
|
||||
* 好友打开后看到订单信息,点击「帮他付款」完成代付
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
requestSn: '',
|
||||
detail: null,
|
||||
loading: true,
|
||||
paying: false,
|
||||
isInitiator: false // 是否发起人,发起人看到「分享给好友」UI,好友看到「帮他付款」
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
const requestSn = (options.requestSn || '').trim()
|
||||
if (!requestSn) {
|
||||
wx.showToast({ title: '代付链接无效', icon: 'none' })
|
||||
setTimeout(() => wx.switchTab({ url: '/pages/index/index' }), 1500)
|
||||
return
|
||||
}
|
||||
this.setData({ requestSn })
|
||||
this.loadDetail()
|
||||
},
|
||||
|
||||
async loadDetail() {
|
||||
const { requestSn } = this.data
|
||||
if (!requestSn) return
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/gift-pay/detail?requestSn=${encodeURIComponent(requestSn)}`)
|
||||
if (res && res.success) {
|
||||
const myId = app.globalData.userInfo?.id || ''
|
||||
const isInitiator = !!myId && res.initiatorUserId === myId
|
||||
this.setData({ detail: res, loading: false, isInitiator })
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: res?.error || '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async doPay() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => wx.switchTab({ url: '/pages/my/my' }), 1500)
|
||||
return
|
||||
}
|
||||
const openId = app.globalData.openId || ''
|
||||
if (!openId) {
|
||||
wx.showToast({ title: '请先完成微信授权', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const { requestSn, detail } = this.data
|
||||
if (!requestSn || !detail) return
|
||||
|
||||
this.setData({ paying: true })
|
||||
wx.showLoading({ title: '创建订单中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/gift-pay/pay',
|
||||
method: 'POST',
|
||||
data: {
|
||||
requestSn,
|
||||
openId,
|
||||
userId: app.globalData.userInfo?.id || ''
|
||||
}
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (!res || !res.success || !res.data?.payParams) {
|
||||
throw new Error(res?.error || '创建订单失败')
|
||||
}
|
||||
const payParams = res.data.payParams
|
||||
payParams._orderSn = res.data.orderSn
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
...payParams,
|
||||
signType: payParams.signType || 'MD5',
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
wx.showToast({ title: '代付成功', icon: 'success' })
|
||||
this.setData({ paying: false })
|
||||
setTimeout(() => {
|
||||
wx.navigateBack({ fail: () => wx.switchTab({ url: '/pages/index/index' }) })
|
||||
}, 1500)
|
||||
} catch (e) {
|
||||
this.setData({ paying: false })
|
||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消支付', icon: 'none' })
|
||||
} else {
|
||||
wx.showToast({ title: e.message || e.error || '支付失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
app.goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const { requestSn } = this.data
|
||||
return {
|
||||
title: '好友请你帮忙代付 - Soul创业派对',
|
||||
path: requestSn ? `/pages/gift-pay/detail?requestSn=${requestSn}` : '/pages/gift-pay/detail'
|
||||
}
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/gift-pay/detail.json
Normal file
4
miniprogram/pages/gift-pay/detail.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {}
|
||||
}
|
||||
74
miniprogram/pages/gift-pay/detail.wxml
Normal file
74
miniprogram/pages/gift-pay/detail.wxml
Normal file
@@ -0,0 +1,74 @@
|
||||
<!-- Soul创业派对 - 代付详情页(美团式:发起人看到分享入口,好友看到帮他付款) -->
|
||||
<view class="page">
|
||||
<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-title">{{isInitiator ? '找朋友代付' : '帮他付款'}}</text>
|
||||
</view>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="content" style="padding-top: calc({{statusBarHeight}}px + 88rpx);">
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="loading-box">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{detail}}">
|
||||
<!-- 营销:章节标题+内容预览,吸引代付人 -->
|
||||
<view class="article-preview" wx:if="{{detail.sectionTitle || detail.contentPreview}}">
|
||||
<text class="article-title">{{detail.sectionTitle || detail.description || '代付商品'}}</text>
|
||||
<text class="article-content" wx:if="{{detail.contentPreview}}">{{detail.contentPreview}}</text>
|
||||
</view>
|
||||
<view class="card">
|
||||
<view class="card-header">
|
||||
<view class="card-badge">代付订单</view>
|
||||
<text class="initiator" wx:if="{{!isInitiator}}">{{detail.initiatorNickname || '好友'}} 请你帮忙付款</text>
|
||||
<text class="initiator" wx:else>分享给好友,好友帮你付款</text>
|
||||
</view>
|
||||
<view class="card-divider"></view>
|
||||
<view class="card-body">
|
||||
<view class="row product-row" wx:if="{{!detail.contentPreview}}">
|
||||
<text class="label">商品</text>
|
||||
<text class="value product-desc">{{detail.sectionTitle || detail.description || '-'}}</text>
|
||||
</view>
|
||||
<view class="row amount-row">
|
||||
<text class="label">金额</text>
|
||||
<text class="amount">¥{{detail.amount ? detail.amount.toFixed(2) : '0.00'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 发起人:分享给好友 -->
|
||||
<block wx:if="{{isInitiator}}">
|
||||
<view class="tips">
|
||||
<text class="tips-icon">💡</text>
|
||||
<text>分享给好友,好友打开后点击「帮他付款」即可为你代付</text>
|
||||
</view>
|
||||
<button class="pay-btn share-btn" open-type="share">
|
||||
<image class="btn-icon-img" src="/assets/icons/share.svg" mode="aspectFit"/>
|
||||
<text>分享给好友</text>
|
||||
</button>
|
||||
</block>
|
||||
<!-- 好友:帮他付款 -->
|
||||
<block wx:else>
|
||||
<view class="tips">
|
||||
<text class="tips-icon">✓</text>
|
||||
<text>付款后,{{detail.initiatorNickname || '好友'}}将获得对应权益</text>
|
||||
</view>
|
||||
<button class="pay-btn" bindtap="doPay" disabled="{{paying}}">
|
||||
{{paying ? '支付中...' : '帮他付款'}}
|
||||
</button>
|
||||
</block>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="empty">
|
||||
<text>代付请求不存在或已处理</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
264
miniprogram/pages/gift-pay/detail.wxss
Normal file
264
miniprogram/pages/gift-pay/detail.wxss
Normal file
@@ -0,0 +1,264 @@
|
||||
/* Soul创业派对 - 代付详情页 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(180deg, #0a0a0a 0%, #000 40%, #000 100%);
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.92);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.nav-back:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 120rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid rgba(0, 206, 209, 0.2);
|
||||
border-top-color: #00CED1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
/* 营销:章节标题+内容预览,与订单卡片统一风格 */
|
||||
.article-preview {
|
||||
background: linear-gradient(145deg, #1a1a1c 0%, #141416 100%);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 16rpx;
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.1);
|
||||
}
|
||||
|
||||
.article-title {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.article-content {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 1.65;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 4;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
/* 订单卡片:与文章预览统一圆角、边距 */
|
||||
.card {
|
||||
background: linear-gradient(145deg, #1a1a1c 0%, #141416 100%);
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
margin-bottom: 24rpx;
|
||||
border: 1rpx solid rgba(0, 206, 209, 0.1);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
padding: 24rpx;
|
||||
}
|
||||
|
||||
.card-badge {
|
||||
display: inline-block;
|
||||
font-size: 22rpx;
|
||||
color: rgba(0, 206, 209, 0.9);
|
||||
background: rgba(0, 206, 209, 0.08);
|
||||
padding: 6rpx 14rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 12rpx;
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
.initiator {
|
||||
display: block;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.4;
|
||||
letter-spacing: 0.3rpx;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
height: 1rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.08), transparent);
|
||||
margin: 0 24rpx;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 20rpx 24rpx 24rpx;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
flex-shrink: 0;
|
||||
width: 80rpx;
|
||||
}
|
||||
|
||||
.product-row .value {
|
||||
flex: 1;
|
||||
text-align: right;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.product-desc {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.amount-row {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.amount-row .amount {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #00CED1;
|
||||
letter-spacing: 1rpx;
|
||||
text-shadow: 0 0 24rpx rgba(0, 206, 209, 0.3);
|
||||
}
|
||||
|
||||
/* 提示文案 */
|
||||
.tips {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10rpx;
|
||||
padding: 0 4rpx 24rpx;
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.tips-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: 28rpx;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
/* 主按钮 */
|
||||
.pay-btn {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
line-height: 96rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #18a8a8 50%, #20B2AA 100%);
|
||||
color: #fff;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 50rpx;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.35);
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
|
||||
.pay-btn:active {
|
||||
opacity: 0.92;
|
||||
transform: scale(0.99);
|
||||
}
|
||||
|
||||
.pay-btn[disabled] {
|
||||
opacity: 0.6;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.btn-icon-img {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 120rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
97
miniprogram/pages/gift-pay/list.js
Normal file
97
miniprogram/pages/gift-pay/list.js
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Soul创业派对 - 我的代付
|
||||
* Tab: 我发起的 / 我帮付的
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
tab: 'requests',
|
||||
requests: [],
|
||||
payments: [],
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.requests.length > 0 || this.data.payments.length > 0) {
|
||||
this.loadData()
|
||||
}
|
||||
},
|
||||
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab || 'requests'
|
||||
this.setData({ tab })
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
const userId = app.globalData.userInfo?.id || ''
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
if (this.data.tab === 'requests') {
|
||||
const res = await app.request(`/api/miniprogram/gift-pay/my-requests?userId=${encodeURIComponent(userId)}`)
|
||||
this.setData({ requests: (res && res.list) || [], loading: false })
|
||||
} else {
|
||||
const res = await app.request(`/api/miniprogram/gift-pay/my-payments?userId=${encodeURIComponent(userId)}`)
|
||||
this.setData({ payments: (res && res.list) || [], loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goToDetail(e) {
|
||||
const requestSn = e.currentTarget.dataset.sn
|
||||
if (requestSn) {
|
||||
wx.navigateTo({ url: `/pages/gift-pay/detail?requestSn=${encodeURIComponent(requestSn)}` })
|
||||
}
|
||||
},
|
||||
|
||||
shareRequest(e) {
|
||||
e.stopPropagation()
|
||||
wx.showToast({ title: '请点击右上角「...」分享给好友', icon: 'none', duration: 2500 })
|
||||
},
|
||||
|
||||
async cancelRequest(e) {
|
||||
e.stopPropagation()
|
||||
const requestSn = e.currentTarget.dataset.sn
|
||||
if (!requestSn) return
|
||||
const ok = await new Promise(r => {
|
||||
wx.showModal({ title: '取消代付', content: '确定取消该代付请求?', success: res => r(res.confirm) })
|
||||
})
|
||||
if (!ok) return
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/gift-pay/cancel',
|
||||
method: 'POST',
|
||||
data: { requestSn, userId: app.globalData.userInfo?.id }
|
||||
})
|
||||
if (res && res.success) {
|
||||
wx.showToast({ title: '已取消', icon: 'success' })
|
||||
this.loadData()
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '取消失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '取消失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
app.goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return { title: '我的代付 - Soul创业派对', path: '/pages/gift-pay/list' }
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/gift-pay/list.json
Normal file
4
miniprogram/pages/gift-pay/list.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationStyle": "custom",
|
||||
"usingComponents": {}
|
||||
}
|
||||
64
miniprogram/pages/gift-pay/list.wxml
Normal file
64
miniprogram/pages/gift-pay/list.wxml
Normal file
@@ -0,0 +1,64 @@
|
||||
<!-- Soul创业派对 - 我的代付 -->
|
||||
<view class="page">
|
||||
<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-title">我的代付</text>
|
||||
</view>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="tabs" style="padding-top: calc({{statusBarHeight}}px + 88rpx);">
|
||||
<view class="tab {{tab === 'requests' ? 'active' : ''}}" data-tab="requests" bindtap="switchTab">我发起的</view>
|
||||
<view class="tab {{tab === 'payments' ? 'active' : ''}}" data-tab="payments" bindtap="switchTab">我帮付的</view>
|
||||
</view>
|
||||
|
||||
<view class="content">
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="loading-box">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:elif="{{tab === 'requests'}}">
|
||||
<block wx:if="{{requests.length === 0}}">
|
||||
<view class="empty">暂无发起的代付</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="card" wx:for="{{requests}}" wx:key="requestSn" bindtap="goToDetail" data-sn="{{item.requestSn}}">
|
||||
<view class="card-row">
|
||||
<text class="desc">{{item.description}}</text>
|
||||
<text class="amount">¥{{item.amount}}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="status {{item.status}}">{{item.status === 'pending' ? '待支付' : item.status === 'paid' ? '已支付' : item.status === 'cancelled' ? '已取消' : '已过期'}}</text>
|
||||
<view class="actions" wx:if="{{item.status === 'pending'}}">
|
||||
<text class="action-text" bindtap="shareRequest" data-sn="{{item.requestSn}}">分享</text>
|
||||
<text class="action-text cancel" bindtap="cancelRequest" data-sn="{{item.requestSn}}">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<block wx:if="{{payments.length === 0}}">
|
||||
<view class="empty">暂无帮付记录</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view class="card" wx:for="{{payments}}" wx:key="requestSn" bindtap="goToDetail" data-sn="{{item.requestSn}}">
|
||||
<view class="card-row">
|
||||
<text class="desc">{{item.description}}</text>
|
||||
<text class="amount">¥{{item.amount}}</text>
|
||||
</view>
|
||||
<view class="card-row">
|
||||
<text class="status {{item.status}}">{{item.status === 'paid' ? '已支付' : item.status}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
160
miniprogram/pages/gift-pay/list.wxss
Normal file
160
miniprogram/pages/gift-pay/list.wxss
Normal file
@@ -0,0 +1,160 @@
|
||||
/* Soul创业派对 - 我的代付 */
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: #1c1c1e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
padding: 24rpx 32rpx;
|
||||
gap: 24rpx;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.tab {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 20rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
border-radius: 12rpx;
|
||||
background: #1c1c1e;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border: 4rpx solid rgba(0, 206, 209, 0.3);
|
||||
border-top-color: #00CED1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.empty {
|
||||
text-align: center;
|
||||
padding: 80rpx 0;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 16rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.card-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.card-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.desc {
|
||||
font-size: 28rpx;
|
||||
color: #fff;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.status {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.status.paid {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 26rpx;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.action-text.cancel {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
@@ -23,8 +23,12 @@ Page({
|
||||
totalSections: 62,
|
||||
bookData: [],
|
||||
|
||||
// 推荐章节:以服务端推荐算法为准,不再预置写死内容
|
||||
featuredSections: [],
|
||||
// 推荐章节
|
||||
featuredSections: [
|
||||
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', tag: '免费', tagClass: 'tag-free', part: '真实的人' },
|
||||
{ id: '3.1', title: '3000万流水如何跑出来', tag: '热门', tagClass: 'tag-pink', part: '真实的行业' },
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
|
||||
],
|
||||
|
||||
// 最新章节(动态计算)
|
||||
latestSection: null,
|
||||
@@ -43,8 +47,9 @@ Page({
|
||||
superMembers: [],
|
||||
superMembersLoading: true,
|
||||
|
||||
// 最新新增章节
|
||||
// 最新新增章节(完整列表 + 展示列表,用于展开/折叠)
|
||||
latestChapters: [],
|
||||
displayLatestChapters: [],
|
||||
|
||||
// 篇章数(从 bookData 计算)
|
||||
partCount: 0,
|
||||
@@ -52,9 +57,21 @@ Page({
|
||||
// 加载状态
|
||||
loading: true,
|
||||
|
||||
// 审核模式
|
||||
auditMode: false,
|
||||
|
||||
// 链接卡若 - 留资弹窗
|
||||
showLeadModal: false,
|
||||
leadPhone: ''
|
||||
leadPhone: '',
|
||||
|
||||
// 展开状态(首页精选/最新)
|
||||
featuredExpanded: false,
|
||||
latestExpanded: false,
|
||||
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
|
||||
featuredExpandedLoading: false,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -73,6 +90,7 @@ Page({
|
||||
}
|
||||
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.loadFeatureConfig()
|
||||
this.initData()
|
||||
},
|
||||
|
||||
@@ -100,6 +118,9 @@ Page({
|
||||
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
|
||||
}
|
||||
|
||||
// 同步审核模式
|
||||
this.setData({ auditMode: app.globalData.auditMode })
|
||||
|
||||
// 更新用户状态
|
||||
this.updateUserStatus()
|
||||
},
|
||||
@@ -176,7 +197,25 @@ Page({
|
||||
}
|
||||
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
|
||||
|
||||
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
|
||||
// 兜底:无 recommended 时从 book/hot 取前3
|
||||
if (featured.length === 0) {
|
||||
try {
|
||||
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
|
||||
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
|
||||
if (hotList.length > 0) {
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
featured = hotList.slice(0, 3).map((s, i) => ({
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
|
||||
part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
}))
|
||||
this.setData({ featuredSections: featured })
|
||||
}
|
||||
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
|
||||
}
|
||||
if (featured.length === 0) {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
@@ -201,10 +240,14 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 最新更新:用 book/latest-chapters 取第1条
|
||||
// 2. 最新更新:用 book/latest-chapters 取第1条(排除「序言」「尾声」「附录」)
|
||||
try {
|
||||
const latestRes = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
|
||||
const latestList = (latestRes && latestRes.data) ? latestRes.data : []
|
||||
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
|
||||
const latestList = rawList.filter(l => {
|
||||
const pt = (l.part_title || l.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (latestList.length > 0) {
|
||||
const l = latestList[0]
|
||||
this.setData({
|
||||
@@ -275,8 +318,26 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
return
|
||||
}
|
||||
const res = await app.request({ url: '/api/miniprogram/config', silent: true })
|
||||
const features = (res && res.features) || {}
|
||||
const searchEnabled = features.searchEnabled !== false
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
if (!this.data.searchEnabled) return
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
@@ -297,10 +358,6 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
goToAbout() {
|
||||
wx.navigateTo({ url: '/pages/about/about' })
|
||||
},
|
||||
|
||||
async onLinkKaruo() {
|
||||
const app = getApp()
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
@@ -316,7 +373,12 @@ Page({
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const leadKey = 'karuo_lead_' + userId
|
||||
// 2 分钟内只能点一次(与后端限频一致)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
@@ -329,15 +391,10 @@ Page({
|
||||
} catch (e) {}
|
||||
}
|
||||
if (phone || wechatId) {
|
||||
const hasLead = wx.getStorageSync(leadKey)
|
||||
if (hasLead) {
|
||||
wx.showToast({ title: '已提交联系方式,卡若会尽快联系你', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/ckb/lead',
|
||||
url: '/api/miniprogram/ckb/index-lead',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
@@ -348,7 +405,7 @@ Page({
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync(leadKey, true)
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
@@ -366,38 +423,91 @@ Page({
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
},
|
||||
|
||||
// 阻止弹窗内部点击事件冒泡到遮罩层
|
||||
stopPropagation() {},
|
||||
|
||||
onLeadPhoneInput(e) {
|
||||
this.setData({ leadPhone: (e.detail.value || '').trim() })
|
||||
},
|
||||
|
||||
async submitLead() {
|
||||
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
|
||||
if (!phone) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
// 一键获取手机号(微信能力),成功后直接提交链接卡若
|
||||
async onGetPhoneNumberForLead(e) {
|
||||
if (e.detail.errMsg !== 'getPhoneNumber:ok') {
|
||||
wx.showToast({ title: '未获取到手机号,请手动输入', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (phone.length < 11) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
const code = e.detail.code
|
||||
if (!code) {
|
||||
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const app = getApp()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
wx.showLoading({ title: '获取中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/phone',
|
||||
method: 'POST',
|
||||
data: { code, userId }
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success && res.phoneNumber) {
|
||||
await this._submitLeadWithPhone(res.phoneNumber)
|
||||
} else {
|
||||
wx.showToast({ title: '获取失败,请手动输入', icon: 'none' })
|
||||
}
|
||||
} catch (err) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: err.message || '获取失败,请手动输入', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 内部:用手机号提交链接卡若(一键获取与手动输入共用)
|
||||
async _submitLeadWithPhone(phone) {
|
||||
const p = (phone || '').trim().replace(/\s/g, '')
|
||||
if (!p || p.length < 11) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const app = getApp()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const leadKey = userId ? ('karuo_lead_' + userId) : ''
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/ckb/lead',
|
||||
url: '/api/miniprogram/ckb/index-lead',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
phone,
|
||||
name: (app.globalData.userInfo?.nickname || '').trim() || undefined
|
||||
}
|
||||
phone: p,
|
||||
name: (app.globalData.userInfo?.nickname || '').trim() || undefined,
|
||||
},
|
||||
})
|
||||
wx.hideLoading()
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
if (res && res.success) {
|
||||
if (leadKey) wx.setStorageSync(leadKey, true)
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
// 同步手机号到用户资料
|
||||
try {
|
||||
if (userId) {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId, phone: p },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.phone = p
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.setStorageSync('user_phone', p)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Index] 同步手机号到用户资料失败:', e && e.message)
|
||||
}
|
||||
wx.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
@@ -408,58 +518,87 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async submitLead() {
|
||||
const phone = (this.data.leadPhone || '').trim().replace(/\s/g, '')
|
||||
if (!phone) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this._submitLeadWithPhone(phone)
|
||||
},
|
||||
|
||||
goToSuperList() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 精选推荐:展开/折叠
|
||||
async toggleFeaturedExpanded() {
|
||||
if (this.data.featuredExpandedLoading) return
|
||||
if (this.data.featuredExpanded) {
|
||||
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
|
||||
this.setData({ featuredExpanded: false, featuredSections: collapsed })
|
||||
return
|
||||
}
|
||||
if (this.data.featuredSectionsFull.length > 0) {
|
||||
this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull })
|
||||
return
|
||||
}
|
||||
this.setData({ featuredExpandedLoading: true })
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
|
||||
const list = (res && res.data) ? res.data : []
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
const full = list.map((s, i) => ({
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
|
||||
part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i % 3] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec'
|
||||
}))
|
||||
this.setData({
|
||||
featuredSectionsFull: full,
|
||||
featuredSections: full,
|
||||
featuredExpanded: true,
|
||||
featuredExpandedLoading: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[Index] 加载精选更多失败:', e)
|
||||
this.setData({ featuredExpandedLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
|
||||
toggleLatestExpanded() {
|
||||
const expanded = !this.data.latestExpanded
|
||||
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
|
||||
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
|
||||
},
|
||||
|
||||
// 最新新增:用 latest-chapters 接口(后端按 updated_at 取前 N 条),不拉全量,支持万级文章
|
||||
async loadLatestChapters() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const res = await app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true })
|
||||
const list = (res && res.data) ? res.data : []
|
||||
const pt = (c) => (c.partTitle || c.part_title || '').toLowerCase()
|
||||
const exclude = c => !pt(c).includes('序言') && !pt(c).includes('尾声') && !pt(c).includes('附录')
|
||||
// stitch_soul:优先取 isNew 标记的章节;若无则取最近更新的前 10 章(排除序言/尾声/附录)
|
||||
let candidates = chapters.filter(c => (c.isNew || c.is_new) === true && exclude(c))
|
||||
if (candidates.length === 0) {
|
||||
candidates = chapters.filter(exclude)
|
||||
}
|
||||
// 解析「第X场」用于倒序,最新(场次大)放在最上方
|
||||
const sessionNum = (c) => {
|
||||
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
|
||||
const m = title.match(/第\s*(\d+)\s*场/) || title.match(/第(\d+)场/)
|
||||
if (m) return parseInt(m[1], 10)
|
||||
const id = c.id != null ? String(c.id) : ''
|
||||
if (/^\d+$/.test(id)) return parseInt(id, 10)
|
||||
return 0
|
||||
}
|
||||
const latest = candidates
|
||||
.sort((a, b) => {
|
||||
const na = sessionNum(a)
|
||||
const nb = sessionNum(b)
|
||||
if (na !== nb) return nb - na // 场次倒序:最新在上
|
||||
return new Date(b.updatedAt || b.updated_at || 0) - new Date(a.updatedAt || a.updated_at || 0)
|
||||
})
|
||||
.slice(0, 10)
|
||||
const latest = list
|
||||
.filter(exclude)
|
||||
.slice(0, 20)
|
||||
.map(c => {
|
||||
const d = new Date(c.updatedAt || c.updated_at || Date.now())
|
||||
const title = c.section_title || c.sectionTitle || c.title || c.chapterTitle || ''
|
||||
const rawContent = (c.content || '').replace(/<[^>]+>/g, '').trim()
|
||||
// 描述仅用正文摘要,避免 #id 或标题重复;截取 36 字
|
||||
let desc = ''
|
||||
if (rawContent && rawContent.length > 0) {
|
||||
const clean = rawContent.replace(/^#[\d.]+\s*/, '').trim()
|
||||
desc = clean.length > 36 ? clean.slice(0, 36) + '...' : clean
|
||||
}
|
||||
return {
|
||||
id: c.id,
|
||||
mid: c.mid ?? c.MID ?? 0,
|
||||
title,
|
||||
desc,
|
||||
desc: '', // latest-chapters 不返回 content,避免大表全量加载
|
||||
price: c.price ?? 1,
|
||||
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
this.setData({ latestChapters: latest })
|
||||
const display = this.data.latestExpanded ? latest : latest.slice(0, 5)
|
||||
this.setData({ latestChapters: latest, displayLatestChapters: display })
|
||||
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
|
||||
},
|
||||
|
||||
|
||||
@@ -24,8 +24,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" bindtap="goToSearch">
|
||||
<!-- 搜索栏(根据配置显示) -->
|
||||
<view class="search-bar" wx:if="{{searchEnabled}}" bindtap="goToSearch">
|
||||
<view class="search-icon-wrap">
|
||||
<text class="search-icon-text">🔍</text>
|
||||
</view>
|
||||
@@ -52,6 +52,19 @@
|
||||
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读进度(设计稿:最新更新→阅读进度→超级个体) -->
|
||||
<view class="progress-card" wx:if="{{isLoggedIn}}" bindtap="goToChapters">
|
||||
<view class="progress-header">
|
||||
<text class="progress-title">阅读进度</text>
|
||||
<text class="progress-count">已读 {{readCount}}/{{totalSections}}</text>
|
||||
</view>
|
||||
<view class="progress-bar-wrapper">
|
||||
<view class="progress-bar-bg">
|
||||
<view class="progress-bar-fill" style="width: {{readCount && totalSections ? (readCount / totalSections * 100) : 0}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
@@ -87,14 +100,18 @@
|
||||
<!-- 已加载无数据 -->
|
||||
<view wx:else class="super-empty">
|
||||
<text class="super-empty-text">成为会员,展示你的项目</text>
|
||||
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
|
||||
<view class="super-empty-btn" bindtap="goToVip" wx:if="{{!auditMode}}">加入创业派对 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选推荐(带 tag,已去掉「查看全部」) -->
|
||||
<!-- 精选推荐(带 tag,支持展开更多) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选推荐</text>
|
||||
<view class="section-more" wx:if="{{featuredSections.length > 0}}" bindtap="toggleFeaturedExpanded">
|
||||
<text class="more-text">{{featuredExpandedLoading ? '加载中...' : (featuredExpanded ? '收起' : '展开更多')}}</text>
|
||||
<text class="more-arrow">{{featuredExpanded ? '▲' : '▼'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
@@ -117,18 +134,24 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新新增(时间线样式) -->
|
||||
<!-- 最新新增(时间线样式,支持展开更多) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header latest-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
<view class="section-header-right">
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
</view>
|
||||
<view class="section-more" wx:if="{{latestChapters.length > 5}}" bindtap="toggleLatestExpanded">
|
||||
<text class="more-text">{{latestExpanded ? '收起' : '展开更多'}}</text>
|
||||
<text class="more-arrow">{{latestExpanded ? '▲' : '▼'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="timeline-wrap">
|
||||
<view class="timeline-line"></view>
|
||||
<view class="timeline-list">
|
||||
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{latestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
|
||||
<view class="timeline-item {{index === 0 ? 'timeline-item-first' : ''}}" wx:for="{{displayLatestChapters}}" wx:key="id" bindtap="goToRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
|
||||
<view class="timeline-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-row">
|
||||
@@ -150,12 +173,17 @@
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
|
||||
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时) -->
|
||||
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时):一键获取 + 手动输入 -->
|
||||
<view class="lead-mask" wx:if="{{showLeadModal}}" catchtap="closeLeadModal">
|
||||
<view class="lead-box" catchtap="">
|
||||
<!-- 使用 catchtap="stopPropagation" 阻止内部点击冒泡到遮罩层,避免点击输入框时弹窗被关闭 -->
|
||||
<view class="lead-box" catchtap="stopPropagation">
|
||||
<text class="lead-title">留下联系方式</text>
|
||||
<text class="lead-desc">方便卡若与您联系</text>
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
<button class="lead-get-phone-btn" open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumberForLead">一键获取手机号</button>
|
||||
<text class="lead-divider">或手动输入</text>
|
||||
<view class="lead-input-wrap">
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
</view>
|
||||
<view class="lead-actions">
|
||||
<button class="lead-btn lead-btn-cancel" bindtap="closeLeadModal">取消</button>
|
||||
<button class="lead-btn lead-btn-submit" bindtap="submitLead">提交</button>
|
||||
|
||||
@@ -445,6 +445,7 @@
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin-top: 8rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 内容概览列表 ===== */
|
||||
@@ -688,6 +689,12 @@
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.section-header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.daily-badge-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -838,6 +845,64 @@
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
/* 展开/收起按钮 */
|
||||
.expand-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 20rpx 0;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.expand-text {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.expand-icon {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 热度排行 */
|
||||
.hot-list {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.hot-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 28rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.hot-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.hot-rank {
|
||||
width: 48rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.hot-rank-top {
|
||||
color: #38bdac;
|
||||
}
|
||||
.hot-title {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.hot-price {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
@@ -877,19 +942,42 @@
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #A0AEC0;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.lead-get-phone-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: rgba(56, 189, 172, 0.2);
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.5);
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
color: #38bdac;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
line-height: normal;
|
||||
}
|
||||
.lead-get-phone-btn::after { border: none; }
|
||||
.lead-divider {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #6B7280;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.lead-input-wrap {
|
||||
padding: 16rpx 24rpx;
|
||||
background: #0a1628;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.lead-input {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
background: #0a1628;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
box-sizing: border-box;
|
||||
font-size: 30rpx;
|
||||
color: #ffffff;
|
||||
margin-bottom: 32rpx;
|
||||
background: transparent;
|
||||
}
|
||||
.lead-actions {
|
||||
display: flex;
|
||||
@@ -898,7 +986,11 @@
|
||||
.lead-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
/* 使用 flex 垂直居中文本,避免小程序默认 padding 导致按钮文字下沉 */
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
|
||||
64
miniprogram/pages/link-preview/link-preview.js
Normal file
64
miniprogram/pages/link-preview/link-preview.js
Normal file
@@ -0,0 +1,64 @@
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
url: '',
|
||||
title: '链接预览',
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
loadError: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const url = decodeURIComponent(options.url || '')
|
||||
const title = options.title ? decodeURIComponent(options.title) : '链接预览'
|
||||
this.setData({
|
||||
url,
|
||||
title,
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
navBarHeight: app.globalData.navBarHeight || 88,
|
||||
})
|
||||
},
|
||||
|
||||
onWebViewError() {
|
||||
this.setData({ loadError: true })
|
||||
wx.showModal({
|
||||
title: '无法在小程序内打开',
|
||||
content: '该链接无法在小程序内预览,是否复制链接到浏览器打开?',
|
||||
confirmText: '复制链接',
|
||||
cancelText: '返回',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: this.data.url,
|
||||
success: () => wx.showToast({ title: '链接已复制,请在浏览器打开', icon: 'none', duration: 2000 }),
|
||||
})
|
||||
} else {
|
||||
this.goBack()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
const pages = getCurrentPages()
|
||||
if (pages.length > 1) {
|
||||
wx.navigateBack()
|
||||
} else {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}
|
||||
},
|
||||
|
||||
copyLink() {
|
||||
const url = (this.data.url || '').trim()
|
||||
if (!url) {
|
||||
wx.showToast({ title: '暂无可复制链接', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.setClipboardData({
|
||||
data: url,
|
||||
success: () => wx.showToast({ title: '链接已复制', icon: 'none' }),
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
4
miniprogram/pages/link-preview/link-preview.json
Normal file
4
miniprogram/pages/link-preview/link-preview.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
34
miniprogram/pages/link-preview/link-preview.wxml
Normal file
34
miniprogram/pages/link-preview/link-preview.wxml
Normal file
@@ -0,0 +1,34 @@
|
||||
<view class="page">
|
||||
<!-- 简单自定义导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content" style="height: {{navBarHeight - statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<text class="back-arrow">←</text>
|
||||
</view>
|
||||
<view class="nav-title">
|
||||
<text class="nav-title-text">{{title}}</text>
|
||||
</view>
|
||||
<view class="nav-actions">
|
||||
<view class="copy-btn" bindtap="copyLink">
|
||||
<text class="copy-text">复制链接</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{navBarHeight}}px;"></view>
|
||||
|
||||
<!-- 链接预览区域 -->
|
||||
<view class="webview-wrap" wx:if="{{url && !loadError}}">
|
||||
<web-view src="{{url}}" binderror="onWebViewError"></web-view>
|
||||
</view>
|
||||
<view class="error-wrap" wx:elif="{{loadError}}">
|
||||
<text class="error-text">该链接无法在小程序内预览</text>
|
||||
<view class="error-btn" bindtap="copyLink">
|
||||
<text class="error-btn-text">复制链接到浏览器打开</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty-wrap" wx:else>
|
||||
<text class="empty-text">暂无链接地址</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
107
miniprogram/pages/link-preview/link-preview.wxss
Normal file
107
miniprogram/pages/link-preview/link-preview.wxss
Normal file
@@ -0,0 +1,107 @@
|
||||
.page {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background-color: #000;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 40rpx;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-title-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.nav-actions {
|
||||
min-width: 120rpx;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.copy-btn {
|
||||
padding: 6rpx 10rpx;
|
||||
border-radius: 999rpx;
|
||||
border: 1px solid #444;
|
||||
}
|
||||
|
||||
.copy-text {
|
||||
font-size: 22rpx;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.webview-wrap {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
web-view {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.empty-wrap {
|
||||
padding: 40rpx;
|
||||
text-align: center;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.error-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 80rpx 40rpx;
|
||||
}
|
||||
|
||||
.error-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.error-btn {
|
||||
padding: 16rpx 40rpx;
|
||||
border-radius: 999rpx;
|
||||
background: #38bdac;
|
||||
}
|
||||
|
||||
.error-btn-text {
|
||||
font-size: 26rpx;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
|
||||
// 默认匹配类型配置
|
||||
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
||||
@@ -74,13 +75,16 @@ Page({
|
||||
|
||||
// 匹配价格(可配置)
|
||||
matchPrice: 1,
|
||||
extraMatches: 0
|
||||
extraMatches: 0,
|
||||
|
||||
auditMode: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
auditMode: app.globalData.auditMode
|
||||
})
|
||||
this.loadMatchConfig()
|
||||
this.loadStoredContact()
|
||||
@@ -511,6 +515,8 @@ Page({
|
||||
}
|
||||
}
|
||||
})
|
||||
// 匹配后规则:引导填写 MBTI/行业信息
|
||||
checkAndExecute('after_match', this)
|
||||
} catch (e) {
|
||||
console.log('上报匹配失败:', e)
|
||||
}
|
||||
@@ -744,9 +750,9 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 打开设置
|
||||
// 打开资料修改页(找伙伴右上角图标)
|
||||
openSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
|
||||
@@ -4,9 +4,7 @@
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-settings" bindtap="openSettings">
|
||||
<text class="settings-icon">⚙️</text>
|
||||
</view>
|
||||
<view class="nav-left-placeholder"></view>
|
||||
<text class="nav-title">找伙伴</text>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
@@ -17,11 +15,15 @@
|
||||
<view style="height: 30rpx;"></view>
|
||||
|
||||
<!-- 匹配提示条 - 简化显示 -->
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook && !auditMode}}">
|
||||
<text class="tip-icon">⚡</text>
|
||||
<text class="tip-text">今日免费次数已用完</text>
|
||||
<view class="tip-btn" bindtap="showUnlockModal">购买次数</view>
|
||||
</view>
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook && auditMode}}">
|
||||
<text class="tip-icon">⚡</text>
|
||||
<text class="tip-text">今日免费次数已用完,明天再来</text>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
@@ -37,11 +39,16 @@
|
||||
<view class="inner-sphere sphere-active">
|
||||
<view class="sphere-gradient"></view>
|
||||
<view class="sphere-content">
|
||||
<block wx:if="{{needPayToMatch}}">
|
||||
<block wx:if="{{needPayToMatch && !auditMode}}">
|
||||
<text class="sphere-icon">⚡</text>
|
||||
<text class="sphere-title gold-text">购买次数</text>
|
||||
<text class="sphere-desc">¥1 = 1次匹配</text>
|
||||
</block>
|
||||
<block wx:elif="{{needPayToMatch && auditMode}}">
|
||||
<text class="sphere-icon">⏳</text>
|
||||
<text class="sphere-title">明天再来</text>
|
||||
<text class="sphere-desc">今日次数已用完</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="sphere-icon">👥</text>
|
||||
<text class="sphere-title">开始匹配</text>
|
||||
@@ -298,8 +305,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解锁弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showUnlockModal}}" bindtap="closeUnlockModal">
|
||||
<!-- 解锁弹窗(审核模式不展示) -->
|
||||
<view class="modal-overlay" wx:if="{{showUnlockModal && !auditMode}}" bindtap="closeUnlockModal">
|
||||
<view class="modal-content unlock-modal" catchtap="preventBubble">
|
||||
<view class="unlock-icon">⚡</view>
|
||||
<text class="unlock-title">购买匹配次数</text>
|
||||
|
||||
@@ -27,15 +27,9 @@
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.nav-settings {
|
||||
.nav-left-placeholder {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #1c1c1e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
@@ -51,10 +45,6 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.settings-icon {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ Page({
|
||||
}
|
||||
const d = this._getUnlockData(member.id)
|
||||
if (d[field]) return
|
||||
const isVip = app.globalData.hasFullBook
|
||||
const isVip = app.globalData.isVip
|
||||
const usedFree = this._hasUsedFreeForMember(member.id)
|
||||
if (isVip || !usedFree) {
|
||||
this._addUnlock(member.id, field)
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -17,7 +19,7 @@ Page({
|
||||
userInfo: null,
|
||||
|
||||
// 统计数据
|
||||
totalSections: 62,
|
||||
totalSections: 0,
|
||||
readCount: 0,
|
||||
referralCount: 0,
|
||||
earnings: '-',
|
||||
@@ -69,6 +71,15 @@ Page({
|
||||
contactWechat: '',
|
||||
contactSaving: false,
|
||||
pendingWithdraw: false,
|
||||
|
||||
// 我的余额(wallet 页入口展示)
|
||||
walletBalance: 0,
|
||||
|
||||
// 我的代付链接
|
||||
giftList: [],
|
||||
|
||||
// 审核模式
|
||||
auditMode: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -79,9 +90,12 @@ Page({
|
||||
})
|
||||
this.loadFeatureConfig()
|
||||
this.initUserStatus()
|
||||
// 规则引擎:登录后检查(填头像等)
|
||||
checkAndExecute('after_login', this)
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode })
|
||||
// 设置TabBar选中状态(根据 matchEnabled 动态设置)
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
@@ -134,6 +148,8 @@ Page({
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadWalletBalance()
|
||||
this.loadGiftList()
|
||||
} else {
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
@@ -251,6 +267,8 @@ Page({
|
||||
const newList = list.filter(x => x.id !== item.id)
|
||||
this.setData({ pendingConfirmList: newList })
|
||||
this.loadPendingConfirm()
|
||||
this.loadMyEarnings()
|
||||
this.loadWalletBalance()
|
||||
}
|
||||
|
||||
if (hasPackage) {
|
||||
@@ -371,6 +389,8 @@ Page({
|
||||
wx.hideLoading()
|
||||
this.setData({ receivingAll: false })
|
||||
this.loadPendingConfirm()
|
||||
this.loadMyEarnings()
|
||||
this.loadWalletBalance()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -454,8 +474,9 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
// 2. 获取上传后的完整URL(OSS 返回完整 URL,本地返回相对路径)
|
||||
const rawUrl = uploadRes.data.url || ''
|
||||
const avatarUrl = rawUrl.startsWith('http://') || rawUrl.startsWith('https://') ? rawUrl : app.globalData.baseUrl + rawUrl
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
@@ -596,8 +617,19 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 复制用户ID
|
||||
// 复制联系方式:优先复制微信号,其次复制用户ID
|
||||
copyUserId() {
|
||||
const userWechat = (this.data.userWechat || '').trim()
|
||||
if (userWechat) {
|
||||
wx.setClipboardData({
|
||||
data: userWechat,
|
||||
success: () => {
|
||||
wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const userId = this.data.userInfo?.id || ''
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '暂无ID', icon: 'none' })
|
||||
@@ -619,6 +651,23 @@ Page({
|
||||
|
||||
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
|
||||
showLogin() {
|
||||
trackClick('my', 'btn_click', '登录')
|
||||
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || getApp().globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为朋友圈单页,仅支持部分体验。想登录并管理账户,请点击底部「前往小程序」后再操作。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[My] 检测单页模式失败,回退为正常登录弹窗:', e)
|
||||
}
|
||||
try {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
} catch (e) {
|
||||
@@ -650,6 +699,7 @@ Page({
|
||||
|
||||
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
|
||||
async handleWechatLogin() {
|
||||
trackClick('my', 'btn_click', '微信登录')
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
@@ -702,6 +752,7 @@ Page({
|
||||
|
||||
// 点击菜单
|
||||
handleMenuTap(e) {
|
||||
trackClick('my', 'btn_click', e.currentTarget.dataset.id || '菜单')
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
@@ -710,6 +761,7 @@ Page({
|
||||
}
|
||||
|
||||
const routes = {
|
||||
wallet: '/pages/wallet/wallet',
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
@@ -732,6 +784,7 @@ Page({
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
trackClick('my', 'nav_click', '目录')
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
@@ -742,11 +795,13 @@ Page({
|
||||
|
||||
// 跳转到匹配
|
||||
goToMatch() {
|
||||
trackClick('my', 'nav_click', '匹配')
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 跳转到推广中心(需登录)
|
||||
goToReferral() {
|
||||
trackClick('my', 'nav_click', '推广')
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
@@ -754,11 +809,6 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 跳转到找伙伴
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
handleLogout() {
|
||||
wx.showModal({
|
||||
@@ -774,20 +824,65 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// VIP状态查询(hasFullBook 优先,兼容模拟支付等本地已置 VIP 的情况)
|
||||
async loadWalletBalance() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
|
||||
const userId = app.globalData.userInfo.id
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
|
||||
if (res && res.data) {
|
||||
this.setData({ walletBalance: (res.data.balance || 0).toFixed(2) })
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
async loadGiftList() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
|
||||
const userId = app.globalData.userInfo.id
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/balance/gifts?userId=${userId}`, silent: true })
|
||||
if (res?.success && res.data?.gifts) {
|
||||
this.setData({ giftList: res.data.gifts })
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
|
||||
onGiftShareTap(e) {
|
||||
const giftCode = e.currentTarget.dataset.code
|
||||
const title = e.currentTarget.dataset.title || '精选文章'
|
||||
const sectionId = e.currentTarget.dataset.sectionId
|
||||
this._pendingGiftShare = { giftCode, title, sectionId }
|
||||
wx.showModal({
|
||||
title: '分享代付链接',
|
||||
content: `将「${title}」的免费阅读链接分享给好友`,
|
||||
confirmText: '立即分享',
|
||||
cancelText: '取消',
|
||||
success: (r) => {
|
||||
if (r.confirm) {
|
||||
wx.shareAppMessage()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// VIP状态查询(注意:hasFullBook=9.9 买断,不等同 VIP)
|
||||
async loadVipStatus() {
|
||||
if (app.globalData.hasFullBook) {
|
||||
this.setData({ isVip: true, vipExpireDate: this.data.vipExpireDate || '' })
|
||||
}
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true })
|
||||
if (res?.success) {
|
||||
const isVip = !!res.data?.isVip
|
||||
app.globalData.isVip = isVip
|
||||
app.globalData.vipExpireDate = res.data?.expireDate || ''
|
||||
this.setData({
|
||||
isVip: res.data?.isVip || app.globalData.hasFullBook,
|
||||
isVip,
|
||||
vipExpireDate: res.data?.expireDate || this.data.vipExpireDate || ''
|
||||
})
|
||||
// 同步到 storage,便于其他页面复用(注意:hasFullBook=买断,isVip=会员)
|
||||
const userInfo = app.globalData.userInfo || {}
|
||||
userInfo.isVip = isVip
|
||||
userInfo.vipExpireDate = res.data?.expireDate || ''
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
}
|
||||
} catch (e) { console.log('[My] VIP查询失败', e) }
|
||||
},
|
||||
@@ -831,7 +926,8 @@ Page({
|
||||
fail: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
const rawAvatarUrl = uploadRes.data.url || ''
|
||||
const avatarUrl = rawAvatarUrl.startsWith('http://') || rawAvatarUrl.startsWith('https://') ? rawAvatarUrl : app.globalData.baseUrl + rawAvatarUrl
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
@@ -849,12 +945,14 @@ Page({
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
trackClick('my', 'nav_click', 'VIP')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
// 进入个人资料编辑页(stitch_soul)
|
||||
goToProfileEdit() {
|
||||
trackClick('my', 'nav_click', '设置')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
@@ -957,6 +1055,17 @@ Page({
|
||||
stopPropagation() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
if (this._pendingGiftShare) {
|
||||
const { giftCode, title, sectionId } = this._pendingGiftShare
|
||||
this._pendingGiftShare = null
|
||||
const ref = app.getMyReferralCode()
|
||||
let path = `/pages/read/read?id=${sectionId}&gift=${giftCode}`
|
||||
if (ref) path += `&ref=${ref}`
|
||||
return {
|
||||
title: `🎁 好友已为你解锁:${title}`,
|
||||
path
|
||||
}
|
||||
}
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的',
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
<view class="profile-meta">
|
||||
<view class="profile-name-row">
|
||||
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
|
||||
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
|
||||
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" bindtap="goToVip" wx:if="{{!auditMode}}">{{isVip ? '会员中心' : '成为会员'}}</view>
|
||||
</view>
|
||||
<view class="vip-tags">
|
||||
<view class="vip-tags" wx:if="{{!auditMode}}">
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToMatch">匹配</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</text>
|
||||
</view>
|
||||
<text class="user-wechat" bindtap="copyUserId">微信号: {{userWechat || userIdShort || '--'}}</text>
|
||||
<text class="user-wechat" wx:if="{{userWechat}}" bindtap="copyUserId">微信号: {{userWechat}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
@@ -53,10 +53,14 @@
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="goToReferral">
|
||||
<view class="profile-stat" bindtap="goToReferral" wx:if="{{!auditMode}}">
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="handleMenuTap" data-id="wallet" wx:if="{{!auditMode}}">
|
||||
<text class="profile-stat-val">{{walletBalance > 0 ? '¥' + walletBalance : '0'}}</text>
|
||||
<text class="profile-stat-label">我的余额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -64,7 +68,7 @@
|
||||
<!-- 已登录:内容区 -->
|
||||
<view class="main-content" wx:if="{{isLoggedIn}}">
|
||||
<!-- 一键收款(仅在有待确认收款时显示) -->
|
||||
<view class="card receive-card" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<view class="card receive-card" wx:if="{{pendingConfirmList.length > 0 && !auditMode}}">
|
||||
<view class="receive-top">
|
||||
<view class="receive-left">
|
||||
<view class="receive-title-row">
|
||||
@@ -138,9 +142,29 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的代付链接 -->
|
||||
<view class="card gift-card" wx:if="{{giftList.length > 0 && !auditMode}}">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/wallet.svg" mode="aspectFit"/>
|
||||
<text class="card-title">我的代付链接</text>
|
||||
</view>
|
||||
<view class="gift-list">
|
||||
<view class="gift-item" wx:for="{{giftList}}" wx:key="giftCode">
|
||||
<view class="gift-left">
|
||||
<text class="gift-title">{{item.sectionTitle}}</text>
|
||||
<text class="gift-meta">¥{{item.amount}} · {{item.status === 'pending' ? '待领取' : '已领取'}} · {{item.createdAt}}</text>
|
||||
</view>
|
||||
<view class="gift-action" wx:if="{{item.status === 'pending'}}" bindtap="onGiftShareTap" data-code="{{item.giftCode}}" data-title="{{item.sectionTitle}}" data-section-id="{{item.sectionId}}">
|
||||
<text class="gift-share-btn">分享</text>
|
||||
</view>
|
||||
<text class="gift-done" wx:else>已送出</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 关于作者 + 设置 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders" wx:if="{{!auditMode}}">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><image class="menu-icon-img" src="/assets/icons/folder-teal.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
|
||||
@@ -59,6 +59,10 @@
|
||||
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
|
||||
.profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
|
||||
.profile-name-actions { display: flex; align-items: center; gap: 16rpx; flex-shrink: 0; }
|
||||
.profile-edit-btn { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 16rpx; background: rgba(255,255,255,0.08); border-radius: 12rpx; }
|
||||
.profile-edit-icon { width: 28rpx; height: 28rpx; opacity: 0.7; }
|
||||
.profile-edit-text { font-size: 24rpx; color: rgba(255,255,255,0.7); }
|
||||
.user-name {
|
||||
font-size: 44rpx; font-weight: bold; color: #fff;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
|
||||
@@ -178,6 +182,8 @@
|
||||
.icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gray { background: rgba(156,163,175,0.15); }
|
||||
.icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gold { background: rgba(200,161,70,0.2); }
|
||||
.icon-gold .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
|
||||
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
|
||||
|
||||
<text class="doc-section">八、联系我们</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
|
||||
@@ -18,6 +18,7 @@ Page({
|
||||
isVip: false,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
shareCardPath: '', // 分享名片封面图(预生成)
|
||||
mbti: '',
|
||||
mbtiIndex: 0,
|
||||
region: '',
|
||||
@@ -37,10 +38,18 @@ Page({
|
||||
showMbtiPicker: false,
|
||||
saving: false,
|
||||
loading: true,
|
||||
showAvatarModal: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
// 从朋友圈/分享打开且带 id:跳转到名片详情(member-detail)
|
||||
if (options?.id) {
|
||||
const ref = options.ref ? `&ref=${options.ref}` : ''
|
||||
wx.redirectTo({ url: `/pages/member-detail/member-detail?id=${options.id}${ref}` })
|
||||
return
|
||||
}
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
@@ -84,6 +93,7 @@ Page({
|
||||
projectIntro: v('projectIntro'),
|
||||
loading: false,
|
||||
})
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
@@ -94,7 +104,169 @@ Page({
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
// 生成分享名片封面图(参考:头像左+昵称右,分隔线,四栏信息 5:4)
|
||||
async generateShareCard() {
|
||||
const { avatar, nickname, region, mbti, industry, position } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('shareCardCanvas', this)
|
||||
const w = 500
|
||||
const h = 400
|
||||
const pad = 32
|
||||
// 背景(深灰卡片感)
|
||||
const grd = ctx.createLinearGradient(0, 0, w, h)
|
||||
grd.addColorStop(0, '#1E293B')
|
||||
grd.addColorStop(1, '#0F172A')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
// 顶部区域:左头像 + 右昵称
|
||||
const avatarSize = 100
|
||||
const avatarX = pad + 10
|
||||
const avatarY = 50
|
||||
const avatarRadius = avatarSize / 2
|
||||
const rightStart = avatarX + avatarSize + 28
|
||||
const drawAvatar = () => new Promise((resolve) => {
|
||||
if (avatar && avatar.startsWith('http')) {
|
||||
wx.downloadFile({
|
||||
url: avatar,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
ctx.save()
|
||||
ctx.beginPath()
|
||||
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
|
||||
ctx.clip()
|
||||
ctx.drawImage(res.tempFilePath, avatarX, avatarY, avatarSize, avatarSize)
|
||||
ctx.restore()
|
||||
} else {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
}
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
resolve()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.drawAvatarPlaceholder(ctx, avatarX, avatarY, avatarSize, nickname)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
await drawAvatar()
|
||||
ctx.setStrokeStyle('rgba(94,234,212,0.5)')
|
||||
ctx.setLineWidth(2)
|
||||
ctx.beginPath()
|
||||
ctx.arc(avatarX + avatarRadius, avatarY + avatarRadius, avatarRadius, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
// 右侧:昵称 + 个人名片
|
||||
const displayName = (nickname || '').trim() || '创业者'
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(26)
|
||||
ctx.setTextAlign('left')
|
||||
ctx.fillText(displayName, rightStart, avatarY + 36)
|
||||
ctx.setFillStyle('#94A3B8')
|
||||
ctx.setFontSize(13)
|
||||
ctx.fillText('个人名片', rightStart, avatarY + 62)
|
||||
// 分隔线
|
||||
const divY = 168
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.08)')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(pad, divY)
|
||||
ctx.lineTo(w - pad, divY)
|
||||
ctx.stroke()
|
||||
// 底部四栏:地区 | MBTI,行业 | 职位
|
||||
const labelGray = '#64748B'
|
||||
const valueWhite = '#F1F5F9'
|
||||
const rowH = 52
|
||||
const colW = (w - pad * 2) / 2
|
||||
const truncate = (text, maxW) => {
|
||||
if (!text) return ''
|
||||
ctx.setFontSize(15)
|
||||
let m = ctx.measureText(text)
|
||||
if (m.width <= maxW) return text
|
||||
for (let i = text.length - 1; i > 0; i--) {
|
||||
const t = text.slice(0, i) + '…'
|
||||
if (ctx.measureText(t).width <= maxW) return t
|
||||
}
|
||||
return text[0] + '…'
|
||||
}
|
||||
const maxValW = colW - 16
|
||||
const items = [
|
||||
{ label: '地区', value: truncate((region || '').trim() || '未填写', maxValW), x: pad },
|
||||
{ label: 'MBTI', value: (mbti || '').trim() || '未填写', x: pad + colW },
|
||||
{ label: '行业', value: truncate((industry || '').trim() || '未填写', maxValW), x: pad },
|
||||
{ label: '职位', value: truncate((position || '').trim() || '未填写', maxValW), x: pad + colW },
|
||||
]
|
||||
items.forEach((item, i) => {
|
||||
const row = Math.floor(i / 2)
|
||||
const baseY = divY + 36 + row * rowH
|
||||
ctx.setFillStyle(labelGray)
|
||||
ctx.setFontSize(12)
|
||||
ctx.fillText(item.label, item.x, baseY - 8)
|
||||
ctx.setFillStyle(valueWhite)
|
||||
ctx.setFontSize(15)
|
||||
ctx.fillText(item.value, item.x, baseY + 14)
|
||||
})
|
||||
ctx.draw(true, () => {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'shareCardCanvas',
|
||||
destWidth: 500,
|
||||
destHeight: 400,
|
||||
success: (res) => {
|
||||
this.setData({ shareCardPath: res.tempFilePath })
|
||||
},
|
||||
}, this)
|
||||
})
|
||||
} catch (e) {
|
||||
console.warn('[ShareCard] 生成失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
drawAvatarPlaceholder(ctx, x, y, size, nickname) {
|
||||
ctx.setFillStyle('rgba(94,234,212,0.2)')
|
||||
ctx.beginPath()
|
||||
ctx.arc(x + size / 2, y + size / 2, size / 2, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#5EEAD4')
|
||||
ctx.setFontSize(size * 0.42)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.fillText((nickname || '?')[0], x + size / 2, y + size / 2 + size * 0.14)
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const nickname = (this.data.nickname || '').trim() || '我'
|
||||
const path = userId
|
||||
? (ref ? `/pages/member-detail/member-detail?id=${userId}&ref=${ref}` : `/pages/member-detail/member-detail?id=${userId}`)
|
||||
: (ref ? `/pages/profile-edit/profile-edit?ref=${ref}` : '/pages/profile-edit/profile-edit')
|
||||
const result = {
|
||||
title: `${nickname}为您分享名片`,
|
||||
path,
|
||||
}
|
||||
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
|
||||
return result
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
const nickname = (this.data.nickname || '').trim() || '我'
|
||||
const query = userId
|
||||
? (ref ? `id=${userId}&ref=${ref}` : `id=${userId}`)
|
||||
: (ref ? `ref=${ref}` : '')
|
||||
const result = {
|
||||
title: `${nickname}为您分享名片`,
|
||||
query: query || '',
|
||||
}
|
||||
if (this.data.shareCardPath) result.imageUrl = this.data.shareCardPath
|
||||
return result
|
||||
},
|
||||
|
||||
onNicknameInput(e) { this.setData({ nickname: e.detail.value }) },
|
||||
onNicknameChange(e) { this.setData({ nickname: e.detail.value }) },
|
||||
onRegionInput(e) { this.setData({ region: e.detail.value }) },
|
||||
onIndustryInput(e) { this.setData({ industry: e.detail.value }) },
|
||||
onBusinessScaleInput(e) { this.setData({ businessScale: e.detail.value }) },
|
||||
@@ -114,7 +286,26 @@ Page({
|
||||
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
|
||||
},
|
||||
|
||||
chooseAvatar() {
|
||||
// 点击头像:选择微信头像或从相册选择
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
},
|
||||
|
||||
// 从相册/相机选择头像
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
@@ -139,7 +330,8 @@ Page({
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
const rawUrl1 = uploadRes.data.url || ''
|
||||
const avatarUrl = rawUrl1.startsWith('http://') || rawUrl1.startsWith('https://') ? rawUrl1 : app.globalData.baseUrl + rawUrl1
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
@@ -152,6 +344,7 @@ Page({
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
@@ -160,6 +353,54 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 微信原生 chooseAvatar 回调:使用当前微信头像
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch {
|
||||
reject(new Error('解析失败'))
|
||||
}
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
|
||||
const rawUrl2 = uploadRes.data.url || ''
|
||||
const avatarUrl = rawUrl2.startsWith('http://') || rawUrl2.startsWith('https://') ? rawUrl2 : app.globalData.baseUrl + rawUrl2
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} catch (err) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: err.message || '上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="chooseAvatar">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
@@ -31,7 +31,18 @@
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入昵称" value="{{nickname}}" bindinput="onNicknameInput"/></view>
|
||||
<view class="form-input-wrap">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称,或手动输入</text>
|
||||
</view>
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
@@ -134,4 +145,18 @@
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 分享名片 canvas(隐藏,用于生成分享图 5:4) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
|
||||
<!-- 头像弹窗:通过 button 获取微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal">✕</view>
|
||||
<text class="avatar-modal-title">使用微信头像</text>
|
||||
<text class="avatar-modal-desc">点击下方按钮,一键同步当前微信头像</text>
|
||||
<button class="btn-choose-avatar" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">使用微信头像</button>
|
||||
<view class="avatar-modal-cancel" bindtap="closeAvatarModal">取消</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
/* 资料编辑 - comprehensive_profile_editor_v1_1 | 配色 enhanced,input/textarea 用 view 包裹 */
|
||||
|
||||
/* 分享名片 canvas:隐藏,仅用于生成图片 */
|
||||
.share-card-canvas {
|
||||
position: fixed; left: -9999px; top: 0; width: 500px; height: 400px;
|
||||
}
|
||||
.page {
|
||||
background: #050B14; min-height: 100vh; color: #fff;
|
||||
width: 100%; box-sizing: border-box; overflow-x: hidden;
|
||||
@@ -101,3 +106,86 @@
|
||||
}
|
||||
.save-btn[disabled] { opacity: 0.6; }
|
||||
.bottom-space { height: 120rpx; }
|
||||
|
||||
/* 昵称提示文案 */
|
||||
.input-tip {
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #94A3B8;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
/* 头像弹窗样式,复用我的页风格 */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0,0,0,0.6);
|
||||
backdrop-filter: blur(16rpx);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 640rpx;
|
||||
background: #0b1220;
|
||||
border-radius: 32rpx;
|
||||
padding: 48rpx;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 24rpx;
|
||||
right: 24rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
.avatar-modal-title {
|
||||
display: block;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.avatar-modal-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #94A3B8;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.btn-choose-avatar {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@
|
||||
<!-- 阅读内容 -->
|
||||
<view class="read-content">
|
||||
<!-- 章节标题 -->
|
||||
<view class="chapter-header">
|
||||
<view class="chapter-header" wx:if="{{section}}">
|
||||
<view class="chapter-meta">
|
||||
<text class="chapter-id">{{section.id}}</text>
|
||||
<text class="tag tag-free" wx:if="{{section.isFree}}">免费</text>
|
||||
@@ -42,10 +42,15 @@
|
||||
<view class="skeleton skeleton-5"></view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 免费或已购买 -->
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text>
|
||||
<text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">@{{seg.nickname}}</text>
|
||||
<text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text>
|
||||
<image wx:elif="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -81,14 +86,21 @@
|
||||
<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>
|
||||
<text class="action-icon-small">📣</text>
|
||||
<text class="action-text-small">分享给好友</text>
|
||||
</button>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="handleGiftPay" wx:if="{{!auditMode}}">
|
||||
<text class="action-icon-small">🎁</text>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="showPosterModal">
|
||||
<text class="action-icon-small">🖼️</text>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -107,7 +119,7 @@
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读20%,登录后查看完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,登录后查看完整内容</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">立即登录</text>
|
||||
@@ -157,18 +169,18 @@
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读20%,购买后继续阅读</text>
|
||||
<text class="paywall-title" wx:if="{{!auditMode}}">解锁完整内容</text>
|
||||
<text class="paywall-title" wx:else>完整内容即将开放</text>
|
||||
<text class="paywall-desc" wx:if="{{!auditMode}}">已阅读50%,购买后继续阅读</text>
|
||||
<text class="paywall-desc" wx:else>该内容正在准备中,敬请期待</text>
|
||||
|
||||
<!-- 购买选项 -->
|
||||
<view class="purchase-options">
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<!-- 购买选项(审核模式隐藏) -->
|
||||
<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>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<text class="btn-sparkle">✨</text>
|
||||
@@ -181,7 +193,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="paywall-tip">分享给好友一起学习,还能赚取佣金</text>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -237,6 +249,14 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享提示浮层(阅读20%后下拉触发) -->
|
||||
<view class="share-float-tip {{showShareTip ? 'show' : ''}}" wx:if="{{showShareTip && !auditMode}}">
|
||||
<text class="share-float-icon">💰</text>
|
||||
<text class="share-float-text">分享给好友,好友购买你可获得 90% 收益</text>
|
||||
<button class="share-float-btn" open-type="share">立即分享</button>
|
||||
<view class="share-float-close" bindtap="closeShareTip">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content poster-modal" catchtap="stopPropagation">
|
||||
@@ -292,8 +312,8 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮分享按钮 -->
|
||||
<button class="fab-share" open-type="share">
|
||||
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</button>
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<text class="fab-moments-icon">🌐</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -182,6 +182,27 @@
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
/* 正文内 @某人 高亮可点 */
|
||||
.paragraph .mention {
|
||||
color: #00CED1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 正文内 #链接标签 高亮可点 */
|
||||
.paragraph .link-tag {
|
||||
color: #FFD700;
|
||||
font-weight: 500;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
|
||||
/* 正文内图片 */
|
||||
.content-image {
|
||||
width: 100%;
|
||||
border-radius: 8rpx;
|
||||
margin: 16rpx 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
@@ -327,18 +348,20 @@
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
.nav-btn-placeholder {
|
||||
flex: 1;
|
||||
max-width: 48%;
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nav-prev {
|
||||
@@ -379,12 +402,16 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
@@ -559,6 +586,48 @@
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 代付分享 ===== */
|
||||
.btn-gift-inline {
|
||||
/* 与 btn-share-inline 同风格 */
|
||||
}
|
||||
.gift-share-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-top: 24rpx;
|
||||
padding: 20rpx;
|
||||
background: rgba(255, 215, 0, 0.08);
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx solid rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
.gift-share-icon { font-size: 32rpx; }
|
||||
.gift-share-text { font-size: 28rpx; color: #FFD700; }
|
||||
.share-modal-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
margin-bottom: 32rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.share-modal-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.share-modal-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx 24rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-radius: 24rpx;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
.share-modal-btn .btn-icon { font-size: 48rpx; }
|
||||
.share-modal-btn text:last-child { font-size: 26rpx; color: rgba(255, 255, 255, 0.8); }
|
||||
|
||||
/* ===== 分享弹窗 ===== */
|
||||
.share-link-box {
|
||||
padding: 32rpx;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
* - 收益统计(90%归分发者)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -62,9 +63,8 @@ Page({
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
posterCaseCount: 62,
|
||||
|
||||
},
|
||||
posterCaseCount: 62
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
@@ -93,28 +93,17 @@ Page({
|
||||
// 生成邀请码
|
||||
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
|
||||
|
||||
console.log('[Referral] 开始加载分销数据,userId:', userInfo.id)
|
||||
|
||||
// 从API获取真实数据
|
||||
let realData = null
|
||||
try {
|
||||
// app.request 第一个参数是 URL 字符串(会自动拼接 baseUrl)
|
||||
const res = await app.request('/api/miniprogram/referral/data?userId=' + userInfo.id)
|
||||
console.log('[Referral] API返回:', JSON.stringify(res).substring(0, 200))
|
||||
|
||||
if (res && res.success && res.data) {
|
||||
realData = res.data
|
||||
console.log('[Referral] ✅ 获取推广数据成功')
|
||||
console.log('[Referral] - bindingCount:', realData.bindingCount)
|
||||
console.log('[Referral] - paidCount:', realData.paidCount)
|
||||
console.log('[Referral] - earnings:', realData.earnings)
|
||||
console.log('[Referral] - expiringCount:', realData.stats?.expiringCount)
|
||||
} else {
|
||||
console.log('[Referral] ❌ API返回格式错误:', res?.error || 'unknown')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Referral] ❌ API调用失败:', e.message || e)
|
||||
console.log('[Referral] 错误详情:', e)
|
||||
console.warn('[Referral] 加载分销数据失败:', e && e.message ? e.message : e)
|
||||
}
|
||||
|
||||
// 使用真实数据或默认值
|
||||
@@ -122,15 +111,9 @@ Page({
|
||||
let convertedBindings = realData?.convertedUsers || []
|
||||
let expiredBindings = realData?.expiredUsers || []
|
||||
|
||||
console.log('[Referral] activeBindings:', activeBindings.length)
|
||||
console.log('[Referral] convertedBindings:', convertedBindings.length)
|
||||
console.log('[Referral] expiredBindings:', expiredBindings.length)
|
||||
|
||||
// 计算即将过期的数量(7天内)
|
||||
const expiringCount = realData?.stats?.expiringCount || activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
|
||||
|
||||
console.log('[Referral] expiringCount:', expiringCount)
|
||||
|
||||
// 计算各类统计
|
||||
const bindingCount = realData?.bindingCount || activeBindings.length
|
||||
const paidCount = realData?.paidCount || convertedBindings.length
|
||||
@@ -152,7 +135,6 @@ Page({
|
||||
purchaseCount: user.purchaseCount || 0,
|
||||
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
|
||||
}
|
||||
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
|
||||
return formatted
|
||||
}
|
||||
|
||||
@@ -168,15 +150,6 @@ Page({
|
||||
const availableEarningsNum = Math.max(0, totalCommissionNum - withdrawnNum - pendingWithdrawNum)
|
||||
const minWithdrawAmount = realData?.minWithdrawAmount || 10
|
||||
|
||||
console.log('=== [Referral] 收益计算(完整版)===')
|
||||
console.log('累计佣金 (totalCommission):', totalCommissionNum)
|
||||
console.log('已提现金额 (withdrawnEarnings):', withdrawnNum)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', pendingWithdrawNum)
|
||||
console.log('可提现金额 = 累计 - 已提现 - 待审核 =', totalCommissionNum, '-', withdrawnNum, '-', pendingWithdrawNum, '=', availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', minWithdrawAmount)
|
||||
console.log('按钮判断:', availableEarningsNum, '>=', minWithdrawAmount, '=', availableEarningsNum >= minWithdrawAmount)
|
||||
console.log('✅ 按钮应该:', availableEarningsNum >= minWithdrawAmount ? '🟢 启用(绿色)' : '⚫ 禁用(灰色)')
|
||||
|
||||
const hasWechatId = !!(userInfo?.wechat || userInfo?.wechatId || wx.getStorageSync('user_wechat'))
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
@@ -232,21 +205,6 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
console.log('[Referral] ✅ 数据设置完成')
|
||||
console.log('[Referral] - 绑定中:', this.data.bindingCount)
|
||||
console.log('[Referral] - 即将过期:', this.data.expiringCount)
|
||||
console.log('[Referral] - 收益:', this.data.earnings)
|
||||
|
||||
console.log('=== [Referral] 按钮状态验证 ===')
|
||||
console.log('累计佣金 (totalCommission):', this.data.totalCommission)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', this.data.pendingWithdrawAmount)
|
||||
console.log('可提现金额 (availableEarnings 显示):', this.data.availableEarnings)
|
||||
console.log('可提现金额 (availableEarningsNum 判断):', this.data.availableEarningsNum, typeof this.data.availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', this.data.minWithdrawAmount, typeof this.data.minWithdrawAmount)
|
||||
console.log('按钮启用条件:', this.data.availableEarningsNum, '>=', this.data.minWithdrawAmount, '=', this.data.availableEarningsNum >= this.data.minWithdrawAmount)
|
||||
console.log('✅ 最终结果: 按钮应该', this.data.availableEarningsNum >= this.data.minWithdrawAmount ? '🟢 启用' : '⚫ 禁用')
|
||||
|
||||
// 隐藏加载提示
|
||||
wx.hideLoading()
|
||||
} else {
|
||||
@@ -257,6 +215,7 @@ Page({
|
||||
|
||||
// 切换Tab
|
||||
switchTab(e) {
|
||||
trackClick('referral', 'tab_click', e.currentTarget.dataset.tab || 'tab')
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
let currentBindings = []
|
||||
|
||||
@@ -278,6 +237,7 @@ Page({
|
||||
|
||||
// 复制邀请链接
|
||||
copyLink() {
|
||||
trackClick('referral', 'btn_click', '复制链接')
|
||||
const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
|
||||
wx.setClipboardData({
|
||||
data: link,
|
||||
@@ -287,6 +247,7 @@ Page({
|
||||
|
||||
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
|
||||
shareToWechat() {
|
||||
trackClick('referral', 'btn_click', '分享朋友圈文案')
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
@@ -314,6 +275,7 @@ Page({
|
||||
|
||||
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
|
||||
handleMoreShare() {
|
||||
trackClick('referral', 'btn_click', '更多分享')
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
@@ -334,6 +296,7 @@ Page({
|
||||
|
||||
// 生成推广海报 - 1:1 对齐 Next.js 设计
|
||||
async generatePoster() {
|
||||
trackClick('referral', 'btn_click', '生成海报')
|
||||
wx.showLoading({ title: '生成中...', mask: true })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
@@ -524,6 +487,7 @@ Page({
|
||||
|
||||
// 保存海报
|
||||
savePoster() {
|
||||
trackClick('referral', 'btn_click', '保存海报')
|
||||
const { posterQrSrc } = this.data
|
||||
if (!posterQrSrc) {
|
||||
wx.showToast({ title: '二维码未生成', icon: 'none' })
|
||||
@@ -624,6 +588,7 @@ Page({
|
||||
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
trackClick('referral', 'btn_click', '提现')
|
||||
const availableEarnings = this.data.availableEarningsNum || 0
|
||||
const minWithdrawAmount = this.data.minWithdrawAmount || 10
|
||||
const hasWechatId = this.data.hasWechatId
|
||||
@@ -670,6 +635,7 @@ Page({
|
||||
|
||||
// 跳转提现记录页
|
||||
goToWithdrawRecords() {
|
||||
trackClick('referral', 'btn_click', '提现记录')
|
||||
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
},
|
||||
|
||||
|
||||
@@ -1,379 +0,0 @@
|
||||
/* ???????? - 1:1??Web?? */
|
||||
.page { min-height: 100vh; background: #000; padding-bottom: 64rpx; }
|
||||
|
||||
/* ??? */
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.9); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-left { display: flex; gap: 16rpx; align-items: center; }
|
||||
.nav-back { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
.nav-icon { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.nav-title { font-size: 34rpx; font-weight: 600; color: #fff; flex: 1; text-align: center; }
|
||||
.nav-right-placeholder { width: 144rpx; }
|
||||
.nav-btn { width: 64rpx; height: 64rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
.content { padding: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
|
||||
/* ?????? */
|
||||
.expiring-banner { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,165,0,0.1); border: 2rpx solid rgba(255,165,0,0.3); border-radius: 24rpx; margin-bottom: 24rpx; }
|
||||
.banner-icon { width: 80rpx; height: 80rpx; background: rgba(255,165,0,0.2); border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.icon-bell-warning { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(64%) sepia(89%) saturate(1363%) hue-rotate(4deg) brightness(101%) contrast(102%); }
|
||||
.banner-content { flex: 1; }
|
||||
.banner-title { font-size: 28rpx; font-weight: 500; color: #fff; display: block; }
|
||||
.banner-desc { font-size: 24rpx; color: rgba(255,165,0,0.8); margin-top: 4rpx; display: block; }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.earnings-card { position: relative; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 48rpx; margin-bottom: 24rpx; overflow: hidden; width: 100%; box-sizing: border-box; }
|
||||
.earnings-bg { position: absolute; top: 0; right: 0; width: 256rpx; height: 256rpx; background: rgba(0,206,209,0.15); border-radius: 50%; filter: blur(100rpx); }
|
||||
.earnings-main { position: relative; }
|
||||
.earnings-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 32rpx; }
|
||||
.earnings-left { display: flex; align-items: center; gap: 16rpx; }
|
||||
.wallet-icon { width: 80rpx; height: 80rpx; background: rgba(0,206,209,0.2); border-radius: 20rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.icon-wallet { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.earnings-info { display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.earnings-label { font-size: 24rpx; color: rgba(255,255,255,0.6); }
|
||||
.commission-rate { font-size: 24rpx; color: #00CED1; font-weight: 500; }
|
||||
.earnings-right { text-align: right; }
|
||||
.earnings-value { font-size: 60rpx; font-weight: 700; color: #fff; display: block; line-height: 1; }
|
||||
.pending-text { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; display: block; }
|
||||
|
||||
.withdraw-btn { padding: 28rpx; background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%); color: #fff; font-size: 32rpx; font-weight: 600; text-align: center; border-radius: 24rpx; box-shadow: 0 8rpx 24rpx rgba(0,206,209,0.3); }
|
||||
.withdraw-btn.btn-disabled { background: rgba(0,206,209,0.2); color: rgba(255,255,255,0.3); box-shadow: none; }
|
||||
|
||||
/* ???? - ?? Next.js 4??? */
|
||||
.stats-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 8rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.stat-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 24rpx 12rpx; text-align: center; }
|
||||
.stat-value { font-size: 40rpx; font-weight: 700; color: #fff; display: block; }
|
||||
.stat-value.orange { color: #FFA500; }
|
||||
.stat-label { font-size: 20rpx; color: rgba(255,255,255,0.6); margin-top: 8rpx; display: block; }
|
||||
|
||||
/* ????? */
|
||||
.visit-stat { display: flex; align-items: center; justify-content: center; gap: 12rpx; padding: 20rpx 32rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 24rpx; }
|
||||
.visit-label { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
.visit-value { font-size: 32rpx; font-weight: 700; color: #00CED1; }
|
||||
.visit-tip { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.rules-card { background: rgba(0,206,209,0.05); border: 2rpx solid rgba(0,206,209,0.2); border-radius: 24rpx; padding: 32rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.rules-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
|
||||
.rules-icon { width: 64rpx; height: 64rpx; background: rgba(0,206,209,0.2); border-radius: 16rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.icon-alert { width: 32rpx; height: 32rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.rules-title { font-size: 28rpx; font-weight: 500; color: #fff; }
|
||||
.rules-list { padding-left: 8rpx; }
|
||||
.rule-item { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 2; display: block; margin-bottom: 4rpx; }
|
||||
.rule-item .gold { color: #FFD700; font-weight: 500; }
|
||||
.rule-item .brand { color: #00CED1; font-weight: 500; }
|
||||
.rule-item .orange { color: #FFA500; font-weight: 500; }
|
||||
|
||||
/* ?????? - ?? Next.js */
|
||||
.binding-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.binding-header { display: flex; align-items: center; justify-content: space-between; padding: 28rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.binding-title { display: flex; align-items: center; gap: 12rpx; }
|
||||
.binding-icon-img { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.title-text { font-size: 30rpx; font-weight: 600; color: #fff; }
|
||||
.binding-count { font-size: 26rpx; color: rgba(255,255,255,0.5); }
|
||||
.toggle-icon { font-size: 24rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
/* Tab?? */
|
||||
.binding-tabs { display: flex; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.tab-item { flex: 1; padding: 24rpx 0; text-align: center; font-size: 26rpx; color: rgba(255,255,255,0.5); position: relative; }
|
||||
.tab-item.tab-active { color: #00CED1; }
|
||||
.tab-item.tab-active::after { content: ''; position: absolute; bottom: 0; left: 50%; transform: translateX(-50%); width: 80rpx; height: 4rpx; background: #00CED1; border-radius: 4rpx; }
|
||||
|
||||
/* ???? */
|
||||
.binding-list { max-height: 640rpx; overflow-y: auto; }
|
||||
.empty-state { padding: 80rpx 0; text-align: center; }
|
||||
.empty-icon { font-size: 64rpx; display: block; margin-bottom: 16rpx; }
|
||||
.empty-text { font-size: 26rpx; color: rgba(255,255,255,0.5); }
|
||||
|
||||
.binding-item { display: flex; align-items: center; padding: 24rpx 32rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.binding-item:last-child { border-bottom: none; }
|
||||
.user-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 28rpx; font-weight: 600; color: #00CED1; margin-right: 24rpx; flex-shrink: 0; }
|
||||
.user-avatar.avatar-converted { background: rgba(76,175,80,0.2); color: #4CAF50; }
|
||||
.user-avatar.avatar-expired { background: rgba(158,158,158,0.2); color: #9E9E9E; }
|
||||
.user-info { flex: 1; }
|
||||
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
|
||||
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
|
||||
.user-status { text-align: right; }
|
||||
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; display: block; }
|
||||
.status-order { font-size: 22rpx; color: rgba(255,255,255,0.5); }
|
||||
.status-tag { font-size: 22rpx; padding: 8rpx 16rpx; border-radius: 16rpx; }
|
||||
.status-tag.tag-green { background: rgba(76,175,80,0.2); color: #4CAF50; }
|
||||
.status-tag.tag-orange { background: rgba(255,165,0,0.2); color: #FFA500; }
|
||||
.status-tag.tag-red { background: rgba(244,67,54,0.2); color: #F44336; }
|
||||
|
||||
/* ????? - ?? Next.js */
|
||||
.invite-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; padding: 40rpx; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.invite-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24rpx; }
|
||||
.invite-title { font-size: 30rpx; font-weight: 600; color: #fff; }
|
||||
.invite-code-box { background: rgba(0,206,209,0.2); padding: 12rpx 24rpx; border-radius: 16rpx; }
|
||||
.invite-code { font-size: 28rpx; font-weight: 600; color: #00CED1; font-family: 'Courier New', monospace; letter-spacing: 2rpx; }
|
||||
.invite-tip { font-size: 24rpx; color: rgba(255,255,255,0.6); line-height: 1.5; display: block; }
|
||||
.invite-tip .gold { color: #FFD700; }
|
||||
.invite-tip .brand { color: #00CED1; }
|
||||
|
||||
/* ?????? - ?? Next.js */
|
||||
.earnings-detail-card { background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 32rpx; overflow: hidden; margin-bottom: 24rpx; width: 100%; box-sizing: border-box; }
|
||||
.detail-header { padding: 40rpx 40rpx 24rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.detail-title { font-size: 30rpx; font-weight: 600; color: #fff; }
|
||||
.detail-list { max-height: 480rpx; overflow-y: auto; }
|
||||
.detail-item { display: flex; align-items: center; justify-content: space-between; padding: 32rpx 40rpx; border-bottom: 2rpx solid rgba(255,255,255,0.05); }
|
||||
.detail-item:last-child { border-bottom: none; }
|
||||
.detail-left { display: flex; align-items: center; gap: 24rpx; flex: 1; }
|
||||
.detail-icon { width: 80rpx; height: 80rpx; border-radius: 20rpx; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.icon-gift { width: 40rpx; height: 40rpx; display: block; filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%); }
|
||||
.detail-info { flex: 1; }
|
||||
.detail-type { font-size: 28rpx; color: #fff; font-weight: 500; display: block; }
|
||||
.detail-time { font-size: 24rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; }
|
||||
.detail-amount { font-size: 30rpx; font-weight: 600; color: #00CED1; }
|
||||
|
||||
/* ???? - ?? Next.js */
|
||||
.share-section { display: flex; flex-direction: column; gap: 12rpx; width: 100%; margin-bottom: 24rpx; }
|
||||
.share-item { display: flex; align-items: center; background: rgba(28, 28, 30, 0.8); backdrop-filter: blur(40rpx); border: 2rpx solid rgba(255,255,255,0.1); border-radius: 24rpx; padding: 32rpx; border: none; margin: 0; text-align: left; width: 100%; box-sizing: border-box; }
|
||||
.share-item::after { border: none; }
|
||||
.share-icon { width: 96rpx; height: 96rpx; border-radius: 20rpx; display: flex; align-items: center; justify-content: center; margin-right: 24rpx; flex-shrink: 0; }
|
||||
.share-icon.poster { background: rgba(103,58,183,0.2); }
|
||||
.share-icon.wechat { background: rgba(7,193,96,0.2); }
|
||||
.share-icon.link { background: rgba(158,158,158,0.2); }
|
||||
.icon-share-btn { width: 48rpx; height: 48rpx; display: block; }
|
||||
.share-icon.poster .icon-share-btn { filter: brightness(0) saturate(100%) invert(37%) sepia(73%) saturate(2296%) hue-rotate(252deg) brightness(96%) contrast(92%); }
|
||||
.share-icon.wechat .icon-share-btn { filter: brightness(0) saturate(100%) invert(58%) sepia(91%) saturate(1255%) hue-rotate(105deg) brightness(96%) contrast(97%); }
|
||||
.share-icon.link .icon-share-btn { filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.share-info { flex: 1; text-align: left; }
|
||||
.share-title { font-size: 28rpx; color: #fff; font-weight: 500; display: block; text-align: left; }
|
||||
.share-desc { font-size: 22rpx; color: rgba(255,255,255,0.5); margin-top: 4rpx; display: block; text-align: left; }
|
||||
.share-arrow-icon { width: 40rpx; height: 40rpx; display: block; flex-shrink: 0; filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%); }
|
||||
.share-btn-wechat { line-height: normal; font-size: inherit; padding: 24rpx 32rpx !important; margin: 0 !important; width: 100% !important; }
|
||||
|
||||
/* ?????????????? + ???? + ???????? */
|
||||
/* ???????? backdrop-filter??????????????? */
|
||||
.modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.8); display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 32rpx; box-sizing: border-box; }
|
||||
|
||||
.poster-dialog { width: 686rpx; border-radius: 24rpx; overflow: hidden; position: relative; background: transparent; }
|
||||
.poster-close { position: absolute; top: 20rpx; right: 20rpx; width: 56rpx; height: 56rpx; border-radius: 28rpx; background: rgba(0,0,0,0.25); color: rgba(255,255,255,0.9); display: flex; align-items: center; justify-content: center; z-index: 5; font-size: 28rpx; }
|
||||
|
||||
/* ???? */
|
||||
.poster-card { position: relative; background: linear-gradient(135deg, #0a1628 0%, #0f2137 50%, #1a3a5c 100%); color: #fff; padding: 44rpx 40rpx 36rpx; }
|
||||
.poster-inner { position: relative; z-index: 2; display: flex; flex-direction: column; align-items: center; text-align: center; }
|
||||
|
||||
/* ???? */
|
||||
/* ???????? filter: blur ??????????????? + ???? */
|
||||
.poster-glow { position: absolute; width: 320rpx; height: 320rpx; border-radius: 50%; opacity: 0.6; z-index: 1; }
|
||||
.poster-glow-left { top: -120rpx; left: -160rpx; background: rgba(0,206,209,0.12); box-shadow: 0 0 140rpx 40rpx rgba(0,206,209,0.18); }
|
||||
.poster-glow-right { bottom: -140rpx; right: -160rpx; background: rgba(255,215,0,0.10); box-shadow: 0 0 160rpx 50rpx rgba(255,215,0,0.14); }
|
||||
.poster-ring { position: absolute; width: 520rpx; height: 520rpx; border-radius: 50%; border: 2rpx solid rgba(0,206,209,0.06); left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; }
|
||||
|
||||
/* ???? */
|
||||
.poster-badges { display: flex; gap: 16rpx; margin-bottom: 24rpx; }
|
||||
.poster-badge { padding: 10rpx 22rpx; font-size: 20rpx; font-weight: 700; border-radius: 999rpx; border: 2rpx solid transparent; }
|
||||
.poster-badge-gold { background: rgba(255,215,0,0.18); color: #FFD700; border-color: rgba(255,215,0,0.28); }
|
||||
.poster-badge-brand { background: rgba(0,206,209,0.18); color: #00CED1; border-color: rgba(0,206,209,0.28); }
|
||||
|
||||
/* ?? */
|
||||
.poster-title { margin-bottom: 8rpx; }
|
||||
.poster-title-line1 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #00CED1; }
|
||||
.poster-title-line2 { display: block; font-size: 44rpx; font-weight: 900; line-height: 1.1; color: #fff; margin-top: 6rpx; }
|
||||
.poster-subtitle { display: block; font-size: 22rpx; color: rgba(255,255,255,0.6); margin-bottom: 28rpx; }
|
||||
|
||||
/* ???? */
|
||||
.poster-stats { width: 100%; display: flex; gap: 16rpx; justify-content: center; margin-bottom: 24rpx; }
|
||||
.poster-stat { flex: 1; max-width: 190rpx; background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 16rpx; padding: 18rpx 10rpx; }
|
||||
.poster-stat-value { display: block; font-size: 44rpx; font-weight: 900; line-height: 1; }
|
||||
.poster-stat-label { display: block; font-size: 20rpx; color: rgba(255,255,255,0.5); margin-top: 8rpx; }
|
||||
.poster-stat-gold { color: #FFD700; }
|
||||
.poster-stat-brand { color: #00CED1; }
|
||||
.poster-stat-pink { color: #E91E63; }
|
||||
|
||||
/* ?? */
|
||||
.poster-tags { display: flex; flex-wrap: wrap; justify-content: center; gap: 10rpx; margin: 0 24rpx 26rpx; }
|
||||
.poster-tag { font-size: 20rpx; color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.05); border: 2rpx solid rgba(255,255,255,0.10); border-radius: 12rpx; padding: 6rpx 14rpx; }
|
||||
|
||||
/* ??? */
|
||||
.poster-recommender { display: flex; align-items: center; gap: 12rpx; background: rgba(0,206,209,0.10); border: 2rpx solid rgba(0,206,209,0.20); border-radius: 999rpx; padding: 12rpx 22rpx; margin-bottom: 22rpx; }
|
||||
.poster-avatar { width: 44rpx; height: 44rpx; border-radius: 22rpx; background: rgba(0,206,209,0.30); display: flex; align-items: center; justify-content: center; }
|
||||
.poster-avatar-text { font-size: 20rpx; font-weight: 800; color: #00CED1; }
|
||||
.poster-recommender-text { font-size: 22rpx; color: #00CED1; }
|
||||
|
||||
/* ??? */
|
||||
.poster-discount { width: 100%; background: linear-gradient(90deg, rgba(255,215,0,0.10) 0%, rgba(233,30,99,0.10) 100%); border: 2rpx solid rgba(255,215,0,0.20); border-radius: 18rpx; padding: 18rpx 18rpx; margin-bottom: 26rpx; }
|
||||
.poster-discount-text { font-size: 22rpx; color: rgba(255,255,255,0.80); }
|
||||
.poster-discount-highlight { color: #00CED1; font-weight: 800; }
|
||||
|
||||
/* ??? */
|
||||
.poster-qr-wrap { background: #fff; padding: 14rpx; border-radius: 16rpx; margin-bottom: 12rpx; box-shadow: 0 16rpx 40rpx rgba(0,0,0,0.35); }
|
||||
.poster-qr-img { width: 240rpx; height: 240rpx; display: block; }
|
||||
.poster-qr-tip { font-size: 20rpx; color: rgba(255,255,255,0.40); margin-bottom: 8rpx; }
|
||||
.poster-code { font-size: 22rpx; font-family: monospace; letter-spacing: 2rpx; color: rgba(0,206,209,0.80); }
|
||||
|
||||
/* ??????? */
|
||||
.poster-footer { background: #fff; padding: 28rpx 28rpx 32rpx; display: flex; flex-direction: column; gap: 18rpx; }
|
||||
.poster-footer-tip { font-size: 22rpx; color: rgba(0,0,0,0.55); text-align: center; }
|
||||
.poster-footer-btn { height: 72rpx; border-radius: 18rpx; border: 2rpx solid rgba(0,0,0,0.15); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(0,0,0,0.75); background: #fff; }
|
||||
|
||||
| ||||