359 lines
12 KiB
JavaScript
359 lines
12 KiB
JavaScript
/**
|
||
* 首页 — 订阅频道 + 免费频道
|
||
*
|
||
* 数据来源:
|
||
* - 订阅列表: 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 初始化时同步调用,避免闪烁
|
||
*/
|
||
function _computeGreeting() {
|
||
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)]
|
||
}
|
||
|
||
Page({
|
||
data: {
|
||
// 同步计算,第一帧就正确,避免闪烁
|
||
greetingSub: _computeGreeting(),
|
||
statusBarHeight: app.globalData.statusBarHeight || 0,
|
||
locationName: '',
|
||
weather: null,
|
||
dateDisplay: '',
|
||
weekDay: '',
|
||
subscribedData: [],
|
||
freeChannels: [],
|
||
isPlaying: false,
|
||
isVip: false,
|
||
loadingSub: true,
|
||
loadingFree: true
|
||
},
|
||
|
||
|
||
|
||
onShow() {
|
||
const gd = app.globalData
|
||
this.setData({
|
||
greetingSub: _computeGreeting(), // 每次进页随机刷新文案
|
||
dateDisplay: util.getDateDisplay(),
|
||
weekDay: util.getWeekDay(),
|
||
locationName: gd.locationName || '',
|
||
weather: gd.weather || null,
|
||
isVip: gd.isVip || false
|
||
// statusBarHeight 小程序运行期间不会变化,无需重设
|
||
})
|
||
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 })
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 拉取频道列表(Section 2)
|
||
* VIP 用户:全部频道;普通用户:仅免费频道
|
||
*/
|
||
_loadFreeChannels() {
|
||
const self = this
|
||
const gd = app.globalData
|
||
const isFirstLoad = self.data.freeChannels.length === 0
|
||
|
||
if (isFirstLoad) {
|
||
self.setData({ loadingFree: true })
|
||
}
|
||
|
||
// VIP 用户加载全量频道,普通用户仅免费频道
|
||
var apiCall = gd.isVip
|
||
? api.getChannelList({ current: 1, pageSize: 50 })
|
||
: api.getFreeChannelList({ current: 1, pageSize: 20 })
|
||
|
||
apiCall
|
||
.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) {
|
||
var cover = ch.cover || '📻'
|
||
return {
|
||
id: ch.id,
|
||
name: ch.name || '未命名',
|
||
cover: cover,
|
||
_isDefaultCover: cover === '📻',
|
||
_initial: (ch.name || '频').substring(0, 1),
|
||
bgColor: self._genColor(ch.id),
|
||
isFree: ch.isFree,
|
||
isVipOnly: ch.isVipOnly
|
||
}
|
||
})
|
||
|
||
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
|
||
const cover = channel.cover || '📻'
|
||
const isDefaultCover = 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,
|
||
_isDefaultCover: isDefaultCover,
|
||
_initial: (channel.name || '频').substring(0, 1),
|
||
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 })
|
||
},
|
||
|
||
// ===================== 工具方法 =====================
|
||
// _computeGreeting 已提至模块级,可在 data 初始化时直接调用
|
||
|
||
// ===================== 左滑操作 =====================
|
||
|
||
onSwipeStart(e) {
|
||
this._touchStartX = e.touches[0].clientX
|
||
this._touchStartY = e.touches[0].clientY
|
||
this._swiping = false
|
||
},
|
||
|
||
onSwipeMove(e) {
|
||
var dx = e.touches[0].clientX - this._touchStartX
|
||
var dy = e.touches[0].clientY - this._touchStartY
|
||
// 水平滑动距离大于垂直才算滑动
|
||
if (Math.abs(dx) < Math.abs(dy)) return
|
||
this._swiping = true
|
||
var idx = e.currentTarget.dataset.idx
|
||
var x = Math.max(-180, Math.min(0, dx))
|
||
var key = 'subscribedData[' + idx + ']._swipeX'
|
||
this.setData({ [key]: x })
|
||
},
|
||
|
||
onSwipeEnd(e) {
|
||
if (!this._swiping) return
|
||
var idx = e.currentTarget.dataset.idx
|
||
var cur = this.data.subscribedData[idx]._swipeX || 0
|
||
var key = 'subscribedData[' + idx + ']._swipeX'
|
||
// 超过 90rpx 则展开操作区,否则回弹
|
||
this.setData({ [key]: cur < -90 ? -180 : 0 })
|
||
},
|
||
|
||
onUnsubscribe(e) {
|
||
var id = e.currentTarget.dataset.id
|
||
var name = e.currentTarget.dataset.name || ''
|
||
wx.showModal({
|
||
title: '取消订阅',
|
||
content: '确定取消订阅「' + name + '」频道吗?',
|
||
success: function (res) {
|
||
if (res.confirm) {
|
||
app.unsubscribeFromDomain(id)
|
||
}
|
||
}
|
||
})
|
||
},
|
||
|
||
// ===================== 分享钩子 =====================
|
||
onShareAppMessage() {
|
||
return {
|
||
title: '全声汇 - 听见世界的声音',
|
||
path: '/pages/index/index'
|
||
}
|
||
},
|
||
|
||
onShareTimeline() {
|
||
return {
|
||
title: '全声汇 - 听见世界的声音',
|
||
query: ''
|
||
}
|
||
}
|
||
})
|