This commit is contained in:
Alex-larget
2026-03-24 18:45:32 +08:00
parent dcb7961945
commit f3d74ce94a
68 changed files with 2461 additions and 2535 deletions

View File

@@ -9,7 +9,9 @@ const { checkAndExecute } = require('./utils/ruleEngine.js')
const DEFAULT_APP_ID = 'wxb8bbb2b10dec74aa'
const DEFAULT_MCH_ID = '1318592501'
const DEFAULT_WITHDRAW_TMPL_ID = 'u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE'
const PRODUCTION_BASE_URL = 'https://soulapi.quwanzhi.com'
// baseUrl 手动切换(注释方式):
const API_BASE_URL = 'http://localhost:8080'
// const API_BASE_URL = 'https://soulapi.quwanzhi.com'
const CONFIG_CACHE_KEY = 'mpConfigCacheV1'
// 与上传版本号对齐;设置页展示优先用 wx.getAccountInfoSync().miniProgram.version正式版否则用本字段
const APP_DISPLAY_VERSION = '1.7.2'
@@ -19,8 +21,8 @@ App({
// 与微信后台上传版本号一致,供设置页等展示(避免与线上 version 字段混淆)
appDisplayVersion: APP_DISPLAY_VERSION,
// API仓库默认生产release 强制生产develop/trial 可读 storage「apiBaseUrl」或用 env-switch
baseUrl: 'https://soulapi.quwanzhi.com',
// API仅使用代码常量手动切换,不再使用运行时开关
baseUrl: API_BASE_URL,
// 小程序配置 - 真实AppID
appId: DEFAULT_APP_ID,
@@ -98,39 +100,17 @@ App({
lastVipContactCheck: 0,
// 头像昵称检测:上次检测时间戳(与 VIP 检测同周期刷新)
lastAvatarNicknameCheck: 0,
// 登录后规则引擎触发时间(用于与本地引导去重,避免短时间二次弹窗)
lastAfterLoginRuleCheck: 0,
/** MBTI → 默认头像 URL/api/miniprogram/config/mbti-avatars供推广海报等 */
mbtiAvatarsMap: {},
mbtiAvatarsExpires: 0,
},
/** 正式版强制生产 API避免误传 localhost 导致审核/线上全挂 */
initApiBaseUrl() {
const KEY = 'apiBaseUrl'
try {
const info = wx.getAccountInfoSync?.()
const env = info?.miniProgram?.envVersion || 'release'
if (env === 'release') {
this.globalData.baseUrl = PRODUCTION_BASE_URL
try {
const saved = wx.getStorageSync(KEY)
if (saved && saved !== PRODUCTION_BASE_URL) wx.removeStorageSync(KEY)
} catch (_) {}
return
}
const saved = wx.getStorageSync(KEY)
if (saved && typeof saved === 'string' && /^https?:\/\//.test(saved)) {
this.globalData.baseUrl = String(saved).replace(/\/$/, '')
} else {
this.globalData.baseUrl = PRODUCTION_BASE_URL
}
} catch (_) {
this.globalData.baseUrl = PRODUCTION_BASE_URL
}
},
onLaunch(options) {
this.initApiBaseUrl()
// baseUrl 固定取 API_BASE_URL通过注释切换
this.globalData.baseUrl = API_BASE_URL
// 昵称等隐私组件需先授权input type="nickname" 不会主动触发,需配合 wx.requirePrivacyAuthorize 使用
if (typeof wx.onNeedPrivacyAuthorization === 'function') {
wx.onNeedPrivacyAuthorization((resolve) => {
@@ -616,7 +596,12 @@ App({
const isVip = vipRes?.data?.isVip || this.globalData.isVip || false
this.globalData.isVip = isVip
if (!isVip) {
this.checkAvatarNicknameAndGuide()
const now = Date.now()
const lastRuleCheck = Number(this.globalData.lastAfterLoginRuleCheck || 0)
// 登录后若规则引擎刚触发过 after_login引导由规则引擎负责避免与本地提示连弹
if (!lastRuleCheck || now - lastRuleCheck > 5000) {
this.checkAvatarNicknameAndGuide()
}
return
}
@@ -933,27 +918,6 @@ App({
return (msg && String(msg).trim()) ? String(msg).trim() : defaultMsg
},
_shouldFallbackToProduction(err) {
const msg = String((err && err.errMsg) || (err && err.message) || '').toLowerCase()
return (
msg.includes('connection_failed') ||
msg.includes('err_proxy_connection_failed') ||
msg.includes('dns') ||
msg.includes('name not resolved') ||
msg.includes('failed to fetch') ||
msg.includes('econnrefused') ||
msg.includes('network') ||
msg.includes('timeout')
)
},
_switchBaseUrlToProduction() {
this.globalData.baseUrl = PRODUCTION_BASE_URL
try {
wx.setStorageSync('apiBaseUrl', PRODUCTION_BASE_URL)
} catch (_) {}
},
_requestOnce(url, options = {}, silent = false) {
const showError = (msg) => {
if (!silent && msg) wx.showToast({ title: msg, icon: 'none', duration: 2500 })
@@ -972,6 +936,11 @@ App({
},
success: (res) => {
const data = res.data
const rejectWithBody = (message, body) => {
const err = new Error(message)
if (body != null && typeof body === 'object') err.response = body
reject(err)
}
if (res.statusCode === 200) {
if (data && data.success === false) {
const msg = this._getApiErrorMsg(data, '操作失败')
@@ -979,7 +948,7 @@ App({
this.logout()
}
showError(msg)
reject(new Error(msg))
rejectWithBody(msg, data)
return
}
resolve(data)
@@ -988,12 +957,14 @@ App({
if (res.statusCode === 401) {
this.logout()
showError('未授权,请重新登录')
reject(new Error('未授权'))
const err = new Error('未授权')
if (data != null && typeof data === 'object') err.response = data
reject(err)
return
}
const msg = this._getApiErrorMsg(data, res.statusCode >= 500 ? '服务器异常,请稍后重试' : '请求失败')
showError(msg)
reject(new Error(msg))
rejectWithBody(msg, data && typeof data === 'object' ? data : undefined)
},
fail: (err) => {
const msg = (err && err.errMsg)
@@ -1033,13 +1004,10 @@ App({
}
const promise = this._requestOnce(url, options, silent).catch(async (err) => {
const currentBase = String(this.globalData.baseUrl || '').replace(/\/$/, '')
if (currentBase !== PRODUCTION_BASE_URL && this._shouldFallbackToProduction(err)) {
this._switchBaseUrlToProduction()
return this._requestOnce(url, options, silent)
}
const msg = (err && err.message) ? err.message : '网络异常,请重试'
throw new Error(msg)
const next = new Error(msg)
if (err && err.response != null) next.response = err.response
throw next
})
if (method === 'GET') {
@@ -1102,8 +1070,9 @@ App({
this.globalData.vipExpireDate = user.vipExpireDate || ''
// 首次登录注册:强制跳转 avatar-nickname 修改头像昵称(不弹窗)
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname?from=new_user' }), 1000)
} else {
this.globalData.lastAfterLoginRuleCheck = Date.now()
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
setTimeout(() => this.connectWsHeartbeat(), 2000)
@@ -1173,8 +1142,9 @@ App({
this.globalData.vipExpireDate = user.vipExpireDate || ''
// 首次登录注册:强制跳转 avatar-nickname
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname?from=new_user' }), 1000)
} else {
this.globalData.lastAfterLoginRuleCheck = Date.now()
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
}
@@ -1234,8 +1204,9 @@ App({
// 首次登录注册:强制跳转 avatar-nickname
if (res.isNewUser === true && this._needsAvatarNickname(user)) {
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname' }), 1000)
setTimeout(() => wx.redirectTo({ url: '/pages/avatar-nickname/avatar-nickname?from=new_user' }), 1000)
} else {
this.globalData.lastAfterLoginRuleCheck = Date.now()
checkAndExecute('after_login', null)
setTimeout(() => this.checkVipContactRequiredAndGuide(), 1200)
}

View File

@@ -1,82 +0,0 @@
/**
* 开发环境专用:可拖拽的 baseURL 切换悬浮按钮
* 正式环境release不显示
*/
const PRODUCTION_URL = 'https://soulapi.quwanzhi.com'
const STORAGE_KEY = 'apiBaseUrl'
const POSITION_KEY = 'envSwitchPosition'
const URL_OPTIONS = [
{ label: '生产', url: PRODUCTION_URL },
{ label: '本地', url: 'http://localhost:8080' },
{ label: '测试', url: 'https://souldev.quwanzhi.com' },
]
Component({
data: {
visible: false,
x: 20,
y: 120,
currentLabel: '生产',
areaWidth: 375,
areaHeight: 812,
},
lifetimes: {
attached() {
try {
const accountInfo = wx.getAccountInfoSync?.()
const envVersion = accountInfo?.miniProgram?.envVersion || 'release'
if (envVersion === 'release') {
return
}
const sys = wx.getSystemInfoSync?.() || {}
const areaWidth = sys.windowWidth || 375
const areaHeight = sys.windowHeight || 812
const saved = wx.getStorageSync(POSITION_KEY)
const pos = saved ? JSON.parse(saved) : { x: 20, y: 120 }
// 与 app.js 一致storage 优先,否则用 globalData已按 env 自动切换)
const current = wx.getStorageSync(STORAGE_KEY) || getApp().globalData?.baseUrl || PRODUCTION_URL
const opt = URL_OPTIONS.find(o => o.url === current) || URL_OPTIONS[0]
this.setData({
visible: true,
x: pos.x ?? 20,
y: pos.y ?? 120,
currentLabel: opt.label,
areaWidth,
areaHeight,
})
} catch (_) {
this.setData({ visible: false })
}
},
},
methods: {
onTap() {
const items = URL_OPTIONS.map(o => o.label)
const current = wx.getStorageSync(STORAGE_KEY) || PRODUCTION_URL
const idx = URL_OPTIONS.findIndex(o => o.url === current)
wx.showActionSheet({
itemList: items,
success: (res) => {
const opt = URL_OPTIONS[res.tapIndex]
wx.setStorageSync(STORAGE_KEY, opt.url)
const app = getApp()
if (app && app.globalData) {
app.globalData.baseUrl = opt.url
}
this.setData({ currentLabel: opt.label })
wx.showToast({ title: `已切到${opt.label}`, icon: 'none', duration: 1500 })
},
})
},
onMovableChange(e) {
const { x, y } = e.detail
if (typeof x === 'number' && typeof y === 'number') {
wx.setStorageSync(POSITION_KEY, JSON.stringify({ x, y }))
}
},
},
})

View File

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

View File

@@ -1,13 +0,0 @@
<movable-area wx:if="{{visible}}" class="env-area" style="width:{{areaWidth}}px;height:{{areaHeight}}px;">
<movable-view
class="env-btn"
direction="all"
inertia
x="{{x}}"
y="{{y}}"
bindchange="onMovableChange"
bindtap="onTap"
>
<view class="env-btn-inner">{{currentLabel}}</view>
</movable-view>
</movable-area>

View File

@@ -1,30 +0,0 @@
.env-area {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 9999;
}
.env-btn {
width: 72rpx;
height: 72rpx;
pointer-events: auto;
}
.env-btn-inner {
width: 100%;
height: 100%;
background: linear-gradient(135deg, #22c55e 0%, #16a34a 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 22rpx;
font-weight: 600;
color: #fff;
box-shadow: 0 4rpx 12rpx rgba(34, 197, 94, 0.4);
border: 2rpx solid rgba(255, 255, 255, 0.3);
}

View File

@@ -3,6 +3,7 @@
* 登录后若仍为默认头像/昵称,在此修改;仅头像与昵称两项
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
Page({
data: {
@@ -14,15 +15,21 @@ Page({
nicknameInputFocus: false,
/** 规则引擎传入avatar | nickname用于高亮对应区块 */
uiFocus: '',
fromNewUser: false,
},
onLoad(options) {
this.setData({ statusBarHeight: app.globalData.statusBarHeight || 44 })
const fromNewUser = String(options.from || '').toLowerCase() === 'new_user'
const focus = String(options.focus || '').toLowerCase()
if (focus === 'avatar' || focus === 'nickname') {
this.setData({ uiFocus: focus })
}
this.setData({ fromNewUser })
this.loadFromUser()
if (fromNewUser) {
trackClick('avatar_nickname', 'page_view', '新注册引导页')
}
if (focus === 'nickname') {
setTimeout(() => {
if (typeof wx.requirePrivacyAuthorize === 'function') {
@@ -92,6 +99,7 @@ Page({
async onChooseAvatar(e) {
const tempAvatarUrl = e.detail?.avatarUrl
if (!tempAvatarUrl) return
trackClick('avatar_nickname', 'btn_click', '选择头像')
await this.uploadAndSaveAvatar(tempAvatarUrl)
},
@@ -132,6 +140,9 @@ Page({
}
wx.hideLoading()
wx.showToast({ title: '头像已更新', icon: 'success' })
if (this.data.fromNewUser) {
trackClick('avatar_nickname', 'form_step_done', '头像更新完成')
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '上传失败', icon: 'none' })
@@ -150,6 +161,7 @@ Page({
wx.showToast({ title: '请输入昵称', icon: 'none' })
return
}
trackClick('avatar_nickname', 'btn_click', '完成保存')
this.setData({ saving: true })
try {
await app.request({
@@ -162,6 +174,13 @@ Page({
if (avatar) app.globalData.userInfo.avatar = avatar
wx.setStorageSync('userInfo', app.globalData.userInfo)
}
if (this.data.fromNewUser) {
wx.setStorageSync('new_user_guide_done_at', Date.now())
trackClick('avatar_nickname', 'form_submit', '新注册引导完成', {
hasAvatar: !!avatar,
nicknameLen: nickname.length,
})
}
wx.showToast({ title: '保存成功', icon: 'success' })
setTimeout(() => getApp().goBackOrToHome(), 800)
} catch (e) {
@@ -171,6 +190,7 @@ Page({
},
goToFullProfile() {
trackClick('avatar_nickname', 'nav_click', '编辑完整档案')
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
},
})

View File

@@ -10,10 +10,12 @@
<view class="content">
<view class="guide-card">
<icon name="handshake" size="64" color="#00CED1" customClass="guide-icon"></icon>
<text class="guide-title">设置对外展示信息</text>
<text class="guide-title">{{fromNewUser ? '欢迎加入,先完成基础信息' : '设置对外展示信息'}}</text>
<text class="guide-badge" wx:if="{{fromNewUser}}">新用户引导</text>
<text class="guide-desc" wx:if="{{uiFocus === 'avatar'}}">请先换一张清晰头像,伙伴更容易认出你。</text>
<text class="guide-desc" wx:elif="{{uiFocus === 'nickname'}}">请改一个真实好记的昵称,方便伙伴称呼你。</text>
<text class="guide-desc" wx:else>头像与昵称会出现在名片与匹配卡片上,方便伙伴认出你。</text>
<text class="guide-desc guide-desc-sub" wx:if="{{fromNewUser}}">完成后可继续编辑完整档案手机号、行业、MBTI 等)。</text>
</view>
<!-- 头像:点击直接弹出微信原生选择器;头像与文字水平对齐 -->

View File

@@ -64,11 +64,25 @@
color: #5EEAD4;
margin-bottom: 12rpx;
}
.guide-badge {
margin-bottom: 10rpx;
padding: 6rpx 16rpx;
font-size: 20rpx;
color: #22d3ee;
border-radius: 999rpx;
border: 1rpx solid rgba(34, 211, 238, 0.4);
background: rgba(34, 211, 238, 0.1);
}
.guide-desc {
font-size: 26rpx;
color: rgba(148, 163, 184, 0.95);
line-height: 1.5;
}
.guide-desc-sub {
margin-top: 8rpx;
color: rgba(148, 163, 184, 0.85);
font-size: 24rpx;
}
.avatar-section {
display: flex;

View File

@@ -54,7 +54,8 @@ Page({
// mp_config.mpUi.chaptersPage
chaptersBookTitle: '一场SOUL的创业实验场',
chaptersBookSubtitle: '来自Soul派对房的真实商业故事'
chaptersBookSubtitle: '来自Soul派对房的真实商业故事',
chaptersNewBadgeText: 'NEW'
},
onLoad() {
@@ -71,13 +72,20 @@ Page({
_applyChaptersMpUi() {
const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {}
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
const newBadgeText = String(c.newBadgeText || c.sectionNewBadgeText || h.latestSectionTitle || 'NEW').trim() || 'NEW'
this.setData({
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
'来自Soul派对房的真实商业故事'
'来自Soul派对房的真实商业故事',
chaptersNewBadgeText: newBadgeText
})
},
_normalizeBadgeText(v) {
return String(v || '').trim().slice(0, 8)
},
async loadFeatureConfig() {
try {
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
@@ -122,10 +130,14 @@ Page({
let icon = String(p.icon || '').trim()
if (icon && !isSafeImageSrc(icon)) icon = ''
const iconEmoji = icon ? '' : partEmojiForBodyIndex(idx)
const partBadgeText = this._normalizeBadgeText(
p.badgeText || p.badge_text || p.partBadgeText || p.part_badge_text
)
return {
id: p.id,
icon,
iconEmoji,
iconText: partBadgeText,
title: p.title,
subtitle: p.subtitle || '',
chapterCount: p.chapterCount || 0,
@@ -176,6 +188,9 @@ Page({
isFree: r.isFree === true || (r.price !== undefined && r.price === 0),
price: r.price ?? 1,
isNew: r.isNew === true || r.is_new === true,
newBadgeText: this._normalizeBadgeText(
r.newBadgeText || r.new_badge_text || r.sectionBadgeText || r.section_badge_text || r.badgeText || r.badge_text
),
isPremium
})
})

View File

@@ -73,6 +73,7 @@
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
<view class="part-left">
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFill"/>
<view wx:elif="{{item.iconText}}" class="part-icon part-icon-text">{{item.iconText}}</view>
<view wx:elif="{{item.iconEmoji}}" class="part-icon part-icon-emoji">{{item.iconEmoji}}</view>
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
<view class="part-info">
@@ -101,7 +102,7 @@
<icon wx:else name="lock" size="24" color="rgba(255,255,255,0.3)" customClass="section-lock lock-closed"></icon>
</view>
<text class="section-title {{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1 ? '' : 'text-muted'}}">{{section.id}} {{section.title}}</text>
<text wx:if="{{section.isNew}}" class="tag tag-new">NEW</text>
<text wx:if="{{section.isNew || section.newBadgeText}}" class="tag tag-new">{{section.newBadgeText || chaptersNewBadgeText}}</text>
</view>
<view class="section-right">
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>

View File

@@ -380,6 +380,13 @@
color: #ffffff;
}
.part-icon-text {
font-size: 22rpx;
font-weight: 700;
letter-spacing: 1rpx;
line-height: 1;
}
.part-subtitle {
font-size: 20rpx;
color: rgba(255, 255, 255, 0.4);

View File

@@ -8,6 +8,7 @@
* 点头像:登录后依次校验本人头像(非默认)、微信号、绑定手机号,再弹「链接「昵称」」;有 ckbLeadToken 走人物获客计划,否则走全局留资
*/
const app = getApp()
const soulBridge = require('../../utils/soulBridge.js')
const { trackClick } = require('../../utils/trackClick')
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
const { resolveAvatarWithMbti } = require('../../utils/mbtiAvatar.js')
@@ -334,45 +335,13 @@ Page({
/** 无人物 token 时:全局留资,便于运营侧主动加好友并协助链接该会员 */
async _doGlobalMemberLeadSubmit(member) {
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
const myUserId = app.globalData.userInfo.id
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于工作人员联系您、协助链接该超级个体。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetNickname: '',
targetMemberId: member.id || undefined,
targetMemberName: (member.name || '').trim() || undefined,
source: 'member_detail_global',
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,请留意工作人员联系', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetMemberId: String(member.id || ''),
targetMemberName: (member.name || '').trim(),
targetNickname: '',
source: 'member_detail_global',
phoneModalContent: '请填写手机号(必填),便于工作人员联系您、协助链接该超级个体。',
})
},
async _resolveLeadPhoneWechat(myUserId) {
@@ -390,58 +359,16 @@ Page({
return { phone, wechatId }
},
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token可选带超级个体 userId 写入留资 params */
/** targetUserId = Person.token可选带超级个体 id/name 写入留资 params(与 read 页 @ 同 soulBridge */
async _doCkbLeadSubmit(targetUserId, targetNickname, targetMemberId, targetMemberName) {
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 myUserId = app.globalData.userInfo.id
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于对方通过获客计划联系您。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
targetMemberId: targetMemberId || undefined,
targetMemberName: targetMemberName || undefined,
source: 'member_detail_avatar'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(app, {
targetUserId,
targetNickname,
targetMemberId: targetMemberId || undefined,
targetMemberName: targetMemberName || undefined,
source: 'member_detail_avatar',
phoneModalContent: '请填写手机号(必填),便于对方通过获客计划联系您。',
})
},
_ensureUnlockedForLink(field) {

View File

@@ -1054,7 +1054,17 @@ Page({
this.loadWalletBalance()
} catch (e) {
wx.hideLoading()
wx.showToast({ title: e.message || '提现失败', icon: 'none' })
const resp = e && e.response
if (resp && (resp.needBind || resp.needBindWechat)) {
wx.showModal({
title: resp.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: resp.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/settings/settings' }) }
})
return
}
wx.showToast({ title: (e && e.message) || '提现失败', icon: 'none' })
}
}
})

View File

@@ -140,6 +140,15 @@
.save-btn[disabled] { opacity: 0.6; }
.bottom-space { height: 120rpx; }
/* 分享图绘制用 canvas仅用于离屏生成不在页面可见 */
.share-card-canvas {
position: fixed;
left: -9999px;
top: -9999px;
opacity: 0;
pointer-events: none;
}
/* 昵称提示文案 */
.input-tip {
margin-top: 8rpx;
@@ -234,93 +243,7 @@
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;

View File

@@ -19,6 +19,7 @@ const { parseScene } = require('../../utils/scene.js')
const contentParser = require('../../utils/contentParser.js')
const { trackClick } = require('../../utils/trackClick')
const { checkAndExecute } = require('../../utils/ruleEngine')
const soulBridge = require('../../utils/soulBridge.js')
const app = getApp()
@@ -729,71 +730,14 @@ Page({
})
},
// 边界:未登录→去登录;无手机/微信号→去资料编辑;重复同一人→本地 key 去重
// 正文 @:统一走 soulBridge登录/手机号校验/提交与错误提示一处维护)
async _doMentionAddFriend(targetUserId, targetNickname) {
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 myUserId = app.globalData.userInfo.id
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 || 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 || !/^1[3-9]\d{9}$/.test(phone)) {
wx.showModal({
title: '补全手机号',
content: '请填写手机号(必填),便于对方联系您。',
confirmText: '去填写',
cancelText: '取消',
success: (res) => {
if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
}
})
return
}
wx.showLoading({ title: '提交中...', mask: true })
try {
const res = await app.request({
url: '/api/miniprogram/ckb/lead',
method: 'POST',
data: {
userId: myUserId,
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
source: 'article_mention'
}
})
wx.hideLoading()
if (res && res.success) {
wx.setStorageSync('lead_last_submit_ts', Date.now())
wx.showToast({ title: res.message || '提交成功,对方会尽快联系您', icon: 'success' })
} else {
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
}
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
}
await soulBridge.submitCkbLead(getApp(), {
targetUserId,
targetNickname,
source: 'article_mention',
phoneModalContent: '请填写手机号(必填),便于对方联系您。',
})
},
// 分享弹窗
@@ -1245,7 +1189,7 @@ Page({
try {
// 0. 尝试余额支付(若余额足够)
const userId = app.globalData.userInfo?.id
const referralCode = wx.getStorageSync('referral_code') || ''
const referralCode = soulBridge.getReferralCodeForPay(app)
if (userId) {
try {
const balanceRes = await app.request({ url: `/api/miniprogram/balance?userId=${userId}`, silent: true })
@@ -1315,8 +1259,8 @@ Page({
? '《一场Soul的创业实验》全书'
: `章节${sectionId}-${sectionTitle.length > 20 ? sectionTitle.slice(0, 20) + '...' : sectionTitle}`
// 邀请码:谁邀请了我(从落地页 ref 或 storage 带入),会写入订单 referrer_id / referral_code 便于分销与对账
const referralCode = wx.getStorageSync('referral_code') || ''
// 邀请码:与 VIP/钱包一致,优先 storage 落地 ref否则回落本人推荐码便于自购归因
const referralCode = soulBridge.getReferralCodeForPay(app)
const res = await app.request('/api/miniprogram/pay', {
method: 'POST',
data: {

View File

@@ -677,7 +677,7 @@ Page({
wx.navigateTo({ url: '/pages/withdraw-records/withdraw-records' })
},
// 执行提现
// 执行提现success:false 时 app.request 会 reject 并挂 e.response需 catch 里处理 needBindWechat
async doWithdraw(amount) {
wx.showLoading({ title: '提现中...' })
@@ -706,24 +706,23 @@ Page({
// 刷新数据(此时待审核金额会增加,可提现金额会减少)
this.initData()
} else {
if (res.needBind || res.needBindWechat) {
wx.showModal({
title: res.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: res.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
} else {
wx.showToast({ title: res.message || res.error || '提现失败', icon: 'none', duration: 3000 })
}
}
} catch (e) {
wx.hideLoading()
console.error('[Referral] 提现失败:', e)
wx.showToast({ title: '提现失败,请重试', icon: 'none' })
const resp = e && e.response
if (resp && (resp.needBind || resp.needBindWechat)) {
wx.showModal({
title: resp.needBindWechat ? '请先绑定微信号' : '需要绑定微信',
content: resp.needBindWechat ? '请到「设置」中绑定微信号后再提现,便于到账核对。' : '请先在设置中绑定微信账号后再提现',
confirmText: '去绑定',
success: (modalRes) => {
if (modalRes.confirm) wx.navigateTo({ url: '/pages/settings/settings' })
}
})
return
}
wx.showToast({ title: (e && e.message) || '提现失败,请重试', icon: 'none' })
}
},

View File

@@ -54,8 +54,14 @@ function stripTrailingAtForMention(before) {
* 将一个 HTML block 字符串解析为 segments 数组
* 处理三种内联元素mention / linkTag(span) / linkTag(a) / img
*/
function parseBlockToSegments(block) {
function parseBlockToSegments(block, config) {
const segs = []
const normalize = s => (s || '').trim().toLowerCase()
const personTokenSet = new Set()
for (const p of ((config && config.persons) || [])) {
const token = normalize((p && p.token) || '')
if (token) personTokenSet.add(token)
}
// 合并匹配所有内联元素
const tokenRe = /<span[^>]*data-type="mention"[^>]*>[\s\S]*?<\/span>|<span[^>]*data-type="linkTag"[^>]*>[\s\S]*?<\/span>|<a[^>]*href="([^"]*)"[^>]*>(#[^<]*)<\/a>|<img[^>]*\/?>/gi
let lastEnd = 0
@@ -78,8 +84,12 @@ function parseBlockToSegments(block) {
const userId = idMatch ? idMatch[1].trim() : ''
let nickname = labelMatch ? labelMatch[1] : innerText.replace(/^[@]\s*/, '')
nickname = cleanMentionNickname((nickname || '').trim())
if (userId || nickname) {
const userExists = !!normalize(userId) && personTokenSet.has(normalize(userId))
if (userExists && nickname) {
segs.push({ type: 'mention', userId, nickname, mentionDisplay: '@' + nickname })
} else if (nickname) {
// 被 @ 人物不存在时降级为普通文本,保持“静态 @某人”展示
segs.push({ type: 'text', text: '@' + nickname })
}
} else if (/data-type="linkTag"/i.test(tag)) {
@@ -155,7 +165,7 @@ function parseHtmlToSegments(html, config) {
for (const block of blocks) {
if (!block.trim()) continue
let blockSegs = parseBlockToSegments(block)
let blockSegs = parseBlockToSegments(block, config)
if (!blockSegs.length) continue
// 纯图片行独立成段

View File

@@ -59,16 +59,20 @@ function syncOrderStatusQuery(app, orderSn) {
/**
* 提交存客宝 lead与阅读页 @、会员详情点头像同接口)
* @param {object} app getApp()
* @param {{ targetUserId: string, targetNickname?: string, source: string, phoneModalContent?: string }} opts
* @param {{ targetUserId?: string, targetNickname?: string, targetMemberId?: string, targetMemberName?: string, source: string, phoneModalContent?: string }} opts
* @returns {Promise<boolean>} 是否提交成功
*/
async function submitCkbLead(app, opts) {
const targetUserId = (opts && opts.targetUserId) || ''
const targetNickname = ((opts && opts.targetNickname) || 'TA').trim() || 'TA'
const targetUserId = ((opts && opts.targetUserId) || '').trim()
const targetMemberId = ((opts && opts.targetMemberId) || '').trim()
let targetNickname = (opts && opts.targetNickname != null) ? String(opts.targetNickname).trim() : ''
if (targetUserId && !targetNickname) targetNickname = 'TA'
const targetMemberName = ((opts && opts.targetMemberName) || '').trim()
const source = (opts && opts.source) || 'article_mention'
const phoneModalContent = (opts && opts.phoneModalContent) || '请先填写手机号(必填),以便对方联系您'
if (!targetUserId) return false
// 文章 @ 为 token会员详情无 token 时用 targetMemberId 走全局获客计划(与后端 CKBLead 一致)
if (!targetUserId && !targetMemberId) return false
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
return await new Promise((resolve) => {
@@ -124,8 +128,10 @@ async function submitCkbLead(app, opts) {
phone: phone || undefined,
wechatId: wechatId || undefined,
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
targetUserId,
targetNickname: targetNickname || undefined,
targetUserId: targetUserId || undefined,
targetNickname: targetNickname !== '' ? targetNickname : undefined,
targetMemberId: targetMemberId || undefined,
targetMemberName: targetMemberName || undefined,
source
}
})
@@ -141,7 +147,9 @@ async function submitCkbLead(app, opts) {
return false
} catch (e) {
wx.hideLoading()
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
const resp = e && e.response
const hint = (resp && (resp.message || resp.error)) || (e && e.message) || '提交失败'
wx.showToast({ title: String(hint), icon: 'none' })
return false
}
}