初始提交:一场soul的创业实验-永平 网站与小程序
Made-with: Cursor
This commit is contained in:
133
miniprogram/pages/about/about.js
Normal file
133
miniprogram/pages/about/about.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* Soul创业派对 - 关于作者页
|
||||
* 开发: 卡若
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
authorLoading: true,
|
||||
author: {
|
||||
name: '卡若',
|
||||
avatar: 'K',
|
||||
avatarImg: '/assets/images/author-avatar.png',
|
||||
title: '',
|
||||
bio: '',
|
||||
stats: [],
|
||||
highlights: []
|
||||
},
|
||||
bookInfo: {
|
||||
title: '一场Soul的创业实验',
|
||||
totalChapters: 62,
|
||||
parts: [
|
||||
{ name: '真实的人', chapters: 10 },
|
||||
{ name: '真实的行业', chapters: 15 },
|
||||
{ name: '真实的错误', chapters: 9 },
|
||||
{ name: '真实的赚钱', chapters: 20 },
|
||||
{ name: '真实的社会', chapters: 9 }
|
||||
],
|
||||
price: 9.9
|
||||
}
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight
|
||||
})
|
||||
this.loadAuthor()
|
||||
this.loadBookStats()
|
||||
},
|
||||
|
||||
async loadAuthor() {
|
||||
this.setData({ authorLoading: true })
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/about/author', silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
let avatarImg = d.avatarImg || ''
|
||||
if (avatarImg && !avatarImg.startsWith('http')) {
|
||||
const base = (app.globalData.baseUrl || '').replace(/\/$/, '')
|
||||
avatarImg = base ? base + (avatarImg.startsWith('/') ? avatarImg : '/' + avatarImg) : avatarImg
|
||||
}
|
||||
this.setData({
|
||||
author: {
|
||||
name: d.name || '卡若',
|
||||
avatar: d.avatar || 'K',
|
||||
avatarImg: avatarImg || '/assets/images/author-avatar.png',
|
||||
title: d.title || '',
|
||||
bio: d.bio || '',
|
||||
stats: Array.isArray(d.stats) ? d.stats : [
|
||||
{ label: '商业案例', value: '62' },
|
||||
{ label: '连续直播', value: '365天' },
|
||||
{ label: '派对分享', value: '1000+' }
|
||||
],
|
||||
highlights: Array.isArray(d.highlights) ? d.highlights : []
|
||||
},
|
||||
authorLoading: false
|
||||
})
|
||||
} else {
|
||||
this.setData({ authorLoading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[About] 加载作者配置失败,使用默认')
|
||||
this.setData({ authorLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 加载书籍统计(合并到作者统计第一项「商业案例」)
|
||||
async loadBookStats() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/stats', silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const total = res.data?.totalChapters || 62
|
||||
this.setData({ 'bookInfo.totalChapters': total })
|
||||
const stats = this.data.author?.stats || []
|
||||
const idx = stats.findIndex((s) => s && (s.label === '商业案例' || s.label === '章节'))
|
||||
if (idx >= 0 && stats[idx]) {
|
||||
const next = [...stats]
|
||||
next[idx] = { ...stats[idx], value: String(total) }
|
||||
this.setData({ 'author.stats': next })
|
||||
} else if (stats.length === 0) {
|
||||
this.setData({
|
||||
'author.stats': [
|
||||
{ label: '商业案例', value: String(total) },
|
||||
{ label: '连续直播', value: '365天' },
|
||||
{ label: '派对分享', value: '1000+' }
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[About] 加载书籍统计失败,使用默认值')
|
||||
}
|
||||
},
|
||||
|
||||
// 联系方式功能已禁用
|
||||
copyWechat() {
|
||||
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
|
||||
},
|
||||
|
||||
callPhone() {
|
||||
wx.showToast({ title: '请在派对房联系作者', icon: 'none' })
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 关于',
|
||||
path: ref ? `/pages/about/about?ref=${ref}` : '/pages/about/about'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 关于', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/about/about.json
Normal file
4
miniprogram/pages/about/about.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
79
miniprogram/pages/about/about.wxml
Normal file
79
miniprogram/pages/about/about.wxml
Normal file
@@ -0,0 +1,79 @@
|
||||
<!--关于作者-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">←</view>
|
||||
<text class="nav-title">关于作者</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view wx:if="{{authorLoading}}" class="loading-row">加载中...</view>
|
||||
<!-- 作者信息卡片 -->
|
||||
<view class="author-card" wx:if="{{!authorLoading}}">
|
||||
<view class="author-avatar-wrap">
|
||||
<image wx:if="{{author.avatarImg}}" class="author-avatar-img" src="{{author.avatarImg}}" mode="aspectFill"/>
|
||||
<view wx:else class="author-avatar">{{author.avatar}}</view>
|
||||
</view>
|
||||
<text class="author-name">{{author.name}}</text>
|
||||
<text class="author-title">{{author.title}}</text>
|
||||
<text class="author-bio">{{author.bio}}</text>
|
||||
|
||||
<!-- 统计数据 -->
|
||||
<view class="stats-row">
|
||||
<view class="stat-item" wx:for="{{author.stats}}" wx:key="label">
|
||||
<text class="stat-value">{{item.value}}</text>
|
||||
<text class="stat-label">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 亮点标签 -->
|
||||
<view class="highlights" wx:if="{{author.highlights}}">
|
||||
<view class="highlight-tag" wx:for="{{author.highlights}}" wx:key="*this">
|
||||
<text class="tag-icon">✓</text>
|
||||
<text>{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书籍信息 -->
|
||||
<view class="book-info-card" wx:if="{{bookInfo && !authorLoading}}">
|
||||
<text class="card-title">📚 {{bookInfo.title}}</text>
|
||||
<view class="book-stats">
|
||||
<view class="book-stat">
|
||||
<text class="book-stat-value">{{bookInfo.totalChapters}}</text>
|
||||
<text class="book-stat-label">篇章节</text>
|
||||
</view>
|
||||
<view class="book-stat">
|
||||
<text class="book-stat-value">5</text>
|
||||
<text class="book-stat-label">大篇章</text>
|
||||
</view>
|
||||
<view class="book-stat">
|
||||
<text class="book-stat-value">¥{{bookInfo.price}}</text>
|
||||
<text class="book-stat-label">全书价格</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="parts-list">
|
||||
<view class="part-item" wx:for="{{bookInfo.parts}}" wx:key="name">
|
||||
<text class="part-name">{{item.name}}</text>
|
||||
<text class="part-chapters">{{item.chapters}}节</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 - 引导到Soul派对房 -->
|
||||
<view class="contact-card" wx:if="{{!authorLoading}}">
|
||||
<text class="card-title">联系作者</text>
|
||||
<view class="contact-item">
|
||||
<text class="contact-icon">🎉</text>
|
||||
<view class="contact-info">
|
||||
<text class="contact-label">Soul派对房</text>
|
||||
<text class="contact-value">每天早上6-9点开播</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-tip">
|
||||
<text>在Soul App搜索"创业实验"或"卡若",加入派对房直接交流</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
43
miniprogram/pages/about/about.wxss
Normal file
43
miniprogram/pages/about/about.wxss
Normal file
@@ -0,0 +1,43 @@
|
||||
.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); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
|
||||
.nav-placeholder { width: 72rpx; }
|
||||
.content { padding: 32rpx; }
|
||||
.loading-row { text-align: center; color: rgba(255,255,255,0.6); font-size: 28rpx; padding: 48rpx 0; }
|
||||
.author-card { background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%); border-radius: 32rpx; padding: 48rpx; text-align: center; margin-bottom: 24rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.author-avatar-wrap { width: 160rpx; height: 160rpx; margin: 0 auto 24rpx; overflow: hidden; border-radius: 50%; border: 4rpx solid rgba(0,206,209,0.3); flex-shrink: 0; }
|
||||
.author-avatar-img { width: 100%; height: 100%; display: block; }
|
||||
.author-avatar { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #00CED1, #20B2AA); display: flex; align-items: center; justify-content: center; font-size: 64rpx; color: #fff; font-weight: 700; }
|
||||
.author-name { font-size: 40rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 8rpx; }
|
||||
.author-title { font-size: 26rpx; color: #00CED1; display: block; margin-bottom: 24rpx; }
|
||||
.author-bio { font-size: 26rpx; color: rgba(255,255,255,0.7); line-height: 1.8; display: block; margin-bottom: 32rpx; }
|
||||
.stats-row { display: flex; justify-content: space-around; padding-top: 32rpx; border-top: 2rpx solid rgba(255,255,255,0.1); }
|
||||
.stat-item { text-align: center; }
|
||||
.stat-value { font-size: 36rpx; font-weight: 700; color: #00CED1; display: block; }
|
||||
.stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
|
||||
.contact-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; }
|
||||
.card-title { font-size: 28rpx; font-weight: 600; color: #fff; display: block; margin-bottom: 24rpx; }
|
||||
.contact-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; margin-bottom: 16rpx; }
|
||||
.contact-item:last-child { margin-bottom: 0; }
|
||||
.contact-icon { font-size: 40rpx; }
|
||||
.contact-info { flex: 1; }
|
||||
.contact-label { font-size: 22rpx; color: rgba(255,255,255,0.5); display: block; }
|
||||
.contact-value { font-size: 28rpx; color: #fff; }
|
||||
.contact-btn { padding: 12rpx 24rpx; background: rgba(0,206,209,0.2); color: #00CED1; font-size: 24rpx; border-radius: 16rpx; }
|
||||
|
||||
/* 亮点标签 */
|
||||
.highlights { display: flex; flex-wrap: wrap; gap: 16rpx; margin-top: 32rpx; padding-top: 24rpx; border-top: 2rpx solid rgba(255,255,255,0.1); justify-content: center; }
|
||||
.highlight-tag { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 24rpx; background: rgba(0,206,209,0.15); border-radius: 24rpx; font-size: 24rpx; color: rgba(255,255,255,0.8); }
|
||||
.tag-icon { color: #00CED1; font-size: 22rpx; }
|
||||
|
||||
/* 书籍信息卡片 */
|
||||
.book-info-card { background: #1c1c1e; border-radius: 32rpx; padding: 32rpx; margin-bottom: 24rpx; }
|
||||
.book-stats { display: flex; justify-content: space-around; padding: 24rpx 0; margin: 16rpx 0; background: rgba(0,0,0,0.3); border-radius: 16rpx; }
|
||||
.book-stat { text-align: center; }
|
||||
.book-stat-value { font-size: 36rpx; font-weight: 700; color: #FFD700; display: block; }
|
||||
.book-stat-label { font-size: 22rpx; color: rgba(255,255,255,0.5); }
|
||||
.parts-list { display: flex; flex-wrap: wrap; gap: 12rpx; margin-top: 16rpx; }
|
||||
.part-item { display: flex; align-items: center; gap: 8rpx; padding: 12rpx 20rpx; background: rgba(255,255,255,0.05); border-radius: 12rpx; }
|
||||
.part-name { font-size: 24rpx; color: rgba(255,255,255,0.8); }
|
||||
.part-chapters { font-size: 22rpx; color: #00CED1; }
|
||||
137
miniprogram/pages/addresses/addresses.js
Normal file
137
miniprogram/pages/addresses/addresses.js
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 收货地址列表页
|
||||
* 参考 Next.js: app/view/my/addresses/page.tsx
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
addressList: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
this.checkLogin()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.isLoggedIn) {
|
||||
this.loadAddresses()
|
||||
}
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLogin() {
|
||||
const isLoggedIn = app.globalData.isLoggedIn
|
||||
const userId = app.globalData.userInfo?.id
|
||||
|
||||
if (!isLoggedIn || !userId) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再管理收货地址',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
} else {
|
||||
getApp().goBackOrToHome()
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
this.setData({ isLoggedIn: true })
|
||||
this.loadAddresses()
|
||||
},
|
||||
|
||||
// 加载地址列表
|
||||
async loadAddresses() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
|
||||
this.setData({ loading: true })
|
||||
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/addresses?userId=${userId}`)
|
||||
if (res.success && res.list) {
|
||||
this.setData({
|
||||
addressList: res.list,
|
||||
loading: false
|
||||
})
|
||||
} else {
|
||||
this.setData({ addressList: [], loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载地址列表失败:', e)
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 编辑地址
|
||||
editAddress(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: `/pages/addresses/edit?id=${id}` })
|
||||
},
|
||||
|
||||
// 删除地址
|
||||
deleteAddress(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
wx.showModal({
|
||||
title: '确认删除',
|
||||
content: '确定要删除该收货地址吗?',
|
||||
confirmColor: '#FF3B30',
|
||||
success: async (res) => {
|
||||
if (res.confirm) {
|
||||
try {
|
||||
const result = await app.request(`/api/miniprogram/user/addresses/${id}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
wx.showToast({ title: '删除成功', icon: 'success' })
|
||||
this.loadAddresses()
|
||||
} else {
|
||||
wx.showToast({ title: result.message || '删除失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('删除地址失败:', e)
|
||||
wx.showToast({ title: '删除失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 新增地址
|
||||
addAddress() {
|
||||
wx.navigateTo({ url: '/pages/addresses/edit' })
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 地址管理',
|
||||
path: ref ? `/pages/addresses/addresses?ref=${ref}` : '/pages/addresses/addresses'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 地址管理', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/addresses/addresses.json
Normal file
5
miniprogram/pages/addresses/addresses.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
66
miniprogram/pages/addresses/addresses.wxml
Normal file
66
miniprogram/pages/addresses/addresses.wxml
Normal file
@@ -0,0 +1,66 @@
|
||||
<!--收货地址列表页-->
|
||||
<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="loading-state" wx:if="{{loading}}">
|
||||
<text class="loading-text">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view class="empty-state" wx:elif="{{addressList.length === 0}}">
|
||||
<text class="empty-icon">📍</text>
|
||||
<text class="empty-text">暂无收货地址</text>
|
||||
<text class="empty-tip">点击下方按钮添加</text>
|
||||
</view>
|
||||
|
||||
<!-- 地址列表 -->
|
||||
<view class="address-list" wx:else>
|
||||
<view
|
||||
class="address-card"
|
||||
wx:for="{{addressList}}"
|
||||
wx:key="id"
|
||||
>
|
||||
<view class="address-header">
|
||||
<text class="receiver-name">{{item.name}}</text>
|
||||
<text class="receiver-phone">{{item.phone}}</text>
|
||||
<text class="default-tag" wx:if="{{item.isDefault}}">默认</text>
|
||||
</view>
|
||||
<text class="address-text">{{item.fullAddress}}</text>
|
||||
<view class="address-actions">
|
||||
<view
|
||||
class="action-btn edit-btn"
|
||||
bindtap="editAddress"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="action-icon">✏️</text>
|
||||
<text class="action-text">编辑</text>
|
||||
</view>
|
||||
<view
|
||||
class="action-btn delete-btn"
|
||||
bindtap="deleteAddress"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="action-icon">🗑️</text>
|
||||
<text class="action-text">删除</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 新增按钮 -->
|
||||
<view class="add-btn" bindtap="addAddress">
|
||||
<text class="add-icon">➕</text>
|
||||
<text class="add-text">新增收货地址</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
217
miniprogram/pages/addresses/addresses.wxss
Normal file
217
miniprogram/pages/addresses/addresses.wxss
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 收货地址列表页样式
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-back:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* ===== 内容区 ===== */
|
||||
.content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 加载状态 ===== */
|
||||
.loading-state {
|
||||
padding: 240rpx 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 空状态 ===== */
|
||||
.empty-state {
|
||||
padding: 240rpx 0;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.empty-icon {
|
||||
font-size: 96rpx;
|
||||
margin-bottom: 24rpx;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 地址列表 ===== */
|
||||
.address-list {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.address-card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 32rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 地址头部 */
|
||||
.address-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.receiver-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.receiver-phone {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.default-tag {
|
||||
font-size: 22rpx;
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.2);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 8rpx;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* 地址文本 */
|
||||
.address-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
line-height: 1.6;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
padding-bottom: 24rpx;
|
||||
border-bottom: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 操作按钮 */
|
||||
.address-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.edit-btn {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
color: #FF3B30;
|
||||
}
|
||||
|
||||
.action-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-text {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* ===== 新增按钮 ===== */
|
||||
.add-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 32rpx;
|
||||
background: #00CED1;
|
||||
border-radius: 24rpx;
|
||||
font-weight: 600;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.add-btn:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.add-icon {
|
||||
font-size: 36rpx;
|
||||
color: #000000;
|
||||
}
|
||||
|
||||
.add-text {
|
||||
font-size: 32rpx;
|
||||
color: #000000;
|
||||
}
|
||||
215
miniprogram/pages/addresses/edit.js
Normal file
215
miniprogram/pages/addresses/edit.js
Normal file
@@ -0,0 +1,215 @@
|
||||
/**
|
||||
* 地址编辑页(新增/编辑)
|
||||
* 参考 Next.js: app/view/my/addresses/[id]/page.tsx
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isEdit: false, // 是否为编辑模式
|
||||
addressId: null,
|
||||
|
||||
// 表单数据
|
||||
name: '',
|
||||
phone: '',
|
||||
province: '',
|
||||
city: '',
|
||||
district: '',
|
||||
detail: '',
|
||||
isDefault: false,
|
||||
|
||||
// 地区选择器
|
||||
region: [],
|
||||
|
||||
saving: false
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
|
||||
// 如果有 id 参数,则为编辑模式
|
||||
if (options.id) {
|
||||
this.setData({
|
||||
isEdit: true,
|
||||
addressId: options.id
|
||||
})
|
||||
this.loadAddress(options.id)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载地址详情(编辑模式)
|
||||
async loadAddress(id) {
|
||||
wx.showLoading({ title: '加载中...', mask: true })
|
||||
|
||||
try {
|
||||
const res = await app.request(`/api/miniprogram/user/addresses/${id}`)
|
||||
if (res.success && res.data) {
|
||||
const addr = res.data
|
||||
this.setData({
|
||||
name: addr.name || '',
|
||||
phone: addr.phone || '',
|
||||
province: addr.province || '',
|
||||
city: addr.city || '',
|
||||
district: addr.district || '',
|
||||
detail: addr.detail || '',
|
||||
isDefault: addr.isDefault || false,
|
||||
region: [addr.province, addr.city, addr.district]
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载地址详情失败:', e)
|
||||
wx.showToast({ title: '加载失败', icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
},
|
||||
|
||||
// 表单输入
|
||||
onNameInput(e) {
|
||||
this.setData({ name: e.detail.value })
|
||||
},
|
||||
|
||||
onPhoneInput(e) {
|
||||
this.setData({ phone: e.detail.value.replace(/\D/g, '').slice(0, 11) })
|
||||
},
|
||||
|
||||
onDetailInput(e) {
|
||||
this.setData({ detail: e.detail.value })
|
||||
},
|
||||
|
||||
// 地区选择
|
||||
onRegionChange(e) {
|
||||
const region = e.detail.value
|
||||
this.setData({
|
||||
region,
|
||||
province: region[0],
|
||||
city: region[1],
|
||||
district: region[2]
|
||||
})
|
||||
},
|
||||
|
||||
// 切换默认地址
|
||||
onDefaultChange(e) {
|
||||
this.setData({ isDefault: e.detail.value })
|
||||
},
|
||||
|
||||
// 表单验证
|
||||
validateForm() {
|
||||
const { name, phone, province, city, district, detail } = this.data
|
||||
|
||||
if (!name || name.trim().length === 0) {
|
||||
wx.showToast({ title: '请输入收货人姓名', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!phone || phone.length !== 11) {
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!province || !city || !district) {
|
||||
wx.showToast({ title: '请选择省市区', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
if (!detail || detail.trim().length === 0) {
|
||||
wx.showToast({ title: '请输入详细地址', icon: 'none' })
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
|
||||
// 保存地址
|
||||
async saveAddress() {
|
||||
if (!this.validateForm()) return
|
||||
if (this.data.saving) return
|
||||
|
||||
this.setData({ saving: true })
|
||||
wx.showLoading({ title: '保存中...', mask: true })
|
||||
|
||||
const { isEdit, addressId, name, phone, province, city, district, detail, isDefault } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
|
||||
if (!userId) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
this.setData({ saving: false })
|
||||
return
|
||||
}
|
||||
|
||||
const addressData = {
|
||||
userId,
|
||||
name,
|
||||
phone,
|
||||
province,
|
||||
city,
|
||||
district,
|
||||
detail,
|
||||
fullAddress: `${province}${city}${district}${detail}`,
|
||||
isDefault
|
||||
}
|
||||
|
||||
try {
|
||||
let res
|
||||
if (isEdit) {
|
||||
// 编辑模式 - PUT 请求
|
||||
res = await app.request(`/api/miniprogram/user/addresses/${addressId}`, {
|
||||
method: 'PUT',
|
||||
data: addressData
|
||||
})
|
||||
} else {
|
||||
// 新增模式 - POST 请求
|
||||
res = await app.request('/api/miniprogram/user/addresses', {
|
||||
method: 'POST',
|
||||
data: addressData
|
||||
})
|
||||
}
|
||||
|
||||
if (res.success) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({
|
||||
title: isEdit ? '保存成功' : '添加成功',
|
||||
icon: 'success'
|
||||
})
|
||||
setTimeout(() => {
|
||||
getApp().goBackOrToHome()
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: res.message || '保存失败', icon: 'none' })
|
||||
this.setData({ saving: false })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('保存地址失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
this.setData({ saving: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 返回
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 编辑地址',
|
||||
path: ref ? `/pages/addresses/edit?ref=${ref}` : '/pages/addresses/edit'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 编辑地址', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
5
miniprogram/pages/addresses/edit.json
Normal file
5
miniprogram/pages/addresses/edit.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enablePullDownRefresh": false
|
||||
}
|
||||
101
miniprogram/pages/addresses/edit.wxml
Normal file
101
miniprogram/pages/addresses/edit.wxml
Normal file
@@ -0,0 +1,101 @@
|
||||
<!--地址编辑页-->
|
||||
<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">{{isEdit ? '编辑地址' : '新增地址'}}</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="form-card">
|
||||
<!-- 收货人 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">👤</text>
|
||||
<text class="label-text">收货人</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入收货人姓名"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{name}}"
|
||||
bindinput="onNameInput"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 手机号 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">📱</text>
|
||||
<text class="label-text">手机号</text>
|
||||
</view>
|
||||
<input
|
||||
class="form-input"
|
||||
type="number"
|
||||
placeholder="请输入11位手机号"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{phone}}"
|
||||
bindinput="onPhoneInput"
|
||||
maxlength="11"
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 地区选择 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">📍</text>
|
||||
<text class="label-text">所在地区</text>
|
||||
</view>
|
||||
<picker
|
||||
mode="region"
|
||||
value="{{region}}"
|
||||
bindchange="onRegionChange"
|
||||
class="region-picker"
|
||||
>
|
||||
<view class="picker-value">
|
||||
{{province || city || district ? province + ' ' + city + ' ' + district : '请选择省市区'}}
|
||||
</view>
|
||||
</picker>
|
||||
</view>
|
||||
|
||||
<!-- 详细地址 -->
|
||||
<view class="form-item">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">🏠</text>
|
||||
<text class="label-text">详细地址</text>
|
||||
</view>
|
||||
<textarea
|
||||
class="form-textarea"
|
||||
placeholder="请输入街道、门牌号等详细地址"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{detail}}"
|
||||
bindinput="onDetailInput"
|
||||
maxlength="200"
|
||||
auto-height
|
||||
/>
|
||||
</view>
|
||||
|
||||
<!-- 设为默认 -->
|
||||
<view class="form-item form-switch">
|
||||
<view class="form-label">
|
||||
<text class="label-icon">⭐</text>
|
||||
<text class="label-text">设为默认地址</text>
|
||||
</view>
|
||||
<switch
|
||||
checked="{{isDefault}}"
|
||||
bindchange="onDefaultChange"
|
||||
color="#00CED1"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 保存按钮 -->
|
||||
<view class="save-btn {{saving ? 'btn-disabled' : ''}}" bindtap="saveAddress">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
186
miniprogram/pages/addresses/edit.wxss
Normal file
186
miniprogram/pages/addresses/edit.wxss
Normal file
@@ -0,0 +1,186 @@
|
||||
/**
|
||||
* 地址编辑页样式
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-back:active {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.back-icon {
|
||||
font-size: 48rpx;
|
||||
color: #ffffff;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 64rpx;
|
||||
}
|
||||
|
||||
/* ===== 内容区 ===== */
|
||||
.content {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 表单卡片 ===== */
|
||||
.form-card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
padding: 32rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
/* 表单项 */
|
||||
.form-item {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.form-item:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.label-icon {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.label-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
/* 输入框 */
|
||||
.form-input {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.input-placeholder {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 地区选择器 */
|
||||
.region-picker {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.picker-value {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.picker-value:empty::before {
|
||||
content: '请选择省市区';
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 多行文本框 */
|
||||
.form-textarea {
|
||||
width: 100%;
|
||||
padding: 24rpx 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
min-height: 160rpx;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.form-textarea:focus {
|
||||
border-color: rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
/* 开关项 */
|
||||
.form-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
.form-switch .form-label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* ===== 保存按钮 ===== */
|
||||
.save-btn {
|
||||
padding: 32rpx;
|
||||
background: #00CED1;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #000000;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.save-btn:active {
|
||||
opacity: 0.8;
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.btn-disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
35
miniprogram/pages/agreement/agreement.js
Normal file
35
miniprogram/pages/agreement/agreement.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Soul创业派对 - 用户协议
|
||||
* 审核要求:登录前可点击《用户协议》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 用户协议',
|
||||
path: ref ? `/pages/agreement/agreement?ref=${ref}` : '/pages/agreement/agreement'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 用户协议', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
1
miniprogram/pages/agreement/agreement.json
Normal file
1
miniprogram/pages/agreement/agreement.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}
|
||||
37
miniprogram/pages/agreement/agreement.wxml
Normal file
37
miniprogram/pages/agreement/agreement.wxml
Normal file
@@ -0,0 +1,37 @@
|
||||
<!--用户协议页 - 审核要求可点击查看-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">←</view>
|
||||
<text class="nav-title">用户协议</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view class="content" scroll-y enhanced show-scrollbar>
|
||||
<view class="doc-card">
|
||||
<text class="doc-title">Soul创业实验 用户服务协议</text>
|
||||
<text class="doc-update">更新日期:以小程序内展示为准</text>
|
||||
|
||||
<text class="doc-section">一、接受条款</text>
|
||||
<text class="doc-p">欢迎使用 Soul创业实验 小程序。使用本服务即表示您已阅读、理解并同意受本协议约束。若不同意,请勿使用本服务。</text>
|
||||
|
||||
<text class="doc-section">二、服务说明</text>
|
||||
<text class="doc-p">本小程序提供《一场Soul的创业实验》等数字内容阅读、推广与相关服务。我们保留变更、中断或终止部分或全部服务的权利。</text>
|
||||
|
||||
<text class="doc-section">三、用户行为规范</text>
|
||||
<text class="doc-p">您应合法、合规使用本服务,不得利用本服务从事违法违规活动,不得侵犯他人权益。违规行为可能导致账号限制或追究责任。</text>
|
||||
|
||||
<text class="doc-section">四、知识产权</text>
|
||||
<text class="doc-p">本小程序内全部内容(包括但不限于文字、图片、音频、视频)的知识产权归本小程序或权利人所有,未经授权不得复制、传播或用于商业用途。</text>
|
||||
|
||||
<text class="doc-section">五、免责与限制</text>
|
||||
<text class="doc-p">在法律允许范围内,因网络、设备或不可抗力导致的服务中断或数据丢失,我们尽力减少损失但不承担超出法律规定的责任。</text>
|
||||
|
||||
<text class="doc-section">六、协议变更</text>
|
||||
<text class="doc-p">我们可能适时修订本协议,修订后将在小程序内公示。若您继续使用服务,即视为接受修订后的协议。</text>
|
||||
|
||||
<text class="doc-section">七、联系我们</text>
|
||||
<text class="doc-p">如有疑问,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
11
miniprogram/pages/agreement/agreement.wxss
Normal file
11
miniprogram/pages/agreement/agreement.wxss
Normal file
@@ -0,0 +1,11 @@
|
||||
.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.95); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
|
||||
.nav-placeholder { width: 72rpx; }
|
||||
.content { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
|
||||
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
|
||||
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
|
||||
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
|
||||
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }
|
||||
365
miniprogram/pages/chapters/chapters.js
Normal file
365
miniprogram/pages/chapters/chapters.js
Normal file
@@ -0,0 +1,365 @@
|
||||
/**
|
||||
* Soul创业派对 - 目录页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
* 数据: 完整真实文章标题
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
purchasedSections: [],
|
||||
|
||||
// 书籍数据 - 完整真实标题
|
||||
totalSections: 62,
|
||||
bookData: [
|
||||
{
|
||||
id: 'part-1',
|
||||
number: '一',
|
||||
title: '真实的人',
|
||||
subtitle: '人与人之间的底层逻辑',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-1',
|
||||
title: '第1章|人与人之间的底层逻辑',
|
||||
sections: [
|
||||
{ id: '1.1', title: '荷包:电动车出租的被动收入模式', isFree: true, price: 1 },
|
||||
{ id: '1.2', title: '老墨:资源整合高手的社交方法', isFree: false, price: 1 },
|
||||
{ id: '1.3', title: '笑声背后的MBTI:为什么ENTJ适合做资源,INTP适合做系统', isFree: false, price: 1 },
|
||||
{ id: '1.4', title: '人性的三角结构:利益、情感、价值观', isFree: false, price: 1 },
|
||||
{ id: '1.5', title: '沟通差的问题:为什么你说的别人听不懂', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-2',
|
||||
title: '第2章|人性困境案例',
|
||||
sections: [
|
||||
{ id: '2.1', title: '相亲故事:你以为找的是人,实际是在找模式', isFree: false, price: 1 },
|
||||
{ id: '2.2', title: '找工作迷茫者:为什么简历解决不了人生', isFree: false, price: 1 },
|
||||
{ id: '2.3', title: '撸运费险:小钱困住大脑的真实心理', isFree: false, price: 1 },
|
||||
{ id: '2.4', title: '游戏上瘾的年轻人:不是游戏吸引他,是生活没吸引力', isFree: false, price: 1 },
|
||||
{ id: '2.5', title: '健康焦虑(我的糖尿病经历):疾病是人生的第一次清醒', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-2',
|
||||
number: '二',
|
||||
title: '真实的行业',
|
||||
subtitle: '电商、内容、传统行业解析',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-3',
|
||||
title: '第3章|电商篇',
|
||||
sections: [
|
||||
{ id: '3.1', title: '3000万流水如何跑出来(退税模式解析)', isFree: false, price: 1 },
|
||||
{ id: '3.2', title: '供应链之王 vs 打工人:利润不在前端', isFree: false, price: 1 },
|
||||
{ id: '3.3', title: '社区团购的底层逻辑', isFree: false, price: 1 },
|
||||
{ id: '3.4', title: '跨境电商与退税套利', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-4',
|
||||
title: '第4章|内容商业篇',
|
||||
sections: [
|
||||
{ id: '4.1', title: '旅游号:30天10万粉的真实逻辑', isFree: false, price: 1 },
|
||||
{ id: '4.2', title: '做号工厂:如何让一个号变成一个机器', isFree: false, price: 1 },
|
||||
{ id: '4.3', title: '情绪内容为什么比专业内容更赚钱', isFree: false, price: 1 },
|
||||
{ id: '4.4', title: '猫与宠物号:为什么宠物赛道永不过时', isFree: false, price: 1 },
|
||||
{ id: '4.5', title: '直播间里的三种人:演员、技术工、系统流', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-5',
|
||||
title: '第5章|传统行业篇',
|
||||
sections: [
|
||||
{ id: '5.1', title: '拍卖行抱朴:一天240万的摇号生意', isFree: false, price: 1 },
|
||||
{ id: '5.2', title: '土地拍卖:招拍挂背后的游戏规则', isFree: false, price: 1 },
|
||||
{ id: '5.3', title: '地摊经济数字化:一个月900块的餐车生意', isFree: false, price: 1 },
|
||||
{ id: '5.4', title: '不良资产拍卖:我错过的一个亿佣金', isFree: false, price: 1 },
|
||||
{ id: '5.5', title: '桶装水李总:跟物业合作的轻资产模式', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-3',
|
||||
number: '三',
|
||||
title: '真实的错误',
|
||||
subtitle: '我和别人犯过的错',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-6',
|
||||
title: '第6章|我人生错过的4件大钱',
|
||||
sections: [
|
||||
{ id: '6.1', title: '电商财税窗口:2016年的千万级机会', isFree: false, price: 1 },
|
||||
{ id: '6.2', title: '供应链金融:我不懂的杠杆游戏', isFree: false, price: 1 },
|
||||
{ id: '6.3', title: '内容红利:2019年我为什么没做抖音', isFree: false, price: 1 },
|
||||
{ id: '6.4', title: '数据资产化:我还在观望的未来机会', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-7',
|
||||
title: '第7章|别人犯的错误',
|
||||
sections: [
|
||||
{ id: '7.1', title: '投资房年轻人的迷茫:资金 vs 能力', isFree: false, price: 1 },
|
||||
{ id: '7.2', title: '信息差骗局:永远有人靠卖学习赚钱', isFree: false, price: 1 },
|
||||
{ id: '7.3', title: '在Soul找恋爱但想赚钱的人', isFree: false, price: 1 },
|
||||
{ id: '7.4', title: '创业者的三种死法:冲动、轻信、没结构', isFree: false, price: 1 },
|
||||
{ id: '7.5', title: '人情生意的终点:关系越多亏得越多', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-4',
|
||||
number: '四',
|
||||
title: '真实的赚钱',
|
||||
subtitle: '底层结构与真实案例',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-8',
|
||||
title: '第8章|底层结构',
|
||||
sections: [
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', isFree: false, price: 1 },
|
||||
{ id: '8.2', title: '价格杠杆:供应链与信息差', isFree: false, price: 1 },
|
||||
{ id: '8.3', title: '时间杠杆:自动化 + AI', isFree: false, price: 1 },
|
||||
{ id: '8.4', title: '情绪杠杆:咨询、婚恋、生意场', isFree: false, price: 1 },
|
||||
{ id: '8.5', title: '社交杠杆:认识谁比你会什么更重要', isFree: false, price: 1 },
|
||||
{ id: '8.6', title: '云阿米巴:分不属于自己的钱', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-9',
|
||||
title: '第9章|我在Soul上亲访的赚钱案例',
|
||||
sections: [
|
||||
{ id: '9.1', title: '游戏账号私域:账号即资产', isFree: false, price: 1 },
|
||||
{ id: '9.2', title: '健康包模式:高复购、高毛利', isFree: false, price: 1 },
|
||||
{ id: '9.3', title: '药物私域:长期关系赛道', isFree: false, price: 1 },
|
||||
{ id: '9.4', title: '残疾机构合作:退税 × AI × 人力成本', isFree: false, price: 1 },
|
||||
{ id: '9.5', title: '私域银行:粉丝即小股东', isFree: false, price: 1 },
|
||||
{ id: '9.6', title: 'Soul派对房:陌生人成交的最快场景', isFree: false, price: 1 },
|
||||
{ id: '9.7', title: '飞书中台:从聊天到成交的流程化体系', isFree: false, price: 1 },
|
||||
{ id: '9.8', title: '餐饮女孩:6万营收、1万利润的死撑生意', isFree: false, price: 1 },
|
||||
{ id: '9.9', title: '电竞生态:从陪玩到签约到酒店的完整链条', isFree: false, price: 1 },
|
||||
{ id: '9.10', title: '淘客大佬:损耗30%的白色通道', isFree: false, price: 1 },
|
||||
{ id: '9.11', title: '蔬菜供应链:农户才是最赚钱的人', isFree: false, price: 1 },
|
||||
{ id: '9.12', title: '美业整合:一个人的公司如何月入十万', isFree: false, price: 1 },
|
||||
{ id: '9.13', title: 'AI工具推广:一个隐藏的高利润赛道', isFree: false, price: 1 },
|
||||
{ id: '9.14', title: '大健康私域:一个月150万的70后', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'part-5',
|
||||
number: '五',
|
||||
title: '真实的社会',
|
||||
subtitle: '未来职业与商业生态',
|
||||
chapters: [
|
||||
{
|
||||
id: 'chapter-10',
|
||||
title: '第10章|未来职业的变化趋势',
|
||||
sections: [
|
||||
{ id: '10.1', title: 'AI时代:哪些工作会消失,哪些会崛起', isFree: false, price: 1 },
|
||||
{ id: '10.2', title: '一人公司:为什么越来越多人选择单干', isFree: false, price: 1 },
|
||||
{ id: '10.3', title: '为什么链接能力会成为第一价值', isFree: false, price: 1 },
|
||||
{ id: '10.4', title: '新型公司:Soul-飞书-线下的三位一体', isFree: false, price: 1 }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'chapter-11',
|
||||
title: '第11章|中国社会商业生态的未来',
|
||||
sections: [
|
||||
{ id: '11.1', title: '私域经济:为什么流量越来越贵', isFree: false, price: 1 },
|
||||
{ id: '11.2', title: '银发经济与孤独经济:两个被忽视的万亿市场', isFree: false, price: 1 },
|
||||
{ id: '11.3', title: '流量红利的终局', isFree: false, price: 1 },
|
||||
{ id: '11.4', title: '大模型 + 供应链的组合拳', isFree: false, price: 1 },
|
||||
{ id: '11.5', title: '社会分层的最终逻辑', isFree: false, price: 1 }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
|
||||
// 展开状态:默认不展开任何篇章,直接显示目录
|
||||
expandedPart: null,
|
||||
|
||||
// 附录
|
||||
appendixList: [
|
||||
{ id: 'appendix-1', title: '附录1|Soul派对房精选对话' },
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单' },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源' }
|
||||
],
|
||||
|
||||
// 每日新增章节
|
||||
dailyChapters: []
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.updateUserStatus()
|
||||
this.loadChaptersOnce()
|
||||
},
|
||||
|
||||
// 固定模块(序言、尾声、附录)不参与中间篇章
|
||||
_isFixedPart(pt) {
|
||||
if (!pt) return false
|
||||
const p = String(pt).toLowerCase().replace(/[_\s||]/g, '')
|
||||
return p.includes('序言') || p.includes('尾声') || p.includes('附录')
|
||||
},
|
||||
|
||||
// 一次请求拉取全量目录,同时更新 totalSections / bookData / dailyChapters
|
||||
async loadChaptersOnce() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const rows = (res && res.data) || (res && res.chapters) || []
|
||||
if (rows.length === 0) return
|
||||
|
||||
// 1. totalSections
|
||||
const totalSections = res.total ?? rows.length
|
||||
|
||||
// 2. bookData(过滤序言/尾声/附录,中间篇章按 part 聚合)
|
||||
const filtered = rows.filter(r => !this._isFixedPart(r.partTitle || r.part_title))
|
||||
const partMap = new Map()
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
||||
filtered.forEach((r) => {
|
||||
const pid = r.partId || r.part_id || 'part-1'
|
||||
const cid = r.chapterId || r.chapter_id || 'chapter-1'
|
||||
if (!partMap.has(pid)) {
|
||||
const partIdx = partMap.size
|
||||
partMap.set(pid, {
|
||||
id: pid,
|
||||
number: numbers[partIdx] || String(partIdx + 1),
|
||||
title: r.partTitle || r.part_title || '未分类',
|
||||
subtitle: r.chapterTitle || r.chapter_title || '',
|
||||
chapters: new Map()
|
||||
})
|
||||
}
|
||||
const part = partMap.get(pid)
|
||||
if (!part.chapters.has(cid)) {
|
||||
part.chapters.set(cid, {
|
||||
id: cid,
|
||||
title: r.chapterTitle || r.chapter_title || '未分类',
|
||||
sections: []
|
||||
})
|
||||
}
|
||||
const ch = part.chapters.get(cid)
|
||||
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
|
||||
})
|
||||
})
|
||||
const bookData = Array.from(partMap.values()).map(p => ({
|
||||
...p,
|
||||
chapters: Array.from(p.chapters.values())
|
||||
}))
|
||||
const firstPart = bookData[0] && bookData[0].id
|
||||
|
||||
// 3. dailyChapters(sort_order > 62 的新增章节,按更新时间取前20)
|
||||
const baseSort = 62
|
||||
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))
|
||||
.slice(0, 20)
|
||||
.map(c => {
|
||||
const d = new Date(c.updatedAt || c.updated_at || Date.now())
|
||||
return {
|
||||
id: c.id,
|
||||
mid: c.mid ?? c.MID ?? 0,
|
||||
title: c.section_title || c.title || c.sectionTitle,
|
||||
price: c.price ?? 1,
|
||||
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
|
||||
this.setData({
|
||||
bookData,
|
||||
totalSections,
|
||||
dailyChapters: daily,
|
||||
expandedPart: this.data.expandedPart
|
||||
})
|
||||
} catch (e) { console.log('[Chapters] 加载目录失败:', e) }
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
tabBar.setData({ selected: 1 })
|
||||
}
|
||||
}
|
||||
this.updateUserStatus()
|
||||
},
|
||||
|
||||
// 更新用户状态
|
||||
updateUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
this.setData({ isLoggedIn, hasFullBook, purchasedSections })
|
||||
},
|
||||
|
||||
// 切换展开状态
|
||||
togglePart(e) {
|
||||
const partId = e.currentTarget.dataset.id
|
||||
this.setData({
|
||||
expandedPart: this.data.expandedPart === partId ? null : partId
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 检查是否已购买
|
||||
hasPurchased(sectionId) {
|
||||
if (this.data.hasFullBook) return true
|
||||
return this.data.purchasedSections.includes(sectionId)
|
||||
},
|
||||
|
||||
// 返回首页
|
||||
goBack() {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 目录',
|
||||
path: ref ? `/pages/chapters/chapters?ref=${ref}` : '/pages/chapters/chapters'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/chapters/chapters.json
Normal file
6
miniprogram/pages/chapters/chapters.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
127
miniprogram/pages/chapters/chapters.wxml
Normal file
127
miniprogram/pages/chapters/chapters.wxml
Normal file
@@ -0,0 +1,127 @@
|
||||
<!--pages/chapters/chapters.wxml-->
|
||||
<!--Soul创业实验 - 目录页 1:1还原Web版本-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-left">
|
||||
<view class="search-btn" bindtap="goToSearch">
|
||||
<text class="search-icon">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-title brand-color">目录</view>
|
||||
<view class="nav-right"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 书籍信息卡 -->
|
||||
<view class="book-info-card card-gradient">
|
||||
<view class="book-icon">
|
||||
<view class="book-icon-inner">📚</view>
|
||||
</view>
|
||||
<view class="book-info">
|
||||
<text class="book-title">一场SOUL的创业实验场</text>
|
||||
<text class="book-subtitle">来自Soul派对房的真实商业故事</text>
|
||||
</view>
|
||||
<view class="book-count">
|
||||
<text class="count-value brand-color">{{totalSections}}</text>
|
||||
<text class="count-label">章节</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 目录内容 -->
|
||||
<view class="chapters-content">
|
||||
<!-- 序言 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="preface">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand">📖</view>
|
||||
<text class="item-title">序言|为什么我每天早上6点在Soul开播?</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<text class="item-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 篇章列表 -->
|
||||
<view class="part-list">
|
||||
<view class="part-item" wx:for="{{bookData}}" wx:key="id">
|
||||
<!-- 篇章标题 -->
|
||||
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
|
||||
<view class="part-left">
|
||||
<view class="part-icon">{{item.number}}</view>
|
||||
<view class="part-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle">{{item.subtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="part-right">
|
||||
<text class="part-count">{{item.chapters.length}}章</text>
|
||||
<text class="part-arrow {{expandedPart === item.id ? 'arrow-down' : ''}}">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 章节列表 - 展开时显示 -->
|
||||
<block wx:if="{{expandedPart === item.id}}">
|
||||
<view class="chapters-list">
|
||||
<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 wx:if="{{section.isNew}}" class="tag tag-new">NEW</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:else class="section-price">¥{{section.price}}</text>
|
||||
<text class="section-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 尾声 -->
|
||||
<view class="chapter-item" bindtap="goToRead" data-id="epilogue">
|
||||
<view class="item-left">
|
||||
<view class="item-icon icon-brand">📖</view>
|
||||
<text class="item-title">尾声|这本书的真实目的</text>
|
||||
</view>
|
||||
<view class="item-right">
|
||||
<text class="tag tag-free">免费</text>
|
||||
<text class="item-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 附录 -->
|
||||
<view class="appendix-card card">
|
||||
<text class="appendix-title">附录</text>
|
||||
<view class="appendix-list">
|
||||
<view
|
||||
class="appendix-item"
|
||||
wx:for="{{appendixList}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<text class="appendix-text">{{item.title}}</text>
|
||||
<text class="appendix-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
505
miniprogram/pages/chapters/chapters.wxss
Normal file
505
miniprogram/pages/chapters/chapters.wxss
Normal file
@@ -0,0 +1,505 @@
|
||||
/**
|
||||
* Soul创业实验 - 目录页样式
|
||||
* 1:1还原Web版本UI
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 自定义导航栏 ===== */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
backdrop-filter: blur(40rpx);
|
||||
-webkit-backdrop-filter: blur(40rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.nav-left,
|
||||
.nav-right {
|
||||
width: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 搜索按钮 */
|
||||
.search-btn {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: #2c2c2e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.search-btn:active {
|
||||
background: #3c3c3e;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 书籍信息卡 ===== */
|
||||
.book-info-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
margin: 32rpx 32rpx 24rpx 32rpx;
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
.card-gradient {
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.book-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.book-icon-inner {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
|
||||
.book-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.book-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.book-count {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.count-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 目录内容 ===== */
|
||||
.chapters-content {
|
||||
padding: 0 32rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 章节项 ===== */
|
||||
.chapter-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.chapter-item:active {
|
||||
background: #2c2c2e;
|
||||
}
|
||||
|
||||
.item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 32rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.icon-brand {
|
||||
background: rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.item-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 标签 ===== */
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
min-width: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag-free {
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.tag-new {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
font-size: 20rpx;
|
||||
padding: 2rpx 8rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.text-brand {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.text-muted {
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* ===== 篇章列表 ===== */
|
||||
.part-list {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.part-item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.part-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 24rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.part-header:active {
|
||||
background: #2c2c2e;
|
||||
}
|
||||
|
||||
.part-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.part-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.part-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.part-subtitle {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
.part-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.part-count {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.part-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.arrow-down {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* ===== 章节组 ===== */
|
||||
.chapters-list {
|
||||
margin-top: 16rpx;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.chapter-group {
|
||||
background: rgba(28, 28, 30, 0.5);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
overflow: hidden;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.chapter-header {
|
||||
padding: 16rpx 24rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-list {
|
||||
/* 小节列表 */
|
||||
}
|
||||
|
||||
.section-item {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx 24rpx;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.section-item:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.section-left {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 小节锁图标 */
|
||||
.section-lock {
|
||||
width: 32rpx;
|
||||
min-width: 32rpx;
|
||||
font-size: 24rpx;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lock-open {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.lock-closed {
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* 小节标题 */
|
||||
.section-title {
|
||||
font-size: 26rpx;
|
||||
color: #ffffff;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 小节价格 */
|
||||
.section-price {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
/* 已购标签 */
|
||||
.tag-purchased {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.section-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
flex-shrink: 0;
|
||||
margin-left: 16rpx;
|
||||
}
|
||||
|
||||
.section-arrow {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 附录 ===== */
|
||||
.card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin: 0 0 24rpx 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.appendix-card {
|
||||
padding: 24rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 0 0 24rpx 0;
|
||||
}
|
||||
|
||||
.appendix-title {
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.appendix-list {
|
||||
/* 附录列表 */
|
||||
}
|
||||
|
||||
.appendix-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 0;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.appendix-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.appendix-item:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.appendix-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.appendix-arrow {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 每日新增章节 ===== */
|
||||
.daily-section { margin: 20rpx 0; padding: 24rpx; background: rgba(255,215,0,0.04); border: 1rpx solid rgba(255,215,0,0.12); border-radius: 16rpx; }
|
||||
.daily-header { display: flex; align-items: center; gap: 12rpx; margin-bottom: 16rpx; }
|
||||
.daily-title { font-size: 30rpx; font-weight: 600; color: #FFD700; }
|
||||
.daily-badge { font-size: 22rpx; background: #FFD700; color: #000; padding: 2rpx 12rpx; border-radius: 20rpx; font-weight: bold; }
|
||||
.daily-list { display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.daily-item { display: flex; justify-content: space-between; align-items: center; padding: 16rpx; background: rgba(255,255,255,0.03); border-radius: 12rpx; }
|
||||
.daily-left { display: flex; align-items: center; gap: 10rpx; flex: 1; min-width: 0; }
|
||||
.daily-new-tag { font-size: 18rpx; background: #FF4444; color: #fff; padding: 2rpx 8rpx; border-radius: 6rpx; font-weight: bold; flex-shrink: 0; }
|
||||
.daily-item-title { font-size: 26rpx; color: rgba(255,255,255,0.85); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.daily-right { display: flex; align-items: center; gap: 12rpx; flex-shrink: 0; }
|
||||
.daily-price { font-size: 26rpx; color: #FFD700; font-weight: 600; }
|
||||
.daily-date { font-size: 20rpx; color: rgba(255,255,255,0.35); }
|
||||
.daily-note { display: block; font-size: 22rpx; color: rgba(255,215,0,0.5); margin-top: 12rpx; text-align: center; }
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
}
|
||||
504
miniprogram/pages/index/index.js
Normal file
504
miniprogram/pages/index/index.js
Normal file
@@ -0,0 +1,504 @@
|
||||
/**
|
||||
* Soul创业派对 - 首页
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户信息
|
||||
isLoggedIn: false,
|
||||
hasFullBook: false,
|
||||
readCount: 0,
|
||||
|
||||
// 书籍数据
|
||||
totalSections: 62,
|
||||
bookData: [],
|
||||
|
||||
// 推荐章节
|
||||
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,
|
||||
latestLabel: '最新更新',
|
||||
|
||||
// 内容概览
|
||||
partsList: [
|
||||
{ id: 'part-1', number: '一', title: '真实的人', subtitle: '人与人之间的底层逻辑' },
|
||||
{ id: 'part-2', number: '二', title: '真实的行业', subtitle: '电商、内容、传统行业解析' },
|
||||
{ id: 'part-3', number: '三', title: '真实的错误', subtitle: '我和别人犯过的错' },
|
||||
{ id: 'part-4', number: '四', title: '真实的赚钱', subtitle: '底层结构与真实案例' },
|
||||
{ id: 'part-5', number: '五', title: '真实的社会', subtitle: '未来职业与商业生态' }
|
||||
],
|
||||
|
||||
// 超级个体(VIP会员)
|
||||
superMembers: [],
|
||||
superMembersLoading: true,
|
||||
|
||||
// 最新新增章节
|
||||
latestChapters: [],
|
||||
|
||||
// 篇章数(从 bookData 计算)
|
||||
partCount: 0,
|
||||
|
||||
// 加载状态
|
||||
loading: true,
|
||||
|
||||
// 链接卡若 - 留资弹窗
|
||||
showLeadModal: false,
|
||||
leadPhone: ''
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
console.log('[Index] ===== onLoad 触发 =====')
|
||||
|
||||
// 获取系统信息
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
|
||||
// 处理分享参数(推荐码绑定)
|
||||
if (options && options.ref) {
|
||||
console.log('[Index] 检测到推荐码:', options.ref)
|
||||
app.handleReferralCode({ query: options })
|
||||
}
|
||||
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.initData()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
console.log('[Index] TabBar 组件:', tabBar ? '已找到' : '未找到')
|
||||
|
||||
// 主动触发配置加载
|
||||
if (tabBar && tabBar.loadFeatureConfig) {
|
||||
console.log('[Index] 主动调用 TabBar.loadFeatureConfig()')
|
||||
tabBar.loadFeatureConfig()
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
if (tabBar && tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else if (tabBar) {
|
||||
tabBar.setData({ selected: 0 })
|
||||
}
|
||||
} else {
|
||||
console.log('[Index] TabBar 组件未找到或 getTabBar 方法不存在')
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
this.updateUserStatus()
|
||||
},
|
||||
|
||||
// 初始化数据:首次进页面并行异步加载,加快首屏展示
|
||||
initData() {
|
||||
this.setData({ loading: false })
|
||||
this.loadBookData()
|
||||
this.loadFeaturedFromServer()
|
||||
this.loadSuperMembers()
|
||||
this.loadLatestChapters()
|
||||
},
|
||||
|
||||
async loadSuperMembers() {
|
||||
this.setData({ superMembersLoading: true })
|
||||
try {
|
||||
// 优先加载 VIP 会员(购买 1980 fullbook/vip 订单的用户)
|
||||
let members = []
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/vip/members', silent: true })
|
||||
if (res && res.success && res.data) {
|
||||
// 不再过滤无头像用户,无头像时用首字母展示
|
||||
members = (Array.isArray(res.data) ? res.data : []).slice(0, 4).map(u => ({
|
||||
id: u.id,
|
||||
name: u.nickname || u.vipName || u.vip_name || '会员', // 超级个体:用户资料优先,随「我的」修改实时生效
|
||||
avatar: u.avatar || '',
|
||||
isVip: true
|
||||
}))
|
||||
if (members.length > 0) {
|
||||
console.log('[Index] 超级个体加载成功:', members.length, '人')
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Index] vip/members 请求失败:', e)
|
||||
}
|
||||
// 不足 4 个则用有头像的普通用户补充
|
||||
if (members.length < 4) {
|
||||
try {
|
||||
const dbRes = await app.request({ url: '/api/miniprogram/users?limit=20', silent: true })
|
||||
if (dbRes && dbRes.success && dbRes.data) {
|
||||
const existIds = new Set(members.map(m => m.id))
|
||||
const extra = (Array.isArray(dbRes.data) ? dbRes.data : [])
|
||||
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
|
||||
.slice(0, 4 - members.length)
|
||||
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
|
||||
members = members.concat(extra)
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
this.setData({ superMembers: members, superMembersLoading: false })
|
||||
} catch (e) {
|
||||
console.log('[Index] 加载超级个体失败:', e)
|
||||
this.setData({ superMembersLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 从服务端获取精选推荐、最新更新(stitch_soul:book/recommended、book/latest-chapters)
|
||||
async loadFeaturedFromServer() {
|
||||
try {
|
||||
// 1. 精选推荐:优先用 book/recommended(按阅读量+算法,带 热门/推荐/精选 标签)
|
||||
let featured = []
|
||||
try {
|
||||
const recRes = await app.request({ url: '/api/miniprogram/book/recommended', silent: true })
|
||||
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
|
||||
featured = recRes.data.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: s.tag || ['热门', '推荐', '精选'][i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
}))
|
||||
this.setData({ featuredSections: featured })
|
||||
}
|
||||
} catch (e) { console.log('[Index] book/recommended 失败:', e) }
|
||||
|
||||
// 兜底:无 recommended 时从 all-chapters 按更新时间取前3
|
||||
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) || []
|
||||
const valid = chapters.filter(c => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (valid.length > 0) {
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
featured = valid
|
||||
.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
.slice(0, 3)
|
||||
.map((s, i) => ({
|
||||
id: s.id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
}))
|
||||
this.setData({ featuredSections: featured })
|
||||
}
|
||||
}
|
||||
|
||||
// 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 : []
|
||||
if (latestList.length > 0) {
|
||||
const l = latestList[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: l.id,
|
||||
mid: l.mid ?? l.MID ?? 0,
|
||||
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
|
||||
part: l.part_title || l.partTitle || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
// 兜底:从 all-chapters 取
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const chapters = (res && res.data) || (res && res.chapters) || []
|
||||
const valid = chapters.filter(c => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
})
|
||||
if (valid.length > 0) {
|
||||
valid.sort((a, b) => new Date(b.updated_at || b.updatedAt || 0) - new Date(a.updated_at || a.updatedAt || 0))
|
||||
const latest = valid[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
id: latest.id,
|
||||
mid: latest.mid ?? latest.MID ?? 0,
|
||||
title: latest.section_title || latest.sectionTitle || latest.title || latest.chapterTitle || '',
|
||||
part: latest.part_title || latest.partTitle || ''
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Index] 从服务端加载推荐失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
async loadBookData() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
if (res && (res.data || res.chapters)) {
|
||||
const chapters = res.data || res.chapters || []
|
||||
const partIds = new Set(chapters.map(c => c.partId || c.part_id || '').filter(Boolean))
|
||||
this.setData({
|
||||
bookData: chapters,
|
||||
totalSections: res.total || chapters.length || 62,
|
||||
partCount: partIds.size || 5
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载书籍数据失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 更新用户状态(已读数 = 用户实际打开过的章节数,仅统计有权限阅读的)
|
||||
updateUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
const readCount = Math.min(app.getReadCount(), this.data.totalSections || 62)
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
readCount
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 跳转到搜索页
|
||||
goToSearch() {
|
||||
wx.navigateTo({ url: '/pages/search/search' })
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 跳转到匹配页
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
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) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再链接卡若',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const leadKey = 'karuo_lead_' + userId
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
phone = (profileRes.data.phone || '').trim()
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
|
||||
}
|
||||
} 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',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechatId || undefined,
|
||||
name: (app.globalData.userInfo.nickname || '').trim() || undefined
|
||||
}
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync(leadKey, true)
|
||||
wx.showToast({ title: res.message || '提交成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
return
|
||||
}
|
||||
this.setData({ showLeadModal: true, leadPhone: '' })
|
||||
},
|
||||
|
||||
closeLeadModal() {
|
||||
this.setData({ showLeadModal: false, leadPhone: '' })
|
||||
},
|
||||
|
||||
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' })
|
||||
return
|
||||
}
|
||||
if (phone.length < 11) {
|
||||
wx.showToast({ title: '请输入正确的手机号', 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',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId,
|
||||
phone,
|
||||
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.showToast({ title: res.message || '提交成功,卡若会尽快联系您', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '提交失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
goToSuperList() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
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 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)
|
||||
.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,
|
||||
price: c.price ?? 1,
|
||||
dateStr: `${d.getMonth() + 1}/${d.getDate()}`
|
||||
}
|
||||
})
|
||||
this.setData({ latestChapters: latest })
|
||||
} catch (e) { console.log('[Index] 加载最新新增失败:', e) }
|
||||
},
|
||||
|
||||
goToMemberDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${id}` })
|
||||
},
|
||||
|
||||
// 跳转到我的页面
|
||||
goToMy() {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
},
|
||||
|
||||
// 下拉刷新(等待各异步加载完成后再结束)
|
||||
async onPullDownRefresh() {
|
||||
await Promise.all([
|
||||
this.loadBookData(),
|
||||
this.loadFeaturedFromServer(),
|
||||
this.loadSuperMembers(),
|
||||
this.loadLatestChapters()
|
||||
])
|
||||
this.updateUserStatus()
|
||||
wx.stopPullDownRefresh()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 真实商业故事', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/index/index.json
Normal file
6
miniprogram/pages/index/index.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": true,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
198
miniprogram/pages/index/index.wxml
Normal file
198
miniprogram/pages/index/index.wxml
Normal file
@@ -0,0 +1,198 @@
|
||||
<!--pages/index/index.wxml-->
|
||||
<!--Soul创业派对 - 首页(按临时需求池/首页页面设计)-->
|
||||
<view class="page page-transition">
|
||||
<!-- 自定义导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部区域(按设计稿:S 图标 + 标题副标题 | 点击链接卡若 + 章数) -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="logo-section">
|
||||
<view class="logo-icon">
|
||||
<text class="logo-text">S</text>
|
||||
</view>
|
||||
<view class="logo-info">
|
||||
<text class="logo-title-text">Soul创业派对</text>
|
||||
<text class="logo-subtitle">来自派对房的真实故事</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="contact-btn" bindtap="onLinkKaruo">
|
||||
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
|
||||
<text class="contact-text">点击链接卡若</text>
|
||||
</view>
|
||||
<view class="chapter-badge">{{totalSections}}章</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<view class="search-bar" bindtap="goToSearch">
|
||||
<view class="search-icon-wrap">
|
||||
<text class="search-icon-text">🔍</text>
|
||||
</view>
|
||||
<text class="search-placeholder">搜索章节标题或内容...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- Banner卡片 - 最新章节(异步加载) -->
|
||||
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
<view class="banner-action">
|
||||
<text class="banner-action-text">开始阅读</text>
|
||||
<view class="banner-arrow">→</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">最新更新</view>
|
||||
<view class="banner-title">加载中...</view>
|
||||
<view class="banner-action"><text class="banner-action-text">开始阅读</text><view class="banner-arrow">→</view></view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读进度卡 -->
|
||||
<view class="progress-card card">
|
||||
<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: {{totalSections > 0 ? (readCount / totalSections) * 100 : 0}}%;"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="progress-stats">
|
||||
<view class="stat-item">
|
||||
<text class="stat-value brand-color">{{readCount}}</text>
|
||||
<text class="stat-label">已读</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{totalSections - readCount}}</text>
|
||||
<text class="stat-label">待读</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{partCount}}</text>
|
||||
<text class="stat-label">篇章</text>
|
||||
</view>
|
||||
<view class="stat-item">
|
||||
<text class="stat-value">{{totalSections}}</text>
|
||||
<text class="stat-label">章节</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">超级个体</text>
|
||||
</view>
|
||||
<!-- 加载中:骨架动画 -->
|
||||
<view wx:if="{{superMembersLoading}}" class="super-loading">
|
||||
<view class="super-loading-inner">
|
||||
<view class="super-loading-item" wx:for="{{[1,2,3,4]}}" wx:key="*this">
|
||||
<view class="super-loading-avatar"></view>
|
||||
<view class="super-loading-name"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 已加载有数据 -->
|
||||
<scroll-view wx:elif="{{superMembers.length > 0}}" class="super-scroll" scroll-x>
|
||||
<view class="super-scroll-inner">
|
||||
<view
|
||||
class="super-item-h"
|
||||
wx:for="{{superMembers}}"
|
||||
wx:key="id"
|
||||
bindtap="goToMemberDetail"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.name[0] || '会'}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<!-- 已加载无数据 -->
|
||||
<view wx:else class="super-empty">
|
||||
<text class="super-empty-text">成为会员,展示你的项目</text>
|
||||
<view class="super-empty-btn" bindtap="goToVip">加入创业派对 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选推荐(带 tag,已去掉「查看全部」) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选推荐</text>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
class="featured-item"
|
||||
wx:for="{{featuredSections}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag || '精选'}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="featured-arrow">›</view>
|
||||
</view>
|
||||
</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>
|
||||
</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-dot"></view>
|
||||
<view class="timeline-content">
|
||||
<view class="timeline-row">
|
||||
<view class="timeline-left">
|
||||
<text class="latest-new-tag">NEW</text>
|
||||
<text class="timeline-title">{{item.title}}</text>
|
||||
</view>
|
||||
<view class="timeline-right">
|
||||
<text class="timeline-price">¥{{item.price}}</text>
|
||||
<text class="timeline-date">{{item.dateStr}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
|
||||
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时) -->
|
||||
<view class="lead-mask" wx:if="{{showLeadModal}}" catchtap="closeLeadModal">
|
||||
<view class="lead-box" catchtap="">
|
||||
<text class="lead-title">留下联系方式</text>
|
||||
<text class="lead-desc">方便卡若与您联系</text>
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
<view class="lead-actions">
|
||||
<button class="lead-btn lead-btn-cancel" bindtap="closeLeadModal">取消</button>
|
||||
<button class="lead-btn lead-btn-submit" bindtap="submitLead">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
914
miniprogram/pages/index/index.wxss
Normal file
914
miniprogram/pages/index/index.wxss
Normal file
@@ -0,0 +1,914 @@
|
||||
/**
|
||||
* Soul创业实验 - 首页样式
|
||||
* 1:1还原Web版本UI
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 导航栏占位 ===== */
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 顶部区域 ===== */
|
||||
.header {
|
||||
padding: 0 32rpx 32rpx;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
padding-top: 24rpx;
|
||||
}
|
||||
|
||||
.logo-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 20rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
color: #000000;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.logo-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.logo-title-text {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.contact-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 20rpx 8rpx 12rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text-white {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.logo-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.chapter-badge {
|
||||
font-size: 22rpx;
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
padding: 8rpx 16rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
/* ===== 搜索栏 ===== */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 32rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.search-icon-wrap {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.search-icon-text {
|
||||
font-size: 24rpx;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.search-placeholder {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.main-content {
|
||||
padding: 0 32rpx;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.main-content > .banner-card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.main-content > .card {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* ===== Banner卡片 ===== */
|
||||
.banner-card {
|
||||
position: relative;
|
||||
padding: 40rpx;
|
||||
border-radius: 32rpx;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, #0d3331 0%, #1a1a2e 50%, #16213e 100%);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.banner-skeleton .banner-title {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.banner-glow {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 256rpx;
|
||||
height: 256rpx;
|
||||
background: #00CED1;
|
||||
border-radius: 50%;
|
||||
filter: blur(120rpx);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.banner-tag {
|
||||
display: inline-block;
|
||||
padding: 8rpx 16rpx;
|
||||
background: #00CED1;
|
||||
color: #000000;
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
border-radius: 8rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.banner-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
margin-bottom: 16rpx;
|
||||
padding-right: 64rpx;
|
||||
}
|
||||
|
||||
.banner-part {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.banner-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.banner-action-text {
|
||||
font-size: 28rpx;
|
||||
color: #00CED1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.banner-arrow {
|
||||
color: #00CED1;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.card {
|
||||
background: #1c1c1e;
|
||||
border-radius: 32rpx;
|
||||
padding: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin: 0 0 24rpx 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* ===== 阅读进度卡 ===== */
|
||||
.progress-card {
|
||||
width: 100%;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
margin: 0 0 24rpx 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.progress-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.progress-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-count {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.progress-bar-wrapper {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.progress-bar-bg {
|
||||
width: 100%;
|
||||
height: 16rpx;
|
||||
background: #2c2c2e;
|
||||
border-radius: 8rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-bar-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
|
||||
border-radius: 8rpx;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.progress-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===== 区块标题 ===== */
|
||||
.section {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.more-text {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.more-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 精选推荐列表 ===== */
|
||||
.featured-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.featured-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.featured-item:active {
|
||||
transform: scale(0.98);
|
||||
background: #2c2c2e;
|
||||
}
|
||||
|
||||
.featured-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.featured-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
.featured-id {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.featured-tag {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.tag-hot {
|
||||
background: rgba(246, 173, 85, 0.15);
|
||||
color: #F6AD55;
|
||||
}
|
||||
|
||||
.tag-rec {
|
||||
background: rgba(0, 206, 209, 0.15);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
min-width: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag-free {
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.tag-pink {
|
||||
background: rgba(233, 30, 99, 0.1);
|
||||
color: #E91E63;
|
||||
}
|
||||
|
||||
.tag-purple {
|
||||
background: rgba(123, 97, 255, 0.1);
|
||||
color: #7B61FF;
|
||||
}
|
||||
|
||||
.featured-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.featured-part {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.featured-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
/* ===== 内容概览列表 ===== */
|
||||
.parts-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.part-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.part-item:active {
|
||||
transform: scale(0.98);
|
||||
background: #2c2c2e;
|
||||
}
|
||||
|
||||
.part-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-number {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.part-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.part-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.part-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.part-arrow {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ===== 序言入口 ===== */
|
||||
.preface-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.preface-card:active {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.preface-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.preface-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.preface-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 超级个体(横向滚动) ===== */
|
||||
/* 加载骨架动画 */
|
||||
.super-loading {
|
||||
width: 100%;
|
||||
margin: 0 -32rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
.super-loading-inner {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
.super-loading-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
.super-loading-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 50%, #2c2c2e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: super-shimmer 1.2s ease-in-out infinite;
|
||||
}
|
||||
.super-loading-name {
|
||||
width: 80rpx;
|
||||
height: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
background: linear-gradient(90deg, #2c2c2e 25%, #3a3a3c 50%, #2c2c2e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: super-shimmer 1.2s ease-in-out infinite 0.2s;
|
||||
}
|
||||
@keyframes super-shimmer {
|
||||
0% { background-position: 100% 0; }
|
||||
100% { background-position: -100% 0; }
|
||||
}
|
||||
|
||||
.super-scroll {
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
margin: 0 -32rpx;
|
||||
padding: 0 32rpx;
|
||||
}
|
||||
|
||||
.super-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.super-scroll-inner {
|
||||
display: inline-flex;
|
||||
gap: 32rpx;
|
||||
padding-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.super-item-h {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
|
||||
.super-scroll .super-avatar {
|
||||
width: 112rpx;
|
||||
height: 112rpx;
|
||||
}
|
||||
|
||||
.super-scroll .super-name {
|
||||
font-size: 20rpx;
|
||||
max-width: 120rpx;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.super-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10rpx;
|
||||
}
|
||||
.super-avatar {
|
||||
width: 108rpx;
|
||||
height: 108rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: rgba(0,206,209,0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3rpx solid rgba(255,255,255,0.1);
|
||||
}
|
||||
.super-avatar-vip {
|
||||
border: 3rpx solid #FFD700;
|
||||
box-shadow: 0 0 12rpx rgba(255,215,0,0.3);
|
||||
}
|
||||
.super-avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.super-avatar-text {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #00CED1;
|
||||
}
|
||||
.super-name {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,255,255,0.7);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.super-empty {
|
||||
padding: 32rpx;
|
||||
text-align: center;
|
||||
background: rgba(255,255,255,0.03);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.super-empty-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,255,255,0.4);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.super-empty-btn {
|
||||
font-size: 26rpx;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
/* ===== 最新新增(时间线样式) ===== */
|
||||
.latest-header {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.daily-badge-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
/* 设计稿 1:1:橙底白字 rounded-full */
|
||||
.daily-badge {
|
||||
background: #F6AD55;
|
||||
color: #ffffff;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
margin-left: 8rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(246, 173, 85, 0.3);
|
||||
}
|
||||
|
||||
/* 设计稿 1:1:pl-3 竖线 left-3 top-2 bottom-2 w-[1px] bg-gray-800 */
|
||||
.timeline-wrap {
|
||||
position: relative;
|
||||
padding-left: 24rpx;
|
||||
}
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
left: 23rpx;
|
||||
top: 16rpx;
|
||||
bottom: 16rpx;
|
||||
width: 2rpx;
|
||||
background: #2c2c2e;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.timeline-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 48rpx;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* 设计稿:pl-6,分隔线在 content 内 */
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 48rpx;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.timeline-item:last-child .timeline-content {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* 设计稿:left-[-4.5px] top-1.5 w-2.5 h-2.5 ring-4 ring-black */
|
||||
.timeline-dot {
|
||||
position: absolute;
|
||||
left: -9rpx;
|
||||
top: 12rpx;
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
border-radius: 50%;
|
||||
background: #2c2c2e;
|
||||
box-shadow: 0 0 0 16rpx #000;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.timeline-item-first .timeline-dot {
|
||||
background: #4FD1C5;
|
||||
}
|
||||
|
||||
.timeline-content {
|
||||
flex: 1;
|
||||
padding-bottom: 32rpx;
|
||||
border-bottom: 2rpx solid #1a1a1a;
|
||||
}
|
||||
|
||||
/* 设计稿:mb-1 justify-between gap-2 */
|
||||
.timeline-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.timeline-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.timeline-right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
flex-shrink: 0;
|
||||
padding-left: 16rpx;
|
||||
}
|
||||
|
||||
/* NEW 标签:黑底黄字黄色边框 */
|
||||
.latest-new-tag {
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: #F6AD55;
|
||||
background: #000000;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 8rpx;
|
||||
border: 2rpx solid #F6AD55;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 设计稿:text-sm font-medium text-white */
|
||||
.timeline-title {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 设计稿 1:1:价格/日期 light grey */
|
||||
.timeline-price {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #F6AD55;
|
||||
}
|
||||
|
||||
.timeline-date {
|
||||
font-size: 20rpx;
|
||||
color: #A0AEC0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 描述仅单行,超出省略 */
|
||||
.timeline-desc {
|
||||
font-size: 24rpx;
|
||||
color: #A0AEC0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
line-height: 1;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
height: 26rpx;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
}
|
||||
|
||||
/* ===== 链接卡若 - 留资弹窗 ===== */
|
||||
.lead-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.lead-box {
|
||||
width: 100%;
|
||||
max-width: 560rpx;
|
||||
background: #1C1C1E;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.3);
|
||||
}
|
||||
.lead-title {
|
||||
display: block;
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.lead-desc {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #A0AEC0;
|
||||
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;
|
||||
}
|
||||
.lead-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.lead-btn {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
line-height: 80rpx;
|
||||
border-radius: 16rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
}
|
||||
.lead-btn-cancel {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
color: #A0AEC0;
|
||||
}
|
||||
.lead-btn-submit {
|
||||
background: #38bdac;
|
||||
color: #ffffff;
|
||||
}
|
||||
767
miniprogram/pages/match/match.js
Normal file
767
miniprogram/pages/match/match.js
Normal file
@@ -0,0 +1,767 @@
|
||||
/**
|
||||
* Soul创业派对 - 找伙伴页
|
||||
* 按H5网页端完全重构
|
||||
* 开发: 卡若
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
// 默认匹配类型配置
|
||||
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
||||
// 资源对接:需要登录+购买章节才能使用,填写2项信息(我能帮到你什么、我需要什么帮助)
|
||||
// 导师顾问:跳转到存客宝添加微信
|
||||
// 团队招募:跳转到存客宝添加微信
|
||||
let MATCH_TYPES = [
|
||||
{ id: 'partner', label: '找伙伴', matchLabel: '找伙伴', icon: '⭐', matchFromDB: true, showJoinAfterMatch: false },
|
||||
{ id: 'investor', label: '资源对接', matchLabel: '资源对接', icon: '👥', matchFromDB: true, showJoinAfterMatch: true, requirePurchase: true },
|
||||
{ id: 'mentor', label: '导师顾问', matchLabel: '导师顾问', icon: '❤️', matchFromDB: true, showJoinAfterMatch: true },
|
||||
{ id: 'team', label: '团队招募', matchLabel: '团队招募', icon: '🎮', matchFromDB: true, showJoinAfterMatch: true }
|
||||
]
|
||||
|
||||
let FREE_MATCH_LIMIT = 3 // 每日免费匹配次数
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
|
||||
// 匹配类型
|
||||
matchTypes: MATCH_TYPES,
|
||||
selectedType: 'partner',
|
||||
currentTypeLabel: '找伙伴',
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
hasPurchased: false,
|
||||
hasFullBook: false,
|
||||
|
||||
// 匹配次数
|
||||
todayMatchCount: 0,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchesRemaining: FREE_MATCH_LIMIT,
|
||||
needPayToMatch: false,
|
||||
|
||||
// 匹配状态
|
||||
isMatching: false,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null,
|
||||
|
||||
// 加入弹窗
|
||||
showJoinModal: false,
|
||||
joinType: null,
|
||||
joinTypeLabel: '',
|
||||
contactType: 'phone',
|
||||
phoneNumber: '',
|
||||
wechatId: '',
|
||||
userPhone: '',
|
||||
isJoining: false,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: false,
|
||||
|
||||
// 资源对接表单
|
||||
canHelp: '',
|
||||
needHelp: '',
|
||||
goodAt: '',
|
||||
|
||||
// 解锁弹窗
|
||||
showUnlockModal: false,
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
contactWechat: '',
|
||||
contactSaving: false,
|
||||
|
||||
// 匹配价格(可配置)
|
||||
matchPrice: 1,
|
||||
extraMatches: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
this.loadMatchConfig()
|
||||
this.loadStoredContact()
|
||||
this.loadTodayMatchCount()
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
tabBar.setData({ selected: 2 })
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
// 加载匹配配置
|
||||
async loadMatchConfig() {
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/match/config', silent: true, method: 'GET',
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
// 更新全局配置,导师顾问类型强制显示「导师顾问」
|
||||
let types = res.data.matchTypes || MATCH_TYPES
|
||||
types = types.map(t => {
|
||||
if (t.id === 'mentor') {
|
||||
return { ...t, label: '导师顾问', matchLabel: '导师顾问' }
|
||||
}
|
||||
return t
|
||||
})
|
||||
MATCH_TYPES = types
|
||||
FREE_MATCH_LIMIT = res.data.freeMatchLimit || FREE_MATCH_LIMIT
|
||||
const matchPrice = res.data.matchPrice || 1
|
||||
|
||||
this.setData({
|
||||
matchTypes: MATCH_TYPES,
|
||||
totalMatchesAllowed: FREE_MATCH_LIMIT,
|
||||
matchPrice: matchPrice
|
||||
})
|
||||
|
||||
console.log('[Match] 加载匹配配置成功:', {
|
||||
types: MATCH_TYPES.length,
|
||||
freeLimit: FREE_MATCH_LIMIT,
|
||||
price: matchPrice
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 加载匹配配置失败,使用默认配置:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载本地存储的联系方式
|
||||
loadStoredContact() {
|
||||
const phone = wx.getStorageSync('user_phone') || ''
|
||||
const wechat = wx.getStorageSync('user_wechat') || ''
|
||||
this.setData({
|
||||
phoneNumber: phone,
|
||||
wechatId: wechat,
|
||||
userPhone: phone
|
||||
})
|
||||
},
|
||||
|
||||
// 加载今日匹配次数
|
||||
loadTodayMatchCount() {
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const stored = wx.getStorageSync('match_count_data')
|
||||
if (stored) {
|
||||
const data = typeof stored === 'string' ? JSON.parse(stored) : stored
|
||||
if (data.date === today) {
|
||||
this.setData({ todayMatchCount: data.count })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('加载匹配次数失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 保存今日匹配次数
|
||||
saveTodayMatchCount(count) {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
wx.setStorageSync('match_count_data', { date: today, count })
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, hasFullBook, purchasedSections } = app.globalData
|
||||
|
||||
// 获取额外购买的匹配次数
|
||||
const extraMatches = wx.getStorageSync('extra_match_count') || 0
|
||||
|
||||
// 总匹配次数 = 每日免费(3) + 额外购买次数
|
||||
// 全书用户无限制
|
||||
const totalMatchesAllowed = hasFullBook ? 999999 : FREE_MATCH_LIMIT + extraMatches
|
||||
const matchesRemaining = hasFullBook ? 999999 : Math.max(0, totalMatchesAllowed - this.data.todayMatchCount)
|
||||
const needPayToMatch = !hasFullBook && matchesRemaining <= 0
|
||||
|
||||
this.setData({
|
||||
isLoggedIn,
|
||||
hasFullBook,
|
||||
hasPurchased: true, // 所有用户都可以使用匹配功能
|
||||
totalMatchesAllowed,
|
||||
matchesRemaining,
|
||||
needPayToMatch,
|
||||
extraMatches
|
||||
})
|
||||
},
|
||||
|
||||
// 选择匹配类型
|
||||
selectType(e) {
|
||||
const typeId = e.currentTarget.dataset.type
|
||||
const type = MATCH_TYPES.find(t => t.id === typeId)
|
||||
this.setData({
|
||||
selectedType: typeId,
|
||||
currentTypeLabel: type?.matchLabel || type?.label || '创业伙伴'
|
||||
})
|
||||
},
|
||||
|
||||
// 点击匹配按钮
|
||||
async handleMatchClick() {
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
// 导师顾问:先播匹配动画,动画完成后再跳转(不在此处直接跳)
|
||||
// 找伙伴/资源对接:需先完善联系方式
|
||||
if (this.data.isLoggedIn && currentType?.matchFromDB) {
|
||||
await this.ensureContactInfo(() => this._handleMatchClickInner(currentType))
|
||||
} else {
|
||||
this._handleMatchClickInner(currentType)
|
||||
}
|
||||
},
|
||||
|
||||
async ensureContactInfo(callback) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
contactWechat: wechat || '',
|
||||
})
|
||||
this._contactCallback = callback
|
||||
} catch (e) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
closeContactModal() {
|
||||
this.setData({ showContactModal: false })
|
||||
this._contactCallback = null
|
||||
},
|
||||
|
||||
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: app.globalData.userInfo?.id,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechat || undefined,
|
||||
},
|
||||
})
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
if (wechat) wx.setStorageSync('user_wechat', wechat)
|
||||
this.loadStoredContact()
|
||||
this.closeContactModal()
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
const cb = this._contactCallback
|
||||
this._contactCallback = null
|
||||
if (cb) cb()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ contactSaving: false })
|
||||
},
|
||||
|
||||
_handleMatchClickInner(currentType) {
|
||||
|
||||
// 资源对接类型需要登录+购买章节才能使用
|
||||
if (currentType && currentType.id === 'investor') {
|
||||
// 检查是否登录
|
||||
if (!this.data.isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再使用资源对接功能',
|
||||
confirmText: '去登录',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否购买过章节
|
||||
const hasPurchased = app.globalData.purchasedSections?.length > 0 || app.globalData.hasFullBook
|
||||
if (!hasPurchased) {
|
||||
wx.showModal({
|
||||
title: '需要购买章节',
|
||||
content: '购买任意章节后即可使用资源对接功能',
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/catalog/catalog' })
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果是需要填写联系方式的类型(资源对接、导师顾问、团队招募)
|
||||
if (currentType && currentType.showJoinAfterMatch) {
|
||||
// 先检查是否已绑定联系方式
|
||||
const hasPhone = !!this.data.phoneNumber
|
||||
const hasWechat = !!this.data.wechatId
|
||||
|
||||
if (!hasPhone && !hasWechat) {
|
||||
// 没有绑定联系方式,先显示绑定提示
|
||||
this.setData({
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
joinTypeLabel: currentType.matchLabel || currentType.label,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 已绑定联系方式,先显示匹配动画1-3秒,再弹出确认
|
||||
this.startMatchingAnimation(currentType)
|
||||
return
|
||||
}
|
||||
|
||||
// 创业合伙类型 - 真正的匹配功能
|
||||
if (this.data.needPayToMatch) {
|
||||
this.setData({ showUnlockModal: true })
|
||||
return
|
||||
}
|
||||
|
||||
this.startMatch()
|
||||
},
|
||||
|
||||
// 匹配动画后弹出加入确认(导师顾问:动画完成后直接跳转导师页)
|
||||
startMatchingAnimation(currentType) {
|
||||
// 显示匹配中状态
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null
|
||||
})
|
||||
|
||||
// 动画计时
|
||||
const timer = setInterval(() => {
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 500)
|
||||
|
||||
// 1.5-3秒后:导师顾问→跳转;其他类型→弹窗
|
||||
const delay = Math.random() * 1500 + 1500
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
this.setData({ isMatching: false })
|
||||
if (currentType && currentType.id === 'mentor') {
|
||||
wx.navigateTo({ url: '/pages/mentors/mentors' })
|
||||
} else {
|
||||
this.setData({
|
||||
showJoinModal: true,
|
||||
joinType: currentType.id,
|
||||
joinTypeLabel: currentType.matchLabel || currentType.label,
|
||||
joinSuccess: false,
|
||||
joinError: '',
|
||||
needBindFirst: false
|
||||
})
|
||||
}
|
||||
}, delay)
|
||||
},
|
||||
|
||||
// 显示购买提示
|
||||
showPurchaseTip() {
|
||||
wx.showModal({
|
||||
title: '需要购买书籍',
|
||||
content: '购买《Soul创业派对》后即可使用匹配功能,仅需9.9元',
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.goToChapters()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 开始匹配 - 只匹配数据库中的真实用户
|
||||
async startMatch() {
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
currentMatch: null
|
||||
})
|
||||
|
||||
// 匹配动画计时器
|
||||
const timer = setInterval(() => {
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 1000)
|
||||
|
||||
// 从数据库获取真实用户匹配
|
||||
let matchedUser = null
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
userId: app.globalData.userInfo?.id || ''
|
||||
}
|
||||
})
|
||||
|
||||
if (res.success && res.data) {
|
||||
matchedUser = res.data
|
||||
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 数据库匹配失败:', e)
|
||||
}
|
||||
|
||||
// 延迟显示结果(模拟匹配过程)
|
||||
const delay = Math.random() * 2000 + 2000
|
||||
setTimeout(() => {
|
||||
clearInterval(timer)
|
||||
|
||||
// 如果没有匹配到用户,提示用户
|
||||
if (!matchedUser) {
|
||||
this.setData({ isMatching: false })
|
||||
wx.showModal({
|
||||
title: '暂无匹配',
|
||||
content: '当前暂无合适的匹配用户,请稍后再试',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 增加今日匹配次数
|
||||
const newCount = this.data.todayMatchCount + 1
|
||||
const matchesRemaining = this.data.hasFullBook ? 999999 : Math.max(0, this.data.totalMatchesAllowed - newCount)
|
||||
|
||||
this.setData({
|
||||
isMatching: false,
|
||||
currentMatch: matchedUser,
|
||||
todayMatchCount: newCount,
|
||||
matchesRemaining,
|
||||
needPayToMatch: !this.data.hasFullBook && matchesRemaining <= 0
|
||||
})
|
||||
this.saveTodayMatchCount(newCount)
|
||||
|
||||
// 上报匹配行为到存客宝
|
||||
this.reportMatch(matchedUser)
|
||||
|
||||
}, delay)
|
||||
},
|
||||
|
||||
// 生成模拟匹配数据
|
||||
generateMockMatch() {
|
||||
const nicknames = ['创业先锋', '资源整合者', '私域专家', '导师顾问', '连续创业者']
|
||||
const concepts = [
|
||||
'专注私域流量运营5年,帮助100+品牌实现从0到1的增长。',
|
||||
'连续创业者,擅长商业模式设计和资源整合。',
|
||||
'在Soul分享真实创业故事,希望找到志同道合的合作伙伴。'
|
||||
]
|
||||
const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
|
||||
|
||||
const index = Math.floor(Math.random() * nicknames.length)
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
return {
|
||||
id: `user_${Date.now()}`,
|
||||
nickname: nicknames[index],
|
||||
avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
|
||||
tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
|
||||
matchScore: Math.floor(Math.random() * 20) + 80,
|
||||
concept: concepts[index % concepts.length],
|
||||
wechat: wechats[index % wechats.length],
|
||||
commonInterests: [
|
||||
{ icon: '📚', text: '都在读《创业派对》' },
|
||||
{ icon: '💼', text: '对私域运营感兴趣' },
|
||||
{ icon: '🎯', text: '相似的创业方向' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
try {
|
||||
await app.request({ url: '/api/miniprogram/ckb/match', silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
phone: this.data.phoneNumber,
|
||||
wechat: this.data.wechatId,
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
nickname: app.globalData.userInfo?.nickname || '',
|
||||
matchedUser: {
|
||||
id: matchedUser.id,
|
||||
nickname: matchedUser.nickname,
|
||||
matchScore: matchedUser.matchScore
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('上报匹配失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 取消匹配
|
||||
cancelMatch() {
|
||||
this.setData({ isMatching: false, matchAttempts: 0 })
|
||||
},
|
||||
|
||||
// 重置匹配(返回)
|
||||
resetMatch() {
|
||||
this.setData({ currentMatch: null })
|
||||
},
|
||||
|
||||
// 添加微信好友
|
||||
handleAddWechat() {
|
||||
if (!this.data.currentMatch) return
|
||||
|
||||
wx.setClipboardData({
|
||||
data: this.data.currentMatch.wechat,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '微信号已复制',
|
||||
content: `微信号:${this.data.currentMatch.wechat}\n\n请打开微信添加好友,备注"创业合作"即可`,
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换联系方式类型
|
||||
switchContactType(e) {
|
||||
const type = e.currentTarget.dataset.type
|
||||
this.setData({ contactType: type, joinError: '' })
|
||||
},
|
||||
|
||||
// 手机号输入
|
||||
onPhoneInput(e) {
|
||||
this.setData({
|
||||
phoneNumber: e.detail.value.replace(/\D/g, '').slice(0, 11),
|
||||
joinError: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 资源对接表单输入
|
||||
onCanHelpInput(e) {
|
||||
this.setData({ canHelp: e.detail.value })
|
||||
},
|
||||
onNeedHelpInput(e) {
|
||||
this.setData({ needHelp: e.detail.value })
|
||||
},
|
||||
onGoodAtInput(e) {
|
||||
this.setData({ goodAt: e.detail.value })
|
||||
},
|
||||
|
||||
// 微信号输入
|
||||
onWechatInput(e) {
|
||||
this.setData({
|
||||
wechatId: e.detail.value,
|
||||
joinError: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 提交加入
|
||||
async handleJoinSubmit() {
|
||||
const { contactType, phoneNumber, wechatId, joinType, isJoining, canHelp, needHelp } = this.data
|
||||
|
||||
if (isJoining) return
|
||||
|
||||
// 验证联系方式
|
||||
if (contactType === 'phone') {
|
||||
if (!phoneNumber || phoneNumber.length !== 11) {
|
||||
this.setData({ joinError: '请输入正确的11位手机号' })
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if (!wechatId || wechatId.length < 6) {
|
||||
this.setData({ joinError: '请输入正确的微信号(至少6位)' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 资源对接需要填写两项信息
|
||||
if (joinType === 'investor') {
|
||||
if (!canHelp || canHelp.trim().length < 2) {
|
||||
this.setData({ joinError: '请填写"我能帮到你什么"' })
|
||||
return
|
||||
}
|
||||
if (!needHelp || needHelp.trim().length < 2) {
|
||||
this.setData({ joinError: '请填写"我需要什么帮助"' })
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
this.setData({ isJoining: true, joinError: '' })
|
||||
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/ckb/join', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
type: joinType,
|
||||
phone: contactType === 'phone' ? phoneNumber : '',
|
||||
wechat: contactType === 'wechat' ? wechatId : '',
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
// 资源对接专属字段
|
||||
canHelp: joinType === 'investor' ? canHelp : '',
|
||||
needHelp: joinType === 'investor' ? needHelp : ''
|
||||
}
|
||||
})
|
||||
|
||||
// 保存联系方式到本地
|
||||
if (phoneNumber) wx.setStorageSync('user_phone', phoneNumber)
|
||||
if (wechatId) wx.setStorageSync('user_wechat', wechatId)
|
||||
|
||||
if (res.success) {
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
} else {
|
||||
// 即使API返回失败,也模拟成功(因为已保存本地)
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
}
|
||||
} catch (e) {
|
||||
// 网络错误时也模拟成功
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
} finally {
|
||||
this.setData({ isJoining: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 关闭加入弹窗
|
||||
closeJoinModal() {
|
||||
if (this.data.isJoining) return
|
||||
this.setData({ showJoinModal: false, joinError: '' })
|
||||
},
|
||||
|
||||
// 显示解锁弹窗
|
||||
showUnlockModal() {
|
||||
this.setData({ showUnlockModal: true })
|
||||
},
|
||||
|
||||
// 关闭解锁弹窗
|
||||
closeUnlockModal() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
},
|
||||
|
||||
// 购买匹配次数
|
||||
async buyMatchCount() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
|
||||
try {
|
||||
// 获取openId
|
||||
let openId = app.globalData.openId || wx.getStorageSync('openId')
|
||||
if (!openId) {
|
||||
openId = await app.getOpenId()
|
||||
}
|
||||
|
||||
if (!openId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 邀请码:与章节支付一致,写入订单便于分销归属与对账
|
||||
const referralCode = wx.getStorageSync('referral_code') || ''
|
||||
// 调用支付接口购买匹配次数
|
||||
const res = await app.request('/api/miniprogram/pay', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
openId,
|
||||
productType: 'match',
|
||||
productId: 'match_1',
|
||||
amount: 1,
|
||||
description: '匹配次数x1',
|
||||
userId: app.globalData.userInfo?.id || '',
|
||||
referralCode: referralCode || undefined
|
||||
}
|
||||
})
|
||||
|
||||
if (res.success && res.data?.payParams) {
|
||||
// 调用微信支付
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.requestPayment({
|
||||
...res.data.payParams,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
// 支付成功,增加匹配次数
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
} else {
|
||||
throw new Error(res.error || '创建订单失败')
|
||||
}
|
||||
} catch (e) {
|
||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消', icon: 'none' })
|
||||
} else {
|
||||
// 测试模式
|
||||
wx.showModal({
|
||||
title: '支付服务暂不可用',
|
||||
content: '是否使用测试模式购买?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到目录页购买
|
||||
goToChapters() {
|
||||
this.setData({ showUnlockModal: false })
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 打开设置
|
||||
openSettings() {
|
||||
wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 找伙伴',
|
||||
path: ref ? `/pages/match/match?ref=${ref}` : '/pages/match/match'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 找伙伴', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/match/match.json
Normal file
6
miniprogram/pages/match/match.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
328
miniprogram/pages/match/match.wxml
Normal file
328
miniprogram/pages/match/match.wxml
Normal file
@@ -0,0 +1,328 @@
|
||||
<!--pages/match/match.wxml-->
|
||||
<!--Soul创业派对 - 找伙伴页 按H5网页端完全重构-->
|
||||
<view class="page">
|
||||
<!-- 自定义导航栏 -->
|
||||
<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>
|
||||
<text class="nav-title">找伙伴</text>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部留白,让内容往下 -->
|
||||
<view style="height: 30rpx;"></view>
|
||||
|
||||
<!-- 匹配提示条 - 简化显示 -->
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
|
||||
<text class="tip-icon">⚡</text>
|
||||
<text class="tip-text">今日免费次数已用完</text>
|
||||
<view class="tip-btn" bindtap="showUnlockModal">购买次数</view>
|
||||
</view>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- 空闲状态 - 未匹配 -->
|
||||
<block wx:if="{{!isMatching && !currentMatch}}">
|
||||
<!-- 中央匹配圆环 -->
|
||||
<view class="match-circle-wrapper" bindtap="handleMatchClick">
|
||||
<!-- 外层光环 -->
|
||||
<view class="outer-glow glow-active"></view>
|
||||
<!-- 中间光环 -->
|
||||
<view class="middle-ring ring-active"></view>
|
||||
<!-- 内层球体 -->
|
||||
<view class="inner-sphere sphere-active">
|
||||
<view class="sphere-gradient"></view>
|
||||
<view class="sphere-content">
|
||||
<block wx:if="{{needPayToMatch}}">
|
||||
<text class="sphere-icon">⚡</text>
|
||||
<text class="sphere-title gold-text">购买次数</text>
|
||||
<text class="sphere-desc">¥1 = 1次匹配</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="sphere-icon">👥</text>
|
||||
<text class="sphere-title">开始匹配</text>
|
||||
<text class="sphere-desc">匹配{{currentTypeLabel}}</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 当前模式显示 -->
|
||||
<view class="current-mode">
|
||||
当前模式: <text class="text-brand">{{currentTypeLabel}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<view class="divider"></view>
|
||||
|
||||
<!-- 选择匹配类型 -->
|
||||
<view class="type-section">
|
||||
<text class="type-section-title">选择匹配类型</text>
|
||||
<view class="type-grid">
|
||||
<view
|
||||
class="type-item {{selectedType === item.id ? 'type-active' : ''}}"
|
||||
wx:for="{{matchTypes}}"
|
||||
wx:key="id"
|
||||
bindtap="selectType"
|
||||
data-type="{{item.id}}"
|
||||
>
|
||||
<text class="type-icon">{{item.icon}}</text>
|
||||
<text class="type-label {{selectedType === item.id ? 'text-brand' : ''}}">{{item.label}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 匹配中状态 - 美化特效 -->
|
||||
<block wx:if="{{isMatching}}">
|
||||
<view class="matching-state">
|
||||
<view class="matching-animation-v2">
|
||||
<!-- 外层旋转光环 -->
|
||||
<view class="matching-outer-ring"></view>
|
||||
<!-- 中层脉冲环 -->
|
||||
<view class="matching-pulse-ring"></view>
|
||||
<!-- 内层球体 -->
|
||||
<view class="matching-core">
|
||||
<view class="matching-core-inner">
|
||||
<text class="matching-icon-v2">🔍</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 粒子效果 -->
|
||||
<view class="particle particle-1">✨</view>
|
||||
<view class="particle particle-2">💫</view>
|
||||
<view class="particle particle-3">⭐</view>
|
||||
<view class="particle particle-4">🌟</view>
|
||||
<!-- 扩散波纹 -->
|
||||
<view class="ripple-v2 ripple-v2-1"></view>
|
||||
<view class="ripple-v2 ripple-v2-2"></view>
|
||||
<view class="ripple-v2 ripple-v2-3"></view>
|
||||
</view>
|
||||
<text class="matching-title-v2">正在匹配{{currentTypeLabel}}...</text>
|
||||
<text class="matching-subtitle-v2">正在从 {{matchAttempts * 127 + 89}} 位创业者中为你寻找</text>
|
||||
<view class="matching-tips">
|
||||
<text class="tip-item" wx:if="{{matchAttempts >= 1}}">✓ 分析兴趣标签</text>
|
||||
<text class="tip-item" wx:if="{{matchAttempts >= 2}}">✓ 匹配创业方向</text>
|
||||
<text class="tip-item" wx:if="{{matchAttempts >= 3}}">✓ 筛选优质伙伴</text>
|
||||
</view>
|
||||
<view class="cancel-btn-v2" bindtap="cancelMatch">取消</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 匹配成功状态 -->
|
||||
<block wx:if="{{currentMatch && !isMatching}}">
|
||||
<view class="matched-state">
|
||||
<!-- 成功动画 -->
|
||||
<view class="success-icon-wrapper">
|
||||
<text class="success-icon">✨</text>
|
||||
</view>
|
||||
|
||||
<!-- 用户卡片 -->
|
||||
<view class="match-card">
|
||||
<view class="card-header">
|
||||
<image class="match-avatar" src="{{currentMatch.avatar}}" mode="aspectFill"></image>
|
||||
<view class="match-info">
|
||||
<text class="match-name">{{currentMatch.nickname}}</text>
|
||||
<view class="match-tags">
|
||||
<text class="match-tag" wx:for="{{currentMatch.tags}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="match-score-box">
|
||||
<text class="score-value">{{currentMatch.matchScore}}%</text>
|
||||
<text class="score-label">匹配度</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 共同兴趣 -->
|
||||
<view class="card-section">
|
||||
<text class="section-title">共同兴趣</text>
|
||||
<view class="interest-list">
|
||||
<view class="interest-item" wx:for="{{currentMatch.commonInterests}}" wx:key="text">
|
||||
<text class="interest-icon">{{item.icon}}</text>
|
||||
<text class="interest-text">{{item.text}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心理念 -->
|
||||
<view class="card-section">
|
||||
<text class="section-title">核心理念</text>
|
||||
<text class="concept-text">{{currentMatch.concept}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<view class="action-buttons">
|
||||
<view class="btn-primary" bindtap="handleAddWechat">一键加好友</view>
|
||||
<view class="btn-secondary" bindtap="resetMatch">返回</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 加入弹窗 - 简洁版 -->
|
||||
<view class="modal-overlay" wx:if="{{showJoinModal}}" bindtap="closeJoinModal">
|
||||
<view class="modal-content join-modal-new" catchtap="preventBubble">
|
||||
<!-- 成功状态 -->
|
||||
<block wx:if="{{joinSuccess}}">
|
||||
<view class="join-success-new">
|
||||
<view class="success-icon-big">✅</view>
|
||||
<text class="success-title-new">提交成功</text>
|
||||
<text class="success-desc-new">工作人员将在24小时内与您联系</text>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 表单状态 -->
|
||||
<block wx:else>
|
||||
<!-- 头部 -->
|
||||
<view class="join-header">
|
||||
<view class="join-icon-wrap">
|
||||
<text class="join-icon">{{joinType === 'investor' ? '👥' : joinType === 'mentor' ? '❤️' : '🎮'}}</text>
|
||||
</view>
|
||||
<text class="join-title">{{joinTypeLabel}}</text>
|
||||
<text class="join-subtitle" wx:if="{{needBindFirst}}">请先绑定联系方式</text>
|
||||
<text class="join-subtitle" wx:else>填写联系方式,专人对接</text>
|
||||
<view class="close-btn-new" bindtap="closeJoinModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式切换 -->
|
||||
<view class="contact-switch">
|
||||
<view
|
||||
class="switch-item {{contactType === 'phone' ? 'switch-active' : ''}}"
|
||||
bindtap="switchContactType"
|
||||
data-type="phone"
|
||||
>
|
||||
<text class="switch-icon">📱</text>
|
||||
<text>手机号</text>
|
||||
</view>
|
||||
<view
|
||||
class="switch-item {{contactType === 'wechat' ? 'switch-active' : ''}}"
|
||||
bindtap="switchContactType"
|
||||
data-type="wechat"
|
||||
>
|
||||
<text class="switch-icon">💬</text>
|
||||
<text>微信号</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 资源对接专用输入(只有两项:我能帮到你什么、我需要什么帮助) -->
|
||||
<block wx:if="{{joinType === 'investor'}}">
|
||||
<view class="resource-form">
|
||||
<view class="form-item">
|
||||
<text class="form-label">我能帮到你什么 <text class="required">*</text></text>
|
||||
<view class="form-input-wrap">
|
||||
<input class="form-input-inner" type="text" placeholder="例如:私域运营、品牌策划、流量资源..." value="{{canHelp}}" bindinput="onCanHelpInput" maxlength="100"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">我需要什么帮助 <text class="required">*</text></text>
|
||||
<view class="form-input-wrap">
|
||||
<input class="form-input-inner" placeholder="例如:技术支持、资金、人脉..." value="{{needHelp}}" bindinput="onNeedHelpInput" maxlength="100"/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 联系方式输入区域(Skill §6:view 包裹、padding 写 view、input width 100%) -->
|
||||
<view class="input-area">
|
||||
<view class="input-wrapper">
|
||||
<text class="input-prefix">{{contactType === 'phone' ? '+86' : '@'}}</text>
|
||||
<view class="input-field-wrap">
|
||||
<input
|
||||
wx:if="{{contactType === 'phone'}}"
|
||||
type="number"
|
||||
class="input-field-inner"
|
||||
placeholder="请输入11位手机号"
|
||||
placeholder-class="input-placeholder-new"
|
||||
value="{{phoneNumber}}"
|
||||
bindinput="onPhoneInput"
|
||||
maxlength="11"
|
||||
disabled="{{isJoining}}"
|
||||
focus="{{contactType === 'phone'}}"
|
||||
/>
|
||||
<input
|
||||
wx:else
|
||||
type="text"
|
||||
class="input-field-inner"
|
||||
placeholder="请输入微信号"
|
||||
placeholder-class="input-placeholder-new"
|
||||
value="{{wechatId}}"
|
||||
bindinput="onWechatInput"
|
||||
disabled="{{isJoining}}"
|
||||
focus="{{contactType === 'wechat'}}"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
<text class="error-msg" wx:if="{{joinError}}">{{joinError}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<view
|
||||
class="submit-btn-new {{isJoining || !(contactType === 'phone' ? phoneNumber : wechatId) ? 'btn-disabled-new' : ''}}"
|
||||
bindtap="handleJoinSubmit"
|
||||
>
|
||||
{{isJoining ? '提交中...' : '确认提交'}}
|
||||
</view>
|
||||
|
||||
<text class="form-notice-new">提交后我们会尽快与您联系</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2) -->
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
<view class="contact-modal" catchtap="preventBubble">
|
||||
<text class="contact-modal-title">请完善联系方式</text>
|
||||
<view class="contact-modal-hint">需完善手机号或微信号才能使用找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">📱</text>
|
||||
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">💬</text>
|
||||
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-modal-btn" bindtap="saveContactInfo" disabled="{{contactSaving}}">
|
||||
{{contactSaving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<text class="contact-modal-cancel" bindtap="closeContactModal">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 解锁弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showUnlockModal}}" bindtap="closeUnlockModal">
|
||||
<view class="modal-content unlock-modal" catchtap="preventBubble">
|
||||
<view class="unlock-icon">⚡</view>
|
||||
<text class="unlock-title">购买匹配次数</text>
|
||||
<text class="unlock-desc">今日3次免费匹配已用完,可付费购买额外次数</text>
|
||||
|
||||
<view class="unlock-info">
|
||||
<view class="info-row">
|
||||
<text class="info-label">单价</text>
|
||||
<text class="info-value text-brand">¥{{matchPrice || 1}} / 次</text>
|
||||
</view>
|
||||
<view class="info-row">
|
||||
<text class="info-label">已购买</text>
|
||||
<text class="info-value">{{extraMatches || 0}} 次</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="unlock-buttons">
|
||||
<view class="btn-gold" bindtap="buyMatchCount">立即购买 ¥{{matchPrice || 1}}</view>
|
||||
<view class="btn-ghost" bindtap="closeUnlockModal">明天再来</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
1245
miniprogram/pages/match/match.wxss
Normal file
1245
miniprogram/pages/match/match.wxss
Normal file
File diff suppressed because it is too large
Load Diff
200
miniprogram/pages/member-detail/member-detail.js
Normal file
200
miniprogram/pages/member-detail/member-detail.js
Normal file
@@ -0,0 +1,200 @@
|
||||
/**
|
||||
* Soul创业派对 - 超级个体/会员详情页
|
||||
* 接口:优先 /api/miniprogram/vip/members?id=xx(VIP),回退 /api/miniprogram/users?id=xx(任意用户)
|
||||
* 头像/昵称:统一用用户资料(nickname/avatar)优先,随「我的」修改实时生效
|
||||
* mbti, region, industry, position, businessScale, skills,
|
||||
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
|
||||
* helpOffer→canHelp, helpNeed→needHelp
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, member: null, loading: true },
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
if (options.id) this.loadMember(options.id)
|
||||
},
|
||||
|
||||
async loadMember(id) {
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/vip/members?id=${id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = Array.isArray(res.data) ? res.data[0] : res.data
|
||||
if (d) { this.setData({ member: this.enrichAndFormat(d), loading: false }); return }
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
try {
|
||||
const dbRes = await app.request({ url: `/api/miniprogram/users?id=${id}`, silent: true })
|
||||
if (dbRes?.success && dbRes.data) {
|
||||
const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data
|
||||
if (u) {
|
||||
this.setData({ member: this.enrichAndFormat({
|
||||
id: u.id, name: u.nickname || u.vipName || u.vip_name || '创业者',
|
||||
avatar: u.avatar || u.vipAvatar || u.vip_avatar || '', isVip: !!(u.is_vip),
|
||||
contactRaw: u.vipContact || u.vip_contact || u.phone || '',
|
||||
wechatId: u.wechatId || u.wechat_id,
|
||||
project: u.vipProject || u.vip_project || u.projectIntro || u.project_intro || '',
|
||||
industry: u.industry, position: u.position, businessScale: u.businessScale || u.business_scale,
|
||||
skills: u.skills, mbti: u.mbti, region: u.region,
|
||||
storyBestMonth: u.storyBestMonth || u.story_best_month,
|
||||
storyAchievement: u.storyAchievement || u.story_achievement,
|
||||
storyTurning: u.storyTurning || u.story_turning,
|
||||
helpOffer: u.helpOffer || u.help_offer,
|
||||
helpNeed: u.helpNeed || u.help_need,
|
||||
}), loading: false })
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
this.setData({ loading: false })
|
||||
},
|
||||
|
||||
// 将空值、「未填写」、纯空格均视为未填写(用于隐藏对应项)
|
||||
_emptyIfPlaceholder(v) {
|
||||
if (v == null || v === undefined) return ''
|
||||
const s = String(v).trim()
|
||||
return (s === '' || s === '未填写') ? '' : s
|
||||
},
|
||||
|
||||
enrichAndFormat(raw) {
|
||||
const e = (v) => this._emptyIfPlaceholder(v)
|
||||
const merged = {
|
||||
id: raw.id,
|
||||
name: raw.nickname || raw.name || raw.vipName || raw.vip_name || '创业者',
|
||||
avatar: raw.avatar || raw.vipAvatar || raw.vip_avatar || '',
|
||||
isVip: !!(raw.isVip || raw.is_vip),
|
||||
mbti: e(raw.mbti),
|
||||
region: e(raw.region),
|
||||
industry: e(raw.industry),
|
||||
position: e(raw.position),
|
||||
businessScale: e(raw.businessScale || raw.business_scale),
|
||||
skills: e(raw.skills),
|
||||
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || '',
|
||||
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || '',
|
||||
bestMonth: e(raw.bestMonth || raw.storyBestMonth || raw.story_best_month),
|
||||
achievement: e(raw.achievement || raw.storyAchievement || raw.story_achievement),
|
||||
turningPoint: e(raw.turningPoint || raw.storyTurning || raw.story_turning),
|
||||
canHelp: e(raw.canHelp || raw.helpOffer || raw.help_offer),
|
||||
needHelp: e(raw.needHelp || raw.helpNeed || raw.help_need),
|
||||
project: e(raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro)
|
||||
}
|
||||
|
||||
const contact = merged.contactRaw || ''
|
||||
const wechat = merged.wechatRaw || ''
|
||||
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
|
||||
const unlockData = this._getUnlockData(merged.id)
|
||||
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
|
||||
merged.contactUnlocked = isMatched || unlockData.contact
|
||||
merged.contactFull = contact
|
||||
merged.wechatDisplay = wechat ? (wechat.slice(0, 4) + '****' + (wechat.length > 8 ? wechat.slice(-3) : '')) : ''
|
||||
merged.wechatUnlocked = isMatched || unlockData.wechat
|
||||
merged.wechatFull = wechat
|
||||
return merged
|
||||
},
|
||||
|
||||
_getUnlockData(memberId) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId || !memberId) return { contact: false, wechat: false }
|
||||
try {
|
||||
const raw = wx.getStorageSync('member_unlocks_' + userId)
|
||||
if (Array.isArray(raw) && raw.includes(memberId)) {
|
||||
return { contact: true, wechat: true }
|
||||
}
|
||||
const data = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
|
||||
const member = data[memberId]
|
||||
return {
|
||||
contact: !!(member && member.contact),
|
||||
wechat: !!(member && member.wechat)
|
||||
}
|
||||
} catch (e) { return { contact: false, wechat: false } }
|
||||
},
|
||||
|
||||
_addUnlock(memberId, field) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId || !memberId || !field) return
|
||||
let obj = wx.getStorageSync('member_unlocks_' + userId)
|
||||
if (Array.isArray(obj)) {
|
||||
obj = obj.reduce((o, id) => { o[id] = { contact: true, wechat: true }; return o }, {})
|
||||
}
|
||||
obj = obj && typeof obj === 'object' ? obj : {}
|
||||
if (!obj[memberId]) obj[memberId] = {}
|
||||
obj[memberId][field] = true
|
||||
wx.setStorageSync('member_unlocks_' + userId, obj)
|
||||
},
|
||||
|
||||
_hasUsedFreeForMember(memberId) {
|
||||
const d = this._getUnlockData(memberId)
|
||||
return d.contact || d.wechat
|
||||
},
|
||||
|
||||
unlockField(e) {
|
||||
const field = e.currentTarget.dataset.field
|
||||
if (!field) return
|
||||
const member = this.data.member
|
||||
if (!member?.id || (field !== 'contact' && field !== 'wechat')) return
|
||||
const isLoggedIn = app.globalData.isLoggedIn
|
||||
if (!isLoggedIn) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '请先登录后再解锁超级个体联系方式',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
const d = this._getUnlockData(member.id)
|
||||
if (d[field]) return
|
||||
const isVip = app.globalData.hasFullBook
|
||||
const usedFree = this._hasUsedFreeForMember(member.id)
|
||||
if (isVip || !usedFree) {
|
||||
this._addUnlock(member.id, field)
|
||||
const m = this.enrichAndFormat(member)
|
||||
this.setData({ member: m })
|
||||
wx.showToast({ title: field === 'contact' ? '已解锁联系方式' : '已解锁微信号', icon: 'success' })
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
|
||||
content: '您的免费解锁次数已用完,开通VIP会员(¥1980/年)可无限解锁',
|
||||
confirmText: '去开通',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
|
||||
})
|
||||
},
|
||||
|
||||
copyContact() {
|
||||
const c = this.data.member?.contactFull
|
||||
if (!c) return
|
||||
wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
copyWechat() {
|
||||
const w = this.data.member?.wechatFull
|
||||
if (!w) return
|
||||
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
|
||||
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
return {
|
||||
title: 'Soul创业派对 - 创业者详情',
|
||||
path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
const q = id ? (ref ? `id=${id}&ref=${ref}` : `id=${id}`) : (ref ? `ref=${ref}` : '')
|
||||
return { title: 'Soul创业派对 - 创业者详情', query: q }
|
||||
}
|
||||
})
|
||||
1
miniprogram/pages/member-detail/member-detail.json
Normal file
1
miniprogram/pages/member-detail/member-detail.json
Normal file
@@ -0,0 +1 @@
|
||||
{ "usingComponents": {}, "navigationStyle": "custom" }
|
||||
150
miniprogram/pages/member-detail/member-detail.wxml
Normal file
150
miniprogram/pages/member-detail/member-detail.wxml
Normal file
@@ -0,0 +1,150 @@
|
||||
<!-- Soul创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<text class="nav-icon">‹</text>
|
||||
</view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
|
||||
<!-- 顶部 profile 卡片 -->
|
||||
<view class="card-profile">
|
||||
<view class="profile-deco"></view>
|
||||
<view class="profile-body">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{member.name[0] || '创'}}</text></view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags" wx:if="{{member.mbti || member.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<text class="tag tag-region" wx:if="{{member.region}}"><text class="pin-icon">📍</text>{{member.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.contactRaw || member.contactDisplay || member.wechatRaw || member.wechatDisplay}}">
|
||||
<view class="card-head">
|
||||
<text class="card-icon">👤</text>
|
||||
<text class="card-label">基本信息</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="field" wx:if="{{member.industry}}">
|
||||
<text class="f-key">行业</text>
|
||||
<text class="f-val">{{member.industry}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.position}}">
|
||||
<text class="f-key">职位</text>
|
||||
<text class="f-val">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.businessScale}}">
|
||||
<text class="f-key">业务体量</text>
|
||||
<text class="f-val">{{member.businessScale}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.industry || member.position || member.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{member.skills}}">
|
||||
<text class="f-key">我擅长</text>
|
||||
<text class="f-val">{{member.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.contactRaw || member.contactDisplay}}">
|
||||
<text class="f-key">联系方式</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.contactUnlocked ? member.contactFull : (member.contactDisplay || member.contactRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockField" data-field="contact">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact">📋</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.wechatRaw || member.wechatDisplay}}">
|
||||
<text class="f-key">微信号</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.wechatUnlocked ? member.wechatFull : (member.wechatDisplay || member.wechatRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockField" data-field="wechat">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat">📋</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="card-head">
|
||||
<text class="card-icon bulb">💡</text>
|
||||
<text class="card-label">个人故事</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="story" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><text class="story-icon">🏆</text><text class="story-q">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.bestMonth}}"></view>
|
||||
<view class="story" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><text class="story-icon">⭐</text><text class="story-q">最有成就感的一件事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.achievement}}"></view>
|
||||
<view class="story" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><text class="story-icon turn">🔄</text><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="card-head">
|
||||
<text class="card-icon">🤝</text>
|
||||
<text class="card-label">互助需求</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="help-box help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tag">我能帮你</text>
|
||||
<text class="help-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-box help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tag need">我需要帮助</text>
|
||||
<text class="help-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="card" wx:if="{{member.project}}">
|
||||
<view class="card-head">
|
||||
<text class="card-icon rocket">🚀</text>
|
||||
<text class="card-label">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-txt">{{member.project}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-wrap">
|
||||
<view class="btn-super" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:160rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载和空状态 -->
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
</view>
|
||||
<view class="state-wrap" wx:if="{{!loading && !member}}">
|
||||
<text class="state-emoji">👤</text>
|
||||
<text class="state-txt">暂无该超级个体信息</text>
|
||||
</view>
|
||||
</view>
|
||||
172
miniprogram/pages/member-detail/member-detail.wxss
Normal file
172
miniprogram/pages/member-detail/member-detail.wxss
Normal file
@@ -0,0 +1,172 @@
|
||||
/* Soul创业派对 - 个人资料页(enhanced_professional_profile 1:1 还原) */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 32rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.9);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.nav-back { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5EEAD4; font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
|
||||
.nav-right { display: flex; align-items: center; gap: 16rpx; }
|
||||
.nav-icon-wrap { padding: 8rpx; }
|
||||
.nav-icon-dot { font-size: 28rpx; color: rgba(255,255,255,0.8); }
|
||||
|
||||
.scroll-wrap { height: calc(100vh - 88px); }
|
||||
|
||||
/* ===== 顶部 Profile 卡片 ===== */
|
||||
.card-profile {
|
||||
position: relative; margin: 32rpx 32rpx 0;
|
||||
padding: 64rpx 40rpx 48rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-deco {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(180deg, rgba(30, 58, 69, 0.3) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.avatar-outer {
|
||||
position: relative;
|
||||
width: 176rpx; height: 176rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.avatar-wrap.vip-ring {
|
||||
border: 4rpx solid transparent;
|
||||
background: linear-gradient(135deg, #F59E0B, #5EEAD4, #F59E0B);
|
||||
background-size: 200% 200%;
|
||||
animation: vipGlow 4s ease infinite;
|
||||
}
|
||||
@keyframes vipGlow {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%;
|
||||
background: #17212F;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
|
||||
}
|
||||
.vip-tag {
|
||||
position: absolute; bottom: -4rpx; right: -4rpx;
|
||||
background: linear-gradient(135deg, #F59E0B, #e8920d);
|
||||
color: #000; font-size: 20rpx; font-weight: 800;
|
||||
padding: 6rpx 14rpx; border-radius: 16rpx;
|
||||
z-index: 2;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
|
||||
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 8rpx 24rpx; border-radius: 999rpx; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94, 234, 212, 0.2); }
|
||||
.tag-region { background: #1F2937; color: #D1D5DB; border: 1rpx solid rgba(255, 255, 255, 0.1); display: flex; align-items: center; gap: 8rpx; }
|
||||
.pin-icon { color: #EF4444; font-size: 22rpx; }
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.card {
|
||||
margin: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.card-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.card-icon { font-size: 40rpx; }
|
||||
.card-icon.bulb { filter: sepia(1) saturate(3) hue-rotate(15deg); }
|
||||
.card-icon.rocket { opacity: 0.9; }
|
||||
.card-label { font-size: 30rpx; font-weight: 700; color: #fff; letter-spacing: 1rpx; }
|
||||
|
||||
.card-body { }
|
||||
.field { margin-bottom: 32rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.f-key { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 12rpx; }
|
||||
.f-val { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.6; }
|
||||
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
|
||||
.f-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
|
||||
.icon-eye-off { display: flex; align-items: center; justify-content: center; }
|
||||
.icon-eye-off .icon-img { width: 40rpx; height: 40rpx; }
|
||||
|
||||
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }
|
||||
|
||||
/* ===== 个人故事 ===== */
|
||||
.story { margin-bottom: 32rpx; }
|
||||
.story:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { font-size: 32rpx; }
|
||||
.story-icon.turn { opacity: 0.9; }
|
||||
.story-q { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 互助需求 ===== */
|
||||
.help-box {
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #17212F;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.help-box:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx; font-weight: 600;
|
||||
padding: 6rpx 16rpx; border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tag { color: #5EEAD4; background: #112D2A; }
|
||||
.help-need .help-tag { color: #F59E0B; background: #2D1F0D; }
|
||||
.help-txt { display: block; font-size: 26rpx; color: #fff; line-height: 1.6; letter-spacing: 1rpx; }
|
||||
|
||||
/* ===== 项目介绍 ===== */
|
||||
.proj-txt { font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 底部按钮 ===== */
|
||||
.bottom-wrap {
|
||||
padding: 48rpx 32rpx 0;
|
||||
}
|
||||
.btn-super {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
width: 100%;
|
||||
padding: 32rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.3);
|
||||
color: #F59E0B;
|
||||
font-size: 30rpx; font-weight: 500;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.btn-arrow { font-size: 36rpx; font-weight: 300; }
|
||||
|
||||
/* ===== 状态 ===== */
|
||||
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 24rpx; }
|
||||
.state-txt { font-size: 28rpx; color: #64748B; }
|
||||
.state-emoji { font-size: 96rpx; }
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2);
|
||||
border-top-color: #5EEAD4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
113
miniprogram/pages/mentor-detail/mentor-detail.js
Normal file
113
miniprogram/pages/mentor-detail/mentor-detail.js
Normal file
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Soul创业派对 - 导师详情(stitch_soul)
|
||||
* 联系导师按钮 → 弹出 v2 弹窗(选择咨询项目)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
mentor: null,
|
||||
loading: true,
|
||||
showConsultModal: false,
|
||||
consultOptions: [],
|
||||
selectedType: '',
|
||||
selectedAmount: 0,
|
||||
creating: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options.id) this.loadDetail(options.id)
|
||||
},
|
||||
|
||||
async loadDetail(id) {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/mentors/${id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
if (d.judgmentStyle && typeof d.judgmentStyle === 'string') {
|
||||
d.judgmentStyleArr = d.judgmentStyle.split(/[,,]/).map(s => s.trim()).filter(Boolean)
|
||||
} else {
|
||||
d.judgmentStyleArr = []
|
||||
}
|
||||
const fmt = v => v != null ? String(Math.floor(v)).replace(/\B(?=(\d{3})+(?!\d))/g, ',') : ''
|
||||
d.priceSingleFmt = fmt(d.priceSingle)
|
||||
d.priceHalfYearFmt = fmt(d.priceHalfYear)
|
||||
d.priceYearFmt = fmt(d.priceYear)
|
||||
const options = []
|
||||
if (d.priceSingle != null) options.push({ type: 'single', label: '单次咨询', desc: '1小时深度沟通', price: d.priceSingle, priceFmt: fmt(d.priceSingle), original: null })
|
||||
if (d.priceHalfYear != null) options.push({ type: 'half_year', label: '半年咨询', desc: '不限次数 · 关键节点陪伴', price: d.priceHalfYear, priceFmt: fmt(d.priceHalfYear), original: '98,000' })
|
||||
if (d.priceYear != null) options.push({ type: 'year', label: '年度咨询', desc: '全年度战略顾问', price: d.priceYear, priceFmt: fmt(d.priceYear), original: '196,000', recommend: true })
|
||||
this.setData({
|
||||
mentor: d,
|
||||
consultOptions: options,
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
onContactTap() {
|
||||
if (!this.data.mentor || this.data.consultOptions.length === 0) {
|
||||
wx.showToast({ title: '暂无咨询项目', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showConsultModal: true,
|
||||
selectedType: this.data.consultOptions[0].type,
|
||||
selectedAmount: this.data.consultOptions[0].price,
|
||||
})
|
||||
},
|
||||
|
||||
closeConsultModal() {
|
||||
this.setData({ showConsultModal: false })
|
||||
},
|
||||
|
||||
onSelectOption(e) {
|
||||
const item = e.currentTarget.dataset.item
|
||||
this.setData({ selectedType: item.type, selectedAmount: item.price })
|
||||
},
|
||||
|
||||
async onConfirmConsult() {
|
||||
const { mentor, selectedType } = this.data
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
wx.navigateTo({ url: '/pages/my/my' })
|
||||
return
|
||||
}
|
||||
this.setData({ creating: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/mentors/${mentor.id}/book`,
|
||||
method: 'POST',
|
||||
data: { userId, consultationType: selectedType },
|
||||
})
|
||||
if (res?.success && res.data) {
|
||||
this.setData({ showConsultModal: false, creating: false })
|
||||
wx.showToast({ title: '预约创建成功', icon: 'success' })
|
||||
// TODO: 调起支付 productType: mentor_consultation, productId: res.data.id
|
||||
wx.showModal({
|
||||
title: '预约成功',
|
||||
content: '请联系客服完成后续对接',
|
||||
showCancel: false,
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: res?.error || '创建失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.showToast({ title: '创建失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ creating: false })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
})
|
||||
1
miniprogram/pages/mentor-detail/mentor-detail.json
Normal file
1
miniprogram/pages/mentor-detail/mentor-detail.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom"}
|
||||
118
miniprogram/pages/mentor-detail/mentor-detail.wxml
Normal file
118
miniprogram/pages/mentor-detail/mentor-detail.wxml
Normal file
@@ -0,0 +1,118 @@
|
||||
<!-- 导师详情 -->
|
||||
<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-r"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<block wx:else>
|
||||
<view class="mentor-header" wx:if="{{mentor}}">
|
||||
<view class="mentor-avatar-wrap">
|
||||
<image wx:if="{{mentor.avatar}}" class="mentor-avatar" src="{{mentor.avatar}}" mode="aspectFill"></image>
|
||||
<view wx:else class="mentor-avatar-placeholder">{{mentor.name ? mentor.name[0] : '?'}}</view>
|
||||
</view>
|
||||
<text class="mentor-name">{{mentor.name}}</text>
|
||||
<text class="mentor-intro">{{mentor.intro}}</text>
|
||||
<view class="mentor-quote" wx:if="{{mentor.quote}}">{{mentor.quote}}</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.whyFind}}">
|
||||
<view class="block-header"><text class="block-num">01</text><text class="block-title">为什么找{{mentor.name}}?</text></view>
|
||||
<text class="block-text">{{mentor.whyFind}}</text>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.offering}}">
|
||||
<view class="block-header"><text class="block-num">02</text><text class="block-title">提供什么?</text></view>
|
||||
<text class="block-text">{{mentor.offering}}</text>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.priceSingle || mentor.priceHalfYear || mentor.priceYear}}">
|
||||
<view class="block-header"><text class="block-num">03</text><text class="block-title">收费标准</text></view>
|
||||
<view class="price-table">
|
||||
<view class="price-thead">
|
||||
<text class="p-col-2">咨询项目</text>
|
||||
<text class="p-col-center">时长</text>
|
||||
<text class="p-col-right">价格</text>
|
||||
</view>
|
||||
<view class="price-row" wx:if="{{mentor.priceSingle}}">
|
||||
<text class="p-col-2">单次咨询</text>
|
||||
<text class="p-col-center">1小时</text>
|
||||
<text class="p-col-right price-num">¥{{mentor.priceSingleFmt || mentor.priceSingle}}</text>
|
||||
</view>
|
||||
<view class="price-row price-row-alt" wx:if="{{mentor.priceHalfYear}}">
|
||||
<text class="p-col-2">半年咨询</text>
|
||||
<text class="p-col-center">-</text>
|
||||
<view class="p-col-right">
|
||||
<text class="price-original">98,000</text>
|
||||
<text class="price-num">¥{{mentor.priceHalfYearFmt || mentor.priceHalfYear}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="price-row" wx:if="{{mentor.priceYear}}">
|
||||
<text class="p-col-2">年度咨询</text>
|
||||
<text class="p-col-center">-</text>
|
||||
<view class="p-col-right">
|
||||
<text class="price-original">196,000</text>
|
||||
<text class="price-num">¥{{mentor.priceYearFmt || mentor.priceYear}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="block" wx:if="{{mentor.judgmentStyle}}">
|
||||
<view class="block-header"><text class="block-num">04</text><text class="block-title">判断风格</text></view>
|
||||
<view class="style-tags">
|
||||
<text class="style-tag" wx:for="{{mentor.judgmentStyleArr}}" wx:key="*this">{{item}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-btn-area">
|
||||
<view class="contact-btn" bindtap="onContactTap">
|
||||
<text class="contact-icon">💬</text>
|
||||
<text>联系导师</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- v2 弹窗:选择咨询项目 -->
|
||||
<view class="modal-overlay" wx:if="{{showConsultModal}}" bindtap="closeConsultModal">
|
||||
<view class="modal-content" catchtap="">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">选择咨询项目</text>
|
||||
<text class="modal-close" bindtap="closeConsultModal">✕</text>
|
||||
</view>
|
||||
<view class="consult-options">
|
||||
<view
|
||||
wx:for="{{consultOptions}}"
|
||||
wx:key="type"
|
||||
class="consult-option {{selectedType === item.type ? 'option-selected' : ''}}"
|
||||
data-item="{{item}}"
|
||||
bindtap="onSelectOption"
|
||||
>
|
||||
<view class="option-row1">
|
||||
<text class="option-label">{{item.label}}</text>
|
||||
<text class="option-rec" wx:if="{{item.recommend}}">推荐</text>
|
||||
<view class="option-radio {{selectedType === item.type ? 'radio-selected' : ''}}"></view>
|
||||
</view>
|
||||
<view class="option-row2">
|
||||
<text class="option-desc">{{item.desc}}</text>
|
||||
<view class="option-price-wrap">
|
||||
<text class="option-price-old" wx:if="{{item.original}}">¥{{item.original}}</text>
|
||||
<text class="option-price">¥{{item.priceFmt || item.price}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="modal-footer">
|
||||
<view class="confirm-btn" bindtap="onConfirmConsult" disabled="{{creating}}">
|
||||
{{creating ? '处理中...' : '确认选择 →'}}
|
||||
</view>
|
||||
<text class="footer-hint">点击确认即代表同意 <text class="footer-link">服务协议</text></text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
70
miniprogram/pages/mentor-detail/mentor-detail.wxss
Normal file
70
miniprogram/pages/mentor-detail/mentor-detail.wxss
Normal file
@@ -0,0 +1,70 @@
|
||||
/* 按 mentor_detail_profile_1 + mentor_detail_profile_2 设计稿 */
|
||||
.page { background: #000; min-height: 100vh; color: #fff; }
|
||||
.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(0,0,0,0.9); border-bottom: 1rpx solid #27272a; }
|
||||
.nav-back { width: 60rpx; }
|
||||
.back-icon { font-size: 44rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 500; }
|
||||
.nav-placeholder-r { width: 60rpx; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #71717a; }
|
||||
.mentor-header { display: flex; flex-direction: column; align-items: center; padding: 48rpx 24rpx; text-align: center; }
|
||||
.mentor-avatar-wrap { margin-bottom: 24rpx; }
|
||||
.mentor-avatar { width: 192rpx; height: 192rpx; border-radius: 50%; border: 4rpx solid #4FD1C5; display: block; background: rgba(255,255,255,0.1); }
|
||||
.mentor-avatar-placeholder { width: 192rpx; height: 192rpx; border-radius: 50%; border: 4rpx solid #4FD1C5; background: rgba(79,209,197,0.2); display: flex; align-items: center; justify-content: center; font-size: 72rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-name { font-size: 48rpx; font-weight: bold; margin-bottom: 8rpx; }
|
||||
.mentor-intro { font-size: 28rpx; color: #4FD1C5; font-weight: 500; margin-bottom: 24rpx; }
|
||||
.mentor-quote { padding: 32rpx; background: #1E1E1E; border-left: 8rpx solid #4FD1C5; border-radius: 24rpx; font-size: 28rpx; color: #d4d4d8; text-align: left; width: 100%; max-width: 600rpx; box-sizing: border-box; }
|
||||
|
||||
.block { padding: 0 24rpx 64rpx; }
|
||||
.block-header { display: flex; align-items: center; margin-bottom: 24rpx; }
|
||||
.block-num { background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: bold; padding: 8rpx 20rpx; border-radius: 999rpx; margin-right: 16rpx; }
|
||||
.block-title { font-size: 36rpx; font-weight: bold; }
|
||||
.block-text { font-size: 26rpx; color: #A0AEC0; line-height: 1.7; display: block; }
|
||||
|
||||
/* 03 收费标准 - 表头青色 */
|
||||
.price-table { background: #1E1E1E; border-radius: 24rpx; overflow: hidden; }
|
||||
.price-thead { display: flex; align-items: center; padding: 24rpx 32rpx; background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: bold; }
|
||||
.p-col-2 { flex: 2; }
|
||||
.p-col-center { flex: 1; text-align: center; }
|
||||
.p-col-right { flex: 1; text-align: right; min-width: 160rpx; }
|
||||
.price-row { display: flex; align-items: center; padding: 32rpx; border-bottom: 1rpx solid #27272a; }
|
||||
.price-row:last-child { border-bottom: none; }
|
||||
.price-row-alt { background: rgba(255,255,255,0.02); }
|
||||
.price-row .p-col-2 { font-size: 28rpx; font-weight: 500; }
|
||||
.price-row .p-col-center { font-size: 24rpx; color: #71717a; }
|
||||
.price-num { font-size: 28rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.price-original { font-size: 20rpx; color: #71717a; text-decoration: line-through; display: block; margin-bottom: 4rpx; }
|
||||
.price-row .p-col-right { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
|
||||
.style-tags { display: flex; flex-wrap: wrap; gap: 24rpx; }
|
||||
.style-tag { padding: 16rpx 32rpx; background: #1E1E1E; border: 1rpx solid #3f3f46; border-radius: 999rpx; font-size: 28rpx; font-weight: 500; }
|
||||
|
||||
.bottom-btn-area { position: fixed; bottom: 0; left: 0; right: 0; padding: 24rpx 32rpx; padding-bottom: calc(24rpx + env(safe-area-inset-bottom)); background: rgba(0,0,0,0.95); border-top: 1rpx solid #27272a; z-index: 50; }
|
||||
.contact-btn { display: flex; align-items: center; justify-content: center; gap: 16rpx; width: 100%; height: 96rpx; background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx; box-shadow: 0 8rpx 32rpx rgba(79,209,197,0.25); }
|
||||
.contact-icon { font-size: 36rpx; }
|
||||
|
||||
/* v2 弹窗 - mentor_detail_profile_2 */
|
||||
.modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.7); backdrop-filter: blur(4px); z-index: 200; display: flex; align-items: flex-end; justify-content: center; }
|
||||
.modal-content { width: 100%; max-height: 85vh; background: #121212; border-radius: 32rpx 32rpx 0 0; padding: 32rpx; padding-bottom: calc(48rpx + env(safe-area-inset-bottom)); border: 1rpx solid rgba(255,255,255,0.08); }
|
||||
.modal-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 32rpx; padding-bottom: 24rpx; border-bottom: 1rpx solid #27272a; }
|
||||
.modal-title { font-size: 40rpx; font-weight: bold; }
|
||||
.modal-close { font-size: 40rpx; color: #71717a; padding: 16rpx; }
|
||||
.consult-options { margin-bottom: 32rpx; }
|
||||
.consult-option { padding: 32rpx; margin-bottom: 24rpx; background: #1E1E1E; border-radius: 24rpx; border: 2rpx solid #3f3f46; }
|
||||
.option-selected { border-color: #4FD1C5; background: rgba(79,209,197,0.08); }
|
||||
.option-row1 { display: flex; align-items: center; margin-bottom: 16rpx; }
|
||||
.option-label { font-size: 32rpx; font-weight: bold; flex: 1; }
|
||||
.option-rec { font-size: 20rpx; padding: 6rpx 16rpx; background: #ED8936; color: #fff; border-radius: 8rpx; margin-right: 16rpx; }
|
||||
.option-radio { width: 40rpx; height: 40rpx; border-radius: 50%; border: 2rpx solid #71717a; flex-shrink: 0; }
|
||||
.radio-selected { border-color: #4FD1C5; background: #4FD1C5; }
|
||||
.option-row2 { display: flex; justify-content: space-between; align-items: flex-end; }
|
||||
.option-desc { font-size: 24rpx; color: #71717a; }
|
||||
.option-price-wrap { display: flex; flex-direction: column; align-items: flex-end; }
|
||||
.option-price-old { font-size: 22rpx; color: #71717a; text-decoration: line-through; margin-bottom: 4rpx; }
|
||||
.option-price { font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.confirm-btn { width: 100%; height: 100rpx; line-height: 100rpx; text-align: center; background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx; box-shadow: 0 8rpx 32rpx rgba(79,209,197,0.25); }
|
||||
.confirm-btn[disabled] { opacity: 0.6; }
|
||||
.footer-hint { display: block; font-size: 22rpx; color: #71717a; text-align: center; margin-top: 24rpx; }
|
||||
.footer-link { color: #4FD1C5; }
|
||||
|
||||
.bottom-space { height: 180rpx; }
|
||||
65
miniprogram/pages/mentors/mentors.js
Normal file
65
miniprogram/pages/mentors/mentors.js
Normal file
@@ -0,0 +1,65 @@
|
||||
/**
|
||||
* Soul创业派对 - 选择导师(stitch_soul)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
mentors: [],
|
||||
loading: true,
|
||||
searchKw: '',
|
||||
filterSkill: '',
|
||||
skills: ['全部', '项目结构判断', '产品架构', 'BP梳理', '职业转型'],
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
onSearchInput(e) {
|
||||
this.setData({ searchKw: e.detail.value })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
onFilterTap(e) {
|
||||
const skill = e.currentTarget.dataset.skill
|
||||
this.setData({ filterSkill: skill === '全部' ? '' : skill })
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
async loadMentors() {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
let url = '/api/miniprogram/mentors'
|
||||
const params = []
|
||||
if (this.data.searchKw) params.push(`q=${encodeURIComponent(this.data.searchKw)}`)
|
||||
if (this.data.filterSkill) params.push(`skill=${encodeURIComponent(this.data.filterSkill)}`)
|
||||
if (params.length) url += '?' + params.join('&')
|
||||
|
||||
const res = await app.request({ url, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
this.setData({ mentors: res.data })
|
||||
} else {
|
||||
this.setData({ mentors: [] })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ mentors: [] })
|
||||
}
|
||||
this.setData({ loading: false })
|
||||
},
|
||||
|
||||
onRefresh() {
|
||||
this.loadMentors()
|
||||
},
|
||||
|
||||
goDetail(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: `/pages/mentor-detail/mentor-detail?id=${id}` })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
})
|
||||
1
miniprogram/pages/mentors/mentors.json
Normal file
1
miniprogram/pages/mentors/mentors.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom"}
|
||||
70
miniprogram/pages/mentors/mentors.wxml
Normal file
70
miniprogram/pages/mentors/mentors.wxml
Normal file
@@ -0,0 +1,70 @@
|
||||
<!-- 选择导师 -->
|
||||
<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-r"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="search-bar">
|
||||
<view class="search-input-wrap">
|
||||
<text class="search-icon">🔍</text>
|
||||
<input
|
||||
class="search-input"
|
||||
placeholder="搜索导师、技能或行业..."
|
||||
value="{{searchKw}}"
|
||||
bindinput="onSearchInput"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="filter-row">
|
||||
<view
|
||||
class="filter-tag {{!filterSkill ? 'filter-active' : ''}}"
|
||||
data-skill="全部"
|
||||
bindtap="onFilterTap"
|
||||
>全部</view>
|
||||
<view
|
||||
wx:for="{{skills}}"
|
||||
wx:key="*this"
|
||||
wx:if="{{item !== '全部'}}"
|
||||
class="filter-tag {{filterSkill === item ? 'filter-active' : ''}}"
|
||||
data-skill="{{item}}"
|
||||
bindtap="onFilterTap"
|
||||
>{{item}}</view>
|
||||
</view>
|
||||
|
||||
<view class="section-header">
|
||||
<text class="section-title">推荐导师</text>
|
||||
<text class="section-more" bindtap="loadMentors">查看全部 ›</text>
|
||||
</view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<view class="mentor-list" wx:else>
|
||||
<view class="mentor-card" wx:for="{{mentors}}" wx:key="id">
|
||||
<view class="mentor-card-inner">
|
||||
<view class="mentor-avatar-wrap">
|
||||
<image wx:if="{{item.avatar}}" class="mentor-avatar" src="{{item.avatar}}" mode="aspectFill"></image>
|
||||
<view wx:else class="mentor-avatar-placeholder">{{item.name ? item.name[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="mentor-info">
|
||||
<text class="mentor-name">{{item.name}}</text>
|
||||
<text class="mentor-intro">{{item.intro || ''}}</text>
|
||||
<view class="mentor-tags" wx:if="{{item.tagsArr && item.tagsArr.length}}">
|
||||
<text class="mentor-tag" wx:for="{{item.tagsArr}}" wx:key="*this" wx:for-item="tag">{{tag}}</text>
|
||||
</view>
|
||||
<view class="mentor-price-row">
|
||||
<view class="mentor-price-wrap">
|
||||
<text class="mentor-price-num">¥{{item.priceSingle || 0}}</text>
|
||||
<text class="mentor-price-unit">起 / 单次咨询</text>
|
||||
</view>
|
||||
<view class="mentor-btn" data-id="{{item.id}}" bindtap="goDetail">预约</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" wx:if="{{!loading && mentors.length === 0}}">暂无导师</view>
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
40
miniprogram/pages/mentors/mentors.wxss
Normal file
40
miniprogram/pages/mentors/mentors.wxss
Normal file
@@ -0,0 +1,40 @@
|
||||
/* 按 mentor_listing_screen 设计稿 */
|
||||
.page { background: #000; min-height: 100vh; color: #fff; }
|
||||
.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(0,0,0,0.95); border-bottom: 1rpx solid #27272a; }
|
||||
.nav-back { width: 60rpx; }
|
||||
.back-icon { font-size: 44rpx; color: #fff; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 500; }
|
||||
.nav-placeholder-r { width: 60rpx; }
|
||||
|
||||
.search-bar { padding: 24rpx 24rpx 16rpx; }
|
||||
.search-input-wrap { display: flex; align-items: center; background: #1C1C1E; border-radius: 24rpx; padding: 24rpx 32rpx; border: 1rpx solid #27272a; }
|
||||
.search-icon { font-size: 36rpx; color: #71717a; margin-right: 16rpx; flex-shrink: 0; }
|
||||
.search-input { flex: 1; font-size: 28rpx; color: #fff; background: transparent; }
|
||||
.filter-row { display: flex; gap: 24rpx; padding: 24rpx; overflow-x: auto; }
|
||||
.filter-tag { flex-shrink: 0; padding: 12rpx 32rpx; border-radius: 999rpx; font-size: 24rpx; background: #1C1C1E; border: 1rpx solid #27272a; color: #d4d4d8; }
|
||||
.filter-active { background: #4FD1C5; border-color: #4FD1C5; color: #000; font-weight: 600; }
|
||||
|
||||
.section-header { display: flex; justify-content: space-between; align-items: flex-end; padding: 0 24rpx 24rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: bold; color: #e4e4e7; }
|
||||
.section-more { font-size: 24rpx; color: #4FD1C5; font-weight: 500; }
|
||||
|
||||
.loading { padding: 48rpx; text-align: center; color: #71717a; }
|
||||
.mentor-list { padding: 0 24rpx 48rpx; }
|
||||
.mentor-card { margin-bottom: 24rpx; background: #1C1C1E; border-radius: 32rpx; padding: 32rpx; border: 1rpx solid #27272a; }
|
||||
.mentor-card-inner { display: flex; gap: 32rpx; }
|
||||
.mentor-avatar-wrap { width: 112rpx; height: 112rpx; flex-shrink: 0; }
|
||||
.mentor-avatar { width: 112rpx; height: 112rpx; border-radius: 50%; display: block; border: 1rpx solid #3f3f46; }
|
||||
.mentor-avatar-placeholder { width: 112rpx; height: 112rpx; border-radius: 50%; background: rgba(79,209,197,0.2); border: 1rpx solid #3f3f46; display: flex; align-items: center; justify-content: center; font-size: 40rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-info { flex: 1; min-width: 0; }
|
||||
.mentor-name { display: block; font-size: 32rpx; font-weight: bold; color: #fff; margin-bottom: 8rpx; }
|
||||
.mentor-intro { display: block; font-size: 24rpx; color: #A1A1AA; margin-bottom: 16rpx; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mentor-tags { display: flex; flex-wrap: wrap; gap: 12rpx; margin-bottom: 24rpx; }
|
||||
.mentor-tag { font-size: 20rpx; padding: 8rpx 16rpx; background: #2C2C2E; color: #E5E7EB; border-radius: 12rpx; border: 1rpx solid rgba(63,63,70,0.5); }
|
||||
.mentor-price-row { display: flex; justify-content: space-between; align-items: center; padding-top: 24rpx; border-top: 1rpx solid #27272a; }
|
||||
.mentor-price-wrap { display: flex; align-items: baseline; gap: 8rpx; }
|
||||
.mentor-price-num { font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.mentor-price-unit { font-size: 24rpx; color: #71717a; }
|
||||
.mentor-btn { padding: 12rpx 32rpx; background: #fff; color: #000; font-size: 24rpx; font-weight: bold; border-radius: 999rpx; }
|
||||
|
||||
.empty { padding: 96rpx; text-align: center; color: rgba(255,255,255,0.4); }
|
||||
.bottom-space { height: 80rpx; }
|
||||
939
miniprogram/pages/my/my.js
Normal file
939
miniprogram/pages/my/my.js
Normal file
@@ -0,0 +1,939 @@
|
||||
/**
|
||||
* Soul创业派对 - 我的页面
|
||||
* 开发: 卡若
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
statusBarHeight: 44,
|
||||
navBarHeight: 88,
|
||||
|
||||
// 用户状态
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
|
||||
// 统计数据
|
||||
totalSections: 62,
|
||||
readCount: 0,
|
||||
referralCount: 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: true,
|
||||
earningsRefreshing: false,
|
||||
|
||||
// 阅读统计
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
|
||||
// 功能配置
|
||||
matchEnabled: false,
|
||||
|
||||
// VIP状态
|
||||
isVip: false,
|
||||
vipExpireDate: '',
|
||||
|
||||
// 待确认收款
|
||||
pendingConfirmList: [],
|
||||
withdrawMchId: '',
|
||||
withdrawAppId: '',
|
||||
pendingConfirmAmount: '0.00',
|
||||
receivingAll: false,
|
||||
|
||||
// 未登录假资料(展示用)
|
||||
guestNickname: '游客',
|
||||
guestAvatar: '',
|
||||
|
||||
// 登录弹窗
|
||||
showLoginModal: false,
|
||||
isLoggingIn: false,
|
||||
// 用户须主动勾选同意协议(审核要求:不得默认同意)
|
||||
agreeProtocol: false,
|
||||
|
||||
// 修改昵称弹窗
|
||||
showNicknameModal: false,
|
||||
editingNickname: '',
|
||||
|
||||
// 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像)
|
||||
showAvatarModal: false,
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
contactWechat: '',
|
||||
contactSaving: false,
|
||||
pendingWithdraw: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
navBarHeight: app.globalData.navBarHeight
|
||||
})
|
||||
this.loadFeatureConfig()
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 设置TabBar选中状态(根据 matchEnabled 动态设置)
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
if (tabBar.updateSelected) {
|
||||
tabBar.updateSelected()
|
||||
} else {
|
||||
const selected = tabBar.data.matchEnabled ? 3 : 2
|
||||
tabBar.setData({ selected })
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/config')
|
||||
const features = (res && res.features) || (res && res.data && res.data.features) || {}
|
||||
this.setData({ matchEnabled: features.matchEnabled === true })
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
this.setData({ matchEnabled: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 初始化用户状态
|
||||
initUserStatus() {
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
|
||||
if (isLoggedIn && userInfo) {
|
||||
const readIds = app.globalData.readSectionIds || []
|
||||
const recentList = readIds.slice(-5).reverse().map(id => ({
|
||||
id,
|
||||
mid: app.getSectionMid(id),
|
||||
title: `章节 ${id}`
|
||||
}))
|
||||
|
||||
const userId = userInfo.id || ''
|
||||
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
|
||||
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
|
||||
|
||||
// 先设基础信息;收益由 loadMyEarnings 专用接口拉取,加载前用 - 占位
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
userInfo,
|
||||
userIdShort,
|
||||
userWechat,
|
||||
readCount: Math.min(app.getReadCount(), this.data.totalSections || 62),
|
||||
referralCount: userInfo.referralCount || 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: true,
|
||||
recentChapters: recentList,
|
||||
totalReadTime: Math.floor(Math.random() * 200) + 50
|
||||
})
|
||||
this.loadMyEarnings()
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
} else {
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
userIdShort: '',
|
||||
readCount: app.getReadCount(),
|
||||
referralCount: 0,
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: []
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 拉取待确认收款列表(用于「确认收款」按钮)
|
||||
async loadPendingConfirm() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) return
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/withdraw/pending-confirm?userId=' + userInfo.id, silent: true })
|
||||
if (res && res.success && res.data) {
|
||||
const list = (res.data.list || []).map(item => ({
|
||||
id: item.id,
|
||||
amount: (item.amount || 0).toFixed(2),
|
||||
package: item.package,
|
||||
createdAt: (item.createdAt ?? item.created_at) ? this.formatDateMy(item.createdAt ?? item.created_at) : '--'
|
||||
}))
|
||||
const total = list.reduce((sum, it) => sum + (parseFloat(it.amount) || 0), 0)
|
||||
this.setData({
|
||||
pendingConfirmList: list,
|
||||
withdrawMchId: res.data.mchId ?? res.data.mch_id ?? '',
|
||||
withdrawAppId: res.data.appId ?? res.data.app_id ?? '',
|
||||
pendingConfirmAmount: total.toFixed(2)
|
||||
})
|
||||
} else {
|
||||
this.setData({ pendingConfirmList: [], withdrawMchId: '', withdrawAppId: '', pendingConfirmAmount: '0.00' })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ pendingConfirmList: [], pendingConfirmAmount: '0.00' })
|
||||
}
|
||||
},
|
||||
|
||||
formatDateMy(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${m}-${day}`
|
||||
},
|
||||
|
||||
// 确认收款:有 package 时调起微信收款页,成功后记录;无 package 时仅调用后端记录「已确认收款」
|
||||
async confirmReceive(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const id = e.currentTarget.dataset.id
|
||||
const list = this.data.pendingConfirmList || []
|
||||
let item = (typeof index === 'number' || (index !== undefined && index !== '')) ? list[index] : null
|
||||
if (!item && id) item = list.find(x => x.id === id) || null
|
||||
if (!item) {
|
||||
wx.showToast({ title: '请稍后刷新再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const mchId = this.data.withdrawMchId
|
||||
const appId = this.data.withdrawAppId
|
||||
const hasPackage = item.package && mchId && appId && wx.canIUse('requestMerchantTransfer')
|
||||
|
||||
const recordConfirmReceived = async () => {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo && userInfo.id) {
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/withdraw/confirm-received',
|
||||
method: 'POST',
|
||||
data: { withdrawalId: item.id, userId: userInfo.id }
|
||||
})
|
||||
} catch (e) { /* 仅记录,不影响前端展示 */ }
|
||||
}
|
||||
const newList = list.filter(x => x.id !== item.id)
|
||||
this.setData({ pendingConfirmList: newList })
|
||||
this.loadPendingConfirm()
|
||||
}
|
||||
|
||||
if (hasPackage) {
|
||||
wx.showLoading({ title: '调起收款...', mask: true })
|
||||
wx.requestMerchantTransfer({
|
||||
mchId,
|
||||
appId,
|
||||
package: item.package,
|
||||
success: async () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '收款成功', icon: 'success' })
|
||||
await recordConfirmReceived()
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
const msg = (err.errMsg || '').includes('cancel') ? '已取消' : (err.errMsg || '收款失败')
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
},
|
||||
complete: () => { wx.hideLoading() }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 无 package 时仅记录「确认已收款」(当前直接打款无 package,用户点按钮即记录)
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
await recordConfirmReceived()
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已记录确认收款', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: (e && e.message) || '操作失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 一键收款:逐条调起微信收款页(有上一页则返回,无则回首页)
|
||||
async handleOneClickReceive() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
if (this.data.receivingAll) return
|
||||
|
||||
const list = this.data.pendingConfirmList || []
|
||||
if (list.length === 0) {
|
||||
wx.showToast({ title: '暂无待收款', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!wx.canIUse('requestMerchantTransfer')) {
|
||||
wx.showToast({ title: '当前微信版本过低,请更新后重试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const mchIdDefault = this.data.withdrawMchId || ''
|
||||
const appIdDefault = this.data.withdrawAppId || ''
|
||||
|
||||
this.setData({ receivingAll: true })
|
||||
|
||||
try {
|
||||
for (let i = 0; i < list.length; i++) {
|
||||
const item = list[i]
|
||||
wx.showLoading({ title: `收款中 ${i + 1}/${list.length}`, mask: true })
|
||||
|
||||
// 兜底:每次收款前取最新 confirm-info,避免 package 不完整或过期
|
||||
let mchId = mchIdDefault
|
||||
let appId = appIdDefault
|
||||
let pkg = item.package
|
||||
try {
|
||||
const infoRes = await app.request({
|
||||
url: '/api/miniprogram/withdraw/confirm-info?id=' + encodeURIComponent(item.id),
|
||||
silent: true
|
||||
})
|
||||
if (infoRes && infoRes.success && infoRes.data) {
|
||||
mchId = infoRes.data.mchId || mchId
|
||||
appId = infoRes.data.appId || appId
|
||||
pkg = infoRes.data.package || pkg
|
||||
}
|
||||
} catch (e) { /* confirm-info 失败不阻断,使用列表字段兜底 */ }
|
||||
|
||||
if (!pkg) {
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '当前订单无法调起收款页,请稍后在「提现记录」中点击“领取零钱”。',
|
||||
confirmText: '去查看',
|
||||
cancelText: '知道了',
|
||||
success: (r) => {
|
||||
if (r.confirm) wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// requestMerchantTransfer:失败/取消会走 fail
|
||||
await new Promise((resolve, reject) => {
|
||||
wx.requestMerchantTransfer({
|
||||
mchId,
|
||||
appId: appId || wx.getAccountInfoSync().miniProgram.appId,
|
||||
package: pkg,
|
||||
success: resolve,
|
||||
fail: reject
|
||||
})
|
||||
})
|
||||
|
||||
// 收款页调起成功后记录确认(后端负责状态流转)
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo && userInfo.id) {
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/withdraw/confirm-received',
|
||||
method: 'POST',
|
||||
data: { withdrawalId: item.id, userId: userInfo.id }
|
||||
})
|
||||
} catch (e) { /* 仅记录,不影响前端 */ }
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = (err && err.errMsg && String(err.errMsg).includes('cancel')) ? '已取消收款' : '收款失败,请重试'
|
||||
wx.showToast({ title: msg, icon: 'none' })
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
this.setData({ receivingAll: false })
|
||||
this.loadPendingConfirm()
|
||||
}
|
||||
},
|
||||
|
||||
// 专用接口:拉取「我的收益」卡片数据(累计、可提现、推荐人数)
|
||||
async loadMyEarnings() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo || !userInfo.id) {
|
||||
this.setData({ earningsLoading: false })
|
||||
return
|
||||
}
|
||||
const formatMoney = (num) => (typeof num === 'number' ? num.toFixed(2) : '0.00')
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/earnings?userId=' + userInfo.id, silent: true })
|
||||
if (!res || !res.success || !res.data) {
|
||||
this.setData({ earningsLoading: false, earnings: '0.00', pendingEarnings: '0.00' })
|
||||
return
|
||||
}
|
||||
const d = res.data
|
||||
this.setData({
|
||||
earnings: formatMoney(d.totalCommission),
|
||||
pendingEarnings: formatMoney(d.availableEarnings),
|
||||
referralCount: d.referralCount ?? this.data.referralCount,
|
||||
earningsLoading: false,
|
||||
earningsRefreshing: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[My] 拉取我的收益失败:', e && e.message)
|
||||
this.setData({
|
||||
earningsLoading: false,
|
||||
earningsRefreshing: false,
|
||||
earnings: '0.00',
|
||||
pendingEarnings: '0.00'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 点击刷新图标:刷新我的收益
|
||||
async refreshEarnings() {
|
||||
if (!this.data.isLoggedIn) return
|
||||
if (this.data.earningsRefreshing) return
|
||||
this.setData({ earningsRefreshing: true })
|
||||
wx.showToast({ title: '刷新中...', icon: 'loading', duration: 2000 })
|
||||
await this.loadMyEarnings()
|
||||
wx.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,真正获取微信头像)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 先上传图片到服务器
|
||||
console.log('[My] 开始上传头像:', tempAvatarUrl)
|
||||
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: {
|
||||
folder: 'avatars'
|
||||
},
|
||||
success: (res) => {
|
||||
try {
|
||||
const data = JSON.parse(res.data)
|
||||
if (data.success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.error || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 4. 同步到服务器数据库
|
||||
await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[My] 上传头像失败:', e)
|
||||
wx.showToast({
|
||||
title: e.message || '上传失败,请重试',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 微信原生获取昵称回调(针对 input type="nickname" 的 bindblur 或 bindchange)
|
||||
async handleNicknameChange(nickname) {
|
||||
if (!nickname || nickname === this.data.userInfo?.nickname) return
|
||||
|
||||
try {
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.nickname = nickname
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 同步到服务器
|
||||
await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, nickname }
|
||||
})
|
||||
|
||||
wx.showToast({ title: '昵称已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
console.error('[My] 同步昵称失败:', e)
|
||||
}
|
||||
},
|
||||
|
||||
// 打开昵称修改弹窗
|
||||
editNickname() {
|
||||
this.setData({
|
||||
showNicknameModal: true,
|
||||
editingNickname: this.data.userInfo?.nickname || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭昵称弹窗
|
||||
closeNicknameModal() {
|
||||
this.setData({
|
||||
showNicknameModal: false,
|
||||
editingNickname: ''
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
// 昵称输入实时更新
|
||||
onNicknameInput(e) {
|
||||
this.setData({
|
||||
editingNickname: e.detail.value
|
||||
})
|
||||
},
|
||||
|
||||
// 昵称变化(微信自动填充时触发)
|
||||
onNicknameChange(e) {
|
||||
const nickname = e.detail.value
|
||||
console.log('[My] 昵称已自动填充:', nickname)
|
||||
this.setData({
|
||||
editingNickname: nickname
|
||||
})
|
||||
// 自动填充时也尝试直接同步
|
||||
this.handleNicknameChange(nickname)
|
||||
},
|
||||
|
||||
// 确认修改昵称
|
||||
async confirmNickname() {
|
||||
const newNickname = this.data.editingNickname.trim()
|
||||
|
||||
if (!newNickname) {
|
||||
wx.showToast({ title: '昵称不能为空', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
if (newNickname.length < 1 || newNickname.length > 20) {
|
||||
wx.showToast({ title: '昵称1-20个字符', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 关闭弹窗
|
||||
this.closeNicknameModal()
|
||||
|
||||
// 显示加载
|
||||
wx.showLoading({ title: '更新中...', mask: true })
|
||||
|
||||
try {
|
||||
// 1. 同步到服务器
|
||||
const res = await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: this.data.userInfo.id,
|
||||
nickname: newNickname
|
||||
}
|
||||
})
|
||||
|
||||
if (res && res.success) {
|
||||
// 2. 更新本地状态
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.nickname = newNickname
|
||||
this.setData({ userInfo })
|
||||
|
||||
// 3. 更新全局和缓存
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '昵称已修改', icon: 'success' })
|
||||
} else {
|
||||
throw new Error(res?.message || '更新失败')
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[My] 修改昵称失败:', e)
|
||||
wx.showToast({ title: '修改失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 复制用户ID
|
||||
copyUserId() {
|
||||
const userId = this.data.userInfo?.id || ''
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '暂无ID', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.setClipboardData({
|
||||
data: userId,
|
||||
success: () => {
|
||||
wx.showToast({ title: 'ID已复制', icon: 'success' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换Tab
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
this.setData({ activeTab: tab })
|
||||
},
|
||||
|
||||
// 显示登录弹窗(每次打开时协议未勾选,符合审核要求)
|
||||
showLogin() {
|
||||
try {
|
||||
this.setData({ showLoginModal: true, agreeProtocol: false })
|
||||
} catch (e) {
|
||||
console.error('[My] showLogin error:', e)
|
||||
this.setData({ showLoginModal: true })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换协议勾选(用户主动勾选,非默认同意)
|
||||
toggleAgree() {
|
||||
this.setData({ agreeProtocol: !this.data.agreeProtocol })
|
||||
},
|
||||
|
||||
// 打开用户协议页(审核要求:点击《用户协议》需有响应)
|
||||
openUserProtocol() {
|
||||
wx.navigateTo({ url: '/pages/agreement/agreement' })
|
||||
},
|
||||
|
||||
// 打开隐私政策页(审核要求:点击《隐私政策》需有响应)
|
||||
openPrivacy() {
|
||||
wx.navigateTo({ url: '/pages/privacy/privacy' })
|
||||
},
|
||||
|
||||
// 关闭登录弹窗
|
||||
closeLoginModal() {
|
||||
if (this.data.isLoggingIn) return
|
||||
this.setData({ showLoginModal: false })
|
||||
},
|
||||
|
||||
// 微信登录(须已勾选同意协议,且做好错误处理避免审核报错)
|
||||
async handleWechatLogin() {
|
||||
if (!this.data.agreeProtocol) {
|
||||
wx.showToast({ title: '请先阅读并同意用户协议和隐私政策', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ isLoggingIn: true })
|
||||
try {
|
||||
const result = await app.login()
|
||||
if (result) {
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false, agreeProtocol: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[My] 微信登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 手机号登录(需要用户授权)
|
||||
async handlePhoneLogin(e) {
|
||||
// 检查是否有授权code
|
||||
if (!e.detail.code) {
|
||||
// 用户拒绝授权或获取失败,尝试使用微信登录
|
||||
console.log('手机号授权失败,尝试微信登录')
|
||||
return this.handleWechatLogin()
|
||||
}
|
||||
|
||||
this.setData({ isLoggingIn: true })
|
||||
|
||||
try {
|
||||
const result = await app.loginWithPhone(e.detail.code)
|
||||
if (result) {
|
||||
this.initUserStatus()
|
||||
this.setData({ showLoginModal: false })
|
||||
wx.showToast({ title: '登录成功', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('手机号登录错误:', e)
|
||||
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isLoggingIn: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 点击菜单
|
||||
handleMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
if (!this.data.isLoggedIn && id !== 'about') {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
|
||||
const routes = {
|
||||
orders: '/pages/purchases/purchases',
|
||||
referral: '/pages/referral/referral',
|
||||
withdrawRecords: '/pages/withdraw-records/withdraw-records',
|
||||
about: '/pages/about/about',
|
||||
settings: '/pages/settings/settings'
|
||||
}
|
||||
|
||||
if (routes[id]) {
|
||||
wx.navigateTo({ url: routes[id] })
|
||||
}
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid || app.getSectionMid(id)
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 跳转到关于页
|
||||
goToAbout() {
|
||||
wx.navigateTo({ url: '/pages/about/about' })
|
||||
},
|
||||
|
||||
// 跳转到匹配
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 跳转到推广中心(需登录)
|
||||
goToReferral() {
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 跳转到找伙伴
|
||||
goToMatch() {
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 退出登录
|
||||
handleLogout() {
|
||||
wx.showModal({
|
||||
title: '退出登录',
|
||||
content: '确定要退出登录吗?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
app.logout()
|
||||
this.initUserStatus()
|
||||
wx.showToast({ title: '已退出登录', icon: 'success' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// VIP状态查询(hasFullBook 优先,兼容模拟支付等本地已置 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) {
|
||||
this.setData({
|
||||
isVip: res.data?.isVip || app.globalData.hasFullBook,
|
||||
vipExpireDate: res.data?.expireDate || this.data.vipExpireDate || ''
|
||||
})
|
||||
}
|
||||
} catch (e) { console.log('[My] VIP查询失败', e) }
|
||||
},
|
||||
|
||||
// 头像点击:已登录弹出选项(微信头像 / 相册)
|
||||
onAvatarTap() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.showActionSheet({
|
||||
itemList: ['获取微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) this.setData({ showAvatarModal: true })
|
||||
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
|
||||
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)
|
||||
data.success ? resolve(data) : reject(new Error(data.error || '上传失败'))
|
||||
} catch (e) { reject(new Error('解析失败')) }
|
||||
},
|
||||
fail: (e) => reject(e)
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
const userInfo = this.data.userInfo
|
||||
userInfo.avatar = avatarUrl
|
||||
this.setData({ userInfo })
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
await app.request('/api/miniprogram/user/update', { method: 'POST', data: { userId: userInfo.id, avatar: avatarUrl } })
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
// 进入个人资料编辑页(stitch_soul)
|
||||
goToProfileEdit() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
async handleWithdraw() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const amount = parseFloat(this.data.pendingEarnings)
|
||||
if (isNaN(amount) || amount <= 0) {
|
||||
wx.showToast({ title: '暂无可提现金额', icon: 'none' })
|
||||
return
|
||||
}
|
||||
await this.ensureContactInfo(() => this.doWithdraw(amount))
|
||||
},
|
||||
|
||||
async doWithdraw(amount) {
|
||||
wx.showModal({
|
||||
title: '申请提现',
|
||||
content: `确认提现 ¥${amount.toFixed(2)} ?`,
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
await app.request({ url: '/api/miniprogram/withdraw', method: 'POST', data: { userId, amount } })
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '提现申请已提交', icon: 'success' })
|
||||
this.loadMyEarnings()
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提现/找伙伴前检查手机或微信号,未填则弹窗(stitch_soul)
|
||||
async ensureContactInfo(callback) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const phone = (res?.data?.phone || '').trim()
|
||||
const wechat = (res?.data?.wechatId || '').trim()
|
||||
if (phone || wechat) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
contactWechat: wechat || '',
|
||||
pendingWithdraw: true,
|
||||
})
|
||||
this._contactCallback = callback
|
||||
} catch (e) {
|
||||
callback()
|
||||
}
|
||||
},
|
||||
|
||||
closeContactModal() {
|
||||
this.setData({ showContactModal: false, pendingWithdraw: false })
|
||||
this._contactCallback = null
|
||||
},
|
||||
|
||||
onContactPhoneInput(e) { this.setData({ contactPhone: e.detail.value }) },
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
try {
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: app.globalData.userInfo?.id,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechat || undefined,
|
||||
},
|
||||
})
|
||||
if (phone) wx.setStorageSync('user_phone', phone)
|
||||
if (wechat) wx.setStorageSync('user_wechat', wechat)
|
||||
this.closeContactModal()
|
||||
wx.showToast({ title: '已保存', icon: 'success' })
|
||||
const cb = this._contactCallback
|
||||
this._contactCallback = null
|
||||
if (cb) cb()
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ contactSaving: false })
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 我的',
|
||||
path: ref ? `/pages/my/my?ref=${ref}` : '/pages/my/my'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 我的', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/my/my.json
Normal file
6
miniprogram/pages/my/my.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000"
|
||||
}
|
||||
246
miniprogram/pages/my/my.wxml
Normal file
246
miniprogram/pages/my/my.wxml
Normal file
@@ -0,0 +1,246 @@
|
||||
<!-- 我的页 - 设计稿 1:1 还原 -->
|
||||
<view class="page">
|
||||
<!-- 顶部导航:左侧资料编辑 + 标题 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-settings" bindtap="goToProfileEdit">
|
||||
<image class="nav-settings-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="nav-title">我的</text>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 未登录:引导登录 -->
|
||||
<view class="guest-block" wx:if="{{!isLoggedIn}}">
|
||||
<view class="guest-avatar">
|
||||
<image wx:if="{{guestAvatar}}" class="guest-avatar-img" src="{{guestAvatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="guest-avatar-text">{{guestNickname[0] || '游'}}</text>
|
||||
</view>
|
||||
<text class="guest-name">{{guestNickname}}</text>
|
||||
<view class="guest-login-btn" bindtap="showLogin">点击登录</view>
|
||||
</view>
|
||||
|
||||
<!-- 已登录:用户卡片(设计稿布局) -->
|
||||
<view class="profile-card" wx:else>
|
||||
<view class="profile-card-inner">
|
||||
<view class="profile-top-row">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
|
||||
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
|
||||
<view class="vip-badge vip-badge-gray" wx:else>VIP</view>
|
||||
</view>
|
||||
<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>
|
||||
<view class="vip-tags">
|
||||
<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>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
<view class="profile-stat" bindtap="goToChapters">
|
||||
<text class="profile-stat-val">{{readCount}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{earnings === '-' ? '--' : earnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已登录:内容区 -->
|
||||
<view class="main-content" wx:if="{{isLoggedIn}}">
|
||||
<!-- 一键收款(仅在有待确认收款时显示) -->
|
||||
<view class="card receive-card" wx:if="{{pendingConfirmList.length > 0}}">
|
||||
<view class="receive-top">
|
||||
<view class="receive-left">
|
||||
<view class="receive-title-row">
|
||||
<image class="card-icon-img" src="/assets/icons/wallet.svg" mode="aspectFit"/>
|
||||
<text class="card-title">一键收款</text>
|
||||
</view>
|
||||
<text class="receive-sub">
|
||||
待收款 {{pendingConfirmList.length}} 笔 · 合计 ¥{{pendingConfirmAmount}}
|
||||
</text>
|
||||
</view>
|
||||
<view class="receive-btn {{receivingAll ? 'receive-btn-disabled' : ''}}" bindtap="handleOneClickReceive">
|
||||
<text class="receive-btn-text">{{receivingAll ? '收款中...' : '立即收款'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="receive-bottom">
|
||||
<text class="receive-tip">将依次调起微信收款页完成领取</text>
|
||||
<text class="receive-link" bindtap="handleMenuTap" data-id="withdrawRecords">查看提现记录 ›</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">阅读统计</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/book-open-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{readCount}}</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/clock-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{totalReadTime}}</text>
|
||||
<text class="stat-label">阅读分钟</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToMatch">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{matchHistory}}</text>
|
||||
<text class="stat-label">匹配伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/book-arrow-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">最近阅读</text>
|
||||
</view>
|
||||
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{recentChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="recent-link">继续阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="recent-empty" wx:else>
|
||||
<text class="recent-empty-text">暂无阅读记录</text>
|
||||
<view class="recent-empty-btn" bindtap="goToChapters">去阅读 →</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 关于作者 + 设置 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<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>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="about">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-blue"><image class="menu-icon-img" src="/assets/icons/info-blue.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">关于作者</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">设置</text>
|
||||
</view>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal">✕</view>
|
||||
<view class="login-icon">🔐</view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{isLoggingIn || !agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>{{isLoggingIn ? '登录中...' : '微信快捷登录'}}</text>
|
||||
</button>
|
||||
<view class="login-modal-cancel" bindtap="closeLoginModal">取消</view>
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 手机/微信号弹窗 -->
|
||||
<view class="modal-overlay contact-modal-overlay" wx:if="{{showContactModal}}" bindtap="closeContactModal">
|
||||
<view class="contact-modal" catchtap="stopPropagation">
|
||||
<text class="contact-modal-title">请完善联系方式</text>
|
||||
<view class="contact-modal-hint">需完善手机号或微信号才能使用提现和找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">📱</text>
|
||||
<input class="form-input" type="tel" placeholder="请输入您的手机号" value="{{contactPhone}}" bindinput="onContactPhoneInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-inner">
|
||||
<text class="form-icon">💬</text>
|
||||
<input class="form-input" type="text" placeholder="请输入您的微信号" value="{{contactWechat}}" bindinput="onContactWechatInput"/>
|
||||
</view>
|
||||
</view>
|
||||
<view class="contact-modal-btn" bindtap="saveContactInfo" disabled="{{contactSaving}}">{{contactSaving ? '保存中...' : '保存'}}</view>
|
||||
<text class="contact-modal-cancel" bindtap="closeContactModal">取消</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:必须点击 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 class="modal-overlay" wx:if="{{showNicknameModal}}" bindtap="closeNicknameModal">
|
||||
<view class="modal-content nickname-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeNicknameModal">✕</view>
|
||||
<view class="modal-header">
|
||||
<text class="modal-icon">✏️</text>
|
||||
<text class="modal-title">修改昵称</text>
|
||||
</view>
|
||||
<view class="nickname-input-wrap">
|
||||
<view class="nickname-input-inner">
|
||||
<input class="nickname-input" type="nickname" value="{{editingNickname}}" placeholder="点击输入昵称" placeholder-class="nickname-placeholder" bindchange="onNicknameChange" bindinput="onNicknameInput" maxlength="20"/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称</text>
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<view class="modal-btn modal-btn-cancel" bindtap="closeNicknameModal">取消</view>
|
||||
<view class="modal-btn modal-btn-confirm" bindtap="confirmNickname">确定</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-space"></view>
|
||||
</view>
|
||||
251
miniprogram/pages/my/my.wxss
Normal file
251
miniprogram/pages/my/my.wxss
Normal file
@@ -0,0 +1,251 @@
|
||||
/**
|
||||
* 我的页 - professional_profile_with_earnings_vip 1:1 还原
|
||||
* 设计稿:primary #4FD1C5, vip-gold #C8A146, card-dark #1A1A1A, card-inner #252525
|
||||
*/
|
||||
|
||||
/* 真机适配:底部留足 TabBar + 安全区,避免「我的订单」被遮挡 */
|
||||
.page {
|
||||
background: #121212;
|
||||
padding-bottom: calc(220rpx + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
|
||||
/* ===== 导航栏(左侧设置 + 标题居中,右侧避让胶囊) ===== */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
background: rgba(18,18,18,0.9); backdrop-filter: blur(8rpx);
|
||||
display: flex; align-items: center;
|
||||
min-height: 44px;
|
||||
padding: 0 120rpx 0 32rpx; /* 右侧避让胶囊 */
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.nav-settings { width: 64rpx; height: 64rpx; flex-shrink: 0; display: flex; align-items: center; justify-content: center; margin-right: 16rpx; }
|
||||
.nav-title { font-size: 40rpx; font-weight: bold; color: #4FD1C5; flex: 1; text-align: center; }
|
||||
.nav-settings-icon { width: 44rpx; height: 44rpx; opacity: 0.7; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
|
||||
/* ===== 未登录 ===== */
|
||||
.guest-block {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
padding: 64rpx 16rpx;
|
||||
}
|
||||
.guest-avatar { width: 144rpx; height: 144rpx; border-radius: 50%; background: #1A1A1A; border: 4rpx solid #374151; overflow: hidden; margin-bottom: 24rpx; }
|
||||
.guest-avatar-img { width: 100%; height: 100%; display: block; }
|
||||
.guest-avatar-text { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 56rpx; font-weight: bold; color: #6B7280; }
|
||||
.guest-name { font-size: 36rpx; font-weight: bold; color: #E5E7EB; margin-bottom: 24rpx; }
|
||||
.guest-login-btn { padding: 20rpx 48rpx; background: #4FD1C5; color: #000; font-size: 28rpx; font-weight: 600; border-radius: 24rpx; }
|
||||
|
||||
/* ===== 用户卡片(设计稿 1:1) ===== */
|
||||
.profile-card { padding: 30rpx; }
|
||||
.profile-card-inner {
|
||||
background: #1A1A1A; border-radius: 24rpx; padding: 32rpx;
|
||||
border: 1rpx solid rgba(75,85,99,0.5);
|
||||
}
|
||||
.profile-top-row { display: flex; align-items: flex-start; gap: 32rpx; }
|
||||
.avatar-wrap { position: relative; flex-shrink: 0; }
|
||||
.avatar-inner {
|
||||
width: 130rpx; height: 130rpx; border-radius: 50%; overflow: hidden;
|
||||
background: #1C2524; border: 5rpx solid #374151;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.avatar-vip { border-color: #C8A146; box-shadow: 0 0 24rpx rgba(200,161,70,0.4); }
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.avatar-text { font-size: 48rpx; font-weight: bold; color: #6B7280; }
|
||||
.vip-badge {
|
||||
position: absolute; bottom: -4rpx; right: -4rpx;
|
||||
background: linear-gradient(135deg, #E6B84D, #D4A017); color: #000;
|
||||
font-size: 18rpx; font-weight: 900; font-style: italic;
|
||||
padding: 4rpx 12rpx; border-radius: 8rpx;
|
||||
}
|
||||
.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; }
|
||||
.user-name {
|
||||
font-size: 44rpx; font-weight: bold; color: #fff;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
|
||||
}
|
||||
.become-member-btn {
|
||||
padding: 12rpx 28rpx; border: 2rpx solid #C8A146; color: #C8A146;
|
||||
font-size: 24rpx; font-weight: 500; border-radius: 40rpx; white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.become-member-vip { border-color: rgba(200,161,70,0.5); color: rgba(200,161,70,0.8); }
|
||||
.vip-tags { display: flex; gap: 12rpx; flex-shrink: 0; }
|
||||
.vip-tag {
|
||||
font-size: 20rpx; padding: 6rpx 12rpx; border-radius: 8rpx;
|
||||
border: 1rpx solid #374151; background: rgba(255,255,255,0.05); color: #9CA3AF;
|
||||
}
|
||||
.vip-tag-active { border-color: #C8A146; background: rgba(200,161,70,0.1); color: #C8A146; }
|
||||
.user-wechat { font-size: 26rpx; color: #6B7280; }
|
||||
.profile-stats-row {
|
||||
display: flex; justify-content: space-around; margin-top: 32rpx;
|
||||
padding-top: 24rpx; border-top: 1rpx solid #374151;
|
||||
}
|
||||
.profile-stat { text-align: center; }
|
||||
.profile-stat-val { display: block; font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.profile-stat-label { display: block; font-size: 22rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.main-content { padding: 0 0 0 0; }
|
||||
|
||||
/* 卡片通用 */
|
||||
.card {
|
||||
background: #1A1A1A; border-radius: 24rpx; padding: 32rpx;
|
||||
margin-bottom: 24rpx; border: 1rpx solid rgba(75,85,99,0.5);
|
||||
margin:0prx 20rpx!important;
|
||||
}
|
||||
.card-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 32rpx; }
|
||||
.card-icon { font-size: 40rpx; }
|
||||
.card-icon-img { width: 40rpx; height: 40rpx; flex-shrink: 0; }
|
||||
.card-title { font-size: 32rpx; font-weight: bold; color: #fff; }
|
||||
|
||||
/* ===== 一键收款卡片 ===== */
|
||||
.receive-card { padding: 28rpx 32rpx; }
|
||||
.receive-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.receive-left { flex: 1; min-width: 0; }
|
||||
.receive-title-row { display: flex; align-items: center; gap: 16rpx; margin-bottom: 12rpx; }
|
||||
.receive-sub { display: block; font-size: 24rpx; color: rgba(255,255,255,0.65); }
|
||||
.receive-btn {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: linear-gradient(135deg, #4FD1C5 0%, #20B2AA 100%);
|
||||
}
|
||||
.receive-btn-text { font-size: 26rpx; font-weight: 700; color: #000; }
|
||||
.receive-btn-disabled { opacity: 0.55; }
|
||||
.receive-bottom { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; margin-top: 18rpx; }
|
||||
.receive-tip { font-size: 22rpx; color: rgba(255,255,255,0.45); }
|
||||
.receive-link { font-size: 24rpx; color: #4FD1C5; }
|
||||
|
||||
/* 分享收益 */
|
||||
.earnings-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr 1fr;
|
||||
text-align: center;
|
||||
}
|
||||
.earnings-col { padding: 16rpx 0; border-left: 1rpx solid #374151; }
|
||||
.earnings-col:first-child { border-left: none; }
|
||||
.earnings-val { display: block; font-size: 40rpx; font-weight: bold; }
|
||||
.earnings-val.primary { color: #4FD1C5; }
|
||||
.earnings-label { display: block; font-size: 24rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
|
||||
/* 阅读统计 - 统一高度避免真机错位 */
|
||||
.stats-grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 24rpx;
|
||||
align-items: stretch;
|
||||
}
|
||||
.stat-box {
|
||||
background: #252525; border-radius: 20rpx; padding: 24rpx;
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 140rpx;
|
||||
}
|
||||
.stat-icon { font-size: 40rpx; margin-bottom: 8rpx; color: #4FD1C5; flex-shrink: 0; }
|
||||
.stat-icon-img { width: 44rpx; height: 44rpx; margin-bottom: 8rpx; flex-shrink: 0; display: block; }
|
||||
.stat-num { font-size: 36rpx; font-weight: bold; color: #fff; line-height: 1.2; }
|
||||
.stat-label { font-size: 20rpx; color: #6B7280; margin-top: 4rpx; line-height: 1.2; }
|
||||
|
||||
/* 最近阅读 */
|
||||
.recent-list { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.recent-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; }
|
||||
.recent-index { font-size: 28rpx; color: #6B7280; font-family: monospace; }
|
||||
.recent-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.recent-link { font-size: 24rpx; color: #4FD1C5; font-weight: 500; flex-shrink: 0; }
|
||||
.recent-empty { padding: 48rpx; text-align: center; }
|
||||
.recent-empty-text { font-size: 28rpx; color: #6B7280; display: block; margin-bottom: 24rpx; }
|
||||
.recent-empty-btn { font-size: 28rpx; color: #4FD1C5; }
|
||||
|
||||
/* 菜单 */
|
||||
.menu-card { padding: 0; margin-bottom: 48rpx; overflow: hidden; }
|
||||
.menu-item {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 32rpx 40rpx; border-bottom: 1rpx solid #374151;
|
||||
}
|
||||
.menu-item:last-child { border-bottom: none; }
|
||||
.menu-left { display: flex; align-items: center; gap: 24rpx; }
|
||||
.menu-icon-wrap {
|
||||
width: 48rpx; height: 48rpx; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.menu-icon-wrap .menu-icon { font-size: 32rpx; }
|
||||
.icon-teal { background: rgba(79,209,197,0.2); }
|
||||
.icon-teal .menu-icon,
|
||||
.icon-teal .menu-icon-img { color: #4FD1C5; }
|
||||
.icon-teal .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-blue { background: rgba(59,130,246,0.2); }
|
||||
.icon-blue .menu-icon,
|
||||
.icon-blue .menu-icon-img { color: #3B82F6; }
|
||||
.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; }
|
||||
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
|
||||
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }
|
||||
|
||||
/* ===== 弹窗(保留) ===== */
|
||||
.modal-overlay {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(20rpx);
|
||||
display: flex; align-items: center; justify-content: center; z-index: 1000; padding: 48rpx;
|
||||
}
|
||||
.modal-content {
|
||||
width: 100%; max-width: 640rpx; background: #1c1c1e;
|
||||
border-radius: 32rpx; padding: 48rpx; position: relative;
|
||||
}
|
||||
.modal-close { position: absolute; top: 24rpx; right: 24rpx; width: 64rpx; height: 64rpx; border-radius: 50%; background: rgba(255,255,255,0.1); display: flex; align-items: center; justify-content: center; font-size: 28rpx; color: rgba(255,255,255,0.6); }
|
||||
.login-icon { font-size: 96rpx; text-align: center; display: block; margin-bottom: 24rpx; }
|
||||
.login-title { font-size: 36rpx; font-weight: 700; color: #fff; text-align: center; display: block; margin-bottom: 16rpx; }
|
||||
.login-desc { font-size: 26rpx; color: rgba(255,255,255,0.6); text-align: center; display: block; margin-bottom: 48rpx; }
|
||||
.btn-wechat { width: 100%; display: flex; align-items: center; justify-content: center; gap: 16rpx; padding: 28rpx; background: #07C160; color: #fff; font-size: 28rpx; font-weight: 500; border-radius: 24rpx; margin-bottom: 24rpx; border: none; }
|
||||
.btn-wechat::after { border: none; }
|
||||
.btn-wechat-icon { width: 40rpx; height: 40rpx; background: rgba(255,255,255,0.2); border-radius: 8rpx; display: flex; align-items: center; justify-content: center; font-size: 24rpx; }
|
||||
.login-modal-cancel { margin-top: 24rpx; padding: 24rpx; font-size: 28rpx; color: rgba(255,255,255,0.5); text-align: center; }
|
||||
.login-agree-row { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; margin-top: 32rpx; font-size: 22rpx; }
|
||||
.agree-checkbox { width: 32rpx; height: 32rpx; border: 2rpx solid rgba(255,255,255,0.5); border-radius: 6rpx; margin-right: 12rpx; display: flex; align-items: center; justify-content: center; font-size: 22rpx; color: #fff; flex-shrink: 0; }
|
||||
.agree-checked { background: #4FD1C5; border-color: #4FD1C5; }
|
||||
.agree-text { color: rgba(255,255,255,0.6); }
|
||||
.agree-link { color: #4FD1C5; text-decoration: underline; padding: 0 4rpx; }
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
|
||||
/* 头像弹窗 */
|
||||
.avatar-modal .avatar-modal-title { display: block; font-size: 36rpx; font-weight: bold; color: #fff; text-align: center; margin-bottom: 16rpx; }
|
||||
.avatar-modal .avatar-modal-desc { display: block; font-size: 26rpx; color: rgba(255,255,255,0.6); text-align: center; margin-bottom: 32rpx; }
|
||||
.avatar-modal .btn-choose-avatar {
|
||||
width: 100%; height: 88rpx; margin: 0 0 24rpx 0; padding: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: #4FD1C5; color: #000; font-size: 30rpx; font-weight: 600;
|
||||
border-radius: 44rpx; border: none;
|
||||
}
|
||||
.avatar-modal .btn-choose-avatar::after { border: none; }
|
||||
.avatar-modal .avatar-modal-cancel { display: block; text-align: center; font-size: 28rpx; color: rgba(255,255,255,0.5); padding: 16rpx; }
|
||||
|
||||
/* 手机/微信号弹窗 */
|
||||
.contact-modal-overlay { background: rgba(0,0,0,0.85); backdrop-filter: blur(8rpx); }
|
||||
.contact-modal { width: 90%; max-width: 600rpx; background: #1A1A1A; border-radius: 48rpx; padding: 48rpx 40rpx; border: 1rpx solid rgba(255,255,255,0.1); }
|
||||
.contact-modal-title { display: block; text-align: center; font-size: 40rpx; font-weight: bold; color: #fff; margin-bottom: 16rpx; }
|
||||
.contact-modal-hint { display: block; font-size: 24rpx; color: #9CA3AF; text-align: center; margin-bottom: 40rpx; }
|
||||
.form-input-wrap { margin-bottom: 32rpx; }
|
||||
.form-label { display: block; font-size: 24rpx; color: #9CA3AF; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
.form-input-inner { display: flex; align-items: center; padding: 24rpx 32rpx; background: #262626; border-radius: 24rpx; }
|
||||
.form-input-inner .form-icon { font-size: 36rpx; margin-right: 16rpx; opacity: 0.7; }
|
||||
.form-input-inner .form-input { flex: 1; font-size: 28rpx; color: #fff; background: transparent; }
|
||||
.contact-modal-btn { width: 100%; height: 96rpx; line-height: 96rpx; text-align: center; background: #4FD1C5; color: #000; font-size: 32rpx; font-weight: bold; border-radius: 24rpx; margin-top: 16rpx; }
|
||||
.contact-modal-btn[disabled] { opacity: 0.6; }
|
||||
.contact-modal-cancel { display: block; text-align: center; font-size: 28rpx; color: #9CA3AF; margin-top: 24rpx; padding: 16rpx; }
|
||||
|
||||
/* 昵称弹窗 */
|
||||
.nickname-modal .modal-header { display: flex; align-items: center; gap: 16rpx; margin-bottom: 32rpx; }
|
||||
.nickname-modal .modal-icon { font-size: 48rpx; }
|
||||
.nickname-modal .modal-title { font-size: 36rpx; font-weight: bold; }
|
||||
.nickname-input-wrap { margin-bottom: 32rpx; }
|
||||
.nickname-input-inner {
|
||||
padding: 24rpx 32rpx; background: #262626; border-radius: 20rpx;
|
||||
}
|
||||
.nickname-input { width: 100%; font-size: 28rpx; color: #fff; background: transparent; box-sizing: border-box; }
|
||||
.nickname-placeholder { color: #9CA3AF; }
|
||||
.input-tip { display: block; font-size: 22rpx; color: #9CA3AF; margin-top: 12rpx; }
|
||||
.modal-actions { display: flex; gap: 24rpx; }
|
||||
.modal-btn { flex: 1; height: 80rpx; line-height: 80rpx; text-align: center; border-radius: 20rpx; font-size: 28rpx; }
|
||||
.modal-btn-cancel { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
.modal-btn-confirm { background: #4FD1C5; color: #000; font-weight: 600; }
|
||||
|
||||
/* 底部留白:配合 page padding-bottom,避免内容被 TabBar 遮挡 */
|
||||
.bottom-space { height: calc(80rpx + env(safe-area-inset-bottom, 0px)); }
|
||||
35
miniprogram/pages/privacy/privacy.js
Normal file
35
miniprogram/pages/privacy/privacy.js
Normal file
@@ -0,0 +1,35 @@
|
||||
/**
|
||||
* Soul创业派对 - 隐私政策
|
||||
* 审核要求:登录前可点击《隐私政策》查看完整内容
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 隐私政策',
|
||||
path: ref ? `/pages/privacy/privacy?ref=${ref}` : '/pages/privacy/privacy'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 隐私政策', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
1
miniprogram/pages/privacy/privacy.json
Normal file
1
miniprogram/pages/privacy/privacy.json
Normal file
@@ -0,0 +1 @@
|
||||
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"隐私政策"}
|
||||
40
miniprogram/pages/privacy/privacy.wxml
Normal file
40
miniprogram/pages/privacy/privacy.wxml
Normal file
@@ -0,0 +1,40 @@
|
||||
<!--隐私政策页 - 审核要求可点击查看-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">←</view>
|
||||
<text class="nav-title">隐私政策</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view class="content" scroll-y enhanced show-scrollbar>
|
||||
<view class="doc-card">
|
||||
<text class="doc-title">Soul创业实验 隐私政策</text>
|
||||
<text class="doc-update">更新日期:以小程序内展示为准</text>
|
||||
|
||||
<text class="doc-section">一、信息收集</text>
|
||||
<text class="doc-p">为向您提供阅读、购买、推广与提现等服务,我们可能收集:微信昵称、头像、openId、手机号(在您授权时)、订单与收益相关数据。我们仅在法律允许及您同意的范围内收集必要信息。</text>
|
||||
|
||||
<text class="doc-section">二、信息使用</text>
|
||||
<text class="doc-p">所收集信息用于账号识别、订单与收益结算、客服与纠纷处理、产品优化及法律义务履行,不会用于与上述目的无关的营销或向第三方出售。</text>
|
||||
|
||||
<text class="doc-section">三、信息存储与安全</text>
|
||||
<text class="doc-p">数据存储在中华人民共和国境内,我们采取合理技术和管理措施保障数据安全,防止未经授权的访问、泄露或篡改。</text>
|
||||
|
||||
<text class="doc-section">四、信息共享</text>
|
||||
<text class="doc-p">未经您同意,我们不会将您的个人信息共享给第三方,法律法规要求或为完成支付、提现等必要合作除外(如微信支付、微信商家转账)。</text>
|
||||
|
||||
<text class="doc-section">五、您的权利</text>
|
||||
<text class="doc-p">您有权查询、更正、删除您的个人信息,或撤回授权。部分权限撤回可能影响相关功能使用。您可通过小程序设置或联系我们就隐私问题提出请求。</text>
|
||||
|
||||
<text class="doc-section">六、未成年人</text>
|
||||
<text class="doc-p">如您为未成年人,请在监护人同意下使用本服务。我们不会主动收集未成年人个人信息。</text>
|
||||
|
||||
<text class="doc-section">七、政策更新</text>
|
||||
<text class="doc-p">我们可能适时更新本政策,更新后将通过小程序内公示等方式通知您。继续使用即视为接受更新后的政策。</text>
|
||||
|
||||
<text class="doc-section">八、联系我们</text>
|
||||
<text class="doc-p">如有隐私相关疑问或投诉,请通过小程序内「关于作者」或 Soul 派对房与我们联系。</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
11
miniprogram/pages/privacy/privacy.wxss
Normal file
11
miniprogram/pages/privacy/privacy.wxss
Normal file
@@ -0,0 +1,11 @@
|
||||
.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.95); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
|
||||
.nav-placeholder { width: 72rpx; }
|
||||
.content { height: calc(100vh - 132rpx); padding: 32rpx; box-sizing: border-box; }
|
||||
.doc-card { background: #1c1c1e; border-radius: 24rpx; padding: 40rpx; border: 2rpx solid rgba(0,206,209,0.2); }
|
||||
.doc-title { font-size: 34rpx; font-weight: 700; color: #fff; display: block; margin-bottom: 16rpx; }
|
||||
.doc-update { font-size: 24rpx; color: rgba(255,255,255,0.5); display: block; margin-bottom: 32rpx; }
|
||||
.doc-section { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin: 24rpx 0 12rpx; }
|
||||
.doc-p { font-size: 26rpx; color: rgba(255,255,255,0.85); line-height: 1.75; display: block; margin-bottom: 16rpx; }
|
||||
222
miniprogram/pages/profile-edit/profile-edit.js
Normal file
222
miniprogram/pages/profile-edit/profile-edit.js
Normal file
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* Soul创业派对 - 资料编辑完整版(comprehensive_profile_editor_v1_1)
|
||||
* 温馨提示、头像、基本信息、核心联系方式、个人故事、互助需求、项目介绍
|
||||
*
|
||||
* 接口约定(/api/miniprogram/user/profile):
|
||||
* - GET ?userId= 返回 data: { avatar, nickname, mbti, region, industry, businessScale, position, skills, phone, wechatId, ... },空值统一为 ""
|
||||
* - POST body:普通用户提交基础字段;VIP 提交全部字段(含 skills、个人故事、互助需求、项目介绍)
|
||||
*
|
||||
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式;VIP 展示全部
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isVip: false,
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
mbti: '',
|
||||
mbtiIndex: 0,
|
||||
region: '',
|
||||
industry: '',
|
||||
businessScale: '',
|
||||
position: '',
|
||||
skills: '',
|
||||
phone: '',
|
||||
wechatId: '',
|
||||
storyBestMonth: '',
|
||||
storyAchievement: '',
|
||||
storyTurning: '',
|
||||
helpOffer: '',
|
||||
helpNeed: '',
|
||||
projectIntro: '',
|
||||
mbtiOptions: MBTI_OPTIONS,
|
||||
showMbtiPicker: false,
|
||||
saving: false,
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
async loadProfile() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo?.id) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => getApp().goBackOrToHome(), 1500)
|
||||
return
|
||||
}
|
||||
try {
|
||||
const userId = userInfo.id
|
||||
const [profileRes, vipRes] = await Promise.all([
|
||||
app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }),
|
||||
app.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }),
|
||||
])
|
||||
this.setData({ isVip: vipRes?.data?.isVip || false })
|
||||
const res = profileRes
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
const v = (k) => (d[k] != null && d[k] !== '') ? String(d[k]) : ''
|
||||
const mbtiIndex = MBTI_OPTIONS.indexOf(v('mbti')) >= 0 ? MBTI_OPTIONS.indexOf(v('mbti')) : 0
|
||||
this.setData({
|
||||
avatar: v('avatar'),
|
||||
nickname: v('nickname'),
|
||||
mbti: v('mbti'),
|
||||
mbtiIndex,
|
||||
region: v('region'),
|
||||
industry: v('industry'),
|
||||
businessScale: v('businessScale'),
|
||||
position: v('position'),
|
||||
skills: v('skills'),
|
||||
phone: v('phone'),
|
||||
wechatId: v('wechatId') || wx.getStorageSync('user_wechat') || '',
|
||||
storyBestMonth: v('storyBestMonth'),
|
||||
storyAchievement: v('storyAchievement'),
|
||||
storyTurning: v('storyTurning'),
|
||||
helpOffer: v('helpOffer'),
|
||||
helpNeed: v('helpNeed'),
|
||||
projectIntro: v('projectIntro'),
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onNicknameInput(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 }) },
|
||||
onPositionInput(e) { this.setData({ position: e.detail.value }) },
|
||||
onSkillsInput(e) { this.setData({ skills: e.detail.value }) },
|
||||
onPhoneInput(e) { this.setData({ phone: e.detail.value }) },
|
||||
onWechatInput(e) { this.setData({ wechatId: e.detail.value }) },
|
||||
onStoryBestMonthInput(e) { this.setData({ storyBestMonth: e.detail.value }) },
|
||||
onStoryAchievementInput(e) { this.setData({ storyAchievement: e.detail.value }) },
|
||||
onStoryTurningInput(e) { this.setData({ storyTurning: e.detail.value }) },
|
||||
onHelpOfferInput(e) { this.setData({ helpOffer: e.detail.value }) },
|
||||
onHelpNeedInput(e) { this.setData({ helpNeed: e.detail.value }) },
|
||||
onProjectIntroInput(e) { this.setData({ projectIntro: e.detail.value }) },
|
||||
|
||||
onMbtiPickerChange(e) {
|
||||
const i = parseInt(e.detail.value, 10)
|
||||
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
|
||||
},
|
||||
|
||||
chooseAvatar() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
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
|
||||
}
|
||||
this.setData({ saving: true })
|
||||
try {
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
const payload = {
|
||||
userId,
|
||||
avatar: s(this.data.avatar),
|
||||
nickname: s(this.data.nickname),
|
||||
mbti: s(this.data.mbti),
|
||||
region: s(this.data.region),
|
||||
industry: s(this.data.industry),
|
||||
businessScale: s(this.data.businessScale),
|
||||
position: s(this.data.position),
|
||||
phone: s(this.data.phone),
|
||||
wechatId: s(this.data.wechatId),
|
||||
}
|
||||
const showHelp = isVip || this.data.helpOffer || this.data.helpNeed
|
||||
if (isVip) {
|
||||
payload.skills = s(this.data.skills)
|
||||
payload.storyBestMonth = s(this.data.storyBestMonth)
|
||||
payload.storyAchievement = s(this.data.storyAchievement)
|
||||
payload.storyTurning = s(this.data.storyTurning)
|
||||
payload.projectIntro = s(this.data.projectIntro)
|
||||
}
|
||||
if (showHelp) {
|
||||
payload.helpOffer = s(this.data.helpOffer)
|
||||
payload.helpNeed = s(this.data.helpNeed)
|
||||
}
|
||||
if (payload.wechatId) wx.setStorageSync('user_wechat', payload.wechatId)
|
||||
if (payload.phone) wx.setStorageSync('user_phone', payload.phone)
|
||||
const hasUpdate = Object.keys(payload).some(k => k !== 'userId' && payload[k] !== '')
|
||||
if (!hasUpdate) {
|
||||
wx.showToast({ title: '无变更', icon: 'none' })
|
||||
this.setData({ saving: false })
|
||||
return
|
||||
}
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
})
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (app.globalData.userInfo) {
|
||||
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
|
||||
if (payload.avatar) app.globalData.userInfo.avatar = payload.avatar
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
setTimeout(() => getApp().goBackOrToHome(), 800)
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
}
|
||||
this.setData({ saving: false })
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/profile-edit/profile-edit.json
Normal file
4
miniprogram/pages/profile-edit/profile-edit.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "编辑资料",
|
||||
"usingComponents": {}
|
||||
}
|
||||
137
miniprogram/pages/profile-edit/profile-edit.wxml
Normal file
137
miniprogram/pages/profile-edit/profile-edit.wxml
Normal file
@@ -0,0 +1,137 @@
|
||||
<!-- 资料编辑 - comprehensive_profile_editor_v1_1 | input/textarea 用 view 包裹,配色 enhanced -->
|
||||
<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="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tip-card">
|
||||
<text class="tip-icon">ℹ</text>
|
||||
<text class="tip-text">温馨提示:需完善手机号和微信号才能使用提现和找伙伴功能</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="chooseAvatar">
|
||||
<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="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>
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
|
||||
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap form-input-suffix">
|
||||
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
|
||||
<text class="form-suffix">📍</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">行业</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/电商" value="{{industry}}" bindinput="onIndustryInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">业务体量</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:年GMV 5000万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">职位</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人/联合创始人" value="{{position}}" bindinput="onPositionInput"/></view>
|
||||
</view>
|
||||
<view class="form-row" wx:if="{{isVip}}">
|
||||
<text class="form-label">我擅长</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频制作、IP打造、私域运营" value="{{skills}}" bindinput="onSkillsInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 核心联系方式 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">📞</text>
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事(仅 VIP 展示) -->
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">💡</text>
|
||||
<text>个人故事</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">你最赚钱的一个月做的是什么</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:2021年主导电商大促,单月GMV突破500W..." value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">最有成就感的一件事</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:帮助3个素人打造个人IP,每月稳定变现5万+" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">人生的转折点</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:辞去大厂工作开始做自媒体..." value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求(VIP 或 资源对接已填写时展示) -->
|
||||
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">🤝</text>
|
||||
<text>互助需求</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我能帮助大家什么</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频脚本、账号冷启动、私域转化" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我需要什么帮助</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:寻找供应链资源、线下活动合作" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍(仅 VIP 展示) -->
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<text class="section-icon">🚀</text>
|
||||
<text>项目介绍</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="详细介绍您的项目,让潜在伙伴更好地了解您..." value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
103
miniprogram/pages/profile-edit/profile-edit.wxss
Normal file
103
miniprogram/pages/profile-edit/profile-edit.wxss
Normal file
@@ -0,0 +1,103 @@
|
||||
/* 资料编辑 - comprehensive_profile_editor_v1_1 | 配色 enhanced,input/textarea 用 view 包裹 */
|
||||
.page {
|
||||
background: #050B14; min-height: 100vh; color: #fff;
|
||||
width: 100%; box-sizing: border-box; overflow-x: hidden;
|
||||
}
|
||||
|
||||
.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; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
|
||||
.scroll-main { width: 100%; height: calc(100vh - 88rpx); padding: 24rpx; box-sizing: border-box; overflow-x: hidden; }
|
||||
|
||||
.tip-card {
|
||||
display: flex; align-items: flex-start; gap: 24rpx;
|
||||
padding: 32rpx;
|
||||
background: rgba(94,234,212,0.08); border: 1rpx solid rgba(94,234,212,0.25);
|
||||
border-radius: 24rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.tip-icon { font-size: 40rpx; color: #5EEAD4; flex-shrink: 0; }
|
||||
.tip-text { font-size: 26rpx; color: rgba(94,234,212,0.95); line-height: 1.6; }
|
||||
|
||||
.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; }
|
||||
|
||||
.section {
|
||||
margin-bottom: 48rpx; padding-top: 32rpx;
|
||||
border-top: 1rpx solid rgba(255,255,255,0.08);
|
||||
}
|
||||
.section-title { display: flex; align-items: center; gap: 16rpx; font-size: 32rpx; font-weight: 600; margin-bottom: 32rpx; }
|
||||
.section-icon { font-size: 40rpx; }
|
||||
|
||||
.form-row { margin-bottom: 32rpx; }
|
||||
.form-row:last-child { margin-bottom: 0; }
|
||||
.form-row-2 { display: flex; gap: 24rpx; }
|
||||
.form-row-2 .form-item { flex: 1; min-width: 0; }
|
||||
.form-label { display: block; font-size: 24rpx; color: #94A3B8; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
|
||||
/* input/textarea 用 view 包裹,padding 写在 view 上 */
|
||||
.form-input-wrap {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 24rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-input-suffix { position: relative; padding-right: 64rpx; }
|
||||
.form-input-suffix .form-suffix {
|
||||
position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
|
||||
font-size: 32rpx; color: #94A3B8;
|
||||
}
|
||||
.form-input-inner {
|
||||
width: 100%; max-width: 100%; font-size: 28rpx; color: #fff; background: transparent;
|
||||
box-sizing: border-box; display: block;
|
||||
}
|
||||
.form-picker { color: #fff; }
|
||||
|
||||
.form-textarea-wrap {
|
||||
padding: 24rpx 32rpx;
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 24rpx; min-height: 160rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-textarea-wrap.form-textarea-lg { min-height: 240rpx; }
|
||||
.form-textarea-inner {
|
||||
width: 100%; max-width: 100%; min-height: 112rpx; font-size: 28rpx; color: #fff;
|
||||
background: transparent; line-height: 1.5; box-sizing: border-box; display: block;
|
||||
}
|
||||
.form-textarea-lg .form-textarea-inner { min-height: 192rpx; }
|
||||
|
||||
.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: 48rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.save-btn[disabled] { opacity: 0.6; }
|
||||
.bottom-space { height: 120rpx; }
|
||||
90
miniprogram/pages/profile-show/profile-show.js
Normal file
90
miniprogram/pages/profile-show/profile-show.js
Normal file
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* Soul创业派对 - 个人资料展示页(stitch_soul enhanced_professional_profile)
|
||||
* 从「我的」页编辑图标进入;展示基本信息、个人故事、互助需求、项目介绍
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
profile: null,
|
||||
loading: true,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.loadProfile()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.profile) this.loadProfile()
|
||||
},
|
||||
|
||||
async loadProfile() {
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (!app.globalData.isLoggedIn || !userInfo?.id) {
|
||||
this.setData({ loading: false })
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
setTimeout(() => getApp().goBackOrToHome(), 1500)
|
||||
return
|
||||
}
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userInfo.id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
const e = (v) => (v == null || v === undefined ? '' : (String(v).trim() === '' || String(v).trim() === '未填写' ? '' : String(v).trim()))
|
||||
const phone = d.phone || ''
|
||||
const wechat = d.wechatId || wx.getStorageSync('user_wechat') || ''
|
||||
this.setData({
|
||||
profile: {
|
||||
...d,
|
||||
industry: e(d.industry),
|
||||
position: e(d.position),
|
||||
businessScale: e(d.businessScale || d.business_scale),
|
||||
skills: e(d.skills),
|
||||
storyBestMonth: e(d.storyBestMonth || d.story_best_month),
|
||||
storyAchievement: e(d.storyAchievement || d.story_achievement),
|
||||
storyTurning: e(d.storyTurning || d.story_turning),
|
||||
helpOffer: e(d.helpOffer || d.help_offer),
|
||||
helpNeed: e(d.helpNeed || d.help_need),
|
||||
projectIntro: e(d.projectIntro || d.project_intro),
|
||||
phoneMask: phone ? phone.slice(0, 3) + '****' + phone.slice(-2) : '',
|
||||
wechatMask: wechat ? (wechat.length > 8 ? wechat.slice(0, 4) + '****' + wechat.slice(-3) : wechat) : '',
|
||||
phone,
|
||||
wechat,
|
||||
},
|
||||
loading: false,
|
||||
})
|
||||
} else {
|
||||
this.setData({ profile: null, loading: false })
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ profile: null, loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
goToEdit() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
copyPhone() {
|
||||
const p = this.data.profile?.phone
|
||||
if (!p) return
|
||||
wx.setClipboardData({ data: p, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
copyWechat() {
|
||||
const w = this.data.profile?.wechat
|
||||
if (!w) return
|
||||
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
|
||||
},
|
||||
|
||||
goToVip() {
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
})
|
||||
4
miniprogram/pages/profile-show/profile-show.json
Normal file
4
miniprogram/pages/profile-show/profile-show.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "个人资料",
|
||||
"usingComponents": {}
|
||||
}
|
||||
135
miniprogram/pages/profile-show/profile-show.wxml
Normal file
135
miniprogram/pages/profile-show/profile-show.wxml
Normal file
@@ -0,0 +1,135 @@
|
||||
<!-- 个人资料展示页 - enhanced_professional_profile 1:1 重构 -->
|
||||
<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-right" bindtap="goToEdit"><text class="nav-more">⋯</text></view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 头像区卡片 -->
|
||||
<view class="hero-card" wx:if="{{profile}}">
|
||||
<view class="hero-gradient"></view>
|
||||
<view class="hero-content">
|
||||
<view class="hero-avatar">
|
||||
<image wx:if="{{profile.avatar}}" class="avatar-img" src="{{profile.avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{profile.nickname ? profile.nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<text class="hero-name">{{profile.nickname || '未设置昵称'}}</text>
|
||||
<view class="hero-tags">
|
||||
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
|
||||
<text class="tag tag-region" wx:if="{{profile.region}}">📍 {{profile.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">👤</text>
|
||||
<text class="section-title">基本信息</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="field" wx:if="{{profile.industry}}">
|
||||
<text class="field-label">行业</text>
|
||||
<text class="field-value">{{profile.industry}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.position}}">
|
||||
<text class="field-label">职位</text>
|
||||
<text class="field-value">{{profile.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.businessScale}}">
|
||||
<text class="field-label">业务体量</text>
|
||||
<text class="field-value">{{profile.businessScale}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.industry || profile.position || profile.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{profile.skills}}">
|
||||
<text class="field-label">我擅长</text>
|
||||
<text class="field-value">{{profile.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.phoneMask || profile.phone}}">
|
||||
<text class="field-label">联系方式</text>
|
||||
<view class="field-value-row" bindtap="copyPhone">
|
||||
<text class="field-value mono">{{profile.phoneMask || profile.phone || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.phone}}">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.wechatMask || profile.wechat}}">
|
||||
<text class="field-label">微信号</text>
|
||||
<view class="field-value-row" bindtap="copyWechat">
|
||||
<text class="field-value mono">{{profile.wechatMask || profile.wechat || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.wechat}}">复制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.phone && !profile.wechat}}">
|
||||
点击右上角 ⋯ 编辑完善资料
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="section" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon section-icon-yellow">💡</text>
|
||||
<text class="section-title">个人故事</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="story-block" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><text class="story-emoji">🏆</text><text class="story-label">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-text">{{profile.storyBestMonth}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyAchievement}}">
|
||||
<view class="story-head"><text class="story-emoji">⭐</text><text class="story-label">最有成就感的一件事</text></view>
|
||||
<text class="story-text">{{profile.storyAchievement}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyTurning}}">
|
||||
<view class="story-head"><text class="story-emoji">🔄</text><text class="story-label">人生的转折点</text></view>
|
||||
<text class="story-text">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="section" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">🤝</text>
|
||||
<text class="section-title">互助需求</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="help-block" wx:if="{{profile.helpOffer}}">
|
||||
<text class="help-tag help-tag-accent">我能帮你</text>
|
||||
<text class="help-text">{{profile.helpOffer}}</text>
|
||||
</view>
|
||||
<view class="help-block" wx:if="{{profile.helpNeed}}">
|
||||
<text class="help-tag help-tag-orange">我需要帮助</text>
|
||||
<text class="help-text">{{profile.helpNeed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="section" wx:if="{{profile.projectIntro}}">
|
||||
<view class="section-head">
|
||||
<text class="section-icon">🚀</text>
|
||||
<text class="section-title">项目介绍</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<text class="project-text">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 - 设计稿为描边橙色 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="vip-btn-outline" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<text class="vip-btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
114
miniprogram/pages/profile-show/profile-show.wxss
Normal file
114
miniprogram/pages/profile-show/profile-show.wxss
Normal file
@@ -0,0 +1,114 @@
|
||||
/* 个人资料展示页 - enhanced_professional_profile 1:1 重构 */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
.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 32rpx;
|
||||
background: rgba(5,11,20,0.9); backdrop-filter: blur(8rpx);
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.nav-back { padding: 16rpx; margin-left: -8rpx; }
|
||||
.back-icon { font-size: 40rpx; color: #5EEAD4; }
|
||||
.nav-title { font-size: 34rpx; font-weight: bold; }
|
||||
.nav-right { padding: 16rpx; }
|
||||
.nav-more { font-size: 48rpx; color: #fff; line-height: 1; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
|
||||
.scroll-main { height: calc(100vh - 120rpx); padding: 0 32rpx 32rpx; }
|
||||
|
||||
/* 头像区卡片 */
|
||||
.hero-card {
|
||||
position: relative; overflow: hidden;
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 64rpx 32rpx; display: flex; flex-direction: column; align-items: center;
|
||||
}
|
||||
.hero-gradient {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(to bottom, rgba(30,58,69,0.3) 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.hero-avatar {
|
||||
width: 176rpx; height: 176rpx; border-radius: 50%;
|
||||
overflow: hidden; border: 2rpx solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.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);
|
||||
}
|
||||
.hero-name { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
|
||||
.hero-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; }
|
||||
.tag { padding: 8rpx 24rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 500; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94,234,212,0.2); }
|
||||
.tag-region { background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
|
||||
|
||||
/* 通用区块 */
|
||||
.section {
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 40rpx; box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.2);
|
||||
}
|
||||
.section-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.section-icon { font-size: 40rpx; }
|
||||
.section-icon-yellow { filter: brightness(1.2); }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; }
|
||||
|
||||
.section-body { }
|
||||
.field { margin-bottom: 48rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field-label { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 16rpx; }
|
||||
.field-value { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.5; }
|
||||
.field-value.mono { font-family: monospace; letter-spacing: 0.02em; }
|
||||
.field-value-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.field-hint { font-size: 24rpx; color: #5EEAD4; }
|
||||
.field-divider { height: 1rpx; background: rgba(255,255,255,0.05); margin: 32rpx 0; }
|
||||
.field-empty { font-size: 26rpx; color: #64748b; }
|
||||
|
||||
/* 个人故事 */
|
||||
.story-block { margin-bottom: 48rpx; }
|
||||
.story-block:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
|
||||
.story-emoji { font-size: 32rpx; }
|
||||
.story-label { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; display: block; }
|
||||
|
||||
/* 互助需求 */
|
||||
.help-block {
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.05);
|
||||
border-radius: 20rpx; padding: 32rpx; margin-bottom: 24rpx;
|
||||
}
|
||||
.help-block:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block; font-size: 22rpx; font-weight: 500;
|
||||
padding: 8rpx 16rpx; border-radius: 8rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.help-tag-accent { background: #112D2A; color: #5EEAD4; }
|
||||
.help-tag-orange { background: #2D1F0D; color: #F59E0B; }
|
||||
.help-text { font-size: 26rpx; color: #fff; line-height: 1.6; display: block; }
|
||||
|
||||
.project-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; }
|
||||
|
||||
.bottom-spacer { height: 180rpx; }
|
||||
|
||||
/* 底部按钮 - 设计稿:透明背景 + 橙色描边 */
|
||||
.bottom-bar {
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
|
||||
padding: 32rpx; padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(5,11,20,0.95); backdrop-filter: blur(8rpx);
|
||||
border-top: 1rpx solid rgba(255,255,255,0.05);
|
||||
}
|
||||
.vip-btn-outline {
|
||||
display: flex; align-items: center; justify-content: center; gap: 16rpx;
|
||||
width: 100%; height: 96rpx;
|
||||
background: transparent; color: #F59E0B;
|
||||
border: 2rpx solid rgba(245,158,11,0.3);
|
||||
border-radius: 999rpx; font-size: 30rpx; font-weight: 500;
|
||||
}
|
||||
.vip-btn-arrow { font-size: 36rpx; }
|
||||
78
miniprogram/pages/purchases/purchases.js
Normal file
78
miniprogram/pages/purchases/purchases.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Soul创业实验 - 订单页
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
orders: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
async loadOrders() {
|
||||
this.setData({ loading: true })
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
|
||||
if (res && res.success && res.data) {
|
||||
const orders = (res.data || []).map(item => ({
|
||||
id: item.id || item.order_sn,
|
||||
sectionId: item.product_id || item.section_id,
|
||||
sectionMid: item.section_mid ?? item.mid ?? 0,
|
||||
title: item.product_name || `章节 ${item.product_id || ''}`,
|
||||
amount: item.amount || 0,
|
||||
status: item.status || 'completed',
|
||||
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--'
|
||||
}))
|
||||
this.setData({ orders })
|
||||
return
|
||||
}
|
||||
}
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const orders = purchasedSections.map((id, index) => ({
|
||||
id: `order_${index}`,
|
||||
sectionId: id,
|
||||
sectionMid: app.getSectionMid(id),
|
||||
title: `章节 ${id}`,
|
||||
amount: 1,
|
||||
status: 'completed',
|
||||
createTime: new Date(Date.now() - index * 86400000).toLocaleDateString()
|
||||
}))
|
||||
this.setData({ orders })
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
this.setData({
|
||||
orders: purchasedSections.map((id, i) => ({
|
||||
id: `order_${i}`, sectionId: id, sectionMid: app.getSectionMid(id), title: `章节 ${id}`, amount: 1, status: 'completed',
|
||||
createTime: new Date(Date.now() - i * 86400000).toLocaleDateString()
|
||||
}))
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
title: 'Soul创业派对 - 购买记录',
|
||||
path: ref ? `/pages/purchases/purchases?ref=${ref}` : '/pages/purchases/purchases'
|
||||
}
|
||||
},
|
||||
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return { title: 'Soul创业派对 - 购买记录', query: ref ? `ref=${ref}` : '' }
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/purchases/purchases.json
Normal file
4
miniprogram/pages/purchases/purchases.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
35
miniprogram/pages/purchases/purchases.wxml
Normal file
35
miniprogram/pages/purchases/purchases.wxml
Normal file
@@ -0,0 +1,35 @@
|
||||
<!--订单页-->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">←</view>
|
||||
<text class="nav-title">我的订单</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<view class="loading" wx:if="{{loading}}">
|
||||
<view class="skeleton"></view>
|
||||
<view class="skeleton"></view>
|
||||
<view class="skeleton"></view>
|
||||
</view>
|
||||
|
||||
<view class="orders-list" wx:elif="{{orders.length > 0}}">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id">
|
||||
<view class="order-info">
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
<text class="order-time">{{item.createTime}}</text>
|
||||
</view>
|
||||
<view class="order-right">
|
||||
<text class="order-amount">¥{{item.amount}}</text>
|
||||
<text class="order-status">已完成</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:else>
|
||||
<text class="empty-icon">📦</text>
|
||||
<text class="empty-text">暂无订单</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
20
miniprogram/pages/purchases/purchases.wxss
Normal file
20
miniprogram/pages/purchases/purchases.wxss
Normal file
@@ -0,0 +1,20 @@
|
||||
.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); backdrop-filter: blur(40rpx); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; background: #1c1c1e; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 32rpx; color: #fff; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; }
|
||||
.nav-placeholder { width: 72rpx; }
|
||||
.content { padding: 32rpx; }
|
||||
.loading { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.skeleton { height: 120rpx; background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%); background-size: 200% 100%; animation: skeleton 1.5s ease-in-out infinite; border-radius: 24rpx; }
|
||||
@keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
.orders-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.order-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #1c1c1e; border-radius: 24rpx; }
|
||||
.order-info { flex: 1; }
|
||||
.order-title { font-size: 28rpx; color: #fff; display: block; margin-bottom: 8rpx; }
|
||||
.order-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.order-right { text-align: right; }
|
||||
.order-amount { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin-bottom: 4rpx; }
|
||||
.order-status { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.empty { display: flex; flex-direction: column; align-items: center; padding: 96rpx; }
|
||||
.empty-icon { font-size: 96rpx; margin-bottom: 24rpx; opacity: 0.5; }
|
||||
.empty-text { font-size: 28rpx; color: rgba(255,255,255,0.4); }
|
||||
1276
miniprogram/pages/read/read.js
Normal file
1276
miniprogram/pages/read/read.js
Normal file
File diff suppressed because it is too large
Load Diff
1055
miniprogram/pages/read/read.js.backup
Normal file
1055
miniprogram/pages/read/read.js.backup
Normal file
File diff suppressed because it is too large
Load Diff
9
miniprogram/pages/read/read.json
Normal file
9
miniprogram/pages/read/read.json
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"icon": "/components/icon/icon"
|
||||
},
|
||||
"enablePullDownRefresh": false,
|
||||
"backgroundTextStyle": "light",
|
||||
"backgroundColor": "#000000",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
299
miniprogram/pages/read/read.wxml
Normal file
299
miniprogram/pages/read/read.wxml
Normal file
@@ -0,0 +1,299 @@
|
||||
<!--pages/read/read.wxml-->
|
||||
<!--Soul创业派对 - 阅读页-->
|
||||
<view class="page">
|
||||
<!-- 阅读进度条 -->
|
||||
<view class="progress-bar-fixed" style="top: {{statusBarHeight}}px;">
|
||||
<view class="progress-fill" style="width: {{readingProgress}}%;"></view>
|
||||
</view>
|
||||
|
||||
<!-- 顶部导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-content">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<text class="back-arrow">←</text>
|
||||
</view>
|
||||
<view class="nav-info">
|
||||
<text class="nav-chapter" wx:if="{{section.title || chapterTitle}}">{{section.title || chapterTitle}}</text>
|
||||
</view>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 阅读内容 -->
|
||||
<view class="read-content">
|
||||
<!-- 章节标题 -->
|
||||
<view class="chapter-header">
|
||||
<view class="chapter-meta">
|
||||
<text class="chapter-id">{{section.id}}</text>
|
||||
<text class="tag tag-free" wx:if="{{section.isFree}}">免费</text>
|
||||
</view>
|
||||
<text class="chapter-title">{{section.title}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<view class="loading-state" wx:if="{{accessState === 'unknown' && loading}}">
|
||||
<view class="skeleton skeleton-1"></view>
|
||||
<view class="skeleton skeleton-2"></view>
|
||||
<view class="skeleton skeleton-3"></view>
|
||||
<view class="skeleton skeleton-4"></view>
|
||||
<view class="skeleton skeleton-5"></view>
|
||||
</view>
|
||||
|
||||
<!-- 完整内容 - 免费或已购买 -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
<view class="chapter-nav">
|
||||
<view class="nav-buttons">
|
||||
<view
|
||||
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
|
||||
bindtap="goToPrev"
|
||||
wx:if="{{prevSection}}"
|
||||
>
|
||||
<text class="btn-label">上一篇</text>
|
||||
<text class="btn-title">{{prevSection.title}}</text>
|
||||
</view>
|
||||
<view class="nav-btn-placeholder" wx:else></view>
|
||||
|
||||
<view
|
||||
class="nav-btn nav-next"
|
||||
bindtap="goToNext"
|
||||
wx:if="{{nextSection}}"
|
||||
>
|
||||
<text class="btn-label">下一篇</text>
|
||||
<view class="btn-row">
|
||||
<text class="btn-title">{{nextSection.title}}</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-btn nav-end" wx:else>
|
||||
<text class="btn-end-text">已是最后一篇 🎉</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享操作区 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<button class="action-btn-inline btn-share-inline" open-type="share">
|
||||
<text class="action-icon-small">📷</text>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</button>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<text class="action-icon-small">🖼️</text>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预览内容 + 付费墙 - 未登录 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'locked_not_login'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读20%,登录后查看完整内容</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">立即登录</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
<view class="chapter-nav chapter-nav-locked">
|
||||
<view class="nav-buttons">
|
||||
<view
|
||||
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
|
||||
bindtap="goToPrev"
|
||||
wx:if="{{prevSection}}"
|
||||
>
|
||||
<text class="btn-label">上一篇</text>
|
||||
<text class="btn-title">章节 {{prevSection.id}}</text>
|
||||
</view>
|
||||
<view class="nav-btn-placeholder" wx:else></view>
|
||||
|
||||
<view
|
||||
class="nav-btn nav-next"
|
||||
bindtap="goToNext"
|
||||
wx:if="{{nextSection}}"
|
||||
>
|
||||
<text class="btn-label">下一篇</text>
|
||||
<view class="btn-row">
|
||||
<text class="btn-title">{{nextSection.title}}</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-btn nav-end" wx:else>
|
||||
<text class="btn-end-text">已是最后一篇 🎉</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 预览内容 + 付费墙 - 已登录未购买 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'locked_not_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">🔒</view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读20%,购买后继续阅读</text>
|
||||
|
||||
<!-- 购买选项 -->
|
||||
<view class="purchase-options">
|
||||
<!-- 购买本章 - 直接调起支付 -->
|
||||
<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>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="paywall-tip">分享给好友一起学习,还能赚取佣金</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
<view class="chapter-nav chapter-nav-locked">
|
||||
<view class="nav-buttons">
|
||||
<view
|
||||
class="nav-btn nav-prev {{!prevSection ? 'nav-disabled' : ''}}"
|
||||
bindtap="goToPrev"
|
||||
wx:if="{{prevSection}}"
|
||||
>
|
||||
<text class="btn-label">上一篇</text>
|
||||
<text class="btn-title">章节 {{prevSection.id}}</text>
|
||||
</view>
|
||||
<view class="nav-btn-placeholder" wx:else></view>
|
||||
|
||||
<view
|
||||
class="nav-btn nav-next"
|
||||
bindtap="goToNext"
|
||||
wx:if="{{nextSection}}"
|
||||
>
|
||||
<text class="btn-label">下一篇</text>
|
||||
<view class="btn-row">
|
||||
<text class="btn-title">{{nextSection.title}}</text>
|
||||
<text class="btn-arrow">→</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-btn nav-end" wx:else>
|
||||
<text class="btn-end-text">已是最后一篇 🎉</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 错误状态 - 网络异常 -->
|
||||
<view class="article preview" wx:if="{{accessState === 'error'}}">
|
||||
<view class="paragraph" wx:for="{{previewParagraphs}}" wx:key="index" wx:if="{{item}}">
|
||||
{{item}}
|
||||
</view>
|
||||
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon">⚠️</view>
|
||||
<text class="paywall-title">网络异常</text>
|
||||
<text class="paywall-desc">无法确认权限,请检查网络后重试</text>
|
||||
|
||||
<view class="login-btn" bindtap="handleRetry">
|
||||
<text class="login-btn-text">重新加载</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content poster-modal" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">生成海报</text>
|
||||
<view class="modal-close" bindtap="closePosterModal">✕</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
|
||||
<view class="poster-actions">
|
||||
<view class="poster-btn btn-save" bindtap="savePoster">
|
||||
<text class="btn-icon">💾</text>
|
||||
<text>保存到相册</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<text class="poster-tip">长按海报可直接分享到微信</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录弹窗 - 须勾选同意协议,《用户协议》《隐私政策》可点击查看 -->
|
||||
<view class="modal-overlay" wx:if="{{showLoginModal}}" bindtap="closeLoginModal">
|
||||
<view class="modal-content login-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeLoginModal">✕</view>
|
||||
<view class="login-icon">🔐</view>
|
||||
<text class="login-title">登录 Soul创业派对</text>
|
||||
<text class="login-desc">登录后可购买章节、解锁更多内容</text>
|
||||
|
||||
<button class="btn-wechat {{agreeProtocol ? '' : 'btn-wechat-disabled'}}" bindtap="handleWechatLogin" disabled="{{!agreeProtocol}}">
|
||||
<text class="btn-wechat-icon">微</text>
|
||||
<text>微信快捷登录</text>
|
||||
</button>
|
||||
|
||||
<view class="login-agree-row" catchtap="toggleAgree">
|
||||
<view class="agree-checkbox {{agreeProtocol ? 'agree-checked' : ''}}">{{agreeProtocol ? '✓' : ''}}</view>
|
||||
<text class="agree-text">我已阅读并同意</text>
|
||||
<text class="agree-link" catchtap="openUserProtocol">《用户协议》</text>
|
||||
<text class="agree-text">和</text>
|
||||
<text class="agree-link" catchtap="openPrivacy">《隐私政策》</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 支付中提示 -->
|
||||
<view class="modal-overlay" wx:if="{{isPaying}}" catchtap="">
|
||||
<view class="loading-box">
|
||||
<view class="loading-spinner"></view>
|
||||
<text class="loading-text">支付处理中...</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮分享按钮 -->
|
||||
<button class="fab-share" open-type="share">
|
||||
<image class="fab-icon" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</button>
|
||||
</view>
|
||||
984
miniprogram/pages/read/read.wxss
Normal file
984
miniprogram/pages/read/read.wxss
Normal file
@@ -0,0 +1,984 @@
|
||||
/**
|
||||
* Soul创业实验 - 阅读页样式
|
||||
* 1:1还原Web版本UI
|
||||
*/
|
||||
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
/* ===== 阅读进度条 ===== */
|
||||
.progress-bar-fixed {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4rpx;
|
||||
background: #1c1c1e;
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
|
||||
transition: width 0.15s ease;
|
||||
}
|
||||
|
||||
/* ===== 导航栏 ===== */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
backdrop-filter: blur(40rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 24rpx;
|
||||
height: 88rpx;
|
||||
}
|
||||
|
||||
.nav-back, .nav-right-placeholder {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-back {
|
||||
border-radius: 50%;
|
||||
background: #1c1c1e;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.nav-right-placeholder {
|
||||
/* 占位保持标题居中 */
|
||||
}
|
||||
|
||||
.back-arrow {
|
||||
font-size: 36rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.nav-info {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.nav-part {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-chapter {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.nav-placeholder {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ===== 阅读内容 ===== */
|
||||
.read-content {
|
||||
max-width: 750rpx;
|
||||
margin: 0 auto;
|
||||
padding: 48rpx 40rpx 200rpx;
|
||||
}
|
||||
|
||||
/* ===== 章节标题 ===== */
|
||||
.chapter-header {
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.chapter-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.chapter-id {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #00CED1;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 32rpx;
|
||||
}
|
||||
|
||||
.tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
padding: 6rpx 16rpx;
|
||||
min-width: 80rpx;
|
||||
border-radius: 8rpx;
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.tag-free {
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.chapter-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ===== 加载状态 ===== */
|
||||
.loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32rpx;
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
height: 32rpx;
|
||||
background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-loading 1.5s ease-in-out infinite;
|
||||
border-radius: 8rpx;
|
||||
}
|
||||
|
||||
.skeleton-1 { width: 75%; }
|
||||
.skeleton-2 { width: 90%; }
|
||||
.skeleton-3 { width: 65%; }
|
||||
.skeleton-4 { width: 85%; }
|
||||
.skeleton-5 { width: 70%; }
|
||||
|
||||
@keyframes skeleton-loading {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* ===== 文章内容 ===== */
|
||||
.article {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 34rpx;
|
||||
line-height: 1.9;
|
||||
}
|
||||
|
||||
.paragraph {
|
||||
margin-bottom: 48rpx;
|
||||
text-align: justify;
|
||||
}
|
||||
|
||||
.preview {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* ===== 渐变遮罩 ===== */
|
||||
.fade-mask {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: -40rpx;
|
||||
right: -40rpx;
|
||||
height: 300rpx;
|
||||
background: linear-gradient(to top, #000000 0%, transparent 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== 付费墙 ===== */
|
||||
.paywall {
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
margin-top: 48rpx;
|
||||
padding: 48rpx;
|
||||
background: linear-gradient(135deg, #1c1c1e 0%, #2c2c2e 100%);
|
||||
border-radius: 32rpx;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.paywall-icon {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
margin: 0 auto 32rpx;
|
||||
background: rgba(0, 206, 209, 0.1);
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 64rpx;
|
||||
}
|
||||
|
||||
.paywall-title {
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.paywall-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
/* ===== 购买选项 ===== */
|
||||
.purchase-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.purchase-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx;
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.purchase-section {
|
||||
background: #2c2c2e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.purchase-fullbook {
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.3);
|
||||
}
|
||||
|
||||
.purchase-section .btn-label {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.purchase-section .btn-price {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.brand-color {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-sparkle {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-label {
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-right {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-price {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.purchase-fullbook .btn-discount {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.paywall-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 章节导航 ===== */
|
||||
.chapter-nav {
|
||||
margin-top: 96rpx;
|
||||
padding-top: 64rpx;
|
||||
border-top: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.nav-buttons {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.nav-btn {
|
||||
flex: 1;
|
||||
padding: 24rpx;
|
||||
border-radius: 24rpx;
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
.nav-btn-placeholder {
|
||||
flex: 1;
|
||||
max-width: 48%;
|
||||
}
|
||||
|
||||
.nav-prev {
|
||||
background: #1c1c1e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.nav-next {
|
||||
background: linear-gradient(90deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.1) 100%);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.nav-end {
|
||||
background: #1c1c1e;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.05);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav-disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.btn-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.nav-next .btn-label {
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.btn-title {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.btn-arrow {
|
||||
font-size: 24rpx;
|
||||
color: #00CED1;
|
||||
flex-shrink: 0;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.btn-end-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 分享操作区 ===== */
|
||||
.action-section {
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
|
||||
.action-row-inline {
|
||||
display: flex;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
.action-btn-inline {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
padding: 24rpx 16rpx;
|
||||
border-radius: 16rpx;
|
||||
border: none;
|
||||
background: transparent;
|
||||
line-height: normal;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.action-btn-inline::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
}
|
||||
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.action-text-small {
|
||||
font-size: 24rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ===== 推广提示区 ===== */
|
||||
.promo-section {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.promo-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 32rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 215, 0, 0.1) 0%, rgba(255, 165, 0, 0.05) 100%);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.2);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.promo-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
}
|
||||
|
||||
.promo-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
|
||||
.promo-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.promo-title {
|
||||
font-size: 30rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.promo-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
|
||||
.promo-right {
|
||||
padding-left: 20rpx;
|
||||
}
|
||||
|
||||
.promo-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
/* ===== 弹窗 ===== */
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
backdrop-filter: blur(20rpx);
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
width: 100%;
|
||||
max-width: 750rpx;
|
||||
background: #1c1c1e;
|
||||
border-radius: 48rpx 48rpx 0 0;
|
||||
padding: 48rpx;
|
||||
padding-bottom: calc(48rpx + env(safe-area-inset-bottom));
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { transform: translateY(100%); }
|
||||
to { transform: translateY(0); }
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 分享弹窗 ===== */
|
||||
.share-link-box {
|
||||
padding: 32rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.link-label {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.link-url {
|
||||
font-size: 26rpx;
|
||||
color: #00CED1;
|
||||
display: block;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.link-tip {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
display: block;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.share-buttons {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.share-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 24rpx;
|
||||
border: none;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.share-btn::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.share-btn-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.icon-copy {
|
||||
background: rgba(0, 206, 209, 0.2);
|
||||
}
|
||||
|
||||
.icon-wechat {
|
||||
background: rgba(7, 193, 96, 0.2);
|
||||
color: #07C160;
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.icon-poster {
|
||||
background: rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
|
||||
.share-btn-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 支付弹窗 ===== */
|
||||
.payment-info {
|
||||
padding: 24rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.payment-type {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.amount-label {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
.amount-value {
|
||||
font-size: 48rpx;
|
||||
font-weight: 700;
|
||||
color: #00CED1;
|
||||
}
|
||||
|
||||
.payment-methods {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
|
||||
.method-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
padding: 24rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 16rpx;
|
||||
border: 2rpx solid transparent;
|
||||
}
|
||||
|
||||
.method-active {
|
||||
border-color: #07C160;
|
||||
}
|
||||
|
||||
.method-icon {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 24rpx;
|
||||
border-radius: 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.method-name {
|
||||
flex: 1;
|
||||
font-size: 28rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.method-check {
|
||||
color: #07C160;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
width: 100%;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #ffffff;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.payment-notice {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 登录提示 ===== */
|
||||
.login-prompt {
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.login-btn {
|
||||
padding: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-btn-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* ===== 登录弹窗 ===== */
|
||||
.login-modal {
|
||||
padding: 48rpx 32rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-icon {
|
||||
font-size: 80rpx;
|
||||
display: block;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
display: block;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
display: block;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
|
||||
.btn-wechat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx;
|
||||
background: #07C160;
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-wechat::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-wechat-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 8rpx;
|
||||
font-size: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.btn-phone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 16rpx;
|
||||
padding: 28rpx;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #ffffff;
|
||||
font-size: 30rpx;
|
||||
border-radius: 24rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.btn-phone::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-phone-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.login-agree-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 32rpx;
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.agree-checkbox {
|
||||
width: 32rpx;
|
||||
height: 32rpx;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.5);
|
||||
border-radius: 6rpx;
|
||||
margin-right: 12rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 22rpx;
|
||||
color: #fff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.agree-checked {
|
||||
background: #00CED1;
|
||||
border-color: #00CED1;
|
||||
}
|
||||
.agree-text { color: rgba(255, 255, 255, 0.6); }
|
||||
.agree-link {
|
||||
color: #00CED1;
|
||||
text-decoration: underline;
|
||||
padding: 0 4rpx;
|
||||
}
|
||||
.btn-wechat-disabled { opacity: 0.6; }
|
||||
|
||||
/* ===== 支付中加载 ===== */
|
||||
.loading-box {
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx 64rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border: 4rpx solid rgba(255, 255, 255, 0.2);
|
||||
border-top-color: #00CED1;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* ===== 海报弹窗 ===== */
|
||||
.poster-modal {
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.poster-preview {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 32rpx 0;
|
||||
padding: 24rpx;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-radius: 24rpx;
|
||||
}
|
||||
|
||||
.poster-canvas {
|
||||
border-radius: 16rpx;
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.poster-actions {
|
||||
display: flex;
|
||||
gap: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.poster-btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
padding: 28rpx;
|
||||
border-radius: 24rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.btn-save {
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
font-size: 32rpx;
|
||||
}
|
||||
|
||||
.poster-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-align: center;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 右下角悬浮分享按钮 ===== */
|
||||
.fab-share {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
width:70rpx!important;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 70rpx;
|
||||
border-radius: 60rpx;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
z-index: 9999;
|
||||
display:flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
padding:16rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
182
miniprogram/pages/referral/earnings-detail-styles.wxss
Normal file
182
miniprogram/pages/referral/earnings-detail-styles.wxss
Normal file
@@ -0,0 +1,182 @@
|
||||
/* ===================================
|
||||
收益明细卡片样式 - 重构版
|
||||
创建时间:2026-02-04
|
||||
说明:修复布局错乱问题,优化文本显示
|
||||
=================================== */
|
||||
|
||||
/* 收益明细卡片容器 */
|
||||
.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;
|
||||
padding: 16rpx 0;
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
收益明细列表项 - 核心样式
|
||||
=================================== */
|
||||
|
||||
/* 列表项容器 - 使用flex布局 */
|
||||
.earnings-detail-card .detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
padding: 24rpx 40rpx;
|
||||
background: transparent;
|
||||
border-bottom: 2rpx solid rgba(255,255,255,0.03);
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-item:active {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
/* 头像容器 - 固定宽度,不收缩 */
|
||||
.earnings-detail-card .detail-avatar-wrap {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(56, 189, 172, 0.2);
|
||||
display: block;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-avatar-text {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #38bdac 0%, #2da396 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
/* 详细信息容器 - 占据剩余空间,允许收缩 */
|
||||
.earnings-detail-card .detail-content {
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许flex子元素收缩到比内容更小 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8rpx;
|
||||
}
|
||||
|
||||
/* 顶部行:昵称 + 金额 */
|
||||
.earnings-detail-card .detail-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 买家昵称 - 允许收缩,显示省略号 */
|
||||
.earnings-detail-card .detail-buyer {
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #ffffff;
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 佣金金额 - 固定宽度,不收缩 */
|
||||
.earnings-detail-card .detail-amount {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #38bdac;
|
||||
flex-shrink: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 商品信息行:书名 + 章节 */
|
||||
.earnings-detail-card .detail-product {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 4rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 书名 - 限制最大宽度,显示省略号 */
|
||||
.earnings-detail-card .detail-book {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 500;
|
||||
max-width: 50%; /* 限制书名最多占一半宽度 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
/* 章节 - 占据剩余空间,显示省略号 */
|
||||
.earnings-detail-card .detail-chapter {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
flex: 1;
|
||||
min-width: 0; /* 关键:允许收缩 */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* 时间信息 */
|
||||
.earnings-detail-card .detail-time {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* ===================================
|
||||
响应式优化
|
||||
=================================== */
|
||||
|
||||
/* 小屏幕优化 */
|
||||
@media (max-width: 375px) {
|
||||
.earnings-detail-card .detail-buyer {
|
||||
font-size: 26rpx;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-amount {
|
||||
font-size: 30rpx;
|
||||
}
|
||||
|
||||
.earnings-detail-card .detail-book {
|
||||
max-width: 45%;
|
||||
}
|
||||
}
|
||||
943
miniprogram/pages/referral/referral.js
Normal file
943
miniprogram/pages/referral/referral.js
Normal file
@@ -0,0 +1,943 @@
|
||||
/**
|
||||
* Soul创业派对 - 分销中心页
|
||||
*
|
||||
* 可见数据:
|
||||
* - 绑定用户数(当前有效绑定)
|
||||
* - 通过链接进的人数(总访问量)
|
||||
* - 带来的付款人数(已转化购买)
|
||||
* - 收益统计(90%归分发者)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
|
||||
// === 核心可见数据 ===
|
||||
bindingCount: 0, // 绑定用户数(当前有效)
|
||||
visitCount: 0, // 通过链接进的人数
|
||||
paidCount: 0, // 带来的付款人数
|
||||
unboughtCount: 0, // 待购买人数(绑定但未付款)
|
||||
expiredCount: 0, // 已过期人数
|
||||
|
||||
// === 收益数据 ===
|
||||
totalCommission: 0, // 累计佣金总额(所有获得的佣金)
|
||||
availableEarnings: 0, // 可提现金额(未申请提现的佣金)- 字符串格式用于显示
|
||||
availableEarningsNum: 0, // 可提现金额 - 数字格式用于判断
|
||||
pendingWithdrawAmount: 0, // 待审核金额(已申请提现但未审核)
|
||||
withdrawnEarnings: 0, // 已提现金额
|
||||
earnings: 0, // 已结算收益(保留兼容)
|
||||
pendingEarnings: 0, // 待结算收益(保留兼容)
|
||||
shareRate: 90, // 分成比例(90%),从 referral/data 或 config 获取
|
||||
minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取
|
||||
bindingDays: 30, // 绑定期天数,从 referral/data 获取
|
||||
userDiscount: 5, // 好友购买优惠%,从 referral/data 获取
|
||||
hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置)
|
||||
|
||||
// === 统计数据 ===
|
||||
referralCount: 0, // 总推荐人数
|
||||
expiringCount: 0, // 即将过期人数
|
||||
|
||||
// 邀请码
|
||||
referralCode: '',
|
||||
|
||||
// 绑定用户列表
|
||||
showBindingList: true,
|
||||
activeTab: 'active',
|
||||
activeBindings: [],
|
||||
convertedBindings: [],
|
||||
expiredBindings: [],
|
||||
currentBindings: [],
|
||||
totalBindings: 0,
|
||||
|
||||
// 收益明细
|
||||
earningsDetails: [],
|
||||
|
||||
// 海报
|
||||
showPosterModal: false,
|
||||
isGeneratingPoster: false,
|
||||
posterQrSrc: '',
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
posterCaseCount: 62,
|
||||
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
this.initData()
|
||||
// 启用分享到朋友圈(需同时有 onShareAppMessage 和 onShareTimeline;menus 在 Android 支持,iOS 为 Beta)
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
},
|
||||
|
||||
onShow() {
|
||||
// 从设置页返回时同步微信号绑定状态,便于提现按钮立即更新
|
||||
const hasWechatId = !!(app.globalData.userInfo?.wechat || app.globalData.userInfo?.wechatId || wx.getStorageSync('user_wechat'))
|
||||
this.setData({ hasWechatId })
|
||||
this.initData()
|
||||
},
|
||||
|
||||
// 初始化数据
|
||||
async initData() {
|
||||
const { isLoggedIn, userInfo } = app.globalData
|
||||
if (isLoggedIn && userInfo) {
|
||||
// 显示加载提示
|
||||
wx.showLoading({
|
||||
title: '加载中...',
|
||||
mask: true // 防止触摸穿透
|
||||
})
|
||||
|
||||
// 生成邀请码
|
||||
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)
|
||||
}
|
||||
|
||||
// 使用真实数据或默认值
|
||||
let activeBindings = realData?.activeUsers || []
|
||||
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
|
||||
const expiredCount = realData?.expiredCount || expiredBindings.length
|
||||
const unboughtCount = bindingCount - paidCount // 绑定中但未付款的
|
||||
|
||||
// 格式化用户数据
|
||||
const formatUser = (user, type) => {
|
||||
const formatted = {
|
||||
id: user.id,
|
||||
nickname: user.nickname || '用户' + (user.id || '').slice(-4),
|
||||
avatar: user.avatar,
|
||||
status: type,
|
||||
daysRemaining: user.daysRemaining || 0,
|
||||
bindingDate: user.bindingDate ? this.formatDate(user.bindingDate) : '--',
|
||||
expiryDate: user.expiryDate ? this.formatDate(user.expiryDate) : '--',
|
||||
commission: (user.commission || 0).toFixed(2),
|
||||
orderAmount: (user.orderAmount || 0).toFixed(2),
|
||||
purchaseCount: user.purchaseCount || 0,
|
||||
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
|
||||
}
|
||||
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
|
||||
return formatted
|
||||
}
|
||||
|
||||
// 格式化金额(保留两位小数)
|
||||
const formatMoney = (num) => {
|
||||
return typeof num === 'number' ? num.toFixed(2) : '0.00'
|
||||
}
|
||||
|
||||
// ✅ 可提现金额 = 累计佣金 - 已提现金额 - 待审核金额,且不低于 0(防止数据不同步时出现负数)
|
||||
const totalCommissionNum = realData?.totalCommission || 0
|
||||
const withdrawnNum = realData?.withdrawnEarnings || 0
|
||||
const pendingWithdrawNum = realData?.pendingWithdrawAmount || 0
|
||||
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,
|
||||
userInfo,
|
||||
hasWechatId,
|
||||
|
||||
// 核心可见数据
|
||||
bindingCount,
|
||||
visitCount: realData?.visitCount || 0,
|
||||
paidCount,
|
||||
unboughtCount: expiringCount, // "即将过期"显示的是 expiringCount
|
||||
expiredCount,
|
||||
|
||||
// 收益数据 - 格式化为两位小数
|
||||
totalCommission: formatMoney(totalCommissionNum),
|
||||
availableEarnings: formatMoney(availableEarningsNum), // ✅ 使用计算后的可提现金额
|
||||
availableEarningsNum: availableEarningsNum, // ✅ 数字格式用于按钮判断
|
||||
pendingWithdrawAmount: formatMoney(pendingWithdrawNum),
|
||||
withdrawnEarnings: formatMoney(realData?.withdrawnEarnings || 0),
|
||||
earnings: formatMoney(realData?.earnings || 0),
|
||||
pendingEarnings: formatMoney(realData?.pendingEarnings || 0),
|
||||
shareRate: realData?.shareRate ?? 90,
|
||||
minWithdrawAmount: minWithdrawAmount,
|
||||
bindingDays: realData?.bindingDays ?? 30,
|
||||
userDiscount: realData?.userDiscount ?? 5,
|
||||
|
||||
// 统计
|
||||
referralCount: realData?.referralCount || realData?.stats?.totalBindings || activeBindings.length + convertedBindings.length,
|
||||
expiringCount,
|
||||
|
||||
referralCode,
|
||||
activeBindings: activeBindings.map(u => formatUser(u, 'active')),
|
||||
convertedBindings: convertedBindings.map(u => formatUser(u, 'converted')),
|
||||
expiredBindings: expiredBindings.map(u => formatUser(u, 'expired')),
|
||||
currentBindings: activeBindings.map(u => formatUser(u, 'active')),
|
||||
totalBindings: activeBindings.length + convertedBindings.length + expiredBindings.length,
|
||||
|
||||
// 收益明细
|
||||
earningsDetails: (realData?.earningsDetails || []).map(item => {
|
||||
// 解析商品描述,获取书名和章节
|
||||
const productInfo = this.parseProductDescription(item.description, item.productType)
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
productType: item.productType,
|
||||
bookTitle: productInfo.bookTitle,
|
||||
chapterTitle: productInfo.chapterTitle,
|
||||
commission: (item.commission || 0).toFixed(2),
|
||||
payTime: item.payTime ? this.formatDate(item.payTime) : '--',
|
||||
buyerNickname: item.buyerNickname || '用户',
|
||||
buyerAvatar: item.buyerAvatar
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
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 {
|
||||
// 未登录时也隐藏loading
|
||||
this.setData({ isLoading: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 切换Tab
|
||||
switchTab(e) {
|
||||
const tab = e.currentTarget.dataset.tab
|
||||
let currentBindings = []
|
||||
|
||||
if (tab === 'active') {
|
||||
currentBindings = this.data.activeBindings
|
||||
} else if (tab === 'converted') {
|
||||
currentBindings = this.data.convertedBindings
|
||||
} else {
|
||||
currentBindings = this.data.expiredBindings
|
||||
}
|
||||
|
||||
this.setData({ activeTab: tab, currentBindings })
|
||||
},
|
||||
|
||||
// 切换绑定列表显示
|
||||
toggleBindingList() {
|
||||
this.setData({ showBindingList: !this.data.showBindingList })
|
||||
},
|
||||
|
||||
// 复制邀请链接
|
||||
copyLink() {
|
||||
const link = `https://soul.quwanzhi.com/?ref=${this.data.referralCode}`
|
||||
wx.setClipboardData({
|
||||
data: link,
|
||||
success: () => wx.showToast({ title: '链接已复制', icon: 'success' })
|
||||
})
|
||||
},
|
||||
|
||||
// 分享到朋友圈 - 1:1 迁移 Next.js 的 handleShareToWechat
|
||||
shareToWechat() {
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
// 与 Next.js 完全相同的文案
|
||||
const shareText = `📖 推荐一本好书《一场SOUL的创业实验场》
|
||||
|
||||
这是卡若每天早上6-9点在Soul派对房分享的真实商业故事,55个真实案例,讲透创业的底层逻辑。
|
||||
|
||||
👉 点击阅读: ${referralLink}
|
||||
|
||||
#创业 #商业思维 #Soul派对`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '朋友圈文案已复制!',
|
||||
content: '打开微信 → 发朋友圈 → 粘贴即可',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 更多分享方式 - 1:1 迁移 Next.js 的 handleShare
|
||||
handleMoreShare() {
|
||||
const { referralCode } = this.data
|
||||
const referralLink = `https://soul.quwanzhi.com/?ref=${referralCode}`
|
||||
|
||||
// 与 Next.js 完全相同的文案
|
||||
const shareText = `我正在读《一场SOUL的创业实验场》,每天6-9点的真实商业故事,推荐给你!${referralLink}`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({
|
||||
title: '分享文案已复制',
|
||||
icon: 'success',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 生成推广海报 - 1:1 对齐 Next.js 设计
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...', mask: true })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
try {
|
||||
const { referralCode, userInfo } = this.data
|
||||
const nickname = userInfo?.nickname || '用户'
|
||||
const scene = `ref=${referralCode}`
|
||||
|
||||
console.log('[Poster] 请求小程序码, scene:', scene)
|
||||
|
||||
// 调用后端接口生成「小程序码」(官方 getwxacodeunlimit),不再使用 H5 二维码
|
||||
const res = await app.request('/api/miniprogram/qrcode', {
|
||||
method: 'POST',
|
||||
data: {
|
||||
scene, // ref=XXXX
|
||||
page: 'pages/index/index',
|
||||
width: 280,
|
||||
},
|
||||
})
|
||||
|
||||
if (!res || !res.success || !res.image) {
|
||||
console.error('[Poster] 生成小程序码失败:', res)
|
||||
throw new Error(res?.error || '生成小程序码失败')
|
||||
}
|
||||
|
||||
// 后端返回的是 data:image/png;base64,... 需要先写入本地临时文件,再作为 <image> 的 src
|
||||
const base64Data = String(res.image).replace(/^data:image\/\w+;base64,/, '')
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/poster_qrcode_${Date.now()}.png`
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => resolve(true),
|
||||
fail: (err) => {
|
||||
console.error('[Poster] 小程序码写入本地失败:', err)
|
||||
reject(err)
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
console.log('[Poster] 小程序码已保存到本地:', filePath)
|
||||
|
||||
this.setData({
|
||||
posterQrSrc: filePath,
|
||||
posterReferralLink: '', // 小程序版本不再使用 H5 链接
|
||||
posterNickname: nickname,
|
||||
posterNicknameInitial: (nickname || '用').charAt(0),
|
||||
isGeneratingPoster: false
|
||||
})
|
||||
wx.hideLoading()
|
||||
} catch (e) {
|
||||
console.error('[Poster] 生成二维码失败:', e)
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '生成失败', icon: 'none' })
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false, posterQrSrc: '', posterReferralLink: '' })
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制数据卡片
|
||||
drawDataCard(ctx, x, y, width, height, value, label, color) {
|
||||
// 卡片背景
|
||||
ctx.setFillStyle('rgba(255,255,255,0.05)')
|
||||
this.drawRoundRect(ctx, x, y, width, height, 8)
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
ctx.setLineWidth(1)
|
||||
ctx.stroke()
|
||||
|
||||
// 数值
|
||||
ctx.setFillStyle(color)
|
||||
ctx.setFontSize(24)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.fillText(value, x + width / 2, y + 24)
|
||||
|
||||
// 标签
|
||||
ctx.setFillStyle('rgba(255,255,255,0.5)')
|
||||
ctx.setFontSize(10)
|
||||
ctx.fillText(label, x + width / 2, y + 40)
|
||||
},
|
||||
|
||||
// 绘制圆角矩形
|
||||
drawRoundRect(ctx, x, y, width, height, radius) {
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x + radius, y)
|
||||
ctx.lineTo(x + width - radius, y)
|
||||
ctx.arc(x + width - radius, y + radius, radius, -Math.PI / 2, 0)
|
||||
ctx.lineTo(x + width, y + height - radius)
|
||||
ctx.arc(x + width - radius, y + height - radius, radius, 0, Math.PI / 2)
|
||||
ctx.lineTo(x + radius, y + height)
|
||||
ctx.arc(x + radius, y + height - radius, radius, Math.PI / 2, Math.PI)
|
||||
ctx.lineTo(x, y + radius)
|
||||
ctx.arc(x + radius, y + radius, radius, Math.PI, Math.PI * 1.5)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
},
|
||||
|
||||
// 光晕(替代 createRadialGradient):用同心圆叠加模拟模糊
|
||||
// centerX/centerY: 圆心坐标;radius: 最大半径;rgb: [r,g,b];maxAlpha: 最内层透明度
|
||||
drawGlow(ctx, centerX, centerY, radius, rgb, maxAlpha = 0.10) {
|
||||
const steps = 14
|
||||
for (let i = steps; i >= 1; i--) {
|
||||
const r = (radius * i) / steps
|
||||
const alpha = (maxAlpha * i) / steps
|
||||
ctx.setFillStyle(`rgba(${rgb[0]},${rgb[1]},${rgb[2]},${alpha})`)
|
||||
ctx.beginPath()
|
||||
ctx.arc(centerX, centerY, r, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制二维码(支持Base64和URL两种格式)
|
||||
async drawQRCode(ctx, qrcodeImage, x, y, size) {
|
||||
return new Promise((resolve) => {
|
||||
if (!qrcodeImage) {
|
||||
console.log('[Poster] 无二维码数据,绘制占位符')
|
||||
this.drawQRPlaceholder(ctx, x, y, size)
|
||||
resolve()
|
||||
return
|
||||
}
|
||||
|
||||
// 判断是Base64还是URL
|
||||
if (qrcodeImage.startsWith('data:image') || !qrcodeImage.startsWith('http')) {
|
||||
// Base64格式(小程序码)
|
||||
console.log('[Poster] 绘制Base64二维码')
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_promo_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
console.log('[Poster] ✅ Base64写入成功')
|
||||
ctx.drawImage(filePath, x, y, size, size)
|
||||
resolve()
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[Poster] ❌ Base64写入失败:', err)
|
||||
this.drawQRPlaceholder(ctx, x, y, size)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// URL格式(第三方二维码)
|
||||
console.log('[Poster] 下载在线二维码:', qrcodeImage)
|
||||
wx.downloadFile({
|
||||
url: qrcodeImage,
|
||||
success: (res) => {
|
||||
if (res.statusCode === 200) {
|
||||
console.log('[Poster] ✅ 二维码下载成功')
|
||||
ctx.drawImage(res.tempFilePath, x, y, size, size)
|
||||
resolve()
|
||||
} else {
|
||||
console.error('[Poster] ❌ 二维码下载失败, status:', res.statusCode)
|
||||
this.drawQRPlaceholder(ctx, x, y, size)
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('[Poster] ❌ 二维码下载失败:', err)
|
||||
this.drawQRPlaceholder(ctx, x, y, size)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
drawQRPlaceholder(ctx, x, y, size) {
|
||||
// 绘制占位符方框
|
||||
ctx.setFillStyle('rgba(200,200,200,0.3)')
|
||||
this.drawRoundRect(ctx, x, y, size, size, 8)
|
||||
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.setFontSize(11)
|
||||
ctx.setTextAlign('center')
|
||||
ctx.fillText('小程序码', x + size / 2, y + size / 2)
|
||||
},
|
||||
|
||||
// 关闭海报弹窗
|
||||
closePosterModal() {
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报
|
||||
savePoster() {
|
||||
const { posterQrSrc } = this.data
|
||||
if (!posterQrSrc) {
|
||||
wx.showToast({ title: '二维码未生成', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '保存中...', mask: true })
|
||||
wx.downloadFile({
|
||||
url: posterQrSrc,
|
||||
success: (res) => {
|
||||
if (res.statusCode !== 200) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '下载失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
},
|
||||
fail: (err) => {
|
||||
wx.hideLoading()
|
||||
if (String(err.errMsg || '').includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存二维码',
|
||||
confirmText: '去设置',
|
||||
success: (r) => {
|
||||
if (r.confirm) wx.openSetting()
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '下载失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 预览二维码
|
||||
previewPosterQr() {
|
||||
const { posterQrSrc } = this.data
|
||||
if (!posterQrSrc) return
|
||||
wx.previewImage({ urls: [posterQrSrc] })
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
stopPropagation() {},
|
||||
|
||||
// 分享到朋友圈 - 随机文案
|
||||
shareToMoments() {
|
||||
// 10条随机文案,基于书的内容
|
||||
const shareTexts = [
|
||||
`🔥 在派对房里听到的真实故事,比虚构的小说精彩100倍!\n\n电动车出租月入5万、私域一年赚1000万、一个人的公司月入10万...\n\n62个真实案例,搜"Soul创业派对"小程序看全部!\n\n#创业 #私域 #商业`,
|
||||
|
||||
`💡 今天终于明白:会赚钱的人,都在用"流量杠杆"\n\n抖音、Soul、飞书...同一套内容,撬动不同平台的流量。\n\n《Soul创业派对》里的实战方法,受用终身!\n\n#流量 #副业 #创业派对`,
|
||||
|
||||
`📚 一个70后大健康私域,一个月150万流水是怎么做到的?\n\n答案在《Soul创业派对》第9章,全是干货。\n\n搜小程序"Soul创业派对",我在里面等你\n\n#大健康 #私域运营 #真实案例`,
|
||||
|
||||
`🎯 "分钱不是分你的钱,是分不属于对方的钱"\n\n这句话改变了我对商业合作的认知。\n\n推荐《Soul创业派对》,创业者必读!\n\n#云阿米巴 #商业思维 #创业`,
|
||||
|
||||
`✨ 资源整合高手的社交方法论,在派对房里学到了\n\n"先让对方赚到钱,自己才能长久赚钱"\n\n这本《Soul创业派对》,每章都是实战经验\n\n#资源整合 #社交 #创业故事`,
|
||||
|
||||
`🚀 AI工具推广:一个隐藏的高利润赛道\n\n客单价高、复购率高、需求旺盛...\n\n《Soul创业派对》里的商业机会,你发现了吗?\n\n#AI #副业 #商业机会`,
|
||||
|
||||
`💰 美业整合:一个人的公司如何月入十万?\n\n不开店、不囤货、轻资产运营...\n\n《Soul创业派对》告诉你答案!\n\n#美业 #轻创业 #月入十万`,
|
||||
|
||||
`🌟 3000万流水是怎么跑出来的?\n\n不是靠运气,是靠系统。\n\n《Soul创业派对》里的电商底层逻辑,值得反复看\n\n#电商 #创业 #商业系统`,
|
||||
|
||||
`📖 "人与人之间的关系,归根结底就三个东西:利益、情感、价值观"\n\n在派对房里聊出的金句,都在《Soul创业派对》里\n\n#人性 #商业 #创业派对`,
|
||||
|
||||
`🔔 未来职业的三个方向:技术型、资源型、服务型\n\n你属于哪一种?\n\n《Soul创业派对》帮你找到答案!\n\n#职业规划 #创业 #未来`
|
||||
]
|
||||
|
||||
// 随机选择一条文案
|
||||
const randomIndex = Math.floor(Math.random() * shareTexts.length)
|
||||
const shareText = shareTexts[randomIndex]
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请打开微信朋友圈,粘贴分享文案,配合推广海报一起发布效果更佳!\n\n再次点击可获取新的随机文案',
|
||||
showCancel: false,
|
||||
confirmText: '去发朋友圈'
|
||||
})
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 提现 - 直接到微信零钱
|
||||
async handleWithdraw() {
|
||||
const availableEarnings = this.data.availableEarningsNum || 0
|
||||
const minWithdrawAmount = this.data.minWithdrawAmount || 10
|
||||
const hasWechatId = this.data.hasWechatId
|
||||
|
||||
if (availableEarnings <= 0) {
|
||||
wx.showToast({ title: '暂无可提现收益', icon: 'none' })
|
||||
return
|
||||
}
|
||||
// 任意金额可提现,不再设最低限额
|
||||
|
||||
// 未绑定微信号时引导去设置
|
||||
if (!hasWechatId) {
|
||||
wx.showModal({
|
||||
title: '请先绑定微信号',
|
||||
content: '提现需先绑定微信号,便于到账核对。请到「设置」中绑定后再提现。',
|
||||
confirmText: '去绑定',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
wx.showModal({
|
||||
title: '确认提现',
|
||||
content: `将提现 ¥${availableEarnings.toFixed(2)} 到您的微信零钱`,
|
||||
confirmText: '立即提现',
|
||||
success: async (res) => {
|
||||
if (!res.confirm) return
|
||||
const tmplId = app.globalData.withdrawSubscribeTmplId
|
||||
if (tmplId && tmplId.length > 10) {
|
||||
wx.requestSubscribeMessage({
|
||||
tmplIds: [tmplId],
|
||||
success: () => { this.doWithdraw(availableEarnings) },
|
||||
fail: () => { this.doWithdraw(availableEarnings) }
|
||||
})
|
||||
} else {
|
||||
await this.doWithdraw(availableEarnings)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 跳转提现记录页
|
||||
goToWithdrawRecords() {
|
||||
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
|
||||
},
|
||||
|
||||
// 执行提现
|
||||
async doWithdraw(amount) {
|
||||
wx.showLoading({ title: '提现中...' })
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const res = await app.request('/api/miniprogram/withdraw', {
|
||||
method: 'POST',
|
||||
data: { userId, amount }
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
|
||||
if (res.success) {
|
||||
wx.showModal({
|
||||
title: '提现申请已提交 ✅',
|
||||
content: res.message || '正在审核中,通过后会自动到账您的微信零钱',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
|
||||
// 刷新数据(此时待审核金额会增加,可提现金额会减少)
|
||||
this.initData()
|
||||
} else {
|
||||
if (res.needBind || res.needBindWechat) {
|
||||
wx.showModal({
|
||||
title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
|
||||
content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
|
||||
confirmText: '去绑定',
|
||||
success: (modalRes) => {
|
||||
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[Referral] 提现失败:', e)
|
||||
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 显示通知
|
||||
showNotification() {
|
||||
wx.showToast({ title: '暂无新消息', icon: 'none' })
|
||||
},
|
||||
|
||||
// 显示设置
|
||||
showSettings() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['自动提现设置', '收益通知设置'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.showAutoWithdrawSettings()
|
||||
} else {
|
||||
this.showNotificationSettings()
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 自动提现设置
|
||||
async showAutoWithdrawSettings() {
|
||||
const app = getApp()
|
||||
const { userInfo } = app.globalData
|
||||
|
||||
if (!userInfo) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前设置
|
||||
let autoWithdrawEnabled = wx.getStorageSync(`autoWithdraw_${userInfo.id}`) || false
|
||||
let autoWithdrawThreshold = wx.getStorageSync(`autoWithdrawThreshold_${userInfo.id}`) || this.data.minWithdrawAmount || 10
|
||||
|
||||
wx.showModal({
|
||||
title: '自动提现设置',
|
||||
content: `当前状态:${autoWithdrawEnabled ? '已开启' : '已关闭'}\n自动提现阈值:¥${autoWithdrawThreshold}\n\n开启后,当可提现金额达到阈值时将自动发起提现申请。`,
|
||||
confirmText: autoWithdrawEnabled ? '关闭' : '开启',
|
||||
cancelText: '修改阈值',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
// 切换开关
|
||||
this.toggleAutoWithdraw(!autoWithdrawEnabled, autoWithdrawThreshold)
|
||||
} else if (res.cancel) {
|
||||
// 修改阈值
|
||||
this.setAutoWithdrawThreshold(autoWithdrawEnabled, autoWithdrawThreshold)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 切换自动提现开关
|
||||
toggleAutoWithdraw(enabled, threshold) {
|
||||
const app = getApp()
|
||||
const { userInfo } = app.globalData
|
||||
|
||||
wx.setStorageSync(`autoWithdraw_${userInfo.id}`, enabled)
|
||||
|
||||
wx.showToast({
|
||||
title: enabled ? '自动提现已开启' : '自动提现已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果开启,检查当前金额是否达到阈值
|
||||
if (enabled && this.data.availableEarningsNum >= threshold) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: `当前可提现金额¥${this.data.availableEarnings}已达到阈值¥${threshold},是否立即提现?`,
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
this.handleWithdraw()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 设置自动提现阈值
|
||||
setAutoWithdrawThreshold(currentEnabled, currentThreshold) {
|
||||
const minAmount = this.data.minWithdrawAmount || 10
|
||||
|
||||
wx.showModal({
|
||||
title: '设置提现阈值',
|
||||
content: `请输入自动提现金额阈值(最低¥${minAmount})`,
|
||||
editable: true,
|
||||
placeholderText: currentThreshold.toString(),
|
||||
success: (res) => {
|
||||
if (res.confirm && res.content) {
|
||||
const threshold = parseFloat(res.content)
|
||||
|
||||
if (isNaN(threshold) || threshold < minAmount) {
|
||||
wx.showToast({
|
||||
title: `请输入不小于¥${minAmount}的金额`,
|
||||
icon: 'none'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const app = getApp()
|
||||
const { userInfo } = app.globalData
|
||||
|
||||
wx.setStorageSync(`autoWithdrawThreshold_${userInfo.id}`, threshold)
|
||||
|
||||
wx.showToast({
|
||||
title: `阈值已设置为¥${threshold}`,
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 重新显示设置界面
|
||||
setTimeout(() => {
|
||||
this.showAutoWithdrawSettings()
|
||||
}, 1500)
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 收益通知设置
|
||||
showNotificationSettings() {
|
||||
const app = getApp()
|
||||
const { userInfo } = app.globalData
|
||||
|
||||
if (!userInfo) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前设置
|
||||
let notifyEnabled = wx.getStorageSync(`earningsNotify_${userInfo.id}`) !== false // 默认开启
|
||||
|
||||
wx.showModal({
|
||||
title: '收益通知设置',
|
||||
content: `当前状态:${notifyEnabled ? '已开启' : '已关闭'}\n\n开启后,将在有新收益时收到小程序通知提醒。`,
|
||||
confirmText: notifyEnabled ? '关闭通知' : '开启通知',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const newState = !notifyEnabled
|
||||
wx.setStorageSync(`earningsNotify_${userInfo.id}`, newState)
|
||||
|
||||
wx.showToast({
|
||||
title: newState ? '收益通知已开启' : '收益通知已关闭',
|
||||
icon: 'success'
|
||||
})
|
||||
|
||||
// 如果开启,请求通知权限
|
||||
if (newState) {
|
||||
wx.requestSubscribeMessage({
|
||||
tmplIds: [''] // 需要配置模板ID
|
||||
}).catch(() => {
|
||||
// 用户拒绝授权,不影响功能
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 分享 - 带推荐码(优先用页面数据,空时用 app.getMyReferralCode)
|
||||
onShareAppMessage() {
|
||||
const ref = this.data.referralCode || app.getMyReferralCode()
|
||||
console.log('[Referral] 分享给好友,推荐码:', ref)
|
||||
return {
|
||||
title: 'Soul创业派对 - 来自派对房的真实商业故事',
|
||||
path: ref ? `/pages/index/index?ref=${ref}` : '/pages/index/index'
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
// 如需自定义图片,请将图片放在 /assets/ 目录并配置路径
|
||||
}
|
||||
},
|
||||
|
||||
// 分享到朋友圈
|
||||
onShareTimeline() {
|
||||
const ref = this.data.referralCode || app.getMyReferralCode()
|
||||
console.log('[Referral] 分享到朋友圈,推荐码:', ref)
|
||||
return {
|
||||
title: `Soul创业派对 - 62个真实商业案例`,
|
||||
query: ref ? `ref=${ref}` : ''
|
||||
// 不设置 imageUrl,使用小程序默认截图
|
||||
}
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
// 解析商品描述,获取书名和章节
|
||||
parseProductDescription(description, productType) {
|
||||
if (!description) {
|
||||
return {
|
||||
bookTitle: '未知商品',
|
||||
chapterTitle: ''
|
||||
}
|
||||
}
|
||||
|
||||
// 匹配格式:《书名》- 章节名
|
||||
const match = description.match(/《(.+?)》(?:\s*-\s*(.+))?/)
|
||||
|
||||
if (match) {
|
||||
return {
|
||||
bookTitle: match[1] || '未知书籍',
|
||||
chapterTitle: match[2] || (productType === 'fullbook' ? '全书购买' : '')
|
||||
}
|
||||
}
|
||||
|
||||
// 如果匹配失败,直接返回原始描述
|
||||
return {
|
||||
bookTitle: description.split('-')[0] || description,
|
||||
chapterTitle: description.split('-')[1] || ''
|
||||
}
|
||||
},
|
||||
|
||||
// 格式化日期
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '--'
|
||||
const d = new Date(dateStr)
|
||||
const month = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${month}-${day}`
|
||||
}
|
||||
})
|
||||
6
miniprogram/pages/referral/referral.json
Normal file
6
miniprogram/pages/referral/referral.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom",
|
||||
"enableShareAppMessage": true,
|
||||
"enableShareTimeline": true
|
||||
}
|
||||
332
miniprogram/pages/referral/referral.wxml
Normal file
332
miniprogram/pages/referral/referral.wxml
Normal file
@@ -0,0 +1,332 @@
|
||||
<!--分销中心 - 按照Web版本1:1还原-->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-left">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<image class="nav-icon" src="/assets/icons/chevron-left.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
<text class="nav-title">分销中心</text>
|
||||
<view class="nav-right-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<!-- 过期提醒横幅 -->
|
||||
<view class="expiring-banner" wx:if="{{expiringCount > 0}}">
|
||||
<view class="banner-icon">
|
||||
<image class="icon-bell-warning" src="/assets/icons/bell.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="banner-content">
|
||||
<text class="banner-title">{{expiringCount}} 位用户绑定即将过期</text>
|
||||
<text class="banner-desc">{{bindingDays}}天内未付款将解除绑定关系</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益卡片 - 对齐 Next.js -->
|
||||
<view class="earnings-card">
|
||||
<view class="earnings-bg"></view>
|
||||
<view class="earnings-main">
|
||||
<view class="earnings-header">
|
||||
<view class="earnings-left">
|
||||
<view class="wallet-icon">
|
||||
<image class="icon-wallet" src="/assets/icons/wallet.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="earnings-info">
|
||||
<text class="earnings-label">可提现金额</text>
|
||||
<text class="commission-rate">{{shareRate}}% 返利</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="earnings-right">
|
||||
<text class="earnings-value">¥{{availableEarnings}}</text>
|
||||
<text class="pending-text">累计: ¥{{totalCommission}} | 待审核: ¥{{pendingWithdrawAmount}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="withdraw-btn {{availableEarningsNum <= 0 || !hasWechatId ? 'btn-disabled' : ''}}" bindtap="handleWithdraw">
|
||||
{{availableEarningsNum <= 0 ? '暂无可提现' : !hasWechatId ? '请先绑定微信号' : '申请提现 ¥' + availableEarnings}}
|
||||
</view>
|
||||
<text class="wechat-tip" wx:if="{{availableEarningsNum > 0 && !hasWechatId}}">为便于提现到账,请先到「设置」中绑定微信号</text>
|
||||
<view class="withdraw-records-link" bindtap="goToWithdrawRecords">查看提现记录</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 数据统计 - 对齐 Next.js -->
|
||||
<view class="stats-grid">
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{bindingCount}}</text>
|
||||
<text class="stat-label">绑定中</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{paidCount}}</text>
|
||||
<text class="stat-label">已付款</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value orange">{{unboughtCount}}</text>
|
||||
<text class="stat-label">即将过期</text>
|
||||
</view>
|
||||
<view class="stat-card">
|
||||
<text class="stat-value">{{referralCount}}</text>
|
||||
<text class="stat-label">总邀请</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 推广规则 - 顺序调整到前面 -->
|
||||
<view class="rules-card">
|
||||
<view class="rules-header">
|
||||
<view class="rules-icon">
|
||||
<image class="icon-alert" src="/assets/icons/alert-circle.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="rules-title">推广规则</text>
|
||||
</view>
|
||||
<view class="rules-list">
|
||||
<text class="rule-item">• 好友通过你的链接购买,<text class="gold">立享{{userDiscount}}%优惠</text></text>
|
||||
<text class="rule-item">• 好友成功付款后,你获得 <text class="brand">{{shareRate}}%</text> 收益</text>
|
||||
<text class="rule-item">• 绑定期<text class="brand">{{bindingDays}}天</text>,期满未付款自动解除</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 绑定用户列表 -->
|
||||
<view class="binding-card">
|
||||
<view class="binding-header" bindtap="toggleBindingList">
|
||||
<view class="binding-title">
|
||||
<image class="binding-icon-img" src="/assets/icons/users.svg" mode="aspectFit"></image>
|
||||
<text class="title-text">绑定用户</text>
|
||||
<text class="binding-count">({{totalBindings}})</text>
|
||||
</view>
|
||||
<text class="toggle-icon">{{showBindingList ? '▲' : '▼'}}</text>
|
||||
</view>
|
||||
|
||||
<block wx:if="{{showBindingList}}">
|
||||
<!-- Tab切换 -->
|
||||
<view class="binding-tabs">
|
||||
<view
|
||||
class="tab-item {{activeTab === 'active' ? 'tab-active' : ''}}"
|
||||
bindtap="switchTab"
|
||||
data-tab="active"
|
||||
>绑定中 ({{activeBindings.length}})</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'converted' ? 'tab-active' : ''}}"
|
||||
bindtap="switchTab"
|
||||
data-tab="converted"
|
||||
>已付款 ({{convertedBindings.length}})</view>
|
||||
<view
|
||||
class="tab-item {{activeTab === 'expired' ? 'tab-active' : ''}}"
|
||||
bindtap="switchTab"
|
||||
data-tab="expired"
|
||||
>已过期 ({{expiredBindings.length}})</view>
|
||||
</view>
|
||||
|
||||
<!-- 用户列表 -->
|
||||
<view class="binding-list">
|
||||
<block wx:if="{{currentBindings.length === 0}}">
|
||||
<view class="empty-state">
|
||||
<text class="empty-icon">👤</text>
|
||||
<text class="empty-text">暂无用户</text>
|
||||
</view>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<view
|
||||
class="binding-item"
|
||||
wx:for="{{currentBindings}}"
|
||||
wx:key="id"
|
||||
>
|
||||
<view class="user-avatar {{item.status === 'converted' ? 'avatar-converted' : item.status === 'expired' ? 'avatar-expired' : ''}}">
|
||||
<text wx:if="{{item.status === 'converted'}}">✓</text>
|
||||
<text wx:elif="{{item.status === 'expired'}}">⏰</text>
|
||||
<text wx:else>{{item.nickname[0] || '用'}}</text>
|
||||
</view>
|
||||
<view class="user-info">
|
||||
<text class="user-name">{{item.nickname || '匿名用户'}}</text>
|
||||
<text class="user-time">绑定于 {{item.bindingDate}}</text>
|
||||
</view>
|
||||
<view class="user-status">
|
||||
<block wx:if="{{item.status === 'converted'}}">
|
||||
<text class="status-amount">+¥{{item.commission}}</text>
|
||||
<text class="status-order">已购{{item.purchaseCount || 1}}次</text>
|
||||
</block>
|
||||
<block wx:elif="{{item.status === 'expired'}}">
|
||||
<text class="status-tag tag-gray">已过期</text>
|
||||
<text class="status-time">{{item.expiryDate}}</text>
|
||||
</block>
|
||||
<block wx:else>
|
||||
<text class="status-tag {{item.daysRemaining <= 3 ? 'tag-red' : item.daysRemaining <= 7 ? 'tag-orange' : 'tag-green'}}">
|
||||
{{item.daysRemaining}}天
|
||||
</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 分享按钮 - 1:1 对齐 Next.js -->
|
||||
<view class="share-section">
|
||||
<view class="share-item" bindtap="generatePoster">
|
||||
<view class="share-icon poster">
|
||||
<image class="icon-share-btn" src="/assets/icons/image.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="share-info">
|
||||
<text class="share-title">生成推广海报</text>
|
||||
<text class="share-desc">一键生成精美海报分享</text>
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<view class="share-item" bindtap="shareToWechat">
|
||||
<view class="share-icon wechat">
|
||||
<image class="icon-share-btn" src="/assets/icons/message-circle.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="share-info">
|
||||
<text class="share-title">分享到朋友圈</text>
|
||||
<text class="share-desc">复制文案发朋友圈</text>
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
|
||||
<view class="share-item" bindtap="handleMoreShare">
|
||||
<view class="share-icon link">
|
||||
<image class="icon-share-btn" src="/assets/icons/share.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<view class="share-info">
|
||||
<text class="share-title">更多分享方式</text>
|
||||
<text class="share-desc">复制链接发给好友;点击右上角 ⋮ 可分享到朋友圈(带推荐码)</text>
|
||||
</view>
|
||||
<image class="share-arrow-icon" src="/assets/icons/arrow-right.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 收益明细 - 增强版 -->
|
||||
<view class="earnings-detail-card" wx:if="{{earningsDetails.length > 0}}">
|
||||
<view class="detail-header">
|
||||
<text class="detail-title">收益明细</text>
|
||||
</view>
|
||||
<view class="detail-list">
|
||||
<view class="detail-item" wx:for="{{earningsDetails}}" wx:key="id">
|
||||
<!-- 买家头像 -->
|
||||
<view class="detail-avatar-wrap">
|
||||
<image
|
||||
class="detail-avatar"
|
||||
wx:if="{{item.buyerAvatar}}"
|
||||
src="{{item.buyerAvatar}}"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="detail-avatar-text" wx:else>
|
||||
{{item.buyerNickname.charAt(0)}}
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 详细信息 -->
|
||||
<view class="detail-content">
|
||||
<view class="detail-top">
|
||||
<text class="detail-buyer">{{item.buyerNickname}}</text>
|
||||
<text class="detail-amount">+¥{{item.commission}}</text>
|
||||
</view>
|
||||
<view class="detail-product">
|
||||
<text class="detail-book">{{item.bookTitle}}</text>
|
||||
<text class="detail-chapter" wx:if="{{item.chapterTitle}}"> - {{item.chapterTitle}}</text>
|
||||
</view>
|
||||
<text class="detail-time">{{item.payTime}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 空状态 - 对齐 Next.js -->
|
||||
<view class="empty-earnings" wx:if="{{earningsDetails.length === 0 && activeBindings.length === 0}}">
|
||||
<view class="empty-icon-wrapper">
|
||||
<image class="empty-gift-icon" src="/assets/icons/gift.svg" mode="aspectFit"></image>
|
||||
</view>
|
||||
<text class="empty-title">暂无收益记录</text>
|
||||
<text class="empty-desc">分享专属链接,好友购买即可获得 {{shareRate}}% 返利</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 - 优化小程序显示 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="poster-dialog" catchtap="stopPropagation">
|
||||
<view class="poster-close" bindtap="closePosterModal">✕</view>
|
||||
|
||||
<!-- 上半部分:海报内容(不使用画布,纯布局 + 二维码图片) -->
|
||||
<view class="poster-card">
|
||||
<!-- 装饰光效 -->
|
||||
<view class="poster-glow poster-glow-left"></view>
|
||||
<view class="poster-glow poster-glow-right"></view>
|
||||
<view class="poster-ring"></view>
|
||||
|
||||
<view class="poster-inner">
|
||||
<!-- 顶部标签 -->
|
||||
<view class="poster-badges">
|
||||
<text class="poster-badge poster-badge-gold">真实商业案例</text>
|
||||
<text class="poster-badge poster-badge-brand">每日更新</text>
|
||||
</view>
|
||||
|
||||
<!-- 标题 -->
|
||||
<view class="poster-title">
|
||||
<text class="poster-title-line1">一场SOUL的</text>
|
||||
<text class="poster-title-line2">创业实验场</text>
|
||||
</view>
|
||||
<text class="poster-subtitle">来自Soul派对房的真实商业故事</text>
|
||||
|
||||
<!-- 核心数据 -->
|
||||
<view class="poster-stats">
|
||||
<view class="poster-stat">
|
||||
<text class="poster-stat-value poster-stat-gold">{{posterCaseCount}}</text>
|
||||
<text class="poster-stat-label">真实案例</text>
|
||||
</view>
|
||||
<view class="poster-stat">
|
||||
<text class="poster-stat-value poster-stat-brand">{{userDiscount}}%</text>
|
||||
<text class="poster-stat-label">好友优惠</text>
|
||||
</view>
|
||||
<view class="poster-stat">
|
||||
<text class="poster-stat-value poster-stat-pink">{{shareRate}}%</text>
|
||||
<text class="poster-stat-label">你的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 标签 -->
|
||||
<view class="poster-tags">
|
||||
<text class="poster-tag">人性观察</text>
|
||||
<text class="poster-tag">行业揭秘</text>
|
||||
<text class="poster-tag">赚钱逻辑</text>
|
||||
<text class="poster-tag">创业复盘</text>
|
||||
<text class="poster-tag">资源对接</text>
|
||||
</view>
|
||||
|
||||
<!-- 推荐人 -->
|
||||
<view class="poster-recommender">
|
||||
<view class="poster-avatar">
|
||||
<text class="poster-avatar-text">{{posterNicknameInitial}}</text>
|
||||
</view>
|
||||
<text class="poster-recommender-text">{{posterNickname}} 推荐你来读</text>
|
||||
</view>
|
||||
|
||||
<!-- 优惠说明 -->
|
||||
<view class="poster-discount">
|
||||
<text class="poster-discount-text">通过我的链接购买,<text class="poster-discount-highlight">立省{{userDiscount}}%</text>
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- 二维码 -->
|
||||
<view class="poster-qr-wrap" bindtap="previewPosterQr">
|
||||
<image
|
||||
class="poster-qr-img"
|
||||
src="{{posterQrSrc}}"
|
||||
mode="aspectFit"
|
||||
show-menu-by-longpress="true"
|
||||
></image>
|
||||
</view>
|
||||
<text class="poster-qr-tip">长按识别 · 立即试读</text>
|
||||
<text class="poster-code">邀请码: {{referralCode}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 下半部分:白色操作区 -->
|
||||
<view class="poster-footer">
|
||||
<text class="poster-footer-tip">长按上方图片保存,或截图分享</text>
|
||||
<view class="poster-footer-btn" bindtap="closePosterModal">关闭</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
302
miniprogram/pages/referral/referral.wxss
Normal file
302
miniprogram/pages/referral/referral.wxss
Normal file
@@ -0,0 +1,302 @@
|
||||
/* ???????? - 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; }
|
||||
.wechat-tip { display: block; font-size: 24rpx; color: rgba(255,165,0,0.9); margin-top: 16rpx; text-align: center; }
|
||||
.withdraw-records-link { display: block; margin-top: 16rpx; text-align: center; font-size: 26rpx; color: #00CED1; }
|
||||
|
||||
/* ???? - ?? 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); gap: 24rpx; }
|
||||
.binding-item:last-child { border-bottom: none; }
|
||||
|
||||
/* ?? */
|
||||
.user-avatar { width: 88rpx; height: 88rpx; border-radius: 50%; background: rgba(0,206,209,0.2); display: flex; align-items: center; justify-content: center; font-size: 32rpx; font-weight: 600; color: #00CED1; 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; min-width: 0; display: flex; flex-direction: column; gap: 4rpx; }
|
||||
.user-name { font-size: 28rpx; color: #fff; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.user-time { font-size: 22rpx; color: rgba(255,255,255,0.5); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
/* ???? */
|
||||
.user-status { flex-shrink: 0; text-align: right; display: flex; flex-direction: column; align-items: flex-end; gap: 4rpx; min-width: 100rpx; }
|
||||
.status-amount { font-size: 28rpx; color: #4CAF50; font-weight: 600; white-space: nowrap; }
|
||||
.status-order { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
|
||||
.status-time { font-size: 20rpx; color: rgba(255,255,255,0.5); white-space: nowrap; }
|
||||
|
||||
/* ???? */
|
||||
.status-tag { font-size: 24rpx; font-weight: 600; padding: 6rpx 16rpx; border-radius: 16rpx; white-space: nowrap; }
|
||||
.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; }
|
||||
.status-tag.tag-gray { background: rgba(158,158,158,0.2); color: #9E9E9E; }
|
||||
|
||||
/* ????? - ?? 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; }
|
||||
|
||||
/* ?????? - ??? */
|
||||
.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; padding: 16rpx 0; }
|
||||
|
||||
/* ??????? */
|
||||
.earnings-detail-card .detail-item { display: flex; align-items: center; gap: 24rpx; padding: 24rpx 40rpx; background: transparent; border-bottom: 2rpx solid rgba(255,255,255,0.03); }
|
||||
.earnings-detail-card .detail-item:last-child { border-bottom: none; }
|
||||
.earnings-detail-card .detail-item:active { background: rgba(255, 255, 255, 0.05); }
|
||||
|
||||
/* ???? */
|
||||
.earnings-detail-card .detail-avatar-wrap { width: 88rpx; height: 88rpx; flex-shrink: 0; }
|
||||
.earnings-detail-card .detail-avatar { width: 100%; height: 100%; border-radius: 50%; border: 2rpx solid rgba(56, 189, 172, 0.2); }
|
||||
.earnings-detail-card .detail-avatar-text { width: 100%; height: 100%; border-radius: 50%; background: linear-gradient(135deg, #38bdac 0%, #2da396 100%); display: flex; align-items: center; justify-content: center; font-size: 36rpx; font-weight: 700; color: #ffffff; }
|
||||
|
||||
/* ???? */
|
||||
.earnings-detail-card .detail-content { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 8rpx; }
|
||||
.earnings-detail-card .detail-top { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; }
|
||||
.earnings-detail-card .detail-buyer { font-size: 28rpx; font-weight: 500; color: #ffffff; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.earnings-detail-card .detail-amount { font-size: 32rpx; font-weight: 700; color: #38bdac; flex-shrink: 0; white-space: nowrap; }
|
||||
|
||||
/* ???? */
|
||||
.earnings-detail-card .detail-product { display: flex; align-items: baseline; gap: 4rpx; font-size: 24rpx; color: rgba(255, 255, 255, 0.6); min-width: 0; overflow: hidden; }
|
||||
.earnings-detail-card .detail-book { color: rgba(255, 255, 255, 0.7); font-weight: 500; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.earnings-detail-card .detail-chapter { color: rgba(255, 255, 255, 0.5); flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.earnings-detail-card .detail-time { font-size: 22rpx; color: rgba(255, 255, 255, 0.4); }
|
||||
|
||||
/* ???? - ?? 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; }
|
||||
|
||||
| ||||