Files
soul-yongping/miniprogram/pages/member-detail/member-detail.js
卡若 fa3da12b16 feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本
- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整
- soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新
- soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL
- .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle

Made-with: Cursor
2026-03-23 18:38:23 +08:00

623 lines
25 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 卡若创业派对 - 超级个体/会员详情页
* 接口:优先 /api/miniprogram/vip/members?id=xxVIP回退 /api/miniprogram/users?id=xx任意用户
* 头像/昵称统一用用户资料nickname/avatar优先随「我的」修改实时生效
* mbti, region, industry, position, businessScale, skills,
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
* helpOffer→canHelp, helpNeed→needHelp
* 点头像:登录后依次校验本人头像(非默认)、微信号、绑定手机号,再弹「链接「昵称」」;有 ckbLeadToken 走人物获客计划,否则走全局留资
*/
const app = getApp()
const { trackClick } = require('../../utils/trackClick')
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
Page({
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false },
onLoad(options) {
wx.showShareMenu({ withShareTimeline: true })
const sb = app.globalData.statusBarHeight || 44
const myId = app.globalData.userInfo?.id
const isOwnProfile = !!(options.id && myId && String(options.id) === String(myId))
this.setData({ statusBarHeight: sb, navBarTotalPx: sb + 44, isOwnProfile })
if (options.id) this.loadMember(options.id)
},
/** 本人名片:去完整编辑资料(单页) */
goMyProfileEdit() {
wx.navigateTo({ url: '/pages/profile-edit/profile-edit?full=1' })
},
async loadMember(id) {
const myId = app.globalData.userInfo?.id
const isOwn = !!(myId && id != null && String(id) === String(myId))
if (isOwn && app.globalData.isLoggedIn && myId) {
try {
const res = await app.request({
url: `/api/miniprogram/user/profile?userId=${encodeURIComponent(String(id))}`,
silent: true
})
if (res?.success && res.data) {
const d = res.data
this.setData({
member: this.enrichAndFormat({
id: d.id,
nickname: d.nickname,
name: d.nickname,
avatar: d.avatar,
phone: d.phone,
wechatId: d.wechatId || d.wechat_id,
isVip: !!app.globalData.isVip,
mbti: d.mbti,
region: d.region,
industry: d.industry,
position: d.position,
businessScale: d.businessScale || d.business_scale,
skills: d.skills,
storyBestMonth: d.storyBestMonth || d.story_best_month,
storyAchievement: d.storyAchievement || d.story_achievement,
storyTurning: d.storyTurning || d.story_turning,
helpOffer: d.helpOffer || d.help_offer,
helpNeed: d.helpNeed || d.help_need,
projectIntro: d.projectIntro || d.project_intro,
ckbLeadToken: d.ckbLeadToken || d.ckb_lead_token
}),
loading: false
})
return
}
} catch (e) {}
}
try {
const res = await app.request({ url: `/api/miniprogram/vip/members?id=${id}`, silent: true })
if (res?.success && res.data) {
const d = Array.isArray(res.data) ? res.data[0] : res.data
if (d) { this.setData({ member: this.enrichAndFormat(d), loading: false }); return }
}
} catch (e) {}
try {
const dbRes = await app.request({ url: `/api/miniprogram/users?id=${id}`, silent: true })
if (dbRes?.success && dbRes.data) {
const u = Array.isArray(dbRes.data) ? dbRes.data[0] : dbRes.data
if (u) {
this.setData({ member: this.enrichAndFormat({
id: u.id, name: u.nickname || u.vipName || u.vip_name || '创业者',
avatar: u.avatar || u.vipAvatar || u.vip_avatar || '', isVip: !!(u.is_vip),
contactRaw: u.vipContact || u.vip_contact || u.phone || '',
wechatId: u.wechatId || u.wechat_id,
project: u.vipProject || u.vip_project || u.projectIntro || u.project_intro || '',
industry: u.industry, position: u.position, businessScale: u.businessScale || u.business_scale,
skills: u.skills, mbti: u.mbti, region: u.region,
storyBestMonth: u.storyBestMonth || u.story_best_month,
storyAchievement: u.storyAchievement || u.story_achievement,
storyTurning: u.storyTurning || u.story_turning,
helpOffer: u.helpOffer || u.help_offer,
helpNeed: u.helpNeed || u.help_need,
ckbLeadToken: u.ckbLeadToken || u.ckb_lead_token,
}), loading: false })
return
}
}
} catch (e) {}
this.setData({ loading: false })
},
// 将空值、「未填写」、纯空格均视为未填写(用于隐藏对应项)
_emptyIfPlaceholder(v) {
if (v == null || v === undefined) return ''
const s = String(v).trim()
return (s === '' || s === '未填写') ? '' : s
},
enrichAndFormat(raw) {
const e = (v) => this._emptyIfPlaceholder(v)
const rawAv = raw.avatar || raw.vipAvatar || raw.vip_avatar || ''
const merged = {
id: raw.id,
name: raw.nickname || raw.name || raw.vipName || raw.vip_name || '创业者',
avatar: isSafeImageSrc(rawAv) ? String(rawAv).trim() : '',
isVip: !!(raw.isVip || raw.is_vip),
mbti: e(raw.mbti),
region: e(raw.region),
industry: e(raw.industry),
position: e(raw.position),
businessScale: e(raw.businessScale || raw.business_scale),
skills: e(raw.skills),
contactRaw: raw.contactRaw || raw.vipContact || raw.vip_contact || raw.phone || '',
wechatRaw: raw.wechatRaw || raw.wechatId || raw.wechat_id || '',
bestMonth: e(raw.bestMonth || raw.storyBestMonth || raw.story_best_month),
achievement: e(raw.achievement || raw.storyAchievement || raw.story_achievement),
turningPoint: e(raw.turningPoint || raw.storyTurning || raw.story_turning),
canHelp: e(raw.canHelp || raw.helpOffer || raw.help_offer),
needHelp: e(raw.needHelp || raw.helpNeed || raw.help_need),
project: e(raw.project || raw.vipProject || raw.vip_project || raw.projectIntro || raw.project_intro),
ckbLeadToken: String(raw.ckbLeadToken || raw.ckb_lead_token || '').trim()
}
const contact = merged.contactRaw || ''
const wechat = merged.wechatRaw || ''
const isMatched = (app.globalData.matchedUsers || []).includes(merged.id)
const unlockData = this._getUnlockData(merged.id)
merged.contactDisplay = contact ? (contact.slice(0, 3) + '****' + (contact.length > 7 ? contact.slice(-2) : '')) : ''
merged.contactUnlocked = isMatched || unlockData.contact
merged.contactFull = contact
merged.wechatDisplay = wechat ? (wechat.slice(0, 4) + '****' + (wechat.length > 8 ? wechat.slice(-3) : '')) : ''
merged.wechatUnlocked = isMatched || unlockData.wechat
merged.wechatFull = wechat
return merged
},
_getUnlockData(memberId) {
const userId = app.globalData.userInfo?.id
if (!userId || !memberId) return { contact: false, wechat: false }
try {
const raw = wx.getStorageSync('member_unlocks_' + userId)
if (Array.isArray(raw) && raw.includes(memberId)) {
return { contact: true, wechat: true }
}
const data = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : {}
const member = data[memberId]
return {
contact: !!(member && member.contact),
wechat: !!(member && member.wechat)
}
} catch (e) { return { contact: false, wechat: false } }
},
_addUnlock(memberId, field) {
const userId = app.globalData.userInfo?.id
if (!userId || !memberId || !field) return
let obj = wx.getStorageSync('member_unlocks_' + userId)
if (Array.isArray(obj)) {
obj = obj.reduce((o, id) => { o[id] = { contact: true, wechat: true }; return o }, {})
}
obj = obj && typeof obj === 'object' ? obj : {}
if (!obj[memberId]) obj[memberId] = {}
obj[memberId][field] = true
wx.setStorageSync('member_unlocks_' + userId, obj)
},
_hasUsedFreeForMember(memberId) {
const d = this._getUnlockData(memberId)
return d.contact || d.wechat
},
/** VIP 或本会员首次免费:写入解锁;否则弹开通 VIP */
_tryFreeUnlock(member, field) {
const isVip = app.globalData.isVip
const usedFree = this._hasUsedFreeForMember(member.id)
if (isVip || !usedFree) {
this._addUnlock(member.id, field)
return true
}
wx.showModal({
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
content: '您的免费解锁次数已用完开通VIP会员¥1980/年)可无限解锁',
confirmText: '去开通',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
})
return false
},
/** 链接前:头像需非空且非默认图;微信号需已填写(与 app._needsAvatarNickname 中头像规则一致) */
_hasCustomAvatarForLink(u) {
const avatar = (u && (u.avatar || u.avatarUrl) || '').trim()
return !!avatar && !avatar.includes('default')
},
_hasWechatFilledForLink(u) {
const w = (u && (u.wechatId || u.wechat_id) || wx.getStorageSync('user_wechat') || '').trim()
return w.length > 0
},
/** 拉取最新 profile 写回 globalData返回合并后的访客资料片段 */
async _refreshVisitorProfileForLink() {
const base = app.globalData.userInfo
if (!base?.id) return base || {}
try {
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${base.id}`, silent: true })
if (profileRes?.success && profileRes.data) {
const d = profileRes.data
const updated = { ...app.globalData.userInfo }
if (d.avatar != null && String(d.avatar).trim()) updated.avatar = String(d.avatar).trim()
if (d.wechatId != null) updated.wechatId = d.wechatId
if (d.wechat_id != null) updated.wechat_id = d.wechat_id
app.globalData.userInfo = updated
wx.setStorageSync('userInfo', updated)
if (d.wechatId) wx.setStorageSync('user_wechat', String(d.wechatId).trim())
return updated
}
} catch (e) {}
return base
},
async _ensureVisitorReadyForMemberLink() {
const u = await this._refreshVisitorProfileForLink()
const avatarOk = this._hasCustomAvatarForLink(u)
const wechatOk = this._hasWechatFilledForLink(u)
if (avatarOk && wechatOk) return true
const miss = []
if (!avatarOk) miss.push('头像')
if (!wechatOk) miss.push('微信号')
wx.showModal({
title: '补全本人档案',
content: `链接前请补全本人${miss.join('与')},便于对方识别与安全对接。`,
confirmText: '去填写',
cancelText: '取消',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return false
},
/** 链接前:必须已绑定大陆手机号(与留资接口校验一致) */
async _ensurePhoneBoundForLink(myUserId) {
const { phone } = await this._resolveLeadPhoneWechat(myUserId)
if (phone && /^1[3-9]\d{9}$/.test(phone)) return true
wx.showModal({
title: '请先绑定手机号',
content: '链接对方前需绑定本人手机号,便于跟进与对接。',
confirmText: '去绑定',
cancelText: '取消',
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
})
return false
},
/**
* 点头像:登录 → 头像+微信号 → 手机号 均校验通过后弹窗说明;确认后 POST ckb/lead。
* 有 ckbLeadToken 走人物计划;无 token 走全局留资。对方已公开联系方式时可取消后在下方自行添加。
*/
async startLinkFlow() {
if (this.data.isOwnProfile) return
const member = this.data.member
if (!member) return
const nickname = (member.name || 'TA').trim() || 'TA'
trackClick('member_detail', 'btn_click', '链接头像_' + (member.id || ''))
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
wx.showModal({
title: `链接「${nickname}`,
content: '请先登录后再发起链接。',
confirmText: '去登录',
cancelText: '取消',
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const myUserId = app.globalData.userInfo.id
wx.showLoading({ title: '请稍候', mask: true })
let profileOk = false
let phoneOk = false
try {
profileOk = await this._ensureVisitorReadyForMemberLink()
if (profileOk) phoneOk = await this._ensurePhoneBoundForLink(myUserId)
} finally {
wx.hideLoading()
}
if (!profileOk || !phoneOk) return
const leadTok = (member.ckbLeadToken || '').trim()
const content = leadTok
? `确定后提交联系方式,平台将按对方配置跟进;智能助手与人工协同协助对接。`
: `智能助手与人工会协同跟进,协助您对接「${nickname}」。\n\n若对方已公开手机或微信,可先点「取消」,在页面下方自行添加。`
wx.showModal({
title: `链接「${nickname}`,
content,
confirmText: '确定链接',
cancelText: '取消',
success: (r) => {
if (!r.confirm) return
if (leadTok) this._doCkbLeadSubmit(leadTok, nickname, member.id, nickname)
else this._doGlobalMemberLeadSubmit(member)
}
})
},
/** 无人物 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' })
}
},
async _resolveLeadPhoneWechat(myUserId) {
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) {}
}
return { phone, wechatId }
},
/** 与 read 页 _doMentionAddFriend 一致targetUserId = Person.token可选带超级个体 userId 写入留资 params */
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' })
}
},
_ensureUnlockedForLink(field) {
const member = this.data.member
if (!member?.id || (field !== 'contact' && field !== 'wechat')) return false
if (field === 'wechat' && member.wechatUnlocked) return true
if (field === 'contact' && member.contactUnlocked) return true
if (!app.globalData.isLoggedIn) {
wx.showModal({
title: '需要登录',
content: field === 'wechat'
? '登录后可解锁并复制对方微信号,再按步骤去微信添加好友。'
: '登录后可解锁并复制对方手机号,便于添加好友或回拨。',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return false
}
const d = this._getUnlockData(member.id)
if ((field === 'wechat' && d.wechat) || (field === 'contact' && d.contact)) {
this.setData({ member: this.enrichAndFormat(member) })
return true
}
if (!this._tryFreeUnlock(member, field)) return false
this.setData({ member: this.enrichAndFormat(member) })
return true
},
_copyAndGuideWechat(wechatId) {
if (!wechatId) return
wx.setClipboardData({
data: String(wechatId),
success: () => {
wx.hideToast()
setTimeout(() => {
wx.hideToast()
wx.showModal({
title: '添加微信好友',
content: '微信号已复制。\n\n请打开微信 → 右上角「+」→ 添加朋友 → 粘贴搜索并添加。',
showCancel: false,
confirmText: '知道了'
})
}, 120)
},
fail: () => wx.showToast({ title: '复制失败', icon: 'none' })
})
},
_copyAndGuidePhone(phone) {
if (!phone) return
wx.setClipboardData({
data: String(phone),
success: () => {
wx.hideToast()
setTimeout(() => {
wx.hideToast()
wx.showModal({
title: '联系对方',
content: '手机号已复制。\n\n可打开微信「添加朋友」搜索手机号或使用手机拨号联系对方。',
showCancel: false,
confirmText: '知道了'
})
}, 120)
},
fail: () => wx.showToast({ title: '复制失败', icon: 'none' })
})
},
unlockField(e) {
const field = e.currentTarget.dataset.field
if (!field) return
const member = this.data.member
if (!member?.id || (field !== 'contact' && field !== 'wechat')) return
const isLoggedIn = app.globalData.isLoggedIn
if (!isLoggedIn) {
wx.showModal({
title: '需要登录',
content: '请先登录后再解锁超级个体联系方式',
confirmText: '去登录',
cancelText: '取消',
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
})
return
}
const d = this._getUnlockData(member.id)
if (d[field]) return
if (this._tryFreeUnlock(member, field)) {
const m = this.enrichAndFormat(member)
this.setData({ member: m })
wx.showToast({ title: field === 'contact' ? '已解锁联系方式' : '已解锁微信号', icon: 'success' })
}
},
tapContactRow() {
const m = this.data.member
if (!m || !(m.contactRaw || m.contactDisplay)) return
if (m.contactUnlocked) this.copyContact()
else this.unlockField({ currentTarget: { dataset: { field: 'contact' } } })
},
tapWechatRow() {
const m = this.data.member
if (!m || !(m.wechatRaw || m.wechatDisplay)) return
if (m.wechatUnlocked) this.copyWechat()
else this.unlockField({ currentTarget: { dataset: { field: 'wechat' } } })
},
copyContact() {
const c = this.data.member?.contactFull
if (!c) return
wx.setClipboardData({ data: c, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
},
copyWechat() {
const w = this.data.member?.wechatFull
if (!w) return
wx.setClipboardData({ data: w, success: () => wx.showToast({ title: '已复制', icon: 'success' }) })
},
goToMatch() { wx.switchTab({ url: '/pages/match/match' }) },
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
goBack() { getApp().goBackOrToHome() },
/**
* 分享标题姓名MBTI有则加擅长无擅长时用个人故事或职业一行。总长控制避免微信卡片截断难看。
*/
_buildMemberShareTitle(maxLen = 56) {
const m = this.data.member
if (!m) return '卡若创业派对 · 超级个体'
const clip = (s, n) => {
if (!s) return ''
const t = String(s).replace(/\s+/g, ' ').trim()
return t.length <= n ? t : t.slice(0, n - 1) + '…'
}
const name = clip((m.name || '创业者').trim() || '创业者', 14)
const mbti = (m.mbti || '').trim()
const skills = (m.skills || '').trim()
const achievement = (m.achievement || '').trim()
const bestMonth = (m.bestMonth || '').trim()
const turning = (m.turningPoint || '').trim()
const story = achievement || bestMonth || turning || ''
const jobLine = [m.industry, m.position].filter(Boolean).join('·')
const sep = ''
const nameSeg = name
const mbtiSeg = mbti ? clip(mbti, 8) : ''
const usedBase = nameSeg.length + (mbtiSeg ? sep.length + mbtiSeg.length : 0)
const willTail = !!(skills || story || jobLine || m.region)
const restBudget = Math.max(6, maxLen - usedBase - (willTail ? sep.length : 0))
let tail = ''
if (skills) tail = clip(skills, restBudget)
else if (story) tail = clip(story, restBudget)
else if (jobLine) tail = clip(jobLine, restBudget)
else if (m.region) tail = clip(m.region, restBudget)
let title = nameSeg
if (mbtiSeg) title += sep + mbtiSeg
if (tail) title += sep + tail
title = clip(title.replace(/\s+/g, ' '), maxLen)
if (!title || title.length < 3) title = clip(`${nameSeg}${sep}超级个体`, maxLen)
return title
},
onShareAppMessage() {
const ref = app.getMyReferralCode()
const id = this.data.member?.id
const title = this._buildMemberShareTitle(64)
return {
title,
path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail'
}
},
onShareTimeline() {
const ref = app.getMyReferralCode()
const id = this.data.member?.id
const m = this.data.member
const q = id ? (ref ? `id=${id}&ref=${ref}` : `id=${id}`) : (ref ? `ref=${ref}` : '')
const title = this._buildMemberShareTitle(56)
const res = { title, query: q }
if (m && m.avatar && /^https?:\/\//.test(String(m.avatar))) {
res.imageUrl = m.avatar
}
return res
}
})