Files
2026-04-28 10:32:19 +08:00

359 lines
12 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 首页 — 订阅频道 + 免费频道
*
* 数据来源:
* - 订阅列表: 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: ''
}
}
})