300 lines
9.5 KiB
JavaScript
300 lines
9.5 KiB
JavaScript
/**
|
||
* 早安电台 — 全局应用入口
|
||
* 负责:全局状态管理、音频管理初始化、登录态检查、事件总线
|
||
*/
|
||
|
||
const audioManager = require('./utils/audioManager')
|
||
const api = require('./utils/api')
|
||
|
||
App({
|
||
globalData: {
|
||
// ======== 用户状态 ========
|
||
isLoggedIn: false,
|
||
isVip: false,
|
||
userInfo: null,
|
||
token: '',
|
||
|
||
// ======== 播放器状态 ========
|
||
activeContent: null, // 当前播放的音频内容对象
|
||
isPlaying: false,
|
||
currentTime: 0, // 当前播放时间(秒)
|
||
duration: 0, // 总时长(秒)
|
||
playbackRate: 1.0, // 播放速率
|
||
|
||
// ======== 系统信息 ========
|
||
statusBarHeight: 0,
|
||
navBarHeight: 0,
|
||
screenHeight: 0,
|
||
windowHeight: 0,
|
||
|
||
// ======== 位置与天气 ========
|
||
locationName: '', // 城市/区域名称
|
||
weather: null // { desc, temp, icon }
|
||
},
|
||
|
||
// ======== 事件总线 ========
|
||
_events: {},
|
||
|
||
/**
|
||
* 注册事件监听
|
||
* @param {string} event - 事件名
|
||
* @param {Function} callback - 回调函数
|
||
*/
|
||
on(event, callback) {
|
||
if (!this._events[event]) {
|
||
this._events[event] = []
|
||
}
|
||
this._events[event].push(callback)
|
||
},
|
||
|
||
/**
|
||
* 移除事件监听
|
||
*/
|
||
off(event, callback) {
|
||
if (!this._events[event]) return
|
||
if (callback) {
|
||
this._events[event] = this._events[event].filter(cb => cb !== callback)
|
||
} else {
|
||
this._events[event] = []
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 触发事件
|
||
*/
|
||
emit(event, data) {
|
||
if (!this._events[event]) return
|
||
this._events[event].forEach(cb => cb(data))
|
||
},
|
||
|
||
onLaunch() {
|
||
// 获取系统信息,用于自定义导航栏计算
|
||
try {
|
||
const systemInfo = wx.getWindowInfo()
|
||
const menuButton = wx.getMenuButtonBoundingClientRect()
|
||
this.globalData.statusBarHeight = systemInfo.statusBarHeight || 0
|
||
this.globalData.navBarHeight = (menuButton.top - systemInfo.statusBarHeight) * 2 + menuButton.height
|
||
this.globalData.screenHeight = systemInfo.screenHeight
|
||
this.globalData.windowHeight = systemInfo.windowHeight
|
||
} catch (e) {
|
||
this.globalData.statusBarHeight = 44
|
||
this.globalData.navBarHeight = 44
|
||
}
|
||
|
||
// 初始化音频管理器
|
||
audioManager.init(this)
|
||
|
||
// 冷启动:获取位置+天气(当天内只请求一次)
|
||
this._fetchLocationWeather()
|
||
},
|
||
|
||
/**
|
||
* 获取位置和天气
|
||
* 策略:当天内只请求一次,结果缓存于 Storage 和 globalData
|
||
*/
|
||
_fetchLocationWeather() {
|
||
const self = this
|
||
const today = new Date().toLocaleDateString()
|
||
let cached = null
|
||
try { cached = wx.getStorageSync('locationWeatherCache') } catch (e) { }
|
||
|
||
// 当天有效缓存,直接用(不发请求)
|
||
if (cached && cached.date === today && cached.locationName) {
|
||
self.globalData.locationName = cached.locationName
|
||
self.globalData.weather = cached.weather || null
|
||
self.emit('locationWeatherReady', {
|
||
locationName: cached.locationName,
|
||
weather: cached.weather || null
|
||
})
|
||
return
|
||
}
|
||
|
||
// 清掉无效缓存
|
||
try { wx.removeStorageSync('locationWeatherCache') } catch (e) { }
|
||
|
||
// 请求微信定位
|
||
wx.getLocation({
|
||
type: 'gcj02',
|
||
isHighAccuracy: false,
|
||
success(locRes) {
|
||
const longitude = locRes.longitude
|
||
const latitude = locRes.latitude
|
||
|
||
// 调用后端:经纬度 → 城市名 + adcode
|
||
api.getLocation(longitude, latitude)
|
||
.then(function (res) {
|
||
if (!res || res.code !== 200 || !res.data) return Promise.reject('位置解析失败')
|
||
|
||
const locationName = res.data.city || res.data.district || res.data.province || '未知'
|
||
const adcode = res.data.adcode || ''
|
||
|
||
self.globalData.locationName = locationName
|
||
// 先更新一次(有城市名,天气还没来)
|
||
self.emit('locationWeatherReady', { locationName, weather: null })
|
||
|
||
if (!adcode) return Promise.reject('无 adcode,跳过天气')
|
||
return api.getWeather(adcode)
|
||
})
|
||
.then(function (res) {
|
||
if (!res || res.code !== 200 || !res.data) return
|
||
|
||
const w = res.data
|
||
const weather = {
|
||
desc: w.weather || w.desc || '',
|
||
temp: w.temperature || w.temp || '',
|
||
icon: _weatherIcon(w.weather || w.desc || '')
|
||
}
|
||
|
||
self.globalData.weather = weather
|
||
self.emit('locationWeatherReady', {
|
||
locationName: self.globalData.locationName,
|
||
weather
|
||
})
|
||
|
||
// 存入 Storage 供当日复用
|
||
try {
|
||
wx.setStorageSync('locationWeatherCache', {
|
||
date: today,
|
||
locationName: self.globalData.locationName,
|
||
weather
|
||
})
|
||
} catch (e) { }
|
||
})
|
||
.catch(function (err) {
|
||
console.warn('[位置天气] 请求失败:', err)
|
||
})
|
||
},
|
||
fail(err) {
|
||
console.warn('[位置天气] wx.getLocation 失败:', err.errMsg)
|
||
}
|
||
})
|
||
},
|
||
|
||
// ======== 用户相关方法 ========
|
||
|
||
/**
|
||
* 小程序静默登录
|
||
* wx.login() → code → 后端 /auth/miniLogin → token + user
|
||
*/
|
||
login() {
|
||
const self = this
|
||
return new Promise((resolve, reject) => {
|
||
wx.login({
|
||
success(loginRes) {
|
||
if (!loginRes.code) {
|
||
reject(new Error('wx.login 获取 code 失败'))
|
||
return
|
||
}
|
||
api.miniLogin(loginRes.code).then(res => {
|
||
if (res.code === 200 && res.data) {
|
||
const { token, user } = res.data
|
||
self.globalData.isLoggedIn = true
|
||
self.globalData.token = token
|
||
self.globalData.userInfo = user
|
||
wx.setStorageSync('token', token)
|
||
self.emit('loginStateChange', { isLoggedIn: true })
|
||
resolve(res.data)
|
||
} else {
|
||
reject(new Error(res.msg || '登录失败'))
|
||
}
|
||
}).catch(reject)
|
||
},
|
||
fail(err) {
|
||
reject(err)
|
||
}
|
||
})
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 订阅频道(调用后端)
|
||
*/
|
||
subscribeToDomain(channelId) {
|
||
const self = this
|
||
return api.subscribe(channelId).then(res => {
|
||
if (res.code === 200) {
|
||
self.emit('subscriptionChange', {})
|
||
return true
|
||
}
|
||
wx.showToast({ title: res.msg || '订阅失败', icon: 'none' })
|
||
return false
|
||
}).catch(err => {
|
||
wx.showToast({ title: err.message || '订阅失败', icon: 'none' })
|
||
return false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 取消订阅频道(调用后端)
|
||
*/
|
||
unsubscribeFromDomain(channelId) {
|
||
const self = this
|
||
return api.unsubscribe(channelId).then(res => {
|
||
if (res.code === 200) {
|
||
self.emit('subscriptionChange', {})
|
||
return true
|
||
}
|
||
wx.showToast({ title: res.msg || '退订失败', icon: 'none' })
|
||
return false
|
||
}).catch(err => {
|
||
wx.showToast({ title: err.message || '退订失败', icon: 'none' })
|
||
return false
|
||
})
|
||
},
|
||
|
||
/**
|
||
* 升级VIP
|
||
*/
|
||
upgradeVip() {
|
||
this.globalData.isVip = true
|
||
this.emit('vipChange', { isVip: true })
|
||
},
|
||
|
||
// ======== 播放相关方法(代理到 audioManager) ========
|
||
|
||
/**
|
||
* 播放指定内容
|
||
*/
|
||
playContent(content) {
|
||
audioManager.playContent(content)
|
||
},
|
||
|
||
/**
|
||
* 切换播放/暂停
|
||
*/
|
||
togglePlay() {
|
||
audioManager.togglePlay()
|
||
},
|
||
|
||
/**
|
||
* 跳转到指定时间
|
||
*/
|
||
seekTo(time) {
|
||
audioManager.seekTo(time)
|
||
},
|
||
|
||
/**
|
||
* 设置播放速率
|
||
*/
|
||
setPlaybackRate(rate) {
|
||
audioManager.setPlaybackRate(rate)
|
||
}
|
||
})
|
||
|
||
/**
|
||
* 天气描述 → emoji 图标
|
||
* @param {string} desc - 天气描述,如"晴"、"多云"、"阵雨"
|
||
*/
|
||
function _weatherIcon(desc) {
|
||
if (!desc) return '🌤'
|
||
if (/晴/.test(desc)) return '☀️'
|
||
if (/多云|阴/.test(desc)) return '☁️'
|
||
if (/雷|电/.test(desc)) return '⛈️'
|
||
if (/暴雨|大雨/.test(desc)) return '🌧️'
|
||
if (/小雨|阵雨|中雨|雨/.test(desc)) return '🌦️'
|
||
if (/雪|冰/.test(desc)) return '❄️'
|
||
if (/雾|霾/.test(desc)) return '🌫️'
|
||
if (/风|大风/.test(desc)) return '💨'
|
||
return '🌤️'
|
||
}
|