初始提交:一场soul的创业实验-永平 网站与小程序

Made-with: Cursor
This commit is contained in:
卡若
2026-03-07 22:58:43 +08:00
commit b7c35a89b0
513 changed files with 89020 additions and 0 deletions

14
miniprogram/.gitignore vendored Normal file
View File

@@ -0,0 +1,14 @@
# Windows
[Dd]esktop.ini
Thumbs.db
$RECYCLE.BIN/
# macOS
.DS_Store
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
# Node.js
node_modules/

138
miniprogram/README.md Normal file
View File

@@ -0,0 +1,138 @@
# Soul创业实验 - 微信小程序
> 一场SOUL的创业实验场 - 来自Soul派对房的真实商业故事
## 📱 项目简介
本项目是《一场SOUL的创业实验场》的微信小程序版本完整还原了Web端的所有UI界面和功能。
## 🎨 设计特点
- **主题色**: Soul青色 (#00CED1)
- **设计风格**: 深色主题 + 毛玻璃效果
- **1:1还原**: 完全复刻Web端的UI设计
## 📂 项目结构
```
miniprogram/
├── app.js # 应用入口
├── app.json # 应用配置
├── app.wxss # 全局样式
├── custom-tab-bar/ # 自定义TabBar组件
│ ├── index.js
│ ├── index.json
│ ├── index.wxml
│ └── index.wxss
├── pages/
│ ├── index/ # 首页
│ ├── chapters/ # 目录页
│ ├── match/ # 找伙伴页
│ ├── my/ # 我的页面
│ ├── read/ # 阅读页
│ ├── about/ # 关于作者
│ ├── referral/ # 推广中心
│ ├── purchases/ # 订单页
│ └── settings/ # 设置页
├── utils/
│ ├── util.js # 工具函数
│ └── payment.js # 支付工具
├── assets/
│ └── icons/ # 图标资源
├── project.config.json # 项目配置
└── sitemap.json # 站点地图
```
## 🚀 功能列表
### 核心功能
- ✅ 首页 - 书籍展示、推荐章节、阅读进度
- ✅ 目录 - 完整章节列表、篇章折叠展开
- ✅ 找伙伴 - 匹配动画、匹配类型选择
- ✅ 我的 - 个人信息、订单、推广中心
- ✅ 阅读 - 付费墙、章节导航、分享功能
### 特色功能
- ✅ 自定义TabBar中间突出的找伙伴按钮
- ✅ 阅读进度条
- ✅ 匹配动画效果
- ✅ 付费墙与购买流程
- ✅ 分享海报功能
- ✅ 推广佣金系统
## 🛠 开发指南
### 环境要求
- 微信开发者工具 >= 1.06.2308310
- 基础库版本 >= 3.3.4
### 快速开始
1. **下载微信开发者工具**
- 前往 [微信开发者工具下载页面](https://developers.weixin.qq.com/miniprogram/dev/devtools/download.html)
2. **导入项目**
- 打开微信开发者工具
- 选择"导入项目"
- 项目目录选择 `miniprogram` 文件夹
- AppID 使用: `wx432c93e275548671`
3. **编译运行**
- 点击"编译"按钮
- 在模拟器中预览效果
### 真机调试
1. 点击工具栏的"预览"按钮
2. 使用微信扫描二维码
3. 在真机上测试所有功能
## 📝 配置说明
### API配置
`app.js` 中修改 `globalData.baseUrl`:
```javascript
globalData: {
baseUrl: 'https://soul.ckb.fit', // 你的API地址
// ...
}
```
### AppID配置
`project.config.json` 中修改:
```json
{
"appid": "你的小程序AppID"
}
```
## 🎯 上线发布
1. **准备工作**
- 确保所有功能测试通过
- 检查API接口是否正常
- 确认支付功能已配置
2. **上传代码**
- 在开发者工具中点击"上传"
- 填写版本号和项目备注
3. **提交审核**
- 登录[微信公众平台](https://mp.weixin.qq.com)
- 进入"版本管理"
- 提交审核
4. **发布上线**
- 审核通过后点击"发布"
## 🔗 相关链接
- **Web版本**: https://soul.ckb.fit
- **作者微信**: 28533368
- **技术支持**: 存客宝
## 📄 版权信息
© 2024 卡若. All rights reserved.

View File

@@ -0,0 +1,67 @@
# miniprogram 功能还原分析报告
## 一、对比结论
| 项目 | miniprogram甲方 | miniprogram2你写的 |
|------|-------------------|------------------------|
| 页面分享 | 仅 read、referral 有 | 几乎所有页面都有 |
| scene 解析 | 无 | 有 utils/scene.js |
| 推荐码获取 | 分散userInfo?.referralCode 等) | 统一 app.getMyReferralCode() |
| 书籍 API | /api/book/all-chapters | /api/miniprogram/book/all-chapters |
| 特有页面 | vip、member-detail | scan、profile-edit |
## 二、已完成的还原项
### 1. 基础能力app.js + utils/scene.js
- **新增** `utils/scene.js`:扫码 scene 参数编解码,支持 `mid``id``ref`
- **app.js**
- 引入 `parseScene``handleReferralCode` 支持 `options.scene` 解析
- 新增 `getMyReferralCode()`:统一获取邀请码
- 新增 `getSectionMid(sectionId)`:根据 id 查 mid
- `loadBookData` 改为 `/api/miniprogram/book/all-chapters`
### 2. 页面分享onShareAppMessage
已为以下页面补充分享,路径统一带 `ref` 参数:
- index、chapters、match、my
- read、referral原有已统一用 getMyReferralCode
- search、settings、purchases、privacy
- withdraw-records、addresses、addresses/edit
- agreement、about、vip、member-detail
### 3. read.js 分享逻辑与 mid 支持
- 使用 `app.getMyReferralCode()` 替代 `userInfo?.referralCode || wx.getStorageSync('referralCode')`
- **mid 支持**onLoad 支持 `options.scene``options.mid`mid 有值无 id 时从 bookData 或 `/api/miniprogram/book/chapter/by-mid/:mid` 解析 id
- 分享 path 优先用 `mid=`(扫码/海报闭环),无则用 `id=`API 返回 `res.mid` 时写入 `sectionMid`
### 4. API 路径修正
- `app.loadBookData``/api/book/all-chapters``/api/miniprogram/book/all-chapters`
- `index.loadBookData``loadFeaturedFromServer``loadLatestChapters`:同上
- `chapters.loadDailyChapters`:同上
- **VIP 接口**`/api/vip/*``/api/miniprogram/vip/*`vip.js、my.js、index.js、member-detail.js
## 三、已处理项
- **vip 相关接口**:已改为 `/api/miniprogram/vip/*`members、status、profile符合项目边界。**后端需在 soul-api 的 miniprogram 组下挂对应路由**,可复用现有 handler。
- **页面结构**:保留 vip、member-detail未引入 scan、profile-edit
## 四、后端已补全soul-api
已在 miniprogram 组下新增以下路由:
| 路径 | 方法 | 用途 |
|------|------|------|
| `/api/miniprogram/vip/status` | GET | 查询用户 VIP 状态(按 fullbook/vip 订单判断) |
| `/api/miniprogram/vip/profile` | GET/POST | 获取/更新 VIP 资料(映射 users 表 nickname/phone |
| `/api/miniprogram/vip/members` | GET | VIP 会员列表(无 id或单个?id= |
| `/api/miniprogram/users` | GET | 用户列表(?limit=)或单个(?id=),首页超级个体、会员详情回退 |
## 五、后续建议
1. **soul-api 路由**:确认 `/api/miniprogram/book/all-chapters` 已注册VIP 接口见「四、后端待办」。
2. **referral.js**:已统一使用 `app.getMyReferralCode()` 作为回退。
3. **read.js mid 支持**:已完成。若 soul-api 未提供 `/api/miniprogram/book/chapter/by-mid/:mid`,扫码带 mid 时需依赖 bookData 已加载完成;建议后端补充 by-mid 接口。

594
miniprogram/app.js Normal file
View File

@@ -0,0 +1,594 @@
/**
* Soul创业派对 - 小程序入口
* 开发: 卡若
*/
const { parseScene } = require('./utils/scene.js')
App({
globalData: {
// API基础地址 - 连接真实后端
baseUrl: 'https://soulapi.quwanzhi.com',
// baseUrl: 'https://souldev.quwanzhi.com',
// baseUrl: 'http://localhost:8080',
// 小程序配置 - 真实AppID
appId: 'wxb8bbb2b10dec74aa',
// 订阅消息:用户点击「申请提现」→「立即提现」时会先弹出订阅授权窗
withdrawSubscribeTmplId: 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE',
// 微信支付配置
mchId: '1318592501', // 商户号
// 用户信息
userInfo: null,
openId: null, // 微信openId支付必需
isLoggedIn: false,
// 书籍数据
bookData: null,
totalSections: 62,
// 购买记录
purchasedSections: [],
hasFullBook: false,
// 已读章节(仅统计有权限打开过的章节,用于首页「已读/待读」)
readSectionIds: [],
// 推荐绑定
pendingReferralCode: null, // 待绑定的推荐码
// 主题配置
theme: {
brandColor: '#00CED1',
brandSecondary: '#20B2AA',
goldColor: '#FFD700',
bgColor: '#000000',
cardBg: '#1c1c1e'
},
// 系统信息
systemInfo: null,
statusBarHeight: 44,
navBarHeight: 88,
// TabBar相关
currentTab: 0,
// 更新检测:上次检测时间戳,避免频繁请求
lastUpdateCheck: 0
},
onLaunch(options) {
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
// 获取系统信息
this.getSystemInfo()
// 检查登录状态
this.checkLoginStatus()
// 加载书籍数据
this.loadBookData()
// 检查更新
this.checkUpdate()
// 处理分享参数(推荐码绑定)
this.handleReferralCode(options)
},
// 小程序显示时:处理分享参数、检测更新(从后台切回时)
onShow(options) {
this.handleReferralCode(options)
this.checkUpdate()
},
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref与 utils/scene 解析闭环
handleReferralCode(options) {
const query = options?.query || {}
let refCode = query.ref || query.referralCode
const sceneStr = (options && (typeof options.scene === 'string' ? options.scene : '')) || ''
if (sceneStr) {
const parsed = parseScene(sceneStr)
if (parsed.mid) this.globalData.initialSectionMid = parsed.mid
if (parsed.id) this.globalData.initialSectionId = parsed.id
if (parsed.ref) refCode = parsed.ref
}
if (refCode) {
console.log('[App] 检测到推荐码:', refCode)
// 立即记录访问(不需要登录,用于统计"通过链接进的人数"
this.recordReferralVisit(refCode)
// 保存待绑定的推荐码(不再在前端做"只能绑定一次"的限制让后端根据30天规则判断续期/抢夺)
this.globalData.pendingReferralCode = refCode
wx.setStorageSync('pendingReferralCode', refCode)
// 同步写入 referral_code供章节/找伙伴支付时传给后端,订单会记录 referrer_id 与 referral_code
wx.setStorageSync('referral_code', refCode)
// 如果已登录,立即尝试绑定,由 /api/miniprogram/referral/bind 按 30 天规则决定 new / renew / takeover
if (this.globalData.isLoggedIn && this.globalData.userInfo) {
this.bindReferralCode(refCode)
}
}
},
// 记录推荐访问(不需要登录,用于统计)
async recordReferralVisit(refCode) {
try {
// 获取openId如果有
const openId = this.globalData.openId || wx.getStorageSync('openId') || ''
const userId = this.globalData.userInfo?.id || ''
await this.request('/api/miniprogram/referral/visit', {
method: 'POST',
data: {
referralCode: refCode,
visitorOpenId: openId,
visitorId: userId,
source: 'miniprogram',
page: getCurrentPages()[getCurrentPages().length - 1]?.route || ''
},
silent: true
})
console.log('[App] 记录推荐访问成功')
} catch (e) {
console.log('[App] 记录推荐访问失败:', e.message)
// 忽略错误,不影响用户体验
}
},
// 绑定推荐码到用户
async bindReferralCode(refCode) {
try {
const userId = this.globalData.userInfo?.id
if (!userId || !refCode) return
console.log('[App] 绑定推荐码:', refCode, '到用户:', userId)
// 调用API绑定推荐关系
const res = await this.request('/api/miniprogram/referral/bind', {
method: 'POST',
data: {
userId,
referralCode: refCode
},
silent: true
})
if (res.success) {
console.log('[App] 推荐码绑定成功')
// 仅记录当前已绑定的推荐码,用于展示/调试是否允许更换由后端根据30天规则判断
wx.setStorageSync('boundReferralCode', refCode)
this.globalData.pendingReferralCode = null
wx.removeStorageSync('pendingReferralCode')
}
} catch (e) {
console.error('[App] 绑定推荐码失败:', e)
}
},
// 根据业务 id 从 bookData 查 mid用于跳转
getSectionMid(sectionId) {
const list = this.globalData.bookData || []
const ch = list.find(c => c.id === sectionId)
return ch?.mid || 0
},
// 获取当前用户的邀请码(用于分享带 ref未登录返回空字符串
getMyReferralCode() {
const user = this.globalData.userInfo
if (!user) return ''
if (user.referralCode) return user.referralCode
if (user.id) return 'SOUL' + String(user.id).toUpperCase().slice(-6)
return ''
},
/**
* 自定义导航栏「返回」:有上一页则返回,否则跳转首页(解决从分享进入时点返回无效的问题)
*/
goBackOrToHome() {
const pages = getCurrentPages()
if (pages.length <= 1) {
wx.switchTab({ url: '/pages/index/index' })
} else {
wx.navigateBack()
}
},
// 获取系统信息
getSystemInfo() {
try {
const systemInfo = wx.getSystemInfoSync()
this.globalData.systemInfo = systemInfo
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 44
// 计算导航栏高度
const menuButton = wx.getMenuButtonBoundingClientRect()
if (menuButton) {
this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height + systemInfo.statusBarHeight
}
} catch (e) {
console.error('获取系统信息失败:', e)
}
},
// 检查登录状态
checkLoginStatus() {
try {
const userInfo = wx.getStorageSync('userInfo')
const token = wx.getStorageSync('token')
if (userInfo && token) {
this.globalData.userInfo = userInfo
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = userInfo.purchasedSections || []
this.globalData.hasFullBook = userInfo.hasFullBook || false
}
} catch (e) {
console.error('检查登录状态失败:', e)
}
},
// 加载书籍数据
async loadBookData() {
try {
// 先从缓存加载
const cachedData = wx.getStorageSync('bookData')
if (cachedData) {
this.globalData.bookData = cachedData
}
// 从服务器获取最新数据
const res = await this.request('/api/miniprogram/book/all-chapters')
if (res && (res.data || res.chapters)) {
const chapters = res.data || res.chapters || []
this.globalData.bookData = chapters
wx.setStorageSync('bookData', chapters)
}
} catch (e) {
console.error('加载书籍数据失败:', e)
}
},
/**
* 小程序更新检测(基于 wx.getUpdateManager
* - 启动时检测;从后台切回前台时也检测(间隔至少 5 分钟,避免频繁请求)
*/
checkUpdate() {
try {
if (!wx.canIUse('getUpdateManager')) return
const now = Date.now()
const lastCheck = this.globalData.lastUpdateCheck || 0
if (lastCheck && now - lastCheck < 5 * 60 * 1000) return // 5 分钟内不重复检测
this.globalData.lastUpdateCheck = now
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate((res) => {
if (res.hasUpdate) {
console.log('[App] 发现新版本,正在下载...')
}
})
updateManager.onUpdateReady(() => {
wx.showModal({
title: '更新提示',
content: '新版本已准备好,重启后即可使用',
confirmText: '立即重启',
cancelText: '稍后',
success: (res) => {
if (res.confirm) {
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(() => {
wx.showToast({
title: '更新失败,请稍后重试',
icon: 'none',
duration: 2500
})
})
} catch (e) {
console.warn('[App] checkUpdate failed:', e)
}
},
/**
* 从 soul-api 返回体中取错误提示文案(兼容 message / error 字段)
*/
_getApiErrorMsg(data, defaultMsg = '请求失败') {
if (!data || typeof data !== 'object') return defaultMsg
const msg = data.message || data.error
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
},
/**
* 统一请求方法。接口失败时会弹窗提示(与 soul-api 返回的 message/error 一致)。
* @param {string|object} urlOrOptions - 接口路径,或 { url, method, data, header, silent }
* @param {object} options - { method, data, header, silent }
* @param {boolean} options.silent - 为 true 时不弹窗,仅 reject用于静默请求如访问统计
*/
request(urlOrOptions, options = {}) {
let url
if (typeof urlOrOptions === 'string') {
url = urlOrOptions
} else if (urlOrOptions && typeof urlOrOptions === 'object' && urlOrOptions.url) {
url = urlOrOptions.url
options = { ...urlOrOptions, url: undefined }
} else {
url = ''
}
const silent = !!options.silent
const showError = (msg) => {
if (!silent && msg) {
wx.showToast({ title: msg, icon: 'none', duration: 2500 })
}
}
return new Promise((resolve, reject) => {
const token = wx.getStorageSync('token')
wx.request({
url: this.globalData.baseUrl + url,
method: options.method || 'GET',
data: options.data || {},
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...options.header
},
success: (res) => {
const data = res.data
if (res.statusCode === 200) {
// 业务失败success === falsesoul-api 用 message 或 error 返回原因
if (data && data.success === false) {
const msg = this._getApiErrorMsg(data, '操作失败')
showError(msg)
reject(new Error(msg))
return
}
resolve(data)
return
}
if (res.statusCode === 401) {
this.logout()
showError('未授权,请重新登录')
reject(new Error('未授权'))
return
}
// 4xx/5xx优先用返回体的 message/error
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
showError(msg)
reject(new Error(msg))
},
fail: (err) => {
const msg = (err && err.errMsg) ? (err.errMsg.indexOf('timeout') !== -1 ? '请求超时,请重试' : '网络异常,请重试') : '网络异常,请重试'
showError(msg)
reject(new Error(msg))
}
})
})
},
// 登录方法 - 获取openId用于支付加固错误处理避免审核报“登录报错”
async login() {
try {
const loginRes = await new Promise((resolve, reject) => {
wx.login({ success: resolve, fail: reject })
})
if (!loginRes || !loginRes.code) {
console.warn('[App] wx.login 未返回 code')
wx.showToast({ title: '获取登录态失败,请重试', icon: 'none' })
return null
}
try {
const res = await this.request('/api/miniprogram/login', {
method: 'POST',
data: { code: loginRes.code }
})
if (res.success && res.data) {
// 保存openId
if (res.data.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
console.log('[App] 获取openId成功')
}
// 保存用户信息
if (res.data.user) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token || '')
// 登录成功后,检查待绑定的推荐码并执行绑定
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
if (pendingRef) {
console.log('[App] 登录后自动绑定推荐码:', pendingRef)
this.bindReferralCode(pendingRef)
}
}
return res.data
}
} catch (apiError) {
console.log('[App] API登录失败:', apiError.message)
// 不使用模拟登录,提示用户网络问题
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
return null
}
return null
} catch (e) {
console.error('[App] 登录失败:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
return null
}
},
// 获取openId (支付必需)
async getOpenId() {
// 先检查缓存
const cachedOpenId = wx.getStorageSync('openId')
if (cachedOpenId) {
this.globalData.openId = cachedOpenId
return cachedOpenId
}
// 没有缓存则登录获取
try {
const loginRes = await new Promise((resolve, reject) => {
wx.login({ success: resolve, fail: reject })
})
const res = await this.request('/api/miniprogram/login', {
method: 'POST',
data: { code: loginRes.code }
})
if (res.success && res.data?.openId) {
this.globalData.openId = res.data.openId
wx.setStorageSync('openId', res.data.openId)
// 接口同时返回 user 时视为登录,补全登录态并从登录开始绑定推荐码
if (res.data.user) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token || '')
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
if (pendingRef) {
console.log('[App] getOpenId 登录后自动绑定推荐码:', pendingRef)
this.bindReferralCode(pendingRef)
}
}
return res.data.openId
}
} catch (e) {
console.error('[App] 获取openId失败:', e)
}
return null
},
// 模拟登录已废弃 - 不再使用
// 现在必须使用真实的微信登录获取openId作为唯一标识
mockLogin() {
console.warn('[App] mockLogin已废弃请使用真实登录')
return null
},
// 手机号登录:需同时传 wx.login 的 code 与 getPhoneNumber 的 phoneCode
async loginWithPhone(phoneCode) {
try {
const loginRes = await new Promise((resolve, reject) => {
wx.login({ success: resolve, fail: reject })
})
if (!loginRes.code) {
wx.showToast({ title: '获取登录态失败', icon: 'none' })
return null
}
const res = await this.request('/api/miniprogram/phone-login', {
method: 'POST',
data: { code: loginRes.code, phoneCode }
})
if (res.success && res.data) {
this.globalData.userInfo = res.data.user
this.globalData.isLoggedIn = true
this.globalData.purchasedSections = res.data.user.purchasedSections || []
this.globalData.hasFullBook = res.data.user.hasFullBook || false
wx.setStorageSync('userInfo', res.data.user)
wx.setStorageSync('token', res.data.token)
// 登录成功后绑定推荐码
const pendingRef = wx.getStorageSync('pendingReferralCode') || this.globalData.pendingReferralCode
if (pendingRef) {
console.log('[App] 手机号登录后自动绑定推荐码:', pendingRef)
this.bindReferralCode(pendingRef)
}
return res.data
}
} catch (e) {
console.log('[App] 手机号登录失败:', e)
wx.showToast({ title: '登录失败,请重试', icon: 'none' })
}
return null
},
// 退出登录
logout() {
this.globalData.userInfo = null
this.globalData.isLoggedIn = false
this.globalData.purchasedSections = []
this.globalData.hasFullBook = false
wx.removeStorageSync('userInfo')
wx.removeStorageSync('token')
},
// 检查是否已购买章节
hasPurchased(sectionId) {
if (this.globalData.hasFullBook) return true
return this.globalData.purchasedSections.includes(sectionId)
},
// 标记章节为已读(仅在有权限打开时由阅读页调用,用于首页已读/待读统计)
markSectionAsRead(sectionId) {
if (!sectionId) return
const list = this.globalData.readSectionIds || []
if (list.includes(sectionId)) return
list.push(sectionId)
this.globalData.readSectionIds = list
wx.setStorageSync('readSectionIds', list)
},
// 已读章节数(用于首页展示)
getReadCount() {
return (this.globalData.readSectionIds || []).length
},
// 获取章节总数
getTotalSections() {
return this.globalData.totalSections
},
// 切换TabBar
switchTab(index) {
this.globalData.currentTab = index
},
// 显示Toast
showToast(title, icon = 'none') {
wx.showToast({
title,
icon,
duration: 2000
})
},
// 显示Loading
showLoading(title = '加载中...') {
wx.showLoading({
title,
mask: true
})
},
// 隐藏Loading
hideLoading() {
wx.hideLoading()
}
})

59
miniprogram/app.json Normal file
View File

@@ -0,0 +1,59 @@
{
"pages": [
"pages/chapters/chapters",
"pages/index/index",
"pages/match/match",
"pages/my/my",
"pages/read/read",
"pages/about/about",
"pages/agreement/agreement",
"pages/privacy/privacy",
"pages/referral/referral",
"pages/purchases/purchases",
"pages/settings/settings",
"pages/search/search",
"pages/addresses/addresses",
"pages/addresses/edit",
"pages/withdraw-records/withdraw-records",
"pages/vip/vip",
"pages/member-detail/member-detail","pages/mentors/mentors","pages/mentor-detail/mentor-detail","pages/profile-show/profile-show","pages/profile-edit/profile-edit"
],
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#000000",
"navigationBarTitleText": "Soul创业派对",
"navigationBarTextStyle": "white",
"backgroundColor": "#000000",
"navigationStyle": "custom"
},
"tabBar": {
"custom": true,
"color": "#8e8e93",
"selectedColor": "#00CED1",
"backgroundColor": "#1c1c1e",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页"
},
{
"pagePath": "pages/chapters/chapters",
"text": "目录"
},
{
"pagePath": "pages/match/match",
"text": "找伙伴"
},
{
"pagePath": "pages/my/my",
"text": "我的"
}
]
},
"usingComponents": {},
"__usePrivacyCheck__": true,
"lazyCodeLoading": "requiredComponents",
"style": "v2",
"sitemapLocation": "sitemap.json"
}

606
miniprogram/app.wxss Normal file
View File

@@ -0,0 +1,606 @@
/**
* Soul创业实验 - 全局样式
* 主题色: #00CED1 (Soul青色)
* 开发: 卡若
*/
/* ===== CSS 变量系统 ===== */
page {
/* 品牌色 */
--app-brand: #00CED1;
--app-brand-light: rgba(0, 206, 209, 0.1);
--app-brand-dark: #20B2AA;
/* 背景色 */
--app-bg-primary: #000000;
--app-bg-secondary: #1c1c1e;
--app-bg-tertiary: #2c2c2e;
/* 文字色 */
--app-text-primary: #ffffff;
--app-text-secondary: rgba(255, 255, 255, 0.7);
--app-text-tertiary: rgba(255, 255, 255, 0.4);
/* 分隔线 */
--app-separator: rgba(255, 255, 255, 0.05);
/* iOS 系统色 */
--ios-indigo: #5856D6;
--ios-green: #30d158;
--ios-red: #FF3B30;
--ios-orange: #FF9500;
--ios-yellow: #FFD700;
/* 金色 */
--gold: #FFD700;
--gold-light: #FFA500;
/* 粉色 */
--pink: #E91E63;
/* 紫色 */
--purple: #7B61FF;
}
/* ===== 页面基础样式 ===== */
page {
background-color: var(--app-bg-primary);
color: var(--app-text-primary);
font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Display', 'SF Pro Text', 'Helvetica Neue', 'PingFang SC', 'Microsoft YaHei', sans-serif;
font-size: 28rpx;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/* ===== 全局容器 ===== */
.container {
min-height: 100vh;
padding: 0;
background: #000000;
padding-bottom: env(safe-area-inset-bottom);
}
/* ===== 品牌色系 ===== */
.brand-color {
color: #00CED1;
}
.brand-bg {
background-color: #00CED1;
}
.brand-gradient {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
}
.gold-color {
color: #FFD700;
}
.gold-bg {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
}
/* ===== 文字渐变 ===== */
.gradient-text {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.gold-gradient-text {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
/* ===== 按钮样式 ===== */
.btn-primary {
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
color: #ffffff;
border: none;
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(0, 206, 209, 0.3);
display: flex;
align-items: center;
justify-content: center;
}
.btn-primary::after {
border: none;
}
.btn-primary:active {
opacity: 0.85;
transform: scale(0.98);
}
.btn-secondary {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
border: 2rpx solid rgba(0, 206, 209, 0.3);
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 500;
}
.btn-secondary::after {
border: none;
}
.btn-secondary:active {
background: rgba(0, 206, 209, 0.2);
}
.btn-ghost {
background: rgba(255, 255, 255, 0.05);
color: #ffffff;
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
}
.btn-ghost::after {
border: none;
}
.btn-ghost:active {
background: rgba(255, 255, 255, 0.1);
}
.btn-gold {
background: linear-gradient(135deg, #FFD700 0%, #FFA500 100%);
color: #000000;
border: none;
border-radius: 48rpx;
padding: 28rpx 48rpx;
font-size: 32rpx;
font-weight: 600;
box-shadow: 0 8rpx 24rpx rgba(255, 215, 0, 0.3);
}
.btn-gold::after {
border: none;
}
/* ===== 卡片样式 ===== */
.card {
background: rgba(28, 28, 30, 0.9);
border-radius: 32rpx;
padding: 32rpx;
margin: 24rpx 32rpx;
border: 2rpx solid rgba(255, 255, 255, 0.05);
}
.card-light {
background: rgba(44, 44, 46, 0.8);
border-radius: 24rpx;
padding: 24rpx;
border: 2rpx solid rgba(255, 255, 255, 0.08);
}
.card-gradient {
background: linear-gradient(135deg, rgba(28, 28, 30, 1) 0%, rgba(44, 44, 46, 1) 100%);
border-radius: 32rpx;
padding: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
.card-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, rgba(32, 178, 170, 0.05) 100%);
border-radius: 32rpx;
padding: 32rpx;
border: 2rpx solid rgba(0, 206, 209, 0.2);
}
/* ===== 输入框样式 ===== */
.input-ios {
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid rgba(255, 255, 255, 0.1);
border-radius: 24rpx;
padding: 28rpx 32rpx;
font-size: 32rpx;
color: #ffffff;
}
.input-ios:focus {
border-color: rgba(0, 206, 209, 0.5);
}
.input-ios-placeholder {
color: rgba(255, 255, 255, 0.3);
}
/* ===== 列表项样式 ===== */
.list-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
background: rgba(28, 28, 30, 0.9);
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
}
.list-item:first-child {
border-radius: 24rpx 24rpx 0 0;
}
.list-item:last-child {
border-radius: 0 0 24rpx 24rpx;
border-bottom: none;
}
.list-item:only-child {
border-radius: 24rpx;
}
.list-item:active {
background: rgba(44, 44, 46, 1);
}
/* ===== 标签样式 ===== */
.tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8rpx 20rpx;
min-width: 80rpx;
border-radius: 8rpx;
font-size: 22rpx;
font-weight: 500;
box-sizing: border-box;
text-align: center;
}
.tag-brand {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
.tag-gold {
background: rgba(255, 215, 0, 0.1);
color: #FFD700;
}
.tag-pink {
background: rgba(233, 30, 99, 0.1);
color: #E91E63;
}
.tag-purple {
background: rgba(123, 97, 255, 0.1);
color: #7B61FF;
}
.tag-free {
background: rgba(0, 206, 209, 0.1);
color: #00CED1;
}
/* ===== 分隔线 ===== */
.divider {
height: 1rpx;
background: rgba(255, 255, 255, 0.05);
margin: 24rpx 0;
}
.divider-vertical {
width: 2rpx;
height: 48rpx;
background: rgba(255, 255, 255, 0.1);
}
/* ===== 骨架屏动画 ===== */
.skeleton {
background: linear-gradient(90deg,
rgba(28, 28, 30, 1) 25%,
rgba(44, 44, 46, 1) 50%,
rgba(28, 28, 30, 1) 75%
);
background-size: 200% 100%;
animation: skeleton-loading 1.5s ease-in-out infinite;
border-radius: 8rpx;
}
@keyframes skeleton-loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* ===== 页面过渡动画 ===== */
.page-transition {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(20rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ===== 弹窗动画 ===== */
.modal-overlay {
animation: modalOverlayIn 0.25s ease-out;
}
.modal-content {
animation: modalContentIn 0.3s cubic-bezier(0.32, 0.72, 0, 1);
}
@keyframes modalOverlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes modalContentIn {
from {
opacity: 0;
transform: scale(0.95) translateY(20rpx);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
/* ===== 脉动动画 ===== */
.pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(1.05);
opacity: 0.8;
}
}
/* ===== 发光效果 ===== */
.glow {
box-shadow: 0 0 40rpx rgba(0, 206, 209, 0.3);
}
.glow-gold {
box-shadow: 0 0 40rpx rgba(255, 215, 0, 0.3);
}
/* ===== 文字样式 ===== */
.text-xs {
font-size: 22rpx;
}
.text-sm {
font-size: 26rpx;
}
.text-base {
font-size: 28rpx;
}
.text-lg {
font-size: 32rpx;
}
.text-xl {
font-size: 36rpx;
}
.text-2xl {
font-size: 44rpx;
}
.text-3xl {
font-size: 56rpx;
}
.text-white {
color: #ffffff;
}
.text-gray {
color: rgba(255, 255, 255, 0.6);
}
.text-muted {
color: rgba(255, 255, 255, 0.4);
}
.text-center {
text-align: center;
}
.font-medium {
font-weight: 500;
}
.font-semibold {
font-weight: 600;
}
.font-bold {
font-weight: 700;
}
/* ===== Flex布局 ===== */
.flex {
display: flex;
}
.flex-col {
flex-direction: column;
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.justify-between {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.flex-1 {
flex: 1;
}
.gap-1 {
gap: 8rpx;
}
.gap-2 {
gap: 16rpx;
}
.gap-3 {
gap: 24rpx;
}
.gap-4 {
gap: 32rpx;
}
/* ===== 间距 ===== */
.p-2 { padding: 16rpx; }
.p-3 { padding: 24rpx; }
.p-4 { padding: 32rpx; }
.p-5 { padding: 40rpx; }
.px-4 { padding-left: 32rpx; padding-right: 32rpx; }
.py-2 { padding-top: 16rpx; padding-bottom: 16rpx; }
.py-3 { padding-top: 24rpx; padding-bottom: 24rpx; }
.m-4 { margin: 32rpx; }
.mx-4 { margin-left: 32rpx; margin-right: 32rpx; }
.my-3 { margin-top: 24rpx; margin-bottom: 24rpx; }
.mb-2 { margin-bottom: 16rpx; }
.mb-3 { margin-bottom: 24rpx; }
.mb-4 { margin-bottom: 32rpx; }
.mt-4 { margin-top: 32rpx; }
/* ===== 圆角 ===== */
.rounded { border-radius: 8rpx; }
.rounded-lg { border-radius: 16rpx; }
.rounded-xl { border-radius: 24rpx; }
.rounded-2xl { border-radius: 32rpx; }
.rounded-full { border-radius: 50%; }
/* ===== 安全区域 ===== */
.safe-bottom {
padding-bottom: calc(env(safe-area-inset-bottom) + 20rpx);
}
.pb-tabbar {
padding-bottom: 200rpx;
}
/* ===== 头部导航占位 ===== */
.nav-placeholder {
height: calc(88rpx + env(safe-area-inset-top, 44rpx));
}
/* ===== 隐藏滚动条 ===== */
::-webkit-scrollbar {
display: none;
width: 0;
height: 0;
}
/* ===== 触摸反馈 ===== */
.touch-feedback {
transition: all 0.15s ease;
}
.touch-feedback:active {
opacity: 0.7;
transform: scale(0.98);
}
/* ===== 进度条 ===== */
.progress-bar {
height: 8rpx;
background: rgba(44, 44, 46, 1);
border-radius: 4rpx;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, #00CED1 0%, #20B2AA 100%);
border-radius: 4rpx;
transition: width 0.3s ease;
}
/* ===== 头像样式 ===== */
.avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
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;
color: #00CED1;
font-weight: 700;
font-size: 32rpx;
border: 4rpx solid rgba(0, 206, 209, 0.3);
}
.avatar-lg {
width: 120rpx;
height: 120rpx;
font-size: 48rpx;
}
/* ===== 图标容器 ===== */
.icon-box {
width: 64rpx;
height: 64rpx;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
}
.icon-box-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.2) 0%, rgba(32, 178, 170, 0.1) 100%);
}
.icon-box-gold {
background: linear-gradient(135deg, rgba(255, 215, 0, 0.2) 0%, rgba(255, 165, 0, 0.1) 100%);
}
/* ===== 渐变背景 ===== */
.bg-gradient-dark {
background: linear-gradient(180deg, #000000 0%, #1a1a1a 100%);
}
.bg-gradient-brand {
background: linear-gradient(135deg, rgba(0, 206, 209, 0.1) 0%, transparent 100%);
}

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="8" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="16" x2="12.01" y2="16" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 459 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 12h14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m12 5 7 7-7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 303 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 8a6 6 0 0 1 12 0c0 7 3 9 3 9H3s3-2 3-9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.3 21a1.94 1.94 0 0 0 3.4 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8l4 4-4 4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 463 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8v8M9 11l3-3 3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 354 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 364 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="m15 18-6-6 6-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="12 6 12 12 16 14" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 317 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="12 6 12 12 16 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 327 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 385 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.576 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<path d="M14.084 14.158a3 3 0 1 1-4.242-4.242" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-4.444" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
<line x1="2" y1="2" x2="22" y2="22" stroke="#94A3B8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" opacity="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 767 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 392 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 402 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 251 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 256 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="8" width="18" height="4" rx="1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 8v13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19 12v7a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 8a2.5 2.5 0 0 1 0-5A4.8 8 0 0 1 12 8a4.8 8 0 0 1 4.5-5 2.5 2.5 0 0 1 0 5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 699 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 611 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="9 22 9 12 15 12 15 22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 358 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="9" r="2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 485 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="16" x2="12" y2="12" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="8" x2="12.01" y2="8" stroke="#3B82F6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

View File

@@ -0,0 +1,5 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="16" x2="12" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="12" y1="8" x2="12.01" y2="8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 458 B

View File

@@ -0,0 +1,8 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<line x1="8" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="8" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="8" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="3" y1="6" x2="3.01" y2="6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="3" y1="12" x2="3.01" y2="12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="3" y1="18" x2="3.01" y2="18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 844 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

View File

@@ -0,0 +1,3 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 304 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 907 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 725 B

View File

@@ -0,0 +1,18 @@
<svg viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<!-- 两个人的头:完全分开,中间留空隙 -->
<!-- 左侧头部 -->
<circle cx="16" cy="18" r="7" fill="white" />
<!-- 右侧头部 -->
<circle cx="32" cy="18" r="7" fill="white" />
<!-- 左侧身体:单独一块,和右侧之间留出明显空隙 -->
<path
d="M10 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H10v-4z"
fill="white"
/>
<!-- 右侧身体:单独一块,和左侧之间留出明显空隙 -->
<path
d="M26 34c0-4 2.5-7 6-7h0c3 0 5.5 3 5.5 7v4H26v-4z"
fill="white"
/>
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 855 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 865 B

View File

@@ -0,0 +1,7 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="18" cy="5" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="6" cy="12" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="18" cy="19" r="3" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 680 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M20 2v4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M22 4h-4" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="4" cy="20" r="2" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 751 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="#9CA3AF" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 331 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="12" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 341 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="7" r="4" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="#4FD1C5" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 573 B

View File

@@ -0,0 +1,6 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="9" cy="7" r="4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16 3.13a4 4 0 0 1 0 7.75" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 593 B

View File

@@ -0,0 +1,4 @@
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19 7V4a1 1 0 0 0-1-1H5a2 2 0 0 0 0 4h15a1 1 0 0 1 1 1v4h-3a2 2 0 0 0 0 4h3a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 5v14a2 2 0 0 0 2 2h15a1 1 0 0 0 1-1v-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 429 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

View File

@@ -0,0 +1,175 @@
# Icon 图标组件
SVG 图标组件,参考 lucide-react 实现,用于在小程序中使用矢量图标。
**技术实现**: 使用 Base64 编码的 SVG + image 组件(小程序不支持直接使用 SVG 标签)
---
## 使用方法
### 1. 在页面 JSON 中引入组件
```json
{
"usingComponents": {
"icon": "/components/icon/icon"
}
}
```
### 2. 在 WXML 中使用
```xml
<!-- 基础用法 -->
<icon name="share" size="48" color="#00CED1"></icon>
<!-- 分享图标 -->
<icon name="share" size="40" color="#ffffff"></icon>
<!-- 箭头图标 -->
<icon name="arrow-up-right" size="32" color="#00CED1"></icon>
<!-- 搜索图标 -->
<icon name="search" size="44" color="#ffffff"></icon>
<!-- 返回图标 -->
<icon name="chevron-left" size="48" color="#ffffff"></icon>
<!-- 心形图标 -->
<icon name="heart" size="40" color="#E91E63"></icon>
```
---
## 属性说明
| 属性 | 类型 | 默认值 | 说明 |
|-----|------|--------|-----|
| name | String | 'share' | 图标名称 |
| size | Number | 48 | 图标大小rpx |
| color | String | 'currentColor' | 图标颜色 |
| customClass | String | '' | 自定义类名 |
| customStyle | String | '' | 自定义样式 |
---
## 可用图标
| 图标名称 | 说明 | 对应 lucide-react |
|---------|------|-------------------|
| `share` | 分享 | `<Share2>` |
| `arrow-up-right` | 右上箭头 | `<ArrowUpRight>` |
| `chevron-left` | 左箭头 | `<ChevronLeft>` |
| `search` | 搜索 | `<Search>` |
| `heart` | 心形 | `<Heart>` |
---
## 添加新图标
`icon.js``getSvgPath` 方法中添加新图标:
```javascript
getSvgPath(name) {
const svgMap = {
'new-icon': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><!-- SVG path 数据 --></svg>',
// ... 其他图标
}
return svgMap[name] || ''
}
```
**获取 SVG 代码**: 访问 [lucide.dev](https://lucide.dev) 搜索图标,复制 SVG 内容。
**注意**: 颜色使用 `COLOR` 占位符,组件会自动替换。
---
## 样式定制
### 1. 使用 customClass
```xml
<icon name="share" size="48" color="#00CED1" customClass="my-icon-class"></icon>
```
```css
.my-icon-class {
opacity: 0.8;
}
```
### 2. 使用 customStyle
```xml
<icon name="share" size="48" color="#ffffff" customStyle="opacity: 0.8; margin-right: 10rpx;"></icon>
```
---
## 技术说明
### 为什么使用 Base64 + image
1. **矢量图标**:任意缩放不失真
2. **灵活着色**:通过 `COLOR` 占位符动态改变颜色
3. **轻量级**:无需加载字体文件或外部图片
4. **兼容性**:小程序不支持直接使用 SVG 标签image 组件支持 Base64 SVG
### 为什么不用字体图标?
小程序对字体文件有限制Base64 编码字体文件会增加包体积SVG 图标更轻量。
### 与 lucide-react 的对应关系
- **lucide-react**: React 组件库,使用 SVG
- **本组件**: 小程序自定义组件,也使用 SVG
- **SVG path 数据**: 完全相同,从 lucide 官网复制
---
## 示例
### 悬浮分享按钮
```xml
<button class="fab-share" open-type="share">
<icon name="share" size="48" color="#ffffff"></icon>
</button>
```
```css
.fab-share {
position: fixed;
right: 32rpx;
bottom: calc(120rpx + env(safe-area-inset-bottom));
width: 96rpx;
height: 96rpx;
border-radius: 50%;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
}
```
---
## 扩展图标库
可以继续添加更多 lucide-react 图标:
- `star` - 星星
- `wallet` - 钱包
- `gift` - 礼物
- `info` - 信息
- `settings` - 设置
- `user` - 用户
- `book-open` - 打开的书
- `eye` - 眼睛
- `clock` - 时钟
- `users` - 用户组
---
**图标组件创建完成!** 🎉

View File

@@ -0,0 +1,83 @@
// components/icon/icon.js
Component({
properties: {
// 图标名称
name: {
type: String,
value: 'share',
observer: 'updateIcon'
},
// 图标大小rpx
size: {
type: Number,
value: 48
},
// 图标颜色
color: {
type: String,
value: '#ffffff',
observer: 'updateIcon'
},
// 自定义类名
customClass: {
type: String,
value: ''
},
// 自定义样式
customStyle: {
type: String,
value: ''
}
},
data: {
svgData: ''
},
lifetimes: {
attached() {
this.updateIcon()
}
},
methods: {
// SVG 图标数据映射
getSvgPath(name) {
const svgMap = {
'share': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>',
'arrow-up-right': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/></svg>',
'chevron-left': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>',
'search': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
'heart': '<svg viewBox="0 0 24 24" fill="none" stroke="COLOR" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/></svg>'
}
return svgMap[name] || ''
},
// 更新图标
updateIcon() {
const { name, color } = this.data
let svgString = this.getSvgPath(name)
if (svgString) {
// 替换颜色占位符
svgString = svgString.replace(/COLOR/g, color)
// 转换为 Base64 Data URL
const svgData = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgString)}`
this.setData({
svgData: svgData
})
} else {
this.setData({
svgData: ''
})
}
}
}
})

View File

@@ -0,0 +1,4 @@
{
"component": true,
"usingComponents": {}
}

View File

@@ -0,0 +1,5 @@
<!-- components/icon/icon.wxml -->
<view class="icon icon-{{name}} {{customClass}}" style="width: {{size}}rpx; height: {{size}}rpx; {{customStyle}}">
<image wx:if="{{svgData}}" class="icon-image" src="{{svgData}}" mode="aspectFit" style="width: {{size}}rpx; height: {{size}}rpx;" />
<text wx:else class="icon-text">{{name}}</text>
</view>

View File

@@ -0,0 +1,18 @@
/* components/icon/icon.wxss */
.icon {
display: inline-flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon-image {
display: block;
width: 100%;
height: 100%;
}
.icon-text {
font-size: 24rpx;
color: currentColor;
}

View File

@@ -0,0 +1,153 @@
/**
* Soul创业实验 - 自定义TabBar组件
* 根据后台配置动态显示/隐藏"找伙伴"按钮
*/
console.log('[TabBar] ===== 组件文件开始加载 =====')
const app = getApp()
console.log('[TabBar] App 对象:', app)
Component({
data: {
selected: 0,
color: '#8e8e93',
selectedColor: '#00CED1',
matchEnabled: false, // 找伙伴功能开关,默认关闭
list: [
{
pagePath: '/pages/index/index',
text: '首页',
iconType: 'home'
},
{
pagePath: '/pages/chapters/chapters',
text: '目录',
iconType: 'list'
},
{
pagePath: '/pages/match/match',
text: '找伙伴',
iconType: 'match',
isSpecial: true
},
{
pagePath: '/pages/my/my',
text: '我的',
iconType: 'user'
}
]
},
lifetimes: {
attached() {
console.log('[TabBar] Component attached 生命周期触发')
this.loadFeatureConfig()
},
ready() {
console.log('[TabBar] Component ready 生命周期触发')
// 如果 attached 中没有成功加载,在 ready 中再次尝试
if (this.data.matchEnabled === undefined || this.data.matchEnabled === null) {
console.log('[TabBar] 在 ready 中重新加载配置')
this.loadFeatureConfig()
}
}
},
// 页面加载时也调用(兼容性更好)
attached() {
console.log('[TabBar] attached() 方法触发')
this.loadFeatureConfig()
},
methods: {
// 加载功能配置
async loadFeatureConfig() {
try {
console.log('[TabBar] 开始加载功能配置...')
console.log('[TabBar] API地址:', app.globalData.baseUrl + '/api/miniprogram/config')
// app.request 的第一个参数是 url 字符串,第二个参数是 options 对象
const res = await app.request('/api/miniprogram/config', {
method: 'GET'
})
// 兼容两种返回格式
let matchEnabled = false
if (res && res.success && res.features) {
console.log('[TabBar] features配置:', JSON.stringify(res.features))
matchEnabled = res.features.matchEnabled === true
console.log('[TabBar] matchEnabled值:', matchEnabled)
} else if (res && res.configs && res.configs.feature_config) {
// 备用格式:从 configs.feature_config 读取
console.log('[TabBar] 使用备用格式从configs读取')
matchEnabled = res.configs.feature_config.matchEnabled === true
console.log('[TabBar] matchEnabled值:', matchEnabled)
} else {
console.log('[TabBar] ⚠️ 未找到features配置使用默认值false')
console.log('[TabBar] res对象keys:', Object.keys(res || {}))
}
this.setData({ matchEnabled }, () => {
console.log('[TabBar] ✅ matchEnabled已设置为:', this.data.matchEnabled)
// 配置加载完成后,根据当前路由设置选中状态
this.updateSelected()
})
// 如果当前在找伙伴页面,但功能已关闭,跳转到首页
if (!matchEnabled) {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
if (currentPage && currentPage.route === 'pages/match/match') {
console.log('[TabBar] 找伙伴功能已关闭从match页面跳转到首页')
wx.switchTab({ url: '/pages/index/index' })
}
}
} catch (error) {
console.log('[TabBar] ❌ 加载功能配置失败:', error)
console.log('[TabBar] 错误详情:', error.message || error)
// 默认关闭找伙伴功能
this.setData({ matchEnabled: false }, () => {
this.updateSelected()
})
}
},
// 根据当前路由更新选中状态
updateSelected() {
const pages = getCurrentPages()
if (pages.length === 0) return
const currentPage = pages[pages.length - 1]
const route = currentPage.route
let selected = 0
const { matchEnabled } = this.data
// 根据路由匹配对应的索引
if (route === 'pages/index/index') {
selected = 0
} else if (route === 'pages/chapters/chapters') {
selected = 1
} else if (route === 'pages/match/match') {
selected = 2
} else if (route === 'pages/my/my') {
selected = matchEnabled ? 3 : 2
}
this.setData({ selected })
},
switchTab(e) {
const data = e.currentTarget.dataset
const url = data.path
const index = data.index
if (this.data.selected === index) return
wx.switchTab({ url })
}
}
})

View File

@@ -0,0 +1,3 @@
{
"component": true
}

View File

@@ -0,0 +1,47 @@
<!--custom-tab-bar/index.wxml-->
<view class="tab-bar {{matchEnabled ? 'tab-bar-four' : 'tab-bar-three'}}">
<view class="tab-bar-border"></view>
<!-- 首页 -->
<view class="tab-bar-item" data-path="{{list[0].pagePath}}" data-index="0" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{selected === 0 ? 'icon-active' : ''}}"
src="/assets/icons/home.svg"
mode="aspectFit"
style="color: {{selected === 0 ? selectedColor : color}}"></image>
</view>
<view class="tab-bar-text" style="color: {{selected === 0 ? selectedColor : color}}">{{list[0].text}}</view>
</view>
<!-- 目录 -->
<view class="tab-bar-item" data-path="{{list[1].pagePath}}" data-index="1" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{selected === 1 ? 'icon-active' : ''}}"
src="/assets/icons/list.svg"
mode="aspectFit"
style="color: {{selected === 1 ? selectedColor : color}}"></image>
</view>
<view class="tab-bar-text" style="color: {{selected === 1 ? selectedColor : color}}">{{list[1].text}}</view>
</view>
<!-- 找伙伴 - 中间突出按钮(根据配置显示) -->
<view class="tab-bar-item special-item" wx:if="{{matchEnabled}}" data-path="{{list[2].pagePath}}" data-index="2" bindtap="switchTab">
<view class="special-button {{selected === 2 ? 'special-active' : ''}}">
<image class="special-icon"
src="/assets/icons/partners.svg"
mode="aspectFit"></image>
</view>
<view class="tab-bar-text special-text" style="color: {{selected === 2 ? selectedColor : color}}">{{list[2].text}}</view>
</view>
<!-- 我的 -->
<view class="tab-bar-item" data-path="{{list[3].pagePath}}" data-index="{{matchEnabled ? 3 : 2}}" bindtap="switchTab">
<view class="icon-wrapper">
<image class="tab-icon {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? 'icon-active' : ''}}"
src="/assets/icons/user.svg"
mode="aspectFit"
style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}"></image>
</view>
<view class="tab-bar-text" style="color: {{(matchEnabled && selected === 3) || (!matchEnabled && selected === 2) ? selectedColor : color}}">{{list[3].text}}</view>
</view>
</view>

View File

@@ -0,0 +1,121 @@
/**
* Soul创业实验 - 自定义TabBar样式
* 实现中间突出的"找伙伴"按钮
*/
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 100rpx;
background: rgba(28, 28, 30, 0.95);
backdrop-filter: blur(40rpx);
-webkit-backdrop-filter: blur(40rpx);
display: flex;
align-items: flex-end;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999;
}
/* 三个tab布局找伙伴功能关闭时 */
.tab-bar-three .tab-bar-item {
flex: 1;
}
/* 四个tab布局找伙伴功能开启时 */
.tab-bar-four .tab-bar-item {
flex: 1;
}
.tab-bar-border {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 1rpx;
background: rgba(255, 255, 255, 0.05);
}
.tab-bar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 10rpx 0 16rpx;
}
.icon-wrapper {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 4rpx;
}
.icon {
width: 44rpx;
height: 44rpx;
display: flex;
align-items: center;
justify-content: center;
}
.tab-bar-text {
font-size: 22rpx;
line-height: 1;
}
/* ===== SVG 图标样式 ===== */
.tab-icon {
width: 48rpx;
height: 48rpx;
display: block;
filter: brightness(0) saturate(100%) invert(60%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(95%) contrast(85%);
}
.tab-icon.icon-active {
filter: brightness(0) saturate(100%) invert(72%) sepia(54%) saturate(2933%) hue-rotate(134deg) brightness(101%) contrast(101%);
}
/* ===== 找伙伴 - 中间特殊按钮 ===== */
.special-item {
position: relative;
margin-top: -32rpx;
}
.special-button {
width: 112rpx;
height: 112rpx;
border-radius: 50%;
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
margin-bottom: 4rpx;
transition: all 0.2s ease;
}
.special-button:active {
transform: scale(0.95);
}
.special-active {
box-shadow: 0 8rpx 40rpx rgba(0, 206, 209, 0.6);
}
.special-text {
margin-top: 4rpx;
}
/* ===== 找伙伴特殊按钮图标 ===== */
.special-icon {
width: 80rpx;
height: 80rpx;
display: block;
filter: brightness(0) saturate(100%) invert(100%) sepia(0%) saturate(0%) hue-rotate(0deg) brightness(100%) contrast(100%);
}

View 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}` : '' }
}
})

View File

@@ -0,0 +1,4 @@
{
"usingComponents": {},
"navigationStyle": "custom"
}

View 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>

View 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; }

View 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}` : '' }
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationStyle": "custom",
"enablePullDownRefresh": false
}

View 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>

View 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;
}

View 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}` : '' }
}
})

