feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调
- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go) - 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken) - 阅读页分享朋友圈复制与 toast 去重 - soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整 - 脚本:content_upload、miniprogram 上传辅助等 Made-with: Cursor
This commit is contained in:
@@ -29,6 +29,9 @@ Page({
|
||||
|
||||
// 已加载的篇章章节缓存 { partId: chapters }
|
||||
_loadedChapters: {},
|
||||
|
||||
// 小三角点击动画:当前触发的子章 id(与 chapter.id 比对)
|
||||
_triangleAnimating: '',
|
||||
|
||||
// 固定模块 id -> mid(序言/尾声/附录,供 goToRead 传 mid)
|
||||
fixedSectionsMap: {},
|
||||
@@ -152,6 +155,12 @@ Page({
|
||||
})
|
||||
})
|
||||
const chapters = Array.from(chMap.values())
|
||||
chapters.forEach(ch => ch.sections.reverse())
|
||||
// 目录子章下列表:默认最多展示 5 条,点小三角每次再展开 5 条
|
||||
chapters.forEach((ch) => {
|
||||
const n = ch.sections.length
|
||||
ch.sectionVisibleLimit = n === 0 ? 0 : Math.min(5, n)
|
||||
})
|
||||
const loaded = { ...this.data._loadedChapters, [partId]: chapters }
|
||||
const bookData = this.data.bookData.map(p =>
|
||||
p.id === partId ? { ...p, chapters } : p
|
||||
@@ -227,6 +236,43 @@ Page({
|
||||
if (isExpanding) await this.loadChaptersByPart(partId)
|
||||
},
|
||||
|
||||
expandSectionChapter(e) {
|
||||
const partId = e.currentTarget.dataset.partId
|
||||
const chapterId = e.currentTarget.dataset.chapterId
|
||||
if (!partId || !chapterId) return
|
||||
trackClick('chapters', 'tab_click', '目录_子章展开5条')
|
||||
|
||||
const part = this.data.bookData.find((p) => p.id === partId)
|
||||
const chapter = part && (part.chapters || []).find((c) => c.id === chapterId)
|
||||
if (!chapter || !chapter.sections || chapter.sections.length === 0) return
|
||||
|
||||
const total = chapter.sections.length
|
||||
const cur = typeof chapter.sectionVisibleLimit === 'number' ? chapter.sectionVisibleLimit : Math.min(5, total)
|
||||
const next = Math.min(cur + 5, total)
|
||||
if (next === cur) return
|
||||
|
||||
const bookData = this.data.bookData.map((p) => {
|
||||
if (p.id !== partId) return p
|
||||
return {
|
||||
...p,
|
||||
chapters: (p.chapters || []).map((ch) =>
|
||||
ch.id === chapterId ? { ...ch, sectionVisibleLimit: next } : ch
|
||||
),
|
||||
}
|
||||
})
|
||||
|
||||
// 先去掉动画 class 再打上,便于连续点击重复触发动画
|
||||
this.setData({ _triangleAnimating: '', bookData })
|
||||
setTimeout(() => {
|
||||
this.setData({ _triangleAnimating: chapterId })
|
||||
setTimeout(() => {
|
||||
if (this.data._triangleAnimating === chapterId) {
|
||||
this.setData({ _triangleAnimating: '' })
|
||||
}
|
||||
}, 480)
|
||||
}, 30)
|
||||
},
|
||||
|
||||
// 跳转到阅读页(优先传 mid,与分享逻辑一致)
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
@@ -88,8 +88,8 @@
|
||||
<block wx:for="{{item.chapters}}" wx:key="id" wx:for-item="chapter">
|
||||
<view class="chapter-header">{{chapter.title}}</view>
|
||||
<view class="section-list">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section">
|
||||
<view class="section-item" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<block wx:for="{{chapter.sections}}" wx:key="id" wx:for-item="section" wx:for-index="secIdx">
|
||||
<view class="section-item" wx:if="{{secIdx < chapter.sectionVisibleLimit}}" bindtap="goToRead" data-id="{{section.id}}" data-mid="{{section.mid}}">
|
||||
<view class="section-left">
|
||||
<view class="section-lock-wrap">
|
||||
<icon wx:if="{{section.isFree || isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" name="lock-open" size="24" color="#00CED1" customClass="section-lock lock-open"></icon>
|
||||
@@ -100,12 +100,14 @@
|
||||
</view>
|
||||
<view class="section-right">
|
||||
<text wx:if="{{section.isFree}}" class="tag tag-free">免费</text>
|
||||
<text wx:elif="{{isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1}}" class="tag tag-purchased">已解锁</text>
|
||||
<text wx:else class="section-price">¥{{section.price}}</text>
|
||||
<text wx:elif="{{!(isVip || (!section.isPremium && hasFullBook) || purchasedSections.indexOf(section.id) > -1)}}" class="section-price">¥{{section.price}}</text>
|
||||
<icon name="chevron-right" size="24" color="rgba(255,255,255,0.3)" customClass="section-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
<view class="section-expand-trigger" wx:if="{{chapter.sections.length > chapter.sectionVisibleLimit}}" bindtap="expandSectionChapter" data-part-id="{{item.id}}" data-chapter-id="{{chapter.id}}">
|
||||
<view class="latest-expand-triangle {{_triangleAnimating === chapter.id ? 'tri-bounce' : ''}}"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -577,6 +577,49 @@
|
||||
color: rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
/* ===== 展开三角 ===== */
|
||||
.section-expand-trigger {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 0 12rpx;
|
||||
}
|
||||
|
||||
.latest-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 18rpx solid transparent;
|
||||
border-right: 18rpx solid transparent;
|
||||
border-top: 14rpx solid rgba(0, 206, 209, 0.55);
|
||||
opacity: 0.85;
|
||||
transform-origin: 50% 0;
|
||||
transition: border-top-color 0.15s ease;
|
||||
}
|
||||
|
||||
.section-expand-trigger:active .latest-expand-triangle {
|
||||
border-top-color: #00CED1;
|
||||
}
|
||||
|
||||
@keyframes catalog-tri-nudge {
|
||||
0% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
40% {
|
||||
transform: translateY(10rpx) scale(1.12);
|
||||
opacity: 1;
|
||||
border-top-color: #00CED1;
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0) scale(1);
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.latest-expand-triangle.tri-bounce {
|
||||
animation: catalog-tri-nudge 0.45s ease-out;
|
||||
}
|
||||
|
||||
/* ===== 底部留白 ===== */
|
||||
.bottom-space {
|
||||
height: 40rpx;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/**
|
||||
* 卡若创业派对 - 开发登录页
|
||||
* 临时:账户=手机号,密码可空,用于切换为对方账号调试
|
||||
* 卡若创业派对 - 开发登录页(仅本地调试)
|
||||
* 勿写入 app.json pages:提审包不得注册本页。
|
||||
* 需要用时在 app.json 的 pages 数组末尾临时加入 "pages/dev-login/dev-login"。
|
||||
*/
|
||||
const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
|
||||
@@ -4,10 +4,23 @@
|
||||
* 技术支持: 存客宝
|
||||
*/
|
||||
|
||||
console.log('[Index] ===== 首页文件开始加载 =====')
|
||||
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser')
|
||||
|
||||
/** 与首页固定「卡若」获客位重复时从横滑列表剔除(含历史误写「卡路」) */
|
||||
function isKaruoHostDuplicateName(displayName) {
|
||||
const s = String(displayName || '').trim()
|
||||
return s === '卡若' || s === '卡路'
|
||||
}
|
||||
|
||||
/** 超级个体无头像占位:仅展示中文首字,避免头像圆里出现英文字母 */
|
||||
function superAvatarLetter(displayName) {
|
||||
const s = String(displayName || '').trim()
|
||||
if (!s) return '会'
|
||||
const ch = s[0]
|
||||
return /[\u4e00-\u9fff]/.test(ch) ? ch : '会'
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -31,8 +44,8 @@ Page({
|
||||
{ id: '8.1', title: '流量杠杆:抖音、Soul、飞书', tag: '推荐', tagClass: 'tag-purple', part: '真实的赚钱' }
|
||||
],
|
||||
|
||||
// 最新章节(动态计算)
|
||||
latestSection: null,
|
||||
// Banner 推荐(优先用 recommended API 第一条,回退 latest-chapters)
|
||||
bannerSection: null,
|
||||
latestLabel: '最新更新',
|
||||
|
||||
// 内容概览
|
||||
@@ -66,8 +79,7 @@ Page({
|
||||
// 展开状态(首页精选/最新)
|
||||
featuredExpanded: false,
|
||||
latestExpanded: false,
|
||||
featuredSectionsFull: [], // 展开时用 book/hot 加载的完整列表
|
||||
featuredExpandedLoading: false,
|
||||
featuredSectionsFull: [], // 精选排行榜全量(最多 50),默认只展示前 3 条
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true,
|
||||
@@ -136,28 +148,22 @@ Page({
|
||||
async loadSuperMembers() {
|
||||
this.setData({ superMembersLoading: true })
|
||||
try {
|
||||
// 并行请求 VIP 会员和普通用户,合并后取前 4 个(VIP 优先)
|
||||
const [vipRes, usersRes] = await Promise.all([
|
||||
app.request({ url: '/api/miniprogram/vip/members', silent: true }).catch(() => null),
|
||||
app.request({ url: '/api/miniprogram/users?limit=20', silent: true }).catch(() => null)
|
||||
])
|
||||
// 仅走后端 VIP 列表排序(vip_sort、vip_activated_at),不在端上拼普通用户
|
||||
const vipRes = await app.request({ url: '/api/miniprogram/vip/members?limit=24', silent: true }).catch(() => null)
|
||||
let members = []
|
||||
if (vipRes && vipRes.success && Array.isArray(vipRes.data) && vipRes.data.length > 0) {
|
||||
members = vipRes.data.slice(0, 4).map(u => ({
|
||||
id: u.id,
|
||||
name: u.nickname || u.vipName || u.vip_name || '会员',
|
||||
avatar: u.avatar || '',
|
||||
isVip: true
|
||||
}))
|
||||
if (members.length > 0) console.log('[Index] 超级个体加载成功:', members.length, '人')
|
||||
}
|
||||
if (members.length < 4 && usersRes && usersRes.success && Array.isArray(usersRes.data)) {
|
||||
const existIds = new Set(members.map(m => m.id))
|
||||
const extra = usersRes.data
|
||||
.filter(u => u.avatar && u.nickname && !existIds.has(u.id))
|
||||
.slice(0, 4 - members.length)
|
||||
.map(u => ({ id: u.id, name: u.nickname, avatar: u.avatar, isVip: u.is_vip === 1 }))
|
||||
members = members.concat(extra)
|
||||
members = vipRes.data.map(u => {
|
||||
const raw = u.name || u.nickname || u.vipName || u.vip_name || '会员'
|
||||
const name = cleanSingleLineField(raw) || '会员'
|
||||
return {
|
||||
id: u.id,
|
||||
name,
|
||||
avatar: u.avatar || '',
|
||||
isVip: true,
|
||||
avatarLetter: superAvatarLetter(name)
|
||||
}
|
||||
}).filter((m) => !isKaruoHostDuplicateName(m.name))
|
||||
console.log('[Index] 超级个体(后端排序):', members.length, '人')
|
||||
}
|
||||
this.setData({ superMembers: members, superMembersLoading: false })
|
||||
} catch (e) {
|
||||
@@ -166,48 +172,79 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 精选推荐 + 最新更新 + 最新列表:一次请求 recommended + latest-chapters,避免重复
|
||||
// 精选推荐 + 最新更新 + 最新列表:顺序以后端为准(recommended=排行榜算法,latest=updated_at)
|
||||
async loadFeaturedAndLatest() {
|
||||
try {
|
||||
const excludeFixed = (c) => {
|
||||
const pt = (c.part_title || c.partTitle || '').toLowerCase()
|
||||
return !pt.includes('序言') && !pt.includes('尾声') && !pt.includes('附录')
|
||||
const tagClassForTag = (tag) => (tag === '热门' ? 'tag-hot' : 'tag-rec')
|
||||
const toSectionFromRanking = (s) => {
|
||||
const tag = s.tag || '精选'
|
||||
return {
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag,
|
||||
tagClass: tagClassForTag(tag)
|
||||
}
|
||||
}
|
||||
const fallbackTags = ['热门', '推荐', '精选']
|
||||
const toSectionFromHot = (s, i) => {
|
||||
const tag = fallbackTags[i % 3]
|
||||
return {
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag,
|
||||
tagClass: tagClassForTag(tag)
|
||||
}
|
||||
}
|
||||
const toSection = (s, i, tagMap = ['热门', '推荐', '精选']) => ({
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.section_title || s.sectionTitle || s.title || s.chapterTitle || '',
|
||||
part: (s.part_title || s.partTitle || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: s.tag || tagMap[i] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i] || 'tag-rec'
|
||||
})
|
||||
|
||||
const [recRes, latestRes] = await Promise.all([
|
||||
app.request({ url: '/api/miniprogram/book/recommended', silent: true }).catch(() => null),
|
||||
app.request({ url: '/api/miniprogram/book/recommended?limit=50', silent: true }).catch(() => null),
|
||||
app.request({ url: '/api/miniprogram/book/latest-chapters', silent: true }).catch(() => null)
|
||||
])
|
||||
|
||||
// 1. 精选推荐(recommended → hot 兜底)
|
||||
let featured = []
|
||||
// 1. 精选推荐:一次拉全量(≤50),默认只显示 3 条;点列表下三角展开(与「最新新增」一致)
|
||||
let featuredFull = []
|
||||
if (recRes && recRes.success && Array.isArray(recRes.data) && recRes.data.length > 0) {
|
||||
featured = recRes.data.map((s, i) => toSection(s, i))
|
||||
featuredFull = recRes.data.map((s) => toSectionFromRanking(s))
|
||||
}
|
||||
if (featured.length === 0) {
|
||||
if (featuredFull.length === 0) {
|
||||
try {
|
||||
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=10', silent: true })
|
||||
const hotRes = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
|
||||
const hotList = (hotRes && hotRes.data) ? hotRes.data : []
|
||||
if (hotList.length > 0) featured = hotList.slice(0, 3).map((s, i) => toSection(s, i))
|
||||
if (hotList.length > 0) featuredFull = hotList.map((s, i) => toSectionFromHot(s, i))
|
||||
} catch (e) { console.log('[Index] book/hot 兜底失败:', e) }
|
||||
}
|
||||
if (featured.length > 0) this.setData({ featuredSections: featured })
|
||||
if (featuredFull.length > 0) {
|
||||
this.setData({
|
||||
featuredSectionsFull: featuredFull,
|
||||
featuredSections: featuredFull.slice(0, 3),
|
||||
featuredExpanded: false
|
||||
})
|
||||
} else {
|
||||
this.setData({
|
||||
featuredSectionsFull: [],
|
||||
featuredSections: [],
|
||||
featuredExpanded: false
|
||||
})
|
||||
}
|
||||
|
||||
// 2. 最新更新 + 最新列表(共用 latest-chapters 数据)
|
||||
// 2. Banner 推荐:优先取 recommended 第一条,回退 latest 第一条
|
||||
const rawList = (latestRes && latestRes.data) ? latestRes.data : []
|
||||
const latestList = rawList.filter(excludeFixed)
|
||||
if (latestList.length > 0) {
|
||||
// 按更新时间倒序,最新在前(与后台展示一致)
|
||||
const latestList = [...rawList].sort((a, b) => {
|
||||
const ta = new Date(a.updatedAt || a.updated_at || 0).getTime()
|
||||
const tb = new Date(b.updatedAt || b.updated_at || 0).getTime()
|
||||
return tb - ta
|
||||
})
|
||||
if (featuredFull.length > 0) {
|
||||
this.setData({ bannerSection: featuredFull[0] })
|
||||
} else if (latestList.length > 0) {
|
||||
const l = latestList[0]
|
||||
this.setData({
|
||||
latestSection: {
|
||||
bannerSection: {
|
||||
id: l.id,
|
||||
mid: l.mid ?? l.MID ?? 0,
|
||||
title: l.section_title || l.sectionTitle || l.title || l.chapterTitle || '',
|
||||
@@ -334,12 +371,6 @@ Page({
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
// 2 分钟内只能点一次(与后端限频一致)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
let phone = (app.globalData.userInfo.phone || '').trim()
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || '').trim()
|
||||
if (!phone && !wechatId) {
|
||||
@@ -439,11 +470,6 @@ Page({
|
||||
wx.showToast({ title: '请输入正确的手机号', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const app = getApp()
|
||||
const userId = app.globalData.userInfo?.id
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
@@ -501,50 +527,24 @@ Page({
|
||||
wx.switchTab({ url: '/pages/match/match' })
|
||||
},
|
||||
|
||||
// 精选推荐:展开/折叠
|
||||
async toggleFeaturedExpanded() {
|
||||
if (this.data.featuredExpandedLoading) return
|
||||
trackClick('home', 'tab_click', this.data.featuredExpanded ? '精选收起' : '精选展开')
|
||||
if (this.data.featuredExpanded) {
|
||||
const collapsed = this.data.featuredSectionsFull.length > 0 ? this.data.featuredSectionsFull.slice(0, 3) : this.data.featuredSections
|
||||
this.setData({ featuredExpanded: false, featuredSections: collapsed })
|
||||
return
|
||||
}
|
||||
if (this.data.featuredSectionsFull.length > 0) {
|
||||
this.setData({ featuredExpanded: true, featuredSections: this.data.featuredSectionsFull })
|
||||
return
|
||||
}
|
||||
this.setData({ featuredExpandedLoading: true })
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/book/hot?limit=50', silent: true })
|
||||
const list = (res && res.data) ? res.data : []
|
||||
const tagMap = ['热门', '推荐', '精选']
|
||||
const full = list.map((s, i) => ({
|
||||
id: s.id || s.section_id,
|
||||
mid: s.mid ?? s.MID ?? 0,
|
||||
title: s.sectionTitle || s.section_title || s.title || s.chapterTitle || '',
|
||||
part: (s.partTitle || s.part_title || '').replace(/[_||]/g, ' ').trim(),
|
||||
tag: tagMap[i % 3] || '精选',
|
||||
tagClass: ['tag-hot', 'tag-rec', 'tag-rec'][i % 3] || 'tag-rec'
|
||||
}))
|
||||
this.setData({
|
||||
featuredSectionsFull: full,
|
||||
featuredSections: full,
|
||||
featuredExpanded: true,
|
||||
featuredExpandedLoading: false
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[Index] 加载精选更多失败:', e)
|
||||
this.setData({ featuredExpandedLoading: false })
|
||||
}
|
||||
// 精选推荐:列表下方小三角展开(数据已在 loadFeaturedAndLatest 一次拉齐)
|
||||
expandFeaturedChapters() {
|
||||
if (this.data.featuredExpanded) return
|
||||
const full = this.data.featuredSectionsFull || []
|
||||
if (full.length <= 3) return
|
||||
trackClick('home', 'tab_click', '精选展开_底部三角')
|
||||
this.setData({ featuredExpanded: true, featuredSections: full })
|
||||
},
|
||||
|
||||
// 最新新增:展开/折叠(默认 5 条,点击展开剩余)
|
||||
toggleLatestExpanded() {
|
||||
trackClick('home', 'tab_click', this.data.latestExpanded ? '最新收起' : '最新展开')
|
||||
const expanded = !this.data.latestExpanded
|
||||
const display = expanded ? this.data.latestChapters : this.data.latestChapters.slice(0, 5)
|
||||
this.setData({ latestExpanded: expanded, displayLatestChapters: display })
|
||||
// 最新新增:列表下方小三角展开(无「收起」,展开后整页向下滚动查看)
|
||||
expandLatestChapters() {
|
||||
if (this.data.latestExpanded) return
|
||||
trackClick('home', 'tab_click', '最新展开_底部三角')
|
||||
const full = this.data.latestChapters || []
|
||||
this.setData({
|
||||
latestExpanded: true,
|
||||
displayLatestChapters: full
|
||||
})
|
||||
},
|
||||
|
||||
goToMemberDetail(e) {
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
<!-- 自定义导航栏占位 -->
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部区域(按设计稿:S 图标 + 标题副标题 | 点击链接卡若) -->
|
||||
<!-- 顶部区域:中文标识 + 标题副标题 | 链接卡若 -->
|
||||
<view class="header">
|
||||
<view class="header-content">
|
||||
<view class="logo-section">
|
||||
<view class="logo-icon">
|
||||
<text class="logo-text">S</text>
|
||||
<text class="logo-text">派</text>
|
||||
</view>
|
||||
<view class="logo-info">
|
||||
<text class="logo-title-text">卡若创业派对</text>
|
||||
@@ -19,7 +19,7 @@
|
||||
<view class="header-right">
|
||||
<view class="contact-btn" bindtap="onLinkKaruo">
|
||||
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
|
||||
<text class="contact-text">点击链接卡若</text>
|
||||
<text class="contact-name">点击链接卡若</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -35,13 +35,13 @@
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<view class="main-content">
|
||||
<!-- Banner卡片 - 最新章节(异步加载) -->
|
||||
<view class="banner-card" wx:if="{{latestSection}}" bindtap="goToRead" data-id="{{latestSection.id}}" data-mid="{{latestSection.mid}}">
|
||||
<!-- Banner 推荐卡片(优先 recommended API 第一条) -->
|
||||
<view class="banner-card" wx:if="{{bannerSection}}" bindtap="goToRead" data-id="{{bannerSection.id}}" data-mid="{{bannerSection.mid}}">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">推荐</view>
|
||||
<view class="banner-title">{{latestSection.title}}</view>
|
||||
<view class="banner-title">{{bannerSection.title}}</view>
|
||||
<view class="banner-action">
|
||||
<text class="banner-action-text">点击阅读123</text>
|
||||
<text class="banner-action-text">点击阅读</text>
|
||||
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
@@ -53,10 +53,11 @@
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 超级个体(横向滚动,已去掉「查看全部」;审核模式隐藏) -->
|
||||
<!-- 超级个体:与匹配页一致,仅 VIP 横向列表(无首位特例) -->
|
||||
<view class="section" wx:if="{{!auditMode}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">超级个体</text>
|
||||
<text class="section-subtitle">获客入口</text>
|
||||
</view>
|
||||
<!-- 加载中:骨架动画 -->
|
||||
<view wx:if="{{superMembersLoading}}" class="super-loading">
|
||||
@@ -76,12 +77,16 @@
|
||||
wx:key="id"
|
||||
bindtap="goToMemberDetail"
|
||||
data-id="{{item.id}}"
|
||||
hover-class="super-item-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{(item.name && item.name[0]) || '会'}}</text>
|
||||
<view class="super-item-stack">
|
||||
<view class="super-avatar {{item.isVip ? 'super-avatar-vip' : ''}}">
|
||||
<image class="super-avatar-img" wx:if="{{item.avatar}}" src="{{item.avatar}}" mode="aspectFill"/>
|
||||
<text class="super-avatar-text" wx:else>{{item.avatarLetter}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
<text class="super-name">{{item.name}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
@@ -92,14 +97,10 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 精选推荐(带 tag,支持展开更多) -->
|
||||
<!-- 精选推荐:默认 3 条,列表下三角展开更多(与最新新增一致) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选推荐</text>
|
||||
<view class="section-more" wx:if="{{featuredSections.length > 0}}" bindtap="toggleFeaturedExpanded">
|
||||
<text class="more-text">{{featuredExpandedLoading ? '加载中...' : (featuredExpanded ? '收起' : '展开更多')}}</text>
|
||||
<icon name="{{featuredExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
@@ -111,30 +112,29 @@
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="featured-content">
|
||||
<view class="featured-meta">
|
||||
<text class="featured-id brand-color">{{item.id}}</text>
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}" wx:if="{{item.tag}}">{{item.tag}}</text>
|
||||
<view class="featured-meta" wx:if="{{item.tag}}">
|
||||
<text class="featured-tag {{item.tagClass || 'tag-rec'}}">{{item.tag}}</text>
|
||||
</view>
|
||||
<text class="featured-title">{{item.title}}</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.6)" customClass="featured-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="latest-expand-hint"
|
||||
wx:if="{{!featuredExpanded && featuredSectionsFull.length > 3}}"
|
||||
bindtap="expandFeaturedChapters"
|
||||
hover-class="latest-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="latest-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最新新增(时间线样式,支持展开更多) -->
|
||||
<!-- 最新新增(时间线样式;超过 5 条时在列表下方点小三角展开,展开后随页面滚动看全部) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header latest-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<view class="section-header-right">
|
||||
<view class="daily-badge-wrap">
|
||||
<text class="daily-badge">+{{latestChapters.length}}</text>
|
||||
</view>
|
||||
<view class="section-more" wx:if="{{latestChapters.length > 5}}" bindtap="toggleLatestExpanded">
|
||||
<text class="more-text">{{latestExpanded ? '收起' : '展开更多'}}</text>
|
||||
<icon name="{{latestExpanded ? 'chevron-up' : 'chevron-down'}}" size="28" color="rgba(255,255,255,0.6)" customClass="more-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="timeline-wrap">
|
||||
<view class="timeline-line"></view>
|
||||
@@ -149,6 +149,16 @@
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 仅文案「展开更多」去掉:下方居中轻点小三角,点一次展开剩余条目 -->
|
||||
<view
|
||||
class="latest-expand-hint"
|
||||
wx:if="{{latestChapters.length > 5 && !latestExpanded}}"
|
||||
bindtap="expandLatestChapters"
|
||||
hover-class="latest-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="latest-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
|
||||
@@ -63,26 +63,28 @@
|
||||
|
||||
.contact-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
padding: 8rpx 20rpx 8rpx 12rpx;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 40rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
gap: 8rpx;
|
||||
padding: 12rpx 16rpx 10rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 20rpx;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.contact-avatar {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.35);
|
||||
}
|
||||
|
||||
.contact-text {
|
||||
font-size: 24rpx;
|
||||
.contact-name {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.logo-title {
|
||||
@@ -330,6 +332,12 @@
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.section-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.42);
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.section-more {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -609,9 +617,18 @@
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
min-width: 140rpx;
|
||||
}
|
||||
.super-item-hover {
|
||||
opacity: 0.88;
|
||||
}
|
||||
.super-item-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.super-scroll .super-avatar {
|
||||
width: 112rpx;
|
||||
@@ -692,6 +709,7 @@
|
||||
.daily-badge-wrap {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 设计稿 1:1:橙底白字 rounded-full */
|
||||
.daily-badge {
|
||||
@@ -701,10 +719,29 @@
|
||||
font-weight: 700;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
margin-left: 8rpx;
|
||||
box-shadow: 0 4rpx 16rpx rgba(246, 173, 85, 0.3);
|
||||
}
|
||||
|
||||
/* 列表下方:仅小三角,点击展开(替代标题栏「展开更多」) */
|
||||
.latest-expand-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8rpx 0 16rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.latest-expand-hint-hover {
|
||||
opacity: 0.65;
|
||||
}
|
||||
/* 向下小三角 */
|
||||
.latest-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 20rpx solid rgba(0, 206, 209, 0.85);
|
||||
}
|
||||
|
||||
/* 设计稿 1:1:pl-3 竖线 left-3 top-2 bottom-2 w-[1px] bg-gray-800 */
|
||||
.timeline-wrap {
|
||||
position: relative;
|
||||
|
||||
@@ -226,6 +226,17 @@ Page({
|
||||
if (!userId) { callback(); return }
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/user/profile?userId=${userId}`, silent: true })
|
||||
const avatar = res?.data?.avatarUrl || app.globalData.userInfo?.avatarUrl || ''
|
||||
const isDefaultAvatar = !avatar || avatar.includes('default') || avatar.includes('132')
|
||||
if (isDefaultAvatar) {
|
||||
wx.showModal({
|
||||
title: '完善头像',
|
||||
content: '请先设置头像后再使用匹配功能',
|
||||
confirmText: '去设置',
|
||||
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
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)) {
|
||||
@@ -311,7 +322,7 @@ Page({
|
||||
confirmText: '去购买',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.switchTab({ url: '/pages/catalog/catalog' })
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -468,34 +479,6 @@ Page({
|
||||
}, delay)
|
||||
},
|
||||
|
||||
// 生成模拟匹配数据
|
||||
generateMockMatch() {
|
||||
const nicknames = ['创业先锋', '资源整合者', '私域专家', '导师顾问', '连续创业者']
|
||||
const concepts = [
|
||||
'专注私域流量运营5年,帮助100+品牌实现从0到1的增长。',
|
||||
'连续创业者,擅长商业模式设计和资源整合。',
|
||||
'在Soul分享真实创业故事,希望找到志同道合的合作伙伴。'
|
||||
]
|
||||
const wechats = ['soul_partner_1', 'soul_business_2024', 'soul_startup_fan']
|
||||
|
||||
const index = Math.floor(Math.random() * nicknames.length)
|
||||
const currentType = MATCH_TYPES.find(t => t.id === this.data.selectedType)
|
||||
|
||||
return {
|
||||
id: `user_${Date.now()}`,
|
||||
nickname: nicknames[index],
|
||||
avatar: `https://picsum.photos/200/200?random=${Date.now()}`,
|
||||
tags: ['创业者', '私域运营', currentType?.label || '创业合伙'],
|
||||
matchScore: Math.floor(Math.random() * 20) + 80,
|
||||
concept: concepts[index % concepts.length],
|
||||
wechat: wechats[index % wechats.length],
|
||||
commonInterests: [
|
||||
{ icon: 'book-open', text: '都在读《创业派对》' },
|
||||
{ icon: 'briefcase', text: '对私域运营感兴趣' },
|
||||
{ icon: 'target', text: '相似的创业方向' }
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
// 上报匹配行为
|
||||
async reportMatch(matchedUser) {
|
||||
@@ -639,18 +622,10 @@ Page({
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
} else {
|
||||
// 即使API返回失败,也模拟成功(因为已保存本地)
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
wx.showToast({ title: res.error || '加入失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
// 网络错误时也模拟成功
|
||||
this.setData({ joinSuccess: true })
|
||||
setTimeout(() => {
|
||||
this.setData({ showJoinModal: false, joinSuccess: false })
|
||||
}, 2000)
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
} finally {
|
||||
this.setData({ isJoining: false })
|
||||
}
|
||||
@@ -727,19 +702,7 @@ Page({
|
||||
if (e.errMsg && e.errMsg.includes('cancel')) {
|
||||
wx.showToast({ title: '已取消', icon: 'none' })
|
||||
} else {
|
||||
// 测试模式
|
||||
wx.showModal({
|
||||
title: '支付服务暂不可用',
|
||||
content: '是否使用测试模式购买?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
const extraMatches = (wx.getStorageSync('extra_match_count') || 0) + 1
|
||||
wx.setStorageSync('extra_match_count', extraMatches)
|
||||
wx.showToast({ title: '测试购买成功', icon: 'success' })
|
||||
this.initUserStatus()
|
||||
}
|
||||
}
|
||||
})
|
||||
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -750,10 +713,6 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
// 打开资料修改页(找伙伴右上角图标)
|
||||
openSettings() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
// 阻止事件冒泡
|
||||
preventBubble() {},
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<!-- 顶部留白,让内容往下 -->
|
||||
<view style="height: 30rpx;"></view>
|
||||
<view style="height: 16rpx;"></view>
|
||||
|
||||
<!-- 匹配提示条 - 简化显示 -->
|
||||
<view class="match-tip-bar" wx:if="{{matchesRemaining <= 0 && !hasFullBook}}">
|
||||
|
||||
@@ -5,15 +5,17 @@
|
||||
* mbti, region, industry, position, businessScale, skills,
|
||||
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
|
||||
* helpOffer→canHelp, helpNeed→needHelp
|
||||
* 点头像:若后台 persons.user_id 已绑定则带 ckbLeadToken,走存客宝 CKBLead(与阅读页 @ 一致)
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, member: null, loading: true },
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true },
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
this.setData({ statusBarHeight: app.globalData.statusBarHeight })
|
||||
const sb = app.globalData.statusBarHeight || 44
|
||||
this.setData({ statusBarHeight: sb, navBarTotalPx: sb + 44 })
|
||||
if (options.id) this.loadMember(options.id)
|
||||
},
|
||||
|
||||
@@ -44,6 +46,7 @@ Page({
|
||||
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
|
||||
}
|
||||
@@ -79,7 +82,8 @@ Page({
|
||||
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)
|
||||
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 || ''
|
||||
@@ -130,6 +134,189 @@ Page({
|
||||
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
|
||||
},
|
||||
|
||||
/**
|
||||
* 点头像:有存客宝人物 token 时优先 POST /api/miniprogram/ckb/lead(与阅读页 @ 同链路,匹配 persons.ckb_api_key 计划)
|
||||
* 否则:解锁后复制微信/手机号并引导
|
||||
*/
|
||||
startLinkFlow() {
|
||||
const member = this.data.member
|
||||
if (!member) return
|
||||
const leadTok = (member.ckbLeadToken || '').trim()
|
||||
if (leadTok) {
|
||||
const nickname = ((member.name || 'TA').trim() || 'TA')
|
||||
wx.showModal({
|
||||
title: '添加好友',
|
||||
content: `是否通过获客计划联系 ${nickname}?提交后将按对方在存客宝后台配置的计划执行。`,
|
||||
confirmText: '确定',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) this._doCkbLeadSubmit(leadTok, nickname)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
if (member.wechatRaw || member.wechatDisplay) {
|
||||
if (!this._ensureUnlockedForLink('wechat')) return
|
||||
const m = this.data.member
|
||||
if (m.wechatFull) this._copyAndGuideWechat(m.wechatFull)
|
||||
return
|
||||
}
|
||||
if (member.contactRaw || member.contactDisplay) {
|
||||
if (!this._ensureUnlockedForLink('contact')) return
|
||||
const m = this.data.member
|
||||
if (m.contactFull) this._copyAndGuidePhone(m.contactFull)
|
||||
return
|
||||
}
|
||||
wx.showToast({ title: '暂未公开联系方式', icon: 'none' })
|
||||
},
|
||||
|
||||
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token */
|
||||
async _doCkbLeadSubmit(targetUserId, targetNickname) {
|
||||
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: '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
|
||||
@@ -148,22 +335,25 @@ Page({
|
||||
}
|
||||
const d = this._getUnlockData(member.id)
|
||||
if (d[field]) return
|
||||
const isVip = app.globalData.isVip
|
||||
const usedFree = this._hasUsedFreeForMember(member.id)
|
||||
if (isVip || !usedFree) {
|
||||
this._addUnlock(member.id, field)
|
||||
if (this._tryFreeUnlock(member, field)) {
|
||||
const m = this.enrichAndFormat(member)
|
||||
this.setData({ member: m })
|
||||
wx.showToast({ title: field === 'contact' ? '已解锁联系方式' : '已解锁微信号', icon: 'success' })
|
||||
return
|
||||
}
|
||||
wx.showModal({
|
||||
title: '解锁' + (field === 'contact' ? '联系方式' : '微信号'),
|
||||
content: '您的免费解锁次数已用完,开通VIP会员(¥1980/年)可无限解锁',
|
||||
confirmText: '去开通',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/vip/vip' }) }
|
||||
})
|
||||
},
|
||||
|
||||
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() {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<!-- 卡若创业派对 - 超级个体详情(按 enhanced_professional_profile 1:1 还原) -->
|
||||
<!-- 卡若创业派对 - 超级个体详情(居中头像区 + 低调联系方式 + 信息卡) -->
|
||||
<view class="page">
|
||||
<!-- 导航栏 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="#5EEAD4" customClass="nav-icon"></icon>
|
||||
@@ -10,135 +9,176 @@
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<scroll-view scroll-y class="scroll-wrap" wx:if="{{member}}">
|
||||
<!-- 顶部 profile 卡片 -->
|
||||
<view class="card-profile">
|
||||
<view class="profile-deco"></view>
|
||||
<view class="profile-body">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{(member.name && member.name[0]) || '创'}}</text></view>
|
||||
<scroll-view scroll-y class="scroll-wrap" style="height: calc(100vh - {{navBarTotalPx}}px);" wx:if="{{member}}">
|
||||
<!-- 首屏:居中头像 + 昵称 + 标签;点头像走添加微信引导(无独立「链接 TA」大按钮) -->
|
||||
<view class="shell">
|
||||
<view class="shell-glow"></view>
|
||||
<view class="hero-profile">
|
||||
<view class="hero-avatar-block" bindtap="startLinkFlow" hover-class="hero-avatar-block-hover" hover-stay-time="80">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap {{member.isVip ? 'vip-ring' : ''}}">
|
||||
<image class="avatar-img" wx:if="{{member.avatar}}" src="{{member.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{(member.name && member.name[0]) || '创'}}</text></view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags profile-tags-modern" wx:if="{{member.mbti || member.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{member.region}}">
|
||||
<icon name="map-pin" size="22" color="currentColor" customClass="pin-icon"></icon>
|
||||
<text>{{member.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vip-tag" wx:if="{{member.isVip}}">VIP</view>
|
||||
</view>
|
||||
<text class="profile-name">{{member.name}}</text>
|
||||
<view class="profile-tags" wx:if="{{member.mbti || member.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{member.mbti}}">{{member.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{member.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="pin-icon"></icon><text>{{member.region}}</text></view>
|
||||
</view>
|
||||
|
||||
<view class="contact-rows contact-rows-subtle">
|
||||
<view
|
||||
class="link-chip link-chip-subtle {{member.contactUnlocked ? 'link-chip-open' : ''}}"
|
||||
wx:if="{{member.contactRaw || member.contactDisplay}}"
|
||||
catchtap="tapContactRow"
|
||||
>
|
||||
<view class="link-chip-icon link-chip-icon-phone link-chip-icon-subtle">
|
||||
<icon name="smartphone" size="26" color="rgba(148,163,184,0.85)" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label link-chip-label-subtle">手机</text>
|
||||
<text class="link-chip-val mono link-chip-val-subtle">{{member.contactDisplay || member.contactRaw}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action link-chip-action-subtle">
|
||||
<text>{{member.contactUnlocked ? '复制' : '解锁'}}</text>
|
||||
<icon wx:if="{{!member.contactUnlocked}}" name="chevron-right" size="22" color="rgba(100,116,139,0.8)" customClass="lc-arr"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view
|
||||
class="link-chip link-chip-subtle {{member.wechatUnlocked ? 'link-chip-open' : ''}}"
|
||||
wx:if="{{member.wechatRaw || member.wechatDisplay}}"
|
||||
catchtap="tapWechatRow"
|
||||
>
|
||||
<view class="link-chip-icon link-chip-icon-wx link-chip-icon-subtle">
|
||||
<icon name="message-circle" size="26" color="rgba(148,163,184,0.85)" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label link-chip-label-subtle">微信</text>
|
||||
<text class="link-chip-val mono link-chip-val-subtle">{{member.wechatDisplay || member.wechatRaw}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action link-chip-action-subtle">
|
||||
<text>{{member.wechatUnlocked ? '复制' : '解锁'}}</text>
|
||||
<icon wx:if="{{!member.wechatUnlocked}}" name="chevron-right" size="22" color="rgba(100,116,139,0.8)" customClass="lc-arr"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="link-empty link-empty-subtle" wx:if="{{!(member.contactRaw || member.contactDisplay) && !(member.wechatRaw || member.wechatDisplay)}}">
|
||||
<text class="link-empty-txt">暂未公开联系方式</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.contactRaw || member.contactDisplay || member.wechatRaw || member.wechatDisplay}}">
|
||||
<view class="card-head">
|
||||
<icon name="user" size="48" color="#00CED1" customClass="card-icon"></icon>
|
||||
<text class="card-label">基本信息</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="field" wx:if="{{member.industry}}">
|
||||
<text class="f-key">行业</text>
|
||||
<text class="f-val">{{member.industry}}</text>
|
||||
<!-- 一体化信息区(单卡片内分区) -->
|
||||
<view class="mono-card mono-card-compact" wx:if="{{member.industry || member.position || member.businessScale || member.skills || member.bestMonth || member.achievement || member.turningPoint || member.canHelp || member.needHelp || member.project}}">
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.industry || member.position || member.businessScale}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.position}}">
|
||||
<text class="f-key">职位</text>
|
||||
<text class="f-val">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.businessScale}}">
|
||||
<text class="f-key">业务体量</text>
|
||||
<text class="f-val">{{member.businessScale}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.industry || member.position || member.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{member.skills}}">
|
||||
<text class="f-key">我擅长</text>
|
||||
<text class="f-val">{{member.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.contactRaw || member.contactDisplay}}">
|
||||
<text class="f-key">联系方式</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.contactUnlocked ? member.contactFull : (member.contactDisplay || member.contactRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.contactRaw && !member.contactUnlocked}}" bindtap="unlockField" data-field="contact">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.contactRaw && member.contactUnlocked}}" bindtap="copyContact"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
|
||||
<view class="kv-grid">
|
||||
<view class="kv-cell" wx:if="{{member.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{member.industry}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{member.wechatRaw || member.wechatDisplay}}">
|
||||
<text class="f-key">微信号</text>
|
||||
<view class="f-row">
|
||||
<text class="f-val mono">{{member.wechatUnlocked ? member.wechatFull : (member.wechatDisplay || member.wechatRaw)}}</text>
|
||||
<view class="icon-copy icon-eye-off" wx:if="{{member.wechatRaw && !member.wechatUnlocked}}" bindtap="unlockField" data-field="wechat">
|
||||
<image class="icon-img" src="/assets/icons/eye-off.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="icon-copy" wx:elif="{{member.wechatRaw && member.wechatUnlocked}}" bindtap="copyWechat"><icon name="clipboard" size="32" color="#00CED1"></icon></view>
|
||||
<view class="kv-cell" wx:if="{{member.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{member.position}}</text>
|
||||
</view>
|
||||
<view class="kv-cell kv-cell-full" wx:if="{{member.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{member.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="card-head">
|
||||
<icon name="lightbulb" size="48" color="#FFD700" customClass="card-icon bulb"></icon>
|
||||
<text class="card-label">个人故事</text>
|
||||
<view class="mono-divider" wx:if="{{(member.industry || member.position || member.businessScale) && member.skills}}"></view>
|
||||
|
||||
<!-- 核心能力 -->
|
||||
<view class="mono-sec mono-sec-tight skills-showcase" wx:if="{{member.skills}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{member.skills}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="story" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月做的是什么</text></view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{member.skills && (member.bestMonth || member.achievement || member.turningPoint)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.bestMonth || member.achievement || member.turningPoint}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story story-compact" wx:if="{{member.bestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{member.bestMonth}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.bestMonth}}"></view>
|
||||
<view class="story" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="28" color="#FFD700" customClass="story-icon"></icon><text class="story-q">最有成就感的一件事</text></view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.bestMonth && (member.achievement || member.turningPoint)}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.achievement}}">
|
||||
<view class="story-head"><icon name="star" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{member.achievement}}</text>
|
||||
</view>
|
||||
<view class="divider" wx:if="{{member.achievement}}"></view>
|
||||
<view class="story" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-icon turn"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<view class="story-gap story-gap-tight" wx:if="{{member.achievement && member.turningPoint}}"></view>
|
||||
<view class="story story-compact" wx:if="{{member.turningPoint}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="24" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{member.turningPoint}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.bestMonth || member.achievement || member.turningPoint) && (member.canHelp || member.needHelp)}}"></view>
|
||||
|
||||
<!-- 互助 -->
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{member.canHelp}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{member.needHelp}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(member.canHelp || member.needHelp) && member.project}}"></view>
|
||||
|
||||
<view class="mono-sec mono-sec-tight" wx:if="{{member.project}}">
|
||||
<view class="mono-sec-head mono-sec-head-tight">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-body proj-body-compact">{{member.project}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求(未填写行已隐藏) -->
|
||||
<view class="card" wx:if="{{member.canHelp || member.needHelp}}">
|
||||
<view class="card-head">
|
||||
<icon name="handshake" size="48" color="#00CED1" customClass="card-icon"></icon>
|
||||
<text class="card-label">互助需求</text>
|
||||
</view>
|
||||
<view class="card-body">
|
||||
<view class="help-box help-give" wx:if="{{member.canHelp}}">
|
||||
<text class="help-tag">我能帮你</text>
|
||||
<text class="help-txt">{{member.canHelp}}</text>
|
||||
<!-- 底部:分享 + 双入口(同一视觉块) -->
|
||||
<view class="footer-panel">
|
||||
<view class="footer-pills">
|
||||
<view class="pill pill-gold" bindtap="goToVip">
|
||||
<icon name="sparkles" size="30" color="#FBBF24" customClass="pill-ic"></icon>
|
||||
<text class="pill-txt">成为超级个体</text>
|
||||
</view>
|
||||
<view class="help-box help-need" wx:if="{{member.needHelp}}">
|
||||
<text class="help-tag need">我需要帮助</text>
|
||||
<text class="help-txt">{{member.needHelp}}</text>
|
||||
<view class="pill pill-teal" bindtap="goToMatch">
|
||||
<icon name="users" size="30" color="#5EEAD4" customClass="pill-ic"></icon>
|
||||
<text class="pill-txt">找更多伙伴</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="card" wx:if="{{member.project}}">
|
||||
<view class="card-head">
|
||||
<icon name="rocket" size="48" color="#00CED1" customClass="card-icon rocket"></icon>
|
||||
<text class="card-label">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-txt">{{member.project}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-wrap">
|
||||
<view class="btn-super" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="36" color="#F59E0B" customClass="btn-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view style="height:160rpx;"></view>
|
||||
<view class="scroll-pad"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 加载和空状态 -->
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
|
||||
@@ -1,172 +1,629 @@
|
||||
/* 卡若创业派对 - 个人资料页(enhanced_professional_profile 1:1 还原) */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
|
||||
/* 导航栏 */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 0 32rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.9);
|
||||
backdrop-filter: blur(24rpx);
|
||||
-webkit-backdrop-filter: blur(24rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
/* 卡若创业派对 - 超级个体详情(高阶单页 · 深色玻璃态) */
|
||||
.page {
|
||||
background: radial-gradient(120% 80% at 50% -20%, rgba(20, 80, 90, 0.35) 0%, #050B14 45%);
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
.nav-back { width: 80rpx; height: 80rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5EEAD4; font-weight: 300; }
|
||||
.nav-title { font-size: 34rpx; font-weight: 700; color: #fff; letter-spacing: 2rpx; }
|
||||
.nav-right { display: flex; align-items: center; gap: 16rpx; }
|
||||
.nav-icon-wrap { padding: 8rpx; }
|
||||
.nav-icon-dot { font-size: 28rpx; color: rgba(255,255,255,0.8); }
|
||||
|
||||
.scroll-wrap { height: calc(100vh - 88px); }
|
||||
/* —— 导航 —— */
|
||||
.nav-bar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 28rpx;
|
||||
height: 44px;
|
||||
background: rgba(5, 11, 20, 0.72);
|
||||
backdrop-filter: blur(20rpx);
|
||||
-webkit-backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.nav-back {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
.nav-icon {
|
||||
font-size: 44rpx;
|
||||
color: #5eead4;
|
||||
font-weight: 300;
|
||||
}
|
||||
.nav-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
|
||||
/* ===== 顶部 Profile 卡片 ===== */
|
||||
.card-profile {
|
||||
position: relative; margin: 32rpx 32rpx 0;
|
||||
padding: 64rpx 40rpx 48rpx;
|
||||
.scroll-wrap {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* —— 首屏外壳(头像 + 链接列) —— */
|
||||
.shell {
|
||||
position: relative;
|
||||
margin: 28rpx 24rpx 0;
|
||||
padding: 40rpx 32rpx 36rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
background: linear-gradient(145deg, rgba(22, 36, 48, 0.95) 0%, rgba(12, 20, 32, 0.98) 100%);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.45), inset 0 1rpx 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.profile-deco {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(180deg, rgba(30, 58, 69, 0.3) 0%, transparent 100%);
|
||||
.shell-glow {
|
||||
position: absolute;
|
||||
top: -40%;
|
||||
right: -20%;
|
||||
width: 70%;
|
||||
height: 80%;
|
||||
background: radial-gradient(circle, rgba(45, 212, 191, 0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.profile-body { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
|
||||
.hero-profile {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
|
||||
.hero-avatar-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 520rpx;
|
||||
}
|
||||
.hero-avatar-block-hover {
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.contact-rows {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12rpx;
|
||||
margin-top: 32rpx;
|
||||
}
|
||||
|
||||
.contact-rows-subtle {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.avatar-outer {
|
||||
position: relative;
|
||||
width: 176rpx; height: 176rpx;
|
||||
margin-bottom: 32rpx;
|
||||
width: 168rpx;
|
||||
height: 168rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.avatar-wrap {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.4);
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.avatar-wrap.vip-ring {
|
||||
border: 4rpx solid transparent;
|
||||
background: linear-gradient(135deg, #F59E0B, #5EEAD4, #F59E0B);
|
||||
border: 3rpx solid transparent;
|
||||
background: linear-gradient(135deg, #f59e0b, #5eead4, #f59e0b);
|
||||
background-size: 200% 200%;
|
||||
animation: vipGlow 4s ease infinite;
|
||||
animation: vipGlow 5s ease infinite;
|
||||
}
|
||||
@keyframes vipGlow {
|
||||
0%, 100% { background-position: 0% 50%; }
|
||||
50% { background-position: 100% 50%; }
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%;
|
||||
background: #17212F;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 56rpx; color: #5EEAD4; font-weight: 700;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #1a2332;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 52rpx;
|
||||
color: #5eead4;
|
||||
font-weight: 700;
|
||||
}
|
||||
.vip-tag {
|
||||
position: absolute; bottom: -4rpx; right: -4rpx;
|
||||
background: linear-gradient(135deg, #F59E0B, #e8920d);
|
||||
color: #000; font-size: 20rpx; font-weight: 800;
|
||||
padding: 6rpx 14rpx; border-radius: 16rpx;
|
||||
position: absolute;
|
||||
bottom: -2rpx;
|
||||
right: -2rpx;
|
||||
background: linear-gradient(135deg, #fbbf24, #d97706);
|
||||
color: #0f172a;
|
||||
font-size: 18rpx;
|
||||
font-weight: 800;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
z-index: 2;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.profile-name { font-size: 40rpx; font-weight: 700; color: #fff; margin-bottom: 24rpx; letter-spacing: 2rpx; }
|
||||
.profile-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; flex-wrap: wrap; }
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 8rpx 24rpx; border-radius: 999rpx; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94, 234, 212, 0.2); }
|
||||
.tag-region { background: #1F2937; color: #D1D5DB; border: 1rpx solid rgba(255, 255, 255, 0.1); display: flex; align-items: center; gap: 8rpx; }
|
||||
.pin-icon { color: #EF4444; font-size: 22rpx; }
|
||||
|
||||
/* ===== 通用卡片 ===== */
|
||||
.card {
|
||||
margin: 32rpx;
|
||||
padding: 40rpx 40rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #0F1720;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
.link-chip {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
padding: 22rpx 22rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1rpx solid rgba(148, 163, 184, 0.15);
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
.card-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.card-icon { font-size: 40rpx; }
|
||||
.card-icon.bulb { filter: sepia(1) saturate(3) hue-rotate(15deg); }
|
||||
.card-icon.rocket { opacity: 0.9; }
|
||||
.card-label { font-size: 30rpx; font-weight: 700; color: #fff; letter-spacing: 1rpx; }
|
||||
|
||||
.card-body { }
|
||||
.field { margin-bottom: 32rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.f-key { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 12rpx; }
|
||||
.f-val { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.6; }
|
||||
.f-val.mono { font-family: ui-monospace, monospace; letter-spacing: 2rpx; }
|
||||
.f-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.icon-copy { font-size: 36rpx; color: #94A3B8; opacity: 0.6; padding: 8rpx; }
|
||||
.icon-eye-off { display: flex; align-items: center; justify-content: center; }
|
||||
.icon-eye-off .icon-img { width: 40rpx; height: 40rpx; }
|
||||
|
||||
.divider { height: 1rpx; background: rgba(255, 255, 255, 0.05); margin: 32rpx 0; }
|
||||
|
||||
/* ===== 个人故事 ===== */
|
||||
.story { margin-bottom: 32rpx; }
|
||||
.story:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { font-size: 32rpx; }
|
||||
.story-icon.turn { opacity: 0.9; }
|
||||
.story-q { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 互助需求 ===== */
|
||||
.help-box {
|
||||
padding: 32rpx;
|
||||
border-radius: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
background: #17212F;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
.link-chip-subtle {
|
||||
gap: 16rpx;
|
||||
padding: 14rpx 8rpx 14rpx 12rpx;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.help-box:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block;
|
||||
font-size: 22rpx; font-weight: 600;
|
||||
padding: 6rpx 16rpx; border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
.link-chip-subtle:last-of-type {
|
||||
border-bottom: none;
|
||||
}
|
||||
.help-give .help-tag { color: #5EEAD4; background: #112D2A; }
|
||||
.help-need .help-tag { color: #F59E0B; background: #2D1F0D; }
|
||||
.help-txt { display: block; font-size: 26rpx; color: #fff; line-height: 1.6; letter-spacing: 1rpx; }
|
||||
|
||||
/* ===== 项目介绍 ===== */
|
||||
.proj-txt { font-size: 28rpx; color: #E5E7EB; line-height: 1.7; }
|
||||
|
||||
/* ===== 底部按钮 ===== */
|
||||
.bottom-wrap {
|
||||
padding: 48rpx 32rpx 0;
|
||||
.link-chip-subtle:active {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
.btn-super {
|
||||
.link-chip-open.link-chip-subtle {
|
||||
border-bottom-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
.link-chip:active {
|
||||
border-color: rgba(94, 234, 212, 0.35);
|
||||
background: rgba(15, 30, 40, 0.75);
|
||||
}
|
||||
.link-chip-subtle:active {
|
||||
border-color: transparent;
|
||||
}
|
||||
.link-chip-open {
|
||||
border-color: rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.link-chip-icon {
|
||||
width: 64rpx;
|
||||
height: 64rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.link-chip-icon-subtle {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
border-radius: 12rpx;
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.link-chip-icon-phone {
|
||||
background: rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.link-chip-icon-wx {
|
||||
background: rgba(52, 211, 153, 0.12);
|
||||
}
|
||||
.link-chip-icon-subtle.link-chip-icon-phone,
|
||||
.link-chip-icon-subtle.link-chip-icon-wx {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
.lc-ic {
|
||||
display: block;
|
||||
}
|
||||
.link-chip-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.link-chip-label {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.link-chip-label-subtle {
|
||||
font-size: 18rpx;
|
||||
color: rgba(100, 116, 139, 0.85);
|
||||
margin-bottom: 4rpx;
|
||||
}
|
||||
.link-chip-val {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
.link-chip-val-subtle {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(148, 163, 184, 0.92);
|
||||
}
|
||||
.link-chip-val.mono {
|
||||
font-family: ui-monospace, monospace;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.link-chip-action {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #5eead4;
|
||||
}
|
||||
.link-chip-action-subtle {
|
||||
font-size: 20rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(100, 116, 139, 0.95);
|
||||
}
|
||||
.lc-arr {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.link-empty {
|
||||
padding: 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
border: 1rpx dashed rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.link-empty-subtle {
|
||||
padding: 16rpx 8rpx;
|
||||
background: transparent;
|
||||
border: none;
|
||||
}
|
||||
.link-empty-txt {
|
||||
font-size: 24rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.link-empty-subtle .link-empty-txt {
|
||||
font-size: 22rpx;
|
||||
color: rgba(100, 116, 139, 0.75);
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: block;
|
||||
text-align: center;
|
||||
margin-top: 20rpx;
|
||||
width: 100%;
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.35;
|
||||
word-break: break-all;
|
||||
}
|
||||
.profile-tags {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16rpx;
|
||||
width: 100%;
|
||||
padding: 32rpx 0;
|
||||
border-radius: 999rpx;
|
||||
background: transparent;
|
||||
border: 1rpx solid rgba(245, 158, 11, 0.3);
|
||||
color: #F59E0B;
|
||||
font-size: 30rpx; font-weight: 500;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.btn-arrow { font-size: 36rpx; font-weight: 300; }
|
||||
.profile-tags-modern {
|
||||
gap: 10rpx;
|
||||
margin-top: 18rpx;
|
||||
}
|
||||
.tag {
|
||||
font-size: 22rpx;
|
||||
font-weight: 500;
|
||||
padding: 8rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.profile-tags-modern .tag-mbti {
|
||||
font-size: 20rpx;
|
||||
font-weight: 600;
|
||||
letter-spacing: 2rpx;
|
||||
padding: 6rpx 18rpx;
|
||||
background: rgba(45, 212, 191, 0.1);
|
||||
color: #7ee8dc;
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
.profile-tags-modern .tag-region {
|
||||
font-size: 20rpx;
|
||||
padding: 6rpx 16rpx 6rpx 14rpx;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
color: rgba(203, 213, 225, 0.95);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.tag-mbti {
|
||||
background: rgba(19, 78, 74, 0.6);
|
||||
color: #5eead4;
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.tag-region {
|
||||
background: rgba(30, 41, 59, 0.8);
|
||||
color: #cbd5e1;
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.pin-icon {
|
||||
color: #f87171;
|
||||
font-size: 22rpx;
|
||||
}
|
||||
|
||||
/* ===== 状态 ===== */
|
||||
.state-wrap { display: flex; flex-direction: column; align-items: center; justify-content: center; min-height: 60vh; gap: 24rpx; }
|
||||
.state-txt { font-size: 28rpx; color: #64748B; }
|
||||
.state-emoji { font-size: 96rpx; }
|
||||
/* —— 一体化信息卡 —— */
|
||||
.mono-card {
|
||||
margin: 24rpx 24rpx 0;
|
||||
padding: 8rpx 0 32rpx;
|
||||
border-radius: 32rpx;
|
||||
background: rgba(15, 23, 34, 0.88);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.07);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mono-card-compact {
|
||||
padding-bottom: 24rpx;
|
||||
}
|
||||
.mono-sec {
|
||||
padding: 28rpx 32rpx 8rpx;
|
||||
}
|
||||
.mono-sec-tight {
|
||||
padding: 18rpx 28rpx 6rpx;
|
||||
}
|
||||
.mono-sec-head {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.mono-sec-head-tight {
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.mono-sec-kicker {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(94, 234, 212, 0.55);
|
||||
letter-spacing: 6rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.mono-sec-title {
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #f8fafc;
|
||||
}
|
||||
.mono-divider {
|
||||
height: 1rpx;
|
||||
margin: 4rpx 28rpx;
|
||||
background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.15), transparent);
|
||||
}
|
||||
|
||||
.kv-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 16rpx 20rpx;
|
||||
}
|
||||
.kv-cell {
|
||||
width: calc(50% - 10rpx);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.kv-cell-full {
|
||||
width: 100%;
|
||||
}
|
||||
.kv-cell .kv-k {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.kv-cell .kv-v {
|
||||
font-size: 26rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.5;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kv {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.kv:last-child {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.kv-k {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #64748b;
|
||||
margin-bottom: 10rpx;
|
||||
}
|
||||
.kv-v {
|
||||
font-size: 28rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.65;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.skills-showcase .skills-quote {
|
||||
padding: 20rpx 22rpx 20rpx 20rpx;
|
||||
border-radius: 18rpx;
|
||||
background: linear-gradient(105deg, rgba(45, 212, 191, 0.08) 0%, rgba(15, 23, 42, 0.5) 100%);
|
||||
border-left: 6rpx solid #2dd4bf;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.skills-quote-text {
|
||||
font-size: 26rpx;
|
||||
color: #f1f5f9;
|
||||
line-height: 1.6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.story {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.story-compact .story-head {
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.story-compact .story-a {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
padding-left: 0;
|
||||
}
|
||||
.story-gap {
|
||||
height: 28rpx;
|
||||
}
|
||||
.story-gap-tight {
|
||||
height: 16rpx;
|
||||
}
|
||||
.story-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.story-icon {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.story-q {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
}
|
||||
.story-a {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
color: #e2e8f0;
|
||||
line-height: 1.7;
|
||||
padding-left: 4rpx;
|
||||
}
|
||||
|
||||
.help-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14rpx;
|
||||
}
|
||||
.help-tile {
|
||||
padding: 22rpx 24rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(23, 33, 47, 0.9);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.help-tile-tag {
|
||||
display: inline-block;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 8rpx 18rpx;
|
||||
border-radius: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tile-tag {
|
||||
color: #5eead4;
|
||||
background: rgba(6, 78, 59, 0.45);
|
||||
border: 1rpx solid rgba(45, 212, 191, 0.2);
|
||||
}
|
||||
.help-need .help-tile-tag.need {
|
||||
color: #fbbf24;
|
||||
background: rgba(69, 47, 8, 0.45);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
.help-tile-txt {
|
||||
font-size: 26rpx;
|
||||
color: #f1f5f9;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.proj-body {
|
||||
font-size: 28rpx;
|
||||
color: #cbd5e1;
|
||||
line-height: 1.75;
|
||||
padding-bottom: 8rpx;
|
||||
}
|
||||
.proj-body-compact {
|
||||
font-size: 26rpx;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
/* —— 底栏:分享 + 双入口 —— */
|
||||
.footer-panel {
|
||||
margin: 32rpx 24rpx 0;
|
||||
padding: 28rpx;
|
||||
border-radius: 28rpx;
|
||||
background: rgba(12, 18, 28, 0.92);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.1);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.footer-pills {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.pill {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10rpx;
|
||||
padding: 22rpx 12rpx;
|
||||
border-radius: 20rpx;
|
||||
background: rgba(30, 41, 59, 0.6);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
.pill:active {
|
||||
background: rgba(51, 65, 85, 0.7);
|
||||
}
|
||||
.pill-gold {
|
||||
border-color: rgba(251, 191, 36, 0.2);
|
||||
}
|
||||
.pill-teal {
|
||||
border-color: rgba(94, 234, 212, 0.2);
|
||||
}
|
||||
.pill-ic {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.pill-txt {
|
||||
font-size: 24rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(248, 250, 252, 0.88);
|
||||
}
|
||||
|
||||
.scroll-pad {
|
||||
height: calc(120rpx + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
/* —— 状态 —— */
|
||||
.state-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 60vh;
|
||||
gap: 24rpx;
|
||||
}
|
||||
.state-txt {
|
||||
font-size: 28rpx;
|
||||
color: #64748b;
|
||||
}
|
||||
.state-emoji {
|
||||
font-size: 96rpx;
|
||||
}
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2);
|
||||
border-top-color: #5EEAD4;
|
||||
border-top-color: #5eead4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,22 @@
|
||||
const app = getApp()
|
||||
const { formatStatNum } = require('../../utils/util.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
|
||||
/** 是否视为「单章解锁」类订单(排除全书/VIP 等聚合商品名) */
|
||||
function isSectionUnlockOrder(o) {
|
||||
const name = String(o.product_name || o.title || '').trim()
|
||||
if (/全书|全書|VIP|会员|年费|买断/.test(name)) return false
|
||||
const pid = String(o.product_id || o.section_id || o.sectionId || '')
|
||||
if (/^\d+\.\d+/.test(pid)) return true
|
||||
return !!pid && pid.length > 0
|
||||
}
|
||||
|
||||
function parseOrderTimeMs(o) {
|
||||
const raw = o.created_at || o.createdAt || o.pay_time || 0
|
||||
const t = new Date(raw).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -33,6 +49,8 @@ Page({
|
||||
readCountText: '0',
|
||||
totalReadTimeText: '0',
|
||||
matchHistoryText: '0',
|
||||
orderCountText: '0',
|
||||
giftPayCountText: '0',
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
@@ -78,6 +96,11 @@ Page({
|
||||
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 已解锁章节(订单倒序;默认最多展示 5 条,底部倒三角展开)
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -159,6 +182,7 @@ Page({
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadWalletBalance()
|
||||
this.loadUnlockedChapters()
|
||||
} else {
|
||||
const guestReadCount = app.getReadCount()
|
||||
this.setData({
|
||||
@@ -172,6 +196,9 @@ Page({
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: [],
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
totalReadTimeText: '0',
|
||||
@@ -180,6 +207,91 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 已解锁章节:优先订单接口(按支付时间倒序);失败时用 purchasedSections + bookData 兜底
|
||||
*/
|
||||
async loadUnlockedChapters() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo?.id) {
|
||||
this.setData({
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false
|
||||
})
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const expanded = this.data.unlockedExpanded
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
const metaById = (id) => {
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
return {
|
||||
mid: row?.mid ?? row?.MID ?? 0,
|
||||
title: cleanSingleLineField(row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || '')
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`, silent: true })
|
||||
let rows = []
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
rows = res.data
|
||||
.map((item) => ({
|
||||
id: item.product_id || item.section_id,
|
||||
mid: item.section_mid ?? item.mid ?? item.MID ?? 0,
|
||||
title: cleanSingleLineField(item.product_name || ''),
|
||||
_ts: parseOrderTimeMs(item)
|
||||
}))
|
||||
.filter((r) => r.id && isSectionUnlockOrder({ product_id: r.id, product_name: r.title }))
|
||||
}
|
||||
rows.sort((a, b) => b._ts - a._ts)
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const r of rows) {
|
||||
if (seen.has(r.id)) continue
|
||||
seen.add(r.id)
|
||||
const meta = metaById(r.id)
|
||||
deduped.push({
|
||||
id: r.id,
|
||||
mid: r.mid || meta.mid,
|
||||
title: cleanSingleLineField(r.title || meta.title || `章节 ${r.id}`)
|
||||
})
|
||||
}
|
||||
if (deduped.length === 0) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])]
|
||||
ids.reverse()
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
} catch (e) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const id of ids) {
|
||||
if (!id || seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
}
|
||||
},
|
||||
|
||||
expandUnlockedChapters() {
|
||||
if (this.data.unlockedExpanded) return
|
||||
trackClick('my', 'tab_click', '已解锁章节_展开')
|
||||
const full = this.data.unlockedChaptersFull || []
|
||||
this.setData({
|
||||
unlockedExpanded: true,
|
||||
displayUnlockedChapters: full
|
||||
})
|
||||
},
|
||||
|
||||
async loadDashboardStats() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
@@ -207,6 +319,8 @@ Page({
|
||||
const readCount = Number(res.data.readCount || 0)
|
||||
const totalReadTime = Number(res.data.totalReadMinutes || 0)
|
||||
const matchHistory = Number(res.data.matchHistory || 0)
|
||||
const orderCount = Number(res.data.orderCount || 0)
|
||||
const giftPayCount = Number(res.data.giftPayCount || 0)
|
||||
this.setData({
|
||||
readCount,
|
||||
totalReadTime,
|
||||
@@ -214,6 +328,8 @@ Page({
|
||||
readCountText: formatStatNum(readCount),
|
||||
totalReadTimeText: formatStatNum(totalReadTime),
|
||||
matchHistoryText: formatStatNum(matchHistory),
|
||||
orderCountText: formatStatNum(orderCount),
|
||||
giftPayCountText: formatStatNum(giftPayCount),
|
||||
recentChapters
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,20 +49,20 @@
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
<view class="profile-stat" bindtap="goToChapters">
|
||||
<text class="profile-stat-val">{{readCountText}}</text>
|
||||
<text class="profile-stat-val">{{readCountText || '0'}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToMatch">
|
||||
<text class="profile-stat-val">{{matchHistoryText}}</text>
|
||||
<text class="profile-stat-label">匹配伙伴</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<text class="profile-stat-val">{{walletBalanceText}}</text>
|
||||
<text class="profile-stat-label">我的余额</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings || '0.00'}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -92,31 +92,63 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<!-- 快捷入口:我的订单 + 我的代付 -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">阅读统计</text>
|
||||
<text class="card-title">快捷入口</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/book-open-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{readCountText}}</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="orders">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">订单</text>
|
||||
<text class="stat-label">我的订单</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/clock-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{totalReadTimeText}}</text>
|
||||
<text class="stat-label">阅读分钟</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">代付</text>
|
||||
<text class="stat-label">我的代付</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToMatch">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{matchHistoryText}}</text>
|
||||
<text class="stat-label">匹配伙伴</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="wallet">
|
||||
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{walletBalanceText}}</text>
|
||||
<text class="stat-label">我的余额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁:仅低调图标区(无标题文案);默认 5 条 + 倒三角展开;倒序由接口/JS 保证 -->
|
||||
<view class="card recent-card unlocked-card" wx:if="{{unlockedChaptersFull.length > 0}}">
|
||||
<view class="unlocked-section-head">
|
||||
<image class="unlocked-section-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="recent-list">
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{displayUnlockedChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="recent-link">阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="unlocked-expand-hint"
|
||||
wx:if="{{unlockedChaptersFull.length > 5 && !unlockedExpanded}}"
|
||||
bindtap="expandUnlockedChapters"
|
||||
hover-class="unlocked-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="unlocked-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
<view class="card-header">
|
||||
@@ -145,23 +177,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 设置 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><image class="menu-icon-img" src="/assets/icons/folder-teal.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gold"><image class="menu-icon-img" src="/assets/icons/gift.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">我的代付</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" wx:if="{{showSettingsEntry}}" bindtap="handleMenuTap" data-id="settings">
|
||||
<!-- 设置 -->
|
||||
<view class="card menu-card" wx:if="{{showSettingsEntry}}">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">设置</text>
|
||||
|
||||
@@ -162,7 +162,20 @@
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; }
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; }
|
||||
/* 已解锁区块:仅顶部弱对比图标,无标题字 */
|
||||
.unlocked-card { padding-top: 28rpx; }
|
||||
.unlocked-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 8rpx 16rpx 8rpx;
|
||||
}
|
||||
.unlocked-section-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.recent-index { font-size: 28rpx; color: #6B7280; font-family: monospace; }
|
||||
.recent-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.recent-link { font-size: 24rpx; color: #4FD1C5; font-weight: 500; flex-shrink: 0; }
|
||||
@@ -170,6 +183,25 @@
|
||||
.recent-empty-text { font-size: 28rpx; color: #6B7280; display: block; margin-bottom: 24rpx; }
|
||||
.recent-empty-btn { font-size: 28rpx; color: #4FD1C5; }
|
||||
|
||||
/* 已解锁章节列表底部倒三角展开(与首页「最新新增」一致) */
|
||||
.unlocked-expand-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8rpx 0 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.unlocked-expand-hint-hover {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.unlocked-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 20rpx solid rgba(79, 209, 197, 0.85);
|
||||
}
|
||||
|
||||
/* 菜单 */
|
||||
.menu-card { padding: 0; margin-bottom: 48rpx; overflow: hidden; }
|
||||
.menu-item {
|
||||
|
||||
@@ -1,135 +1,175 @@
|
||||
<!-- 个人资料展示页 - enhanced_professional_profile 1:1 重构 -->
|
||||
<!-- 卡若创业派对 - 个人资料展示页(与 member-detail 同一视觉) -->
|
||||
<view class="page">
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-back" bindtap="goBack"><icon name="chevron-left" size="44" color="#5EEAD4" customClass="back-icon"></icon></view>
|
||||
<view class="nav-back" bindtap="goBack">
|
||||
<icon name="chevron-left" size="44" color="#5EEAD4" customClass="nav-icon"></icon>
|
||||
</view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-right" bindtap="goToEdit"><text class="nav-more">⋯</text></view>
|
||||
<view class="nav-edit" bindtap="goToEdit">
|
||||
<icon name="edit" size="32" color="#5EEAD4"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 头像区卡片 -->
|
||||
<view class="hero-card" wx:if="{{profile}}">
|
||||
<view class="hero-gradient"></view>
|
||||
<view class="hero-content">
|
||||
<view class="hero-avatar">
|
||||
<image wx:if="{{profile.avatar}}" class="avatar-img" src="{{profile.avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{profile.nickname ? profile.nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<text class="hero-name">{{profile.nickname || '未设置昵称'}}</text>
|
||||
<view class="hero-tags">
|
||||
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{profile.region}}"><icon name="map-pin" size="24" color="currentColor" customClass="tag-icon"></icon><text>{{profile.region}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="state-wrap" wx:if="{{loading}}">
|
||||
<view class="loading-dot"></view>
|
||||
<text class="state-txt">加载中...</text>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="section-head">
|
||||
<icon name="user" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text class="section-title">基本信息</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="field" wx:if="{{profile.industry}}">
|
||||
<text class="field-label">行业</text>
|
||||
<text class="field-value">{{profile.industry}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.position}}">
|
||||
<text class="field-label">职位</text>
|
||||
<text class="field-value">{{profile.position}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.businessScale}}">
|
||||
<text class="field-label">业务体量</text>
|
||||
<text class="field-value">{{profile.businessScale}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.industry || profile.position || profile.businessScale}}"></view>
|
||||
<view class="field" wx:if="{{profile.skills}}">
|
||||
<text class="field-label">我擅长</text>
|
||||
<text class="field-value">{{profile.skills}}</text>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.phoneMask || profile.phone}}">
|
||||
<text class="field-label">联系方式</text>
|
||||
<view class="field-value-row" bindtap="copyPhone">
|
||||
<text class="field-value mono">{{profile.phoneMask || profile.phone || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.phone}}">复制</text>
|
||||
<scroll-view scroll-y class="scroll-wrap" style="height: calc(100vh - {{statusBarHeight + 44}}px - 120rpx);" wx:if="{{!loading && profile}}">
|
||||
<!-- 首屏壳:头像 + 联系方式 -->
|
||||
<view class="shell">
|
||||
<view class="shell-glow"></view>
|
||||
<view class="hero-row">
|
||||
<view class="avatar-outer">
|
||||
<view class="avatar-wrap">
|
||||
<image class="avatar-img" wx:if="{{profile.avatar}}" src="{{profile.avatar}}" mode="aspectFill"/>
|
||||
<view class="avatar-ph" wx:else><text>{{profile.nickname ? profile.nickname[0] : '?'}}</text></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field" wx:if="{{profile.wechatMask || profile.wechat}}">
|
||||
<text class="field-label">微信号</text>
|
||||
<view class="field-value-row" bindtap="copyWechat">
|
||||
<text class="field-value mono">{{profile.wechatMask || profile.wechat || '未填写'}}</text>
|
||||
<text class="field-hint" wx:if="{{profile.wechat}}">复制</text>
|
||||
|
||||
<view class="link-column">
|
||||
<text class="link-column-title">我的联系方式</text>
|
||||
|
||||
<view class="link-chip" wx:if="{{profile.phone}}" bindtap="copyPhone">
|
||||
<view class="link-chip-icon link-chip-icon-phone">
|
||||
<icon name="smartphone" size="34" color="#5EEAD4" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label">手机号</text>
|
||||
<text class="link-chip-val mono">{{profile.phone}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action"><text>复制</text></view>
|
||||
</view>
|
||||
|
||||
<view class="link-chip" wx:if="{{profile.wechat}}" bindtap="copyWechat">
|
||||
<view class="link-chip-icon link-chip-icon-wx">
|
||||
<icon name="message-circle" size="34" color="#34D399" customClass="lc-ic"></icon>
|
||||
</view>
|
||||
<view class="link-chip-main">
|
||||
<text class="link-chip-label">微信号</text>
|
||||
<text class="link-chip-val mono">{{profile.wechat}}</text>
|
||||
</view>
|
||||
<view class="link-chip-action"><text>复制</text></view>
|
||||
</view>
|
||||
|
||||
<view class="link-empty" wx:if="{{!profile.phone && !profile.wechat}}">
|
||||
<text class="link-empty-txt">未填写联系方式</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="field-empty" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.phone && !profile.wechat}}">
|
||||
点击右上角 ⋯ 编辑完善资料
|
||||
</view>
|
||||
|
||||
<text class="profile-name">{{profile.nickname || '未设置昵称'}}</text>
|
||||
<view class="profile-tags" wx:if="{{profile.mbti || profile.region}}">
|
||||
<text class="tag tag-mbti" wx:if="{{profile.mbti}}">{{profile.mbti}}</text>
|
||||
<view class="tag tag-region" wx:if="{{profile.region}}">
|
||||
<icon name="map-pin" size="24" color="currentColor" customClass="pin-icon"></icon>
|
||||
<text>{{profile.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="section" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="section-head">
|
||||
<icon name="lightbulb" size="40" color="#FFD700" customClass="section-icon section-icon-yellow"></icon>
|
||||
<text class="section-title">个人故事</text>
|
||||
<!-- 一体化信息卡 -->
|
||||
<view class="mono-card" wx:if="{{profile.industry || profile.position || profile.businessScale || profile.skills || profile.storyBestMonth || profile.storyAchievement || profile.storyTurning || profile.helpOffer || profile.helpNeed || profile.projectIntro}}">
|
||||
|
||||
<!-- 职业画像 -->
|
||||
<view class="mono-sec" wx:if="{{profile.industry || profile.position || profile.businessScale}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">职业画像</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.industry}}">
|
||||
<text class="kv-k">行业</text>
|
||||
<text class="kv-v">{{profile.industry}}</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.position}}">
|
||||
<text class="kv-k">职位</text>
|
||||
<text class="kv-v">{{profile.position}}</text>
|
||||
</view>
|
||||
<view class="kv" wx:if="{{profile.businessScale}}">
|
||||
<text class="kv-k">业务体量</text>
|
||||
<text class="kv-v">{{profile.businessScale}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="story-block" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最赚钱的一个月做的是什么</text></view>
|
||||
<text class="story-text">{{profile.storyBestMonth}}</text>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.industry || profile.position || profile.businessScale) && profile.skills}}"></view>
|
||||
|
||||
<!-- 我擅长 -->
|
||||
<view class="mono-sec skills-showcase" wx:if="{{profile.skills}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">我擅长</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyAchievement}}">
|
||||
<view class="story-head"><icon name="star" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">最有成就感的一件事</text></view>
|
||||
<text class="story-text">{{profile.storyAchievement}}</text>
|
||||
<view class="skills-quote">
|
||||
<text class="skills-quote-text">{{profile.skills}}</text>
|
||||
</view>
|
||||
<view class="field-divider" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
|
||||
<view class="story-block" wx:if="{{profile.storyTurning}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="28" color="#FFD700" customClass="story-emoji"></icon><text class="story-label">人生的转折点</text></view>
|
||||
<text class="story-text">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{profile.skills && (profile.storyBestMonth || profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
|
||||
<!-- 个人故事 -->
|
||||
<view class="mono-sec" wx:if="{{profile.storyBestMonth || profile.storyAchievement || profile.storyTurning}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">个人故事</text>
|
||||
</view>
|
||||
<view class="story" wx:if="{{profile.storyBestMonth}}">
|
||||
<view class="story-head"><icon name="trophy" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最赚钱的一个月</text></view>
|
||||
<text class="story-a">{{profile.storyBestMonth}}</text>
|
||||
</view>
|
||||
<view class="story-gap" wx:if="{{profile.storyBestMonth && (profile.storyAchievement || profile.storyTurning)}}"></view>
|
||||
<view class="story" wx:if="{{profile.storyAchievement}}">
|
||||
<view class="story-head"><icon name="star" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">最有成就感的事</text></view>
|
||||
<text class="story-a">{{profile.storyAchievement}}</text>
|
||||
</view>
|
||||
<view class="story-gap" wx:if="{{profile.storyAchievement && profile.storyTurning}}"></view>
|
||||
<view class="story" wx:if="{{profile.storyTurning}}">
|
||||
<view class="story-head"><icon name="refresh-cw" size="28" color="#FBBF24" customClass="story-icon"></icon><text class="story-q">人生的转折点</text></view>
|
||||
<text class="story-a">{{profile.storyTurning}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.storyBestMonth || profile.storyAchievement || profile.storyTurning) && (profile.helpOffer || profile.helpNeed)}}"></view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="mono-sec" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">互助需求</text>
|
||||
</view>
|
||||
<view class="help-grid">
|
||||
<view class="help-tile help-give" wx:if="{{profile.helpOffer}}">
|
||||
<text class="help-tile-tag">我能帮你</text>
|
||||
<text class="help-tile-txt">{{profile.helpOffer}}</text>
|
||||
</view>
|
||||
<view class="help-tile help-need" wx:if="{{profile.helpNeed}}">
|
||||
<text class="help-tile-tag need">我需要</text>
|
||||
<text class="help-tile-txt">{{profile.helpNeed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="mono-divider" wx:if="{{(profile.helpOffer || profile.helpNeed) && profile.projectIntro}}"></view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="mono-sec" wx:if="{{profile.projectIntro}}">
|
||||
<view class="mono-sec-head">
|
||||
<text class="mono-sec-title">项目介绍</text>
|
||||
</view>
|
||||
<text class="proj-body">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 互助需求 -->
|
||||
<view class="section" wx:if="{{profile.helpOffer || profile.helpNeed}}">
|
||||
<view class="section-head">
|
||||
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text class="section-title">互助需求</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<view class="help-block" wx:if="{{profile.helpOffer}}">
|
||||
<text class="help-tag help-tag-accent">我能帮你</text>
|
||||
<text class="help-text">{{profile.helpOffer}}</text>
|
||||
</view>
|
||||
<view class="help-block" wx:if="{{profile.helpNeed}}">
|
||||
<text class="help-tag help-tag-orange">我需要帮助</text>
|
||||
<text class="help-text">{{profile.helpNeed}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 空态 -->
|
||||
<view class="empty-hint" wx:if="{{!profile.industry && !profile.position && !profile.businessScale && !profile.skills && !profile.storyBestMonth && !profile.storyAchievement && !profile.storyTurning && !profile.helpOffer && !profile.helpNeed && !profile.projectIntro}}">
|
||||
<icon name="edit" size="48" color="#334155"></icon>
|
||||
<text class="empty-hint-txt">资料尚未完善,点击右上角编辑</text>
|
||||
</view>
|
||||
|
||||
<!-- 项目介绍 -->
|
||||
<view class="section" wx:if="{{profile.projectIntro}}">
|
||||
<view class="section-head">
|
||||
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text class="section-title">项目介绍</text>
|
||||
</view>
|
||||
<view class="section-body">
|
||||
<text class="project-text">{{profile.projectIntro}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="bottom-spacer"></view>
|
||||
<view class="scroll-pad"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部按钮 - 设计稿为描边橙色 -->
|
||||
<view class="bottom-bar">
|
||||
<view class="vip-btn-outline" bindtap="goToVip">
|
||||
<!-- 底部按钮 -->
|
||||
<view class="bottom-bar" wx:if="{{!loading}}">
|
||||
<view class="vip-btn" bindtap="goToVip">
|
||||
<text>成为超级个体</text>
|
||||
<icon name="chevron-right" size="36" color="#00CED1" customClass="vip-btn-arrow"></icon>
|
||||
<icon name="chevron-right" size="32" color="#0f172a"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,115 +1,183 @@
|
||||
/* 个人资料展示页 - enhanced_professional_profile 1:1 重构 */
|
||||
.page { background: #050B14; min-height: 100vh; color: #fff; }
|
||||
/* 卡若创业派对 - 个人资料展示(与 member-detail 同一视觉语言) */
|
||||
.page {
|
||||
background: radial-gradient(120% 80% at 50% -20%, rgba(20, 80, 90, 0.35) 0%, #050B14 45%);
|
||||
min-height: 100vh;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* —— 导航 —— */
|
||||
.nav-bar {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 100;
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 999;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
height: 44px; padding: 0 32rpx;
|
||||
background: rgba(5,11,20,0.9); backdrop-filter: blur(8rpx);
|
||||
border-bottom: 1rpx solid rgba(255,255,255,0.05);
|
||||
padding: 0 28rpx; height: 44px;
|
||||
background: rgba(5, 11, 20, 0.72);
|
||||
backdrop-filter: blur(20rpx); -webkit-backdrop-filter: blur(20rpx);
|
||||
border-bottom: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.nav-back { padding: 16rpx; margin-left: -8rpx; }
|
||||
.back-icon { font-size: 40rpx; color: #5EEAD4; }
|
||||
.nav-title { font-size: 34rpx; font-weight: bold; }
|
||||
.nav-right { padding: 16rpx; }
|
||||
.nav-more { font-size: 48rpx; color: #fff; line-height: 1; }
|
||||
.nav-placeholder { width: 100%; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: flex-start; }
|
||||
.nav-icon { font-size: 44rpx; color: #5eead4; font-weight: 300; }
|
||||
.nav-title { font-size: 32rpx; font-weight: 600; color: #f8fafc; letter-spacing: 4rpx; }
|
||||
.nav-edit { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: flex-end; }
|
||||
|
||||
.loading { padding: 96rpx; text-align: center; color: #94A3B8; }
|
||||
.scroll-wrap { box-sizing: border-box; }
|
||||
|
||||
.scroll-main { height: calc(100vh - 120rpx); padding: 0 32rpx 32rpx; }
|
||||
|
||||
/* 头像区卡片 */
|
||||
.hero-card {
|
||||
position: relative; overflow: hidden;
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 64rpx 32rpx; display: flex; flex-direction: column; align-items: center;
|
||||
/* —— 首屏外壳 —— */
|
||||
.shell {
|
||||
position: relative; margin: 28rpx 24rpx 0; padding: 40rpx 32rpx 36rpx;
|
||||
border-radius: 32rpx;
|
||||
background: linear-gradient(145deg, rgba(22, 36, 48, 0.95) 0%, rgba(12, 20, 32, 0.98) 100%);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.12);
|
||||
box-shadow: 0 24rpx 80rpx rgba(0, 0, 0, 0.45), inset 0 1rpx 0 rgba(255, 255, 255, 0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero-gradient {
|
||||
position: absolute; top: 0; left: 0; right: 0; height: 128rpx;
|
||||
background: linear-gradient(to bottom, rgba(30,58,69,0.3) 0%, transparent 100%);
|
||||
.shell-glow {
|
||||
position: absolute; top: -40%; right: -20%; width: 70%; height: 80%;
|
||||
background: radial-gradient(circle, rgba(45, 212, 191, 0.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; }
|
||||
.hero-avatar {
|
||||
width: 176rpx; height: 176rpx; border-radius: 50%;
|
||||
overflow: hidden; border: 2rpx solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 32rpx;
|
||||
|
||||
.hero-row { position: relative; z-index: 1; display: flex; align-items: flex-start; gap: 28rpx; }
|
||||
|
||||
.avatar-outer { position: relative; width: 168rpx; height: 168rpx; flex-shrink: 0; }
|
||||
.avatar-wrap {
|
||||
width: 100%; height: 100%; border-radius: 50%; overflow: hidden;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: 0 12rpx 40rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
.avatar-img { width: 100%; height: 100%; display: block; }
|
||||
.avatar-placeholder {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
font-size: 72rpx; font-weight: bold; color: #5EEAD4;
|
||||
background: rgba(94,234,212,0.2);
|
||||
.avatar-img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.avatar-ph {
|
||||
width: 100%; height: 100%; background: #1a2332;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 52rpx; color: #5eead4; font-weight: 700;
|
||||
}
|
||||
.hero-name { font-size: 40rpx; font-weight: bold; margin-bottom: 24rpx; }
|
||||
.hero-tags { display: flex; align-items: center; justify-content: center; gap: 24rpx; }
|
||||
.tag { padding: 8rpx 24rpx; border-radius: 999rpx; font-size: 24rpx; font-weight: 500; }
|
||||
.tag-mbti { background: #134E4A; color: #5EEAD4; border: 1rpx solid rgba(94,234,212,0.2); }
|
||||
.tag-region { display: flex; align-items: center; gap: 8rpx; background: #1F2937; color: #d1d5db; border: 1rpx solid rgba(255,255,255,0.1); }
|
||||
.tag-region .tag-icon { flex-shrink: 0; }
|
||||
|
||||
/* 通用区块 */
|
||||
.section {
|
||||
background: #0F1720; border: 1rpx solid rgba(255,255,255,0.08);
|
||||
border-radius: 32rpx; margin-bottom: 32rpx;
|
||||
padding: 40rpx; box-shadow: 0 16rpx 32rpx rgba(0,0,0,0.2);
|
||||
/* 右侧联系方式列 */
|
||||
.link-column { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.link-column-title { font-size: 26rpx; font-weight: 700; color: #f1f5f9; letter-spacing: 2rpx; }
|
||||
|
||||
.link-chip {
|
||||
display: flex; align-items: center; gap: 20rpx;
|
||||
padding: 22rpx; border-radius: 20rpx;
|
||||
background: rgba(15, 23, 42, 0.65);
|
||||
border: 1rpx solid rgba(94, 234, 212, 0.25);
|
||||
}
|
||||
.section-head { display: flex; align-items: center; gap: 20rpx; margin-bottom: 40rpx; }
|
||||
.section-icon { font-size: 40rpx; }
|
||||
.section-icon-yellow { filter: brightness(1.2); }
|
||||
.section-title { font-size: 30rpx; font-weight: bold; }
|
||||
|
||||
.section-body { }
|
||||
.field { margin-bottom: 48rpx; }
|
||||
.field:last-child { margin-bottom: 0; }
|
||||
.field-label { display: block; font-size: 26rpx; color: #94A3B8; margin-bottom: 16rpx; }
|
||||
.field-value { font-size: 30rpx; font-weight: 500; color: #fff; line-height: 1.5; }
|
||||
.field-value.mono { font-family: monospace; letter-spacing: 0.02em; }
|
||||
.field-value-row { display: flex; align-items: center; gap: 16rpx; }
|
||||
.field-hint { font-size: 24rpx; color: #5EEAD4; }
|
||||
.field-divider { height: 1rpx; background: rgba(255,255,255,0.05); margin: 32rpx 0; }
|
||||
.field-empty { font-size: 26rpx; color: #64748b; }
|
||||
|
||||
/* 个人故事 */
|
||||
.story-block { margin-bottom: 48rpx; }
|
||||
.story-block:last-child { margin-bottom: 0; }
|
||||
.story-head { display: flex; align-items: center; gap: 16rpx; margin-bottom: 16rpx; }
|
||||
.story-emoji { font-size: 32rpx; }
|
||||
.story-label { font-size: 26rpx; font-weight: 500; color: #94A3B8; }
|
||||
.story-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; display: block; }
|
||||
|
||||
/* 互助需求 */
|
||||
.help-block {
|
||||
background: #17212F; border: 1rpx solid rgba(255,255,255,0.05);
|
||||
border-radius: 20rpx; padding: 32rpx; margin-bottom: 24rpx;
|
||||
.link-chip:active { background: rgba(15, 30, 40, 0.75); }
|
||||
.link-chip-icon {
|
||||
width: 64rpx; height: 64rpx; border-radius: 16rpx;
|
||||
display: flex; align-items: center; justify-content: center; flex-shrink: 0;
|
||||
}
|
||||
.help-block:last-child { margin-bottom: 0; }
|
||||
.help-tag {
|
||||
display: inline-block; font-size: 22rpx; font-weight: 500;
|
||||
padding: 8rpx 16rpx; border-radius: 8rpx; margin-bottom: 16rpx;
|
||||
.link-chip-icon-phone { background: rgba(45, 212, 191, 0.12); }
|
||||
.link-chip-icon-wx { background: rgba(52, 211, 153, 0.12); }
|
||||
.lc-ic { display: block; }
|
||||
.link-chip-main { flex: 1; min-width: 0; }
|
||||
.link-chip-label { display: block; font-size: 20rpx; color: #94a3b8; margin-bottom: 6rpx; }
|
||||
.link-chip-val { display: block; font-size: 26rpx; font-weight: 600; color: #f8fafc; line-height: 1.35; word-break: break-all; }
|
||||
.link-chip-val.mono { font-family: ui-monospace, monospace; letter-spacing: 1rpx; }
|
||||
.link-chip-action { flex-shrink: 0; font-size: 22rpx; font-weight: 600; color: #5eead4; }
|
||||
|
||||
.link-empty { padding: 24rpx; border-radius: 20rpx; background: rgba(15, 23, 42, 0.4); border: 1rpx dashed rgba(148, 163, 184, 0.2); }
|
||||
.link-empty-txt { font-size: 24rpx; color: #64748b; }
|
||||
|
||||
.profile-name {
|
||||
position: relative; z-index: 1; display: block; text-align: center;
|
||||
margin-top: 36rpx; font-size: 40rpx; font-weight: 700; color: #fff; letter-spacing: 4rpx;
|
||||
}
|
||||
.help-tag-accent { background: #112D2A; color: #5EEAD4; }
|
||||
.help-tag-orange { background: #2D1F0D; color: #F59E0B; }
|
||||
.help-text { font-size: 26rpx; color: #fff; line-height: 1.6; display: block; }
|
||||
.profile-tags {
|
||||
position: relative; z-index: 1;
|
||||
display: flex; align-items: center; justify-content: center; gap: 20rpx; flex-wrap: wrap;
|
||||
margin-top: 24rpx;
|
||||
}
|
||||
.tag { font-size: 24rpx; font-weight: 500; padding: 10rpx 26rpx; border-radius: 999rpx; }
|
||||
.tag-mbti { background: rgba(19, 78, 74, 0.6); color: #5eead4; border: 1rpx solid rgba(94, 234, 212, 0.25); }
|
||||
.tag-region {
|
||||
background: rgba(30, 41, 59, 0.8); color: #cbd5e1; border: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
display: flex; align-items: center; gap: 8rpx;
|
||||
}
|
||||
.pin-icon { color: #f87171; font-size: 22rpx; }
|
||||
|
||||
.project-text { font-size: 28rpx; color: #e5e7eb; line-height: 1.6; }
|
||||
/* —— 一体化信息卡 —— */
|
||||
.mono-card {
|
||||
margin: 24rpx 24rpx 0; padding: 8rpx 0 32rpx; border-radius: 32rpx;
|
||||
background: rgba(15, 23, 34, 0.88);
|
||||
border: 1rpx solid rgba(255, 255, 255, 0.07);
|
||||
box-shadow: 0 16rpx 48rpx rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
.mono-sec { padding: 28rpx 32rpx 8rpx; }
|
||||
.mono-sec-head { margin-bottom: 24rpx; }
|
||||
.mono-sec-title { font-size: 32rpx; font-weight: 700; color: #f8fafc; }
|
||||
.mono-divider { height: 1rpx; margin: 8rpx 32rpx; background: linear-gradient(90deg, transparent, rgba(148, 163, 184, 0.15), transparent); }
|
||||
|
||||
.bottom-spacer { height: 180rpx; }
|
||||
.kv { margin-bottom: 28rpx; }
|
||||
.kv:last-child { margin-bottom: 8rpx; }
|
||||
.kv-k { display: block; font-size: 22rpx; color: #64748b; margin-bottom: 10rpx; }
|
||||
.kv-v { font-size: 28rpx; color: #e2e8f0; line-height: 1.65; font-weight: 500; }
|
||||
|
||||
/* 底部按钮 - 设计稿:透明背景 + 橙色描边 */
|
||||
.skills-showcase .skills-quote {
|
||||
padding: 28rpx 28rpx 28rpx 24rpx; border-radius: 20rpx;
|
||||
background: linear-gradient(105deg, rgba(45, 212, 191, 0.08) 0%, rgba(15, 23, 42, 0.5) 100%);
|
||||
border-left: 6rpx solid #2dd4bf;
|
||||
box-shadow: inset 0 0 0 1rpx rgba(45, 212, 191, 0.12);
|
||||
}
|
||||
.skills-quote-text { font-size: 30rpx; color: #f1f5f9; line-height: 1.75; font-weight: 500; }
|
||||
|
||||
.story { margin-bottom: 8rpx; }
|
||||
.story-gap { height: 28rpx; }
|
||||
.story-head { display: flex; align-items: center; gap: 12rpx; margin-bottom: 12rpx; }
|
||||
.story-icon { flex-shrink: 0; }
|
||||
.story-q { font-size: 24rpx; font-weight: 600; color: #94a3b8; }
|
||||
.story-a { display: block; font-size: 28rpx; color: #e2e8f0; line-height: 1.7; padding-left: 4rpx; }
|
||||
|
||||
.help-grid { display: flex; flex-direction: column; gap: 20rpx; }
|
||||
.help-tile {
|
||||
padding: 28rpx; border-radius: 22rpx;
|
||||
background: rgba(23, 33, 47, 0.9); border: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.help-tile-tag {
|
||||
display: inline-block; font-size: 20rpx; font-weight: 700;
|
||||
padding: 8rpx 18rpx; border-radius: 12rpx; margin-bottom: 16rpx;
|
||||
}
|
||||
.help-give .help-tile-tag { color: #5eead4; background: rgba(6, 78, 59, 0.45); border: 1rpx solid rgba(45, 212, 191, 0.2); }
|
||||
.help-need .help-tile-tag.need { color: #fbbf24; background: rgba(69, 47, 8, 0.45); border: 1rpx solid rgba(251, 191, 36, 0.2); }
|
||||
.help-tile-txt { font-size: 28rpx; color: #f1f5f9; line-height: 1.65; }
|
||||
|
||||
.proj-body { font-size: 28rpx; color: #cbd5e1; line-height: 1.75; padding-bottom: 8rpx; }
|
||||
|
||||
/* —— 空态 —— */
|
||||
.empty-hint {
|
||||
margin: 48rpx 24rpx; padding: 64rpx 32rpx;
|
||||
display: flex; flex-direction: column; align-items: center; gap: 24rpx;
|
||||
border-radius: 28rpx; background: rgba(15, 23, 34, 0.5);
|
||||
border: 1rpx dashed rgba(148, 163, 184, 0.15);
|
||||
}
|
||||
.empty-hint-txt { font-size: 26rpx; color: #64748b; }
|
||||
|
||||
.scroll-pad { height: calc(80rpx + env(safe-area-inset-bottom)); }
|
||||
|
||||
/* —— 底部 —— */
|
||||
.bottom-bar {
|
||||
position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
|
||||
padding: 32rpx; padding-bottom: calc(32rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(5,11,20,0.95); backdrop-filter: blur(8rpx);
|
||||
border-top: 1rpx solid rgba(255,255,255,0.05);
|
||||
padding: 20rpx 24rpx; padding-bottom: calc(20rpx + env(safe-area-inset-bottom));
|
||||
background: rgba(5, 11, 20, 0.92); backdrop-filter: blur(20rpx);
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
.vip-btn-outline {
|
||||
display: flex; align-items: center; justify-content: center; gap: 16rpx;
|
||||
width: 100%; height: 96rpx;
|
||||
background: transparent; color: #F59E0B;
|
||||
border: 2rpx solid rgba(245,158,11,0.3);
|
||||
border-radius: 999rpx; font-size: 30rpx; font-weight: 500;
|
||||
.vip-btn {
|
||||
display: flex; align-items: center; justify-content: center; gap: 12rpx;
|
||||
width: 100%; height: 88rpx; border-radius: 999rpx; border: none;
|
||||
background: linear-gradient(135deg, #5eead4 0%, #2dd4bf 50%, #14b8a6 100%);
|
||||
color: #0f172a; font-size: 28rpx; font-weight: 700;
|
||||
box-shadow: 0 8rpx 28rpx rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
.vip-btn-arrow { font-size: 36rpx; }
|
||||
.vip-btn:active { opacity: 0.85; }
|
||||
|
||||
/* —— 加载态 —— */
|
||||
.state-wrap {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
min-height: 60vh; gap: 24rpx;
|
||||
}
|
||||
.state-txt { font-size: 28rpx; color: #64748b; }
|
||||
.loading-dot {
|
||||
width: 56rpx; height: 56rpx; border-radius: 50%;
|
||||
border: 4rpx solid rgba(94, 234, 212, 0.2); border-top-color: #5eead4;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
@@ -23,20 +23,23 @@ Page({
|
||||
if (userId) {
|
||||
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
|
||||
if (res && res.success && res.data) {
|
||||
const orders = (res.data || []).map(item => ({
|
||||
const raw = (res.data || []).map(item => ({
|
||||
id: item.id || item.order_sn,
|
||||
sectionId: item.product_id || item.section_id,
|
||||
sectionMid: item.section_mid ?? item.mid ?? 0,
|
||||
title: item.product_name || `章节 ${item.product_id || ''}`,
|
||||
amount: item.amount || 0,
|
||||
status: item.status || 'completed',
|
||||
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--'
|
||||
createTime: item.created_at ? new Date(item.created_at).toLocaleDateString() : '--',
|
||||
_sortMs: new Date(item.created_at || item.pay_time || 0).getTime() || 0
|
||||
}))
|
||||
raw.sort((a, b) => b._sortMs - a._sortMs)
|
||||
const orders = raw.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ orders })
|
||||
return
|
||||
}
|
||||
}
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const orders = purchasedSections.map((id, index) => ({
|
||||
id: `order_${index}`,
|
||||
sectionId: id,
|
||||
@@ -49,7 +52,7 @@ Page({
|
||||
this.setData({ orders })
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
const purchasedSections = app.globalData.purchasedSections || []
|
||||
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
this.setData({
|
||||
orders: purchasedSections.map((id, i) => ({
|
||||
id: `order_${i}`, sectionId: id, sectionMid: 0, title: `章节 ${id}`, amount: 1, status: 'completed',
|
||||
@@ -61,6 +64,14 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
goToRead(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const mid = e.currentTarget.dataset.mid
|
||||
if (!id) return
|
||||
const q = mid ? `mid=${mid}` : `id=${id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
onShareAppMessage() {
|
||||
|
||||
@@ -15,9 +15,12 @@
|
||||
</view>
|
||||
|
||||
<view class="orders-list" wx:elif="{{orders.length > 0}}">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id" bindtap="goToRead" data-id="{{item.sectionId}}" data-mid="{{item.sectionMid}}">
|
||||
<view class="order-info">
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
<view class="order-title-row">
|
||||
<text class="order-unlock-icon">🔓</text>
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="order-time">{{item.createTime}}</text>
|
||||
</view>
|
||||
<view class="order-right">
|
||||
|
||||
@@ -9,8 +9,11 @@
|
||||
@keyframes skeleton { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
|
||||
.orders-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.order-item { display: flex; align-items: center; justify-content: space-between; padding: 24rpx; background: #1c1c1e; border-radius: 24rpx; }
|
||||
.order-info { flex: 1; }
|
||||
.order-title { font-size: 28rpx; color: #fff; display: block; margin-bottom: 8rpx; }
|
||||
.order-item:active { opacity: 0.92; }
|
||||
.order-info { flex: 1; min-width: 0; }
|
||||
.order-title-row { display: flex; align-items: flex-start; gap: 12rpx; margin-bottom: 8rpx; }
|
||||
.order-unlock-icon { font-size: 26rpx; line-height: 1.35; opacity: 0.55; flex-shrink: 0; }
|
||||
.order-title { font-size: 28rpx; color: #fff; flex: 1; min-width: 0; }
|
||||
.order-time { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.order-right { text-align: right; }
|
||||
.order-amount { font-size: 28rpx; font-weight: 600; color: #00CED1; display: block; margin-bottom: 4rpx; }
|
||||
|
||||
@@ -13,14 +13,44 @@
|
||||
* - contentSegments 解析每行,mention 高亮可点;点击→确认→登录/资料校验→POST /api/miniprogram/ckb/lead
|
||||
*/
|
||||
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
import readingTracker from '../../utils/readingTracker'
|
||||
const accessManager = require('../../utils/chapterAccessManager')
|
||||
const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
/** 阅读页解析正文用:人物字典 + #标签(与 /config/read-extras 一致) */
|
||||
function getContentParseConfig() {
|
||||
const g = getApp().globalData || {}
|
||||
const raw = Array.isArray(g.mentionPersons) ? g.mentionPersons : []
|
||||
const persons = raw.map((p) => ({
|
||||
personId: p.personId || '',
|
||||
token: p.token || '',
|
||||
name: (p.name || '').trim(),
|
||||
label: (p.label || '').trim(),
|
||||
aliases: p.aliases != null ? String(p.aliases) : '',
|
||||
}))
|
||||
const linkTags = Array.isArray(g.linkTagsConfig) ? g.linkTagsConfig : []
|
||||
return { persons, linkTags }
|
||||
}
|
||||
|
||||
/** 补全 mentionDisplay,避免旧数据无字段;昵称去空白防「@ 名」 */
|
||||
function normalizeMentionSegments(segments) {
|
||||
if (!Array.isArray(segments)) return []
|
||||
return segments.map((row) => {
|
||||
if (!Array.isArray(row)) return row
|
||||
return row.map((seg) => {
|
||||
if (!seg || seg.type !== 'mention') return seg
|
||||
const nick = String(seg.nickname || '')
|
||||
.replace(/^[\s\u00a0\u200b\u3000]+/g, '')
|
||||
.replace(/[\s\u00a0\u200b\u3000]+$/g, '')
|
||||
return { ...seg, nickname: nick, mentionDisplay: '@' + nick }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
// 系统信息
|
||||
@@ -309,6 +339,8 @@ Page({
|
||||
async loadContent(id, accessState, prefetchedChapter) {
|
||||
const cacheKey = `chapter_${id}`
|
||||
try {
|
||||
await app.getReadExtras()
|
||||
const parseCfg = getContentParseConfig()
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
let res = prefetchedChapter
|
||||
if (!res || !res.content) {
|
||||
@@ -325,13 +357,13 @@ Page({
|
||||
// 已解锁用 data.content(完整内容),未解锁用 content(预览);先 determineAccessState 再 loadContent 保证顺序正确
|
||||
const displayContent = accessManager.canAccessFullContent(accessState) ? (res.data?.content ?? res.content) : res.content
|
||||
if (res && displayContent) {
|
||||
const { lines, segments } = contentParser.parseContent(displayContent)
|
||||
const { lines, segments } = contentParser.parseContent(displayContent, parseCfg)
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const updates = {
|
||||
content: displayContent,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
chapterTitle: res.chapterTitle || ''
|
||||
@@ -349,13 +381,14 @@ Page({
|
||||
try {
|
||||
const cached = wx.getStorageSync(cacheKey)
|
||||
if (cached && cached.content) {
|
||||
const { lines, segments } = contentParser.parseContent(cached.content)
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(cached.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
this.setData({
|
||||
content: cached.content,
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
@@ -454,8 +487,9 @@ Page({
|
||||
},
|
||||
|
||||
// 设置章节内容(兼容纯文本/Markdown 与 TipTap HTML)
|
||||
setChapterContent(res) {
|
||||
const { lines, segments } = contentParser.parseContent(res.content)
|
||||
async setChapterContent(res) {
|
||||
await app.getReadExtras()
|
||||
const { lines, segments } = contentParser.parseContent(res.content, getContentParseConfig())
|
||||
// 预览内容由后端统一截取比例,这里展示全部预览内容
|
||||
const previewCount = lines.length
|
||||
const sectionPrice = this.data.sectionPrice ?? 1
|
||||
@@ -472,7 +506,7 @@ Page({
|
||||
content: res.content,
|
||||
previewContent: lines.slice(0, previewCount).join('\n'),
|
||||
contentParagraphs: lines,
|
||||
contentSegments: segments,
|
||||
contentSegments: normalizeMentionSegments(segments),
|
||||
previewParagraphs: lines.slice(0, previewCount),
|
||||
partTitle: res.partTitle || '',
|
||||
// 导航栏、分享等使用的文章标题,同样统一为 sectionTitle
|
||||
@@ -498,7 +532,7 @@ Page({
|
||||
if (currentRetry >= maxRetries) {
|
||||
this.setData({
|
||||
contentParagraphs: ['内容加载失败', '请检查网络连接后下拉刷新重试'],
|
||||
contentSegments: contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments,
|
||||
contentSegments: normalizeMentionSegments(contentParser.parseContent('内容加载失败\n请检查网络连接后下拉刷新重试').segments),
|
||||
previewParagraphs: ['内容加载失败']
|
||||
})
|
||||
return
|
||||
@@ -508,7 +542,7 @@ Page({
|
||||
try {
|
||||
const res = await this.fetchChapterWithTimeout(id, 8000)
|
||||
if (res && res.content) {
|
||||
this.setChapterContent(res)
|
||||
await this.setChapterContent(res)
|
||||
wx.setStorageSync(`chapter_${id}`, res)
|
||||
console.log('[Read] 重试成功:', id, '第', currentRetry + 1, '次')
|
||||
return
|
||||
@@ -674,12 +708,6 @@ Page({
|
||||
})
|
||||
return
|
||||
}
|
||||
// 2 分钟内只能点一次(与后端限频一致,与首页链接卡若共用)
|
||||
const leadLastTs = wx.getStorageSync('lead_last_submit_ts') || 0
|
||||
if (Date.now() - leadLastTs < 2 * 60 * 1000) {
|
||||
wx.showToast({ title: '操作太频繁,请2分钟后再试', icon: 'none' })
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
@@ -875,10 +903,8 @@ Page({
|
||||
#创业派对 #私域运营 #商业案例`
|
||||
|
||||
wx.setClipboardData({
|
||||
data: shareText,
|
||||
success: () => {
|
||||
wx.showToast({ title: '文案已复制', icon: 'success' })
|
||||
}
|
||||
data: shareText
|
||||
// 不额外 showToast:系统已有「内容已复制」,避免与自定义文案叠两层
|
||||
})
|
||||
},
|
||||
|
||||
@@ -929,12 +955,17 @@ Page({
|
||||
wx.setClipboardData({
|
||||
data: copyText,
|
||||
success: () => {
|
||||
wx.showModal({
|
||||
title: '文案已复制',
|
||||
content: '请点击右上角「···」菜单,选择「分享到朋友圈」即可发布',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
// 系统会在 setClipboardData 成功后自动 toast「内容已复制」,与下方引导弹窗重复,先关掉再弹窗
|
||||
wx.hideToast()
|
||||
setTimeout(() => {
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '分享到朋友圈',
|
||||
content: '文案已复制。\n\n请点击右上角「···」菜单,选择「分享到朋友圈」即可发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
}, 120)
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '复制失败,请手动复制', icon: 'none' })
|
||||
@@ -1093,7 +1124,28 @@ Page({
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
// 检查金额是否有效
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
const avatar = userInfo.avatarUrl || ''
|
||||
const nickname = userInfo.nickname || userInfo.nickName || ''
|
||||
const needProfile = !avatar || avatar.includes('default') || avatar.includes('132') || !nickname || nickname === '微信用户'
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '购买前请先完善头像和昵称',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
success: resolve
|
||||
})
|
||||
})
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!amount || amount <= 0) {
|
||||
console.error('[Pay] 金额无效:', amount)
|
||||
wx.showToast({ title: '价格信息错误', icon: 'none' })
|
||||
@@ -1240,13 +1292,13 @@ Page({
|
||||
// 支付接口失败时,显示客服联系方式
|
||||
wx.showModal({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(28533368)手动购买,感谢理解!',
|
||||
content: '微信支付正在审核中,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => {
|
||||
wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
}
|
||||
@@ -1288,13 +1340,13 @@ Page({
|
||||
// 支付失败,可能是参数错误或权限问题
|
||||
wx.showModal({
|
||||
title: '支付失败',
|
||||
content: '微信支付暂不可用,请添加客服微信(28533368)手动购买',
|
||||
content: '微信支付暂不可用,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
data: '28533368',
|
||||
data: app.globalData.serviceWechat || '28533368',
|
||||
success: () => wx.showToast({ title: '微信号已复制', icon: 'success' })
|
||||
})
|
||||
}
|
||||
@@ -1447,18 +1499,22 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
// 生成海报
|
||||
// 生成海报(Canvas 2D API)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true })
|
||||
|
||||
await new Promise((resolve) => {
|
||||
this.setData({ showPosterModal: true, isGeneratingPoster: true }, () => resolve())
|
||||
})
|
||||
await new Promise((resolve) => {
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(resolve)
|
||||
else setTimeout(resolve, 50)
|
||||
})
|
||||
|
||||
try {
|
||||
const ctx = wx.createCanvasContext('posterCanvas', this)
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
const userInfo = app.globalData.userInfo
|
||||
const userId = userInfo?.id || ''
|
||||
|
||||
// 获取小程序码(带推荐人参数,优先 mid 与新链接一致)
|
||||
|
||||
let qrcodeImage = null
|
||||
try {
|
||||
const q = sectionMid ? `mid=${sectionMid}` : `id=${sectionId}`
|
||||
@@ -1467,109 +1523,102 @@ Page({
|
||||
method: 'POST',
|
||||
data: { scene, page: 'pages/read/read', width: 280 }
|
||||
})
|
||||
if (qrRes.success && qrRes.image) {
|
||||
qrcodeImage = qrRes.image
|
||||
if (qrRes.success && qrRes.image) qrcodeImage = qrRes.image
|
||||
} catch (_) {}
|
||||
|
||||
const canvasNode = await new Promise((resolve, reject) => {
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (res && res[0] && res[0].node) resolve(res[0])
|
||||
else reject(new Error('canvas node not found'))
|
||||
})
|
||||
})
|
||||
|
||||
const canvas = canvasNode.node
|
||||
const ctx = canvas.getContext('2d')
|
||||
let dpr = 2
|
||||
try {
|
||||
if (typeof wx.getWindowInfo === 'function') {
|
||||
dpr = wx.getWindowInfo().pixelRatio || 2
|
||||
} else if (wx.getSystemInfoSync) {
|
||||
dpr = wx.getSystemInfoSync().pixelRatio || 2
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Poster] 获取小程序码失败,使用占位符')
|
||||
} catch (_) {
|
||||
dpr = 2
|
||||
}
|
||||
|
||||
// 海报尺寸 300x450
|
||||
const width = 300
|
||||
const height = 450
|
||||
|
||||
// 背景渐变
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.setFillStyle(grd)
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// 顶部装饰条
|
||||
ctx.setFillStyle('#00CED1')
|
||||
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
|
||||
// 标题区域
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(14)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
|
||||
// 章节标题
|
||||
ctx.setFontSize(18)
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText(ctx, title, width - 40, 18)
|
||||
const titleLines = this.wrapText2d(ctx, title, width - 40)
|
||||
let y = 70
|
||||
titleLines.forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 26
|
||||
})
|
||||
|
||||
// 分隔线
|
||||
ctx.setStrokeStyle('rgba(255,255,255,0.1)')
|
||||
titleLines.forEach(line => { ctx.fillText(line, 20, y); y += 26 })
|
||||
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.1)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(width - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
// 内容摘要
|
||||
ctx.setFontSize(12)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.8)')
|
||||
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
||||
y += 30
|
||||
const summary = contentParagraphs.slice(0, 3).join(' ').slice(0, 150) + '...'
|
||||
const summaryLines = this.wrapText(ctx, summary, width - 40, 12)
|
||||
summaryLines.slice(0, 6).forEach(line => {
|
||||
ctx.fillText(line, 20, y)
|
||||
y += 20
|
||||
})
|
||||
|
||||
// 底部区域背景
|
||||
ctx.setFillStyle('rgba(0,206,209,0.1)')
|
||||
const summaryLines = this.wrapText2d(ctx, summary, width - 40)
|
||||
summaryLines.slice(0, 6).forEach(line => { ctx.fillText(line, 20, y); y += 20 })
|
||||
|
||||
ctx.fillStyle = 'rgba(0,206,209,0.1)'
|
||||
ctx.fillRect(0, height - 100, width, 100)
|
||||
|
||||
// 左侧提示文字
|
||||
ctx.setFillStyle('#ffffff')
|
||||
ctx.setFontSize(13)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.setFillStyle('rgba(255,255,255,0.6)')
|
||||
ctx.setFontSize(11)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
// 绘制小程序码或占位符
|
||||
const drawQRCode = () => {
|
||||
return new Promise((resolve) => {
|
||||
if (qrcodeImage) {
|
||||
// 下载base64图片并绘制
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64Data,
|
||||
encoding: 'base64',
|
||||
success: () => {
|
||||
ctx.drawImage(filePath, width - 85, height - 85, 70, 70)
|
||||
resolve()
|
||||
},
|
||||
fail: () => {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder(ctx, width, height)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
if (qrcodeImage) {
|
||||
try {
|
||||
const fs = wx.getFileSystemManager()
|
||||
const filePath = `${wx.env.USER_DATA_PATH}/qrcode_${Date.now()}.png`
|
||||
const base64Data = qrcodeImage.replace(/^data:image\/\w+;base64,/, '')
|
||||
fs.writeFileSync(filePath, base64Data, 'base64')
|
||||
const img = canvas.createImage()
|
||||
await new Promise((resolve, reject) => {
|
||||
img.onload = resolve
|
||||
img.onerror = reject
|
||||
img.src = filePath
|
||||
})
|
||||
ctx.drawImage(img, width - 85, height - 85, 70, 70)
|
||||
} catch (_) {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
}
|
||||
|
||||
await drawQRCode()
|
||||
|
||||
ctx.draw(true, () => {
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
})
|
||||
|
||||
wx.hideLoading()
|
||||
this.setData({ isGeneratingPoster: false })
|
||||
} catch (e) {
|
||||
console.error('生成海报失败:', e)
|
||||
wx.hideLoading()
|
||||
@@ -1577,21 +1626,19 @@ Page({
|
||||
this.setData({ showPosterModal: false, isGeneratingPoster: false })
|
||||
}
|
||||
},
|
||||
|
||||
// 绘制小程序码占位符
|
||||
drawQRPlaceholder(ctx, width, height) {
|
||||
ctx.setFillStyle('#ffffff')
|
||||
|
||||
drawQRPlaceholder2d(ctx, width, height) {
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.beginPath()
|
||||
ctx.arc(width - 50, height - 50, 35, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.setFillStyle('#00CED1')
|
||||
ctx.setFontSize(9)
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.font = '9px sans-serif'
|
||||
ctx.fillText('扫码', width - 57, height - 52)
|
||||
ctx.fillText('阅读', width - 57, height - 40)
|
||||
},
|
||||
|
||||
// 文字换行处理
|
||||
wrapText(ctx, text, maxWidth, fontSize) {
|
||||
|
||||
wrapText2d(ctx, text, maxWidth) {
|
||||
const lines = []
|
||||
let line = ''
|
||||
for (let i = 0; i < text.length; i++) {
|
||||
@@ -1613,39 +1660,47 @@ Page({
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
|
||||
// 保存海报到相册
|
||||
// 保存海报到相册(Canvas 2D)
|
||||
savePoster() {
|
||||
wx.canvasToTempFilePath({
|
||||
canvasId: 'posterCanvas',
|
||||
success: (res) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.openSetting()
|
||||
}
|
||||
wx.createSelectorQuery().in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec(res => {
|
||||
if (!res || !res[0] || !res[0].node) {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
const canvas = res[0].node
|
||||
wx.canvasToTempFilePath({
|
||||
canvas,
|
||||
success: (r2) => {
|
||||
wx.saveImageToPhotosAlbum({
|
||||
filePath: r2.tempFilePath,
|
||||
success: () => {
|
||||
wx.showToast({ title: '已保存到相册', icon: 'success' })
|
||||
this.setData({ showPosterModal: false })
|
||||
},
|
||||
fail: (err) => {
|
||||
if (err.errMsg.includes('auth deny')) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '需要相册权限才能保存海报',
|
||||
confirmText: '去设置',
|
||||
success: (m) => {
|
||||
if (m.confirm) wx.openSetting()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '保存失败', icon: 'none' })
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
})
|
||||
},
|
||||
fail: () => {
|
||||
wx.showToast({ title: '生成图片失败', icon: 'none' })
|
||||
}
|
||||
}, this)
|
||||
}, this)
|
||||
})
|
||||
},
|
||||
|
||||
// 阻止冒泡
|
||||
|
||||
@@ -54,11 +54,9 @@
|
||||
<!-- 完整内容 - 免费或已购买(支持 @ mention / #linkTag / 图片) -->
|
||||
<view class="article" wx:if="{{accessState === 'free' || accessState === 'unlocked_purchased'}}">
|
||||
<view class="paragraph" wx:for="{{contentSegments}}" wx:key="index">
|
||||
<text user-select wx:if="{{!(item.length === 1 && item[0].type === 'image')}}"><block wx:for="{{item}}" wx:key="index" wx:for-item="seg"><text wx:if="{{seg.type === 'text'}}">{{seg.text}}</text><text wx:elif="{{seg.type === 'mention'}}" class="mention" bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">{{seg.mentionDisplay}}</text><text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text></block></text>
|
||||
<block wx:for="{{item}}" wx:key="index" wx:for-item="seg">
|
||||
<text wx:if="{{seg.type === 'text'}}" user-select>{{seg.text}}</text>
|
||||
<text wx:elif="{{seg.type === 'mention'}}" class="mention" user-select bindtap="onMentionTap" data-user-id="{{seg.userId}}" data-nickname="{{seg.nickname}}">@{{seg.nickname}}</text>
|
||||
<text wx:elif="{{seg.type === 'linkTag'}}" class="link-tag" user-select bindtap="onLinkTagTap" data-url="{{seg.url}}" data-label="{{seg.label}}" data-tag-type="{{seg.tagType}}" data-page-path="{{seg.pagePath}}" data-tag-id="{{seg.tagId}}" data-app-id="{{seg.appId}}" data-mp-key="{{seg.mpKey}}">#{{seg.label}}</text>
|
||||
<image wx:elif="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
<image wx:if="{{seg.type === 'image'}}" class="content-image" src="{{seg.src}}" mode="widthFix" show-menu-by-longpress bindtap="onImageTap" data-src="{{seg.src}}"></image>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
@@ -124,15 +122,23 @@
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录 -->
|
||||
<!-- 付费墙 - 未登录:显示购买按钮(朋友圈/分享场景) -->
|
||||
<view class="paywall">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">登录后继续阅读</text>
|
||||
<text class="paywall-desc">已阅读50%,登录后查看完整内容</text>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并购买后阅读全文</text>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal">
|
||||
<text class="login-btn-text">手机号一键登录</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="handlePurchaseSection">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥{{section && section.price != null ? section.price : sectionPrice}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="login-btn" bindtap="showLoginModal" style="margin-top:12px">
|
||||
<text class="login-btn-text">手机号登录后购买</text>
|
||||
</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -274,7 +280,7 @@
|
||||
|
||||
<!-- 海报预览 -->
|
||||
<view class="poster-preview">
|
||||
<canvas canvas-id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
<canvas type="2d" id="posterCanvas" class="poster-canvas" style="width: 300px; height: 450px;"></canvas>
|
||||
</view>
|
||||
|
||||
<view class="poster-actions">
|
||||
@@ -353,6 +359,6 @@
|
||||
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<icon name="globe" size="40" color="#ffffff" customClass="fab-moments-icon"></icon>
|
||||
<icon name="share" size="44" color="#0f172a" customClass="fab-share-icon"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1287,7 +1287,7 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-moments-icon {
|
||||
.fab-share-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
@@ -63,8 +63,7 @@ Page({
|
||||
posterReferralLink: '',
|
||||
posterNickname: '',
|
||||
posterNicknameInitial: '',
|
||||
posterCaseCount: 62,
|
||||
|
||||
posterCaseCount: 62
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -94,28 +93,14 @@ Page({
|
||||
// 生成邀请码
|
||||
const referralCode = userInfo.referralCode || 'SOUL' + (userInfo.id || Date.now().toString(36)).toUpperCase().slice(-6)
|
||||
|
||||
console.log('[Referral] 开始加载分销数据,userId:', userInfo.id)
|
||||
|
||||
// 从API获取真实数据
|
||||
let realData = null
|
||||
try {
|
||||
// app.request 第一个参数是 URL 字符串(会自动拼接 baseUrl)
|
||||
const res = await app.request('/api/miniprogram/referral/data?userId=' + userInfo.id)
|
||||
console.log('[Referral] API返回:', JSON.stringify(res).substring(0, 200))
|
||||
|
||||
if (res && res.success && res.data) {
|
||||
realData = res.data
|
||||
console.log('[Referral] ✅ 获取推广数据成功')
|
||||
console.log('[Referral] - bindingCount:', realData.bindingCount)
|
||||
console.log('[Referral] - paidCount:', realData.paidCount)
|
||||
console.log('[Referral] - earnings:', realData.earnings)
|
||||
console.log('[Referral] - expiringCount:', realData.stats?.expiringCount)
|
||||
} else {
|
||||
console.log('[Referral] ❌ API返回格式错误:', res?.error || 'unknown')
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Referral] ❌ API调用失败:', e.message || e)
|
||||
console.log('[Referral] 错误详情:', e)
|
||||
console.error('[Referral] API调用失败:', e.message || e)
|
||||
}
|
||||
|
||||
// 使用真实数据或默认值
|
||||
@@ -123,14 +108,10 @@ Page({
|
||||
let convertedBindings = realData?.convertedUsers || []
|
||||
let expiredBindings = realData?.expiredUsers || []
|
||||
|
||||
console.log('[Referral] activeBindings:', activeBindings.length)
|
||||
console.log('[Referral] convertedBindings:', convertedBindings.length)
|
||||
console.log('[Referral] expiredBindings:', expiredBindings.length)
|
||||
|
||||
// 计算即将过期的数量(7天内)
|
||||
const expiringCount = realData?.stats?.expiringCount || activeBindings.filter(b => b.daysRemaining <= 7 && b.daysRemaining > 0).length
|
||||
|
||||
console.log('[Referral] expiringCount:', expiringCount)
|
||||
|
||||
// 计算各类统计
|
||||
const bindingCount = realData?.bindingCount || activeBindings.length
|
||||
@@ -153,7 +134,6 @@ Page({
|
||||
purchaseCount: user.purchaseCount || 0,
|
||||
conversionDate: user.conversionDate ? this.formatDate(user.conversionDate) : '--'
|
||||
}
|
||||
console.log('[Referral] 格式化用户:', formatted.nickname, formatted.status, formatted.daysRemaining + '天')
|
||||
return formatted
|
||||
}
|
||||
|
||||
@@ -169,15 +149,6 @@ Page({
|
||||
const availableEarningsNum = Math.max(0, totalCommissionNum - withdrawnNum - pendingWithdrawNum)
|
||||
const minWithdrawAmount = realData?.minWithdrawAmount || 10
|
||||
|
||||
console.log('=== [Referral] 收益计算(完整版)===')
|
||||
console.log('累计佣金 (totalCommission):', totalCommissionNum)
|
||||
console.log('已提现金额 (withdrawnEarnings):', withdrawnNum)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', pendingWithdrawNum)
|
||||
console.log('可提现金额 = 累计 - 已提现 - 待审核 =', totalCommissionNum, '-', withdrawnNum, '-', pendingWithdrawNum, '=', availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', minWithdrawAmount)
|
||||
console.log('按钮判断:', availableEarningsNum, '>=', minWithdrawAmount, '=', availableEarningsNum >= minWithdrawAmount)
|
||||
console.log('✅ 按钮应该:', availableEarningsNum >= minWithdrawAmount ? '🟢 启用(绿色)' : '⚫ 禁用(灰色)')
|
||||
|
||||
const hasWechatId = !!(userInfo?.wechat || userInfo?.wechatId || wx.getStorageSync('user_wechat'))
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
@@ -234,20 +205,6 @@ Page({
|
||||
})
|
||||
|
||||
|
||||
console.log('[Referral] ✅ 数据设置完成')
|
||||
console.log('[Referral] - 绑定中:', this.data.bindingCount)
|
||||
console.log('[Referral] - 即将过期:', this.data.expiringCount)
|
||||
console.log('[Referral] - 收益:', this.data.earnings)
|
||||
|
||||
console.log('=== [Referral] 按钮状态验证 ===')
|
||||
console.log('累计佣金 (totalCommission):', this.data.totalCommission)
|
||||
console.log('待审核金额 (pendingWithdrawAmount):', this.data.pendingWithdrawAmount)
|
||||
console.log('可提现金额 (availableEarnings 显示):', this.data.availableEarnings)
|
||||
console.log('可提现金额 (availableEarningsNum 判断):', this.data.availableEarningsNum, typeof this.data.availableEarningsNum)
|
||||
console.log('最低提现金额 (minWithdrawAmount):', this.data.minWithdrawAmount, typeof this.data.minWithdrawAmount)
|
||||
console.log('按钮启用条件:', this.data.availableEarningsNum, '>=', this.data.minWithdrawAmount, '=', this.data.availableEarningsNum >= this.data.minWithdrawAmount)
|
||||
console.log('✅ 最终结果: 按钮应该', this.data.availableEarningsNum >= this.data.minWithdrawAmount ? '🟢 启用' : '⚫ 禁用')
|
||||
|
||||
// 隐藏加载提示
|
||||
wx.hideLoading()
|
||||
} else {
|
||||
|
||||
@@ -3,20 +3,13 @@
|
||||
* 账号绑定功能
|
||||
*/
|
||||
const app = getApp()
|
||||
const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
version: '1.0.0',
|
||||
isDevMode: false, // 是否开发版(用于显示切换账号入口)
|
||||
|
||||
// 切换账号(开发)
|
||||
showSwitchAccountModal: false,
|
||||
switchAccountUserId: '',
|
||||
switchAccountLoading: false,
|
||||
version: '',
|
||||
|
||||
// 绑定信息
|
||||
phoneNumber: '',
|
||||
@@ -38,16 +31,27 @@ Page({
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : null
|
||||
const envVersion = accountInfo?.miniProgram?.envVersion || ''
|
||||
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
|
||||
const displayVersion =
|
||||
wxPkgVersion ||
|
||||
(app.globalData.appDisplayVersion || '1.7.1')
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight,
|
||||
isLoggedIn: app.globalData.isLoggedIn,
|
||||
userInfo: app.globalData.userInfo,
|
||||
isDevMode: envVersion === 'develop'
|
||||
isDevMode: envVersion === 'develop',
|
||||
version: displayVersion
|
||||
})
|
||||
this.loadBindingInfo()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
const accountInfo = wx.getAccountInfoSync ? wx.getAccountInfoSync() : null
|
||||
const wxPkgVersion = (accountInfo?.miniProgram?.version || '').trim()
|
||||
const displayVersion =
|
||||
wxPkgVersion ||
|
||||
(app.globalData.appDisplayVersion || '1.7.1')
|
||||
this.setData({ version: displayVersion })
|
||||
this.loadBindingInfo()
|
||||
},
|
||||
|
||||
@@ -247,92 +251,6 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
// 获取微信头像(新版授权)
|
||||
async getWechatAvatar() {
|
||||
try {
|
||||
const res = await wx.getUserProfile({
|
||||
desc: '用于完善会员资料'
|
||||
})
|
||||
|
||||
if (res.userInfo) {
|
||||
const { nickName, avatarUrl: tempAvatarUrl } = res.userInfo
|
||||
|
||||
wx.showLoading({ title: '上传中...', mask: true })
|
||||
|
||||
// 1. 先上传图片到服务器
|
||||
console.log('[Settings] 开始上传头像:', tempAvatarUrl)
|
||||
|
||||
const uploadRes = await new Promise((resolve, reject) => {
|
||||
wx.uploadFile({
|
||||
url: app.globalData.baseUrl + '/api/miniprogram/upload',
|
||||
filePath: tempAvatarUrl,
|
||||
name: 'file',
|
||||
formData: {
|
||||
folder: 'avatars'
|
||||
},
|
||||
success: (uploadResult) => {
|
||||
try {
|
||||
const data = JSON.parse(uploadResult.data)
|
||||
if (data.success) {
|
||||
resolve(data)
|
||||
} else {
|
||||
reject(new Error(data.error || '上传失败'))
|
||||
}
|
||||
} catch (err) {
|
||||
reject(new Error('解析响应失败'))
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// 2. 获取上传后的完整URL(显示用);保存时只传路径
|
||||
let avatarUrl = uploadRes.data?.url || uploadRes.url
|
||||
if (avatarUrl && !avatarUrl.startsWith('http')) {
|
||||
avatarUrl = app.globalData.baseUrl + avatarUrl
|
||||
}
|
||||
console.log('[Settings] 头像上传成功:', avatarUrl)
|
||||
|
||||
// 3. 更新本地
|
||||
this.setData({
|
||||
userInfo: {
|
||||
...this.data.userInfo,
|
||||
nickname: nickName,
|
||||
avatar: avatarUrl
|
||||
}
|
||||
})
|
||||
|
||||
// 4. 同步到服务器数据库(只保存路径,不含域名)
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
await app.request('/api/miniprogram/user/profile', {
|
||||
method: 'POST',
|
||||
data: { userId, nickname: nickName, avatar: toAvatarPath(avatarUrl) }
|
||||
})
|
||||
}
|
||||
|
||||
// 5. 更新全局
|
||||
if (app.globalData.userInfo) {
|
||||
app.globalData.userInfo.nickname = nickName
|
||||
app.globalData.userInfo.avatar = avatarUrl
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '头像更新成功', icon: 'success' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
console.error('[Settings] 获取头像失败:', e)
|
||||
wx.showToast({
|
||||
title: e.message || '获取头像失败',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
// 微信隐私协议同意(getPhoneNumber 需先同意)
|
||||
onAgreePrivacyForPhone() {
|
||||
if (app._privacyResolve) {
|
||||
@@ -402,71 +320,6 @@ Page({
|
||||
this.setData({ showBindModal: false })
|
||||
},
|
||||
|
||||
// 跳转账户密码登录页(开发)
|
||||
goToDevLogin() {
|
||||
wx.navigateTo({ url: '/pages/dev-login/dev-login' })
|
||||
},
|
||||
|
||||
// 打开切换账号弹窗(开发)
|
||||
openSwitchAccountModal() {
|
||||
this.setData({
|
||||
showSwitchAccountModal: true,
|
||||
switchAccountUserId: app.globalData.userInfo?.id || ''
|
||||
})
|
||||
},
|
||||
|
||||
// 关闭切换账号弹窗
|
||||
closeSwitchAccountModal() {
|
||||
if (this.data.switchAccountLoading) return
|
||||
this.setData({ showSwitchAccountModal: false, switchAccountUserId: '' })
|
||||
},
|
||||
|
||||
// 切换账号 userId 输入
|
||||
onSwitchAccountUserIdInput(e) {
|
||||
this.setData({ switchAccountUserId: e.detail.value.trim() })
|
||||
},
|
||||
|
||||
// 确认切换账号
|
||||
async confirmSwitchAccount() {
|
||||
const userId = this.data.switchAccountUserId.trim()
|
||||
if (!userId || this.data.switchAccountLoading) return
|
||||
this.setData({ switchAccountLoading: true })
|
||||
try {
|
||||
const res = await app.request('/api/miniprogram/dev/login-as', {
|
||||
method: 'POST',
|
||||
data: { userId }
|
||||
})
|
||||
if (res && res.success && res.data) {
|
||||
const { token, user } = res.data
|
||||
const openId = res.data.openId || ''
|
||||
wx.setStorageSync('token', token)
|
||||
wx.setStorageSync('userInfo', user)
|
||||
app.globalData.userInfo = user
|
||||
app.globalData.isLoggedIn = true
|
||||
app.globalData.purchasedSections = user.purchasedSections || []
|
||||
app.globalData.hasFullBook = user.hasFullBook || false
|
||||
app.globalData.isVip = user.isVip || false
|
||||
app.globalData.vipExpireDate = user.vipExpireDate || ''
|
||||
if (openId) {
|
||||
app.globalData.openId = openId
|
||||
wx.setStorageSync('openId', openId)
|
||||
}
|
||||
this.setData({
|
||||
showSwitchAccountModal: false,
|
||||
switchAccountUserId: '',
|
||||
switchAccountLoading: false
|
||||
})
|
||||
this.loadBindingInfo()
|
||||
wx.showToast({ title: '已切换为 ' + (user.nickname || userId), icon: 'success' })
|
||||
} else {
|
||||
throw new Error(res?.error || '切换失败')
|
||||
}
|
||||
} catch (e) {
|
||||
this.setData({ switchAccountLoading: false })
|
||||
wx.showToast({ title: e.message || '切换失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
// 清除缓存
|
||||
clearCache() {
|
||||
wx.showModal({
|
||||
|
||||
@@ -113,21 +113,7 @@
|
||||
<text class="tip-text">提示:绑定微信号才能使用提现功能</text>
|
||||
</view>
|
||||
|
||||
<!-- 开发专用:切换账号(仅开发版显示) -->
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="openSwitchAccountModal">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="wrench" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">切换账号(开发)</text>
|
||||
<text class="dev-switch-desc">输入 userId 切换为其他账号调试</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="dev-switch-card" wx:if="{{isDevMode}}" bindtap="goToDevLogin">
|
||||
<view class="dev-switch-inner">
|
||||
<icon name="smartphone" size="40" color="#8e8e93" customClass="dev-switch-icon"></icon>
|
||||
<text class="dev-switch-text">账户密码登录</text>
|
||||
<text class="dev-switch-desc">输入对方手机号登录,密码可留空</text>
|
||||
</view>
|
||||
</view>
|
||||
<!-- 开发专用入口已移除 -->
|
||||
|
||||
<view class="logout-btn" wx:if="{{isLoggedIn}}" bindtap="handleLogout">退出登录</view>
|
||||
</view>
|
||||
@@ -164,28 +150,4 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 切换账号弹窗(开发) -->
|
||||
<view class="modal-overlay" wx:if="{{showSwitchAccountModal}}" bindtap="closeSwitchAccountModal">
|
||||
<view class="modal-content" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">切换账号(开发)</text>
|
||||
<view class="modal-close" bindtap="closeSwitchAccountModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
</view>
|
||||
<view class="modal-body">
|
||||
<view class="input-wrapper">
|
||||
<input
|
||||
class="form-input"
|
||||
placeholder="请输入目标用户的 userId(如 ogpTW5fmXRGNpoUbXB3UEqnVe5Tg)"
|
||||
placeholder-class="input-placeholder"
|
||||
value="{{switchAccountUserId}}"
|
||||
bindinput="onSwitchAccountUserIdInput"
|
||||
/>
|
||||
</view>
|
||||
<text class="bind-tip">从管理端或数据库获取要调试的用户 ID,切换后将以该用户身份操作</text>
|
||||
<view class="btn-primary {{!switchAccountUserId || switchAccountLoading ? 'btn-disabled' : ''}}" bindtap="confirmSwitchAccount">
|
||||
{{switchAccountLoading ? '切换中...' : '确认切换'}}
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import accessManager from '../../utils/chapterAccessManager'
|
||||
const accessManager = require('../../utils/chapterAccessManager')
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
@@ -12,16 +12,16 @@ Page({
|
||||
originalPrice: 6980,
|
||||
/* 按 premium_membership_landing_v1 设计稿 */
|
||||
contentRights: [
|
||||
{ title: '解锁全部章节', desc: '365天全案精读', icon: 'book-open' },
|
||||
{ title: '案例库', desc: '100+创业实战案例', icon: 'book-open' },
|
||||
{ title: '智能纪要', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ title: '会议纪要库', desc: '往期完整沉淀', icon: 'folder' }
|
||||
{ title: '匹配伙伴', desc: '精准匹配创业伙伴', icon: 'users' },
|
||||
{ title: '派对专属', desc: '创业派对房专享', icon: 'star' },
|
||||
{ title: '老板排行', desc: '创业老板排行榜', icon: 'bar-chart' },
|
||||
{ title: '轮流置顶', desc: '首页获客曝光位', icon: 'arrow-up' }
|
||||
],
|
||||
socialRights: [
|
||||
{ title: '匹配创业伙伴', desc: '精准人脉匹配', icon: 'users' },
|
||||
{ title: '创业老板排行', desc: '项目曝光展示', icon: 'bar-chart' },
|
||||
{ title: '链接资源', desc: '深度私域资源池', icon: 'link' },
|
||||
{ title: '专属VIP标识', desc: '金色尊享光圈', icon: 'check' }
|
||||
{ title: '案例宝库', desc: '100+赚钱案例库', icon: 'book-open' },
|
||||
{ title: '全书解锁', desc: '365天全案精读', icon: 'folder' },
|
||||
{ title: '每日总结', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ title: '获取客资', desc: '文章@你即可获客', icon: 'link' }
|
||||
],
|
||||
purchasing: false
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user