first commit

This commit is contained in:
Blizzard
2026-03-05 09:08:21 +08:00
commit 0a61c4ddec
2189 changed files with 38610 additions and 0 deletions
+299
View File
@@ -0,0 +1,299 @@
/**
* 早安电台 — 全局应用入口
* 负责:全局状态管理、音频管理初始化、登录态检查、事件总线
*/
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 '🌤️'
}