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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user