增强用户隐私保护,新增昵称授权功能。更新头像选择逻辑,用户可直接通过按钮选择微信头像或相册图片。优化个人资料页面,强化手机号必填提示,提升用户体验。调整多个页面以支持新隐私授权机制,确保符合最新隐私规范。
This commit is contained in:
@@ -80,10 +80,28 @@ App({
|
||||
supportWechat: '',
|
||||
// config 统一缓存(5min),减少重复请求
|
||||
configCache: null,
|
||||
configCacheExpires: 0
|
||||
configCacheExpires: 0,
|
||||
// VIP 联系方式检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
lastVipContactCheck: 0,
|
||||
// 头像昵称检测:上次检测时间戳,onShow 节流 5 分钟
|
||||
lastAvatarNicknameCheck: 0,
|
||||
},
|
||||
|
||||
|
||||
onLaunch(options) {
|
||||
// 昵称等隐私组件需先授权:input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
|
||||
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
|
||||
wx.onNeedPrivacyAuthorization((resolve) => {
|
||||
this._privacyResolve = resolve
|
||||
const pages = getCurrentPages()
|
||||
const cur = pages[pages.length - 1]
|
||||
if (cur && typeof cur.setData === 'function' && cur.route && (cur.route.includes('avatar-nickname') || cur.route.includes('profile-edit'))) {
|
||||
cur.setData({ showPrivacyModal: true })
|
||||
} else {
|
||||
resolve({ event: 'disagree' })
|
||||
}
|
||||
})
|
||||
}
|
||||
this.globalData.readSectionIds = wx.getStorageSync('readSectionIds') || []
|
||||
// 加载 iconfont(字体图标)。注意:小程序不支持在 wxss 里用本地 @font-face 引用字体文件,
|
||||
// 需使用 loadFontFace 动态加载(字体文件建议走 https CDN)。
|
||||
@@ -103,6 +121,11 @@ App({
|
||||
|
||||
// 检查登录状态
|
||||
this.checkLoginStatus()
|
||||
// 每次进入:先获取 VIP 状态,VIP 走 profile-edit,非 VIP 走头像/昵称引导(由 checkVipContactRequiredAndGuide 内部链式调用)
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1500)
|
||||
setTimeout(() => this.connectWsHeartbeat(), 2000)
|
||||
}
|
||||
|
||||
// 加载书籍数据
|
||||
this.loadBookData()
|
||||
@@ -143,6 +166,23 @@ App({
|
||||
this.globalData.lastMpConfigCheck = now
|
||||
this.getAuditMode()
|
||||
}
|
||||
// 从后台切回:先 VIP 强制跳转,再头像/昵称,节流 5 分钟
|
||||
const throttle = 5 * 60 * 1000
|
||||
if (this.globalData.isLoggedIn && this.globalData.userInfo?.id) {
|
||||
if (!this.globalData.lastVipContactCheck || now - this.globalData.lastVipContactCheck > throttle) {
|
||||
this.globalData.lastVipContactCheck = now
|
||||
this.globalData.lastAvatarNicknameCheck = now
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 500)
|
||||
}
|
||||
// 从后台切回:若 WSS 已断开则重连(微信后台可能回收连接)
|
||||
try {
|
||||
const need = !this._wsSocketTask || (this._wsSocketTask.readyState !== 0 && this._wsSocketTask.readyState !== 1)
|
||||
if (need) {
|
||||
this.clearWsReconnect()
|
||||
setTimeout(() => this.connectWsHeartbeat(), 1000)
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
},
|
||||
|
||||
// 处理推荐码绑定:官方以 options.scene 接收扫码参数(可同时带 mid/id + ref),与 utils/scene 解析闭环
|
||||
@@ -322,6 +362,44 @@ App({
|
||||
return false
|
||||
},
|
||||
|
||||
/** 判断头像/昵称是否未完善(默认状态) */
|
||||
_needsAvatarNickname(user) {
|
||||
const u = user || this.globalData.userInfo || {}
|
||||
const avatar = (u.avatar || u.avatarUrl || '').trim()
|
||||
const nickname = (u.nickname || u.nickName || '').trim()
|
||||
return !avatar || avatar.includes('default') || !nickname || nickname === '微信用户' || nickname.startsWith('微信用户')
|
||||
},
|
||||
|
||||
/**
|
||||
* 头像/昵称未改则引导:老用户弹窗后跳 avatar-nickname;新用户由登录处强制 redirectTo
|
||||
* VIP 用户不在此处理,统一走 checkVipContactRequiredAndGuide 只跳 profile-edit,避免乱跳
|
||||
*/
|
||||
checkAvatarNicknameAndGuide() {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
if (this.globalData.isVip) return // VIP 统一走 profile-edit,此处不触发
|
||||
if (!this._needsAvatarNickname()) return
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const last = pages[pages.length - 1]
|
||||
const route = (last && last.route) || ''
|
||||
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
|
||||
} catch (_) {}
|
||||
// 老用户:弹窗提示后跳转
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const lastDate = wx.getStorageSync('lastAvatarGuideDate') || ''
|
||||
if (lastDate === today) return
|
||||
wx.setStorageSync('lastAvatarGuideDate', today)
|
||||
wx.showModal({
|
||||
title: '完善个人资料',
|
||||
content: '请设置头像和昵称,让其他创业者更好地认识你',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 检查登录状态
|
||||
checkLoginStatus() {
|
||||
try {
|
||||
@@ -341,6 +419,168 @@ App({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* WSS 在线心跳(占位):登录后连接 ws,发送 auth + 心跳,供管理端统计在线人数
|
||||
* 容错:任意异常均不向外抛出,不影响登录、API 请求等核心功能
|
||||
*/
|
||||
clearWsReconnect() {
|
||||
try {
|
||||
if (this._wsReconnectTimerId) {
|
||||
clearTimeout(this._wsReconnectTimerId)
|
||||
this._wsReconnectTimerId = null
|
||||
}
|
||||
this._wsReconnectDelay = 3000
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
scheduleWsReconnect() {
|
||||
try {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
if (this._wsReconnectTimerId) return
|
||||
const delay = this._wsReconnectDelay || 3000
|
||||
this._wsReconnectTimerId = setTimeout(() => {
|
||||
this._wsReconnectTimerId = null
|
||||
this._wsReconnectDelay = Math.min(60000, (this._wsReconnectDelay || 3000) * 2)
|
||||
this.connectWsHeartbeat()
|
||||
}, delay)
|
||||
} catch (_) {}
|
||||
},
|
||||
|
||||
connectWsHeartbeat() {
|
||||
try {
|
||||
this.clearWsReconnect()
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
const userId = this.globalData.userInfo.id
|
||||
const base = (this.globalData.baseUrl || '').replace(/\/$/, '')
|
||||
if (!base) return
|
||||
const wsUrl = base.replace(/^http/, 'ws') + '/ws/miniprogram'
|
||||
if (this._wsHeartbeatTimer) {
|
||||
clearInterval(this._wsHeartbeatTimer)
|
||||
this._wsHeartbeatTimer = null
|
||||
}
|
||||
if (this._wsSocketTask) {
|
||||
try { this._wsSocketTask.close() } catch (_) {}
|
||||
this._wsSocketTask = null
|
||||
}
|
||||
let task
|
||||
try {
|
||||
task = wx.connectSocket({
|
||||
url: wsUrl,
|
||||
fail: () => { try { this.scheduleWsReconnect() } catch (_) {} }
|
||||
})
|
||||
} catch (e) {
|
||||
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 连接失败(静默):', e?.message || e)
|
||||
try { this.scheduleWsReconnect() } catch (_) {}
|
||||
return
|
||||
}
|
||||
task.onOpen(() => {
|
||||
try {
|
||||
this.clearWsReconnect()
|
||||
task.send({ data: JSON.stringify({ type: 'auth', userId }) })
|
||||
this._wsHeartbeatTimer = setInterval(() => {
|
||||
try {
|
||||
if (task && task.readyState === 1) task.send({ data: JSON.stringify({ type: 'heartbeat' }) })
|
||||
} catch (_) {}
|
||||
}, 30000)
|
||||
} catch (_) {}
|
||||
})
|
||||
task.onClose(() => {
|
||||
try {
|
||||
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
|
||||
this._wsSocketTask = null
|
||||
this.scheduleWsReconnect()
|
||||
} catch (_) {}
|
||||
})
|
||||
task.onError(() => {
|
||||
try {
|
||||
if (this._wsHeartbeatTimer) { clearInterval(this._wsHeartbeatTimer); this._wsHeartbeatTimer = null }
|
||||
this._wsSocketTask = null
|
||||
this.scheduleWsReconnect()
|
||||
} catch (_) {}
|
||||
})
|
||||
this._wsSocketTask = task
|
||||
} catch (e) {
|
||||
if (typeof console !== 'undefined' && console.warn) console.warn('[WS] 心跳异常(静默,不影响业务):', e?.message || e)
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* VIP 用户登录后检测:手机号/头像昵称等需完善时,统一只跳 profile-edit,避免与 avatar-nickname 乱跳。
|
||||
* 旧数据(VIP 但头像昵称未改):弹窗「为了更好服务,请完善资料」→ redirectTo profile-edit
|
||||
*/
|
||||
async checkVipContactRequiredAndGuide() {
|
||||
if (!this.globalData.isLoggedIn || !this.globalData.userInfo?.id) return
|
||||
const now = Date.now()
|
||||
if (this._lastVipGuideRun && now - this._lastVipGuideRun < 3000) return // 3 秒内不重复执行,避免 onLaunch+onShow 双重触发
|
||||
this._lastVipGuideRun = now
|
||||
const userId = this.globalData.userInfo.id
|
||||
try {
|
||||
const pages = getCurrentPages()
|
||||
const last = pages[pages.length - 1]
|
||||
const route = (last && last.route) || ''
|
||||
if (route.indexOf('profile-edit') !== -1 || route.indexOf('avatar-nickname') !== -1) return
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
const [vipRes, profileRes] = await Promise.all([
|
||||
this.request({ url: `/api/miniprogram/vip/status?userId=${userId}`, silent: true }).catch(() => null),
|
||||
this.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true }).catch(() => null)
|
||||
])
|
||||
const isVip = vipRes?.data?.isVip || this.globalData.isVip || false
|
||||
this.globalData.isVip = isVip
|
||||
if (!isVip) {
|
||||
this.checkAvatarNicknameAndGuide()
|
||||
return
|
||||
}
|
||||
|
||||
const profileData = profileRes?.data || this.globalData.userInfo || {}
|
||||
const phone = (profileData.phone || this.globalData.userInfo?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechatId = (profileData.wechatId || profileData.wechat_id || this.globalData.userInfo?.wechatId || this.globalData.userInfo?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
const needsAvatarNickname = this._needsAvatarNickname(profileData)
|
||||
|
||||
// VIP 头像/昵称未改(含旧数据):统一只跳 profile-edit,弹窗「为了更好服务,请完善资料」
|
||||
if (needsAvatarNickname) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '为了更好为您服务,请完善资料',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (phone && wechatId) return
|
||||
|
||||
// VIP 无手机号:弹窗说明后跳转
|
||||
if (!phone) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: 'VIP会员需完善手机号,以便使用找伙伴、提现等功能',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
// 有手机号但缺微信号:弹窗引导(非强制)
|
||||
wx.showModal({
|
||||
title: '完善联系方式',
|
||||
content: '请到资料页完善微信号,便于他人联系您',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[App] checkVipContactRequiredAndGuide 失败:', e?.message)
|
||||
}
|
||||
},
|
||||
|
||||
// 加载书籍元数据(totalSections),不再预加载 all-chapters
|
||||
async loadBookData() {
|
||||
try {
|
||||
@@ -659,8 +899,17 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管,完善头像吸收到规则引擎)
|
||||
checkAndExecute('after_login', null)
|
||||
// 同步 isVip(与 checkLoginStatus 一致)
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
// 首次登录注册:强制跳转 avatar-nickname 修改头像昵称(不弹窗)
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
setTimeout(() => this.connectWsHeartbeat(), 2000)
|
||||
}
|
||||
}
|
||||
|
||||
return res.data
|
||||
@@ -721,8 +970,16 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管)
|
||||
checkAndExecute('after_login', null)
|
||||
// 同步 isVip
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
// 首次登录注册:强制跳转 avatar-nickname
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
}
|
||||
}
|
||||
return res.data.openId
|
||||
}
|
||||
@@ -764,6 +1021,8 @@ App({
|
||||
this.globalData.isLoggedIn = true
|
||||
this.globalData.purchasedSections = user.purchasedSections || []
|
||||
this.globalData.hasFullBook = user.hasFullBook || false
|
||||
this.globalData.isVip = user.isVip || false
|
||||
this.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
|
||||
wx.setStorageSync('userInfo', user)
|
||||
wx.setStorageSync('token', res.data.token)
|
||||
@@ -775,9 +1034,14 @@ App({
|
||||
this.bindReferralCode(pendingRef)
|
||||
}
|
||||
|
||||
// 登录后引导完善资料(规则引擎接管)
|
||||
checkAndExecute('after_login', null)
|
||||
|
||||
// 首次登录注册:强制跳转 avatar-nickname
|
||||
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
|
||||
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
|
||||
} else {
|
||||
checkAndExecute('after_login', null)
|
||||
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
|
||||
}
|
||||
|
||||
return res.data
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
119
miniprogram/docs/资料完善引导流程图.md
Normal file
119
miniprogram/docs/资料完善引导流程图.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Soul 小程序 - 资料完善引导流程
|
||||
|
||||
> **入口统一**:仅 `checkVipContactRequiredAndGuide` 被 onLaunch/onShow/登录 调度;非 VIP 时内部链式调用 `checkAvatarNicknameAndGuide`。
|
||||
|
||||
---
|
||||
|
||||
## 一、整体流程(冷启动 / onShow)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([入口:onLaunch 1.5s / onShow 0.5s]) --> CheckLogin{已登录?}
|
||||
CheckLogin -->|否| End1([结束])
|
||||
CheckLogin -->|是| VIPCheck[checkVipContactRequiredAndGuide]
|
||||
VIPCheck --> CoolDown{3秒内已执行过?}
|
||||
CoolDown -->|是| End0([结束,防重复])
|
||||
CoolDown -->|否| RouteSkip{当前在 profile-edit<br/>或 avatar-nickname?}
|
||||
RouteSkip -->|是| End0
|
||||
RouteSkip -->|否| FetchAPI
|
||||
VIPCheck --> FetchAPI[请求 API:VIP 状态 + 用户资料]
|
||||
FetchAPI --> UpdateIsVip[更新 globalData.isVip]
|
||||
UpdateIsVip --> IsVip{VIP?}
|
||||
|
||||
IsVip -->|否| AvatarCheck[checkAvatarNicknameAndGuide]
|
||||
AvatarCheck --> End2([结束])
|
||||
|
||||
IsVip -->|是| SkipProfile{当前在 profile-edit?}
|
||||
SkipProfile -->|是| End3([结束])
|
||||
SkipProfile -->|否| NeedAvatar{头像/昵称未改?}
|
||||
|
||||
NeedAvatar -->|是| Modal1[弹窗:为了更好为您服务,请完善资料]
|
||||
Modal1 --> Redirect1[redirectTo profile-edit]
|
||||
Redirect1 --> End4([结束])
|
||||
|
||||
NeedAvatar -->|否| HasPhone{有手机号?}
|
||||
HasPhone -->|否| Modal2[弹窗:VIP会员需完善手机号...]
|
||||
Modal2 --> Redirect2[redirectTo profile-edit]
|
||||
Redirect2 --> End5([结束])
|
||||
|
||||
HasPhone -->|是| HasWechat{有微信号?}
|
||||
HasWechat -->|是| End6([结束])
|
||||
HasWechat -->|否| Modal3[弹窗:请到资料页完善微信号]
|
||||
Modal3 --> Nav1[navigateTo profile-edit]
|
||||
Nav1 --> End7([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、头像/昵称引导(非 VIP 用户)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A([checkAvatarNicknameAndGuide]) --> B{globalData.isVip?}
|
||||
B -->|是| X([跳过,由 VIP 检测处理])
|
||||
B -->|否| C{头像/昵称已完善?}
|
||||
C -->|是| Y([结束])
|
||||
C -->|否| D{当前在 profile-edit<br/>或 avatar-nickname?}
|
||||
D -->|是| Z([结束])
|
||||
D -->|否| E{今日已提示过?}
|
||||
E -->|是| W([结束])
|
||||
E -->|否| F[弹窗:请设置头像和昵称]
|
||||
F --> G{点击去完善?}
|
||||
G -->|是| H[navigateTo avatar-nickname]
|
||||
G -->|否 稍后| I([结束])
|
||||
H --> I
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、首次登录(新注册用户)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Login([登录成功]) --> NewUser{isNewUser?}
|
||||
NewUser -->|否| VIPFlow[checkVipContactRequiredAndGuide]
|
||||
VIPFlow --> Flow1([见流程图一])
|
||||
|
||||
NewUser -->|是| NeedAvatar{头像/昵称未改?}
|
||||
NeedAvatar -->|否| VIPFlow
|
||||
NeedAvatar -->|是| ForceRedirect[redirectTo avatar-nickname<br/>无弹窗,强制跳转]
|
||||
ForceRedirect --> End([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、VIP 购买成功(超级个体)
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Pay([VIP 支付成功]) --> Sync[同步权益 / 拉取 VIP 状态]
|
||||
Sync --> Modal[弹窗:为了更好为您服务,请填写好资料]
|
||||
Modal --> Redirect[redirectTo profile-edit?from=vip]
|
||||
Redirect --> End([结束])
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、页面职责
|
||||
|
||||
| 页面 | 用途 |
|
||||
|------|------|
|
||||
| **avatar-nickname** | 仅头像 + 昵称,专注引导新用户/非 VIP |
|
||||
| **profile-edit** | 完整资料(手机号、微信号、MBTI、行业等),VIP 用户统一跳转 |
|
||||
|
||||
---
|
||||
|
||||
## 六、触发时机汇总
|
||||
|
||||
| 时机 | 执行的函数 | 延迟 |
|
||||
|------|------------|------|
|
||||
| onLaunch | checkVipContactRequiredAndGuide | 1500ms |
|
||||
| onShow | checkVipContactRequiredAndGuide | 500ms(节流 5min)|
|
||||
| login 成功 | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| getOpenId 返回 user | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| loginWithPhone 成功 | checkVipContactRequiredAndGuide | 1200ms |
|
||||
| VIP 支付成功 | _onVipPaymentSuccess | 即时 |
|
||||
|
||||
---
|
||||
|
||||
*文档生成于资料完善引导逻辑调整后*
|
||||
@@ -10,7 +10,8 @@ Page({
|
||||
avatar: '',
|
||||
nickname: '',
|
||||
saving: false,
|
||||
showAvatarModal: false,
|
||||
showPrivacyModal: false,
|
||||
nicknameInputFocus: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -40,39 +41,38 @@ Page({
|
||||
onNicknameChange(e) {
|
||||
this.setData({ nickname: e.detail.value })
|
||||
},
|
||||
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
onNicknameBlur() {
|
||||
this.setData({ nicknameInputFocus: false })
|
||||
},
|
||||
onNicknameAreaTouch() {
|
||||
if (typeof wx.requirePrivacyAuthorize !== 'function') return
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: () => {
|
||||
this.setData({ nicknameInputFocus: true })
|
||||
},
|
||||
fail: () => {},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
preventMove() {},
|
||||
handleAgreePrivacy() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false, nicknameInputFocus: true })
|
||||
},
|
||||
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
await this.uploadAndSaveAvatar(tempPath)
|
||||
},
|
||||
})
|
||||
handleDisagreePrivacy() {
|
||||
const app = getApp()
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ event: 'disagree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
await this.uploadAndSaveAvatar(tempAvatarUrl)
|
||||
},
|
||||
@@ -98,7 +98,10 @@ Page({
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
|
||||
@@ -15,35 +15,48 @@
|
||||
<text class="guide-desc">让他人更好地认识你,展示更专业的形象</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<!-- 头像:点击直接弹出微信原生选择器;头像与文字水平对齐 -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<text class="avatar-change">点击更换头像</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 昵称 -->
|
||||
<!-- 昵称:点击前先请求隐私授权,解决 errno:104 昵称选择器无法弹出 -->
|
||||
<view class="form-section">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap">
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击输入框自动填充昵称,或手动输入</text>
|
||||
</view>
|
||||
|
||||
<!-- 隐私授权弹窗(昵称需授权后方可唤起微信昵称选择器) -->
|
||||
<view class="privacy-mask" wx:if="{{showPrivacyModal}}" catchtouchmove="preventMove">
|
||||
<view class="privacy-modal">
|
||||
<text class="privacy-title">温馨提示</text>
|
||||
<text class="privacy-desc">为获取微信昵称,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-btn" class="privacy-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacy">同意</button>
|
||||
<view class="privacy-cancel" bindtap="handleDisagreePrivacy">拒绝</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '完成'}}
|
||||
</view>
|
||||
@@ -54,14 +67,4 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 头像弹窗:使用微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></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>
|
||||
|
||||
@@ -72,10 +72,20 @@
|
||||
|
||||
.avatar-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 32rpx;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 192rpx;
|
||||
@@ -126,7 +136,6 @@
|
||||
font-size: 28rpx;
|
||||
color: #5EEAD4;
|
||||
font-weight: 500;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
@@ -263,9 +272,79 @@
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-album {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(94, 234, 212, 0.15);
|
||||
color: #5EEAD4;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #5EEAD4;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 隐私授权弹窗(昵称选择器需先授权) */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 48rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 100%;
|
||||
max-width: 560rpx;
|
||||
background: #17212F;
|
||||
border-radius: 24rpx;
|
||||
padding: 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.privacy-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.privacy-desc {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
text-align: center;
|
||||
background: #5EEAD4;
|
||||
color: #050B14;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
.privacy-btn::after { border: none; }
|
||||
.privacy-cancel {
|
||||
margin-top: 24rpx;
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ Page({
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ menus: ['shareAppMessage', 'shareTimeline'] })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options?.ref || options?.referralCode) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
const requestSn = (options.requestSn || '').trim()
|
||||
const sectionId = (options.sectionId || '').trim()
|
||||
const isSinglePage = (wx.getSystemInfoSync?.()?.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
@@ -323,10 +326,10 @@ Page({
|
||||
const { requestSn } = this.data
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
let path = '/pages/gift-pay/detail'
|
||||
if (requestSn) {
|
||||
path = `/pages/gift-pay/detail?requestSn=${requestSn}`
|
||||
if (ref) path += `&ref=${encodeURIComponent(ref)}`
|
||||
}
|
||||
const params = []
|
||||
if (requestSn) params.push(`requestSn=${encodeURIComponent(requestSn)}`)
|
||||
if (ref) params.push(`ref=${encodeURIComponent(ref)}`)
|
||||
if (params.length) path += '?' + params.join('&')
|
||||
return {
|
||||
title: '好友送你一篇好文 - Soul创业派对',
|
||||
path
|
||||
@@ -336,14 +339,12 @@ Page({
|
||||
onShareTimeline() {
|
||||
const { requestSn } = this.data
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
let query = ''
|
||||
if (requestSn) {
|
||||
query = `requestSn=${requestSn}`
|
||||
if (ref) query += `&ref=${encodeURIComponent(ref)}`
|
||||
}
|
||||
const params = []
|
||||
if (requestSn) params.push(`requestSn=${encodeURIComponent(requestSn)}`)
|
||||
if (ref) params.push(`ref=${encodeURIComponent(ref)}`)
|
||||
return {
|
||||
title: '好友送你一篇好文 - Soul创业派对',
|
||||
query: query || ''
|
||||
query: params.length ? params.join('&') : ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
@@ -10,8 +10,11 @@ Page({
|
||||
loading: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
if (options && (options.ref || options.referralCode)) {
|
||||
app.handleReferralCode({ query: { ref: options.ref || options.referralCode } })
|
||||
}
|
||||
this.loadData()
|
||||
},
|
||||
|
||||
@@ -82,6 +85,8 @@ Page({
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
return { title: '我的代付 - Soul创业派对', path: '/pages/gift-pay/list' }
|
||||
const ref = app.getMyReferralCode?.() || app.globalData.userInfo?.referralCode || ''
|
||||
const path = ref ? `/pages/gift-pay/list?ref=${encodeURIComponent(ref)}` : '/pages/gift-pay/list'
|
||||
return { title: '我的代付 - Soul创业派对', path }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -226,9 +226,9 @@ Page({
|
||||
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) {
|
||||
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
@@ -252,10 +252,10 @@ Page({
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号(必填)', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
|
||||
@@ -274,7 +274,7 @@
|
||||
<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="contact-modal-hint">手机号必填,微信号建议填写,以便使用找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
|
||||
@@ -68,9 +68,6 @@ Page({
|
||||
showNicknameModal: false,
|
||||
editingNickname: '',
|
||||
|
||||
// 头像弹窗(含 chooseAvatar 按钮,必须用户点击才可获取微信头像)
|
||||
showAvatarModal: false,
|
||||
|
||||
// 手机/微信号弹窗(stitch_soul comprehensive_profile_editor_v1_2)
|
||||
showContactModal: false,
|
||||
contactPhone: '',
|
||||
@@ -458,10 +455,9 @@ Page({
|
||||
wx.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,真正获取微信头像)
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,点击头像直接唤起选择器)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
@@ -495,8 +491,11 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[My] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地头像
|
||||
@@ -506,7 +505,7 @@ Page({
|
||||
app.globalData.userInfo = userInfo
|
||||
wx.setStorageSync('userInfo', userInfo)
|
||||
|
||||
// 4. 同步到服务器数据库
|
||||
// 4. 同步到服务器数据库(只保存路径,不含域名)
|
||||
await app.request('/api/miniprogram/user/update', {
|
||||
method: 'POST',
|
||||
data: { userId: userInfo.id, avatar: avatarUrl }
|
||||
@@ -548,12 +547,9 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 打开昵称修改弹窗
|
||||
// 点击昵称:跳转资料编辑页(type="nickname" 在弹窗内无法触发微信昵称选择器,需在主页面)
|
||||
editNickname() {
|
||||
this.setData({
|
||||
showNicknameModal: true,
|
||||
editingNickname: this.data.userInfo?.nickname || ''
|
||||
})
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 关闭昵称弹窗
|
||||
@@ -875,62 +871,6 @@ Page({
|
||||
} catch (e) { console.log('[My] 余额查询失败', 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() {
|
||||
trackClick('my', 'btn_click', '会员中心')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
@@ -982,18 +922,19 @@ Page({
|
||||
})
|
||||
},
|
||||
|
||||
// 提现/找伙伴前检查手机或微信号,未填则弹窗(stitch_soul)
|
||||
// 提现/找伙伴前检查联系方式:手机号必填(与 profile-edit 规则一致)
|
||||
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) {
|
||||
const phone = (res?.data?.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const hasValidPhone = !!phone && /^1[3-9]\d{9}$/.test(phone)
|
||||
if (hasValidPhone) {
|
||||
callback()
|
||||
return
|
||||
}
|
||||
const wechat = (res?.data?.wechatId || res?.data?.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
this.setData({
|
||||
showContactModal: true,
|
||||
contactPhone: phone || '',
|
||||
@@ -1015,10 +956,14 @@ Page({
|
||||
onContactWechatInput(e) { this.setData({ contactWechat: e.detail.value }) },
|
||||
|
||||
async saveContactInfo() {
|
||||
const phone = (this.data.contactPhone || '').trim()
|
||||
const phone = (this.data.contactPhone || '').trim().replace(/\s/g, '')
|
||||
const wechat = (this.data.contactWechat || '').trim()
|
||||
if (!phone && !wechat) {
|
||||
wx.showToast({ title: '请至少填写手机号或微信号', icon: 'none' })
|
||||
if (!phone) {
|
||||
wx.showToast({ title: '请输入手机号(必填)', icon: 'none' })
|
||||
return
|
||||
}
|
||||
if (!/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ contactSaving: true })
|
||||
|
||||
@@ -23,14 +23,16 @@
|
||||
<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>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<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="vip-badge" wx:if="{{isVip}}">VIP</view>
|
||||
<view class="vip-badge vip-badge-gray" wx:else>VIP</view>
|
||||
</view>
|
||||
</button>
|
||||
<view class="profile-meta">
|
||||
<view class="profile-name-row">
|
||||
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
|
||||
@@ -199,7 +201,7 @@
|
||||
<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="contact-modal-hint">手机号必填,微信号建议填写,以便使用提现和找伙伴功能</view>
|
||||
<view class="form-input-wrap">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-inner">
|
||||
@@ -219,17 +221,6 @@
|
||||
</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"><icon name="x" size="36" color="#8e8e93"></icon></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">
|
||||
|
||||
@@ -41,6 +41,14 @@
|
||||
border: 1rpx solid rgba(75,85,99,0.5);
|
||||
}
|
||||
.profile-top-row { display: flex; align-items: flex-start; gap: 32rpx; }
|
||||
/* 头像按钮:透明无边框,宽高与头像一致,点击直接唤起微信选择器(微信头像/相册/拍照) */
|
||||
.avatar-wrap-btn {
|
||||
width: 40rpx; height: 130rpx;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap { position: relative; flex-shrink: 0; }
|
||||
.avatar-inner {
|
||||
width: 130rpx; height: 130rpx; border-radius: 50%; overflow: hidden;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
* 表单展示:普通用户仅展示 温馨提示、头像、昵称、MBTI、地区、行业、业务体量、职位、核心联系方式;VIP 展示全部
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
@@ -38,11 +39,15 @@ Page({
|
||||
showMbtiPicker: false,
|
||||
saving: false,
|
||||
loading: true,
|
||||
showAvatarModal: false,
|
||||
showPrivacyModal: false,
|
||||
nicknameInputFocus: false,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
fromVip: options?.from === 'vip',
|
||||
})
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
// 从朋友圈/分享打开且带 id:跳转到名片详情(member-detail)
|
||||
if (options?.id) {
|
||||
@@ -104,6 +109,30 @@ Page({
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onNicknameAreaTouch() {
|
||||
if (typeof wx.requirePrivacyAuthorize !== 'function') return
|
||||
wx.requirePrivacyAuthorize({
|
||||
success: () => { this.setData({ nicknameInputFocus: true }) },
|
||||
fail: () => {},
|
||||
})
|
||||
},
|
||||
onNicknameBlur() { this.setData({ nicknameInputFocus: false }) },
|
||||
preventMove() {},
|
||||
handleAgreePrivacy() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ buttonId: 'agree-btn', event: 'agree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false, nicknameInputFocus: true })
|
||||
},
|
||||
handleDisagreePrivacy() {
|
||||
if (app._privacyResolve) {
|
||||
app._privacyResolve({ event: 'disagree' })
|
||||
app._privacyResolve = null
|
||||
}
|
||||
this.setData({ showPrivacyModal: false })
|
||||
},
|
||||
|
||||
// 生成分享名片封面图(参考:头像左+昵称右,分隔线,四栏信息 5:4)
|
||||
async generateShareCard() {
|
||||
const { avatar, nickname, region, mbti, industry, position } = this.data
|
||||
@@ -286,76 +315,9 @@ Page({
|
||||
this.setData({ mbtiIndex: i, mbti: MBTI_OPTIONS[i] })
|
||||
},
|
||||
|
||||
// 点击头像:选择微信头像或从相册选择
|
||||
onAvatarTap() {
|
||||
wx.showActionSheet({
|
||||
itemList: ['使用微信头像', '从相册选择'],
|
||||
success: (res) => {
|
||||
if (res.tapIndex === 0) {
|
||||
this.setData({ showAvatarModal: true })
|
||||
} else if (res.tapIndex === 1) {
|
||||
this.chooseAvatarFromAlbum()
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
closeAvatarModal() {
|
||||
this.setData({ showAvatarModal: false })
|
||||
},
|
||||
|
||||
// 从相册/相机选择头像
|
||||
chooseAvatarFromAlbum() {
|
||||
wx.chooseMedia({
|
||||
count: 1,
|
||||
mediaType: ['image'],
|
||||
sourceType: ['album', 'camera'],
|
||||
success: async (res) => {
|
||||
const tempPath = res.tempFiles[0].tempFilePath
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
try {
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempPath,
|
||||
name: 'file',
|
||||
formData: { folder: 'avatars' },
|
||||
success: (r) => {
|
||||
try {
|
||||
const data = JSON.parse(r.data)
|
||||
if (data.success) resolve(data)
|
||||
else reject(new Error(data.error || '上传失败'))
|
||||
} catch { reject(new Error('解析失败')) }
|
||||
},
|
||||
fail: reject,
|
||||
})
|
||||
})
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
this.setData({ avatar: avatarUrl })
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像已更新', icon: 'success' })
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
},
|
||||
|
||||
// 微信原生 chooseAvatar 回调:使用当前微信头像
|
||||
// 微信原生 chooseAvatar 回调(点击头像直接弹出原生选择器:用微信头像/从相册选择/拍照)
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
this.setData({ showAvatarModal: false })
|
||||
if (!tempAvatarUrl) return
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
@@ -379,12 +341,16 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
this.setData({ avatar: avatarUrl })
|
||||
const avatarToSave = toAvatarPath(avatarUrl)
|
||||
await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarUrl },
|
||||
data: { userId: app.globalData.userInfo?.id, avatar: avatarToSave },
|
||||
})
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
@@ -405,20 +371,32 @@ Page({
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
// 手机号必填,格式校验(支持带空格/连字符输入)
|
||||
const phoneRaw = s(this.data.phone)
|
||||
if (!phoneRaw) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const phoneNum = phoneRaw.replace(/\D/g, '')
|
||||
if (!/^1[3-9]\d{9}$/.test(phoneNum)) {
|
||||
wx.showToast({ title: '请输入正确的11位手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const phoneToSave = phoneNum
|
||||
this.setData({ saving: true })
|
||||
try {
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
const payload = {
|
||||
userId,
|
||||
avatar: s(this.data.avatar),
|
||||
avatar: toAvatarPath(s(this.data.avatar)),
|
||||
nickname: s(this.data.nickname),
|
||||
mbti: s(this.data.mbti),
|
||||
region: s(this.data.region),
|
||||
industry: s(this.data.industry),
|
||||
businessScale: s(this.data.businessScale),
|
||||
position: s(this.data.position),
|
||||
phone: s(this.data.phone),
|
||||
phone: phoneToSave,
|
||||
wechatId: s(this.data.wechatId),
|
||||
}
|
||||
const showHelp = isVip || this.data.helpOffer || this.data.helpNeed
|
||||
@@ -441,7 +419,7 @@ Page({
|
||||
this.setData({ saving: false })
|
||||
return
|
||||
}
|
||||
await app.request({
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/user/profile',
|
||||
method: 'POST',
|
||||
data: payload,
|
||||
@@ -449,7 +427,7 @@ Page({
|
||||
wx.showToast({ title: '保存成功', icon: 'success' })
|
||||
if (app.globalData.userInfo) {
|
||||
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
|
||||
if (payload.avatar) app.globalData.userInfo.avatar = payload.avatar
|
||||
if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
setTimeout(() => getApp().goBackOrToHome(), 800)
|
||||
|
||||
@@ -9,36 +9,39 @@
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 温馨提示 -->
|
||||
<view class="tip-card">
|
||||
<!-- 温馨提示:from=vip 时强化权益说明 -->
|
||||
<view class="tip-card {{fromVip ? 'tip-card-highlight' : ''}}">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">温馨提示:需完善手机号和微信号才能使用提现和找伙伴功能</text>
|
||||
<text class="tip-text">{{fromVip ? '恭喜成为VIP!完善资料后即可使用找伙伴、提现等功能,手机号必填' : '温馨提示:手机号必填,微信号建议填写,以便使用提现和找伙伴功能'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像 -->
|
||||
<!-- 头像:点击直接弹出微信原生选择器(用微信头像/从相册选择/拍照) -->
|
||||
<view class="avatar-section">
|
||||
<view class="avatar-wrap" bindtap="onAvatarTap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<text class="avatar-change">更换头像</text>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap">
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
@@ -84,8 +87,8 @@
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
<text class="form-label">手机号<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号(必填)" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
@@ -149,14 +152,13 @@
|
||||
<!-- 分享名片 canvas(隐藏,用于生成分享图 5:4) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
|
||||
<!-- 头像弹窗:通过 button 获取微信头像 -->
|
||||
<view class="modal-overlay" wx:if="{{showAvatarModal}}" bindtap="closeAvatarModal">
|
||||
<view class="modal-content avatar-modal" catchtap="stopPropagation">
|
||||
<view class="modal-close" bindtap="closeAvatarModal"><icon name="x" size="36" color="#8e8e93"></icon></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 class="privacy-mask" wx:if="{{showPrivacyModal}}" catchtouchmove="preventMove">
|
||||
<view class="privacy-modal">
|
||||
<text class="privacy-title">温馨提示</text>
|
||||
<text class="privacy-desc">为获取微信昵称,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-btn" class="privacy-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="handleAgreePrivacy">同意</button>
|
||||
<view class="privacy-cancel" bindtap="handleDisagreePrivacy">拒绝</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -31,10 +31,20 @@
|
||||
background: rgba(94,234,212,0.08); border: 1rpx solid rgba(94,234,212,0.25);
|
||||
border-radius: 24rpx; margin-bottom: 48rpx;
|
||||
}
|
||||
.tip-card-highlight {
|
||||
background: rgba(94,234,212,0.12); border-color: rgba(94,234,212,0.4);
|
||||
}
|
||||
.tip-icon { font-size: 40rpx; color: #5EEAD4; flex-shrink: 0; }
|
||||
.tip-text { font-size: 26rpx; color: rgba(94,234,212,0.95); line-height: 1.6; }
|
||||
|
||||
.avatar-section { display: flex; flex-direction: column; align-items: center; margin-bottom: 48rpx; }
|
||||
/* 头像按钮:透明无边框,点击直接弹出微信原生选择器 */
|
||||
.avatar-wrap-btn {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; margin: 0; background: transparent; border: none;
|
||||
width: 192rpx; height: 192rpx; border-radius: 50%; overflow: visible;
|
||||
}
|
||||
.avatar-wrap-btn::after { border: none; }
|
||||
.avatar-wrap {
|
||||
position: relative; width: 192rpx; height: 192rpx; border-radius: 50%;
|
||||
border: 4rpx solid #5EEAD4; box-shadow: 0 0 30rpx rgba(94,234,212,0.3);
|
||||
@@ -67,6 +77,7 @@
|
||||
.form-row-2 { display: flex; gap: 24rpx; }
|
||||
.form-row-2 .form-item { flex: 1; min-width: 0; }
|
||||
.form-label { display: block; font-size: 24rpx; color: #94A3B8; margin-bottom: 12rpx; margin-left: 8rpx; }
|
||||
.required-mark { color: #F87171; margin-left: 4rpx; }
|
||||
|
||||
/* input/textarea 用 view 包裹,padding 写在 view 上 */
|
||||
.form-input-wrap {
|
||||
@@ -75,10 +86,37 @@
|
||||
border-radius: 24rpx;
|
||||
box-sizing: border-box; min-width: 0; width: 100%;
|
||||
}
|
||||
.form-input-suffix { position: relative; padding-right: 64rpx; }
|
||||
/* 地区等带后缀图标的输入框:与 MBTI 同高,图标垂直居中 */
|
||||
.form-input-suffix {
|
||||
position: relative;
|
||||
padding-right: 64rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 88rpx;
|
||||
}
|
||||
.form-input-suffix .form-input-inner {
|
||||
flex: 1;
|
||||
min-height: 40rpx;
|
||||
line-height: 40rpx;
|
||||
}
|
||||
.form-input-suffix .form-suffix {
|
||||
position: absolute; right: 24rpx; top: 50%; transform: translateY(-50%);
|
||||
font-size: 32rpx; color: #94A3B8;
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 32rpx;
|
||||
color: #94A3B8;
|
||||
pointer-events: none;
|
||||
}
|
||||
/* form-row-2 下两列统一最小高度,与 MBTI 一致 */
|
||||
.form-row-2 .form-input-wrap,
|
||||
.form-row-2 .form-picker {
|
||||
min-height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.form-row-2 .form-picker {
|
||||
color: #fff;
|
||||
}
|
||||
.form-input-inner {
|
||||
width: 100%; max-width: 100%; font-size: 28rpx; color: #fff; background: transparent;
|
||||
@@ -183,9 +221,123 @@
|
||||
.btn-choose-avatar::after {
|
||||
border: none;
|
||||
}
|
||||
.btn-choose-album {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(94, 234, 212, 0.15);
|
||||
color: #5EEAD4;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: 2rpx solid #5EEAD4;
|
||||
}
|
||||
.avatar-modal-cancel {
|
||||
margin-top: 24rpx;
|
||||
text-align: center;
|
||||
font-size: 28rpx;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
|
||||
/* 昵称隐私弹窗 */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 560rpx;
|
||||
padding: 48rpx 40rpx;
|
||||
background: #1E293B;
|
||||
border-radius: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-modal .privacy-title { font-size: 34rpx; font-weight: 600; color: #fff; margin-bottom: 24rpx; }
|
||||
.privacy-modal .privacy-desc { font-size: 28rpx; color: #94A3B8; line-height: 1.6; margin-bottom: 40rpx; }
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
line-height: 88rpx;
|
||||
background: #5EEAD4;
|
||||
color: #0F172A;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* 隐私授权弹窗 */
|
||||
.privacy-mask {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 580rpx;
|
||||
background: #1E293B;
|
||||
border-radius: 24rpx;
|
||||
padding: 40rpx 32rpx 32rpx;
|
||||
}
|
||||
.privacy-modal-title {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
color: #F8FAFC;
|
||||
margin-bottom: 24rpx;
|
||||
text-align: center;
|
||||
}
|
||||
.privacy-modal-desc {
|
||||
font-size: 28rpx;
|
||||
color: #94A3B8;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.privacy-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #5EEAD4;
|
||||
color: #0F172A;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border-radius: 44rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.privacy-btn:last-of-type {
|
||||
margin-bottom: 0;
|
||||
background: transparent;
|
||||
color: #94A3B8;
|
||||
border: 2rpx solid #475569;
|
||||
}
|
||||
|
||||
/* 昵称隐私授权弹窗(解决 errno:104) */
|
||||
.privacy-mask {
|
||||
position: fixed; inset: 0; background: rgba(0,0,0,0.7); z-index: 9999;
|
||||
display: flex; align-items: center; justify-content: center; padding: 48rpx;
|
||||
}
|
||||
.privacy-modal {
|
||||
width: 100%; max-width: 600rpx; background: #1E293B; border-radius: 24rpx; padding: 48rpx;
|
||||
}
|
||||
.privacy-title { font-size: 36rpx; font-weight: 700; display: block; margin-bottom: 16rpx; }
|
||||
.privacy-desc { font-size: 28rpx; color: #94A3B8; line-height: 1.5; display: block; margin-bottom: 32rpx; }
|
||||
.privacy-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: #5EEAD4; color: #050B14; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; border: none; }
|
||||
.privacy-btn::after { border: none; }
|
||||
.privacy-cancel { text-align: center; margin-top: 24rpx; font-size: 28rpx; color: #94A3B8; }
|
||||
|
||||
@@ -651,21 +651,21 @@ Page({
|
||||
return
|
||||
}
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${myUserId}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
phone = (profileRes.data.phone || '').trim()
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || '').trim()
|
||||
phone = (profileRes.data.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
wechatId = (profileRes.data.wechatId || profileRes.data.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!phone && !wechatId) {
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号或微信号,以便对方联系您',
|
||||
content: '请先填写手机号(必填),以便对方联系您',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 账号绑定功能
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -286,8 +287,11 @@ Page({
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL
|
||||
const avatarUrl = app.globalData.baseUrl + uploadRes.data.url
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[Settings] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地
|
||||
@@ -299,12 +303,12 @@ Page({
|
||||
}
|
||||
})
|
||||
|
||||
// 4. 同步到服务器数据库
|
||||
// 4. 同步到服务器数据库(只保存路径,不含域名)
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, nickname: nickName, avatar: avatarUrl }
|
||||
data: { userId, nickname: nickName, avatar: toAvatarPath(avatarUrl) }
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -85,20 +85,7 @@ Page({
|
||||
return
|
||||
}
|
||||
}
|
||||
// 支付前:若头像/昵称仍为默认值,引导先完善(仅头像+昵称)
|
||||
if (this._shouldGuideAvatarNickname()) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '开通超级个体前,请先设置头像和昵称,让他人更好地认识你',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: (res) => {
|
||||
if (res.confirm) wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// VIP 购买后才引导完善资料:购买前不拦截,购买成功后跳转 profile-edit
|
||||
this.setData({ purchasing: true })
|
||||
const amount = this.data.price
|
||||
try {
|
||||
@@ -173,23 +160,21 @@ Page({
|
||||
else if (typeof p.updateUserStatus === 'function') p.updateUserStatus()
|
||||
})
|
||||
|
||||
// 开通成功后兜底:仍为默认头像/昵称则引导完善
|
||||
if (this._shouldGuideAvatarNickname()) {
|
||||
wx.navigateTo({ url: '/pages/avatar-nickname/avatar-nickname' })
|
||||
}
|
||||
// 超级个体购买后:弹窗提示,强制跳转资料编辑页
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '为了更好为您服务,请填写好资料',
|
||||
confirmText: '去完善',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit?from=vip' })
|
||||
}
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('[VIP] 支付后同步失败:', e)
|
||||
wx.hideLoading()
|
||||
}
|
||||
wx.hideLoading()
|
||||
},
|
||||
|
||||
_shouldGuideAvatarNickname() {
|
||||
const user = app.globalData.userInfo || {}
|
||||
const avatar = (user.avatar || user.avatarUrl || '').trim()
|
||||
const nickname = (user.nickname || user.nickName || '').trim()
|
||||
// 与 ruleEngine.checkRule_FillAvatar 保持同口径(允许前端兜底)
|
||||
if (avatar && !avatar.includes('default') && nickname && nickname !== '微信用户' && !nickname.startsWith('微信用户')) return false
|
||||
return true
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
@@ -24,12 +24,26 @@
|
||||
"miniprogram": {
|
||||
"list": [
|
||||
{
|
||||
"name": "pages/gift-pay/list",
|
||||
"pathName": "pages/gift-pay/list",
|
||||
"name": "pages/my/my",
|
||||
"pathName": "pages/my/my",
|
||||
"query": "",
|
||||
"scene": null,
|
||||
"launchMode": "default"
|
||||
},
|
||||
{
|
||||
"name": "个人资料",
|
||||
"pathName": "pages/avatar-nickname/avatar-nickname",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "pages/gift-pay/list",
|
||||
"pathName": "pages/gift-pay/list",
|
||||
"query": "",
|
||||
"launchMode": "default",
|
||||
"scene": null
|
||||
},
|
||||
{
|
||||
"name": "代付",
|
||||
"pathName": "pages/gift-pay/detail",
|
||||
|
||||
@@ -86,9 +86,11 @@ function getRuleInfo(rules, triggerName) {
|
||||
return rules.find(r => r.trigger === triggerName)
|
||||
}
|
||||
|
||||
// 稳定版:跳转 avatar-nickname(与 _ensureProfileCompletedAfterLogin 一致)
|
||||
// 稳定版:跳转 avatar-nickname(专注头像+昵称,首次登录由 app.login 强制 redirect)
|
||||
// VIP 用户不触发:统一由 checkVipContactRequiredAndGuide 引导到 profile-edit,避免与主流程冲突
|
||||
function checkRule_FillAvatar(rules) {
|
||||
if (!isRuleEnabled(rules, '注册')) return null
|
||||
if (app.globalData.isVip) return null
|
||||
const user = getUserInfo()
|
||||
if (!user.id) return null
|
||||
const avatar = user.avatar || user.avatarUrl || ''
|
||||
|
||||
@@ -174,6 +174,20 @@ const showConfirm = (title, content) => {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从头像 URL 提取路径部分(不含域名),用于保存到后端
|
||||
* 例如:https://xxx.com/uploads/avatars/1.jpg → /uploads/avatars/1.jpg
|
||||
* @param {string} url - 完整 URL 或路径
|
||||
* @returns {string}
|
||||
*/
|
||||
const toAvatarPath = url => {
|
||||
if (!url || typeof url !== 'string') return url || ''
|
||||
const idx = url.indexOf('/uploads/')
|
||||
if (idx >= 0) return url.substring(idx)
|
||||
if (url.startsWith('/')) return url
|
||||
return url
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
formatTime,
|
||||
formatDate,
|
||||
@@ -191,5 +205,6 @@ module.exports = {
|
||||
showToast,
|
||||
showLoading,
|
||||
hideLoading,
|
||||
showConfirm
|
||||
showConfirm,
|
||||
toAvatarPath
|
||||
}
|
||||
|
||||
94
miniprogram/模拟测试清单.md
Normal file
94
miniprogram/模拟测试清单.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Soul 小程序 - 模拟测试清单
|
||||
|
||||
在**微信开发者工具**中打开 `miniprogram` 项目,按以下步骤逐项验证。
|
||||
|
||||
---
|
||||
|
||||
## 一、前置准备
|
||||
|
||||
1. 启动微信开发者工具,打开项目 `e:\Gongsi\Mycontent\miniprogram`
|
||||
2. 确认后端 soul-api 已启动(baseUrl 指向正确,如 `http://localhost:8080`)
|
||||
3. 准备测试账号:① 普通用户(未完善) ② VIP 用户(无手机号) ③ 已完善用户
|
||||
|
||||
---
|
||||
|
||||
## 二、VIP 无手机号 / 头像未改(先判断 VIP)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 2.1 | 用 **VIP 且无手机号** 的账号登录 | 登录成功 |
|
||||
| 2.2 | 完全关闭小程序,重新编译并启动 | 约 1.5s 后弹窗「VIP会员需完善手机号...」→ 点击「去完善」→ redirectTo profile-edit |
|
||||
| 2.3 | 用 **VIP 有手机号但头像/昵称未改**(旧数据) | 弹窗「为了更好为您服务,请完善资料」→ redirectTo profile-edit |
|
||||
| 2.4 | 在资料页填写并保存手机号 | 保存成功,可返回首页 |
|
||||
| 2.5 | 再次关闭并重新启动 | 不再弹窗或跳转,正常进入首页 |
|
||||
|
||||
---
|
||||
|
||||
## 三、头像/昵称引导(非 VIP 老用户)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 3.1 | 用 **非 VIP、头像/昵称仍为默认** 的账号冷启动 | 先获取 VIP 状态 → 非 VIP → 弹窗「请设置头像和昵称」,每日最多 1 次 |
|
||||
| 3.2 | 点击「去完善」 | navigateTo avatar-nickname |
|
||||
| 3.3 | 点击「稍后」 | 弹窗关闭,可正常使用 |
|
||||
| 3.4 | **新注册用户**(isNewUser)且头像未改 | 无弹窗,直接 redirectTo avatar-nickname |
|
||||
| 3.5 | 当前已在 profile-edit 或 avatar-nickname 页 | 不再重复弹窗或跳转 |
|
||||
|
||||
---
|
||||
|
||||
## 四、检测顺序(VIP 优先)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 4.1 | 用 **VIP 无手机号 + 头像默认** 账号登录 | 先执行 VIP 检测 → 直接 redirectTo profile-edit |
|
||||
| 4.2 | 验证不会先到头像页 | VIP 优先,直接进 profile-edit,不会先到 avatar-nickname |
|
||||
|
||||
---
|
||||
|
||||
## 五、VIP 购买流程
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 5.1 | 进入 VIP 页点击「开通」并完成购买 | 购买前不拦截;购买成功后弹窗说明,确认后跳转 profile-edit?from=vip |
|
||||
| 5.2 | VIP 购买成功后点击「去完善」 | 跳转到 profile-edit(含手机号必填) |
|
||||
| 5.3 | 在 profile-edit 不填手机号直接保存(VIP 账号) | 提示「请输入手机号(VIP会员必填)」 |
|
||||
| 5.4 | 填写正确手机号后保存 | 保存成功 |
|
||||
|
||||
---
|
||||
|
||||
## 六、微信号引导(非强制)
|
||||
|
||||
| 步骤 | 操作 | 预期结果 |
|
||||
|------|------|----------|
|
||||
| 6.1 | 用 **VIP 有手机号但无微信号** 账号登录 | 弹窗「请到资料页完善微信号」 |
|
||||
| 6.2 | 点击「去完善」或「稍后」 | 去完善→跳转;稍后→关闭 |
|
||||
|
||||
---
|
||||
|
||||
## 七、快捷自检命令(开发者工具控制台)
|
||||
|
||||
在**调试器 → Console** 中可粘贴以下代码快速模拟状态:
|
||||
|
||||
```javascript
|
||||
// 查看当前 userInfo
|
||||
getApp().globalData.userInfo
|
||||
|
||||
// 手动触发检测(需已登录)
|
||||
getApp().checkVipContactRequiredAndGuide()
|
||||
getApp().checkAvatarNicknameAndGuide()
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、常见问题排查
|
||||
|
||||
| 现象 | 可能原因 |
|
||||
|------|----------|
|
||||
| 未触发 VIP redirect | 1) 非 VIP 2) 已填手机号 3) 当前已在 profile-edit 页 |
|
||||
| 头像未改却未强制跳转 | 检查当前页是否为 profile-edit 或 avatar-nickname(会跳过) |
|
||||
| 后端请求失败 | 确认 soul-api 已启动,baseUrl 正确 |
|
||||
| 登录态丢失 | 检查 token、userInfo 是否持久化 |
|
||||
|
||||
---
|
||||
|
||||
测试完成后,可在本文件末尾记录:✅ 通过 / ❌ 失败及原因。
|
||||
Reference in New Issue
Block a user