Merge branch 'devlop' into yongxu-dev
# Conflicts: # .cursor/skills/miniprogram-dev/SKILL.md resolved by devlop version # miniprogram/pages/index/index.js resolved by devlop version # miniprogram/pages/index/index.wxml resolved by devlop version # miniprogram/pages/my/my.wxml resolved by devlop version # miniprogram/pages/read/read.wxml resolved by devlop version # miniprogram/pages/read/read.wxss resolved by devlop version # soul-admin/dist/index.html resolved by devlop version # soul-admin/src/pages/content/ChapterTree.tsx resolved by devlop version # soul-admin/src/pages/content/ContentPage.tsx resolved by devlop version # soul-admin/src/pages/distribution/DistributionPage.tsx resolved by devlop version # soul-api/internal/handler/book.go resolved by devlop version # soul-api/internal/handler/ckb.go resolved by devlop version # soul-api/internal/handler/db_book.go resolved by devlop version # soul-api/internal/handler/db_ckb_leads.go resolved by devlop version # soul-api/internal/handler/db_person.go resolved by devlop version # soul-api/internal/model/chapter.go resolved by devlop version
This commit is contained in:
@@ -7,6 +7,22 @@
|
||||
const app = getApp()
|
||||
const { formatStatNum } = require('../../utils/util.js')
|
||||
const { trackClick } = require('../../utils/trackClick')
|
||||
const { cleanSingleLineField } = require('../../utils/contentParser.js')
|
||||
|
||||
/** 是否视为「单章解锁」类订单(排除全书/VIP 等聚合商品名) */
|
||||
function isSectionUnlockOrder(o) {
|
||||
const name = String(o.product_name || o.title || '').trim()
|
||||
if (/全书|全書|VIP|会员|年费|买断/.test(name)) return false
|
||||
const pid = String(o.product_id || o.section_id || o.sectionId || '')
|
||||
if (/^\d+\.\d+/.test(pid)) return true
|
||||
return !!pid && pid.length > 0
|
||||
}
|
||||
|
||||
function parseOrderTimeMs(o) {
|
||||
const raw = o.created_at || o.createdAt || o.pay_time || 0
|
||||
const t = new Date(raw).getTime()
|
||||
return Number.isFinite(t) ? t : 0
|
||||
}
|
||||
|
||||
Page({
|
||||
data: {
|
||||
@@ -33,6 +49,8 @@ Page({
|
||||
readCountText: '0',
|
||||
totalReadTimeText: '0',
|
||||
matchHistoryText: '0',
|
||||
orderCountText: '0',
|
||||
giftPayCountText: '0',
|
||||
|
||||
// 最近阅读
|
||||
recentChapters: [],
|
||||
@@ -78,6 +96,11 @@ Page({
|
||||
|
||||
// 我的余额
|
||||
walletBalanceText: '--',
|
||||
|
||||
// 已解锁章节(订单倒序;默认最多展示 5 条,底部倒三角展开)
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
@@ -164,6 +187,7 @@ Page({
|
||||
this.loadPendingConfirm()
|
||||
this.loadVipStatus()
|
||||
this.loadWalletBalance()
|
||||
this.loadUnlockedChapters()
|
||||
} else {
|
||||
const guestReadCount = app.getReadCount()
|
||||
this.setData({
|
||||
@@ -177,6 +201,9 @@ Page({
|
||||
pendingEarnings: '-',
|
||||
earningsLoading: false,
|
||||
recentChapters: [],
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false,
|
||||
totalReadTime: 0,
|
||||
matchHistory: 0,
|
||||
totalReadTimeText: '0',
|
||||
@@ -185,6 +212,91 @@ Page({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 已解锁章节:优先订单接口(按支付时间倒序);失败时用 purchasedSections + bookData 兜底
|
||||
*/
|
||||
async loadUnlockedChapters() {
|
||||
if (!app.globalData.isLoggedIn || !app.globalData.userInfo?.id) {
|
||||
this.setData({
|
||||
unlockedChaptersFull: [],
|
||||
displayUnlockedChapters: [],
|
||||
unlockedExpanded: false
|
||||
})
|
||||
return
|
||||
}
|
||||
const userId = app.globalData.userInfo.id
|
||||
const expanded = this.data.unlockedExpanded
|
||||
const bookFlat = Array.isArray(app.globalData.bookData) ? app.globalData.bookData : []
|
||||
const metaById = (id) => {
|
||||
const row = bookFlat.find((s) => s.id === id)
|
||||
return {
|
||||
mid: row?.mid ?? row?.MID ?? 0,
|
||||
title: cleanSingleLineField(row?.sectionTitle || row?.section_title || row?.title || row?.chapterTitle || '')
|
||||
}
|
||||
}
|
||||
try {
|
||||
const res = await app.request({ url: `/api/miniprogram/orders?userId=${encodeURIComponent(userId)}`, silent: true })
|
||||
let rows = []
|
||||
if (res && res.success && Array.isArray(res.data)) {
|
||||
rows = res.data
|
||||
.map((item) => ({
|
||||
id: item.product_id || item.section_id,
|
||||
mid: item.section_mid ?? item.mid ?? item.MID ?? 0,
|
||||
title: cleanSingleLineField(item.product_name || ''),
|
||||
_ts: parseOrderTimeMs(item)
|
||||
}))
|
||||
.filter((r) => r.id && isSectionUnlockOrder({ product_id: r.id, product_name: r.title }))
|
||||
}
|
||||
rows.sort((a, b) => b._ts - a._ts)
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const r of rows) {
|
||||
if (seen.has(r.id)) continue
|
||||
seen.add(r.id)
|
||||
const meta = metaById(r.id)
|
||||
deduped.push({
|
||||
id: r.id,
|
||||
mid: r.mid || meta.mid,
|
||||
title: cleanSingleLineField(r.title || meta.title || `章节 ${r.id}`)
|
||||
})
|
||||
}
|
||||
if (deduped.length === 0) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])]
|
||||
ids.reverse()
|
||||
for (const id of ids) {
|
||||
if (seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
} catch (e) {
|
||||
const ids = [...(app.globalData.purchasedSections || [])].reverse()
|
||||
const seen = new Set()
|
||||
const deduped = []
|
||||
for (const id of ids) {
|
||||
if (!id || seen.has(id)) continue
|
||||
seen.add(id)
|
||||
const meta = metaById(id)
|
||||
deduped.push({ id, mid: meta.mid, title: cleanSingleLineField(meta.title || `章节 ${id}`) })
|
||||
}
|
||||
const display = expanded ? deduped : deduped.slice(0, 5)
|
||||
this.setData({ unlockedChaptersFull: deduped, displayUnlockedChapters: display })
|
||||
}
|
||||
},
|
||||
|
||||
expandUnlockedChapters() {
|
||||
if (this.data.unlockedExpanded) return
|
||||
trackClick('my', 'tab_click', '已解锁章节_展开')
|
||||
const full = this.data.unlockedChaptersFull || []
|
||||
this.setData({
|
||||
unlockedExpanded: true,
|
||||
displayUnlockedChapters: full
|
||||
})
|
||||
},
|
||||
|
||||
async loadDashboardStats() {
|
||||
const userId = app.globalData.userInfo?.id
|
||||
if (!userId) return
|
||||
@@ -212,6 +324,8 @@ Page({
|
||||
const readCount = Number(res.data.readCount || 0)
|
||||
const totalReadTime = Number(res.data.totalReadMinutes || 0)
|
||||
const matchHistory = Number(res.data.matchHistory || 0)
|
||||
const orderCount = Number(res.data.orderCount || 0)
|
||||
const giftPayCount = Number(res.data.giftPayCount || 0)
|
||||
this.setData({
|
||||
readCount,
|
||||
totalReadTime,
|
||||
@@ -219,6 +333,8 @@ Page({
|
||||
readCountText: formatStatNum(readCount),
|
||||
totalReadTimeText: formatStatNum(totalReadTime),
|
||||
matchHistoryText: formatStatNum(matchHistory),
|
||||
orderCountText: formatStatNum(orderCount),
|
||||
giftPayCountText: formatStatNum(giftPayCount),
|
||||
recentChapters
|
||||
})
|
||||
} catch (e) {
|
||||
|
||||
@@ -49,20 +49,20 @@
|
||||
</view>
|
||||
<view class="profile-stats-row">
|
||||
<view class="profile-stat" bindtap="goToChapters">
|
||||
<text class="profile-stat-val">{{readCountText}}</text>
|
||||
<text class="profile-stat-val">{{readCountText || '0'}}</text>
|
||||
<text class="profile-stat-label">已读章节</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{referralCount}}</text>
|
||||
<text class="profile-stat-label">推荐好友</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{referralEnabled}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings === '-' ? '--' : pendingEarnings}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToMatch">
|
||||
<text class="profile-stat-val">{{matchHistoryText}}</text>
|
||||
<text class="profile-stat-label">匹配伙伴</text>
|
||||
</view>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="handleMenuTap" data-id="wallet">
|
||||
<text class="profile-stat-val">{{walletBalanceText}}</text>
|
||||
<text class="profile-stat-label">我的余额</text>
|
||||
<view class="profile-stat" wx:if="{{!auditMode}}" bindtap="goToReferral">
|
||||
<text class="profile-stat-val">{{pendingEarnings || '0.00'}}</text>
|
||||
<text class="profile-stat-label">我的收益</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -92,31 +92,63 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 阅读统计 -->
|
||||
<!-- 快捷入口:我的订单 + 我的代付 -->
|
||||
<view class="card stats-card">
|
||||
<view class="card-header">
|
||||
<image class="card-icon-img" src="/assets/icons/eye-teal.svg" mode="aspectFit"/>
|
||||
<text class="card-title">阅读统计</text>
|
||||
<text class="card-title">快捷入口</text>
|
||||
</view>
|
||||
<view class="stats-grid">
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/book-open-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{readCountText}}</text>
|
||||
<text class="stat-label">已读章节</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="orders">
|
||||
<image class="stat-icon-img" src="/assets/icons/list-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">订单</text>
|
||||
<text class="stat-label">我的订单</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToChapters">
|
||||
<image class="stat-icon-img" src="/assets/icons/clock-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{totalReadTimeText}}</text>
|
||||
<text class="stat-label">阅读分钟</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<image class="stat-icon-img" src="/assets/icons/share-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">代付</text>
|
||||
<text class="stat-label">我的代付</text>
|
||||
</view>
|
||||
<view class="stat-box" bindtap="goToMatch">
|
||||
<image class="stat-icon-img" src="/assets/icons/users-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{matchHistoryText}}</text>
|
||||
<text class="stat-label">匹配伙伴</text>
|
||||
<view class="stat-box" bindtap="handleMenuTap" data-id="wallet">
|
||||
<image class="stat-icon-img" src="/assets/icons/wallet-teal.svg" mode="aspectFit"/>
|
||||
<text class="stat-num">{{walletBalanceText}}</text>
|
||||
<text class="stat-label">我的余额</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 已解锁:仅低调图标区(无标题文案);默认 5 条 + 倒三角展开;倒序由接口/JS 保证 -->
|
||||
<view class="card recent-card unlocked-card" wx:if="{{unlockedChaptersFull.length > 0}}">
|
||||
<view class="unlocked-section-head">
|
||||
<image class="unlocked-section-icon" src="/assets/icons/unlock-muted-teal.svg" mode="aspectFit"/>
|
||||
</view>
|
||||
<view class="recent-list">
|
||||
<view
|
||||
class="recent-item"
|
||||
wx:for="{{displayUnlockedChapters}}"
|
||||
wx:key="id"
|
||||
bindtap="goToRead"
|
||||
data-id="{{item.id}}"
|
||||
data-mid="{{item.mid}}"
|
||||
>
|
||||
<view class="recent-left">
|
||||
<text class="recent-index">{{index + 1}}</text>
|
||||
<text class="recent-title">{{item.title}}</text>
|
||||
</view>
|
||||
<text class="recent-link">阅读</text>
|
||||
</view>
|
||||
</view>
|
||||
<view
|
||||
class="unlocked-expand-hint"
|
||||
wx:if="{{unlockedChaptersFull.length > 5 && !unlockedExpanded}}"
|
||||
bindtap="expandUnlockedChapters"
|
||||
hover-class="unlocked-expand-hint-hover"
|
||||
hover-stay-time="80"
|
||||
>
|
||||
<view class="unlocked-expand-triangle"></view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 最近阅读 -->
|
||||
<view class="card recent-card">
|
||||
<view class="card-header">
|
||||
@@ -145,23 +177,9 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 我的订单 + 设置 -->
|
||||
<view class="card menu-card">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="orders">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><image class="menu-icon-img" src="/assets/icons/folder-teal.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">我的订单</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="giftPay">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-teal"><icon name="gift" size="32" color="#4FD1C5" customClass="menu-icon"></icon></view>
|
||||
<text class="menu-text">我的代付</text>
|
||||
</view>
|
||||
<icon name="chevron-right" size="28" color="rgba(255,255,255,0.35)" customClass="menu-arrow"></icon>
|
||||
</view>
|
||||
<view class="menu-item" wx:if="{{showSettingsEntry}}" bindtap="handleMenuTap" data-id="settings">
|
||||
<!-- 设置 -->
|
||||
<view class="card menu-card" wx:if="{{showSettingsEntry}}">
|
||||
<view class="menu-item" bindtap="handleMenuTap" data-id="settings">
|
||||
<view class="menu-left">
|
||||
<view class="menu-icon-wrap icon-gray"><image class="menu-icon-img" src="/assets/icons/settings-gray.svg" mode="aspectFit"/></view>
|
||||
<text class="menu-text">设置</text>
|
||||
|
||||
@@ -74,10 +74,6 @@
|
||||
.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-actions { display: flex; align-items: center; gap: 16rpx; flex-shrink: 0; }
|
||||
.profile-edit-btn { display: flex; align-items: center; gap: 8rpx; padding: 8rpx 16rpx; background: rgba(255,255,255,0.08); border-radius: 12rpx; }
|
||||
.profile-edit-icon { width: 28rpx; height: 28rpx; opacity: 0.7; }
|
||||
.profile-edit-text { font-size: 24rpx; color: rgba(255,255,255,0.7); }
|
||||
.user-name {
|
||||
font-size: 44rpx; font-weight: bold; color: #fff;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0;
|
||||
@@ -166,7 +162,20 @@
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 24rpx; background: #252525; border-radius: 20rpx;
|
||||
}
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; }
|
||||
.recent-left { display: flex; align-items: center; gap: 24rpx; overflow: hidden; min-width: 0; }
|
||||
/* 已解锁区块:仅顶部弱对比图标,无标题字 */
|
||||
.unlocked-card { padding-top: 28rpx; }
|
||||
.unlocked-section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
padding: 0 8rpx 16rpx 8rpx;
|
||||
}
|
||||
.unlocked-section-icon {
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
opacity: 0.92;
|
||||
}
|
||||
.recent-index { font-size: 28rpx; color: #6B7280; font-family: monospace; }
|
||||
.recent-title { font-size: 28rpx; color: #E5E7EB; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.recent-link { font-size: 24rpx; color: #4FD1C5; font-weight: 500; flex-shrink: 0; }
|
||||
@@ -174,6 +183,25 @@
|
||||
.recent-empty-text { font-size: 28rpx; color: #6B7280; display: block; margin-bottom: 24rpx; }
|
||||
.recent-empty-btn { font-size: 28rpx; color: #4FD1C5; }
|
||||
|
||||
/* 已解锁章节列表底部倒三角展开(与首页「最新新增」一致) */
|
||||
.unlocked-expand-hint {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 8rpx 0 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.unlocked-expand-hint-hover {
|
||||
opacity: 0.65;
|
||||
}
|
||||
.unlocked-expand-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 16rpx solid transparent;
|
||||
border-right: 16rpx solid transparent;
|
||||
border-top: 20rpx solid rgba(79, 209, 197, 0.85);
|
||||
}
|
||||
|
||||
/* 菜单 */
|
||||
.menu-card { padding: 0; margin-bottom: 48rpx; overflow: hidden; }
|
||||
.menu-item {
|
||||
@@ -197,8 +225,10 @@
|
||||
.icon-blue .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gray { background: rgba(156,163,175,0.15); }
|
||||
.icon-gray .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-gold { background: rgba(200,161,70,0.2); }
|
||||
.icon-gold .menu-icon-img { width: 32rpx; height: 32rpx; }
|
||||
.icon-amber { background: rgba(245,158,11,0.2); }
|
||||
.menu-icon-emoji { font-size: 28rpx; }
|
||||
.menu-right { display: flex; align-items: center; gap: 12rpx; }
|
||||
.menu-balance { font-size: 26rpx; color: #4FD1C5; font-weight: 500; }
|
||||
.menu-text { font-size: 28rpx; color: #E5E7EB; font-weight: 500; }
|
||||
.menu-arrow { font-size: 36rpx; color: #9CA3AF; }
|
||||
|
||||
@@ -272,5 +302,14 @@
|
||||
.modal-btn-cancel { background: rgba(255,255,255,0.1); color: #fff; }
|
||||
.modal-btn-confirm { background: #4FD1C5; color: #000; font-weight: 600; }
|
||||
|
||||
/* 代付链接卡片 */
|
||||
.gift-list { display: flex; flex-direction: column; gap: 16rpx; }
|
||||
.gift-item { display: flex; align-items: center; justify-content: space-between; padding: 20rpx 24rpx; background: rgba(255,255,255,0.05); border-radius: 16rpx; }
|
||||
.gift-left { flex: 1; min-width: 0; }
|
||||
.gift-title { display: block; font-size: 28rpx; color: #fff; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.gift-meta { display: block; font-size: 22rpx; color: #9CA3AF; margin-top: 6rpx; }
|
||||
.gift-share-btn { display: inline-block; padding: 8rpx 28rpx; background: #4FD1C5; color: #000; font-size: 24rpx; font-weight: 600; border-radius: 20rpx; }
|
||||
.gift-done { font-size: 24rpx; color: #6B7280; }
|
||||
|
||||
/* 底部留白:配合 page padding-bottom,避免内容被 TabBar 遮挡 */
|
||||
.bottom-space { height: calc(80rpx + env(safe-area-inset-bottom, 0px)); }
|
||||
|
||||
Reference in New Issue
Block a user