first commit
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* 频道详情 — 从后端获取频道信息 + 节目列表
|
||||
*
|
||||
* 性能优化:
|
||||
* - 使用 channel detail 返回的 hasSubscribed 字段,不再单独请求订阅列表
|
||||
* - 展示 expiredAt 字段标记订阅到期时间
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
const util = require('../../utils/util')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
domain: {},
|
||||
isSubscribed: false,
|
||||
isExpired: false, // 订阅是否已过期
|
||||
expiredAt: '', // 到期时间(格式化)
|
||||
isFree: false, // 快捷字段,避免模板 domain.isFree
|
||||
isVipOnly: false,
|
||||
domainContents: [],
|
||||
isPlaying: false,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const id = options.id
|
||||
this._domainId = id
|
||||
this._loadChannelDetail()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._loadPrograms()
|
||||
this._onPlayerChange = () => this._updatePlayState()
|
||||
this._onSubChange = () => this._loadPrograms()
|
||||
app.on('playerStateChange', this._onPlayerChange)
|
||||
app.on('subscriptionChange', this._onSubChange)
|
||||
},
|
||||
|
||||
onHide() {
|
||||
if (this._onPlayerChange) app.off('playerStateChange', this._onPlayerChange)
|
||||
if (this._onSubChange) app.off('subscriptionChange', this._onSubChange)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载频道详情
|
||||
* hasSubscribed / expiredAt 都从这个接口返回
|
||||
*/
|
||||
_loadChannelDetail() {
|
||||
const self = this
|
||||
api.getChannelDetail(this._domainId).then(function (res) {
|
||||
if (res.code === 200 && res.data) {
|
||||
const ch = res.data
|
||||
const isFree = ch.isFree === 1
|
||||
|
||||
// 免费频道:不关心订阅状态和到期时间
|
||||
var expiredAt = ''
|
||||
var isExpired = false
|
||||
var isSubscribed = false
|
||||
if (!isFree) {
|
||||
if (ch.expiredAt) {
|
||||
expiredAt = ch.expiredAt.substring(0, 10).replace(/-/g, '.')
|
||||
isExpired = new Date(ch.expiredAt) < new Date()
|
||||
}
|
||||
isSubscribed = ch.hasSubscribed === 1 && !isExpired
|
||||
}
|
||||
|
||||
self.setData({
|
||||
domain: ch,
|
||||
isSubscribed,
|
||||
isExpired,
|
||||
expiredAt,
|
||||
isFree,
|
||||
isVipOnly: ch.isVipOnly === 1
|
||||
})
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[ChannelDetail] 加载频道详情失败:', err)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载节目列表(静默刷新,不重复请求订阅状态)
|
||||
*/
|
||||
_loadPrograms() {
|
||||
const self = this
|
||||
|
||||
api.getProgramList({ channelId: self._domainId, current: 1, pageSize: 50 })
|
||||
.then(function (progRes) {
|
||||
var contents = []
|
||||
if (progRes.code === 200 && progRes.data) {
|
||||
contents = progRes.data.list || progRes.data || []
|
||||
}
|
||||
|
||||
var gd = app.globalData
|
||||
var isSubscribed = self.data.isSubscribed
|
||||
var isFree = self.data.domain.isFree === 1
|
||||
var total = contents.length
|
||||
|
||||
contents = contents.map(function (item, idx) {
|
||||
return Object.assign({}, item, {
|
||||
_displayIndex: String(total - idx).padStart(2, '0'),
|
||||
_dateDot: item.createdAt ? item.createdAt.substring(0, 10).replace(/-/g, '.') : '',
|
||||
durationText: util.formatTime(item.duration || 0),
|
||||
_isThisPlaying: gd.activeContent && gd.activeContent.id === item.id,
|
||||
_isLocked: !isSubscribed && !isFree && idx > 0
|
||||
})
|
||||
})
|
||||
|
||||
self.setData({
|
||||
domainContents: contents,
|
||||
isPlaying: gd.isPlaying,
|
||||
loading: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('[ChannelDetail] 加载节目失败:', err)
|
||||
self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 仅更新播放状态(不重新请求)
|
||||
*/
|
||||
_updatePlayState() {
|
||||
var gd = app.globalData
|
||||
var contents = this.data.domainContents.map(function (item) {
|
||||
return Object.assign({}, item, {
|
||||
_isThisPlaying: gd.activeContent && gd.activeContent.id === item.id
|
||||
})
|
||||
})
|
||||
this.setData({ domainContents: contents, isPlaying: gd.isPlaying })
|
||||
},
|
||||
|
||||
onPlayItem(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const idx = parseInt(e.currentTarget.dataset.idx)
|
||||
const gd = app.globalData
|
||||
|
||||
if (!this.data.isSubscribed && !(this.data.domain.isFree === 1) && idx > 0) {
|
||||
wx.showToast({ title: '请先订阅该频道以解锁往期内容', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
var content = null
|
||||
for (var i = 0; i < this.data.domainContents.length; i++) {
|
||||
if (this.data.domainContents[i].id === id) {
|
||||
content = this.data.domainContents[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!content) return
|
||||
|
||||
if (gd.activeContent && gd.activeContent.id === id) {
|
||||
app.togglePlay()
|
||||
} else {
|
||||
app.playContent(content)
|
||||
}
|
||||
},
|
||||
|
||||
onSubscribe() {
|
||||
const id = this._domainId
|
||||
const domain = this.data.domain
|
||||
|
||||
// 已订阅 → 已在订阅中,无需操作
|
||||
if (this.data.isSubscribed) {
|
||||
wx.showToast({ title: '您已订阅该频道', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 免费频道 → 直接收听
|
||||
if (domain.isFree === 1) {
|
||||
wx.showToast({ title: '免费频道,直接收听!', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// VIP专享且未开通 → VIP页
|
||||
if (domain.isVipOnly === 1 && !app.globalData.isVip) {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
return
|
||||
}
|
||||
|
||||
// 付费频道(含已过期续费)→ 跳转订阅/支付页
|
||||
var params = 'channelId=' + id
|
||||
+ '&channelName=' + encodeURIComponent(domain.name || '')
|
||||
+ '&monthlyPrice=' + (domain.monthlyPrice || 0)
|
||||
+ '&quarterlyPrice=' + (domain.quarterlyPrice || 0)
|
||||
+ '&annualPrice=' + (domain.annualPrice || 0)
|
||||
wx.navigateTo({ url: '/pages/vip/index?' + params })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"global-player": "/components/global-player/index",
|
||||
"t-message": "tdesign-miniprogram/message/message"
|
||||
},
|
||||
"navigationBarTitleText": "频道详情"
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<!-- 频道详情 —— 频道信息头部 + 往期内容列表 -->
|
||||
<view class="detail-page">
|
||||
|
||||
<!-- 频道信息头部 -->
|
||||
<view class="hero" style="background: linear-gradient(135deg, {{domain.bgColor || '#FF9D42'}}, {{domain.bgColorEnd || '#FFB366'}});">
|
||||
<!-- 频道信息 -->
|
||||
<view class="hero-content">
|
||||
<text class="hero-icon">{{domain.cover || domain.icon || '📻'}}</text>
|
||||
<text class="hero-name">{{domain.name}}</text>
|
||||
<text class="hero-tag">{{domain.tag || domain.description || ''}}</text>
|
||||
|
||||
<!-- ═══ 按钮区:根据频道类型和订阅状态分情况 ═══ -->
|
||||
|
||||
<!-- 1. 免费频道 -->
|
||||
<block wx:if="{{isFree}}">
|
||||
<view class="hero-badge free-badge">🎁 永久免费</view>
|
||||
<button class="hero-sub-btn free-btn" bindtap="onSubscribe">
|
||||
<text>▶ 开始收听</text>
|
||||
</button>
|
||||
</block>
|
||||
|
||||
<!-- 2. VIP专享 -->
|
||||
<block wx:elif="{{isVipOnly}}">
|
||||
<view class="hero-badge vip-badge">👑 VIP专享</view>
|
||||
<button class="hero-sub-btn vip-btn" bindtap="onSubscribe">
|
||||
<text>开通 VIP 解锁</text>
|
||||
</button>
|
||||
</block>
|
||||
|
||||
<!-- 3. 已订阅且有效 -->
|
||||
<block wx:elif="{{isSubscribed}}">
|
||||
<button class="hero-sub-btn subscribed" bindtap="onSubscribe">
|
||||
<text>✓ 已订阅</text>
|
||||
</button>
|
||||
<text wx:if="{{expiredAt}}" class="hero-expired">有效至 {{expiredAt}}</text>
|
||||
</block>
|
||||
|
||||
<!-- 4. 订阅已过期(重新订阅) -->
|
||||
<block wx:elif="{{isExpired}}">
|
||||
<view class="hero-badge expired-badge">⏰ 订阅已到期</view>
|
||||
<button class="hero-sub-btn renew-btn" bindtap="onSubscribe">
|
||||
<text>续费订阅</text>
|
||||
</button>
|
||||
<text wx:if="{{expiredAt}}" class="hero-expired">已于 {{expiredAt}} 到期</text>
|
||||
</block>
|
||||
|
||||
<!-- 5. 未订阅付费频道 -->
|
||||
<block wx:else>
|
||||
<button class="hero-sub-btn" bindtap="onSubscribe">
|
||||
<text>订阅频道</text>
|
||||
</button>
|
||||
</block>
|
||||
</view>
|
||||
|
||||
<!-- 底部波浪装饰 -->
|
||||
<view class="hero-wave"></view>
|
||||
</view>
|
||||
|
||||
<!-- 内容区域 -->
|
||||
<view class="content-area">
|
||||
|
||||
<!-- 提示条:根据状态动态切换 -->
|
||||
<!-- VIP专享未开通 -->
|
||||
<view wx:if="{{isVipOnly && !isSubscribed}}" class="trial-notice vip-notice" bindtap="onSubscribe">
|
||||
<text class="notice-icon">👑</text>
|
||||
<view class="notice-info">
|
||||
<text class="notice-title">VIP专属频道</text>
|
||||
<text class="notice-desc">开通全频道会员,畅享本频道全部内容及免广告特权</text>
|
||||
</view>
|
||||
<text class="notice-action">去开通 ›</text>
|
||||
</view>
|
||||
|
||||
<!-- 订阅已过期(非免费频道) -->
|
||||
<view wx:elif="{{isExpired && !isFree}}" class="trial-notice expired-notice" bindtap="onSubscribe">
|
||||
<text class="notice-icon">⏰</text>
|
||||
<view class="notice-info">
|
||||
<text class="notice-title">订阅已到期</text>
|
||||
<text class="notice-desc">您的订阅已于 {{expiredAt}} 到期,续费后可继续收听全部内容</text>
|
||||
</view>
|
||||
<text class="notice-action">续费 ›</text>
|
||||
</view>
|
||||
|
||||
<!-- 付费频道未订阅(试听) -->
|
||||
<view wx:elif="{{!isSubscribed && !isFree}}" class="trial-notice" bindtap="onSubscribe">
|
||||
<text class="notice-icon">🔒</text>
|
||||
<view class="notice-info">
|
||||
<text class="notice-title">试听模式</text>
|
||||
<text class="notice-desc">可试听最新一期,订阅后解锁全部历史内容</text>
|
||||
</view>
|
||||
<text class="notice-action">订阅 ›</text>
|
||||
</view>
|
||||
|
||||
<!-- 内容列表标题 -->
|
||||
<view class="list-header">
|
||||
<text class="list-title">内容列表</text>
|
||||
<view class="list-count">{{domainContents.length}} 期</view>
|
||||
</view>
|
||||
|
||||
<!-- 内容列表 -->
|
||||
<view class="list">
|
||||
<view
|
||||
wx:for="{{domainContents}}"
|
||||
wx:key="id"
|
||||
class="list-item {{item._isThisPlaying ? 'playing' : ''}} {{item._isLocked ? 'locked' : ''}}"
|
||||
bindtap="onPlayItem"
|
||||
data-id="{{item.id}}"
|
||||
data-idx="{{index}}"
|
||||
>
|
||||
<!-- 序号 -->
|
||||
<text class="item-index">{{item._displayIndex}}</text>
|
||||
|
||||
<!-- 信息 -->
|
||||
<view class="item-info">
|
||||
<text class="item-title {{item._isThisPlaying ? 'text-primary' : ''}}">{{item.title}}</text>
|
||||
<view class="item-meta">
|
||||
<text>{{item._dateDot}}</text>
|
||||
<text class="meta-dot">·</text>
|
||||
<text>{{item.durationText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 播放按钮 -->
|
||||
<view wx:if="{{!item._isLocked}}" class="item-play-btn {{item._isThisPlaying ? 'active' : ''}}">
|
||||
<image
|
||||
wx:if="{{item._isThisPlaying && isPlaying}}"
|
||||
src="/assets/icons/pause.svg"
|
||||
class="item-play-icon"
|
||||
/>
|
||||
<image
|
||||
wx:else
|
||||
src="/assets/icons/play.svg"
|
||||
class="item-play-icon"
|
||||
/>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 200rpx;"></view>
|
||||
<global-player />
|
||||
<t-message id="t-message" />
|
||||
</view>
|
||||
@@ -0,0 +1,295 @@
|
||||
/* 频道详情页样式 */
|
||||
|
||||
.detail-page {
|
||||
min-height: 100vh;
|
||||
background: #FCFCFC;
|
||||
}
|
||||
|
||||
/* 沉浸式头部 */
|
||||
.hero {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
left: 32rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(10px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.back-arrow {
|
||||
font-size: 48rpx;
|
||||
color: #FFF;
|
||||
font-weight: 300;
|
||||
margin-top: -4rpx;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-bottom: 100rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
.hero-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 24rpx;
|
||||
text-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.hero-name {
|
||||
font-size: 52rpx;
|
||||
font-weight: 800;
|
||||
color: #FFF;
|
||||
letter-spacing: -2rpx;
|
||||
margin-bottom: 12rpx;
|
||||
text-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.hero-tag {
|
||||
font-size: 26rpx;
|
||||
font-weight: 500;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
.hero-sub-btn {
|
||||
padding: 16rpx 56rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
background: #FFF;
|
||||
color: #333;
|
||||
border: none;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.15);
|
||||
z-index: 10;
|
||||
}
|
||||
.hero-sub-btn::after { border: none; }
|
||||
.hero-sub-btn:active { transform: scale(0.97); }
|
||||
.hero-sub-btn.subscribed {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(10px);
|
||||
color: #FFF;
|
||||
border: 2rpx solid rgba(255, 255, 255, 0.3);
|
||||
box-shadow: none;
|
||||
}
|
||||
.hero-expired {
|
||||
display: block;
|
||||
margin-top: 12rpx;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
letter-spacing: 0.5rpx;
|
||||
}
|
||||
|
||||
/* 徽标(免费 / VIP / 到期)*/
|
||||
.hero-badge {
|
||||
display: inline-block;
|
||||
padding: 8rpx 24rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.free-badge { background: rgba(46, 204, 113, 0.25); color: #FFF; border: 1rpx solid rgba(255,255,255,0.3); }
|
||||
.vip-badge { background: rgba(251, 191, 36, 0.3); color: #FFC; border: 1rpx solid rgba(255,255,255,0.3); }
|
||||
.expired-badge { background: rgba(239, 68, 68, 0.3); color: #FFF; border: 1rpx solid rgba(255,255,255,0.3); }
|
||||
|
||||
/* 按钮变体 */
|
||||
.hero-sub-btn.free-btn { background: #2ECC71; color: #FFF; box-shadow: 0 8rpx 24rpx rgba(46,204,113,0.3); }
|
||||
.hero-sub-btn.vip-btn { background: linear-gradient(135deg, #FBBF24, #D97706); color: #1F2937; box-shadow: 0 8rpx 24rpx rgba(251,191,36,0.35); }
|
||||
.hero-sub-btn.renew-btn { background: linear-gradient(135deg, #FF9D42, #FF7832); color: #FFF; box-shadow: 0 8rpx 24rpx rgba(255,120,50,0.35); }
|
||||
|
||||
/* 波浪 */
|
||||
.hero-wave {
|
||||
position: absolute;
|
||||
bottom: -4rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 64rpx;
|
||||
background: #FCFCFC;
|
||||
border-radius: 100% 100% 0 0;
|
||||
}
|
||||
|
||||
/* 内容区 */
|
||||
.content-area {
|
||||
padding: 0 32rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 试听提示 */
|
||||
.trial-notice {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
background: var(--color-primary-light);
|
||||
border: 1rpx solid rgba(255, 157, 66, 0.2);
|
||||
border-radius: 24rpx;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 32rpx;
|
||||
}
|
||||
/* VIP提示(金色) */
|
||||
.vip-notice {
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
border-color: rgba(251, 191, 36, 0.3);
|
||||
}
|
||||
/* 到期提示(红色) */
|
||||
.expired-notice {
|
||||
background: rgba(239, 68, 68, 0.06);
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
.notice-action {
|
||||
flex-shrink: 0;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
margin-left: 8rpx;
|
||||
align-self: center;
|
||||
}
|
||||
.notice-icon {
|
||||
font-size: 32rpx;
|
||||
margin-right: 16rpx;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.notice-info {
|
||||
flex: 1;
|
||||
}
|
||||
.notice-title {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
.notice-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* 列表头 */
|
||||
.list-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 0 24rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
background: rgba(252, 252, 252, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
border-bottom: 1rpx solid rgba(0, 0, 0, 0.04);
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.list-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
.list-count {
|
||||
margin-left: 12rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
background: #F5F5F5;
|
||||
padding: 4rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 列表项 */
|
||||
.list-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
border-radius: 32rpx;
|
||||
border: 1rpx solid #F5F5F5;
|
||||
background: #FFF;
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
|
||||
margin-bottom: 16rpx;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.list-item:active {
|
||||
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.list-item.playing {
|
||||
background: rgba(255, 157, 66, 0.06);
|
||||
border-color: rgba(255, 157, 66, 0.15);
|
||||
}
|
||||
.list-item.locked {
|
||||
opacity: 0.45;
|
||||
filter: grayscale(0.3);
|
||||
}
|
||||
|
||||
.item-index {
|
||||
width: 72rpx;
|
||||
text-align: center;
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #DDD;
|
||||
font-style: italic;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.item-info {
|
||||
flex: 1;
|
||||
padding-right: 16rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.item-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
line-height: 1.4;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
.item-title.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.item-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 8rpx;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
.meta-dot {
|
||||
margin: 0 8rpx;
|
||||
}
|
||||
|
||||
.item-play-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
background: #F5F5F5;
|
||||
border: 1rpx solid #EEE;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.item-play-btn.active {
|
||||
background: var(--color-primary);
|
||||
border-color: transparent;
|
||||
box-shadow: 0 4rpx 16rpx rgba(255, 157, 66, 0.3);
|
||||
}
|
||||
.item-play-icon {
|
||||
width: 28rpx;
|
||||
height: 28rpx;
|
||||
}
|
||||
.item-play-btn.active .item-play-icon {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 频道广场 — 从后端获取分类 + 频道列表
|
||||
*
|
||||
* 性能优化:直接使用 channel list 返回的 hasSubscribed 字段,
|
||||
* 无需额外请求订阅列表,减少一次 HTTP 请求
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
isVip: false,
|
||||
categories: [],
|
||||
activeFilter: '',
|
||||
filteredDomains: [],
|
||||
loading: true
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this._loadCategories()
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._loadChannels()
|
||||
this._onSubChange = () => this._loadChannels()
|
||||
this._onVipChange = () => this._loadChannels()
|
||||
app.on('subscriptionChange', this._onSubChange)
|
||||
app.on('vipChange', this._onVipChange)
|
||||
},
|
||||
|
||||
onHide() {
|
||||
if (this._onSubChange) app.off('subscriptionChange', this._onSubChange)
|
||||
if (this._onVipChange) app.off('vipChange', this._onVipChange)
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载分类列表
|
||||
*/
|
||||
_loadCategories() {
|
||||
const self = this
|
||||
api.getCategoryList().then(function (res) {
|
||||
if (res.code === 200 && res.data) {
|
||||
const list = Array.isArray(res.data) ? res.data : (res.data.list || [])
|
||||
const categories = [{ id: '', name: '全部' }].concat(list)
|
||||
self.setData({ categories: categories })
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[Discover] 加载分类失败:', err)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 加载频道列表
|
||||
* 直接使用后端返回的 hasSubscribed 字段,无需单独请求订阅列表
|
||||
*/
|
||||
_loadChannels() {
|
||||
const self = this
|
||||
const gd = app.globalData
|
||||
const isFirstLoad = self.data.filteredDomains.length === 0
|
||||
if (isFirstLoad) self.setData({ loading: true })
|
||||
|
||||
api.getChannelList({ categoryId: self.data.activeFilter, current: 1, pageSize: 50 })
|
||||
.then(function (channelRes) {
|
||||
var channels = []
|
||||
if (channelRes.code === 200 && channelRes.data) {
|
||||
channels = channelRes.data.list || channelRes.data || []
|
||||
}
|
||||
|
||||
var filtered = channels.map(function (ch) {
|
||||
var isFree = ch.isFree === 1
|
||||
var isVipOnly = ch.isVipOnly === 1
|
||||
// 最低价(分→元)
|
||||
var lowestPrice = null
|
||||
if (!isFree && !isVipOnly) {
|
||||
var prices = [
|
||||
{ label: '包月', value: ch.monthlyPrice },
|
||||
{ label: '包季', value: ch.quarterlyPrice },
|
||||
{ label: '包年', value: ch.annualPrice }
|
||||
].filter(function (p) { return p.value > 0 })
|
||||
if (prices.length > 0) {
|
||||
prices.sort(function (a, b) { return a.value - b.value })
|
||||
lowestPrice = { label: prices[0].label, value: (prices[0].value / 100).toFixed(2) }
|
||||
}
|
||||
}
|
||||
return Object.assign({}, ch, {
|
||||
_isSubscribed: ch.hasSubscribed === 1,
|
||||
_isFree: isFree,
|
||||
_isVipOnly: isVipOnly,
|
||||
_lowestPrice: lowestPrice
|
||||
})
|
||||
})
|
||||
|
||||
self.setData({
|
||||
isVip: gd.isVip,
|
||||
filteredDomains: filtered,
|
||||
loading: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('[Discover] 加载频道失败:', err)
|
||||
self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换分类筛选
|
||||
*/
|
||||
onFilter(e) {
|
||||
this.setData({ activeFilter: e.currentTarget.dataset.cat })
|
||||
this._loadChannels()
|
||||
},
|
||||
|
||||
/**
|
||||
* 按鈕操作
|
||||
* 1. 已订阅 → 跳转频道详情
|
||||
* 2. isFree → 直接跳转频道详情(收听)
|
||||
* 3. isVipOnly → 引导去 VIP 页
|
||||
* 4. 付费订阅 → 跳转支付页
|
||||
*/
|
||||
onAction(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
|
||||
var channel = null
|
||||
for (var i = 0; i < this.data.filteredDomains.length; i++) {
|
||||
if (this.data.filteredDomains[i].id === id) {
|
||||
channel = this.data.filteredDomains[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!channel) return
|
||||
|
||||
// 已订阅 → 直接进详情页
|
||||
if (channel._isSubscribed) {
|
||||
wx.navigateTo({ url: '/pages/channel-detail/index?id=' + id })
|
||||
return
|
||||
}
|
||||
|
||||
// 免费 → 直接进详情收听
|
||||
if (channel._isFree) {
|
||||
wx.navigateTo({ url: '/pages/channel-detail/index?id=' + id })
|
||||
return
|
||||
}
|
||||
|
||||
// VIP专享 → 引导 VIP 页
|
||||
if (channel._isVipOnly) {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
return
|
||||
}
|
||||
|
||||
// 付费订阅 → 跳转 VIP/订阅页(channel 模式)
|
||||
var params = 'channelId=' + id
|
||||
+ '&channelName=' + encodeURIComponent(channel.name || '')
|
||||
+ '&monthlyPrice=' + (channel.monthlyPrice || 0)
|
||||
+ '&quarterlyPrice=' + (channel.quarterlyPrice || 0)
|
||||
+ '&annualPrice=' + (channel.annualPrice || 0)
|
||||
wx.navigateTo({ url: '/pages/vip/index?' + params })
|
||||
},
|
||||
|
||||
/**
|
||||
* 跳转频道详情
|
||||
*/
|
||||
goDetail(e) {
|
||||
wx.navigateTo({ url: '/pages/channel-detail/index?id=' + e.currentTarget.dataset.id })
|
||||
},
|
||||
|
||||
/**
|
||||
* 跳转VIP
|
||||
*/
|
||||
goVip() {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"global-player": "/components/global-player/index",
|
||||
"t-message": "tdesign-miniprogram/message/message",
|
||||
"t-image": "tdesign-miniprogram/image/image",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading"
|
||||
},
|
||||
"navigationBarTitleText": "频道广场"
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<!-- 频道广场 —— 后端频道列表 + 分类筛选 -->
|
||||
<page-meta page-style="overflow: hidden;" />
|
||||
<view class="discover-page">
|
||||
|
||||
<!-- 分类筛选(横向滚动) -->
|
||||
<scroll-view scroll-x enhanced show-scrollbar="{{false}}" class="filter-bar">
|
||||
<view
|
||||
wx:for="{{categories}}"
|
||||
wx:key="id"
|
||||
class="filter-tag {{activeFilter === item.id ? 'active' : ''}}"
|
||||
bindtap="onFilter"
|
||||
data-cat="{{item.id}}"
|
||||
>
|
||||
<text>{{item.name}}</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 频道网格(可滚动) -->
|
||||
<scroll-view scroll-y enhanced show-scrollbar="{{false}}" class="grid-scroll">
|
||||
|
||||
<!-- 加载中(骨架屏) -->
|
||||
<block wx:if="{{loading}}">
|
||||
<view class="grid">
|
||||
<view class="grid-item skeleton-item" wx:for="{{[1,2,3,4,5,6]}}" wx:key="*this">
|
||||
<view class="skele-circle skele-bg"></view>
|
||||
<view class="skele-line skele-bg"></view>
|
||||
<view class="skele-btn skele-bg"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<block wx:else>
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{filteredDomains.length === 0}}" class="empty-wrap">
|
||||
<t-empty description="暂无频道" />
|
||||
</view>
|
||||
|
||||
<!-- 频道网格 -->
|
||||
<view class="grid" wx:else>
|
||||
<view
|
||||
wx:for="{{filteredDomains}}"
|
||||
wx:key="id"
|
||||
class="grid-item"
|
||||
>
|
||||
<!-- 封面 emoji -->
|
||||
<view class="item-icon" style="background: {{item.bgColor || '#FFE8CC'}};" bindtap="goDetail" data-id="{{item.id}}">
|
||||
<text class="icon-emoji">{{item.cover || '📻'}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 名称 -->
|
||||
<text class="item-name" bindtap="goDetail" data-id="{{item.id}}">{{item.name}}</text>
|
||||
|
||||
<!-- 类型标签 -->
|
||||
<view class="item-tag-row">
|
||||
<t-tag wx:if="{{item._isFree}}" size="small" variant="light" theme="success">免费</t-tag>
|
||||
<t-tag wx:elif="{{item._isVipOnly}}" size="small" variant="light" theme="warning">VIP专享</t-tag>
|
||||
<t-tag wx:elif="{{item._lowestPrice}}" size="small" variant="light" theme="default">¥{{item._lowestPrice.value}}/{{item._lowestPrice.label}}</t-tag>
|
||||
</view>
|
||||
|
||||
<!-- 行动按钮 -->
|
||||
<!-- 已订阅 -->
|
||||
<view wx:if="{{item._isSubscribed}}" class="sub-btn subscribed" bindtap="onAction" data-id="{{item.id}}">
|
||||
<t-icon name="check-circle" size="28rpx" color="#999" />
|
||||
<text>已订阅</text>
|
||||
</view>
|
||||
|
||||
<!-- 免费 → 收听 -->
|
||||
<view wx:elif="{{item._isFree}}" class="sub-btn free" bindtap="onAction" data-id="{{item.id}}">
|
||||
<t-icon name="play-circle" size="28rpx" color="#FFF" />
|
||||
<text>收听</text>
|
||||
</view>
|
||||
|
||||
<!-- VIP专享 -->
|
||||
<view wx:elif="{{item._isVipOnly}}" class="sub-btn vip" bindtap="onAction" data-id="{{item.id}}">
|
||||
<text>👑 VIP专享</text>
|
||||
</view>
|
||||
|
||||
<!-- 付费订阅 -->
|
||||
<view wx:else class="sub-btn paid" bindtap="onAction" data-id="{{item.id}}">
|
||||
<t-icon name="shop" size="28rpx" color="#FFF" />
|
||||
<text>订阅</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 底部VIP横幅 -->
|
||||
<view class="vip-banner tap-active" wx:if="{{!isVip}}" bindtap="goVip">
|
||||
<view class="vip-banner-icon">
|
||||
<text>👑</text>
|
||||
</view>
|
||||
<view class="vip-banner-info">
|
||||
<text class="vip-banner-title">✨ 解锁全部频道 + 免广告</text>
|
||||
<text class="vip-banner-desc">立享极致畅听体验</text>
|
||||
</view>
|
||||
<view class="vip-banner-price">
|
||||
<text>19.9元/月</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 200rpx;"></view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 全局播放条 -->
|
||||
<global-player />
|
||||
<t-message id="t-message" />
|
||||
</view>
|
||||
@@ -0,0 +1,209 @@
|
||||
/* 频道广场样式 */
|
||||
::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
|
||||
|
||||
.discover-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #F6F6F6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 分类筛选 */
|
||||
.filter-bar {
|
||||
flex-shrink: 0;
|
||||
padding: 16rpx 32rpx;
|
||||
white-space: nowrap;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.filter-tag {
|
||||
display: inline-block;
|
||||
padding: 12rpx 28rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 24rpx;
|
||||
font-weight: 500;
|
||||
margin-right: 12rpx;
|
||||
background: #F5F5F5;
|
||||
color: #666;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.filter-tag.active {
|
||||
background: #1A1A1A;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
/* 网格滚动区 */
|
||||
.grid-scroll {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 20rpx 20rpx 0;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.grid-item {
|
||||
width: calc(33.33% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 24rpx 12rpx 20rpx;
|
||||
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
|
||||
.item-icon {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 14rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.icon-emoji { font-size: 44rpx; }
|
||||
|
||||
.item-name {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 10rpx;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 类型标签行 */
|
||||
.item-tag-row {
|
||||
margin-bottom: 14rpx;
|
||||
min-height: 36rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 行动按钮(通用容器) */
|
||||
.sub-btn {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
border-radius: 999rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
transition: opacity 0.2s, transform 0.15s;
|
||||
}
|
||||
.sub-btn:active { transform: scale(0.95); opacity: 0.85; }
|
||||
|
||||
/* 已订阅 */
|
||||
.sub-btn.subscribed {
|
||||
background: #F0F0F0;
|
||||
color: #999;
|
||||
}
|
||||
/* 免费收听 */
|
||||
.sub-btn.free {
|
||||
background: linear-gradient(135deg, #2ECC71, #27AE60);
|
||||
color: #FFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 200, 83, 0.25);
|
||||
}
|
||||
/* VIP专享 */
|
||||
.sub-btn.vip {
|
||||
background: linear-gradient(135deg, #F59E0B, #D97706);
|
||||
color: #FFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba(245, 158, 11, 0.3);
|
||||
}
|
||||
/* 付费订阅 */
|
||||
.sub-btn.paid {
|
||||
background: linear-gradient(135deg, #FF9D42, #FF7832);
|
||||
color: #FFF;
|
||||
box-shadow: 0 4rpx 12rpx rgba(255, 157, 66, 0.3);
|
||||
}
|
||||
|
||||
/* ========== 骨架屏 ========== */
|
||||
@keyframes skele-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.skele-bg {
|
||||
background: linear-gradient(90deg, #F0F0F0 25%, #E8E8E8 37%, #F0F0F0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: skele-shimmer 1.4s ease infinite;
|
||||
}
|
||||
.skeleton-item {
|
||||
background: #FFF;
|
||||
}
|
||||
.skele-circle {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 50%;
|
||||
margin-bottom: 14rpx;
|
||||
}
|
||||
.skele-line {
|
||||
width: 80%;
|
||||
height: 28rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.skele-btn {
|
||||
width: 100%;
|
||||
height: 60rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-wrap { padding: 80rpx 0; }
|
||||
|
||||
/* VIP横幅 */
|
||||
.vip-banner {
|
||||
margin: 32rpx 20rpx;
|
||||
padding: 28rpx;
|
||||
background: linear-gradient(135deg, #1F2937, #111827);
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
box-shadow: 0 12rpx 32rpx rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
.vip-banner-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(251, 191, 36, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
flex-shrink: 0;
|
||||
font-size: 36rpx;
|
||||
}
|
||||
.vip-banner-info { flex: 1; overflow: hidden; }
|
||||
.vip-banner-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #FDE68A;
|
||||
}
|
||||
.vip-banner-desc {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #9CA3AF;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.vip-banner-price {
|
||||
background: #FBBF24;
|
||||
color: #1F2937;
|
||||
padding: 12rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 收听历史 — 从后端获取历史列表
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
const util = require('../../utils/util')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
filter: 'all',
|
||||
cleared: false,
|
||||
historyList: [],
|
||||
isPlaying: false,
|
||||
loading: true
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._refresh()
|
||||
this._onPlayerChange = () => this._updatePlayState()
|
||||
app.on('playerStateChange', this._onPlayerChange)
|
||||
},
|
||||
|
||||
onHide() {
|
||||
if (this._onPlayerChange) app.off('playerStateChange', this._onPlayerChange)
|
||||
},
|
||||
|
||||
_refresh() {
|
||||
if (this.data.cleared) return
|
||||
|
||||
const self = this
|
||||
const gd = app.globalData
|
||||
|
||||
api.getHistoryList({ current: 1, pageSize: 30 }).then(function (res) {
|
||||
if (res.code === 200 && res.data) {
|
||||
var list = res.data.list || res.data || []
|
||||
|
||||
// 附带格式化信息
|
||||
list = list.map(function (item) {
|
||||
// 节目可能包含频道信息,根据后端返回结构适配
|
||||
var channel = item.channel || item.program && item.program.channel || {}
|
||||
var program = item.program || item
|
||||
|
||||
return Object.assign({}, program, {
|
||||
_domainName: channel.name || program.channelName || '',
|
||||
_icon: channel.icon || '🎵',
|
||||
_bgColor: channel.bgColor || '#F0F0F0',
|
||||
_coverUrl: (channel.cover && channel.cover.url) || channel.coverUrl || '',
|
||||
_friendlyDate: util.getFriendlyDate(
|
||||
program.createdAt ? program.createdAt.substring(0, 10) : ''
|
||||
),
|
||||
durationText: util.formatTime(program.duration || 0),
|
||||
_isThisPlaying: gd.activeContent && gd.activeContent.id === program.id
|
||||
})
|
||||
})
|
||||
|
||||
self.setData({ historyList: list, isPlaying: gd.isPlaying, loading: false })
|
||||
} else {
|
||||
self.setData({ historyList: [], loading: false })
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[History] 加载历史失败:', err)
|
||||
self.setData({ loading: false })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 仅更新播放状态
|
||||
*/
|
||||
_updatePlayState() {
|
||||
var gd = app.globalData
|
||||
var list = this.data.historyList.map(function (item) {
|
||||
return Object.assign({}, item, {
|
||||
_isThisPlaying: gd.activeContent && gd.activeContent.id === item.id
|
||||
})
|
||||
})
|
||||
this.setData({ historyList: list, isPlaying: gd.isPlaying })
|
||||
},
|
||||
|
||||
setFilter(e) {
|
||||
this.setData({ filter: e.currentTarget.dataset.val })
|
||||
},
|
||||
|
||||
onPlay(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const gd = app.globalData
|
||||
|
||||
// 从已加载数据中查找
|
||||
var content = null
|
||||
for (var i = 0; i < this.data.historyList.length; i++) {
|
||||
if (this.data.historyList[i].id === id) {
|
||||
content = this.data.historyList[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!content) return
|
||||
|
||||
if (gd.activeContent && gd.activeContent.id === id) {
|
||||
app.togglePlay()
|
||||
} else {
|
||||
app.playContent(content)
|
||||
}
|
||||
},
|
||||
|
||||
onClear() {
|
||||
const self = this
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '确定要清空所有收听历史吗?',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
self.setData({ cleared: true, historyList: [] })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"global-player": "/components/global-player/index"
|
||||
},
|
||||
"navigationBarTitleText": "收听历史"
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<!-- 收听历史 —— 按日期倒序的已听列表 -->
|
||||
<view class="history-page">
|
||||
|
||||
<!-- 筛选Tab + 清空按钮 -->
|
||||
<view class="filter-header">
|
||||
<view class="filter-tabs">
|
||||
<text
|
||||
class="tab {{filter === 'all' ? 'active' : ''}}"
|
||||
bindtap="setFilter"
|
||||
data-val="all"
|
||||
>全部片段</text>
|
||||
<text
|
||||
class="tab {{filter === 'subscribed' ? 'active' : ''}}"
|
||||
bindtap="setFilter"
|
||||
data-val="subscribed"
|
||||
>仅看已订阅</text>
|
||||
</view>
|
||||
<view class="clear-btn tap-active" bindtap="onClear">
|
||||
<text class="clear-icon">🗑</text>
|
||||
<text class="clear-text">清空</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 历史列表 -->
|
||||
<view class="list-area">
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{cleared || historyList.length === 0}}" class="empty-state">
|
||||
<view class="empty-icon-wrap">
|
||||
<text class="empty-emoji">📭</text>
|
||||
</view>
|
||||
<text class="empty-text">暂无收听历史</text>
|
||||
</view>
|
||||
|
||||
<!-- 历史条目 -->
|
||||
<view
|
||||
wx:for="{{historyList}}"
|
||||
wx:key="id"
|
||||
class="history-item card"
|
||||
bindtap="onPlay"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<!-- 频道图标 -->
|
||||
<view class="h-icon" style="background: {{item._bgColor}};">
|
||||
<image wx:if="{{item._coverUrl}}" src="{{item._coverUrl}}" class="h-cover-img" mode="aspectFill" />
|
||||
<text wx:else class="h-emoji">{{item._icon}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 信息 -->
|
||||
<view class="h-info">
|
||||
<text class="h-channel">{{item._domainName}}</text>
|
||||
<text class="h-title {{item._isThisPlaying ? 'text-primary' : ''}}">{{item.title}}</text>
|
||||
<text class="h-meta">{{item._friendlyDate}} · {{item.durationText}}</text>
|
||||
</view>
|
||||
|
||||
<!-- 播放指示 -->
|
||||
<view wx:if="{{item._isThisPlaying && isPlaying}}" class="playing-indicator">
|
||||
<view class="bar bar-1"></view>
|
||||
<view class="bar bar-2"></view>
|
||||
<view class="bar bar-3"></view>
|
||||
</view>
|
||||
<view wx:else class="play-mini">
|
||||
<image src="/assets/icons/play.svg" class="play-mini-icon" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
<view style="height: 200rpx;"></view>
|
||||
|
||||
<global-player />
|
||||
</view>
|
||||
@@ -0,0 +1,177 @@
|
||||
/* 收听历史样式 */
|
||||
|
||||
.history-page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* 筛选头部 */
|
||||
.filter-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16rpx 32rpx;
|
||||
background: #FFFFFF;
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.clear-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.clear-icon {
|
||||
font-size: 22rpx;
|
||||
margin-right: 6rpx;
|
||||
}
|
||||
.clear-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 筛选Tab */
|
||||
.filter-tabs {
|
||||
display: flex;
|
||||
gap: 32rpx;
|
||||
}
|
||||
.tab {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
padding-bottom: 12rpx;
|
||||
border-bottom: 4rpx solid transparent;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.tab.active {
|
||||
color: #333;
|
||||
border-bottom-color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 列表 */
|
||||
.list-area {
|
||||
padding: 20rpx 32rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 160rpx 0;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.empty-icon-wrap {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F5F5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.empty-emoji {
|
||||
font-size: 48rpx;
|
||||
}
|
||||
.empty-text {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 历史条目 */
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.history-item:active {
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.h-icon {
|
||||
width: 88rpx;
|
||||
height: 88rpx;
|
||||
border-radius: 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.h-emoji {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
.h-info {
|
||||
flex: 1;
|
||||
padding: 0 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.h-channel {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
color: #999;
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.h-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.h-title.text-primary {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
.h-meta {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #BBB;
|
||||
margin-top: 6rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 播放指示器 */
|
||||
.playing-indicator {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 4rpx;
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bar {
|
||||
width: 6rpx;
|
||||
border-radius: 4rpx;
|
||||
background: var(--color-primary);
|
||||
animation: bounce 0.6s ease-in-out infinite;
|
||||
}
|
||||
.bar-1 { height: 24rpx; animation-delay: 0s; }
|
||||
.bar-2 { height: 40rpx; animation-delay: 0.1s; }
|
||||
.bar-3 { height: 16rpx; animation-delay: 0.2s; }
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 100% { transform: scaleY(0.4); }
|
||||
50% { transform: scaleY(1); }
|
||||
}
|
||||
|
||||
.play-mini {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid #EEE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.play-mini-icon {
|
||||
width: 20rpx;
|
||||
height: 20rpx;
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
/**
|
||||
* 首页 — 订阅频道 + 免费频道
|
||||
*
|
||||
* 数据来源:
|
||||
* - 订阅列表: POST /radio/subscription/list
|
||||
* - 免费频道: POST /radio/channel/freeList
|
||||
*
|
||||
* 订阅返回结构: { data: { list: [{ id, channel: { id, name, cover, Programs[] } }] } }
|
||||
* 免费频道结构: { data: { list: [{ id, name, cover, Programs[] }] } } (待确认类似)
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
const util = require('../../utils/util')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
greetingSub: '',
|
||||
locationName: '',
|
||||
weather: null,
|
||||
dateDisplay: '',
|
||||
weekDay: '',
|
||||
subscribedData: [],
|
||||
freeChannels: [],
|
||||
isPlaying: false,
|
||||
isVip: false,
|
||||
statusBarHeight: 0,
|
||||
loadingSub: true,
|
||||
loadingFree: true
|
||||
},
|
||||
|
||||
|
||||
|
||||
onShow() {
|
||||
const gd = app.globalData
|
||||
this.setData({
|
||||
greetingSub: this._getGreeting(),
|
||||
dateDisplay: util.getDateDisplay(),
|
||||
weekDay: util.getWeekDay(),
|
||||
locationName: gd.locationName || '',
|
||||
weather: gd.weather || null,
|
||||
isVip: gd.isVip || false,
|
||||
statusBarHeight: gd.statusBarHeight || 0
|
||||
})
|
||||
this._loadAll()
|
||||
this._bindEvents()
|
||||
},
|
||||
|
||||
onHide() {
|
||||
this._unbindEvents()
|
||||
},
|
||||
|
||||
// ===================== 数据加载 =====================
|
||||
|
||||
/** 并行加载订阅列表 + 免费频道 */
|
||||
_loadAll() {
|
||||
this._refreshSubscriptions()
|
||||
this._loadFreeChannels()
|
||||
},
|
||||
|
||||
/**
|
||||
* 拉取已订阅频道列表
|
||||
* 首次无数据时显示骨架屏,后续刷新静默替换(无闪烁)
|
||||
*/
|
||||
_refreshSubscriptions() {
|
||||
const self = this
|
||||
const gd = app.globalData
|
||||
const isFirstLoad = self.data.subscribedData.length === 0
|
||||
|
||||
// 只有首次加载才显示骨架屏
|
||||
if (isFirstLoad) {
|
||||
self.setData({ loadingSub: true })
|
||||
}
|
||||
|
||||
api.getSubscriptionList({ current: 1, pageSize: 50 })
|
||||
.then(function (res) {
|
||||
if (res.code !== 200 || !res.data) {
|
||||
self.setData({ subscribedData: [], loadingSub: false })
|
||||
return
|
||||
}
|
||||
|
||||
const subList = res.data.list || []
|
||||
const subscribedData = subList.map(function (subItem) {
|
||||
return self._mapChannel(subItem.channel || {}, gd)
|
||||
})
|
||||
|
||||
self.setData({
|
||||
subscribedData: subscribedData,
|
||||
isPlaying: gd.isPlaying || false,
|
||||
loadingSub: false
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('[首页] 订阅列表失败', err)
|
||||
self.setData({ loadingSub: false })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 拉取免费频道列表
|
||||
* 首次无数据时显示骨架屏,后续静默刷新
|
||||
*/
|
||||
_loadFreeChannels() {
|
||||
const self = this
|
||||
const isFirstLoad = self.data.freeChannels.length === 0
|
||||
|
||||
if (isFirstLoad) {
|
||||
self.setData({ loadingFree: true })
|
||||
}
|
||||
|
||||
api.getFreeChannelList({ current: 1, pageSize: 20 })
|
||||
.then(function (res) {
|
||||
if (res.code !== 200 || !res.data) {
|
||||
self.setData({ freeChannels: [], loadingFree: false })
|
||||
return
|
||||
}
|
||||
|
||||
const list = res.data.list || []
|
||||
|
||||
const freeChannels = list.map(function (ch) {
|
||||
return {
|
||||
id: ch.id,
|
||||
name: ch.name || '未命名',
|
||||
// cover 直接是 emoji 字符串
|
||||
cover: ch.cover || '📻',
|
||||
bgColor: self._genColor(ch.id),
|
||||
programCount: (ch.Programs || ch.programs || []).length,
|
||||
isFree: ch.isFree
|
||||
}
|
||||
})
|
||||
|
||||
self.setData({ freeChannels: freeChannels, loadingFree: false })
|
||||
})
|
||||
.catch(function (err) {
|
||||
console.error('[首页] 免费频道失败', err)
|
||||
self.setData({ loadingFree: false })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 将频道对象映射为 UI 数据结构(订阅区通用)
|
||||
*/
|
||||
_mapChannel(channel, gd) {
|
||||
const programs = channel.programs || []
|
||||
const latest = programs.length > 0 ? programs[0] : null
|
||||
// cover 直接是 emoji 字符串
|
||||
const cover = channel.cover || '📻'
|
||||
const todayContent = latest ? Object.assign({}, latest, {
|
||||
durationText: util.formatTime(latest.duration || 0)
|
||||
}) : null
|
||||
const isThisPlaying = !!(todayContent &&
|
||||
gd.activeContent &&
|
||||
gd.activeContent.id === todayContent.id)
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name || '未命名频道',
|
||||
description: channel.description || '',
|
||||
isFree: channel.isFree,
|
||||
isVipOnly: channel.isVipOnly,
|
||||
cover: cover,
|
||||
bgColor: this._genColor(channel.id),
|
||||
_todayContent: todayContent,
|
||||
_isThisPlaying: isThisPlaying
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 根据 id 生成稳定的暖色系颜色
|
||||
*/
|
||||
_genColor(id) {
|
||||
const palette = [
|
||||
'#FF9D42', '#FFB366', '#FF8C69', '#FFA07A',
|
||||
'#E8956D', '#D4845A', '#F4A460', '#CD853F'
|
||||
]
|
||||
if (!id) return palette[0]
|
||||
var hash = 0
|
||||
for (var i = 0; i < id.length; i++) {
|
||||
hash = (hash + id.charCodeAt(i)) % palette.length
|
||||
}
|
||||
return palette[hash]
|
||||
},
|
||||
|
||||
// ===================== 事件监听 =====================
|
||||
|
||||
_bindEvents() {
|
||||
const self = this
|
||||
|
||||
// 播放状态变化
|
||||
this._onPlayerChange = function () {
|
||||
const gd = app.globalData
|
||||
const data = self.data.subscribedData.map(function (channel) {
|
||||
const latest = channel._todayContent
|
||||
return Object.assign({}, channel, {
|
||||
_isThisPlaying: !!(latest &&
|
||||
gd.activeContent &&
|
||||
gd.activeContent.id === latest.id)
|
||||
})
|
||||
})
|
||||
self.setData({ subscribedData: data, isPlaying: gd.isPlaying || false })
|
||||
}
|
||||
|
||||
// 订阅变化
|
||||
this._onSubChange = function () {
|
||||
self._loadAll()
|
||||
}
|
||||
|
||||
// 冷启动位置/天气数据就绪(异步,可能比页面加载晚)
|
||||
this._onLocationWeather = function (data) {
|
||||
self.setData({
|
||||
locationName: data.locationName || '',
|
||||
weather: data.weather || null
|
||||
})
|
||||
}
|
||||
|
||||
app.on('playerStateChange', this._onPlayerChange)
|
||||
app.on('subscriptionChange', this._onSubChange)
|
||||
app.on('locationWeatherReady', this._onLocationWeather)
|
||||
},
|
||||
|
||||
_unbindEvents() {
|
||||
if (this._onPlayerChange) app.off('playerStateChange', this._onPlayerChange)
|
||||
if (this._onSubChange) app.off('subscriptionChange', this._onSubChange)
|
||||
if (this._onLocationWeather) app.off('locationWeatherReady', this._onLocationWeather)
|
||||
},
|
||||
|
||||
// ===================== 用户操作 =====================
|
||||
|
||||
/** 点击播放/暂停 */
|
||||
onPlayContent(e) {
|
||||
const contentId = e.currentTarget.dataset.contentId
|
||||
const gd = app.globalData
|
||||
|
||||
var content = null
|
||||
for (var i = 0; i < this.data.subscribedData.length; i++) {
|
||||
var c = this.data.subscribedData[i]._todayContent
|
||||
if (c && c.id === contentId) { content = c; break }
|
||||
}
|
||||
if (!content) return
|
||||
|
||||
if (gd.activeContent && gd.activeContent.id === contentId) {
|
||||
app.togglePlay()
|
||||
} else {
|
||||
app.playContent(content)
|
||||
}
|
||||
},
|
||||
|
||||
/** 跳转发现广场 */
|
||||
goDiscover() {
|
||||
wx.switchTab({ url: '/pages/discover/index' })
|
||||
},
|
||||
|
||||
/** 跳转 VIP 页 */
|
||||
goVip() {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
},
|
||||
|
||||
/** 跳转频道详情(订阅或免费) */
|
||||
goChannel(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
wx.navigateTo({ url: '/pages/channel-detail/index?id=' + id })
|
||||
},
|
||||
|
||||
// ===================== 工具方法 =====================
|
||||
|
||||
/** 根据时间段返回问候语 */
|
||||
_getGreeting() {
|
||||
const hour = new Date().getHours()
|
||||
var pool
|
||||
if (hour >= 5 && hour < 9) {
|
||||
pool = [
|
||||
'新的一天,从声音开始',
|
||||
'早起的人,先听一段',
|
||||
'清晨的第一居声,属于你',
|
||||
'晚起不如早起,早起不如听起'
|
||||
]
|
||||
} else if (hour >= 9 && hour < 12) {
|
||||
pool = [
|
||||
'上午充能量,声音加持',
|
||||
'专注工作,也别忘了呼吸',
|
||||
'高效早上,入耳知识',
|
||||
'好状态,从一段内容开始'
|
||||
]
|
||||
} else if (hour >= 12 && hour < 14) {
|
||||
pool = [
|
||||
'午后小憩,听点轻松的',
|
||||
'借耳机隔绝喧嚣,中午也有自己的时间',
|
||||
'饭后十分钟,充电一下',
|
||||
'慢下来,下午还长'
|
||||
]
|
||||
} else if (hour >= 14 && hour < 17) {
|
||||
pool = [
|
||||
'下午了,来点声音撤个困',
|
||||
'下午三点,刚好开始听一段',
|
||||
'下午的阳光和一段好内容',
|
||||
'止止广告,喝口内容'
|
||||
]
|
||||
} else if (hour >= 17 && hour < 19) {
|
||||
pool = [
|
||||
'日落时分,放慢脚步',
|
||||
'下班了,耳机里换一个频道',
|
||||
'偶尔不刷短视频,试试听点实的',
|
||||
'归途中最适合听一段'
|
||||
]
|
||||
} else if (hour >= 19 && hour < 22) {
|
||||
pool = [
|
||||
'夜晚温柔,适合倾听',
|
||||
'夜晚的声音,不用赶时间',
|
||||
'放下手机,喂口内容',
|
||||
'夹着夜色,听一段值得的'
|
||||
]
|
||||
} else {
|
||||
pool = [
|
||||
'夜深了,听点帮助入眠的',
|
||||
'出发前最后一段,晚安',
|
||||
'夜奥的小时光,留给自己',
|
||||
'好梦将至,晚安'
|
||||
]
|
||||
}
|
||||
return pool[Math.floor(Math.random() * pool.length)]
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"global-player": "/components/global-player/index",
|
||||
"t-message": "tdesign-miniprogram/message/message",
|
||||
"t-image": "tdesign-miniprogram/image/image",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag",
|
||||
"t-empty": "tdesign-miniprogram/empty/empty",
|
||||
"t-divider": "tdesign-miniprogram/divider/divider",
|
||||
"t-loading": "tdesign-miniprogram/loading/loading",
|
||||
"t-cell": "tdesign-miniprogram/cell/cell",
|
||||
"t-badge": "tdesign-miniprogram/badge/badge"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
<!-- 禁用页面原生滚动 -->
|
||||
<page-meta page-style="overflow: hidden;" />
|
||||
|
||||
<!-- 整体容器:flex 纵向布局 -->
|
||||
<view class="index-page">
|
||||
|
||||
<!-- 状态栏占位 -->
|
||||
<view style="height: {{statusBarHeight}}px; flex-shrink: 0; background: #FCFCFC;"></view>
|
||||
|
||||
<!-- ===== 自定义导航栏 ===== -->
|
||||
<view class="custom-nav">
|
||||
<text class="nav-brand-name">全声汇</text>
|
||||
</view>
|
||||
|
||||
<!-- ===== 顶部问候栏(吸顶) ===== -->
|
||||
<view class="meta-bar">
|
||||
<!-- 时段文案(根据时间变化) -->
|
||||
<text class="greeting-sub">{{greetingSub}}</text>
|
||||
<!-- 信息行 -->
|
||||
<view class="info-row">
|
||||
<view class="info-item" wx:if="{{locationName}}">
|
||||
<t-icon name="location" size="26rpx" color="#FF9D42" />
|
||||
<text class="info-text loc-name-text">{{locationName}}</text>
|
||||
</view>
|
||||
<text class="info-dot" wx:if="{{locationName}}">·</text>
|
||||
<text class="info-text">{{dateDisplay}} {{weekDay}}</text>
|
||||
<block wx:if="{{weather}}">
|
||||
<text class="info-dot">·</text>
|
||||
<text class="weather-icon-sm">{{weather.icon}}</text>
|
||||
<text class="info-text">{{weather.desc}} {{weather.temp}}°C</text>
|
||||
</block>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- ===== 可滚动内容区 ===== -->
|
||||
<scroll-view
|
||||
scroll-y
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
class="main-scroll"
|
||||
>
|
||||
<view class="content-area">
|
||||
|
||||
<!-- ─── Section 1: 我的订阅 ─── -->
|
||||
<view class="section-header">
|
||||
<view class="section-title-wrap">
|
||||
<text class="section-dot"></text>
|
||||
<text class="section-title">我的订阅</text>
|
||||
</view>
|
||||
<view class="section-action tap-active" bindtap="goDiscover">
|
||||
<text class="section-action-text">管理</text>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#CCC" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订阅加载中 (骨架屏) -->
|
||||
<block wx:if="{{loadingSub}}">
|
||||
<view class="skeleton-sub-card card" wx:for="{{[1,2]}}" wx:key="*this">
|
||||
<view class="skele-header">
|
||||
<view class="skele-avatar skele-bg"></view>
|
||||
<view class="skele-text-group">
|
||||
<view class="skele-title skele-bg"></view>
|
||||
<view class="skele-subtitle skele-bg"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view class="skele-play-row skele-bg"></view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 订阅空状态 -->
|
||||
<view wx:elif="{{subscribedData.length === 0}}" class="sub-empty-card" bindtap="goDiscover">
|
||||
<view class="sub-empty-icon">
|
||||
<text class="sub-empty-emoji">🎤</text>
|
||||
</view>
|
||||
<view class="sub-empty-body">
|
||||
<text class="sub-empty-title">还没有订阅</text>
|
||||
<text class="sub-empty-desc">去发现感兴趣的频道,每天为你准时送达</text>
|
||||
</view>
|
||||
<view class="sub-empty-arrow">
|
||||
<t-icon name="chevron-right" size="36rpx" color="#CCC" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订阅列表 -->
|
||||
<block wx:else>
|
||||
<view
|
||||
wx:for="{{subscribedData}}"
|
||||
wx:key="id"
|
||||
class="channel-card card"
|
||||
>
|
||||
<!-- 左侧色条 -->
|
||||
<view class="card-accent" style="background: {{item.bgColor}};"></view>
|
||||
|
||||
<!-- 频道头部 -->
|
||||
<view class="card-header" bindtap="goChannel" data-id="{{item.id}}">
|
||||
<view class="channel-icon" style="background: {{item.bgColor || '#FFE8CC'}};">
|
||||
<text class="icon-emoji">{{item.cover || '📻'}}</text>
|
||||
</view>
|
||||
<view class="channel-info">
|
||||
<text class="channel-name">{{item.name}}</text>
|
||||
<view class="channel-meta">
|
||||
<t-tag wx:if="{{item.isFree === 1}}" size="small" variant="light" theme="success">免费</t-tag>
|
||||
<t-tag wx:elif="{{item.isVipOnly === 1}}" size="small" variant="light" theme="warning">👑 VIP</t-tag>
|
||||
<t-tag wx:if="{{item._todayContent}}" size="small" variant="light" theme="primary">新内容</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
<t-icon name="chevron-right" size="40rpx" color="#CCC" />
|
||||
</view>
|
||||
|
||||
<!-- 最新节目播放行 -->
|
||||
<view
|
||||
wx:if="{{item._todayContent}}"
|
||||
class="play-row {{item._isThisPlaying ? 'playing' : ''}}"
|
||||
bindtap="onPlayContent"
|
||||
data-content-id="{{item._todayContent.id}}"
|
||||
>
|
||||
<!-- 左:音波动画 or 音乐图标 -->
|
||||
<view class="play-left">
|
||||
<view wx:if="{{item._isThisPlaying && isPlaying}}" class="sound-wave">
|
||||
<view class="wave-bar wave-1"></view>
|
||||
<view class="wave-bar wave-2"></view>
|
||||
<view class="wave-bar wave-3"></view>
|
||||
<view class="wave-bar wave-4"></view>
|
||||
</view>
|
||||
<t-icon wx:else name="sound" size="36rpx" color="{{item._isThisPlaying ? '#FF9D42' : '#BBB'}}" />
|
||||
</view>
|
||||
<!-- 中:标题 + 时长 -->
|
||||
<view class="play-info">
|
||||
<text class="play-title {{item._isThisPlaying ? 'text-primary' : ''}}">{{item._todayContent.title}}</text>
|
||||
<text class="play-duration">{{item._todayContent.durationText}}</text>
|
||||
</view>
|
||||
<!-- 右:播放按钮 -->
|
||||
<view class="play-btn-wrap">
|
||||
<view class="play-btn-ring {{item._isThisPlaying ? 'active' : ''}}"></view>
|
||||
<view class="play-btn {{item._isThisPlaying ? 'active' : ''}}">
|
||||
<t-icon wx:if="{{item._isThisPlaying && isPlaying}}" name="pause" size="32rpx" color="#FFF" />
|
||||
<t-icon wx:else name="play" size="32rpx" color="{{item._isThisPlaying ? '#FFF' : '#555'}}" />
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 该频道暂无节目 -->
|
||||
<view wx:else class="no-content">
|
||||
<text class="no-content-text">暂无节目,敬请期待</text>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ─── Section 2: 免费频道 ─── -->
|
||||
<t-divider style="margin: 12rpx 0;" />
|
||||
|
||||
<view class="section-header section-header-free">
|
||||
<view class="section-title-wrap">
|
||||
<text class="section-dot dot-free"></text>
|
||||
<text class="section-title">免费频道</text>
|
||||
<text class="section-subtitle">无需订阅,随时收听</text>
|
||||
</view>
|
||||
<view class="section-action tap-active" bindtap="goDiscover">
|
||||
<text class="section-action-text">全部</text>
|
||||
<t-icon name="chevron-right" size="32rpx" color="#CCC" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 免费频道加载中 (骨架屏) -->
|
||||
<block wx:if="{{loadingFree}}">
|
||||
<view class="free-list" style="padding: 4rpx 32rpx 16rpx;">
|
||||
<view class="skeleton-free-card" wx:for="{{[1,2,3,4]}}" wx:key="*this">
|
||||
<view class="skele-free-avatar skele-bg"></view>
|
||||
<view class="skele-free-text skele-bg"></view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- 免费频道横向滚动列表 -->
|
||||
<block wx:if="{{!loadingFree && freeChannels.length > 0}}">
|
||||
<scroll-view
|
||||
scroll-x
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
class="free-scroll"
|
||||
>
|
||||
<view class="free-list">
|
||||
<view
|
||||
wx:for="{{freeChannels}}"
|
||||
wx:key="id"
|
||||
class="free-card tap-active"
|
||||
bindtap="goChannel"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="free-icon" style="background: {{item.bgColor || '#FFE8CC'}};">
|
||||
<text class="free-emoji">{{item.cover || '📻'}}</text>
|
||||
</view>
|
||||
<text class="free-name">{{item.name}}</text>
|
||||
<t-tag size="small" variant="light-outline" theme="success">免费</t-tag>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</block>
|
||||
|
||||
<!-- 免费区无数据 -->
|
||||
<view wx:if="{{!loadingFree && freeChannels.length === 0}}" class="free-empty">
|
||||
<t-empty description="暂无免费频道" />
|
||||
</view>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<view style="height: 200rpx;"></view>
|
||||
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 全局播放条 -->
|
||||
<global-player />
|
||||
<t-message id="t-message" />
|
||||
</view>
|
||||
@@ -0,0 +1,496 @@
|
||||
/* ============================================
|
||||
首页样式 — 订阅频道 + 免费频道
|
||||
============================================ */
|
||||
/* 强制隐藏本页所有滚动条 */
|
||||
::-webkit-scrollbar {
|
||||
display: none !important;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
|
||||
/* 整体容器:flex 纵向,meta-bar 固定 + scroll 填充 */
|
||||
.index-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== 自定义导航栏 ========== */
|
||||
.custom-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 80rpx;
|
||||
background: #FFFAF5;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.nav-brand-name {
|
||||
font-size: 34rpx;
|
||||
font-weight: 600;
|
||||
font-family: 'PingFang SC', 'Helvetica Neue', sans-serif;
|
||||
color: #2C1A08;
|
||||
letter-spacing: 4rpx;
|
||||
}
|
||||
|
||||
/* ========== 顶部问候栏(吸顶) ========== */
|
||||
.meta-bar {
|
||||
flex-shrink: 0;
|
||||
background: linear-gradient(180deg, #FFFAF5 0%, #FFFFFF 100%);
|
||||
padding: 28rpx 36rpx 22rpx;
|
||||
border-bottom: 1rpx solid #F0ECE8;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
/* 问候语行:emoji + 文字 */
|
||||
.greeting-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.greeting-emoji {
|
||||
font-size: 52rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.greeting-text-wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.greeting {
|
||||
font-size: 48rpx;
|
||||
font-weight: 800;
|
||||
color: #2A2A2A;
|
||||
letter-spacing: 2rpx;
|
||||
line-height: 1.15;
|
||||
font-family: 'PingFang SC', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
.greeting-sub {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
color: #5C3D1E;
|
||||
letter-spacing: 1rpx;
|
||||
margin-bottom: 12rpx;
|
||||
font-family: 'PingFang SC', -apple-system, sans-serif;
|
||||
}
|
||||
|
||||
/* 信息行:位置 · 日期 · 天气 */
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8rpx;
|
||||
}
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6rpx;
|
||||
background: rgba(255, 157, 66, 0.08);
|
||||
padding: 6rpx 16rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.loc-name-text {
|
||||
font-weight: 600;
|
||||
color: #8B6914;
|
||||
}
|
||||
.info-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
.info-dot {
|
||||
font-size: 22rpx;
|
||||
color: #D5D0CA;
|
||||
margin: 0 2rpx;
|
||||
}
|
||||
.weather-icon-sm { font-size: 22rpx; }
|
||||
|
||||
/* ========== 可滚动内容区 ========== */
|
||||
.main-scroll {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ========== 主内容区 ========== */
|
||||
.content-area {
|
||||
padding: 24rpx 32rpx 0;
|
||||
}
|
||||
|
||||
|
||||
/* ========== Section Header ========== */
|
||||
.section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.section-header-free {
|
||||
margin-top: 40rpx;
|
||||
}
|
||||
.section-title-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.section-dot {
|
||||
width: 8rpx;
|
||||
height: 28rpx;
|
||||
border-radius: 999rpx;
|
||||
background: var(--color-primary);
|
||||
display: block;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.dot-free {
|
||||
background: #00C853;
|
||||
}
|
||||
.section-title {
|
||||
font-size: 32rpx;
|
||||
font-weight: 800;
|
||||
color: #1A1A1A;
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.section-subtitle {
|
||||
font-size: 22rpx;
|
||||
color: #AAAAAA;
|
||||
font-weight: 400;
|
||||
}
|
||||
.section-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4rpx;
|
||||
}
|
||||
.section-action-text {
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
.section-action-arrow {
|
||||
font-size: 32rpx;
|
||||
color: #CCC;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* ========== 骨架屏动画 ========== */
|
||||
@keyframes skele-shimmer {
|
||||
0% { background-position: -200% 0; }
|
||||
100% { background-position: 200% 0; }
|
||||
}
|
||||
.skele-bg {
|
||||
background: linear-gradient(90deg, #F0F0F0 25%, #E8E8E8 37%, #F0F0F0 63%);
|
||||
background-size: 400% 100%;
|
||||
animation: skele-shimmer 1.4s ease infinite;
|
||||
}
|
||||
|
||||
/* 订阅卡片骨架 */
|
||||
.skeleton-sub-card {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
.skele-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
.skele-avatar {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 22rpx;
|
||||
margin-right: 18rpx;
|
||||
}
|
||||
.skele-text-group { flex: 1; }
|
||||
.skele-title {
|
||||
width: 40%;
|
||||
height: 32rpx;
|
||||
border-radius: 6rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.skele-subtitle {
|
||||
width: 25%;
|
||||
height: 24rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
.skele-play-row {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
/* ========== 订阅空状态(横向卡片样式) ========== */
|
||||
.sub-empty-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20rpx;
|
||||
background: #FFFFFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx 28rpx;
|
||||
margin-bottom: 20rpx;
|
||||
border: 2rpx dashed #EDE8E2;
|
||||
box-shadow: none;
|
||||
}
|
||||
.sub-empty-card:active { opacity: 0.75; }
|
||||
.sub-empty-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 24rpx;
|
||||
background: linear-gradient(135deg, rgba(255, 157, 66, 0.12), rgba(255, 120, 50, 0.2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.sub-empty-emoji { font-size: 38rpx; }
|
||||
.sub-empty-body { flex: 1; min-width: 0; }
|
||||
.sub-empty-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.sub-empty-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #AAA;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.sub-empty-arrow { flex-shrink: 0; }
|
||||
|
||||
/* ========== 订阅频道卡片 ========== */
|
||||
.channel-card {
|
||||
margin-bottom: 20rpx;
|
||||
padding: 28rpx 28rpx 24rpx;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card-accent {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 8rpx;
|
||||
border-radius: 0 6rpx 6rpx 0;
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 12rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.channel-icon {
|
||||
width: 76rpx;
|
||||
height: 76rpx;
|
||||
border-radius: 22rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 18rpx;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.icon-emoji { font-size: 36rpx; }
|
||||
|
||||
.channel-info { flex: 1; min-width: 0; }
|
||||
.channel-name {
|
||||
display: block;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.channel-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8rpx;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
/* t-tag 和 t-icon 已替代旧 .tag / .card-arrow */
|
||||
|
||||
/* 播放行 */
|
||||
.play-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx 18rpx 20rpx 16rpx;
|
||||
border-radius: 24rpx;
|
||||
background: #F7F7F7;
|
||||
transition: all 0.3s ease;
|
||||
gap: 16rpx;
|
||||
}
|
||||
.play-row:active { background: #EEEEEE; }
|
||||
.play-row.playing {
|
||||
background: linear-gradient(135deg, rgba(255, 157, 66, 0.08) 0%, rgba(255, 120, 50, 0.15) 100%);
|
||||
border: 1rpx solid rgba(255, 157, 66, 0.15);
|
||||
}
|
||||
|
||||
/* 左侧:音波 or 图标 */
|
||||
.play-left {
|
||||
width: 48rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 音波动画 */
|
||||
.sound-wave {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 5rpx;
|
||||
height: 36rpx;
|
||||
}
|
||||
.wave-bar {
|
||||
width: 6rpx;
|
||||
border-radius: 6rpx;
|
||||
background: var(--color-primary);
|
||||
animation: wave-bounce 0.8s ease-in-out infinite;
|
||||
}
|
||||
.wave-1 { height: 14rpx; animation-delay: 0s; }
|
||||
.wave-2 { height: 26rpx; animation-delay: 0.15s; }
|
||||
.wave-3 { height: 20rpx; animation-delay: 0.3s; }
|
||||
.wave-4 { height: 30rpx; animation-delay: 0.45s; }
|
||||
|
||||
@keyframes wave-bounce {
|
||||
0%, 100% { transform: scaleY(0.4); opacity: 0.6; }
|
||||
50% { transform: scaleY(1); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 中间信息 */
|
||||
.play-info { flex: 1; padding-right: 12rpx; overflow: hidden; }
|
||||
.play-title {
|
||||
display: block;
|
||||
font-size: 27rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.play-title.text-primary { color: var(--color-primary); }
|
||||
.play-duration { display: block; font-size: 21rpx; color: #AAA; margin-top: 6rpx; }
|
||||
|
||||
/* 右侧:播放按钮 + 光环 */
|
||||
.play-btn-wrap {
|
||||
position: relative;
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
/* 外圈光环 */
|
||||
.play-btn-ring {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid transparent;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.play-btn-ring.active {
|
||||
border-color: rgba(255, 157, 66, 0.25);
|
||||
animation: ring-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
@keyframes ring-pulse {
|
||||
0%, 100% { transform: scale(1); opacity: 1; }
|
||||
50% { transform: scale(1.15); opacity: 0.4; }
|
||||
}
|
||||
/* 按钮主体 */
|
||||
.play-btn {
|
||||
width: 68rpx;
|
||||
height: 68rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #ECECEC;
|
||||
flex-shrink: 0;
|
||||
transition: all 0.25s ease;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
.play-btn:active { transform: scale(0.88); }
|
||||
.play-btn.active {
|
||||
background: linear-gradient(135deg, #FF9D42 0%, #FF7832 100%);
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 120, 50, 0.4);
|
||||
}
|
||||
/* t-icon 已替代旧 .play-icon / .pause-bars */
|
||||
|
||||
/* 暂无节目 */
|
||||
.no-content { padding: 16rpx 12rpx; }
|
||||
.no-content-text { font-size: 23rpx; color: #CCCCCC; }
|
||||
|
||||
/* ========== 免费频道横向区 ========== */
|
||||
|
||||
.skeleton-free-card {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 160rpx;
|
||||
vertical-align: top;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.skele-free-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 36rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.skele-free-text {
|
||||
width: 100rpx;
|
||||
height: 26rpx;
|
||||
border-radius: 6rpx;
|
||||
}
|
||||
|
||||
.free-scroll {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
.free-list {
|
||||
padding: 4rpx 4rpx 16rpx;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.free-card {
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 160rpx;
|
||||
vertical-align: top;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.free-icon {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 12rpx;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
.free-cover { width: 100%; height: 100%; }
|
||||
.free-emoji { font-size: 48rpx; }
|
||||
.free-name {
|
||||
font-size: 22rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 150rpx;
|
||||
margin-bottom: 8rpx;
|
||||
line-height: 1.3;
|
||||
}
|
||||
/* t-tag 已替代 .free-badge,t-empty 已替代 .free-empty */
|
||||
.free-empty {
|
||||
padding: 40rpx 0 20rpx;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* 首次引导页 — 选择2个免费频道
|
||||
*/
|
||||
const app = getApp()
|
||||
const mock = require('../../utils/mock')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
domains: mock.DOMAINS,
|
||||
selectedIds: [],
|
||||
isValid: false,
|
||||
statusBarHeight: 0
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
this.setData({
|
||||
statusBarHeight: app.globalData.statusBarHeight
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换频道选中状态
|
||||
*/
|
||||
onToggle(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
let selected = this.data.selectedIds.slice()
|
||||
|
||||
const idx = selected.indexOf(id)
|
||||
if (idx > -1) {
|
||||
// 取消选中
|
||||
selected.splice(idx, 1)
|
||||
} else {
|
||||
// 选中(最多2个)
|
||||
if (selected.length >= 2) {
|
||||
wx.showToast({ title: '最多选择2个免费频道', icon: 'none' })
|
||||
return
|
||||
}
|
||||
selected.push(id)
|
||||
}
|
||||
|
||||
this.setData({
|
||||
selectedIds: selected,
|
||||
isValid: selected.length > 0 && selected.length <= 2
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认选择
|
||||
*/
|
||||
onConfirm() {
|
||||
if (!this.data.isValid) return
|
||||
|
||||
const self = this
|
||||
this.data.selectedIds.forEach(function (id) {
|
||||
app.subscribeToDomain(id)
|
||||
})
|
||||
|
||||
wx.showToast({ title: '订阅成功!', icon: 'success' })
|
||||
|
||||
setTimeout(function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 800)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-message": "tdesign-miniprogram/message/message"
|
||||
},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!-- 首次引导页 —— 选择2个免费频道 -->
|
||||
<view class="onboarding-page">
|
||||
<!-- 顶部区域 -->
|
||||
<view class="header" style="padding-top: {{statusBarHeight + 10}}px;">
|
||||
<text class="header-title">选择感兴趣的频道</text>
|
||||
<view class="header-sub">
|
||||
<text class="sub-text">可免费选择 2 个</text>
|
||||
<text class="sub-warn">选择后不可更换</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 频道网格 -->
|
||||
<scroll-view scroll-y enhanced show-scrollbar="{{false}}" class="grid-scroll">
|
||||
<view class="grid">
|
||||
<view
|
||||
wx:for="{{domains}}"
|
||||
wx:key="id"
|
||||
class="grid-item {{selectedIds.indexOf(item.id) > -1 ? 'selected' : ''}}"
|
||||
bindtap="onToggle"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<!-- 图标 -->
|
||||
<view class="item-icon" style="background: {{item.bgColor}};">
|
||||
<text class="icon-emoji">{{item.icon}}</text>
|
||||
</view>
|
||||
<!-- 名称 -->
|
||||
<text class="item-name">{{item.name}}</text>
|
||||
<text class="item-tag">{{item.tag}}</text>
|
||||
|
||||
<!-- 选中对勾 -->
|
||||
<view class="check-mark" wx:if="{{selectedIds.indexOf(item.id) > -1}}">
|
||||
<text class="check-icon">✓</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部确认按钮 -->
|
||||
<view class="footer">
|
||||
<button
|
||||
class="confirm-btn {{isValid ? 'active' : 'disabled'}}"
|
||||
bindtap="onConfirm"
|
||||
disabled="{{!isValid}}"
|
||||
>
|
||||
确认选择 {{selectedIds.length > 0 ? '(' + selectedIds.length + '/2)' : ''}}
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<t-message id="t-message" />
|
||||
</view>
|
||||
@@ -0,0 +1,170 @@
|
||||
/* 首次引导页样式 */
|
||||
|
||||
.onboarding-page {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
|
||||
/* 顶部 */
|
||||
.header {
|
||||
padding: 24rpx 40rpx 24rpx;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-bottom: 1rpx solid #F5F5F5;
|
||||
}
|
||||
.header-title {
|
||||
font-size: 44rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
.header-sub {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16rpx;
|
||||
}
|
||||
.sub-text {
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
}
|
||||
.sub-warn {
|
||||
font-size: 22rpx;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* 网格滚动 */
|
||||
.grid-scroll {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
.grid-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0 !important;
|
||||
height: 0 !important;
|
||||
}
|
||||
.grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 24rpx 20rpx 200rpx;
|
||||
gap: 16rpx;
|
||||
}
|
||||
|
||||
/* 网格项 */
|
||||
.grid-item {
|
||||
width: calc(33.33% - 12rpx);
|
||||
box-sizing: border-box;
|
||||
background: #F8F8F8;
|
||||
border-radius: 32rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 28rpx 16rpx;
|
||||
position: relative;
|
||||
border: 4rpx solid transparent;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
.grid-item.selected {
|
||||
border-color: var(--color-primary);
|
||||
background: var(--color-primary-light);
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 157, 66, 0.15);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
/* 图标 */
|
||||
.item-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.icon-emoji {
|
||||
font-size: 40rpx;
|
||||
}
|
||||
|
||||
/* 文字 */
|
||||
.item-name {
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.item-tag {
|
||||
font-size: 18rpx;
|
||||
color: #999;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* 选中对勾 */
|
||||
.check-mark {
|
||||
position: absolute;
|
||||
top: 12rpx;
|
||||
right: 12rpx;
|
||||
width: 40rpx;
|
||||
height: 40rpx;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.check-icon {
|
||||
font-size: 22rpx;
|
||||
color: #FFF;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* 底部 */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: 24rpx 40rpx;
|
||||
padding-bottom: calc(24rpx + env(safe-area-inset-bottom));
|
||||
background: linear-gradient(to top, #FFFFFF, #FFFFFF, rgba(255,255,255,0));
|
||||
}
|
||||
.confirm-btn {
|
||||
width: 100%;
|
||||
height: 100rpx;
|
||||
border-radius: 999rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
.confirm-btn.active {
|
||||
background: var(--color-primary);
|
||||
color: #FFF;
|
||||
box-shadow: 0 12rpx 32rpx rgba(255, 157, 66, 0.3);
|
||||
}
|
||||
.confirm-btn.active:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
.confirm-btn.disabled {
|
||||
background: #E5E5E5;
|
||||
color: #BBB;
|
||||
}
|
||||
.confirm-btn::after {
|
||||
border: none;
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
/**
|
||||
* 播放器详情页
|
||||
* 大封面、进度条、倍速切换、播放控制
|
||||
* 从 globalData.activeContent 获取当前节目
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
const util = require('../../utils/util')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
domain: {},
|
||||
activeContent: null,
|
||||
isPlaying: false,
|
||||
isVip: false,
|
||||
currentTime: 0,
|
||||
duration: 0,
|
||||
currentTimeText: '00:00',
|
||||
durationText: '00:00',
|
||||
displayDate: '',
|
||||
playbackRate: 1.0,
|
||||
statusBarHeight: 0,
|
||||
showTranscript: false, // 封面 ⇔ 文案切换
|
||||
showSpeedSheet: false,
|
||||
speedItems: [
|
||||
{ label: '0.5x' },
|
||||
{ label: '0.75x' },
|
||||
{ label: '1.0x' },
|
||||
{ label: '1.25x' },
|
||||
{ label: '1.5x' },
|
||||
{ label: '2.0x' }
|
||||
]
|
||||
},
|
||||
|
||||
_isSeeking: false,
|
||||
|
||||
|
||||
onLoad() {
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._syncState()
|
||||
|
||||
// 监听播放状态
|
||||
this._onPlayerChange = (state) => {
|
||||
if (this._isSeeking) return
|
||||
this.setData({
|
||||
activeContent: state.activeContent,
|
||||
isPlaying: state.isPlaying,
|
||||
playbackRate: state.playbackRate
|
||||
})
|
||||
this._updateDomain()
|
||||
}
|
||||
|
||||
// 监听时间更新
|
||||
this._onTimeUpdate = (data) => {
|
||||
if (this._isSeeking) return
|
||||
this.setData({
|
||||
currentTime: data.currentTime,
|
||||
duration: data.duration || this.data.duration,
|
||||
currentTimeText: util.formatTime(data.currentTime),
|
||||
durationText: util.formatTime(data.duration || this.data.duration)
|
||||
})
|
||||
}
|
||||
|
||||
app.on('playerStateChange', this._onPlayerChange)
|
||||
app.on('timeUpdate', this._onTimeUpdate)
|
||||
},
|
||||
|
||||
onHide() {
|
||||
if (this._onPlayerChange) app.off('playerStateChange', this._onPlayerChange)
|
||||
if (this._onTimeUpdate) app.off('timeUpdate', this._onTimeUpdate)
|
||||
},
|
||||
|
||||
/**
|
||||
* 同步当前状态
|
||||
*/
|
||||
_syncState() {
|
||||
const gd = app.globalData
|
||||
const content = gd.activeContent
|
||||
|
||||
if (!content) {
|
||||
wx.navigateBack()
|
||||
return
|
||||
}
|
||||
|
||||
var dateStr = ''
|
||||
if (content.createdAt) {
|
||||
dateStr = content.createdAt.substring(0, 10).replace(/-/g, '.')
|
||||
} else if (content.date) {
|
||||
dateStr = util.dateToDot(content.date)
|
||||
}
|
||||
|
||||
this.setData({
|
||||
activeContent: content,
|
||||
isPlaying: gd.isPlaying,
|
||||
isVip: gd.isVip,
|
||||
currentTime: gd.currentTime,
|
||||
duration: gd.duration || content.duration,
|
||||
currentTimeText: util.formatTime(gd.currentTime),
|
||||
durationText: util.formatTime(gd.duration || content.duration),
|
||||
displayDate: dateStr,
|
||||
playbackRate: gd.playbackRate,
|
||||
statusBarHeight: gd.statusBarHeight || 0
|
||||
})
|
||||
|
||||
this._updateDomain()
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取频道信息 — 从后端 API 获取
|
||||
*/
|
||||
_updateDomain() {
|
||||
const content = this.data.activeContent
|
||||
if (!content) return
|
||||
|
||||
var channelId = content.channelId || (content.channel && content.channel.id)
|
||||
if (!channelId) {
|
||||
// 如果节目数据中直接包含 channel 信息
|
||||
if (content.channel) {
|
||||
this.setData({ domain: content.channel })
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经加载过且 channelId 没变,跳过
|
||||
if (this.data.domain && this.data.domain.id === channelId) return
|
||||
|
||||
var self = this
|
||||
api.getChannelDetail(channelId).then(function (res) {
|
||||
if (res.code === 200 && res.data) {
|
||||
self.setData({ domain: res.data })
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[Player] 获取频道信息失败:', err)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 播放/暂停
|
||||
*/
|
||||
onTogglePlay() {
|
||||
app.togglePlay()
|
||||
},
|
||||
|
||||
/**
|
||||
* 进度条拖动中
|
||||
*/
|
||||
onSliderChanging(e) {
|
||||
this._isSeeking = true
|
||||
this.setData({
|
||||
currentTime: e.detail.value,
|
||||
currentTimeText: util.formatTime(e.detail.value)
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 进度条拖动完成 → 跳转播放
|
||||
*/
|
||||
onSliderChange(e) {
|
||||
this._isSeeking = false
|
||||
app.seekTo(e.detail.value)
|
||||
},
|
||||
|
||||
/**
|
||||
* 快退 15 秒
|
||||
*/
|
||||
onBackward() {
|
||||
const newTime = Math.max(0, this.data.currentTime - 15)
|
||||
app.seekTo(newTime)
|
||||
},
|
||||
|
||||
/**
|
||||
* 快进 15 秒
|
||||
*/
|
||||
onForward() {
|
||||
const newTime = Math.min(this.data.duration, this.data.currentTime + 15)
|
||||
app.seekTo(newTime)
|
||||
},
|
||||
|
||||
/**
|
||||
* 倍速设置(VIP 功能)
|
||||
*/
|
||||
onSpeed() {
|
||||
if (!this.data.isVip) {
|
||||
wx.showModal({
|
||||
title: '会员提示',
|
||||
content: '倍速播放是会员专属功能,是否前往开通?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
this.setData({ showSpeedSheet: true })
|
||||
}
|
||||
},
|
||||
|
||||
onSpeedSelect(e) {
|
||||
const label = this.data.speedItems[e.detail.index].label
|
||||
const rate = parseFloat(label)
|
||||
app.setPlaybackRate(rate)
|
||||
this.setData({ showSpeedSheet: false, playbackRate: rate })
|
||||
},
|
||||
|
||||
onSpeedCancel() {
|
||||
this.setData({ showSpeedSheet: false })
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载(VIP 功能)
|
||||
*/
|
||||
onDownload() {
|
||||
if (!this.data.isVip) {
|
||||
wx.showModal({
|
||||
title: '会员提示',
|
||||
content: '音频下载是会员专属功能,是否前往开通?',
|
||||
success: (res) => {
|
||||
if (res.confirm) {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
wx.showToast({ title: '下载功能开发中', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 查看文案
|
||||
*/
|
||||
onTranscript() {
|
||||
const content = this.data.activeContent
|
||||
if (content && content.content) {
|
||||
wx.showModal({
|
||||
title: '完整文案',
|
||||
content: content.content,
|
||||
showCancel: false
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 点击中央 Banner:封面 ⇔ 文案切换
|
||||
*/
|
||||
onBannerTap() {
|
||||
this.setData({ showTranscript: !this.data.showTranscript })
|
||||
},
|
||||
|
||||
onLike() {
|
||||
const content = this.data.activeContent
|
||||
if (!content) return
|
||||
api.toggleLike({ contentId: content.id }).then(function (res) {
|
||||
wx.showToast({ title: res.code === 200 ? '已收藏 ♥' : '操作失败', icon: 'none' })
|
||||
}).catch(function () {
|
||||
wx.showToast({ title: '网络异常', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
onShare() {
|
||||
wx.showToast({ title: '分享功能开发中', icon: 'none' })
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-action-sheet": "tdesign-miniprogram/action-sheet/action-sheet"
|
||||
},
|
||||
"navigationStyle": "custom",
|
||||
"navigationBarTitleText": "正在播放"
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<!-- 播放器详情页 -->
|
||||
<page-meta page-style="overflow:hidden; background:#1A1208;" />
|
||||
<view class="player-page">
|
||||
|
||||
<!-- 状态栏占位(custom 导航模式必须手动留出状态栏高度) -->
|
||||
<view style="height: {{statusBarHeight}}px; flex-shrink: 0;"></view>
|
||||
|
||||
<!-- 环境光背景 -->
|
||||
<view class="bg-glow"
|
||||
style="background: radial-gradient(ellipse at 50% -10%, {{domain.bgColor || '#FF9D42'}}44 0%, transparent 65%);">
|
||||
</view>
|
||||
|
||||
<!-- 顶部:返回 + 标题 + 分享 -->
|
||||
<view class="top-bar">
|
||||
<view class="top-btn tap-active" bindtap="goBack">
|
||||
<text class="top-back">‹</text>
|
||||
</view>
|
||||
<view class="top-title-wrap">
|
||||
<text class="top-label">正在播放</text>
|
||||
<text class="top-title">{{activeContent.title || '加载中...'}}</text>
|
||||
</view>
|
||||
<view class="top-btn tap-active" bindtap="onShare">
|
||||
<text class="top-share">↑</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 中央 Banner:封面 ↔ 文案 -->
|
||||
<view class="banner-area" bindtap="onBannerTap">
|
||||
|
||||
<!-- 封面视图(默认) -->
|
||||
<view class="banner-cover {{showTranscript ? 'banner-hidden' : ''}}">
|
||||
<!-- 涟漪声波(播放中才显示) -->
|
||||
<view class="ripple-wrap" wx:if="{{isPlaying}}">
|
||||
<view class="ripple-ring" style="animation-delay: 0s;"></view>
|
||||
<view class="ripple-ring" style="animation-delay: 0.7s;"></view>
|
||||
<view class="ripple-ring" style="animation-delay: 1.4s;"></view>
|
||||
</view>
|
||||
|
||||
<!-- 大 emoji,无卡片背景 -->
|
||||
<text class="cover-emoji {{isPlaying ? 'emoji-active' : ''}}">📻</text>
|
||||
|
||||
<!-- 频道名 -->
|
||||
<text class="cover-channel">{{domain.name}}</text>
|
||||
|
||||
<text class="banner-hint">点击查看文案</text>
|
||||
</view>
|
||||
|
||||
<!-- 文案视图 -->
|
||||
<view class="banner-transcript {{showTranscript ? '' : 'banner-hidden'}}">
|
||||
<scroll-view scroll-y enhanced show-scrollbar="{{false}}" class="transcript-scroll">
|
||||
<text class="transcript-content" wx:if="{{activeContent.content}}">{{activeContent.content}}</text>
|
||||
<view wx:else class="transcript-empty">
|
||||
<text class="transcript-empty-icon">📄</text>
|
||||
<text class="transcript-empty-text">暂无文案</text>
|
||||
</view>
|
||||
</scroll-view>
|
||||
<text class="banner-hint">点击返回封面</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 日期 + like 同行,进度条上方 -->
|
||||
<view class="date-like-row">
|
||||
<text class="date-text">{{displayDate}}</text>
|
||||
<view class="like-btn-inline tap-active" bindtap="onLike">
|
||||
<text class="like-icon">♡</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 进度条 -->
|
||||
<view class="progress-section">
|
||||
<slider
|
||||
class="progress-slider"
|
||||
min="0"
|
||||
max="{{duration}}"
|
||||
value="{{currentTime}}"
|
||||
activeColor="#FF9D42"
|
||||
backgroundColor="rgba(255,255,255,0.12)"
|
||||
block-size="14"
|
||||
block-color="#FF9D42"
|
||||
bindchange="onSliderChange"
|
||||
bindchanging="onSliderChanging"
|
||||
/>
|
||||
<view class="time-row">
|
||||
<text class="time-text">{{currentTimeText}}</text>
|
||||
<text class="time-text">{{durationText}}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 控制区:← 15s | 播放/暂停 | 15s → -->
|
||||
<view class="controls-row">
|
||||
<view class="skip-btn tap-active" bindtap="onBackward">
|
||||
<text class="skip-arrow">↺</text>
|
||||
<text class="skip-sec">15</text>
|
||||
</view>
|
||||
|
||||
<view class="play-btn tap-active" bindtap="onTogglePlay">
|
||||
<view wx:if="{{isPlaying}}" class="pause-group">
|
||||
<view class="pause-bar"></view>
|
||||
<view class="pause-bar"></view>
|
||||
</view>
|
||||
<text wx:else class="play-tri">▶</text>
|
||||
</view>
|
||||
|
||||
<view class="skip-btn tap-active" bindtap="onForward">
|
||||
<text class="skip-arrow">↻</text>
|
||||
<text class="skip-sec">15</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 倍速选择面板 -->
|
||||
<t-action-sheet
|
||||
visible="{{showSpeedSheet}}"
|
||||
items="{{speedItems}}"
|
||||
bind:selected="onSpeedSelect"
|
||||
bind:cancel="onSpeedCancel"
|
||||
/>
|
||||
</view>
|
||||
@@ -0,0 +1,322 @@
|
||||
/* 播放器 — 暖棕沉浸主题 */
|
||||
::-webkit-scrollbar { display: none !important; }
|
||||
|
||||
.player-page {
|
||||
height: 100vh;
|
||||
background: #1A1208;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 环境光 */
|
||||
.bg-glow {
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 60vh;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* ── 顶部栏 ── */
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16rpx 32rpx 12rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.top-btn {
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: rgba(255,255,255,0.06);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.top-back {
|
||||
font-size: 48rpx;
|
||||
color: rgba(255,240,210,0.9);
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
margin-top: -4rpx;
|
||||
}
|
||||
.top-share {
|
||||
font-size: 32rpx;
|
||||
color: rgba(255,220,160,0.6);
|
||||
font-weight: 600;
|
||||
}
|
||||
.top-title-wrap {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
padding: 0 16rpx;
|
||||
}
|
||||
.top-label {
|
||||
display: block;
|
||||
font-size: 18rpx;
|
||||
color: rgba(255,200,120,0.45);
|
||||
letter-spacing: 2rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.top-title {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: rgba(255,240,210,0.95);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ── 中央 Banner ── */
|
||||
.banner-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
padding: 0 40rpx;
|
||||
}
|
||||
|
||||
.banner-cover,
|
||||
.banner-transcript {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.35s ease, transform 0.35s ease;
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
.banner-hidden {
|
||||
opacity: 0;
|
||||
transform: scale(0.96);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ── 涟漪声波 ── */
|
||||
.ripple-wrap {
|
||||
position: absolute;
|
||||
width: 300rpx;
|
||||
height: 300rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.ripple-ring {
|
||||
position: absolute;
|
||||
width: 220rpx;
|
||||
height: 220rpx;
|
||||
border-radius: 50%;
|
||||
border: 2rpx solid rgba(255, 157, 66, 0.5);
|
||||
animation: ripple-out 2.1s ease-out infinite;
|
||||
}
|
||||
@keyframes ripple-out {
|
||||
0% { transform: scale(0.6); opacity: 0.8; }
|
||||
100% { transform: scale(2.4); opacity: 0; }
|
||||
}
|
||||
|
||||
/* ── 大 Emoji(无卡片背景) ── */
|
||||
.cover-emoji {
|
||||
font-size: 180rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: transform 0.45s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||
transform: scale(0.9);
|
||||
filter: drop-shadow(0 20rpx 40rpx rgba(0,0,0,0.5));
|
||||
}
|
||||
.cover-emoji.emoji-active {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
.cover-channel {
|
||||
font-size: 22rpx;
|
||||
font-weight: 600;
|
||||
color: rgba(255,200,120,0.45);
|
||||
letter-spacing: 2rpx;
|
||||
margin-top: 24rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 提示文字 */
|
||||
.banner-hint {
|
||||
display: block;
|
||||
margin-top: 20rpx;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255,200,120,0.3);
|
||||
letter-spacing: 1rpx;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* ── 文案视图 ── */
|
||||
.banner-transcript {
|
||||
padding: 24rpx 48rpx 16rpx;
|
||||
flex-direction: column;
|
||||
}
|
||||
.transcript-scroll {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
max-height: 480rpx;
|
||||
background: rgba(255,200,120,0.05);
|
||||
border-radius: 32rpx;
|
||||
border: 1rpx solid rgba(255,200,120,0.12);
|
||||
padding: 32rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.transcript-content {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.95;
|
||||
color: rgba(255,240,210,0.8);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.transcript-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
.transcript-empty-icon { font-size: 60rpx; }
|
||||
.transcript-empty-text {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255,200,120,0.4);
|
||||
margin-top: 12rpx;
|
||||
}
|
||||
|
||||
/* 日期 + like 同行 */
|
||||
.date-like-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 48rpx;
|
||||
margin-bottom: 4rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.date-text {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,200,120,0.4);
|
||||
letter-spacing: 1rpx;
|
||||
}
|
||||
.like-btn-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rpx 0;
|
||||
}
|
||||
.like-icon {
|
||||
font-size: 40rpx;
|
||||
color: rgba(255,200,120,0.55);
|
||||
}
|
||||
|
||||
/* ── 进度条 + like 浮动 ── */
|
||||
.progress-section {
|
||||
padding: 0 48rpx;
|
||||
margin-bottom: 16rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
/* like 按钮已内联至 time-row,旧绝对定位样式已移除 */
|
||||
|
||||
.progress-slider { margin: 0; }
|
||||
.time-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8rpx;
|
||||
}
|
||||
.time-text {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255,200,120,0.4);
|
||||
font-family: 'Menlo', 'SF Mono', monospace;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* like 按鈕内联在时间行中间 */
|
||||
.like-btn-inline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 4rpx 16rpx;
|
||||
}
|
||||
.like-icon {
|
||||
font-size: 40rpx;
|
||||
color: rgba(255,200,120,0.55);
|
||||
}
|
||||
|
||||
/* ── 控制区 ── */
|
||||
.controls-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 64rpx;
|
||||
padding: 16rpx 48rpx 64rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.skip-btn {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
.skip-arrow {
|
||||
font-size: 76rpx;
|
||||
color: rgba(255,220,160,0.8);
|
||||
line-height: 1;
|
||||
}
|
||||
.skip-sec {
|
||||
position: absolute;
|
||||
font-size: 19rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(255,220,160,0.8);
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -46%);
|
||||
letter-spacing: -1rpx;
|
||||
}
|
||||
.play-btn {
|
||||
width: 136rpx;
|
||||
height: 136rpx;
|
||||
border-radius: 50%;
|
||||
background: #FF9D42;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
0 16rpx 48rpx rgba(255, 157, 66, 0.5),
|
||||
0 0 0 12rpx rgba(255, 157, 66, 0.15);
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
.play-btn:active {
|
||||
transform: scale(0.92);
|
||||
box-shadow:
|
||||
0 8rpx 24rpx rgba(255, 157, 66, 0.4),
|
||||
0 0 0 8rpx rgba(255, 157, 66, 0.1);
|
||||
}
|
||||
.play-tri {
|
||||
font-size: 52rpx;
|
||||
color: #FFF8EE;
|
||||
margin-left: 8rpx;
|
||||
line-height: 1;
|
||||
}
|
||||
.pause-group {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.pause-bar {
|
||||
width: 10rpx;
|
||||
height: 44rpx;
|
||||
background: #FFF8EE;
|
||||
border-radius: 5rpx;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 个人中心 — 用户信息 + 订阅管理 + 菜单
|
||||
* 从后端获取已订阅频道列表
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
isVip: false,
|
||||
userInfo: null,
|
||||
subscribedData: [],
|
||||
menuItems: [
|
||||
{ id: 'vip', label: '会员中心', icon: '👑', desc: '未开通', highlight: true },
|
||||
{ id: 'help', label: '帮助与反馈', icon: '❓', desc: '', highlight: false },
|
||||
{ id: 'about', label: '关于我们', icon: '📄', desc: 'v1.0.0', highlight: false }
|
||||
]
|
||||
},
|
||||
|
||||
onShow() {
|
||||
this._refresh()
|
||||
this._onSubChange = () => this._refresh()
|
||||
this._onVipChange = () => this._refresh()
|
||||
app.on('subscriptionChange', this._onSubChange)
|
||||
app.on('vipChange', this._onVipChange)
|
||||
},
|
||||
|
||||
onHide() {
|
||||
if (this._onSubChange) app.off('subscriptionChange', this._onSubChange)
|
||||
if (this._onVipChange) app.off('vipChange', this._onVipChange)
|
||||
},
|
||||
|
||||
_refresh() {
|
||||
const self = this
|
||||
const gd = app.globalData
|
||||
|
||||
this.setData({
|
||||
isVip: gd.isVip,
|
||||
userInfo: gd.userInfo
|
||||
})
|
||||
|
||||
// 从后端获取已订阅频道
|
||||
api.getSubscriptionList({ current: 1, pageSize: 50 }).then(function (res) {
|
||||
if (res.code === 200 && res.data) {
|
||||
var subList = res.data.list || res.data || []
|
||||
|
||||
// 适配封面 URL
|
||||
subList = subList.map(function (ch) {
|
||||
return Object.assign({}, ch, {
|
||||
_coverUrl: (ch.cover && ch.cover.url) || ch.coverUrl || ''
|
||||
})
|
||||
})
|
||||
|
||||
self.setData({ subscribedData: subList })
|
||||
}
|
||||
}).catch(function (err) {
|
||||
console.error('[Profile] 加载订阅失败:', err)
|
||||
})
|
||||
|
||||
// 更新菜单VIP状态
|
||||
var menuItems = this.data.menuItems.slice()
|
||||
menuItems[0].desc = gd.isVip ? '已开通' : '未开通'
|
||||
menuItems[0].highlight = !gd.isVip
|
||||
this.setData({ menuItems: menuItems })
|
||||
},
|
||||
|
||||
onUnsubscribe(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
const name = e.currentTarget.dataset.name
|
||||
const self = this
|
||||
|
||||
wx.showModal({
|
||||
title: '提示',
|
||||
content: '确定要取消订阅【' + name + '】吗?',
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
app.unsubscribeFromDomain(id).then(function () {
|
||||
self._refresh()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
onMenuTap(e) {
|
||||
const id = e.currentTarget.dataset.id
|
||||
if (id === 'vip') {
|
||||
wx.navigateTo({ url: '/pages/vip/index' })
|
||||
} else if (id === 'help') {
|
||||
wx.showToast({ title: '帮助中心开发中', icon: 'none' })
|
||||
} else if (id === 'about') {
|
||||
wx.showToast({ title: '早安电台 v1.0.0', icon: 'none' })
|
||||
}
|
||||
},
|
||||
|
||||
goDiscover() {
|
||||
wx.switchTab({ url: '/pages/discover/index' })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"global-player": "/components/global-player/index"
|
||||
},
|
||||
"navigationBarTitleText": "我的"
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<!-- 个人中心 —— 用户信息 + 订阅管理 + 菜单 -->
|
||||
<view class="profile-page">
|
||||
|
||||
<!-- 橙色头部背景 -->
|
||||
<view class="header-bg">
|
||||
<view class="user-info">
|
||||
<!-- 头像 -->
|
||||
<view class="avatar-wrap">
|
||||
<image wx:if="{{userInfo && userInfo.avatarId}}" src="{{userInfo.avatar.url || ''}}" class="avatar-img" mode="aspectFill" />
|
||||
<text wx:else class="avatar-emoji">😊</text>
|
||||
</view>
|
||||
<text class="user-name">
|
||||
{{userInfo.nickName || userInfo.name || '微信用户'}}
|
||||
<text wx:if="{{isVip}}" class="vip-crown">👑</text>
|
||||
</text>
|
||||
<text class="user-desc">
|
||||
{{isVip ? '全频道会员' : '免费用户 · 开通会员畅听全频道'}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 订阅管理卡片(上移覆盖) -->
|
||||
<view class="sub-card-wrap">
|
||||
<view class="card sub-card">
|
||||
|
||||
<!-- 空状态 -->
|
||||
<view wx:if="{{subscribedData.length === 0}}" class="sub-empty">
|
||||
<text class="sub-empty-text">暂未订阅任何频道</text>
|
||||
<button class="btn-ghost" bindtap="goDiscover">去广场添加</button>
|
||||
</view>
|
||||
|
||||
<!-- 订阅列表 -->
|
||||
<view wx:else>
|
||||
<view
|
||||
wx:for="{{subscribedData}}"
|
||||
wx:key="id"
|
||||
class="sub-item"
|
||||
>
|
||||
<view class="sub-item-left">
|
||||
<view class="sub-icon" style="background: {{item.bgColor || '#F0F0F0'}};">
|
||||
<image wx:if="{{item._coverUrl}}" src="{{item._coverUrl}}" class="sub-cover-img" mode="aspectFill" />
|
||||
<text wx:else class="sub-emoji">{{item.icon || '📻'}}</text>
|
||||
</view>
|
||||
<view class="sub-info">
|
||||
<text class="sub-name">{{item.name}}</text>
|
||||
<text class="sub-tag {{item.isFree === 1 ? 'free' : ''}}">
|
||||
{{item.isFree === 1 ? '免费' : '已订阅'}}
|
||||
</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="sub-del tap-active" bindtap="onUnsubscribe" data-id="{{item.id}}" data-name="{{item.name}}">
|
||||
<text class="del-icon">🗑</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 菜单列表 -->
|
||||
<view class="menu-card-wrap">
|
||||
<view class="card menu-card">
|
||||
<view
|
||||
wx:for="{{menuItems}}"
|
||||
wx:key="id"
|
||||
class="menu-item tap-active"
|
||||
bindtap="onMenuTap"
|
||||
data-id="{{item.id}}"
|
||||
>
|
||||
<view class="menu-item-left">
|
||||
<view class="menu-icon-wrap {{item.highlight ? 'highlight' : ''}}">
|
||||
<text class="menu-emoji">{{item.icon}}</text>
|
||||
</view>
|
||||
<text class="menu-label">{{item.label}}</text>
|
||||
</view>
|
||||
<view class="menu-item-right">
|
||||
<text class="menu-desc {{item.highlight ? 'highlight' : ''}}">{{item.desc}}</text>
|
||||
<text class="menu-arrow">›</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<view style="height: 200rpx;"></view>
|
||||
<global-player />
|
||||
</view>
|
||||
@@ -0,0 +1,223 @@
|
||||
/* 个人中心样式 */
|
||||
|
||||
.profile-page {
|
||||
min-height: 100vh;
|
||||
background: var(--color-bg-page);
|
||||
}
|
||||
|
||||
/* 橙色头部 */
|
||||
.header-bg {
|
||||
background: #F38600;
|
||||
border-radius: 0 0 48rpx 48rpx;
|
||||
padding-bottom: 200rpx;
|
||||
}
|
||||
.user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32rpx 40rpx;
|
||||
}
|
||||
.avatar-wrap {
|
||||
width: 128rpx;
|
||||
height: 128rpx;
|
||||
border-radius: 50%;
|
||||
border: 6rpx solid rgba(255, 255, 255, 0.3);
|
||||
background: linear-gradient(135deg, #FFB366, #FFE0B2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.avatar-emoji {
|
||||
font-size: 64rpx;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.user-name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 700;
|
||||
color: #FFF;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.vip-crown {
|
||||
font-size: 28rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
.user-desc {
|
||||
font-size: 22rpx;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
margin-top: 8rpx;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 订阅卡片 */
|
||||
.sub-card-wrap {
|
||||
padding: 0 32rpx;
|
||||
margin-top: -140rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.sub-card {
|
||||
padding: 32rpx;
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.sub-empty {
|
||||
text-align: center;
|
||||
padding: 40rpx 0;
|
||||
}
|
||||
.sub-empty-text {
|
||||
display: block;
|
||||
font-size: 26rpx;
|
||||
color: #999;
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
/* 订阅列表项 */
|
||||
.sub-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20rpx;
|
||||
border-radius: 32rpx;
|
||||
background: #FEFEFE;
|
||||
border: 1rpx solid rgba(0,0,0,0.04);
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.02);
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.sub-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sub-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1);
|
||||
}
|
||||
.sub-emoji {
|
||||
font-size: 36rpx;
|
||||
}
|
||||
.sub-cover-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.sub-info {
|
||||
margin-left: 24rpx;
|
||||
overflow: hidden;
|
||||
}
|
||||
.sub-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
}
|
||||
.sub-tag {
|
||||
display: inline-block;
|
||||
margin-top: 8rpx;
|
||||
padding: 4rpx 12rpx;
|
||||
border-radius: 6rpx;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
}
|
||||
.sub-tag.free {
|
||||
background: rgba(45, 90, 39, 0.1);
|
||||
color: #2D5A27;
|
||||
}
|
||||
|
||||
.sub-del {
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F5F5;
|
||||
border: 1rpx solid #EEE;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.del-icon {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
|
||||
.sub-notice {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 157, 66, 0.7);
|
||||
text-align: center;
|
||||
margin-top: 12rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 菜单卡片 */
|
||||
.menu-card-wrap {
|
||||
padding: 24rpx 32rpx 0;
|
||||
}
|
||||
.menu-card {
|
||||
padding: 8rpx 0;
|
||||
}
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx 32rpx;
|
||||
border-bottom: 1rpx solid rgba(0,0,0,0.03);
|
||||
}
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.menu-item-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.menu-icon-wrap {
|
||||
width: 52rpx;
|
||||
height: 52rpx;
|
||||
border-radius: 50%;
|
||||
background: #F5F5F5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 20rpx;
|
||||
}
|
||||
.menu-icon-wrap.highlight {
|
||||
background: rgba(251, 191, 36, 0.1);
|
||||
}
|
||||
.menu-emoji {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.menu-label {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
.menu-item-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
.menu-desc {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
.menu-desc.highlight {
|
||||
color: #D97706;
|
||||
}
|
||||
.menu-arrow {
|
||||
font-size: 28rpx;
|
||||
color: #CCC;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 启动页 — 每次启动都执行 wx.login → 后端登录
|
||||
* 微信的 code 是一次性的,必须每次重新获取
|
||||
*/
|
||||
const app = getApp()
|
||||
|
||||
Page({
|
||||
data: {
|
||||
loginState: 'loading' // 'loading' | 'success' | 'error'
|
||||
},
|
||||
|
||||
onLoad() {
|
||||
// 每次进入启动页都执行登录流程(不跳过)
|
||||
this._startLogin()
|
||||
},
|
||||
|
||||
/**
|
||||
* 执行静默登录
|
||||
* wx.login() → code → 后端 /auth/miniLogin → token + user
|
||||
*/
|
||||
_startLogin() {
|
||||
const self = this
|
||||
|
||||
app.login().then(function (data) {
|
||||
self.setData({ loginState: 'success' })
|
||||
|
||||
// 登录成功,多停留一会儿再跳转(让用户看清启动页)
|
||||
setTimeout(function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 1800)
|
||||
}).catch(function (err) {
|
||||
console.error('[Splash] 登录失败:', err)
|
||||
self.setData({ loginState: 'error' })
|
||||
|
||||
// 登录失败也跳转首页(游客模式浏览)
|
||||
setTimeout(function () {
|
||||
wx.switchTab({ url: '/pages/index/index' })
|
||||
}, 1500)
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"usingComponents": {},
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<!-- 启动页 —— 沉浸式暖棕主题 -->
|
||||
<page-meta page-style="overflow:hidden; background:#1A1208;" />
|
||||
<view class="splash-page">
|
||||
|
||||
<!-- 环境光 -->
|
||||
<view class="splash-glow"></view>
|
||||
|
||||
<!-- 主内容 -->
|
||||
<view class="splash-content {{loginState === 'success' ? 'fade-out' : ''}}">
|
||||
|
||||
<!-- 收音机图标 + 脉冲 -->
|
||||
<view class="logo-wrap">
|
||||
<view class="pulse-ring" wx:if="{{loginState === 'loading'}}"></view>
|
||||
<view class="pulse-ring pulse-ring-2" wx:if="{{loginState === 'loading'}}"></view>
|
||||
<text class="logo-icon">📻</text>
|
||||
</view>
|
||||
|
||||
<!-- 应用名 + slogan -->
|
||||
<text class="app-title">全声汇</text>
|
||||
<text class="app-sub">QuanShengHui</text>
|
||||
<text class="app-slogan">好内容,随时听</text>
|
||||
|
||||
<!-- 状态 -->
|
||||
<view class="status-area">
|
||||
<view wx:if="{{loginState === 'loading'}}" class="status-row">
|
||||
<view class="loading-dots">
|
||||
<view class="dot" style="animation-delay:0s"></view>
|
||||
<view class="dot" style="animation-delay:0.2s"></view>
|
||||
<view class="dot" style="animation-delay:0.4s"></view>
|
||||
</view>
|
||||
</view>
|
||||
<view wx:elif="{{loginState === 'success'}}" class="status-row">
|
||||
<text class="status-check">✓</text>
|
||||
<text class="status-ok">已就绪</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 底部版权 -->
|
||||
<view class="splash-footer">
|
||||
<text class="footer-text">每天三分钟,精准获取所需</text>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,152 @@
|
||||
/* 启动页 — 暖棕沉浸主题,与播放器保持一致 */
|
||||
|
||||
.splash-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #1A1208;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 环境光晕 */
|
||||
.splash-glow {
|
||||
position: absolute;
|
||||
top: -10vh;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 120vw;
|
||||
height: 60vh;
|
||||
background: radial-gradient(ellipse at center, rgba(255,157,66,0.22) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 主内容容器 */
|
||||
.splash-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
transition: opacity 1.0s ease, transform 1.0s ease, filter 1.0s ease;
|
||||
}
|
||||
.splash-content.fade-out {
|
||||
opacity: 0;
|
||||
transform: scale(1.06) translateY(-12rpx);
|
||||
filter: blur(6px);
|
||||
}
|
||||
|
||||
/* ── 图标 ── */
|
||||
.logo-wrap {
|
||||
width: 200rpx;
|
||||
height: 200rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 64rpx;
|
||||
position: relative;
|
||||
}
|
||||
.logo-icon {
|
||||
font-size: 120rpx;
|
||||
filter: drop-shadow(0 16rpx 40rpx rgba(255,120,0,0.4));
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
/* 双层脉冲环 */
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
inset: -20rpx;
|
||||
border-radius: 50%;
|
||||
border: 3rpx solid rgba(255, 157, 66, 0.45);
|
||||
animation: pulse-expand 2s ease-out infinite;
|
||||
}
|
||||
.pulse-ring-2 {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
@keyframes pulse-expand {
|
||||
0% { transform: scale(0.7); opacity: 0.8; }
|
||||
100% { transform: scale(1.6); opacity: 0; }
|
||||
}
|
||||
|
||||
/* ── 文字 ── */
|
||||
.app-title {
|
||||
font-size: 64rpx;
|
||||
font-weight: 800;
|
||||
color: rgba(255, 240, 210, 0.95);
|
||||
letter-spacing: -2rpx;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.app-sub {
|
||||
font-size: 22rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 200, 120, 0.45);
|
||||
letter-spacing: 6rpx;
|
||||
margin-bottom: 28rpx;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.app-slogan {
|
||||
font-size: 26rpx;
|
||||
font-weight: 400;
|
||||
color: rgba(255, 200, 120, 0.55);
|
||||
letter-spacing: 3rpx;
|
||||
}
|
||||
|
||||
/* ── 状态区 ── */
|
||||
.status-area {
|
||||
margin-top: 100rpx;
|
||||
height: 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.status-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
}
|
||||
|
||||
/* 三点加载动画 */
|
||||
.loading-dots {
|
||||
display: flex;
|
||||
gap: 12rpx;
|
||||
align-items: center;
|
||||
}
|
||||
.dot {
|
||||
width: 12rpx;
|
||||
height: 12rpx;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 157, 66, 0.7);
|
||||
animation: dot-bounce 1.2s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes dot-bounce {
|
||||
0% { transform: translateY(0); opacity: 0.3; }
|
||||
100% { transform: translateY(-12rpx); opacity: 1; }
|
||||
}
|
||||
|
||||
/* 成功状态 */
|
||||
.status-check {
|
||||
font-size: 32rpx;
|
||||
color: #FF9D42;
|
||||
font-weight: 700;
|
||||
}
|
||||
.status-ok {
|
||||
font-size: 26rpx;
|
||||
color: rgba(255, 200, 120, 0.7);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── 底部版权 ── */
|
||||
.splash-footer {
|
||||
position: absolute;
|
||||
bottom: 80rpx;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.footer-text {
|
||||
font-size: 20rpx;
|
||||
color: rgba(255, 200, 120, 0.2);
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/**
|
||||
* 订阅支付页
|
||||
* 接收参数:channelId, channelName, monthlyPrice, quarterlyPrice, annualPrice
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
channelId: '',
|
||||
channelName: '',
|
||||
monthlyPrice: 0,
|
||||
quarterlyPrice: 0,
|
||||
annualPrice: 0,
|
||||
selectedPlan: '', // 'monthly' | 'quarterly' | 'annual'
|
||||
currentPrice: 0
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
// 后端价格单位为「分」,除以 100 转为「元」
|
||||
const monthly = (parseFloat(options.monthlyPrice) || 0) / 100
|
||||
const quarterly = (parseFloat(options.quarterlyPrice) || 0) / 100
|
||||
const annual = (parseFloat(options.annualPrice) || 0) / 100
|
||||
|
||||
// 默认选中"包年"(如果有),否则最便宜的
|
||||
let defaultPlan = ''
|
||||
let defaultPrice = 0
|
||||
if (annual > 0) {
|
||||
defaultPlan = 'annual'
|
||||
defaultPrice = annual
|
||||
} else if (quarterly > 0) {
|
||||
defaultPlan = 'quarterly'
|
||||
defaultPrice = quarterly
|
||||
} else if (monthly > 0) {
|
||||
defaultPlan = 'monthly'
|
||||
defaultPrice = monthly
|
||||
}
|
||||
|
||||
this.setData({
|
||||
channelId: options.channelId || '',
|
||||
channelName: decodeURIComponent(options.channelName || ''),
|
||||
monthlyPrice: monthly,
|
||||
quarterlyPrice: quarterly,
|
||||
annualPrice: annual,
|
||||
selectedPlan: defaultPlan,
|
||||
currentPrice: defaultPrice,
|
||||
// 预计算节省金额和月均价(WXML 不支持 .toFixed())
|
||||
_quarterlySaving: monthly > 0 && quarterly > 0 ? Math.round(monthly * 3 - quarterly) : 0,
|
||||
_quarterlyMonthly: quarterly > 0 ? (quarterly / 3).toFixed(1) : '0',
|
||||
_annualSaving: monthly > 0 && annual > 0 ? Math.round(monthly * 12 - annual) : 0,
|
||||
_annualMonthly: annual > 0 ? (annual / 12).toFixed(1) : '0'
|
||||
})
|
||||
},
|
||||
|
||||
/** 选择套餐 */
|
||||
selectPlan(e) {
|
||||
const plan = e.currentTarget.dataset.plan
|
||||
const priceMap = {
|
||||
monthly: this.data.monthlyPrice,
|
||||
quarterly: this.data.quarterlyPrice,
|
||||
annual: this.data.annualPrice
|
||||
}
|
||||
this.setData({
|
||||
selectedPlan: plan,
|
||||
currentPrice: priceMap[plan] || 0
|
||||
})
|
||||
},
|
||||
|
||||
/** 发起支付 */
|
||||
onPay() {
|
||||
const { channelId, selectedPlan, currentPrice, channelName } = this.data
|
||||
|
||||
if (!selectedPlan) {
|
||||
wx.showToast({ title: '请选择订阅方案', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 调用订阅/支付接口
|
||||
wx.showLoading({ title: '处理中...' })
|
||||
api.subscribeChannel({
|
||||
channelId,
|
||||
plan: selectedPlan,
|
||||
price: currentPrice
|
||||
}).then(function (res) {
|
||||
wx.hideLoading()
|
||||
if (res.code === 200) {
|
||||
wx.showToast({ title: '订阅成功!', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
wx.navigateBack()
|
||||
}, 1500)
|
||||
} else {
|
||||
wx.showToast({ title: res.msg || '订阅失败', icon: 'none' })
|
||||
}
|
||||
}).catch(function (err) {
|
||||
wx.hideLoading()
|
||||
console.error('[订阅] 失败:', err)
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
},
|
||||
"navigationBarTitleText": "订阅频道"
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
<!-- 订阅支付页 -->
|
||||
<page-meta page-style="overflow: hidden;" />
|
||||
<view class="subscribe-page">
|
||||
|
||||
<!-- 顶部频道信息 -->
|
||||
<view class="channel-card">
|
||||
<view class="channel-avatar">
|
||||
<text class="channel-avatar-emoji">📻</text>
|
||||
</view>
|
||||
<text class="channel-title">{{channelName}}</text>
|
||||
<text class="channel-desc">选择适合你的订阅方案</text>
|
||||
</view>
|
||||
|
||||
<!-- 价格套餐选择 -->
|
||||
<view class="plans-wrap">
|
||||
<!-- 包月 -->
|
||||
<view
|
||||
wx:if="{{monthlyPrice > 0}}"
|
||||
class="plan-card {{selectedPlan === 'monthly' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="monthly"
|
||||
>
|
||||
<view class="plan-top">
|
||||
<text class="plan-name">包月</text>
|
||||
<view class="plan-badge" wx:if="{{selectedPlan === 'monthly'}}">
|
||||
<t-icon name="check-circle-filled" size="36rpx" color="#FF9D42" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="plan-price">¥{{monthlyPrice}}</text>
|
||||
<text class="plan-unit">/ 月</text>
|
||||
</view>
|
||||
|
||||
<!-- 包季 -->
|
||||
<view
|
||||
wx:if="{{quarterlyPrice > 0}}"
|
||||
class="plan-card {{selectedPlan === 'quarterly' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="quarterly"
|
||||
>
|
||||
<view class="plan-top">
|
||||
<text class="plan-name">包季</text>
|
||||
<t-tag wx:if="{{quarterlyPrice > 0 && monthlyPrice > 0}}" size="small" variant="filled" theme="success">省{{_quarterlySaving}}元</t-tag>
|
||||
<view class="plan-badge" wx:if="{{selectedPlan === 'quarterly'}}">
|
||||
<t-icon name="check-circle-filled" size="36rpx" color="#FF9D42" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="plan-price">¥{{quarterlyPrice}}</text>
|
||||
<text class="plan-unit">/ 季 · 约¥{{_quarterlyMonthly}}/月</text>
|
||||
</view>
|
||||
|
||||
<!-- 包年 -->
|
||||
<view
|
||||
wx:if="{{annualPrice > 0}}"
|
||||
class="plan-card popular {{selectedPlan === 'annual' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="annual"
|
||||
>
|
||||
<view class="plan-hot-tag">最受欢迎</view>
|
||||
<view class="plan-top">
|
||||
<text class="plan-name">包年</text>
|
||||
<t-tag wx:if="{{annualPrice > 0 && monthlyPrice > 0}}" size="small" variant="filled" theme="success">省{{_annualSaving}}元</t-tag>
|
||||
<view class="plan-badge" wx:if="{{selectedPlan === 'annual'}}">
|
||||
<t-icon name="check-circle-filled" size="36rpx" color="#FF9D42" />
|
||||
</view>
|
||||
</view>
|
||||
<text class="plan-price">¥{{annualPrice}}</text>
|
||||
<text class="plan-unit">/ 年 · 约¥{{_annualMonthly}}/月</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 权益说明 -->
|
||||
<view class="benefits">
|
||||
<view class="benefit-item">
|
||||
<t-icon name="check" size="28rpx" color="#FF9D42" />
|
||||
<text>订阅后可无限收听该频道所有节目</text>
|
||||
</view>
|
||||
<view class="benefit-item">
|
||||
<t-icon name="check" size="28rpx" color="#FF9D42" />
|
||||
<text>后台播放,边听边做其他事</text>
|
||||
</view>
|
||||
<view class="benefit-item">
|
||||
<t-icon name="check" size="28rpx" color="#FF9D42" />
|
||||
<text>新节目第一时间推送通知</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 底部支付按钮 -->
|
||||
<view class="pay-bar">
|
||||
<view class="pay-amount" wx:if="{{selectedPlan}}">
|
||||
<text class="pay-label">合计</text>
|
||||
<text class="pay-price">¥{{currentPrice}}</text>
|
||||
</view>
|
||||
<view class="pay-btn tap-active" bindtap="onPay">
|
||||
<text>立即订阅</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
@@ -0,0 +1,147 @@
|
||||
/* 订阅支付页 */
|
||||
.subscribe-page {
|
||||
min-height: 100vh;
|
||||
background: #F6F6F6;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 160rpx;
|
||||
}
|
||||
|
||||
/* 顶部频道卡片 */
|
||||
.channel-card {
|
||||
background: linear-gradient(135deg, #FF9D42, #FF7832);
|
||||
padding: 60rpx 40rpx 48rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
.channel-avatar {
|
||||
width: 120rpx;
|
||||
height: 120rpx;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20rpx;
|
||||
}
|
||||
.channel-avatar-emoji { font-size: 56rpx; }
|
||||
.channel-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #FFF;
|
||||
margin-bottom: 8rpx;
|
||||
}
|
||||
.channel-desc {
|
||||
font-size: 24rpx;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
/* 套餐区域 */
|
||||
.plans-wrap {
|
||||
padding: 32rpx 32rpx 20rpx;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20rpx;
|
||||
}
|
||||
.plan-card {
|
||||
background: #FFF;
|
||||
border-radius: 28rpx;
|
||||
padding: 32rpx 36rpx;
|
||||
border: 3rpx solid transparent;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
box-shadow: 0 2rpx 12rpx rgba(0,0,0,0.04);
|
||||
}
|
||||
.plan-card.selected {
|
||||
border-color: #FF9D42;
|
||||
background: #FFFAF5;
|
||||
}
|
||||
.plan-card.popular {
|
||||
border-color: #FFD580;
|
||||
}
|
||||
.plan-card.popular.selected {
|
||||
border-color: #FF9D42;
|
||||
}
|
||||
/* 最受欢迎标签 */
|
||||
.plan-hot-tag {
|
||||
position: absolute;
|
||||
top: -20rpx;
|
||||
left: 36rpx;
|
||||
background: linear-gradient(135deg, #FF9D42, #FF7832);
|
||||
color: #FFF;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 999rpx;
|
||||
}
|
||||
.plan-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12rpx;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.plan-name {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #222;
|
||||
flex: 1;
|
||||
}
|
||||
.plan-badge { margin-left: auto; }
|
||||
.plan-price {
|
||||
font-size: 52rpx;
|
||||
font-weight: 800;
|
||||
color: #FF7832;
|
||||
line-height: 1;
|
||||
}
|
||||
.plan-unit {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
margin-top: 6rpx;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 权益说明 */
|
||||
.benefits {
|
||||
margin: 0 32rpx;
|
||||
background: #FFF;
|
||||
border-radius: 24rpx;
|
||||
padding: 28rpx 32rpx;
|
||||
}
|
||||
.benefit-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16rpx;
|
||||
margin-bottom: 18rpx;
|
||||
font-size: 26rpx;
|
||||
color: #444;
|
||||
}
|
||||
.benefit-item:last-child { margin-bottom: 0; }
|
||||
|
||||
/* 底部支付栏 */
|
||||
.pay-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: #FFF;
|
||||
padding: 20rpx 32rpx 48rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24rpx;
|
||||
border-top: 1rpx solid #F0F0F0;
|
||||
box-shadow: 0 -4rpx 20rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
.pay-amount { flex: 1; }
|
||||
.pay-label { font-size: 22rpx; color: #999; display: block; }
|
||||
.pay-price { font-size: 44rpx; font-weight: 800; color: #FF7832; line-height: 1; }
|
||||
.pay-btn {
|
||||
background: linear-gradient(135deg, #FF9D42, #FF7832);
|
||||
color: #FFF;
|
||||
font-size: 30rpx;
|
||||
font-weight: 700;
|
||||
padding: 24rpx 64rpx;
|
||||
border-radius: 999rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(255, 120, 50, 0.35);
|
||||
}
|
||||
.pay-btn:active { transform: scale(0.96); opacity: 0.9; }
|
||||
@@ -0,0 +1,222 @@
|
||||
/**
|
||||
* 会员/订阅中心
|
||||
*
|
||||
* 支持两种入口模式:
|
||||
* mode=vip(默认): 开通全频道会员
|
||||
* mode=channel : 订阅指定频道,URL 需带 channelId/channelName/monthlyPrice/quarterlyPrice/annualPrice
|
||||
*/
|
||||
const app = getApp()
|
||||
const api = require('../../utils/api')
|
||||
|
||||
Page({
|
||||
data: {
|
||||
isVip: false,
|
||||
// 模式:'vip' | 'channel'
|
||||
mode: 'vip',
|
||||
// 频道订阅模式下的频道信息
|
||||
channelId: '',
|
||||
channelName: '',
|
||||
monthlyPrice: 0,
|
||||
quarterlyPrice: 0,
|
||||
annualPrice: 0,
|
||||
// 预计算(WXML 不支持方法调用)
|
||||
_quarterlySaving: 0,
|
||||
_quarterlyMonthly: '0',
|
||||
_annualSaving: 0,
|
||||
_annualMonthly: '0',
|
||||
// 当前选中的套餐,vip 模式固定 'vip-all',channel 模式为 'monthly'|'quarterly'|'annual'
|
||||
selectedPlan: 'vip-all',
|
||||
currentPrice: '19.9'
|
||||
},
|
||||
|
||||
onLoad(options) {
|
||||
const isChannelMode = !!options.channelId
|
||||
if (isChannelMode) {
|
||||
// ─── 频道订阅模式 ───
|
||||
const monthly = (parseFloat(options.monthlyPrice) || 0) / 100
|
||||
const quarterly = (parseFloat(options.quarterlyPrice) || 0) / 100
|
||||
const annual = (parseFloat(options.annualPrice) || 0) / 100
|
||||
|
||||
// 默认选中包年,否则最合算的
|
||||
let defaultPlan = 'monthly'
|
||||
let defaultPrice = monthly
|
||||
if (annual > 0) { defaultPlan = 'annual'; defaultPrice = annual }
|
||||
else if (quarterly > 0) { defaultPlan = 'quarterly'; defaultPrice = quarterly }
|
||||
|
||||
this.setData({
|
||||
mode: 'channel',
|
||||
channelId: options.channelId,
|
||||
channelName: decodeURIComponent(options.channelName || ''),
|
||||
monthlyPrice: monthly,
|
||||
quarterlyPrice: quarterly,
|
||||
annualPrice: annual,
|
||||
selectedPlan: defaultPlan,
|
||||
currentPrice: defaultPrice.toFixed(2),
|
||||
_quarterlySaving: (monthly > 0 && quarterly > 0) ? Math.round(monthly * 3 - quarterly) : 0,
|
||||
_quarterlyMonthly: quarterly > 0 ? (quarterly / 3).toFixed(1) : '0',
|
||||
_annualSaving: (monthly > 0 && annual > 0) ? Math.round(monthly * 12 - annual) : 0,
|
||||
_annualMonthly: annual > 0 ? (annual / 12).toFixed(1) : '0'
|
||||
})
|
||||
} else {
|
||||
// ─── VIP 会员模式 ───
|
||||
this.setData({
|
||||
mode: 'vip',
|
||||
isVip: app.globalData.isVip,
|
||||
selectedPlan: 'vip-all',
|
||||
currentPrice: '19.9'
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
onShow() {
|
||||
if (this.data.mode === 'vip') {
|
||||
this.setData({ isVip: app.globalData.isVip })
|
||||
}
|
||||
},
|
||||
|
||||
/** 选择套餐 */
|
||||
selectPlan(e) {
|
||||
const plan = e.currentTarget.dataset.plan
|
||||
let price = '19.9'
|
||||
if (this.data.mode === 'channel') {
|
||||
const map = {
|
||||
monthly: this.data.monthlyPrice,
|
||||
quarterly: this.data.quarterlyPrice,
|
||||
annual: this.data.annualPrice
|
||||
}
|
||||
price = (map[plan] || 0).toFixed(2)
|
||||
}
|
||||
this.setData({ selectedPlan: plan, currentPrice: price })
|
||||
},
|
||||
|
||||
/** 发起支付 */
|
||||
onPay() {
|
||||
const self = this
|
||||
const { mode, selectedPlan, currentPrice, channelId } = this.data
|
||||
|
||||
if (mode === 'vip') {
|
||||
// ── VIP 全频道(模拟,后续接入时替换) ──
|
||||
wx.showModal({
|
||||
title: '确认支付',
|
||||
content: `即将支付 ¥${currentPrice} 开通全频道会员`,
|
||||
success(res) {
|
||||
if (res.confirm) {
|
||||
app.upgradeVip()
|
||||
wx.showToast({ title: '开通成功!', icon: 'success' })
|
||||
setTimeout(function () {
|
||||
self.setData({ isVip: true })
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// ── 频道订阅:唤起微信支付 ──
|
||||
if (!selectedPlan) {
|
||||
wx.showToast({ title: '请选择订阅方案', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
// 套餐 → type 映射(后端约定:1=包月 2=包季 3=包年)
|
||||
const typeMap = { monthly: '1', quarterly: '2', annual: '3' }
|
||||
const payType = typeMap[selectedPlan]
|
||||
if (!payType) {
|
||||
wx.showToast({ title: '未知套餐类型', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
wx.showLoading({ title: '获取支付信息...' })
|
||||
|
||||
api.unlockChannel(channelId, payType)
|
||||
.then(function (res) {
|
||||
if (res.code !== 200 || !res.data || !res.data.payments) {
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: res.msg || '获取支付信息失败', icon: 'none' })
|
||||
return
|
||||
}
|
||||
|
||||
const payments = res.data.payments
|
||||
const outTradeNo = res.data.outTradeNo
|
||||
wx.hideLoading()
|
||||
|
||||
// 唤起微信支付
|
||||
wx.requestPayment({
|
||||
timeStamp: payments.timeStamp,
|
||||
nonceStr: payments.nonceStr,
|
||||
package: payments.package,
|
||||
signType: payments.signType || 'RSA',
|
||||
paySign: payments.paySign,
|
||||
success() {
|
||||
// 支付 UI 完成后,主动轮询查询支付结果
|
||||
// 策略:立即查一次,失败则间隔 2s 重试,最多 3 次
|
||||
// 原因:微信回调是异步的,可能比 success 回调晚几秒到
|
||||
self._pollPayStatus(outTradeNo, 3, 2000)
|
||||
},
|
||||
fail(err) {
|
||||
if (err.errMsg && err.errMsg.indexOf('cancel') > -1) {
|
||||
return // 用户主动取消,静默处理
|
||||
}
|
||||
wx.showToast({ title: '支付失败,请重试', icon: 'none' })
|
||||
console.error('[支付] wx.requestPayment 失败:', err)
|
||||
}
|
||||
})
|
||||
})
|
||||
.catch(function (err) {
|
||||
wx.hideLoading()
|
||||
console.error('[支付] unlockChannel 请求失败:', err)
|
||||
wx.showToast({ title: '网络异常,请重试', icon: 'none' })
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* 轮询支付状态
|
||||
* @param {string} outTradeNo 商户订单号
|
||||
* @param {number} retries 剩余重试次数
|
||||
* @param {number} interval 每次重试间隔(ms)
|
||||
*/
|
||||
_pollPayStatus(outTradeNo, retries, interval) {
|
||||
const self = this
|
||||
wx.showLoading({ title: '验证中...' })
|
||||
|
||||
api.queryPayStatus(outTradeNo)
|
||||
.then(function (paid) {
|
||||
if (paid) {
|
||||
// ✅ 支付确认成功
|
||||
wx.hideLoading()
|
||||
wx.showToast({ title: '订阅成功!', icon: 'success' })
|
||||
app.emit('subscriptionChange')
|
||||
setTimeout(function () { wx.navigateBack() }, 1500)
|
||||
} else if (retries > 1) {
|
||||
// 🔄 尚未到账,等待后重试(回调可能还在路上)
|
||||
setTimeout(function () {
|
||||
self._pollPayStatus(outTradeNo, retries - 1, interval)
|
||||
}, interval)
|
||||
} else {
|
||||
// ⏳ 重试耗尽:回调可能仍在处理,给用户友好提示后返回
|
||||
wx.hideLoading()
|
||||
wx.showModal({
|
||||
title: '支付处理中',
|
||||
content: '支付已完成,订阅正在确认中,稍后请刷新查看',
|
||||
showCancel: false,
|
||||
success: function () { wx.navigateBack() }
|
||||
})
|
||||
}
|
||||
})
|
||||
.catch(function (err) {
|
||||
wx.hideLoading()
|
||||
console.error('[支付] 查询状态失败:', err)
|
||||
// 查询本身网络失败,乐观处理——让后端 webhook 兜底
|
||||
wx.showModal({
|
||||
title: '支付处理中',
|
||||
content: '支付已提交,订阅确认中,稍后请刷新查看',
|
||||
showCancel: false,
|
||||
success: function () { wx.navigateBack() }
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
goBack() {
|
||||
wx.navigateBack()
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"usingComponents": {
|
||||
"t-button": "tdesign-miniprogram/button/button",
|
||||
"t-icon": "tdesign-miniprogram/icon/icon",
|
||||
"t-tag": "tdesign-miniprogram/tag/tag"
|
||||
},
|
||||
"navigationBarTitleText": "开通会员"
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
<!-- 会员/订阅中心 -->
|
||||
<page-meta page-style="overflow: hidden;" />
|
||||
<view class="vip-page">
|
||||
|
||||
<!-- ── 已是VIP,直接返回 ── -->
|
||||
<view wx:if="{{isVip && mode === 'vip'}}" class="vip-done">
|
||||
<text class="vip-done-icon">👑</text>
|
||||
<text class="vip-done-title">您已经是全频道会员</text>
|
||||
<text class="vip-done-desc">畅享全部频道,尊享专属权益</text>
|
||||
<button class="done-back-btn" bindtap="goBack">返回</button>
|
||||
</view>
|
||||
|
||||
<!-- ── 主Scroll区域 ── -->
|
||||
<scroll-view
|
||||
wx:else
|
||||
scroll-y
|
||||
enhanced
|
||||
show-scrollbar="{{false}}"
|
||||
class="vip-scroll"
|
||||
>
|
||||
|
||||
<!-- 头部区域,根据模式切换文案 -->
|
||||
<view class="vip-hero">
|
||||
<text class="vip-hero-title">{{mode === 'channel' ? channelName : '开通全频道会员'}}</text>
|
||||
<text class="vip-hero-desc">
|
||||
{{mode === 'channel' ? '选择适合你的订阅方案,随时随地收听' : '解锁全部频道,告别无聊早晨'}}
|
||||
</text>
|
||||
</view>
|
||||
|
||||
<!-- ────── MODE: VIP 全频道 ────── -->
|
||||
<block wx:if="{{mode === 'vip'}}">
|
||||
<!-- 权益卡片 -->
|
||||
<view class="benefits-card-wrap">
|
||||
<view class="card benefits-card">
|
||||
<view class="benefits-title">
|
||||
<text class="benefit-crown">👑</text>
|
||||
<text>会员专属特权</text>
|
||||
</view>
|
||||
<view class="benefits-grid">
|
||||
<view class="benefit-item">
|
||||
<text class="benefit-check">✅</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">全频道特权</text>
|
||||
<text class="benefit-desc">所有频道自由听</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefit-item">
|
||||
<text class="benefit-check">🎧</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">纯净免广告</text>
|
||||
<text class="benefit-desc">收听无任何打扰</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefit-item">
|
||||
<text class="benefit-check">⬇️</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">音频全量下载</text>
|
||||
<text class="benefit-desc">支持离线随时听</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefit-item">
|
||||
<text class="benefit-check">⏰</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">晨间定时播</text>
|
||||
<text class="benefit-desc">专属智能闹钟</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 套餐 -->
|
||||
<view class="plan-section">
|
||||
<text class="plan-title">选择套餐</text>
|
||||
<view
|
||||
class="plan-card {{selectedPlan === 'vip-all' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="vip-all"
|
||||
>
|
||||
<view wx:if="{{selectedPlan === 'vip-all'}}" class="plan-badge">限时特惠</view>
|
||||
<view class="plan-info">
|
||||
<text class="plan-name">全频道连续包月</text>
|
||||
<text class="plan-desc">自动续费,随时可取消</text>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="price-amount"><text class="price-symbol">¥</text>19.9</text>
|
||||
<text class="price-original">¥29.9</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
<!-- ────── MODE: 频道订阅 ────── -->
|
||||
<block wx:else>
|
||||
<!-- 权益说明 -->
|
||||
<view class="benefits-card-wrap">
|
||||
<view class="card benefits-card">
|
||||
<view class="benefits-title">
|
||||
<text class="benefit-crown">🎙️</text>
|
||||
<text>订阅后专属权益</text>
|
||||
</view>
|
||||
<view class="benefit-item" style="width:100%; margin-bottom:16rpx;">
|
||||
<text class="benefit-check">✅</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">无限收听该频道全部节目</text>
|
||||
<text class="benefit-desc">订阅期内随时播放,无次数限制</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefit-item" style="width:100%; margin-bottom:16rpx;">
|
||||
<text class="benefit-check">🔔</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">新节目第一时间通知</text>
|
||||
<text class="benefit-desc">每日晨间准时推送</text>
|
||||
</view>
|
||||
</view>
|
||||
<view class="benefit-item" style="width:100%;">
|
||||
<text class="benefit-check">🎧</text>
|
||||
<view class="benefit-info">
|
||||
<text class="benefit-name">后台播放,不中断</text>
|
||||
<text class="benefit-desc">边听边做其他事</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 价格套餐 -->
|
||||
<view class="plan-section">
|
||||
<text class="plan-title">选择方案</text>
|
||||
|
||||
<!-- 包月 -->
|
||||
<view
|
||||
wx:if="{{monthlyPrice > 0}}"
|
||||
class="plan-card {{selectedPlan === 'monthly' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="monthly"
|
||||
>
|
||||
<view class="plan-info">
|
||||
<text class="plan-name">包月</text>
|
||||
<text class="plan-desc">按月订阅,随时取消</text>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="price-amount"><text class="price-symbol">¥</text>{{monthlyPrice}}</text>
|
||||
<text class="price-original">/ 月</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 包季 -->
|
||||
<view
|
||||
wx:if="{{quarterlyPrice > 0}}"
|
||||
class="plan-card {{selectedPlan === 'quarterly' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="quarterly"
|
||||
>
|
||||
<view wx:if="{{_quarterlySaving > 0}}" class="plan-badge plan-badge-save">省{{_quarterlySaving}}元</view>
|
||||
<view class="plan-info">
|
||||
<text class="plan-name">包季</text>
|
||||
<text class="plan-desc">约¥{{_quarterlyMonthly}}/月,比包月更划算</text>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="price-amount"><text class="price-symbol">¥</text>{{quarterlyPrice}}</text>
|
||||
<text class="price-original">/ 季</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 包年 -->
|
||||
<view
|
||||
wx:if="{{annualPrice > 0}}"
|
||||
class="plan-card {{selectedPlan === 'annual' ? 'selected' : ''}}"
|
||||
bindtap="selectPlan"
|
||||
data-plan="annual"
|
||||
>
|
||||
<view class="plan-badge plan-badge-hot">最受欢迎</view>
|
||||
<view wx:if="{{_annualSaving > 0}}" class="plan-badge plan-badge-save" style="right: 96rpx;">省{{_annualSaving}}元</view>
|
||||
<view class="plan-info">
|
||||
<text class="plan-name">包年</text>
|
||||
<text class="plan-desc">约¥{{_annualMonthly}}/月,最优惠</text>
|
||||
</view>
|
||||
<view class="plan-price">
|
||||
<text class="price-amount"><text class="price-symbol">¥</text>{{annualPrice}}</text>
|
||||
<text class="price-original">/ 年</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</block>
|
||||
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部支付栏(scroll-view 外,flex 布局固定底部,真机可靠) -->
|
||||
<view class="pay-bar" wx:if="{{!isVip || mode === 'channel'}}">
|
||||
<view class="pay-summary">
|
||||
<text class="pay-label">总计:</text>
|
||||
<text class="pay-amount">¥{{currentPrice}}</text>
|
||||
</view>
|
||||
<view class="pay-method">
|
||||
<text class="pay-method-text">使用 微信支付 支付</text>
|
||||
<text class="wechat-check">✓</text>
|
||||
</view>
|
||||
<t-button
|
||||
theme="primary"
|
||||
shape="round"
|
||||
block
|
||||
size="large"
|
||||
bind:tap="onPay"
|
||||
style="--td-button-primary-bg-color: #FF9D42; --td-button-primary-active-bg-color: #E88A35;"
|
||||
>
|
||||
{{mode === 'channel' ? '立即订阅并支付' : '立即开通并支付'}}
|
||||
</t-button>
|
||||
</view>
|
||||
</view>
|
||||
@@ -0,0 +1,287 @@
|
||||
/* 会员中心样式 */
|
||||
::-webkit-scrollbar { display: none !important; width: 0 !important; height: 0 !important; }
|
||||
|
||||
.vip-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: #FCFCFC;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 已是VIP */
|
||||
.vip-done {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100vh;
|
||||
padding: 40rpx;
|
||||
background: #FFFFFF;
|
||||
}
|
||||
.vip-done-icon {
|
||||
font-size: 120rpx;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.vip-done-title {
|
||||
font-size: 36rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
.vip-done-desc {
|
||||
font-size: 28rpx;
|
||||
color: #999;
|
||||
margin-bottom: 48rpx;
|
||||
}
|
||||
.done-back-btn {
|
||||
padding: 16rpx 48rpx;
|
||||
background: #F5F5F5;
|
||||
border-radius: 999rpx;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
color: #666;
|
||||
}
|
||||
.done-back-btn::after { border: none; }
|
||||
|
||||
/* 可滚动内容区 */
|
||||
.vip-scroll {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 支付栏(flex 子元素,不用 position:fixed,真机更可靠) */
|
||||
.pay-bar {
|
||||
flex-shrink: 0;
|
||||
background: #FFF;
|
||||
border-top: 1rpx solid #F5F5F5;
|
||||
padding: 20rpx 32rpx 48rpx;
|
||||
box-shadow: 0 -8rpx 32rpx rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
/* 深色头部 */
|
||||
.vip-hero {
|
||||
background: linear-gradient(to bottom, #1F2937, #111827);
|
||||
padding: 20rpx 40rpx 80rpx;
|
||||
border-radius: 0 0 64rpx 64rpx;
|
||||
box-shadow: 0 16rpx 40rpx rgba(0, 0, 0, 0.2);
|
||||
position: relative;
|
||||
}
|
||||
.back-btn {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 24rpx;
|
||||
width: 72rpx;
|
||||
height: 72rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
}
|
||||
.back-arrow {
|
||||
font-size: 48rpx;
|
||||
color: #FFF;
|
||||
font-weight: 300;
|
||||
}
|
||||
.vip-hero-title {
|
||||
display: block;
|
||||
font-size: 52rpx;
|
||||
font-weight: 800;
|
||||
color: #FFF;
|
||||
letter-spacing: -2rpx;
|
||||
margin-bottom: 12rpx;
|
||||
margin-top: 48rpx;
|
||||
}
|
||||
.vip-hero-desc {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 500;
|
||||
color: #FCD34D;
|
||||
}
|
||||
|
||||
/* 权益卡片 */
|
||||
.benefits-card-wrap {
|
||||
padding: 0 32rpx;
|
||||
margin-top: -40rpx;
|
||||
position: relative;
|
||||
z-index: 10;
|
||||
}
|
||||
.benefits-card {
|
||||
padding: 36rpx;
|
||||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.06);
|
||||
border: 1rpx solid rgba(251, 191, 36, 0.15);
|
||||
}
|
||||
.benefits-title {
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 28rpx;
|
||||
}
|
||||
.benefit-crown {
|
||||
font-size: 36rpx;
|
||||
margin-right: 12rpx;
|
||||
}
|
||||
|
||||
.benefits-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 28rpx 16rpx;
|
||||
}
|
||||
.benefit-item {
|
||||
width: calc(50% - 8rpx);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
.benefit-check {
|
||||
font-size: 28rpx;
|
||||
margin-right: 12rpx;
|
||||
flex-shrink: 0;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
.benefit-info {
|
||||
flex: 1;
|
||||
}
|
||||
.benefit-name {
|
||||
display: block;
|
||||
font-size: 24rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.benefit-desc {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
margin-top: 4rpx;
|
||||
}
|
||||
|
||||
/* 套餐 */
|
||||
.plan-section {
|
||||
padding: 32rpx 32rpx 0;
|
||||
}
|
||||
.plan-title {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 24rpx;
|
||||
padding-left: 8rpx;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 28rpx;
|
||||
border: 4rpx solid #F0F0F0;
|
||||
border-radius: 24rpx;
|
||||
background: #FFF;
|
||||
margin-bottom: 16rpx;
|
||||
position: relative;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.plan-card.selected {
|
||||
border-color: #FBBF24;
|
||||
background: rgba(251, 191, 36, 0.08);
|
||||
}
|
||||
.plan-badge {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
background: #FBBF24;
|
||||
color: #1F2937;
|
||||
font-size: 20rpx;
|
||||
font-weight: 700;
|
||||
padding: 6rpx 20rpx;
|
||||
border-radius: 0 20rpx 0 20rpx;
|
||||
}
|
||||
.plan-badge-hot {
|
||||
background: linear-gradient(135deg, #FF9D42, #FF7832);
|
||||
color: #FFF;
|
||||
}
|
||||
.plan-badge-save {
|
||||
background: #2ECC71;
|
||||
color: #FFF;
|
||||
border-radius: 0 0 0 20rpx;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
.plan-info {
|
||||
flex: 1;
|
||||
}
|
||||
.plan-name {
|
||||
display: block;
|
||||
font-size: 28rpx;
|
||||
font-weight: 700;
|
||||
color: #333;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.plan-desc {
|
||||
display: block;
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.plan-price {
|
||||
text-align: right;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.price-amount {
|
||||
font-size: 40rpx;
|
||||
font-weight: 800;
|
||||
color: #D97706;
|
||||
}
|
||||
.price-amount.single {
|
||||
font-size: 32rpx;
|
||||
color: #333;
|
||||
}
|
||||
.price-symbol {
|
||||
font-size: 24rpx;
|
||||
}
|
||||
.price-original {
|
||||
display: block;
|
||||
font-size: 20rpx;
|
||||
color: #CCC;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
|
||||
.pay-summary {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
margin-bottom: 12rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
.pay-label {
|
||||
font-size: 26rpx;
|
||||
font-weight: 700;
|
||||
color: #666;
|
||||
}
|
||||
.pay-amount {
|
||||
font-size: 36rpx;
|
||||
font-weight: 800;
|
||||
color: #D97706;
|
||||
margin-left: 4rpx;
|
||||
}
|
||||
.pay-method {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20rpx;
|
||||
padding: 0 8rpx;
|
||||
}
|
||||
.pay-method-text {
|
||||
font-size: 22rpx;
|
||||
color: #999;
|
||||
}
|
||||
.wechat-check {
|
||||
font-size: 18rpx;
|
||||
background: #07C160;
|
||||
color: #FFF;
|
||||
padding: 2rpx 8rpx;
|
||||
border-radius: 4rpx;
|
||||
margin-left: 8rpx;
|
||||
}
|
||||
Reference in New Issue
Block a user