first commit

This commit is contained in:
Blizzard
2026-03-05 09:08:21 +08:00
commit 0a61c4ddec
2189 changed files with 38610 additions and 0 deletions
+172
View File
@@ -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' })
}
})
+12
View File
@@ -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": "频道广场"
}
+107
View File
@@ -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>
+209
View File
@@ -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;
}