View File

@@ -0,0 +1,5 @@
{
"usingComponents": {},
"navigationStyle": "custom",
"enablePullDownRefresh": false
}

View 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>

View 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;
}

View 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}` : '' }
}
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom","navigationBarTitleText":"用户协议"}

View 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>

View 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; }

View 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: '附录1Soul派对房精选对话' },
{ 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. dailyChapterssort_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}` : '' }
}
})

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
}

View 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>

View 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;
}

View 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_soulbook/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}` : '' }
}
})

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {},
"enablePullDownRefresh": true,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
}

View 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>

View 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:1pl-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;
}

View 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}` : '' }
}
})

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
}

View 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 §6view 包裹、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>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,200 @@
/**
* Soul创业派对 - 超级个体/会员详情页
* 接口:优先 /api/miniprogram/vip/members?id=xxVIP回退 /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 }
}
})

View File

@@ -0,0 +1 @@
{ "usingComponents": {}, "navigationStyle": "custom" }

View 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>

View 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); } }

View 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()
},
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom"}

View 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>

View 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; }

View 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()
},
})

View File

@@ -0,0 +1 @@
{"usingComponents":{},"navigationStyle":"custom"}

View 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>

View 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
View 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}` : '' }
}
})

View File

@@ -0,0 +1,6 @@
{
"usingComponents": {},
"enablePullDownRefresh": false,
"backgroundTextStyle": "light",
"backgroundColor": "#000000"
}

View 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>

View 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)); }

Some files were not shown because too many files have changed in this diff Show More