/** * 早安电台 — 全局应用入口 * 负责:全局状态管理、音频管理初始化、登录态检查、事件总线 */ 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 '🌤️' }