first commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user