feat: 小程序阅读记录与资料链路、管理端用户规则、API/VIP/推荐与运营脚本
- miniprogram: reading-records、imageUrl/mpNavigate、多页资料与 VIP 展示调整 - soul-admin: Users/Settings/UserDetailModal、dist 构建产物更新 - soul-api: user/vip/referral/ckb/db、MBTI 头像管理、user_rule_completion、迁移 SQL - .cursor: karuo-party 与飞书文档;.gitignore 忽略 .tmp_skill_bundle Made-with: Cursor
This commit is contained in:
@@ -61,7 +61,7 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 联系方式 - 引导到Soul派对房 -->
|
||||
<!-- 联系方式 - Soul 派对房 -->
|
||||
<view class="contact-card" wx:if="{{!authorLoading}}">
|
||||
<text class="card-title">联系作者</text>
|
||||
<view class="contact-item">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 卡若创业派对 - 头像昵称引导页
|
||||
* 登录后资料未完善时引导用户修改默认头像和昵称,仅包含头像+昵称两项
|
||||
* 卡若创业派对 - 头像与昵称设置页
|
||||
* 登录后若仍为默认头像/昵称,在此修改;仅头像与昵称两项
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"navigationBarTitleText": "完善资料",
|
||||
"navigationBarTitleText": "头像与昵称",
|
||||
"usingComponents": {}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
<!--卡若创业派对 - 头像昵称引导页,仅头像+昵称-->
|
||||
<!--卡若创业派对 - 头像与昵称设置页-->
|
||||
<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="rgba(255,255,255,0.8)" customClass="back-icon"></icon></view>
|
||||
<text class="nav-title">完善资料</text>
|
||||
<text class="nav-title">头像与昵称</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content">
|
||||
<!-- 引导文案 -->
|
||||
<view class="guide-card">
|
||||
<icon name="handshake" size="64" color="#00CED1" customClass="guide-icon"></icon>
|
||||
<text class="guide-title">完善头像和昵称</text>
|
||||
<text class="guide-desc">让他人更好地认识你,展示更专业的形象</text>
|
||||
<text class="guide-title">设置对外展示信息</text>
|
||||
<text class="guide-desc">头像与昵称会出现在名片与匹配卡片上,方便伙伴认出你。</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像:点击直接弹出微信原生选择器;头像与文字水平对齐 -->
|
||||
@@ -62,7 +61,7 @@
|
||||
</view>
|
||||
|
||||
<view class="link-row" bindtap="goToFullProfile">
|
||||
<text class="link-text">完善更多资料</text>
|
||||
<text class="link-text">编辑完整档案</text>
|
||||
<icon name="chevron-right" size="28" color="#00CED1" customClass="link-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* 卡若创业派对 - 头像昵称引导页 */
|
||||
/* 卡若创业派对 - 头像与昵称设置页 */
|
||||
.page {
|
||||
background: #050B14;
|
||||
min-height: 100vh;
|
||||
|
||||
@@ -26,6 +26,7 @@ Page({
|
||||
|
||||
// 展开状态
|
||||
expandedPart: null,
|
||||
bookCollapsed: false,
|
||||
|
||||
// 已加载的篇章章节缓存 { partId: chapters }
|
||||
_loadedChapters: {},
|
||||
@@ -47,7 +48,11 @@ Page({
|
||||
partsLoading: true,
|
||||
|
||||
// 功能配置(搜索开关)
|
||||
searchEnabled: true
|
||||
searchEnabled: true,
|
||||
|
||||
// mp_config.mpUi.chaptersPage
|
||||
chaptersBookTitle: '一场SOUL的创业实验场',
|
||||
chaptersBookSubtitle: '来自Soul派对房的真实商业故事'
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -62,10 +67,20 @@ Page({
|
||||
this.loadFeatureConfig()
|
||||
},
|
||||
|
||||
_applyChaptersMpUi() {
|
||||
const c = app.globalData.configCache?.mpConfig?.mpUi?.chaptersPage || {}
|
||||
this.setData({
|
||||
chaptersBookTitle: String(c.bookTitle || '一场SOUL的创业实验场').trim() || '一场SOUL的创业实验场',
|
||||
chaptersBookSubtitle: String(c.bookSubtitle || '来自Soul派对房的真实商业故事').trim() ||
|
||||
'来自Soul派对房的真实商业故事'
|
||||
})
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
if (app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean') {
|
||||
this.setData({ searchEnabled: app.globalData.features.searchEnabled })
|
||||
this._applyChaptersMpUi()
|
||||
return
|
||||
}
|
||||
const res = await app.getConfig()
|
||||
@@ -74,8 +89,10 @@ Page({
|
||||
if (!app.globalData.features) app.globalData.features = {}
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
this.setData({ searchEnabled })
|
||||
this._applyChaptersMpUi()
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true })
|
||||
this._applyChaptersMpUi()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -92,7 +109,6 @@ Page({
|
||||
totalSections = res.totalSections ?? 0
|
||||
fixedSections = res.fixedSections || []
|
||||
}
|
||||
const numbers = ['一', '二', '三', '四', '五', '六', '七', '八', '九', '十', '十一', '十二']
|
||||
const fixedMap = {}
|
||||
fixedSections.forEach(f => { fixedMap[f.id] = f.mid })
|
||||
const appendixList = [
|
||||
@@ -100,13 +116,14 @@ Page({
|
||||
{ id: 'appendix-2', title: '附录2|创业者自检清单', mid: fixedMap['appendix-2'] },
|
||||
{ id: 'appendix-3', title: '附录3|本书提到的工具和资源', mid: fixedMap['appendix-3'] }
|
||||
]
|
||||
const bookData = parts.map((p, idx) => ({
|
||||
const bookData = parts.map((p) => ({
|
||||
id: p.id,
|
||||
number: numbers[idx] || String(idx + 1),
|
||||
icon: p.icon || '',
|
||||
title: p.title,
|
||||
subtitle: p.subtitle || '',
|
||||
chapterCount: p.chapterCount || 0,
|
||||
chapters: [] // 展开时懒加载
|
||||
chapters: [],
|
||||
alwaysShow: (p.title || '').indexOf('每日派对干货') > -1
|
||||
}))
|
||||
app.globalData.totalSections = totalSections
|
||||
this.setData({
|
||||
@@ -186,6 +203,7 @@ Page({
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._applyChaptersMpUi()
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
const tabBar = this.getTabBar()
|
||||
@@ -225,7 +243,11 @@ Page({
|
||||
this.setData({ isLoggedIn, hasFullBook, purchasedSections, isVip })
|
||||
},
|
||||
|
||||
// 切换展开状态,展开时懒加载该篇章章节
|
||||
toggleBookCollapse() {
|
||||
trackClick('chapters', 'btn_click', '折叠书名')
|
||||
this.setData({ bookCollapsed: !this.data.bookCollapsed })
|
||||
},
|
||||
|
||||
async togglePart(e) {
|
||||
trackClick('chapters', 'tab_click', e.currentTarget.dataset.id || '篇章')
|
||||
const partId = e.currentTarget.dataset.id
|
||||
|
||||
@@ -34,18 +34,21 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 书籍信息卡 -->
|
||||
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}">
|
||||
<!-- 书籍信息卡(点击折叠/展开除"每日派对干货"外的篇章) -->
|
||||
<view class="book-info-card card-gradient" wx:if="{{!partsLoading}}" bindtap="toggleBookCollapse">
|
||||
<view class="book-icon">
|
||||
<view class="book-icon-inner"><icon name="book-open" size="56" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
<view class="book-info">
|
||||
<text class="book-title">一场SOUL的创业实验场</text>
|
||||
<text class="book-subtitle">来自Soul派对房的真实商业故事</text>
|
||||
<text class="book-title">{{chaptersBookTitle}}</text>
|
||||
<text class="book-subtitle">{{chaptersBookSubtitle}}</text>
|
||||
</view>
|
||||
<view class="book-count">
|
||||
<text class="count-value brand-color">{{totalSections}}</text>
|
||||
<text class="count-label">章节</text>
|
||||
<view class="book-right-area">
|
||||
<view class="book-count">
|
||||
<text class="count-value brand-color">{{totalSections}}</text>
|
||||
<text class="count-label">章节</text>
|
||||
</view>
|
||||
<text class="book-collapse-hint">{{bookCollapsed ? '展开 ▸' : '折叠 ▾'}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -65,11 +68,12 @@
|
||||
|
||||
<!-- 篇章列表 -->
|
||||
<view class="part-list">
|
||||
<view class="part-item" wx:for="{{bookData}}" wx:key="id">
|
||||
<view class="part-item" wx:for="{{bookData}}" wx:key="id" wx:if="{{!bookCollapsed || item.alwaysShow}}">
|
||||
<!-- 篇章标题 -->
|
||||
<view class="part-header" bindtap="togglePart" data-id="{{item.id}}">
|
||||
<view class="part-left">
|
||||
<view class="part-icon">{{item.number}}</view>
|
||||
<image wx:if="{{item.icon}}" class="part-icon-img" src="{{item.icon}}" mode="aspectFit"/>
|
||||
<view wx:else class="part-icon">{{item.title[0] || '篇'}}</view>
|
||||
<view class="part-info">
|
||||
<text class="part-title">{{item.title}}</text>
|
||||
<text class="part-subtitle">{{item.subtitle}}</text>
|
||||
|
||||
@@ -195,20 +195,11 @@
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.book-count {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.count-value {
|
||||
font-size: 40rpx;
|
||||
font-weight: 700;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.count-label {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
.book-right-area { display: flex; flex-direction: column; align-items: flex-end; gap: 8rpx; }
|
||||
.book-count { text-align: right; }
|
||||
.count-value { font-size: 40rpx; font-weight: 700; display: block; }
|
||||
.count-label { font-size: 20rpx; color: rgba(255, 255, 255, 0.4); }
|
||||
.book-collapse-hint { font-size: 20rpx; color: #00CED1; opacity: 0.7; }
|
||||
|
||||
/* ===== 目录内容 ===== */
|
||||
.chapters-content {
|
||||
@@ -365,6 +356,9 @@
|
||||
color: #ffffff;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.part-icon-img {
|
||||
width: 64rpx; height: 64rpx; border-radius: 16rpx; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.part-info {
|
||||
display: flex;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser')
|
||||
const { navigateMpPath } = require('../../utils/mpNavigate.js')
|
||||
|
||||
/** 与首页固定「卡若」获客位重复时从横滑列表剔除(含历史误写「卡路」) */
|
||||
function isKaruoHostDuplicateName(displayName) {
|
||||
@@ -85,7 +86,19 @@ Page({
|
||||
searchEnabled: true,
|
||||
|
||||
// 审核模式:隐藏支付相关入口
|
||||
auditMode: false
|
||||
auditMode: false,
|
||||
|
||||
// mp_config.mpUi.homePage(后台系统设置 mpUi)
|
||||
mpUiLogoTitle: '卡若创业派对',
|
||||
mpUiLogoSubtitle: '来自派对房的真实故事',
|
||||
mpUiLinkKaruoText: '点击链接卡若',
|
||||
mpUiSearchPlaceholder: '搜索章节标题或内容...',
|
||||
mpUiBannerTag: '推荐',
|
||||
mpUiBannerReadMore: '点击阅读',
|
||||
mpUiSuperTitle: '超级个体',
|
||||
mpUiSuperLinkText: '获客入口',
|
||||
mpUiPickTitle: '精选推荐',
|
||||
mpUiLatestTitle: '最新新增'
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
@@ -111,6 +124,7 @@ Page({
|
||||
onShow() {
|
||||
console.log('[Index] onShow 触发')
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
this._applyHomeMpUi()
|
||||
|
||||
// 设置TabBar选中状态
|
||||
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
|
||||
@@ -305,6 +319,30 @@ Page({
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
_applyHomeMpUi() {
|
||||
const h = app.globalData.configCache?.mpConfig?.mpUi?.homePage || {}
|
||||
this.setData({
|
||||
mpUiLogoTitle: String(h.logoTitle || '卡若创业派对').trim() || '卡若创业派对',
|
||||
mpUiLogoSubtitle: String(h.logoSubtitle || '来自派对房的真实故事').trim() || '来自派对房的真实故事',
|
||||
mpUiLinkKaruoText: String(h.linkKaruoText || '点击链接卡若').trim() || '点击链接卡若',
|
||||
mpUiSearchPlaceholder: String(h.searchPlaceholder || '搜索章节标题或内容...').trim() || '搜索章节标题或内容...',
|
||||
mpUiBannerTag: String(h.bannerTag || '推荐').trim() || '推荐',
|
||||
mpUiBannerReadMore: String(h.bannerReadMoreText || '点击阅读').trim() || '点击阅读',
|
||||
mpUiSuperTitle: String(h.superSectionTitle || '超级个体').trim() || '超级个体',
|
||||
mpUiSuperLinkText: String(h.superSectionLinkText || '获客入口').trim() || '获客入口',
|
||||
mpUiPickTitle: String(h.pickSectionTitle || '精选推荐').trim() || '精选推荐',
|
||||
mpUiLatestTitle: String(h.latestSectionTitle || '最新新增').trim() || '最新新增'
|
||||
})
|
||||
},
|
||||
|
||||
/** 超级个体右侧文案:默认跳转找伙伴 Tab(路径可由 homePage.superSectionLinkPath 配置) */
|
||||
goSuperSectionLink() {
|
||||
const p = String(
|
||||
app.globalData.configCache?.mpConfig?.mpUi?.homePage?.superSectionLinkPath || '/pages/match/match'
|
||||
).trim()
|
||||
if (p) navigateMpPath(p)
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
try {
|
||||
const hasCachedFeatures = app.globalData.features && typeof app.globalData.features.searchEnabled === 'boolean'
|
||||
@@ -313,6 +351,7 @@ Page({
|
||||
searchEnabled: app.globalData.features.searchEnabled,
|
||||
auditMode: app.globalData.auditMode || false
|
||||
})
|
||||
this._applyHomeMpUi()
|
||||
return
|
||||
}
|
||||
const res = await app.getConfig()
|
||||
@@ -324,8 +363,10 @@ Page({
|
||||
app.globalData.features.searchEnabled = searchEnabled
|
||||
app.globalData.auditMode = auditMode
|
||||
this.setData({ searchEnabled, auditMode })
|
||||
this._applyHomeMpUi()
|
||||
} catch (e) {
|
||||
this.setData({ searchEnabled: true, auditMode: app.globalData.auditMode || false })
|
||||
this._applyHomeMpUi()
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -12,16 +12,11 @@
|
||||
<text class="logo-text">派</text>
|
||||
</view>
|
||||
<view class="logo-info">
|
||||
<text class="logo-title-text">卡若创业派对</text>
|
||||
<text class="logo-subtitle">来自派对房的真实故事</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right">
|
||||
<view class="contact-btn" bindtap="onLinkKaruo">
|
||||
<image class="contact-avatar" src="/assets/images/author-avatar.png" mode="aspectFill"/>
|
||||
<text class="contact-name">点击链接卡若</text>
|
||||
<text class="logo-title-text">{{mpUiLogoTitle}}</text>
|
||||
<text class="logo-subtitle">{{mpUiLogoSubtitle}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="header-right"></view>
|
||||
</view>
|
||||
|
||||
<!-- 搜索栏(根据配置显示) -->
|
||||
@@ -29,7 +24,7 @@
|
||||
<view class="search-icon-wrap">
|
||||
<icon name="search" size="40" color="#8e8e93" customClass="search-icon-text"></icon>
|
||||
</view>
|
||||
<text class="search-placeholder">搜索章节标题或内容...</text>
|
||||
<text class="search-placeholder">{{mpUiSearchPlaceholder}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -38,26 +33,26 @@
|
||||
<!-- 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-tag">{{mpUiBannerTag}}</view>
|
||||
<view class="banner-title">{{bannerSection.title}}</view>
|
||||
<view class="banner-action">
|
||||
<text class="banner-action-text">点击阅读</text>
|
||||
<text class="banner-action-text">{{mpUiBannerReadMore}}</text>
|
||||
<icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon>
|
||||
</view>
|
||||
</view>
|
||||
<view class="banner-card banner-skeleton" wx:else bindtap="goToChapters">
|
||||
<view class="banner-glow"></view>
|
||||
<view class="banner-tag">推荐</view>
|
||||
<view class="banner-tag">{{mpUiBannerTag}}</view>
|
||||
<view class="banner-title">加载中...</view>
|
||||
<view class="banner-action"><text class="banner-action-text">点击阅读</text><icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon></view>
|
||||
<view class="banner-action"><text class="banner-action-text">{{mpUiBannerReadMore}}</text><icon name="direction-right" size="32" color="#00CED1" customClass="banner-arrow"></icon></view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 超级个体:与匹配页一致,仅 VIP 横向列表(无首位特例) -->
|
||||
<view class="section" wx:if="{{!auditMode}}">
|
||||
<view class="section-header">
|
||||
<text class="section-title">超级个体</text>
|
||||
<text class="section-subtitle">获客入口</text>
|
||||
<text class="section-title">{{mpUiSuperTitle}}</text>
|
||||
<text class="section-subtitle" bindtap="goSuperSectionLink">{{mpUiSuperLinkText}}</text>
|
||||
</view>
|
||||
<!-- 加载中:骨架动画 -->
|
||||
<view wx:if="{{superMembersLoading}}" class="super-loading">
|
||||
@@ -100,7 +95,7 @@
|
||||
<!-- 精选推荐:默认 3 条,列表下三角展开更多(与最新新增一致) -->
|
||||
<view class="section">
|
||||
<view class="section-header">
|
||||
<text class="section-title">精选推荐</text>
|
||||
<text class="section-title">{{mpUiPickTitle}}</text>
|
||||
</view>
|
||||
<view class="featured-list">
|
||||
<view
|
||||
@@ -134,7 +129,7 @@
|
||||
<!-- 最新新增(时间线样式;超过 5 条时在列表下方点小三角展开,展开后随页面滚动看全部) -->
|
||||
<view class="section" wx:if="{{latestChapters.length > 0}}">
|
||||
<view class="section-header latest-header">
|
||||
<text class="section-title">最新新增</text>
|
||||
<text class="section-title">{{mpUiLatestTitle}}</text>
|
||||
</view>
|
||||
<view class="timeline-wrap">
|
||||
<view class="timeline-line"></view>
|
||||
@@ -165,25 +160,4 @@
|
||||
<!-- 底部留白 -->
|
||||
<view class="bottom-space"></view>
|
||||
|
||||
<!-- 链接卡若 - 留资弹窗(未填手机/微信号时):一键获取 + 手动输入 -->
|
||||
<view class="lead-mask" wx:if="{{showLeadModal}}" catchtap="closeLeadModal">
|
||||
<!-- 使用 catchtap="stopPropagation" 阻止内部点击冒泡到遮罩层,避免点击输入框时弹窗被关闭 -->
|
||||
<view class="lead-box" catchtap="stopPropagation">
|
||||
<text class="lead-title">留下联系方式</text>
|
||||
<text class="lead-desc">方便卡若与您联系</text>
|
||||
<button id="agree-lead-phone-btn" class="lead-get-phone-btn" open-type="getPhoneNumber|agreePrivacyAuthorization" bindgetphonenumber="onGetPhoneNumberForLead" bindagreeprivacyauthorization="onAgreePrivacyForLead">一键获取手机号</button>
|
||||
<view class="privacy-wechat-row" wx:if="{{showPrivacyModal}}">
|
||||
<text class="privacy-wechat-desc">为获取手机号,请先同意《用户隐私保护指引》</text>
|
||||
<button id="agree-privacy-btn" class="privacy-agree-btn" open-type="agreePrivacyAuthorization" bindagreeprivacyauthorization="onAgreePrivacyForLead">同意</button>
|
||||
</view>
|
||||
<text class="lead-divider">或手动输入</text>
|
||||
<view class="lead-input-wrap">
|
||||
<input class="lead-input" placeholder="请输入手机号" type="number" maxlength="11" value="{{leadPhone}}" bindinput="onLeadPhoneInput"/>
|
||||
</view>
|
||||
<view class="lead-actions">
|
||||
<button class="lead-btn lead-btn-cancel" bindtap="closeLeadModal">取消</button>
|
||||
<button class="lead-btn lead-btn-submit" bindtap="submitLead">提交</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -8,6 +8,18 @@ const app = getApp()
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
|
||||
/** 是否视为未设置头像(勿用 includes('132'):微信 CDN 合法头像 URL 普遍含 /132/ 尺寸段) */
|
||||
function isMissingOrPlaceholderAvatar(avatarUrl, hasAvatarFromServer) {
|
||||
if (hasAvatarFromServer === true || hasAvatarFromServer === 1) return false
|
||||
const a = (avatarUrl || '').trim()
|
||||
if (!a) return true
|
||||
const u = a.toLowerCase()
|
||||
if (u.includes('default')) return true
|
||||
// 微信默认占位常见以 /0 结尾;/132/ 为正常尺寸路径,不能当作占位
|
||||
if (/\/0($|[?#])/.test(u)) return true
|
||||
return false
|
||||
}
|
||||
|
||||
// 默认匹配类型配置
|
||||
// 找伙伴:真正的匹配功能,匹配数据库中的真实用户
|
||||
// 资源对接:需要登录+购买章节才能使用,填写2项信息(我能帮到你什么、我需要什么帮助)
|
||||
@@ -226,19 +238,21 @@ 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) {
|
||||
const d = res?.data || {}
|
||||
const avatar = (d.avatar || d.avatarUrl || app.globalData.userInfo?.avatar || app.globalData.userInfo?.avatarUrl || '').trim()
|
||||
const hasAvatarFlag = d.hasAvatar === true || d.hasAvatar === 1
|
||||
if (isMissingOrPlaceholderAvatar(avatar, hasAvatarFlag)) {
|
||||
wx.showModal({
|
||||
title: '完善头像',
|
||||
content: '请先设置头像后再使用匹配功能',
|
||||
confirmText: '去设置',
|
||||
cancelText: '取消',
|
||||
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()
|
||||
const phone = (d.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechat = (d.wechatId || d.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone)) {
|
||||
callback()
|
||||
return
|
||||
@@ -413,6 +427,16 @@ Page({
|
||||
|
||||
// 开始匹配 - 只匹配数据库中的真实用户
|
||||
async startMatch() {
|
||||
const uidEarly = app.globalData.userInfo?.id
|
||||
if (!uidEarly) {
|
||||
wx.showModal({
|
||||
title: '需要登录',
|
||||
content: '找伙伴匹配需登录账号,请先登录',
|
||||
confirmText: '去登录',
|
||||
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) },
|
||||
})
|
||||
return
|
||||
}
|
||||
this.setData({
|
||||
isMatching: true,
|
||||
matchAttempts: 0,
|
||||
@@ -424,23 +448,39 @@ Page({
|
||||
this.setData({ matchAttempts: this.data.matchAttempts + 1 })
|
||||
}, 1000)
|
||||
|
||||
// 从数据库获取真实用户匹配
|
||||
// 从数据库获取真实用户匹配(带上手机/微信写入 match_records,与流量池运营对齐)
|
||||
let matchedUser = null
|
||||
let matchFailHint = ''
|
||||
const uid = app.globalData.userInfo?.id || ''
|
||||
const phoneForMatch = (this.data.phoneNumber || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
const wechatForMatch = (this.data.wechatId || wx.getStorageSync('user_wechat') || '').trim()
|
||||
try {
|
||||
const res = await app.request({ url: '/api/miniprogram/match/users', silent: true,
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/match/users',
|
||||
silent: true,
|
||||
method: 'POST',
|
||||
data: {
|
||||
matchType: this.data.selectedType,
|
||||
userId: app.globalData.userInfo?.id || ''
|
||||
}
|
||||
userId: uid,
|
||||
phone: phoneForMatch || undefined,
|
||||
wechatId: wechatForMatch || undefined,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
if (res.success && res.data) {
|
||||
matchedUser = res.data
|
||||
console.log('[Match] 从数据库匹配到用户:', matchedUser.nickname)
|
||||
} else if (res && !res.success) {
|
||||
matchFailHint = res.message || res.error || ''
|
||||
if (res.code === 'QUOTA_EXCEEDED') {
|
||||
matchFailHint = matchFailHint || '今日免费次数已用完,可购买额外匹配次数后再试'
|
||||
} else if (res.code === 'NO_USERS') {
|
||||
matchFailHint = matchFailHint || '当前流量池暂无可匹配用户,可稍后再试;补全档案后匹配范围通常更大。'
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('[Match] 数据库匹配失败:', e)
|
||||
matchFailHint = (e && e.message) ? String(e.message) : '网络异常,请稍后重试'
|
||||
}
|
||||
|
||||
// 延迟显示结果(模拟匹配过程)
|
||||
@@ -453,7 +493,7 @@ Page({
|
||||
this.setData({ isMatching: false })
|
||||
wx.showModal({
|
||||
title: '暂无匹配',
|
||||
content: '当前暂无合适的匹配用户,请稍后再试',
|
||||
content: matchFailHint || '当前暂无合适的匹配用户,请稍后再试',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
@@ -498,7 +538,7 @@ Page({
|
||||
}
|
||||
}
|
||||
})
|
||||
// 匹配后规则:引导填写 MBTI/行业信息
|
||||
// 匹配后规则:资料未齐时提示补全(服务端 profile 合并,见 ruleEngine)
|
||||
checkAndExecute('after_match', this)
|
||||
} catch (e) {
|
||||
console.log('上报匹配失败:', e)
|
||||
|
||||
@@ -5,21 +5,70 @@
|
||||
* mbti, region, industry, position, businessScale, skills,
|
||||
* storyBestMonth→bestMonth, storyAchievement→achievement, storyTurning→turningPoint,
|
||||
* helpOffer→canHelp, helpNeed→needHelp
|
||||
* 点头像:若后台 persons.user_id 已绑定则带 ckbLeadToken,走存客宝 CKBLead(与阅读页 @ 一致)
|
||||
* 点头像:登录后依次校验本人头像(非默认)、微信号、绑定手机号,再弹「链接「昵称」」;有 ckbLeadToken 走人物获客计划,否则走全局留资
|
||||
*/
|
||||
const app = getApp()
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
|
||||
Page({
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true },
|
||||
data: { statusBarHeight: 44, navBarTotalPx: 88, member: null, loading: true, isOwnProfile: false },
|
||||
|
||||
onLoad(options) {
|
||||
wx.showShareMenu({ withShareTimeline: true })
|
||||
const sb = app.globalData.statusBarHeight || 44
|
||||
this.setData({ statusBarHeight: sb, navBarTotalPx: sb + 44 })
|
||||
const myId = app.globalData.userInfo?.id
|
||||
const isOwnProfile = !!(options.id && myId && String(options.id) === String(myId))
|
||||
this.setData({ statusBarHeight: sb, navBarTotalPx: sb + 44, isOwnProfile })
|
||||
if (options.id) this.loadMember(options.id)
|
||||
},
|
||||
|
||||
/** 本人名片:去完整编辑资料(单页) */
|
||||
goMyProfileEdit() {
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit?full=1' })
|
||||
},
|
||||
|
||||
async loadMember(id) {
|
||||
const myId = app.globalData.userInfo?.id
|
||||
const isOwn = !!(myId && id != null && String(id) === String(myId))
|
||||
if (isOwn && app.globalData.isLoggedIn && myId) {
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/user/profile?userId=${encodeURIComponent(String(id))}`,
|
||||
silent: true
|
||||
})
|
||||
if (res?.success && res.data) {
|
||||
const d = res.data
|
||||
this.setData({
|
||||
member: this.enrichAndFormat({
|
||||
id: d.id,
|
||||
nickname: d.nickname,
|
||||
name: d.nickname,
|
||||
avatar: d.avatar,
|
||||
phone: d.phone,
|
||||
wechatId: d.wechatId || d.wechat_id,
|
||||
isVip: !!app.globalData.isVip,
|
||||
mbti: d.mbti,
|
||||
region: d.region,
|
||||
industry: d.industry,
|
||||
position: d.position,
|
||||
businessScale: d.businessScale || d.business_scale,
|
||||
skills: d.skills,
|
||||
storyBestMonth: d.storyBestMonth || d.story_best_month,
|
||||
storyAchievement: d.storyAchievement || d.story_achievement,
|
||||
storyTurning: d.storyTurning || d.story_turning,
|
||||
helpOffer: d.helpOffer || d.help_offer,
|
||||
helpNeed: d.helpNeed || d.help_need,
|
||||
projectIntro: d.projectIntro || d.project_intro,
|
||||
ckbLeadToken: d.ckbLeadToken || d.ckb_lead_token
|
||||
}),
|
||||
loading: false
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/vip/members?id=${id}`, silent: true })
|
||||
if (res?.success && res.data) {
|
||||
@@ -64,10 +113,11 @@ Page({
|
||||
|
||||
enrichAndFormat(raw) {
|
||||
const e = (v) => this._emptyIfPlaceholder(v)
|
||||
const rawAv = raw.avatar || raw.vipAvatar || raw.vip_avatar || ''
|
||||
const merged = {
|
||||
id: raw.id,
|
||||
name: raw.nickname || raw.name || raw.vipName || raw.vip_name || '创业者',
|
||||
avatar: raw.avatar || raw.vipAvatar || raw.vip_avatar || '',
|
||||
avatar: isSafeImageSrc(rawAv) ? String(rawAv).trim() : '',
|
||||
isVip: !!(raw.isVip || raw.is_vip),
|
||||
mbti: e(raw.mbti),
|
||||
region: e(raw.region),
|
||||
@@ -152,55 +202,167 @@ Page({
|
||||
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' })
|
||||
/** 链接前:头像需非空且非默认图;微信号需已填写(与 app._needsAvatarNickname 中头像规则一致) */
|
||||
_hasCustomAvatarForLink(u) {
|
||||
const avatar = (u && (u.avatar || u.avatarUrl) || '').trim()
|
||||
return !!avatar && !avatar.includes('default')
|
||||
},
|
||||
|
||||
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token */
|
||||
async _doCkbLeadSubmit(targetUserId, targetNickname) {
|
||||
_hasWechatFilledForLink(u) {
|
||||
const w = (u && (u.wechatId || u.wechat_id) || wx.getStorageSync('user_wechat') || '').trim()
|
||||
return w.length > 0
|
||||
},
|
||||
|
||||
/** 拉取最新 profile 写回 globalData,返回合并后的访客资料片段 */
|
||||
async _refreshVisitorProfileForLink() {
|
||||
const base = app.globalData.userInfo
|
||||
if (!base?.id) return base || {}
|
||||
try {
|
||||
const profileRes = await app.request({ url: `/api/miniprogram/user/profile?userId=${base.id}`, silent: true })
|
||||
if (profileRes?.success && profileRes.data) {
|
||||
const d = profileRes.data
|
||||
const updated = { ...app.globalData.userInfo }
|
||||
if (d.avatar != null && String(d.avatar).trim()) updated.avatar = String(d.avatar).trim()
|
||||
if (d.wechatId != null) updated.wechatId = d.wechatId
|
||||
if (d.wechat_id != null) updated.wechat_id = d.wechat_id
|
||||
app.globalData.userInfo = updated
|
||||
wx.setStorageSync('userInfo', updated)
|
||||
if (d.wechatId) wx.setStorageSync('user_wechat', String(d.wechatId).trim())
|
||||
return updated
|
||||
}
|
||||
} catch (e) {}
|
||||
return base
|
||||
},
|
||||
|
||||
async _ensureVisitorReadyForMemberLink() {
|
||||
const u = await this._refreshVisitorProfileForLink()
|
||||
const avatarOk = this._hasCustomAvatarForLink(u)
|
||||
const wechatOk = this._hasWechatFilledForLink(u)
|
||||
if (avatarOk && wechatOk) return true
|
||||
const miss = []
|
||||
if (!avatarOk) miss.push('头像')
|
||||
if (!wechatOk) miss.push('微信号')
|
||||
wx.showModal({
|
||||
title: '补全本人档案',
|
||||
content: `链接前请补全本人${miss.join('与')},便于对方识别与安全对接。`,
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
/** 链接前:必须已绑定大陆手机号(与留资接口校验一致) */
|
||||
async _ensurePhoneBoundForLink(myUserId) {
|
||||
const { phone } = await this._resolveLeadPhoneWechat(myUserId)
|
||||
if (phone && /^1[3-9]\d{9}$/.test(phone)) return true
|
||||
wx.showModal({
|
||||
title: '请先绑定手机号',
|
||||
content: '链接对方前需绑定本人手机号,便于跟进与对接。',
|
||||
confirmText: '去绑定',
|
||||
cancelText: '取消',
|
||||
success: (r) => { if (r.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return false
|
||||
},
|
||||
|
||||
/**
|
||||
* 点头像:登录 → 头像+微信号 → 手机号 均校验通过后弹窗说明;确认后 POST ckb/lead。
|
||||
* 有 ckbLeadToken 走人物计划;无 token 走全局留资。对方已公开联系方式时可取消后在下方自行添加。
|
||||
*/
|
||||
async startLinkFlow() {
|
||||
if (this.data.isOwnProfile) return
|
||||
const member = this.data.member
|
||||
if (!member) return
|
||||
const nickname = (member.name || 'TA').trim() || 'TA'
|
||||
trackClick('member_detail', 'btn_click', '链接头像_' + (member.id || ''))
|
||||
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再添加好友',
|
||||
title: `链接「${nickname}」`,
|
||||
content: '请先登录后再发起链接。',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
success: (r) => { if (r.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
wx.showLoading({ title: '请稍候', mask: true })
|
||||
let profileOk = false
|
||||
let phoneOk = false
|
||||
try {
|
||||
profileOk = await this._ensureVisitorReadyForMemberLink()
|
||||
if (profileOk) phoneOk = await this._ensurePhoneBoundForLink(myUserId)
|
||||
} finally {
|
||||
wx.hideLoading()
|
||||
}
|
||||
if (!profileOk || !phoneOk) return
|
||||
|
||||
const leadTok = (member.ckbLeadToken || '').trim()
|
||||
const content = leadTok
|
||||
? `确定后提交联系方式,平台将按对方配置跟进;智能助手与人工协同协助对接。`
|
||||
: `智能助手与人工会协同跟进,协助您对接「${nickname}」。\n\n若对方已公开手机或微信,可先点「取消」,在页面下方自行添加。`
|
||||
|
||||
wx.showModal({
|
||||
title: `链接「${nickname}」`,
|
||||
content,
|
||||
confirmText: '确定链接',
|
||||
cancelText: '取消',
|
||||
success: (r) => {
|
||||
if (!r.confirm) return
|
||||
if (leadTok) this._doCkbLeadSubmit(leadTok, nickname, member.id, nickname)
|
||||
else this._doGlobalMemberLeadSubmit(member)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/** 无人物 token 时:全局留资,便于运营侧主动加好友并协助链接该会员 */
|
||||
async _doGlobalMemberLeadSubmit(member) {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) return
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '补全手机号',
|
||||
content: '请填写手机号(必填),便于工作人员联系您、协助链接该超级个体。',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
wx.showLoading({ title: '提交中...', mask: true })
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: '/api/miniprogram/ckb/lead',
|
||||
method: 'POST',
|
||||
data: {
|
||||
userId: myUserId,
|
||||
phone: phone || undefined,
|
||||
wechatId: wechatId || undefined,
|
||||
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
|
||||
targetNickname: '',
|
||||
targetMemberId: member.id || undefined,
|
||||
targetMemberName: (member.name || '').trim() || undefined,
|
||||
source: 'member_detail_global',
|
||||
}
|
||||
})
|
||||
wx.hideLoading()
|
||||
if (res && res.success) {
|
||||
wx.setStorageSync('lead_last_submit_ts', Date.now())
|
||||
wx.showToast({ title: res.message || '提交成功,请留意工作人员联系', icon: 'success' })
|
||||
} else {
|
||||
wx.showToast({ title: (res && res.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: (e && e.message) || '提交失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
async _resolveLeadPhoneWechat(myUserId) {
|
||||
let phone = (app.globalData.userInfo.phone || wx.getStorageSync('user_phone') || '').trim().replace(/\s/g, '')
|
||||
let wechatId = (app.globalData.userInfo.wechatId || app.globalData.userInfo.wechat_id || wx.getStorageSync('user_wechat') || '').trim()
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
@@ -212,10 +374,27 @@ Page({
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
return { phone, wechatId }
|
||||
},
|
||||
|
||||
/** 与 read 页 _doMentionAddFriend 一致:targetUserId = Person.token;可选带超级个体 userId 写入留资 params */
|
||||
async _doCkbLeadSubmit(targetUserId, targetNickname, targetMemberId, targetMemberName) {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo) {
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '请先登录后再添加好友',
|
||||
confirmText: '去登录',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.switchTab({ url: '/pages/my/my' }) }
|
||||
})
|
||||
return
|
||||
}
|
||||
const myUserId = app.globalData.userInfo.id
|
||||
let { phone, wechatId } = await this._resolveLeadPhoneWechat(myUserId)
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号(必填),以便对方通过获客计划联系您',
|
||||
title: '补全手机号',
|
||||
content: '请填写手机号(必填),便于对方通过获客计划联系您。',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => { if (res.confirm) wx.navigateTo({ url: '/pages/profile-edit/profile-edit' }) }
|
||||
@@ -234,6 +413,8 @@ Page({
|
||||
name: (app.globalData.userInfo.nickname || '').trim() || undefined,
|
||||
targetUserId,
|
||||
targetNickname: targetNickname || undefined,
|
||||
targetMemberId: targetMemberId || undefined,
|
||||
targetMemberName: targetMemberName || undefined,
|
||||
source: 'member_detail_avatar'
|
||||
}
|
||||
})
|
||||
@@ -372,11 +553,56 @@ Page({
|
||||
goToVip() { wx.navigateTo({ url: '/pages/vip/vip' }) },
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
/**
|
||||
* 分享标题:姓名|MBTI(有则加)|擅长;无擅长时用个人故事或职业一行。总长控制避免微信卡片截断难看。
|
||||
*/
|
||||
_buildMemberShareTitle(maxLen = 56) {
|
||||
const m = this.data.member
|
||||
if (!m) return '卡若创业派对 · 超级个体'
|
||||
|
||||
const clip = (s, n) => {
|
||||
if (!s) return ''
|
||||
const t = String(s).replace(/\s+/g, ' ').trim()
|
||||
return t.length <= n ? t : t.slice(0, n - 1) + '…'
|
||||
}
|
||||
|
||||
const name = clip((m.name || '创业者').trim() || '创业者', 14)
|
||||
const mbti = (m.mbti || '').trim()
|
||||
const skills = (m.skills || '').trim()
|
||||
const achievement = (m.achievement || '').trim()
|
||||
const bestMonth = (m.bestMonth || '').trim()
|
||||
const turning = (m.turningPoint || '').trim()
|
||||
const story = achievement || bestMonth || turning || ''
|
||||
const jobLine = [m.industry, m.position].filter(Boolean).join('·')
|
||||
|
||||
const sep = '|'
|
||||
const nameSeg = name
|
||||
const mbtiSeg = mbti ? clip(mbti, 8) : ''
|
||||
const usedBase = nameSeg.length + (mbtiSeg ? sep.length + mbtiSeg.length : 0)
|
||||
const willTail = !!(skills || story || jobLine || m.region)
|
||||
const restBudget = Math.max(6, maxLen - usedBase - (willTail ? sep.length : 0))
|
||||
|
||||
let tail = ''
|
||||
if (skills) tail = clip(skills, restBudget)
|
||||
else if (story) tail = clip(story, restBudget)
|
||||
else if (jobLine) tail = clip(jobLine, restBudget)
|
||||
else if (m.region) tail = clip(m.region, restBudget)
|
||||
|
||||
let title = nameSeg
|
||||
if (mbtiSeg) title += sep + mbtiSeg
|
||||
if (tail) title += sep + tail
|
||||
|
||||
title = clip(title.replace(/\s+/g, ' '), maxLen)
|
||||
if (!title || title.length < 3) title = clip(`${nameSeg}${sep}超级个体`, maxLen)
|
||||
return title
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
const title = this._buildMemberShareTitle(64)
|
||||
return {
|
||||
title: '卡若创业派对 - 创业者详情',
|
||||
title,
|
||||
path: id && ref ? `/pages/member-detail/member-detail?id=${id}&ref=${ref}` : id ? `/pages/member-detail/member-detail?id=${id}` : ref ? `/pages/member-detail/member-detail?ref=${ref}` : '/pages/member-detail/member-detail'
|
||||
}
|
||||
},
|
||||
@@ -384,7 +610,13 @@ Page({
|
||||
onShareTimeline() {
|
||||
const ref = app.getMyReferralCode()
|
||||
const id = this.data.member?.id
|
||||
const m = this.data.member
|
||||
const q = id ? (ref ? `id=${id}&ref=${ref}` : `id=${id}`) : (ref ? `ref=${ref}` : '')
|
||||
return { title: '卡若创业派对 - 创业者详情', query: q }
|
||||
const title = this._buildMemberShareTitle(56)
|
||||
const res = { title, query: q }
|
||||
if (m && m.avatar && /^https?:\/\//.test(String(m.avatar))) {
|
||||
res.imageUrl = m.avatar
|
||||
}
|
||||
return res
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,20 +1,29 @@
|
||||
<!-- 卡若创业派对 - 超级个体详情(居中头像区 + 低调联系方式 + 信息卡) -->
|
||||
<!-- 卡若创业派对 - 超级个体详情(点头像申请对接 + 有则展示联系方式 + 信息卡) -->
|
||||
<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>
|
||||
</view>
|
||||
<text class="nav-title">个人资料</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
<text class="nav-title">{{isOwnProfile ? '我的名片' : '个人资料'}}</text>
|
||||
<view class="nav-edit-wrap" wx:if="{{isOwnProfile}}" bindtap="goMyProfileEdit">
|
||||
<text class="nav-edit-text">编辑</text>
|
||||
</view>
|
||||
<view class="nav-placeholder" wx:else></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></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="hero-avatar-block"
|
||||
wx:if="{{!isOwnProfile}}"
|
||||
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"/>
|
||||
@@ -30,10 +39,39 @@
|
||||
<text>{{member.region}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="avatar-link-hint-one">点头像 · 申请对接</text>
|
||||
</view>
|
||||
<view
|
||||
class="hero-avatar-block hero-avatar-block-self"
|
||||
wx:else
|
||||
bindtap="goMyProfileEdit"
|
||||
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>
|
||||
<text class="avatar-link-hint-one self-hint">这是我的超级个体名片 · 可转发分享 · 点头像去编辑</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="contact-rows contact-rows-subtle">
|
||||
<view
|
||||
class="contact-rows contact-rows-subtle"
|
||||
wx:if="{{member.contactRaw || member.contactDisplay || member.wechatRaw || member.wechatDisplay}}"
|
||||
>
|
||||
<view class="contact-sec-label">联系方式</view>
|
||||
<view
|
||||
class="link-chip link-chip-subtle {{member.contactUnlocked ? 'link-chip-open' : ''}}"
|
||||
wx:if="{{member.contactRaw || member.contactDisplay}}"
|
||||
@@ -69,10 +107,6 @@
|
||||
<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>
|
||||
|
||||
|
||||
@@ -43,6 +43,23 @@
|
||||
.nav-placeholder {
|
||||
width: 72rpx;
|
||||
}
|
||||
.nav-edit-wrap {
|
||||
min-width: 72rpx;
|
||||
padding: 12rpx 8rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.nav-edit-text {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #5eead4;
|
||||
}
|
||||
.self-hint {
|
||||
font-size: 22rpx !important;
|
||||
line-height: 1.45;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.scroll-wrap {
|
||||
box-sizing: border-box;
|
||||
@@ -75,7 +92,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 8rpx;
|
||||
padding-bottom: 4rpx;
|
||||
}
|
||||
|
||||
.hero-avatar-block {
|
||||
@@ -88,6 +105,27 @@
|
||||
opacity: 0.94;
|
||||
}
|
||||
|
||||
.avatar-link-hint-one {
|
||||
margin-top: 16rpx;
|
||||
padding: 0 24rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(148, 163, 184, 0.88);
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.contact-sec-label {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(148, 163, 184, 0.7);
|
||||
letter-spacing: 4rpx;
|
||||
margin-bottom: 8rpx;
|
||||
padding-left: 6rpx;
|
||||
}
|
||||
|
||||
.contact-rows {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -98,8 +136,8 @@
|
||||
}
|
||||
|
||||
.contact-rows-subtle {
|
||||
margin-top: 24rpx;
|
||||
padding-top: 24rpx;
|
||||
margin-top: 28rpx;
|
||||
padding-top: 28rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
@@ -153,7 +191,7 @@
|
||||
font-weight: 800;
|
||||
padding: 6rpx 12rpx;
|
||||
border-radius: 12rpx;
|
||||
z-index: 2;
|
||||
z-index: 5;
|
||||
box-shadow: 0 4rpx 14rpx rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
@@ -273,26 +311,6 @@
|
||||
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;
|
||||
|
||||
@@ -8,21 +8,8 @@ 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
|
||||
}
|
||||
const { navigateMpPath } = require('../../utils/mpNavigate.js')
|
||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -97,10 +84,12 @@ Page({
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 已解锁章节(订单倒序;默认最多展示 5 条,底部倒三角展开)
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
// mp_config.mpUi.myPage(后台可改文案/跳转)
|
||||
mpUiCardLabel: '名片',
|
||||
mpUiVipLabelVip: '会员中心',
|
||||
mpUiVipLabelGuest: '成为会员',
|
||||
mpUiReadStatLabel: '已读章节',
|
||||
mpUiRecentTitle: '最近阅读',
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -130,6 +119,27 @@ Page({
|
||||
}
|
||||
}
|
||||
this.initUserStatus()
|
||||
this._applyMyMpUiLabels()
|
||||
},
|
||||
|
||||
_getMyPageUi() {
|
||||
const cache = app.globalData.configCache || {}
|
||||
const fromNew = cache?.mpConfig?.mpUi?.myPage
|
||||
if (fromNew && typeof fromNew === 'object') return fromNew
|
||||
const fromLegacy = cache?.configs?.mp_config?.mpUi?.myPage
|
||||
if (fromLegacy && typeof fromLegacy === 'object') return fromLegacy
|
||||
return {}
|
||||
},
|
||||
|
||||
_applyMyMpUiLabels() {
|
||||
const my = this._getMyPageUi()
|
||||
this.setData({
|
||||
mpUiCardLabel: String(my.cardLabel || '名片').trim() || '名片',
|
||||
mpUiVipLabelVip: String(my.vipLabelVip || '会员中心').trim() || '会员中心',
|
||||
mpUiVipLabelGuest: String(my.vipLabelGuest || '成为会员').trim() || '成为会员',
|
||||
mpUiReadStatLabel: String(my.readStatLabel || '已读章节').trim() || '已读章节',
|
||||
mpUiRecentTitle: String(my.recentReadTitle || '最近阅读').trim() || '最近阅读'
|
||||
})
|
||||
},
|
||||
|
||||
async loadFeatureConfig() {
|
||||
@@ -144,9 +154,11 @@ Page({
|
||||
app.globalData.auditMode = auditMode
|
||||
app.globalData.features = { matchEnabled, referralEnabled, searchEnabled }
|
||||
this.setData({ matchEnabled, referralEnabled, searchEnabled, auditMode })
|
||||
this._applyMyMpUiLabels()
|
||||
} catch (error) {
|
||||
console.log('加载功能配置失败:', error)
|
||||
this.setData({ matchEnabled: false, referralEnabled: true, searchEnabled: true })
|
||||
this._applyMyMpUiLabels()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -158,11 +170,17 @@ Page({
|
||||
const userId = userInfo.id || ''
|
||||
const userIdShort = userId.length > 20 ? userId.slice(0, 10) + '...' + userId.slice(-6) : userId
|
||||
const userWechat = wx.getStorageSync('user_wechat') || userInfo.wechat || ''
|
||||
|
||||
const safeUser = { ...userInfo }
|
||||
if (!isSafeImageSrc(safeUser.avatar)) safeUser.avatar = ''
|
||||
app.globalData.userInfo = safeUser
|
||||
try {
|
||||
wx.setStorageSync('userInfo', safeUser)
|
||||
} catch (_) {}
|
||||
|
||||
// 先设基础信息;阅读统计与收益再分别从后端刷新
|
||||
this.setData({
|
||||
isLoggedIn: true,
|
||||
userInfo,
|
||||
userInfo: safeUser,
|
||||
userIdShort,
|
||||
userWechat,
|
||||
readCount: 0,
|
||||
@@ -182,9 +200,9 @@ Page({
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadWalletBalance()
|
||||
this.loadUnlockedChapters()
|
||||
} else {
|
||||
const guestReadCount = app.getReadCount()
|
||||
const guestRecent = this._mergeRecentChaptersFromLocal([])
|
||||
this.setData({
|
||||
isLoggedIn: false,
|
||||
userInfo: null,
|
||||
@@ -195,10 +213,7 @@ Page({
|
||||
earnings: '-',
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: [],
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
recentChapters: guestRecent,
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
totalReadTimeText: '0',
|
||||
@@ -207,88 +222,83 @@ 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 || '')
|
||||
}
|
||||
}
|
||||
/** 本地已打开的章节 id(reading_progress 键 + 历史 readSectionIds),用于与服务端合并展示 */
|
||||
_localSectionIdsFromStorage() {
|
||||
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 })
|
||||
const progressData = wx.getStorageSync('reading_progress') || {}
|
||||
const fromProgress = Object.keys(progressData).filter(Boolean)
|
||||
let fromReadList = []
|
||||
try {
|
||||
const rs = wx.getStorageSync('readSectionIds')
|
||||
if (Array.isArray(rs)) fromReadList = rs.filter(Boolean)
|
||||
} catch (_) {}
|
||||
return [...new Set([...fromProgress, ...fromReadList])]
|
||||
} catch (_) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
expandUnlockedChapters() {
|
||||
if (this.data.unlockedExpanded) return
|
||||
trackClick('my', 'tab_click', '已解锁章节_展开')
|
||||
const full = this.data.unlockedChaptersFull || []
|
||||
/** 接口无最近阅读时,用本地 reading_progress + recent_section_opens + bookData 补全 */
|
||||
_mergeRecentChaptersFromLocal(apiList) {
|
||||
const normalized = Array.isArray(apiList)
|
||||
? apiList.map((item) => ({
|
||||
id: item.id,
|
||||
mid: item.mid,
|
||||
title: cleanSingleLineField(item.title || '') || `章节 ${item.id}`
|
||||
}))
|
||||
: []
|
||||
if (normalized.length > 0) return normalized
|
||||
try {
|
||||
const progressData = wx.getStorageSync('reading_progress') || {}
|
||||
let opens = wx.getStorageSync('recent_section_opens')
|
||||
if (!Array.isArray(opens)) opens = []
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
const titleOf = (id) => {
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
return cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
}
|
||||
const midOf = (id) => {
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
return row?.mid ?? row?.MID ?? 0
|
||||
}
|
||||
const latest = new Map()
|
||||
const bump = (sid, ts) => {
|
||||
if (!sid) return
|
||||
const id = String(sid)
|
||||
const t = typeof ts === 'number' && !isNaN(ts) ? ts : 0
|
||||
const prev = latest.get(id) || 0
|
||||
if (t >= prev) latest.set(id, t)
|
||||
}
|
||||
Object.keys(progressData).forEach((id) => {
|
||||
const row = progressData[id]
|
||||
bump(id, row?.lastOpenAt || row?.last_open_at || 0)
|
||||
})
|
||||
opens.forEach((o) => bump(o && o.id, o && o.t))
|
||||
return [...latest.entries()]
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5)
|
||||
.map(([id]) => ({ id, mid: midOf(id), title: titleOf(id) }))
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
},
|
||||
|
||||
/** 接口失败或无 data 时,仅用本地 reading_progress / readSectionIds 刷新已读与最近 */
|
||||
_hydrateReadStatsFromLocal() {
|
||||
const localExtra = this._localSectionIdsFromStorage()
|
||||
const readSectionIds = [...new Set(localExtra.filter(Boolean))]
|
||||
app.globalData.readSectionIds = readSectionIds
|
||||
try {
|
||||
wx.setStorageSync('readSectionIds', readSectionIds)
|
||||
} catch (_) {}
|
||||
const recentChapters = this._mergeRecentChaptersFromLocal([])
|
||||
const readCount = readSectionIds.length
|
||||
this.setData({
|
||||
unlockedExpanded: true,
|
||||
displayUnlockedChapters: full
|
||||
readCount,
|
||||
readCountText: formatStatNum(readCount),
|
||||
recentChapters
|
||||
})
|
||||
},
|
||||
|
||||
@@ -302,21 +312,29 @@ Page({
|
||||
silent: true
|
||||
})
|
||||
|
||||
if (!res?.success || !res.data) return
|
||||
if (!res?.success || !res.data) {
|
||||
this._hydrateReadStatsFromLocal()
|
||||
return
|
||||
}
|
||||
|
||||
const apiIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds.filter(Boolean) : []
|
||||
const localExtra = this._localSectionIdsFromStorage()
|
||||
const prevGlobal = Array.isArray(app.globalData.readSectionIds) ? app.globalData.readSectionIds.filter(Boolean) : []
|
||||
const readSectionIds = [...new Set([...apiIds, ...prevGlobal, ...localExtra])]
|
||||
|
||||
const readSectionIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds : []
|
||||
app.globalData.readSectionIds = readSectionIds
|
||||
wx.setStorageSync('readSectionIds', readSectionIds)
|
||||
|
||||
const recentChapters = Array.isArray(res.data.recentChapters)
|
||||
const apiRecent = Array.isArray(res.data.recentChapters)
|
||||
? res.data.recentChapters.map((item) => ({
|
||||
id: item.id,
|
||||
mid: item.mid,
|
||||
title: item.title || `章节 ${item.id}`
|
||||
}))
|
||||
: []
|
||||
const recentChapters = this._mergeRecentChaptersFromLocal(apiRecent)
|
||||
|
||||
const readCount = Number(res.data.readCount || 0)
|
||||
const readCount = readSectionIds.length
|
||||
const totalReadTime = Number(res.data.totalReadMinutes || 0)
|
||||
const matchHistory = Number(res.data.matchHistory || 0)
|
||||
const orderCount = Number(res.data.orderCount || 0)
|
||||
@@ -334,6 +352,7 @@ Page({
|
||||
})
|
||||
} catch (e) {
|
||||
console.log('[My] 拉取阅读统计失败:', e && e.message)
|
||||
this._hydrateReadStatsFromLocal()
|
||||
}
|
||||
},
|
||||
|
||||
@@ -574,7 +593,11 @@ Page({
|
||||
wx.showToast({ title: '已刷新', icon: 'success' })
|
||||
},
|
||||
|
||||
// 微信原生获取头像(button open-type="chooseAvatar" 回调,点击头像直接唤起选择器)
|
||||
tapAvatar() {
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
wx.navigateTo({ url: '/pages/profile-edit/profile-edit' })
|
||||
},
|
||||
|
||||
async onChooseAvatar(e) {
|
||||
const tempAvatarUrl = e.detail?.avatarUrl
|
||||
if (!tempAvatarUrl) return
|
||||
@@ -856,9 +879,33 @@ Page({
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
},
|
||||
|
||||
// 跳转到目录
|
||||
goToChapters() {
|
||||
// 已读章节:进入阅读记录页(有列表);路径可由 mpUi.myPage.readStatPath 配置
|
||||
goToReadStat() {
|
||||
trackClick('my', 'nav_click', '已读章节')
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
const p = String(this._getMyPageUi().readStatPath || '').trim()
|
||||
if (p && navigateMpPath(p)) return
|
||||
navigateMpPath('/pages/reading-records/reading-records?focus=all')
|
||||
},
|
||||
|
||||
/** 最近阅读区块标题点击:进入阅读记录(最近维度) */
|
||||
goToRecentReadHub() {
|
||||
trackClick('my', 'nav_click', '最近阅读区块')
|
||||
if (!this.data.isLoggedIn) {
|
||||
this.showLogin()
|
||||
return
|
||||
}
|
||||
const p = String(this._getMyPageUi().recentReadPath || '').trim()
|
||||
if (p && navigateMpPath(p)) return
|
||||
navigateMpPath('/pages/reading-records/reading-records?focus=recent')
|
||||
},
|
||||
|
||||
// 去目录(空状态等)
|
||||
goToChapters() {
|
||||
trackClick('my', 'nav_click', '去目录')
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
@@ -932,10 +979,22 @@ Page({
|
||||
goToVip() {
|
||||
trackClick('my', 'btn_click', '会员中心')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const p = String(this._getMyPageUi().vipPath || '').trim()
|
||||
if (p && navigateMpPath(p)) return
|
||||
wx.navigateTo({ url: '/pages/vip/vip' })
|
||||
},
|
||||
|
||||
// 进入个人资料编辑页(stitch_soul)
|
||||
// 本人对外名片:默认与「超级个体」同款 member-detail;mpUi.myPage.cardPath 可覆盖(需含完整 query)
|
||||
goToMySuperCard() {
|
||||
trackClick('my', 'btn_click', '名片')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
const uid = this.data.userInfo?.id
|
||||
if (!uid) return
|
||||
const p = String(this._getMyPageUi().cardPath || '').trim()
|
||||
if (p && navigateMpPath(p)) return
|
||||
wx.navigateTo({ url: `/pages/member-detail/member-detail?id=${encodeURIComponent(uid)}` })
|
||||
},
|
||||
|
||||
goToProfileEdit() {
|
||||
trackClick('my', 'nav_click', '资料编辑')
|
||||
if (!this.data.isLoggedIn) { this.showLogin(); return }
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
<!-- 我的页 - 设计稿 1:1 还原 -->
|
||||
<view class="page">
|
||||
<!-- 顶部导航:左侧资料编辑 + 标题 -->
|
||||
<!-- 顶部导航 -->
|
||||
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px;">
|
||||
<view class="nav-settings" bindtap="goToProfileEdit">
|
||||
<image class="nav-settings-icon" src="/assets/icons/edit-gray.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<text class="nav-title">我的</text>
|
||||
</view>
|
||||
<view class="nav-placeholder" style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
@@ -23,34 +20,28 @@
|
||||
<view class="profile-card" wx:else>
|
||||
<view class="profile-card-inner">
|
||||
<view class="profile-top-row">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-wrap" bindtap="tapAvatar">
|
||||
<view class="avatar-inner {{isVip ? 'avatar-vip' : ''}}">
|
||||
<image wx:if="{{userInfo.avatar}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<image wx:if="{{userInfo.avatar && userInfo.avatar.length > 5}}" class="avatar-img" src="{{userInfo.avatar}}" mode="aspectFill"/>
|
||||
<text wx:else class="avatar-text">{{userInfo.nickname ? userInfo.nickname[0] : '?'}}</text>
|
||||
</view>
|
||||
<view class="vip-badge" wx:if="{{isVip}}">VIP</view>
|
||||
<view class="vip-badge vip-badge-gray" wx:else>VIP</view>
|
||||
<button class="avatar-overlay-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar"></button>
|
||||
</view>
|
||||
<view class="profile-meta">
|
||||
<view class="profile-name-row">
|
||||
<text class="user-name" bindtap="editNickname">{{userInfo.nickname || '点击设置昵称'}}</text>
|
||||
<view class="profile-name-actions">
|
||||
<view class="become-member-btn {{isVip ? 'become-member-vip' : ''}}" wx:if="{{!auditMode}}" bindtap="goToVip">{{isVip ? '会员中心' : '成为会员'}}</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="vip-tags" wx:if="{{!auditMode}}">
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">会员</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToMatch">匹配</text>
|
||||
<text class="vip-tag {{isVip ? 'vip-tag-active' : ''}}" bindtap="goToVip">排行</text>
|
||||
<view class="profile-actions-row" wx:if="{{!auditMode}}">
|
||||
<view class="profile-action-btn" catchtap="goToMySuperCard">{{mpUiCardLabel}}</view>
|
||||
<view class="profile-action-btn" catchtap="goToVip">{{isVip ? mpUiVipLabelVip : mpUiVipLabelGuest}}</view>
|
||||
</view>
|
||||
<text class="user-wechat" wx:if="{{userWechat}}" bindtap="copyUserId">微信号: {{userWechat}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
<view class="profile-stat" bindtap="goToChapters">
|
||||
<view class="profile-stat" bindtap="goToReadStat">
|
||||
<text class="profile-stat-val">{{readCountText || '0'}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
<text class="profile-stat-label">{{mpUiReadStatLabel}}</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
@@ -117,43 +108,13 @@
|
||||
</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">
|
||||
<view class="card-header" bindtap="goToRecentReadHub">
|
||||
<image class="card-icon-img" src="/assets/icons/book-arrow-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">最近阅读</text>
|
||||
<text class="card-title">{{mpUiRecentTitle}}</text>
|
||||
</view>
|
||||
<view class="recent-list" wx:if="{{recentChapters.length > 0}}">
|
||||
<view
|
||||
|
||||
@@ -73,23 +73,49 @@
|
||||
}
|
||||
.vip-badge-gray { background: rgba(255,255,255,0.2); color: rgba(255,255,255,0.5); }
|
||||
.profile-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 12rpx; }
|
||||
.profile-name-row { display: flex; align-items: center; justify-content: space-between; gap: 16rpx; flex-wrap: wrap; }
|
||||
.profile-name-row { display: flex; align-items: center; justify-content: flex-start; gap: 16rpx; flex-wrap: wrap; }
|
||||
.user-name {
|
||||
font-size: 44rpx; font-weight: bold; color: #fff;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
|
||||
}
|
||||
.become-member-btn {
|
||||
padding: 12rpx 28rpx; border: 2rpx solid #C8A146; color: #C8A146;
|
||||
.profile-actions-row { display: flex; flex-wrap: wrap; align-items: center; gap: 12rpx; }
|
||||
/* 名片 / 会员中心:统一品牌青,与 tabBar 选中色一致 */
|
||||
.profile-action-btn {
|
||||
padding: 12rpx 28rpx; border: 2rpx solid #4FD1C5; color: #4FD1C5;
|
||||
font-size: 24rpx; font-weight: 500; border-radius: 40rpx; white-space: nowrap; flex-shrink: 0;
|
||||
}
|
||||
.become-member-vip { border-color: rgba(200,161,70,0.5); color: rgba(200,161,70,0.8); }
|
||||
.vip-tags { display: flex; gap: 12rpx; flex-shrink: 0; }
|
||||
.vip-tag {
|
||||
font-size: 20rpx; padding: 6rpx 12rpx; border-radius: 8rpx;
|
||||
border: 1rpx solid #374151; background: rgba(255,255,255,0.05); color: #9CA3AF;
|
||||
}
|
||||
.vip-tag-active { border-color: #C8A146; background: rgba(200,161,70,0.1); color: #C8A146; }
|
||||
.profile-action-btn:active { opacity: 0.75; }
|
||||
.user-wechat { font-size: 26rpx; color: #6B7280; }
|
||||
.super-card-entry {
|
||||
position: relative;
|
||||
margin-top: 24rpx;
|
||||
padding: 24rpx 56rpx 24rpx 28rpx;
|
||||
border-radius: 16rpx;
|
||||
background: rgba(79, 209, 197, 0.08);
|
||||
border: 1rpx solid rgba(79, 209, 197, 0.28);
|
||||
}
|
||||
.super-card-entry-txt {
|
||||
font-size: 28rpx;
|
||||
font-weight: 600;
|
||||
color: #4fd1c5;
|
||||
display: block;
|
||||
}
|
||||
.super-card-entry-sub {
|
||||
font-size: 22rpx;
|
||||
color: #9ca3af;
|
||||
margin-top: 8rpx;
|
||||
display: block;
|
||||
}
|
||||
.super-card-entry-arrow {
|
||||
position: absolute;
|
||||
right: 24rpx;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
font-size: 40rpx;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.profile-stats-row {
|
||||
display: flex; justify-content: space-around; margin-top: 32rpx;
|
||||
padding-top: 24rpx; border-top: 1rpx solid #374151;
|
||||
@@ -98,6 +124,15 @@
|
||||
.profile-stat-val { display: block; font-size: 36rpx; font-weight: bold; color: #4FD1C5; }
|
||||
.profile-stat-label { display: block; font-size: 22rpx; color: #6B7280; margin-top: 8rpx; }
|
||||
|
||||
.profile-edit-bar {
|
||||
display: flex; align-items: center; gap: 16rpx;
|
||||
margin-top: 24rpx; padding: 20rpx 24rpx;
|
||||
background: rgba(79,209,197,0.06); border-radius: 12rpx;
|
||||
}
|
||||
.profile-edit-icon { width: 32rpx; height: 32rpx; opacity: 0.6; flex-shrink: 0; }
|
||||
.profile-edit-text { flex: 1; font-size: 26rpx; color: #9ca3af; }
|
||||
.profile-edit-arrow { font-size: 36rpx; color: rgba(255,255,255,0.3); font-weight: 300; }
|
||||
|
||||
/* ===== 主内容区 ===== */
|
||||
.main-content { padding: 0 0 0 0; }
|
||||
|
||||
@@ -163,19 +198,6 @@
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.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; }
|
||||
@@ -183,25 +205,6 @@
|
||||
.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 {
|
||||
|
||||
@@ -13,6 +13,9 @@ const { toAvatarPath } = require('../../utils/util.js')
|
||||
|
||||
const MBTI_OPTIONS = ['INTJ', 'INFP', 'INTP', 'ENTP', 'ENFP', 'ENTJ', 'ENFJ', 'INFJ', 'ISTJ', 'ISFJ', 'ESTJ', 'ESFJ', 'ISTP', 'ISFP', 'ESTP', 'ESFP']
|
||||
|
||||
/** 首次分步完善完成后写入;与手机号+昵称齐全时自动写入,老用户免向导 */
|
||||
const PROFILE_WIZARD_DONE_KEY = 'profile_wizard_v1_done'
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
@@ -41,9 +44,17 @@ Page({
|
||||
loading: true,
|
||||
showPrivacyModal: false,
|
||||
nicknameInputFocus: false,
|
||||
/** 首次完善:分 3 步;full=1 或未达标资料时为单页 */
|
||||
wizardMode: false,
|
||||
wizardStep: 1,
|
||||
totalWizardSteps: 3,
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
this._wizardRouteOpts = {
|
||||
full: options?.full === '1',
|
||||
wizardOff: options?.wizard === '0',
|
||||
}
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
fromVip: options?.from === 'vip',
|
||||
@@ -98,6 +109,7 @@ Page({
|
||||
projectIntro: v('projectIntro'),
|
||||
loading: false,
|
||||
})
|
||||
this._applyWizardModeFromProfile(d)
|
||||
setTimeout(() => this.generateShareCard(), 200)
|
||||
} else {
|
||||
this.setData({ loading: false })
|
||||
@@ -109,6 +121,49 @@ Page({
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
/**
|
||||
* 是否走三步向导:资料未「手机号+昵称」齐全且未标记完成,且非 full=1、非 VIP 开通页强制单页。
|
||||
* 老用户已齐全则自动写 DONE,避免重复向导。
|
||||
*/
|
||||
_applyWizardModeFromProfile(d) {
|
||||
const ro = this._wizardRouteOpts || {}
|
||||
const forceFull = ro.full === true
|
||||
const forceNoWizard = ro.wizardOff === true
|
||||
const phoneNum = String(d.phone || '').replace(/\D/g, '')
|
||||
const phoneOk = /^1[3-9]\d{9}$/.test(phoneNum)
|
||||
const nickOk = !!(d.nickname && String(d.nickname).trim())
|
||||
if (phoneOk && nickOk && !wx.getStorageSync(PROFILE_WIZARD_DONE_KEY)) {
|
||||
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
|
||||
}
|
||||
const wizardMode = !forceFull && !forceNoWizard && !wx.getStorageSync(PROFILE_WIZARD_DONE_KEY)
|
||||
this.setData({ wizardMode, wizardStep: 1 })
|
||||
},
|
||||
|
||||
onWizardPrev() {
|
||||
if (this.data.wizardStep > 1) {
|
||||
this.setData({ wizardStep: this.data.wizardStep - 1 })
|
||||
}
|
||||
},
|
||||
|
||||
onWizardNext() {
|
||||
if (this.data.saving) return
|
||||
const { wizardMode, wizardStep } = this.data
|
||||
if (!wizardMode) return
|
||||
if (wizardStep === 1) {
|
||||
if (!(this.data.nickname || '').trim()) {
|
||||
wx.showToast({ title: '请填写昵称', icon: 'none' })
|
||||
return
|
||||
}
|
||||
this.setData({ wizardStep: 2 })
|
||||
return
|
||||
}
|
||||
if (wizardStep === 2) {
|
||||
this.setData({ wizardStep: 3 })
|
||||
return
|
||||
}
|
||||
this._doSaveProfile({ wizardComplete: true })
|
||||
},
|
||||
|
||||
onNicknameAreaTouch() {
|
||||
if (typeof wx.requirePrivacyAuthorize !== 'function') return
|
||||
wx.requirePrivacyAuthorize({
|
||||
@@ -365,7 +420,11 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
async saveProfile() {
|
||||
saveProfile() {
|
||||
this._doSaveProfile({ wizardComplete: false })
|
||||
},
|
||||
|
||||
async _doSaveProfile({ wizardComplete }) {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) {
|
||||
wx.showToast({ title: '请先登录', icon: 'none' })
|
||||
@@ -373,7 +432,6 @@ Page({
|
||||
}
|
||||
const s = (v) => (v || '').toString().trim()
|
||||
const isVip = this.data.isVip
|
||||
// 手机号必填,格式校验(支持带空格/连字符输入)
|
||||
const phoneRaw = s(this.data.phone)
|
||||
if (!phoneRaw) {
|
||||
wx.showToast({ title: '请输入手机号', icon: 'none' })
|
||||
@@ -428,8 +486,17 @@ Page({
|
||||
if (app.globalData.userInfo) {
|
||||
if (payload.nickname) app.globalData.userInfo.nickname = payload.nickname
|
||||
if (res?.data?.avatar) app.globalData.userInfo.avatar = res.data.avatar
|
||||
if (payload.phone) app.globalData.userInfo.phone = payload.phone
|
||||
wx.setStorageSync('userInfo', app.globalData.userInfo)
|
||||
}
|
||||
if (wizardComplete) {
|
||||
wx.setStorageSync(PROFILE_WIZARD_DONE_KEY, '1')
|
||||
this.setData({ wizardMode: false, saving: false })
|
||||
setTimeout(() => {
|
||||
wx.redirectTo({ url: `/pages/member-detail/member-detail?id=${userId}` })
|
||||
}, 400)
|
||||
return
|
||||
}
|
||||
setTimeout(() => getApp().goBackOrToHome(), 800)
|
||||
} catch (e) {
|
||||
wx.showToast({ title: e.message || '保存失败', icon: 'none' })
|
||||
|
||||
@@ -1,158 +1,311 @@
|
||||
<!-- 资料编辑 - comprehensive_profile_editor_v1_1 | input/textarea 用 view 包裹,配色 enhanced -->
|
||||
<!-- 资料编辑:单页 full=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="back-icon"></icon></view>
|
||||
<text class="nav-title">编辑资料</text>
|
||||
<text class="nav-title">{{wizardMode ? '分步档案(' + wizardStep + '/3)' : '编辑资料'}}</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="loading" wx:if="{{loading}}">加载中...</view>
|
||||
<scroll-view wx:else class="scroll-main" scroll-y>
|
||||
<!-- 温馨提示:from=vip 时强化权益说明 -->
|
||||
<view class="tip-card {{fromVip ? 'tip-card-highlight' : ''}}">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">{{fromVip ? '恭喜成为VIP!完善资料后即可使用找伙伴、提现等功能,手机号必填' : '温馨提示:手机号必填,微信号建议填写,以便使用提现和找伙伴功能'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 头像:点击直接弹出微信原生选择器(用微信头像/从相册选择/拍照) -->
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- 基本信息 -->
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称,或手动输入</text>
|
||||
<!-- —— 三步向导(首次) —— -->
|
||||
<block wx:if="{{wizardMode}}">
|
||||
<view class="wizard-bar">
|
||||
<view class="wizard-step {{wizardStep >= 1 ? 'wizard-step-on' : ''}}">1</view>
|
||||
<view class="wizard-line {{wizardStep >= 2 ? 'wizard-line-on' : ''}}"></view>
|
||||
<view class="wizard-step {{wizardStep >= 2 ? 'wizard-step-on' : ''}}">2</view>
|
||||
<view class="wizard-line {{wizardStep >= 3 ? 'wizard-line-on' : ''}}"></view>
|
||||
<view class="wizard-step {{wizardStep >= 3 ? 'wizard-step-on' : ''}}">3</view>
|
||||
</view>
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
|
||||
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
|
||||
</picker>
|
||||
<text class="wizard-sub">分步完善,保存后将进入「我的超级个体名片」,可转发分享</text>
|
||||
|
||||
<block wx:if="{{wizardStep === 1}}">
|
||||
<view class="tip-card tip-card-wizard">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">第 1 步:先设置对外展示的头像与昵称</text>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap form-input-suffix">
|
||||
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
|
||||
<icon name="map-pin" size="32" color="#8e8e93" customClass="form-suffix"></icon>
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="section section-wizard">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">可用微信昵称或手动输入</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">行业</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/电商" value="{{industry}}" bindinput="onIndustryInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">业务体量</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:年GMV 5000万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">职位</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人/联合创始人" value="{{position}}" bindinput="onPositionInput"/></view>
|
||||
</view>
|
||||
<view class="form-row" wx:if="{{isVip}}">
|
||||
<text class="form-label">我擅长</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频制作、IP打造、私域运营" value="{{skills}}" bindinput="onSkillsInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 核心联系方式 -->
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<icon name="phone" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号(必填)" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<block wx:if="{{wizardStep === 2}}">
|
||||
<view class="tip-card tip-card-wizard">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">第 2 步:补充职业画像(可后补,尽量填写便于匹配)</text>
|
||||
</view>
|
||||
<view class="section section-wizard">
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
|
||||
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap form-input-suffix">
|
||||
<input class="form-input-inner" placeholder="例如:福建厦门" value="{{region}}" bindinput="onRegionInput"/>
|
||||
<icon name="map-pin" size="32" color="#8e8e93" customClass="form-suffix"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">行业</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/企业服务" value="{{industry}}" bindinput="onIndustryInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">业务体量</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:年 GMV 5000 万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">职位</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人 / 业务负责人" value="{{position}}" bindinput="onPositionInput"/></view>
|
||||
</view>
|
||||
<view class="form-row" wx:if="{{isVip}}">
|
||||
<text class="form-label">我擅长</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:私域运营、投融资对接" value="{{skills}}" bindinput="onSkillsInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 个人故事(仅 VIP 展示) -->
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="lightbulb" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>个人故事</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">你最赚钱的一个月做的是什么</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:2021年主导电商大促,单月GMV突破500W..." value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">最有成就感的一件事</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:帮助3个素人打造个人IP,每月稳定变现5万+" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">人生的转折点</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:辞去大厂工作开始做自媒体..." value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<block wx:if="{{wizardStep === 3}}">
|
||||
<view class="tip-card tip-card-wizard">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">第 3 步:手机号必填;微信号建议填写,便于链接与提现</text>
|
||||
</view>
|
||||
<view class="section section-wizard">
|
||||
<view class="section-title">
|
||||
<icon name="phone" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="11 位手机号" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="选填" value="{{wechatId}}" bindinput="onWechatInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="lightbulb" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>个人故事</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">你最赚钱的一个月</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="选填" value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">最有成就感的一件事</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="选填" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">人生的转折点</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="选填" value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
|
||||
<view class="section-title">
|
||||
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>互助需求</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我能帮助大家什么</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="选填" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我需要什么帮助</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="选填" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>项目介绍</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="选填" value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 互助需求(VIP 或 资源对接已填写时展示) -->
|
||||
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
|
||||
<view class="section-title">
|
||||
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>互助需求</text>
|
||||
<view class="wizard-actions">
|
||||
<view class="wizard-btn-secondary" wx:if="{{wizardStep > 1}}" bindtap="onWizardPrev">上一步</view>
|
||||
<view class="wizard-btn-primary {{wizardStep === 1 ? 'wizard-btn-full' : ''}}" bindtap="onWizardNext">
|
||||
{{wizardStep === 3 ? (saving ? '保存中...' : '保存并查看名片') : '下一步'}}
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我能帮助大家什么</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频脚本、账号冷启动、私域转化" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我需要什么帮助</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:寻找供应链资源、线下活动合作" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
</block>
|
||||
|
||||
<!-- 项目介绍(仅 VIP 展示) -->
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>项目介绍</text>
|
||||
<!-- —— 单页完整编辑(已完善过或 full=1) —— -->
|
||||
<block wx:else>
|
||||
<view class="tip-card {{fromVip ? 'tip-card-highlight' : ''}}">
|
||||
<icon name="info" size="36" color="#00CED1" customClass="tip-icon"></icon>
|
||||
<text class="tip-text">{{fromVip ? '恭喜成为 VIP。补全资料后找伙伴、提现与群对接更顺畅;手机号为必填' : '手机号为必填;建议填写微信号,便于提现核对与对方联系。'}}</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="详细介绍您的项目,让潜在伙伴更好地了解您..." value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
<view class="avatar-section">
|
||||
<button class="avatar-wrap-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
|
||||
<view class="avatar-wrap">
|
||||
<view class="avatar-inner">
|
||||
<image wx:if="{{avatar}}" class="avatar-img" src="{{avatar}}" mode="aspectFill"/>
|
||||
<view wx:else class="avatar-placeholder">{{nickname ? nickname[0] : '?'}}</view>
|
||||
</view>
|
||||
<view class="avatar-camera"><icon name="camera" size="48" color="#ffffff"></icon></view>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="form-row">
|
||||
<text class="form-label">昵称</text>
|
||||
<view class="form-input-wrap" catchtouchstart="onNicknameAreaTouch">
|
||||
<input
|
||||
class="form-input-inner"
|
||||
type="nickname"
|
||||
placeholder="请输入昵称"
|
||||
value="{{nickname}}"
|
||||
focus="{{nicknameInputFocus}}"
|
||||
bindinput="onNicknameInput"
|
||||
bindchange="onNicknameChange"
|
||||
bindblur="onNicknameBlur"
|
||||
maxlength="20"
|
||||
/>
|
||||
</view>
|
||||
<text class="input-tip">微信用户可点击自动填充昵称,或手动输入</text>
|
||||
</view>
|
||||
<view class="form-row form-row-2">
|
||||
<view class="form-item">
|
||||
<text class="form-label">MBTI</text>
|
||||
<picker mode="selector" range="{{mbtiOptions}}" value="{{mbtiIndex}}" bindchange="onMbtiPickerChange">
|
||||
<view class="form-input-wrap form-picker">{{mbti || '请选择'}}</view>
|
||||
</picker>
|
||||
</view>
|
||||
<view class="form-item">
|
||||
<text class="form-label">地区</text>
|
||||
<view class="form-input-wrap form-input-suffix">
|
||||
<input class="form-input-inner" placeholder="例如:杭州·余杭区" value="{{region}}" bindinput="onRegionInput"/>
|
||||
<icon name="map-pin" size="32" color="#8e8e93" customClass="form-suffix"></icon>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">行业</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:新媒体/电商" value="{{industry}}" bindinput="onIndustryInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">业务体量</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:年GMV 5000万+" value="{{businessScale}}" bindinput="onBusinessScaleInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">职位</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:创始人/联合创始人" value="{{position}}" bindinput="onPositionInput"/></view>
|
||||
</view>
|
||||
<view class="form-row" wx:if="{{isVip}}">
|
||||
<text class="form-label">我擅长</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频制作、IP打造、私域运营" value="{{skills}}" bindinput="onSkillsInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section">
|
||||
<view class="section-title">
|
||||
<icon name="phone" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>核心联系方式</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">手机号<text class="required-mark">*</text></text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" type="tel" placeholder="请输入手机号(必填)" value="{{phone}}" bindinput="onPhoneInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">微信号</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="请输入微信号" value="{{wechatId}}" bindinput="onWechatInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="lightbulb" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>个人故事</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">你最赚钱的一个月做的是什么</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:2021年主导电商大促,单月GMV突破500W..." value="{{storyBestMonth}}" bindinput="onStoryBestMonthInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">最有成就感的一件事</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:帮助3个素人打造个人IP,每月稳定变现5万+" value="{{storyAchievement}}" bindinput="onStoryAchievementInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">人生的转折点</text>
|
||||
<view class="form-textarea-wrap"><textarea class="form-textarea-inner" placeholder="例如:辞去大厂工作开始做自媒体..." value="{{storyTurning}}" bindinput="onStoryTurningInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{isVip || helpOffer || helpNeed}}">
|
||||
<view class="section-title">
|
||||
<icon name="handshake" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>互助需求</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我能帮助大家什么</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:短视频脚本、账号冷启动、私域转化" value="{{helpOffer}}" bindinput="onHelpOfferInput"/></view>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<text class="form-label">我需要什么帮助</text>
|
||||
<view class="form-input-wrap"><input class="form-input-inner" placeholder="例如:寻找供应链资源、线下活动合作" value="{{helpNeed}}" bindinput="onHelpNeedInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{isVip}}">
|
||||
<view class="section-title">
|
||||
<icon name="rocket" size="40" color="#00CED1" customClass="section-icon"></icon>
|
||||
<text>项目介绍</text>
|
||||
</view>
|
||||
<view class="form-row">
|
||||
<view class="form-textarea-wrap form-textarea-lg"><textarea class="form-textarea-inner" placeholder="详细介绍您的项目,让潜在伙伴更好地了解您..." value="{{projectIntro}}" bindinput="onProjectIntroInput"/></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="save-btn" bindtap="saveProfile" disabled="{{saving}}">
|
||||
{{saving ? '保存中...' : '保存'}}
|
||||
</view>
|
||||
<view class="bottom-space"></view>
|
||||
</block>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 分享名片 canvas(隐藏,用于生成分享图 5:4) -->
|
||||
<canvas canvas-id="shareCardCanvas" class="share-card-canvas" style="width: 500px; height: 400px;"></canvas>
|
||||
|
||||
<!-- 隐私授权弹窗(昵称需授权后方可唤起微信昵称选择器) -->
|
||||
<view class="privacy-mask" wx:if="{{showPrivacyModal}}" catchtouchmove="preventMove">
|
||||
<view class="privacy-modal">
|
||||
<text class="privacy-title">温馨提示</text>
|
||||
|
||||
@@ -333,3 +333,90 @@
|
||||
.privacy-btn { width: 100%; height: 88rpx; line-height: 88rpx; background: #5EEAD4; color: #050B14; font-size: 30rpx; font-weight: 600; border-radius: 44rpx; border: none; }
|
||||
.privacy-btn::after { border: none; }
|
||||
.privacy-cancel { text-align: center; margin-top: 24rpx; font-size: 28rpx; color: #94A3B8; }
|
||||
|
||||
/* —— 三步向导 —— */
|
||||
.wizard-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
.wizard-step {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
background: rgba(148, 163, 184, 0.15);
|
||||
border: 2rpx solid rgba(148, 163, 184, 0.25);
|
||||
}
|
||||
.wizard-step-on {
|
||||
color: #042f2e;
|
||||
background: linear-gradient(135deg, #5eead4, #2dd4bf);
|
||||
border-color: transparent;
|
||||
}
|
||||
.wizard-line {
|
||||
width: 48rpx;
|
||||
height: 4rpx;
|
||||
border-radius: 4rpx;
|
||||
background: rgba(148, 163, 184, 0.2);
|
||||
}
|
||||
.wizard-line-on {
|
||||
background: linear-gradient(90deg, #5eead4, #2dd4bf);
|
||||
}
|
||||
.wizard-sub {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: #94a3b8;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
margin-bottom: 32rpx;
|
||||
padding: 0 12rpx;
|
||||
}
|
||||
.tip-card-wizard {
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.section-wizard {
|
||||
border-top: none;
|
||||
padding-top: 0;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
.wizard-actions {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 24rpx;
|
||||
margin-top: 24rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.wizard-btn-secondary {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
border: 2rpx solid rgba(148, 163, 184, 0.35);
|
||||
background: transparent;
|
||||
}
|
||||
.wizard-btn-primary {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 28rpx 24rpx;
|
||||
border-radius: 48rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #042f2e;
|
||||
background: linear-gradient(135deg, #5eead4, #2dd4bf);
|
||||
box-shadow: 0 8rpx 24rpx rgba(45, 212, 191, 0.25);
|
||||
}
|
||||
.wizard-btn-full {
|
||||
flex: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* 从「我的」页编辑图标进入;展示基本信息、个人故事、互助需求、项目介绍
|
||||
*/
|
||||
const app = getApp()
|
||||
const { isSafeImageSrc } = require('../../utils/imageUrl.js')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -36,9 +37,11 @@ Page({
|
||||
const e = (v) => (v == null || v === undefined ? '' : (String(v).trim() === '' || String(v).trim() === '未填写' ? '' : String(v).trim()))
|
||||
const phone = d.phone || ''
|
||||
const wechat = d.wechatId || wx.getStorageSync('user_wechat') || ''
|
||||
const av = d.avatar
|
||||
this.setData({
|
||||
profile: {
|
||||
...d,
|
||||
avatar: isSafeImageSrc(av) ? String(av).trim() : '',
|
||||
industry: e(d.industry),
|
||||
position: e(d.position),
|
||||
businessScale: e(d.businessScale || d.business_scale),
|
||||
|
||||
@@ -1,13 +1,92 @@
|
||||
/**
|
||||
* Soul创业实验 - 订单页
|
||||
* Soul创业实验 - 订单页(已支付消费流水:章节/VIP/余额/代付等)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
|
||||
const PAID_STATUSES = new Set(['paid', 'completed', 'success'])
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function formatShortDate(ms) {
|
||||
if (!ms) return '--'
|
||||
const d = new Date(ms)
|
||||
const m = (d.getMonth() + 1).toString().padStart(2, '0')
|
||||
const day = d.getDate().toString().padStart(2, '0')
|
||||
return `${m}-${day}`
|
||||
}
|
||||
|
||||
function midForSection(sectionId, bookFlat) {
|
||||
const row = bookFlat.find((s) => s.id === sectionId)
|
||||
return row?.mid ?? row?.MID ?? 0
|
||||
}
|
||||
|
||||
function classifyNav(productType, productId, mid) {
|
||||
const pt = String(productType || '').toLowerCase()
|
||||
if (pt === 'section' && productId) {
|
||||
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
|
||||
}
|
||||
if (pt === 'fullbook') {
|
||||
return { kind: 'switchTab', path: '/pages/chapters/chapters', label: '去目录' }
|
||||
}
|
||||
if (pt === 'vip') {
|
||||
return { kind: 'page', path: '/pages/vip/vip', label: '会员中心' }
|
||||
}
|
||||
if (pt === 'match') {
|
||||
return { kind: 'switchTab', path: '/pages/match/match', label: '找伙伴' }
|
||||
}
|
||||
if (pt === 'balance_recharge') {
|
||||
return { kind: 'page', path: '/pages/wallet/wallet', label: '余额' }
|
||||
}
|
||||
if (pt === 'gift_pay' || pt === 'gift_pay_batch') {
|
||||
return { kind: 'page', path: '/pages/gift-pay/list', label: '代付记录' }
|
||||
}
|
||||
if (productId && (/^\d+\.\d+/.test(productId) || productId.length > 0)) {
|
||||
return { kind: 'read', id: productId, mid: mid || 0, label: '阅读' }
|
||||
}
|
||||
return { kind: 'none', label: '--' }
|
||||
}
|
||||
|
||||
function mapApiOrderToRow(item, bookFlat) {
|
||||
const status = String(item.status || '').toLowerCase()
|
||||
if (!PAID_STATUSES.has(status)) return null
|
||||
|
||||
const pt = String(item.product_type || '').toLowerCase()
|
||||
const productId = String(item.product_id || item.section_id || '').trim()
|
||||
let mid = Number(item.section_mid ?? item.mid ?? item.MID ?? 0) || 0
|
||||
if (pt === 'section' && productId && !mid) mid = midForSection(productId, bookFlat)
|
||||
|
||||
const titleRaw = cleanSingleLineField(item.product_name || '')
|
||||
const title =
|
||||
titleRaw ||
|
||||
(pt === 'balance_recharge' ? '余额充值' : productId ? `订单 ${productId}` : '消费记录')
|
||||
|
||||
const amt = Number(item.amount)
|
||||
const amountStr = Number.isFinite(amt) ? amt.toFixed(2) : '--'
|
||||
const t = parseOrderTimeMs(item)
|
||||
const nav = classifyNav(pt, productId, mid)
|
||||
|
||||
return {
|
||||
rowKey: String(item.order_sn || item.id || `o_${t}`),
|
||||
title,
|
||||
subLine: `¥${amountStr} · ${formatShortDate(t)}`,
|
||||
actionLabel: nav.label,
|
||||
nav,
|
||||
_sortMs: t
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
orders: [],
|
||||
loading: true
|
||||
loading: true,
|
||||
allRows: [],
|
||||
displayRows: [],
|
||||
historyExpanded: false
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -16,63 +95,95 @@ Page({
|
||||
this.loadOrders()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (!this._purchasesFirstOnShowSkipped) {
|
||||
this._purchasesFirstOnShowSkipped = true
|
||||
return
|
||||
}
|
||||
if (app.globalData.isLoggedIn) this.loadOrders()
|
||||
},
|
||||
|
||||
applyDisplay(expanded) {
|
||||
const all = this.data.allRows || []
|
||||
const display = expanded || all.length <= 5 ? all : all.slice(0, 5)
|
||||
this.setData({ displayRows: display, historyExpanded: !!expanded })
|
||||
},
|
||||
|
||||
expandHistory() {
|
||||
if (this.data.historyExpanded) return
|
||||
this.applyDisplay(true)
|
||||
},
|
||||
|
||||
async loadOrders() {
|
||||
this.setData({ loading: true })
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
const userId = app.globalData.userInfo?.id
|
||||
|
||||
try {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (userId) {
|
||||
const res = await app.request(`/api/miniprogram/orders?userId=${userId}`)
|
||||
if (res && res.success && res.data) {
|
||||
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() : '--',
|
||||
_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 })
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
const rows = res.data
|
||||
.map((item) => mapApiOrderToRow(item, bookFlat))
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => b._sortMs - a._sortMs)
|
||||
.map(({ _sortMs, ...rest }) => rest)
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
this.applyDisplay(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
const purchasedSections = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const orders = purchasedSections.map((id, index) => ({
|
||||
id: `order_${index}`,
|
||||
sectionId: id,
|
||||
sectionMid: 0,
|
||||
title: `章节 ${id}`,
|
||||
amount: 1,
|
||||
status: 'completed',
|
||||
createTime: new Date(Date.now() - index * 86400000).toLocaleDateString()
|
||||
}))
|
||||
this.setData({ orders })
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const rows = ids.map((id, index) => {
|
||||
const mid = midForSection(id, bookFlat)
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
const title =
|
||||
cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${id}`
|
||||
const t = Date.now() - index * 86400000
|
||||
return {
|
||||
rowKey: `p_${id}_${index}`,
|
||||
title,
|
||||
subLine: `已解锁 · ${formatShortDate(t)}`,
|
||||
actionLabel: '阅读',
|
||||
nav: { kind: 'read', id, mid, label: '阅读' }
|
||||
}
|
||||
})
|
||||
this.setData({ allRows: rows, loading: false })
|
||||
this.applyDisplay(false)
|
||||
} catch (e) {
|
||||
console.error('加载订单失败:', e)
|
||||
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',
|
||||
createTime: new Date(Date.now() - i * 86400000).toLocaleDateString()
|
||||
}))
|
||||
})
|
||||
} finally {
|
||||
this.setData({ loading: false })
|
||||
this.setData({ allRows: [], loading: false })
|
||||
this.applyDisplay(false)
|
||||
}
|
||||
},
|
||||
|
||||
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}` })
|
||||
onOrderRowTap(e) {
|
||||
const index = e.currentTarget.dataset.index
|
||||
const row = (this.data.displayRows || [])[index]
|
||||
if (!row || !row.nav) return
|
||||
const { nav } = row
|
||||
if (nav.kind === 'read' && nav.id) {
|
||||
const q = nav.mid ? `mid=${nav.mid}` : `id=${nav.id}`
|
||||
wx.navigateTo({ url: `/pages/read/read?${q}` })
|
||||
return
|
||||
}
|
||||
if (nav.kind === 'page' && nav.path) {
|
||||
wx.navigateTo({ url: nav.path })
|
||||
return
|
||||
}
|
||||
if (nav.kind === 'switchTab' && nav.path) {
|
||||
wx.switchTab({ url: nav.path })
|
||||
}
|
||||
},
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
|
||||
@@ -14,20 +14,37 @@
|
||||
<view class="skeleton"></view>
|
||||
</view>
|
||||
|
||||
<view class="orders-list" wx:elif="{{orders.length > 0}}">
|
||||
<view class="order-item" wx:for="{{orders}}" wx:key="id" bindtap="goToRead" data-id="{{item.sectionId}}" data-mid="{{item.sectionMid}}">
|
||||
<view class="order-info">
|
||||
<view class="order-title-row">
|
||||
<text class="order-unlock-icon">🔓</text>
|
||||
<text class="order-title">{{item.title}}</text>
|
||||
<view class="order-history-card" wx:elif="{{displayRows.length > 0}}">
|
||||
<view class="order-history-head">
|
||||
<image class="order-history-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="oh-list">
|
||||
<view
|
||||
class="oh-row"
|
||||
wx:for="{{displayRows}}"
|
||||
wx:key="rowKey"
|
||||
bindtap="onOrderRowTap"
|
||||
data-index="{{index}}"
|
||||
>
|
||||
<view class="oh-left">
|
||||
<text class="oh-index">{{index + 1}}</text>
|
||||
<view class="oh-text-wrap">
|
||||
<text class="oh-title">{{item.title}}</text>
|
||||
<text class="oh-sub" wx:if="{{item.subLine}}">{{item.subLine}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<text class="order-time">{{item.createTime}}</text>
|
||||
</view>
|
||||
<view class="order-right">
|
||||
<text class="order-amount">¥{{item.amount}}</text>
|
||||
<text class="order-status">已完成</text>
|
||||
<text class="oh-link">{{item.actionLabel}}</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="oh-expand"
|
||||
wx:if="{{allRows.length > 5 && !historyExpanded}}"
|
||||
bindtap="expandHistory"
|
||||
hover-class="oh-expand-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="oh-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="empty" wx:else>
|
||||
|
||||
@@ -7,17 +7,29 @@
|
||||
.loading { display: flex; flex-direction: column; gap: 24rpx; }
|
||||
.skeleton { height: 120rpx; background: linear-gradient(90deg, #1c1c1e 25%, #2c2c2e 50%, #1c1c1e 75%); background-size: 200% 100%; animation: skeleton 1.5s ease-in-out infinite; border-radius: 24rpx; }
|
||||
@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-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; }
|
||||
.order-status { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.order-history-card { background: #1c1c1e; border-radius: 24rpx; padding: 28rpx 24rpx 16rpx; }
|
||||
.order-history-head { padding: 0 8rpx 16rpx 8rpx; }
|
||||
.order-history-icon { width: 40rpx; height: 40rpx; opacity: 0.92; }
|
||||
.oh-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.oh-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.oh-row:active { opacity: 0.92; }
|
||||
.oh-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; flex: 1; }
|
||||
.oh-text-wrap { display: flex; flex-direction: column; gap: 6rpx; min-width: 0; flex: 1; }
|
||||
.oh-index { font-size: 28rpx; color: #6B7280; font-family: monospace; flex-shrink: 0; }
|
||||
.oh-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.oh-sub { font-size: 22rpx; color: rgba(255,255,255,0.4); }
|
||||
.oh-link { font-size: 24rpx; color: #00CED1; font-weight: 500; flex-shrink: 0; margin-left: 16rpx; }
|
||||
.oh-expand { display: flex; justify-content: center; align-items: center; padding: 8rpx 0 8rpx; margin-top: 8rpx; }
|
||||
.oh-expand-hover { opacity: 0.65; }
|
||||
.oh-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);
|
||||
}
|
||||
.empty { display: flex; flex-direction: column; align-items: center; padding: 96rpx; }
|
||||
.empty-icon { font-size: 96rpx; margin-bottom: 24rpx; opacity: 0.5; }
|
||||
.empty-text { font-size: 28rpx; color: rgba(255,255,255,0.4); }
|
||||
|
||||
@@ -18,6 +18,7 @@ const readingTracker = require('../../utils/readingTracker')
|
||||
const { parseScene } = require('../../utils/scene.js')
|
||||
const contentParser = require('../../utils/contentParser.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { checkAndExecute } = require('../../utils/ruleEngine')
|
||||
|
||||
const app = getApp()
|
||||
|
||||
@@ -120,10 +121,64 @@ Page({
|
||||
|
||||
// 好友从代付分享进入:待自动领取的 requestSn
|
||||
pendingGiftRequestSn: '',
|
||||
|
||||
// 朋友圈单页模式(scene 1154 / systemInfo.mode):无法登录与支付,仅引导「前往小程序」
|
||||
readSinglePageMode: false,
|
||||
// 朋友圈单页付费墙:说明默认收起,点「购买本章」后展开极简文案 + 底栏箭头(无长 Modal)
|
||||
momentsPaywallExpanded: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否处于朋友圈等「单页预览」环境。
|
||||
* 兼容:部分机型/基础库首帧 getSystemInfoSync().mode 未就绪,需结合 launch/enter scene 1154、getWindowInfo。
|
||||
* 命中时同步 app.globalData.isSinglePageMode,保证 ensureFullAppForAuth 与页内 wx:if 一致。
|
||||
*/
|
||||
_detectReadSinglePage() {
|
||||
try {
|
||||
const launch = typeof wx.getLaunchOptionsSync === 'function' ? wx.getLaunchOptionsSync() : null
|
||||
if (launch && Number(launch.scene) === 1154) {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const enter = typeof wx.getEnterOptionsSync === 'function' ? wx.getEnterOptionsSync() : null
|
||||
if (enter && Number(enter.scene) === 1154) {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const win = typeof wx.getWindowInfo === 'function' ? wx.getWindowInfo() : null
|
||||
if (win && win.mode === 'singlePage') {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
if (sys && sys.mode === 'singlePage') {
|
||||
app.globalData.isSinglePageMode = true
|
||||
}
|
||||
} catch (e) {}
|
||||
return !!app.globalData.isSinglePageMode
|
||||
},
|
||||
|
||||
/** 单页模式下点「购买本章」:触觉反馈 + 展开极简说明;引导靠页内文案 + 底栏箭头,不再弹长 Modal */
|
||||
onUnlockTapInSinglePage() {
|
||||
trackClick('read', 'btn_click', '单页_解锁引导')
|
||||
try {
|
||||
wx.vibrateShort({ type: 'light' })
|
||||
} catch (e) {}
|
||||
if (this._detectReadSinglePage() && typeof this.setData === 'function') {
|
||||
this.setData({ readSinglePageMode: true, momentsPaywallExpanded: true })
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ auditMode: app.globalData.auditMode || false })
|
||||
const sp = this._detectReadSinglePage()
|
||||
this.setData({
|
||||
auditMode: app.globalData.auditMode || false,
|
||||
readSinglePageMode: sp,
|
||||
...(sp ? {} : { momentsPaywallExpanded: false }),
|
||||
})
|
||||
},
|
||||
|
||||
async onLoad(options) {
|
||||
@@ -186,7 +241,9 @@ Page({
|
||||
sectionMid: mid || null,
|
||||
loading: true,
|
||||
accessState: 'unknown',
|
||||
pendingGiftRequestSn: giftRequestSn || ''
|
||||
pendingGiftRequestSn: giftRequestSn || '',
|
||||
readSinglePageMode: this._detectReadSinglePage(),
|
||||
momentsPaywallExpanded: false,
|
||||
})
|
||||
|
||||
if (ref) {
|
||||
@@ -234,9 +291,10 @@ Page({
|
||||
}
|
||||
}
|
||||
|
||||
// 【标准流程】4. 如果有权限,初始化阅读追踪
|
||||
if (canAccess) {
|
||||
readingTracker.init(id)
|
||||
} else {
|
||||
app.touchRecentSection(id)
|
||||
}
|
||||
|
||||
// 5. 导航:文章详情已带 prev/next
|
||||
@@ -375,6 +433,7 @@ Page({
|
||||
if (accessManager.canAccessFullContent(accessState)) {
|
||||
app.markSectionAsRead(id)
|
||||
}
|
||||
app.touchRecentSection(id)
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('[Read] 加载内容失败,尝试本地缓存:', e)
|
||||
@@ -393,6 +452,7 @@ Page({
|
||||
partTitle: cached.partTitle || '',
|
||||
chapterTitle: cached.chapterTitle || ''
|
||||
})
|
||||
app.touchRecentSection(id)
|
||||
console.log('[Read] 从本地缓存加载成功')
|
||||
return
|
||||
}
|
||||
@@ -698,8 +758,8 @@ Page({
|
||||
}
|
||||
if (!phone || !/^1[3-9]\d{9}$/.test(phone)) {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '请先填写手机号(必填),以便对方联系您',
|
||||
title: '补全手机号',
|
||||
content: '请填写手机号(必填),便于对方联系您。',
|
||||
confirmText: '去填写',
|
||||
cancelText: '取消',
|
||||
success: (res) => {
|
||||
@@ -784,11 +844,7 @@ Page({
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '朋友圈单页',
|
||||
content: '当前为朋友圈单页,无法发起代付支付。请点击底部「前往小程序」进入完整版后再操作。',
|
||||
showCancel: false
|
||||
})
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
} catch (e) {}
|
||||
@@ -932,17 +988,9 @@ Page({
|
||||
return { title, path }
|
||||
},
|
||||
|
||||
// 底部「分享到朋友圈」按钮点击:微信不支持 button open-type=shareTimeline,只能通过右上角菜单分享,点击时引导用户
|
||||
onShareTimelineTap() {
|
||||
wx.showToast({
|
||||
title: '请点击右上角「...」→ 分享到朋友圈',
|
||||
icon: 'none',
|
||||
duration: 2500
|
||||
})
|
||||
},
|
||||
|
||||
// 右下角悬浮按钮:分享到朋友圈(复制文案 + 引导点右上角)
|
||||
// 分享到朋友圈:复制摘要 + 引导用户用右上角「···」发圈(无 open-type=shareTimeline)
|
||||
shareToMoments() {
|
||||
trackClick('read', 'btn_click', '分享到朋友圈_' + (this.data.sectionId || ''))
|
||||
const title = this.data.section?.title || this.data.chapterTitle || '好文推荐'
|
||||
const raw = (this.data.content || '')
|
||||
.replace(/<[^>]+>/g, '\n')
|
||||
@@ -961,7 +1009,7 @@ Page({
|
||||
wx.hideToast()
|
||||
wx.showModal({
|
||||
title: '分享到朋友圈',
|
||||
content: '文案已复制。\n\n请点击右上角「···」菜单,选择「分享到朋友圈」即可发布。',
|
||||
content: '已复制发圈文案(非分享给好友)。\n\n请点击右上角「···」→「分享到朋友圈」粘贴发布。',
|
||||
showCancel: false,
|
||||
confirmText: '知道了'
|
||||
})
|
||||
@@ -988,21 +1036,10 @@ Page({
|
||||
|
||||
// 显示登录弹窗(每次打开协议未勾选,符合审核要求)
|
||||
showLoginModal() {
|
||||
// 朋友圈等单页模式下,不直接弹登录,用官方推荐的方式引导用户「前往小程序」
|
||||
try {
|
||||
const sys = wx.getSystemInfoSync()
|
||||
const isSinglePage = (sys && sys.mode === 'singlePage') || app.globalData.isSinglePageMode
|
||||
if (isSinglePage) {
|
||||
wx.showModal({
|
||||
title: '请前往完整小程序',
|
||||
content: '当前为朋友圈单页,仅支持部分浏览。想登录继续阅读,请点击底部「前往小程序」后再操作。',
|
||||
showCancel: false,
|
||||
confirmText: '我知道了',
|
||||
})
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[Read] 检测单页模式失败,回退为正常登录流程:', e)
|
||||
// 单页模式无法弹登录组件:页内已说明「前往小程序」,不再弹 Modal
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
try {
|
||||
this.setData({ showLoginModal: true })
|
||||
@@ -1086,6 +1123,10 @@ Page({
|
||||
|
||||
// 购买章节 - 直接调起支付
|
||||
async handlePurchaseSection() {
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
trackClick('read', 'btn_click', '购买章节_' + this.data.sectionId)
|
||||
console.log('[Pay] 点击购买章节按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
@@ -1105,6 +1146,10 @@ Page({
|
||||
|
||||
// 购买全书 - 直接调起支付
|
||||
async handlePurchaseFullBook() {
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
console.log('[Pay] 点击购买全书按钮')
|
||||
wx.showLoading({ title: '处理中...', mask: true })
|
||||
|
||||
@@ -1123,6 +1168,14 @@ Page({
|
||||
// 处理支付 - 调用真实微信支付接口
|
||||
async processPayment(type, sectionId, amount) {
|
||||
console.log('[Pay] processPayment开始:', { type, sectionId, amount })
|
||||
|
||||
if (this.data.readSinglePageMode || this._detectReadSinglePage()) {
|
||||
try {
|
||||
wx.hideLoading()
|
||||
} catch (e) {}
|
||||
this.onUnlockTapInSinglePage()
|
||||
return
|
||||
}
|
||||
|
||||
const userInfo = app.globalData.userInfo
|
||||
if (userInfo?.id) {
|
||||
@@ -1132,10 +1185,10 @@ Page({
|
||||
if (needProfile) {
|
||||
const res = await new Promise(resolve => {
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '购买前请先完善头像和昵称',
|
||||
confirmText: '去完善',
|
||||
cancelText: '稍后',
|
||||
title: '设置头像与昵称',
|
||||
content: '支付订单会关联你的对外展示信息,请先设置头像与昵称,避免账单与对方看到默认占位。',
|
||||
confirmText: '去设置',
|
||||
cancelText: '关闭',
|
||||
success: resolve
|
||||
})
|
||||
})
|
||||
@@ -1294,7 +1347,7 @@ Page({
|
||||
title: '支付通道维护中',
|
||||
content: '微信支付正在审核中,请添加客服微信(' + (app.globalData.serviceWechat || '28533368') + ')手动购买,感谢理解!',
|
||||
confirmText: '复制微信号',
|
||||
cancelText: '稍后再说',
|
||||
cancelText: '关闭',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.setClipboardData({
|
||||
@@ -1414,6 +1467,7 @@ Page({
|
||||
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '购买成功', icon: 'success' })
|
||||
checkAndExecute('after_pay', this)
|
||||
|
||||
} catch (e) {
|
||||
wx.hideLoading()
|
||||
@@ -1499,6 +1553,25 @@ Page({
|
||||
wx.navigateTo({ url: '/pages/referral/referral' })
|
||||
},
|
||||
|
||||
/** 海报 canvas 在弹层渲染后偶现取不到 node,多次重试 */
|
||||
async _queryPosterCanvasNode(maxTry = 10, delayMs = 100) {
|
||||
for (let i = 0; i < maxTry; i++) {
|
||||
const node = await new Promise((resolve) => {
|
||||
wx.createSelectorQuery()
|
||||
.in(this)
|
||||
.select('#posterCanvas')
|
||||
.fields({ node: true, size: true })
|
||||
.exec((res) => {
|
||||
if (res && res[0] && res[0].node) resolve(res[0])
|
||||
else resolve(null)
|
||||
})
|
||||
})
|
||||
if (node) return node
|
||||
await new Promise((r) => setTimeout(r, delayMs))
|
||||
}
|
||||
return null
|
||||
},
|
||||
|
||||
// 生成海报(Canvas 2D API)
|
||||
async generatePoster() {
|
||||
wx.showLoading({ title: '生成中...' })
|
||||
@@ -1509,6 +1582,7 @@ Page({
|
||||
if (typeof wx.nextTick === 'function') wx.nextTick(resolve)
|
||||
else setTimeout(resolve, 50)
|
||||
})
|
||||
await new Promise((r) => setTimeout(r, 120))
|
||||
|
||||
try {
|
||||
const { section, contentParagraphs, sectionId, sectionMid } = this.data
|
||||
@@ -1526,18 +1600,12 @@ Page({
|
||||
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 canvasNode = await this._queryPosterCanvasNode()
|
||||
if (!canvasNode) {
|
||||
throw new Error('canvas node not found')
|
||||
}
|
||||
|
||||
const canvas = canvasNode.node
|
||||
const ctx = canvas.getContext('2d')
|
||||
let dpr = 2
|
||||
try {
|
||||
if (typeof wx.getWindowInfo === 'function') {
|
||||
@@ -1548,73 +1616,100 @@ Page({
|
||||
} catch (_) {
|
||||
dpr = 2
|
||||
}
|
||||
const width = 300
|
||||
const height = 450
|
||||
canvas.width = width * dpr
|
||||
canvas.height = height * dpr
|
||||
// 布局尺寸:优先用节点测量;为 0 时回退 300×450(避免真机 query 过早得到 0 导致空白)
|
||||
const layoutW = (canvasNode.width && canvasNode.width > 1) ? Math.round(canvasNode.width) : 300
|
||||
const layoutH = (canvasNode.height && canvasNode.height > 1) ? Math.round(canvasNode.height) : 450
|
||||
canvas.width = Math.max(1, Math.floor(layoutW * dpr))
|
||||
canvas.height = Math.max(1, Math.floor(layoutH * dpr))
|
||||
const ctx = canvas.getContext('2d')
|
||||
if (!ctx) throw new Error('canvas 2d not supported')
|
||||
ctx.scale(dpr, dpr)
|
||||
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, height)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
const paintPoster = async () => {
|
||||
const w = layoutW
|
||||
const h = layoutH
|
||||
const grd = ctx.createLinearGradient(0, 0, 0, h)
|
||||
grd.addColorStop(0, '#1a1a2e')
|
||||
grd.addColorStop(1, '#16213e')
|
||||
ctx.fillStyle = grd
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, width, 4)
|
||||
ctx.fillStyle = '#00CED1'
|
||||
ctx.fillRect(0, 0, w, 4)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('📚 卡若创业派对', 20, 35)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '14px sans-serif'
|
||||
ctx.fillText('卡若创业派对', 20, 35)
|
||||
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText2d(ctx, title, width - 40)
|
||||
let y = 70
|
||||
titleLines.forEach(line => { ctx.fillText(line, 20, y); y += 26 })
|
||||
ctx.font = '18px sans-serif'
|
||||
ctx.fillStyle = '#ffffff'
|
||||
const title = section?.title || '精彩内容'
|
||||
const titleLines = this.wrapText2d(ctx, title, w - 40)
|
||||
let y = 70
|
||||
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.strokeStyle = 'rgba(255,255,255,0.1)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(20, y + 10)
|
||||
ctx.lineTo(w - 20, y + 10)
|
||||
ctx.stroke()
|
||||
|
||||
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.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.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, height - 60)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, height - 38)
|
||||
|
||||
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)
|
||||
ctx.font = '12px sans-serif'
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.8)'
|
||||
y += 30
|
||||
let paras = Array.isArray(contentParagraphs) ? contentParagraphs.filter(Boolean) : []
|
||||
if (!paras.length && this.data.content) {
|
||||
const plain = String(this.data.content)
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
if (plain) paras = [plain.slice(0, 400)]
|
||||
}
|
||||
const rawSummary = paras.slice(0, 3).join(' ').trim() || '卡若创业派对 · 真实商业故事'
|
||||
const summary = rawSummary.length > 160 ? rawSummary.slice(0, 157) + '...' : rawSummary
|
||||
const summaryLines = this.wrapText2d(ctx, summary, w - 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, h - 100, w, 100)
|
||||
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.font = '13px sans-serif'
|
||||
ctx.fillText('长按识别小程序码', 20, h - 60)
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)'
|
||||
ctx.font = '11px sans-serif'
|
||||
ctx.fillText('长按小程序码阅读全文', 20, h - 38)
|
||||
|
||||
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, w - 85, h - 85, 70, 70)
|
||||
} catch (_) {
|
||||
this.drawQRPlaceholder2d(ctx, w, h)
|
||||
}
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof canvas.requestAnimationFrame === 'function') {
|
||||
await new Promise((resolve, reject) => {
|
||||
canvas.requestAnimationFrame(() => {
|
||||
paintPoster().then(resolve).catch(reject)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.drawQRPlaceholder2d(ctx, width, height)
|
||||
await paintPoster()
|
||||
}
|
||||
|
||||
wx.hideLoading()
|
||||
|
||||
@@ -89,21 +89,21 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 分享操作区 -->
|
||||
<!-- 分享区:仅好友/海报/代付;完整小程序发圈见右下角悬浮钮 -->
|
||||
<view class="action-section">
|
||||
<view class="action-row-inline">
|
||||
<view class="action-btn-inline btn-share-inline" bindtap="onShareTimelineTap">
|
||||
<icon name="megaphone" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">分享到朋友圈</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-poster-inline" bindtap="generatePoster">
|
||||
<button plain class="action-share-native action-tile-unified" open-type="share" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="share" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">分享给好友</text>
|
||||
</button>
|
||||
<button plain class="action-share-native action-tile-unified" bindtap="generatePoster" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="image" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">生成海报</text>
|
||||
</view>
|
||||
<view class="action-btn-inline btn-gift-inline" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
</button>
|
||||
<button plain class="action-share-native action-tile-unified" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}" hover-class="action-share-native-hover" hover-stop-propagation>
|
||||
<icon name="gift" size="32" color="#00CED1" customClass="action-icon-small"></icon>
|
||||
<text class="action-text-small">代付分享</text>
|
||||
</view>
|
||||
</button>
|
||||
</view>
|
||||
<view class="share-tip-inline" wx:if="{{!auditMode}}">
|
||||
<text class="share-tip-text">分享后好友购买,你可获得 90% 收益</text>
|
||||
@@ -122,23 +122,36 @@
|
||||
<!-- 渐变遮罩 -->
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 未登录:显示购买按钮(朋友圈/分享场景) -->
|
||||
<view class="paywall">
|
||||
<!-- 付费墙 - 未登录:完整小程序登录+价;朋友圈单页与正文同款「购买本章 ¥1」,点后再展开极简说明 -->
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并购买后阅读全文</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>
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<text class="paywall-title">解锁全文</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥1</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-desc paywall-desc--moments-expanded" wx:if="{{momentsPaywallExpanded}}">预览不可付款,请点底部「前往小程序」。</text>
|
||||
</block>
|
||||
|
||||
<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>
|
||||
<block wx:else>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已预览部分内容,登录并支付 ¥1 后阅读全文</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>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -182,39 +195,47 @@
|
||||
<view class="fade-mask"></view>
|
||||
|
||||
<!-- 付费墙 - 已登录未购买 -->
|
||||
<view class="paywall">
|
||||
<view class="paywall {{readSinglePageMode ? 'paywall--single-preview' : ''}}">
|
||||
<view class="paywall-icon"><icon name="lock" size="80" color="#00CED1"></icon></view>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</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>
|
||||
|
||||
<!-- 解锁全书 - 只有购买超过3章才显示 -->
|
||||
<view class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice || 9.9}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
<block wx:if="{{readSinglePageMode}}">
|
||||
<text class="paywall-title">解锁全文</text>
|
||||
<view class="purchase-options" wx:if="{{!auditMode}}">
|
||||
<view class="purchase-btn purchase-section" bindtap="onUnlockTapInSinglePage">
|
||||
<text class="btn-label">购买本章</text>
|
||||
<text class="btn-price brand-color">¥1</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-desc paywall-desc--moments-expanded" wx:if="{{momentsPaywallExpanded}}">预览不可付款,请点底部「前往小程序」。</text>
|
||||
</block>
|
||||
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
<!-- 代付分享:帮好友购买(审核模式隐藏) -->
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="40" color="#00CED1" customClass="gift-share-icon"></icon>
|
||||
<text class="gift-share-text">代付分享</text>
|
||||
</view>
|
||||
<block wx:else>
|
||||
<text class="paywall-title">解锁完整内容</text>
|
||||
<text class="paywall-desc">已阅读50%,购买后继续阅读</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 class="purchase-btn purchase-fullbook" bindtap="handlePurchaseFullBook" wx:if="{{purchasedCount >= 3}}">
|
||||
<view class="btn-left">
|
||||
<icon name="sparkles" size="32" color="#FFD700" customClass="btn-sparkle"></icon>
|
||||
<text class="btn-label">解锁全部 {{totalSections}} 章</text>
|
||||
</view>
|
||||
<view class="btn-right">
|
||||
<text class="btn-price">¥{{fullBookPrice}}</text>
|
||||
<text class="btn-discount">省82%</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="paywall-audit-tip" wx:if="{{auditMode}}">审核中,暂不支持购买</view>
|
||||
<text class="paywall-tip" wx:if="{{!auditMode}}">分享给好友一起学习,还能赚取佣金</text>
|
||||
<view class="gift-share-row" bindtap="showGiftShareModal" wx:if="{{isLoggedIn && !auditMode}}">
|
||||
<icon name="gift" size="40" color="#00CED1" customClass="gift-share-icon"></icon>
|
||||
<text class="gift-share-text">代付分享</text>
|
||||
</view>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 章节导航 -->
|
||||
@@ -270,9 +291,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 海报生成弹窗 -->
|
||||
<view class="modal-overlay" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content poster-modal" catchtap="stopPropagation">
|
||||
<!-- 海报生成弹窗:居中 + z-index 高于右下角悬浮,避免「空白」错觉 -->
|
||||
<view class="modal-overlay modal-overlay-center" wx:if="{{showPosterModal}}" bindtap="closePosterModal">
|
||||
<view class="modal-content modal-content-center poster-modal" catchtap="stopPropagation">
|
||||
<view class="modal-header">
|
||||
<text class="modal-title">生成海报</text>
|
||||
<view class="modal-close" bindtap="closePosterModal"><icon name="x" size="36" color="#8e8e93"></icon></view>
|
||||
@@ -357,8 +378,12 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 右下角悬浮按钮 - 分享到朋友圈 -->
|
||||
<view class="fab-share" bindtap="shareToMoments">
|
||||
<icon name="share" size="44" color="#0f172a" customClass="fab-share-icon"></icon>
|
||||
<!-- 单页预览(朋友圈):指向底栏「前往小程序」,字少;完整小程序仍保留发圈悬浮钮 -->
|
||||
<view class="singlepage-launch-pointer" wx:if="{{readSinglePageMode && momentsPaywallExpanded}}" aria-hidden="true">
|
||||
<view class="singlepage-launch-pointer__arrow">↘</view>
|
||||
</view>
|
||||
|
||||
<view class="fab-share-moments" wx:if="{{!readSinglePageMode}}" bindtap="shareToMoments" hover-class="fab-share-moments-hover" aria-label="分享到朋友圈">
|
||||
<icon name="share" size="44" color="#ffffff" customClass="fab-share-moments-icon"></icon>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -280,6 +280,36 @@
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:与完整小程序同款购买行,留白略紧 */
|
||||
.paywall--single-preview {
|
||||
padding-top: 40rpx;
|
||||
padding-bottom: 40rpx;
|
||||
}
|
||||
.paywall--single-preview .paywall-icon {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.paywall--single-preview .paywall-title {
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.paywall-desc--moments-expanded {
|
||||
margin-top: 28rpx !important;
|
||||
margin-bottom: 0 !important;
|
||||
font-size: 26rpx !important;
|
||||
line-height: 1.45;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
|
||||
/* 朋友圈单页:未点解锁前的一行轻提示 */
|
||||
.paywall-hint-compact {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 255, 255, 0.48);
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin-bottom: 36rpx;
|
||||
line-height: 1.55;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
|
||||
.paywall-desc {
|
||||
font-size: 28rpx;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
@@ -360,6 +390,33 @@
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
|
||||
.paywall-singlepage-note {
|
||||
display: block;
|
||||
margin-top: 8rpx;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-align: center;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 朋友圈单页付费墙底部:与正文文末「分享赚收益」文案一致 */
|
||||
.paywall-share-earn-wrap {
|
||||
margin-top: 28rpx;
|
||||
padding-top: 24rpx;
|
||||
border-top: 1rpx solid rgba(255, 255, 255, 0.08);
|
||||
text-align: center;
|
||||
}
|
||||
.paywall-share-earn-wrap .share-tip-text {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
line-height: 1.5;
|
||||
}
|
||||
.paywall-share-earn-sub {
|
||||
margin-top: 12rpx !important;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.paywall-tip {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
@@ -470,7 +527,14 @@
|
||||
.action-row-inline {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
gap: 16rpx;
|
||||
align-items: stretch;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 底部三按钮:同一底纹与描边(好友 / 海报 / 代付) */
|
||||
.action-tile-unified {
|
||||
background: rgba(255, 255, 255, 0.06) !important;
|
||||
border: 2rpx solid rgba(0, 206, 209, 0.28) !important;
|
||||
}
|
||||
|
||||
.action-btn-inline {
|
||||
@@ -489,21 +553,38 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.action-btn-inline::after {
|
||||
/* 分享给好友:原生 button + open-type=share,样式与 action-btn-inline 对齐 */
|
||||
.action-share-native {
|
||||
flex: 1 1 0;
|
||||
min-width: 0;
|
||||
min-height: 96rpx;
|
||||
margin: 0;
|
||||
padding: 24rpx 12rpx;
|
||||
border-radius: 16rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
line-height: normal;
|
||||
font-size: inherit;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
.action-share-native::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.btn-share-inline {
|
||||
background: rgba(7, 193, 96, 0.15);
|
||||
border: 2rpx solid rgba(7, 193, 96, 0.3);
|
||||
button.action-share-native {
|
||||
color: inherit;
|
||||
}
|
||||
.action-share-native-hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.btn-poster-inline {
|
||||
background: rgba(255, 215, 0, 0.15);
|
||||
border: 2rpx solid rgba(255, 215, 0, 0.3);
|
||||
.action-btn-inline::after {
|
||||
border: none;
|
||||
}
|
||||
|
||||
|
||||
.action-icon-small {
|
||||
font-size: 28rpx;
|
||||
flex-shrink: 0;
|
||||
@@ -597,7 +678,8 @@
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
/* 高于右下角悬浮钮,避免弹层被盖住或 canvas 不可见 */
|
||||
z-index: 10050;
|
||||
}
|
||||
|
||||
.modal-overlay-center {
|
||||
@@ -1201,6 +1283,9 @@
|
||||
/* ===== 海报弹窗 ===== */
|
||||
.poster-modal {
|
||||
padding-bottom: calc(64rpx + env(safe-area-inset-bottom));
|
||||
max-height: 85vh;
|
||||
overflow-y: auto;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.poster-preview {
|
||||
@@ -1251,44 +1336,54 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ===== 右下角悬浮分享按钮 ===== */
|
||||
.fab-share {
|
||||
/* ===== 右下角:分享到朋友圈(固定悬浮小圆钮,不在文末分享行) ===== */
|
||||
.fab-share-moments {
|
||||
position: fixed;
|
||||
right: 32rpx;
|
||||
width:70rpx!important;
|
||||
bottom: calc(120rpx + env(safe-area-inset-bottom));
|
||||
height: 70rpx;
|
||||
border-radius: 60rpx;
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, #00CED1 0%, #20B2AA 100%);
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.4);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
z-index: 9999;
|
||||
display:flex;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 206, 209, 0.42);
|
||||
z-index: 9980;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
transition: transform 0.15s ease, opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.fab-share::after {
|
||||
border: none;
|
||||
.fab-share-moments-hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.fab-share:active {
|
||||
transform: scale(0.95);
|
||||
box-shadow: 0 4rpx 20rpx rgba(0, 206, 209, 0.5);
|
||||
.fab-share-moments:active {
|
||||
transform: scale(0.94);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
padding:16rpx;
|
||||
width: 50rpx;
|
||||
height: 50rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fab-share-icon {
|
||||
.fab-share-moments-icon {
|
||||
font-size: 44rpx;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
/* 朋友圈单页:点「购买本章」后,箭头指向底栏「前往小程序」(字少,仅符号) */
|
||||
.singlepage-launch-pointer {
|
||||
position: fixed;
|
||||
right: 48rpx;
|
||||
bottom: calc(168rpx + env(safe-area-inset-bottom));
|
||||
z-index: 99985;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.singlepage-launch-pointer__arrow {
|
||||
font-size: 56rpx;
|
||||
line-height: 1;
|
||||
color: #00CED1;
|
||||
text-shadow: 0 0 20rpx rgba(0, 206, 209, 0.55);
|
||||
transform: rotate(0deg);
|
||||
animation: singlepage-launch-pulse 1.25s ease-in-out infinite;
|
||||
}
|
||||
@keyframes singlepage-launch-pulse {
|
||||
0%, 100% { opacity: 0.75; transform: translate(0, 0) scale(1); }
|
||||
50% { opacity: 1; transform: translate(8rpx, 10rpx) scale(1.06); }
|
||||
}
|
||||
|
||||
178
miniprogram/pages/reading-records/reading-records.js
Normal file
178
miniprogram/pages/reading-records/reading-records.js
Normal file
@@ -0,0 +1,178 @@
|
||||
/**
|
||||
* 阅读记录:最近阅读 + 已读章节(与「我的」数据源一致)
|
||||
*/
|
||||
const app = getApp()
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
|
||||
function titleFromBookData(sectionId, bookFlat) {
|
||||
const row = bookFlat.find((s) => s.id === sectionId)
|
||||
return cleanSingleLineField(
|
||||
row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || ''
|
||||
) || `章节 ${sectionId}`
|
||||
}
|
||||
|
||||
function midFromBookData(sectionId, bookFlat) {
|
||||
const row = bookFlat.find((s) => s.id === sectionId)
|
||||
return row?.mid ?? row?.MID ?? 0
|
||||
}
|
||||
|
||||
function mergeRecentFromLocal(apiList) {
|
||||
const normalized = Array.isArray(apiList)
|
||||
? apiList.map((item) => ({
|
||||
id: item.id,
|
||||
mid: item.mid,
|
||||
title: cleanSingleLineField(item.title || '') || `章节 ${item.id}`
|
||||
}))
|
||||
: []
|
||||
if (normalized.length > 0) return normalized
|
||||
try {
|
||||
const progressData = wx.getStorageSync('reading_progress') || {}
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
return Object.keys(progressData)
|
||||
.map((id) => ({
|
||||
id,
|
||||
ts: progressData[id]?.lastOpenAt || progressData[id]?.last_open_at || 0
|
||||
}))
|
||||
.filter((e) => e.id)
|
||||
.sort((a, b) => b.ts - a.ts)
|
||||
.slice(0, 20)
|
||||
.map((e) => ({
|
||||
id: e.id,
|
||||
mid: midFromBookData(e.id, bookFlat),
|
||||
title: titleFromBookData(e.id, bookFlat)
|
||||
}))
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
statusBarHeight: 44,
|
||||
isLoggedIn: false,
|
||||
focus: 'all',
|
||||
recentList: [],
|
||||
readAllList: [],
|
||||
recentSectionTitle: '最近阅读',
|
||||
readSectionTitle: '已读章节'
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const focus = (options.focus === 'recent' || options.focus === 'read') ? options.focus : 'all'
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight || 44,
|
||||
focus
|
||||
})
|
||||
this._applyMpUiTitles()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this.setData({ isLoggedIn: !!(app.globalData.isLoggedIn && app.globalData.userInfo?.id) })
|
||||
this._applyMpUiTitles()
|
||||
if (this.data.isLoggedIn) this.loadData()
|
||||
},
|
||||
|
||||
_applyMpUiTitles() {
|
||||
const my = app.globalData.configCache?.mpConfig?.mpUi?.myPage || {}
|
||||
this.setData({
|
||||
recentSectionTitle: my.recentReadTitle || '最近阅读',
|
||||
readSectionTitle: my.readStatLabel || '已读章节'
|
||||
})
|
||||
},
|
||||
|
||||
async _ensureBookFlat() {
|
||||
let flat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
if (flat.length) return flat
|
||||
try {
|
||||
const r = await app.request({ url: '/api/miniprogram/book/all-chapters', silent: true })
|
||||
const list = r?.data
|
||||
if (Array.isArray(list) && list.length) {
|
||||
app.globalData.bookData = list
|
||||
return list
|
||||
}
|
||||
} catch (_) {}
|
||||
return []
|
||||
},
|
||||
|
||||
async loadData() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
try {
|
||||
const res = await app.request({
|
||||
url: `/api/miniprogram/user/dashboard-stats?userId=${encodeURIComponent(userId)}`,
|
||||
silent: true
|
||||
})
|
||||
const bookFlat = await this._ensureBookFlat()
|
||||
let recent = []
|
||||
let readIds = []
|
||||
if (res?.success && res.data) {
|
||||
const apiRecent = Array.isArray(res.data.recentChapters) ? res.data.recentChapters : []
|
||||
recent = mergeRecentFromLocal(apiRecent)
|
||||
readIds = Array.isArray(res.data.readSectionIds) ? res.data.readSectionIds.filter(Boolean) : []
|
||||
} else {
|
||||
recent = mergeRecentFromLocal([])
|
||||
}
|
||||
try {
|
||||
const progressData = wx.getStorageSync('reading_progress') || {}
|
||||
const fromKeys = Object.keys(progressData).filter(Boolean)
|
||||
const stored = wx.getStorageSync('readSectionIds')
|
||||
const fromStored = Array.isArray(stored) ? stored.filter(Boolean) : []
|
||||
const fromGlobal = Array.isArray(app.globalData.readSectionIds)
|
||||
? app.globalData.readSectionIds.filter(Boolean)
|
||||
: []
|
||||
readIds = [...new Set([...readIds, ...fromGlobal, ...fromStored, ...fromKeys])]
|
||||
} catch (_) {}
|
||||
if (readIds.length === 0 && recent.length > 0) {
|
||||
readIds = recent.map((r) => r.id)
|
||||
}
|
||||
const readAllList = readIds.map((id) => ({
|
||||
id,
|
||||
mid: midFromBookData(id, bookFlat),
|
||||
title: titleFromBookData(id, bookFlat)
|
||||
}))
|
||||
this.setData({ recentList: recent, readAllList })
|
||||
} catch (e) {
|
||||
console.warn('[reading-records]', e)
|
||||
try {
|
||||
const bookFlat = await this._ensureBookFlat()
|
||||
let readIds = Array.isArray(app.globalData.readSectionIds) ? [...app.globalData.readSectionIds] : []
|
||||
if (!readIds.length) {
|
||||
try {
|
||||
const stored = wx.getStorageSync('readSectionIds')
|
||||
if (Array.isArray(stored)) readIds = [...stored]
|
||||
} catch (_) {}
|
||||
}
|
||||
const recent = mergeRecentFromLocal([])
|
||||
if (!readIds.length && recent.length) readIds = recent.map((r) => r.id)
|
||||
const readAllList = readIds.map((id) => ({
|
||||
id,
|
||||
mid: midFromBookData(id, bookFlat),
|
||||
title: titleFromBookData(id, bookFlat)
|
||||
}))
|
||||
this.setData({ recentList: recent, readAllList })
|
||||
} catch (_) {
|
||||
this.setData({ recentList: [], readAllList: [] })
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
goRead(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}` })
|
||||
},
|
||||
|
||||
goChapters() {
|
||||
wx.switchTab({ url: '/pages/chapters/chapters' })
|
||||
},
|
||||
|
||||
goLogin() {
|
||||
wx.switchTab({ url: '/pages/my/my' })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
getApp().goBackOrToHome()
|
||||
}
|
||||
})
|
||||
4
miniprogram/pages/reading-records/reading-records.json
Normal file
4
miniprogram/pages/reading-records/reading-records.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
51
miniprogram/pages/reading-records/reading-records.wxml
Normal file
51
miniprogram/pages/reading-records/reading-records.wxml
Normal file
@@ -0,0 +1,51 @@
|
||||
<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="rgba(255,255,255,0.8)" customClass="back-icon"></icon></view>
|
||||
<text class="nav-title">阅读记录</text>
|
||||
<view class="nav-placeholder"></view>
|
||||
</view>
|
||||
<view style="height: {{statusBarHeight + 44}}px;"></view>
|
||||
|
||||
<view class="content" wx:if="{{isLoggedIn}}">
|
||||
<view class="section" wx:if="{{focus === 'all' || focus === 'recent'}}">
|
||||
<view class="section-head">
|
||||
<text class="section-title">{{recentSectionTitle}}</text>
|
||||
<text class="section-count" wx:if="{{recentList.length > 0}}">{{recentList.length}} 条</text>
|
||||
</view>
|
||||
<view class="list" wx:if="{{recentList.length > 0}}">
|
||||
<view class="row" wx:for="{{recentList}}" wx:key="id" bindtap="goRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
|
||||
<text class="row-idx">{{index + 1}}</text>
|
||||
<text class="row-title">{{item.title}}</text>
|
||||
<text class="row-link">阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" wx:else>
|
||||
<text class="empty-t">暂无最近阅读</text>
|
||||
<text class="empty-a" bindtap="goChapters">去目录逛逛</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="section" wx:if="{{focus === 'all' || focus === 'read'}}">
|
||||
<view class="section-head">
|
||||
<text class="section-title">{{readSectionTitle}}</text>
|
||||
<text class="section-count" wx:if="{{readAllList.length > 0}}">共 {{readAllList.length}} 章</text>
|
||||
</view>
|
||||
<view class="list" wx:if="{{readAllList.length > 0}}">
|
||||
<view class="row" wx:for="{{readAllList}}" wx:key="id" bindtap="goRead" data-id="{{item.id}}" data-mid="{{item.mid}}">
|
||||
<text class="row-idx">{{index + 1}}</text>
|
||||
<text class="row-title">{{item.title}}</text>
|
||||
<text class="row-link">阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="empty" wx:else>
|
||||
<text class="empty-t">暂无已读记录</text>
|
||||
<text class="empty-a" bindtap="goChapters">去目录选章阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view class="guest" wx:else>
|
||||
<text class="guest-t">登录后查看阅读记录</text>
|
||||
<view class="guest-btn" bindtap="goLogin">去登录</view>
|
||||
</view>
|
||||
</view>
|
||||
25
miniprogram/pages/reading-records/reading-records.wxss
Normal file
25
miniprogram/pages/reading-records/reading-records.wxss
Normal file
@@ -0,0 +1,25 @@
|
||||
.page { min-height: 100vh; background: #000; padding-bottom: 48rpx; }
|
||||
.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 100; background: rgba(0,0,0,0.92); display: flex; align-items: center; justify-content: space-between; padding: 0 32rpx; height: 88rpx; }
|
||||
.nav-back { width: 72rpx; height: 72rpx; display: flex; align-items: center; justify-content: center; }
|
||||
.nav-title { font-size: 36rpx; font-weight: 600; color: #00CED1; flex: 1; text-align: center; }
|
||||
.nav-placeholder { width: 72rpx; }
|
||||
.content { padding: 32rpx; }
|
||||
.section { margin-bottom: 48rpx; }
|
||||
.section-head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 24rpx; }
|
||||
.section-title { font-size: 32rpx; font-weight: 600; color: #e5e7eb; }
|
||||
.section-count { font-size: 24rpx; color: #6b7280; }
|
||||
.list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.row {
|
||||
display: flex; align-items: center; gap: 20rpx;
|
||||
padding: 24rpx; background: #1c1c1e; border-radius: 20rpx;
|
||||
}
|
||||
.row:active { opacity: 0.9; }
|
||||
.row-idx { font-size: 26rpx; color: #6b7280; font-family: monospace; flex-shrink: 0; }
|
||||
.row-title { flex: 1; min-width: 0; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.row-link { font-size: 24rpx; color: #00CED1; flex-shrink: 0; }
|
||||
.empty { padding: 48rpx 24rpx; text-align: center; }
|
||||
.empty-t { display: block; font-size: 28rpx; color: #6b7280; margin-bottom: 24rpx; }
|
||||
.empty-a { font-size: 28rpx; color: #00CED1; }
|
||||
.guest { padding: 120rpx 48rpx; text-align: center; }
|
||||
.guest-t { display: block; font-size: 30rpx; color: #9ca3af; margin-bottom: 32rpx; }
|
||||
.guest-btn { display: inline-block; padding: 20rpx 48rpx; background: #00CED1; color: #000; font-size: 28rpx; font-weight: 600; border-radius: 24rpx; }
|
||||
@@ -35,7 +35,7 @@ Page({
|
||||
minWithdrawAmount: 10, // 最低提现金额,从 referral/data 获取
|
||||
bindingDays: 30, // 绑定期天数,从 referral/data 获取
|
||||
userDiscount: 5, // 好友购买优惠%,从 referral/data 获取
|
||||
hasWechatId: false, // 是否已绑定微信号(未绑定时需引导去设置)
|
||||
hasWechatId: false, // 是否已绑定微信号(未绑定时提示去设置页)
|
||||
|
||||
// === 统计数据 ===
|
||||
referralCount: 0, // 总推荐人数
|
||||
@@ -598,7 +598,7 @@ Page({
|
||||
}
|
||||
// 任意金额可提现,不再设最低限额
|
||||
|
||||
// 未绑定微信号时引导去设置
|
||||
// 未绑定微信号:说明提现到账核对所需
|
||||
if (!hasWechatId) {
|
||||
wx.showModal({
|
||||
title: '请先绑定微信号',
|
||||
|
||||
@@ -12,16 +12,16 @@ Page({
|
||||
originalPrice: 6980,
|
||||
/* 按 premium_membership_landing_v1 设计稿 */
|
||||
contentRights: [
|
||||
{ title: '匹配伙伴', desc: '精准匹配创业伙伴', icon: 'users' },
|
||||
{ title: '派对专属', desc: '创业派对房专享', icon: 'star' },
|
||||
{ title: '老板排行', desc: '创业老板排行榜', icon: 'bar-chart' },
|
||||
{ title: '轮流置顶', desc: '首页获客曝光位', icon: 'arrow-up' }
|
||||
{ key: 'match', title: '匹配伙伴', desc: '精准匹配创业伙伴', icon: 'users' },
|
||||
{ key: 'party', title: '派对专属', desc: '创业派对房专享', icon: 'star' },
|
||||
{ key: 'rank', title: '老板排行', desc: '创业老板排行榜', icon: 'bar-chart' },
|
||||
{ key: 'top', title: '轮流置顶', desc: '首页获客曝光位', icon: 'chevron-up' }
|
||||
],
|
||||
socialRights: [
|
||||
{ title: '案例宝库', desc: '100+赚钱案例库', icon: 'book-open' },
|
||||
{ title: '全书解锁', desc: '365天全案精读', icon: 'folder' },
|
||||
{ title: '每日总结', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ title: '获取客资', desc: '文章@你即可获客', icon: 'link' }
|
||||
{ key: 'cases', title: '案例宝库', desc: '100+赚钱案例库', icon: 'book-open' },
|
||||
{ key: 'fullbook', title: '全书解锁', desc: '365天全案精读', icon: 'folder' },
|
||||
{ key: 'daily', title: '每日总结', desc: 'AI每日精华推送', icon: 'lightbulb' },
|
||||
{ key: 'leads', title: '获取客资', desc: '文章@你即可获客', icon: 'link' }
|
||||
],
|
||||
purchasing: false
|
||||
},
|
||||
@@ -85,7 +85,7 @@ Page({
|
||||
return
|
||||
}
|
||||
}
|
||||
// VIP 购买后才引导完善资料:购买前不拦截,购买成功后跳转 profile-edit
|
||||
// VIP 购买成功后再跳转资料页:购买前不拦截
|
||||
this.setData({ purchasing: true })
|
||||
const amount = this.data.price
|
||||
try {
|
||||
@@ -163,9 +163,9 @@ Page({
|
||||
// 超级个体购买后:弹窗提示,强制跳转资料编辑页
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '完善资料',
|
||||
content: '为了更好为您服务,请填写好资料',
|
||||
confirmText: '去完善',
|
||||
title: '补全 VIP 资料',
|
||||
content: '补全资料后,找伙伴、提现与 VIP 群对接会更顺畅;手机号等为必填项。',
|
||||
confirmText: '去填写',
|
||||
showCancel: false,
|
||||
success: () => {
|
||||
wx.redirectTo({ url: '/pages/profile-edit/profile-edit?from=vip' })
|
||||
@@ -179,6 +179,33 @@ Page({
|
||||
|
||||
goBack() { getApp().goBackOrToHome() },
|
||||
|
||||
/**
|
||||
* 权益卡片跳转:会员权利 / 派对权利 统一点击进对应能力页
|
||||
*/
|
||||
onBenefitTap(e) {
|
||||
const key = e.currentTarget.dataset.key
|
||||
if (!key) return
|
||||
trackClick('vip', 'benefit_tap', key)
|
||||
const tab = (path) => {
|
||||
wx.switchTab({ url: path })
|
||||
}
|
||||
const nav = (path) => {
|
||||
wx.navigateTo({ url: path })
|
||||
}
|
||||
const routes = {
|
||||
match: () => tab('/pages/match/match'),
|
||||
party: () => nav('/pages/mentors/mentors'),
|
||||
rank: () => tab('/pages/index/index'),
|
||||
top: () => tab('/pages/index/index'),
|
||||
cases: () => tab('/pages/chapters/chapters'),
|
||||
fullbook: () => tab('/pages/chapters/chapters'),
|
||||
daily: () => tab('/pages/chapters/chapters'),
|
||||
leads: () => nav('/pages/profile-edit/profile-edit')
|
||||
}
|
||||
const fn = routes[key]
|
||||
if (typeof fn === 'function') fn()
|
||||
},
|
||||
|
||||
onShareAppMessage() {
|
||||
const ref = app.getMyReferralCode()
|
||||
return {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
<text class="rights-dot rights-dot-teal"></text>
|
||||
<text class="rights-col-title">会员权利</text>
|
||||
</view>
|
||||
<view class="benefit-card" wx:for="{{contentRights}}" wx:key="title">
|
||||
<view class="benefit-card benefit-card-tap" wx:for="{{contentRights}}" wx:key="key" bindtap="onBenefitTap" data-key="{{item.key}}" hover-class="benefit-card-hover">
|
||||
<icon name="{{item.icon || 'check'}}" size="40" color="#00CED1" customClass="benefit-icon"></icon>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-title">{{item.title}}</text>
|
||||
@@ -34,7 +34,7 @@
|
||||
<text class="rights-dot rights-dot-gold"></text>
|
||||
<text class="rights-col-title rights-col-title-gold">派对权利</text>
|
||||
</view>
|
||||
<view class="benefit-card" wx:for="{{socialRights}}" wx:key="title">
|
||||
<view class="benefit-card benefit-card-tap" wx:for="{{socialRights}}" wx:key="key" bindtap="onBenefitTap" data-key="{{item.key}}" hover-class="benefit-card-hover">
|
||||
<icon name="{{item.icon || 'check'}}" size="40" color="#FFD700" customClass="benefit-icon benefit-icon-gold"></icon>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-title">{{item.title}}</text>
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
.rights-col-title { font-size: 24rpx; font-weight: bold; color: #4FD1C5; letter-spacing: 2rpx; }
|
||||
.rights-col-title-gold { color: #FFBD2E; }
|
||||
.benefit-card { display: flex; flex-direction: column; gap: 16rpx; padding: 24rpx; margin-bottom: 16rpx; background: #141414; border: 1rpx solid rgba(255,255,255,0.05); border-radius: 24rpx; }
|
||||
.benefit-card-tap { transition: opacity 0.15s; }
|
||||
.benefit-card-hover { opacity: 0.88; background: #1a1a1a; }
|
||||
.benefit-icon { font-size: 36rpx; color: #4FD1C5; }
|
||||
.benefit-icon-gold { color: #FFBD2E; }
|
||||
.benefit-info { display: flex; flex-direction: column; }
|
||||
|
||||
Reference in New Issue
Block a user