This commit is contained in:
CNLuminous
2025-11-04 15:15:28 +08:00
commit a6af28e4c5
719 changed files with 570630 additions and 0 deletions
+125
View File
@@ -0,0 +1,125 @@
// 管理员API
const ADMIN_BASE_URL = ''
function getAdminToken() {
return localStorage.getItem('admin_token') || ''
}
async function request(url, options = {}) {
const token = getAdminToken()
const response = await fetch(`${ADMIN_BASE_URL}${url}`, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { 'Authorization': `Bearer ${token}` } : {}),
...(options.headers || {}),
},
})
if (!response.ok) {
if (response.status === 401 || response.status === 403) {
// token无效,清除并跳转登录
localStorage.removeItem('admin_token')
if (window.location.pathname !== '/admin/login') {
window.location.href = '/admin/login'
}
}
throw new Error(`HTTP error! status: ${response.status}`)
}
return await response.json()
}
/**
* 管理员登录
*/
export async function adminLogin(password) {
return await request('/api/admin/login', {
method: 'POST',
body: JSON.stringify({ password }),
})
}
/**
* 播放
*/
export async function adminPlay() {
return await request('/api/admin/playback/play', {
method: 'POST',
})
}
/**
* 暂停
*/
export async function adminPause() {
return await request('/api/admin/playback/pause', {
method: 'POST',
})
}
/**
* 发送公告
*/
export async function adminSendAnnouncement(announcement) {
return await request('/api/admin/announcement', {
method: 'POST',
body: JSON.stringify(announcement),
})
}
/**
* 设置特定用户的音量
*/
export async function adminSetUserVolume(userId, volume, muted) {
// 确保userId是字符串
const userIdStr = String(userId)
console.log('调用adminSetUserVolume:', userIdStr, volume, muted)
return await request(`/api/admin/user/${encodeURIComponent(userIdStr)}/volume`, {
method: 'POST',
body: JSON.stringify({ volume, muted }),
})
}
/**
* 设置特定用户的静音状态
*/
export async function adminSetUserMute(userId, muted) {
// 确保userId是字符串
const userIdStr = String(userId)
console.log('调用adminSetUserMute:', userIdStr, muted)
return await request(`/api/admin/user/${encodeURIComponent(userIdStr)}/mute`, {
method: 'POST',
body: JSON.stringify({ muted }),
})
}
/**
* 获取播放状态
*/
export async function adminGetPlaybackState() {
return await request('/api/admin/playback/state', {
method: 'GET',
})
}
/**
* 获取所有在线用户
*/
export async function adminGetAllUsers() {
return await request('/api/admin/users', {
method: 'GET',
})
}
/**
* 调整播放进度
*/
export async function adminSeek(time) {
return await request('/api/admin/playback/seek', {
method: 'POST',
body: JSON.stringify({ time }),
})
}
+187
View File
@@ -0,0 +1,187 @@
// Midway后端API
// 使用相对路径,通过Vite代理转发到后端
const BASE_URL = ''
async function request(path, options = {}) {
// 使用相对路径,Vite会通过代理转发到后端
const url = path.startsWith('/') ? path : `/${path}`
const { method = 'GET', body, params = {} } = options
// 构建URL,添加查询参数
let requestUrl = url
if (method === 'GET' && params && Object.keys(params).length > 0) {
const searchParams = new URLSearchParams()
Object.entries(params).forEach(([k, v]) => {
if (v !== undefined && v !== null && v !== '') {
searchParams.append(k, v)
}
})
const queryString = searchParams.toString()
if (queryString) {
requestUrl += `?${queryString}`
}
}
const config = {
method,
headers: {
'Content-Type': 'application/json',
},
}
if (body && method !== 'GET') {
config.body = JSON.stringify(body)
}
const res = await fetch(requestUrl, config)
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (!data.success) {
throw new Error(data.message || '请求失败')
}
return data.data
}
/**
* 搜索歌曲
*/
export async function searchSongs(keywords, limit = 20) {
return await request('/api/music/search', {
method: 'GET',
params: { keywords, limit }
})
}
/**
* 获取歌曲详情
*/
export async function getSongDetail(id) {
return await request(`/api/music/song/${id}/detail`, {
method: 'GET'
})
}
/**
* 获取歌曲播放URL
*/
export async function getSongUrlV1(id, level = 'standard') {
const data = await request(`/api/music/song/${id}/url`, {
method: 'GET',
params: { level }
})
return data.url || ''
}
/**
* 获取播放队列
*/
export async function getQueue() {
return await request('/api/music/queue', {
method: 'GET'
})
}
/**
* 添加到播放队列
*/
export async function addToQueue(song) {
const result = await request('/api/music/queue/add', {
method: 'POST',
body: { song }
})
return result
}
/**
* 播放下一首
*/
export async function playNext() {
const result = await request('/api/music/queue/play-next', {
method: 'POST'
})
return result
}
/**
* 清空队列
*/
export async function clearQueue() {
return await request('/api/music/queue/clear', {
method: 'POST'
})
}
/**
* 从队列移除歌曲
*/
export async function removeFromQueue(songId) {
return await request('/api/music/queue/remove', {
method: 'POST',
body: { songId }
})
}
/**
* 获取歌词
*/
export async function getLyric(id) {
const data = await request(`/api/music/song/${id}/lyric`, {
method: 'GET'
})
return data || { raw: '', tlyric: '' }
}
/**
* 设置当前播放歌曲
*/
export async function setPlaybackSong(songId) {
return await request('/api/music/playback/set-song', {
method: 'POST',
body: { songId }
})
}
/**
* 播放控制 - 播放
*/
export async function play() {
return await request('/api/music/playback/play', {
method: 'POST'
})
}
/**
* 播放控制 - 暂停
*/
export async function pause() {
return await request('/api/music/playback/pause', {
method: 'POST'
})
}
/**
* 播放控制 - 跳转时间
*/
export async function seek(time) {
return await request('/api/music/playback/seek', {
method: 'POST',
body: { time }
})
}
/**
* 获取当前播放状态
*/
export async function getPlaybackState() {
return await request('/api/music/playback/state', {
method: 'GET'
})
}
export function getBaseUrl() {
// 返回空字符串,使用相对路径通过代理
return ''
}
+139
View File
@@ -0,0 +1,139 @@
// WebSocket连接管理
// 使用相对路径,通过Vite代理转发到后端
// 在开发环境下,Vite会代理WebSocket连接
const WS_BASE_URL = import.meta.env.DEV
? `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}`
: 'ws://localhost:7001'
let ws = null
let reconnectTimer = null
let reconnectAttempts = 0
const MAX_RECONNECT_ATTEMPTS = 5
const RECONNECT_DELAY = 3000
/**
* 获取或创建客户端唯一标识
*/
function getClientId() {
let clientId = localStorage.getItem('client_id')
if (!clientId) {
// 生成唯一客户端ID(基于时间戳和随机数)
clientId = `client_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`
localStorage.setItem('client_id', clientId)
}
return clientId
}
/**
* 初始化WebSocket连接
*/
export function initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId) {
if (ws && ws.readyState === WebSocket.OPEN) {
console.log('WebSocket已经连接')
return
}
// 获取客户端唯一标识
const clientId = getClientId()
const wsUrl = `${WS_BASE_URL}/ws?clientId=${encodeURIComponent(clientId)}`
console.log('正在连接WebSocket:', wsUrl)
try {
ws = new WebSocket(wsUrl)
ws.onopen = () => {
console.log('WebSocket连接已建立')
reconnectAttempts = 0
}
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
if (data.type === 'queue_update') {
onQueueUpdate(data.data || [])
} else if (data.type === 'playback_state') {
if (onPlaybackState) {
onPlaybackState(data.data)
}
} else if (data.type === 'online_count') {
if (onOnlineCount) {
onOnlineCount(data.data?.count || 0)
}
} else if (data.type === 'announcement') {
if (onAnnouncement) {
onAnnouncement(data.data)
}
} else if (data.type === 'volume_control') {
if (onVolumeControl) {
onVolumeControl(data.data)
}
} else if (data.type === 'user_id') {
if (onUserId) {
onUserId(data.data?.userId)
}
}
} catch (error) {
console.error('解析WebSocket消息失败:', error)
}
}
ws.onerror = (error) => {
console.error('WebSocket错误:', error)
}
ws.onclose = () => {
console.log('WebSocket连接已关闭')
ws = null
// 尝试重连
if (reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
reconnectAttempts++
console.log(`尝试重连 (${reconnectAttempts}/${MAX_RECONNECT_ATTEMPTS})...`)
reconnectTimer = setTimeout(() => {
initWebSocket(onQueueUpdate, onPlaybackState, onOnlineCount, onAnnouncement, onVolumeControl, onUserId)
}, RECONNECT_DELAY)
} else {
console.error('WebSocket重连失败,已达到最大重试次数')
}
}
} catch (error) {
console.error('创建WebSocket连接失败:', error)
}
}
/**
* 关闭WebSocket连接
*/
export function closeWebSocket() {
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (ws) {
ws.close()
ws = null
}
reconnectAttempts = MAX_RECONNECT_ATTEMPTS // 阻止自动重连
}
/**
* 获取WebSocket连接状态
*/
export function getWebSocketStatus() {
if (!ws) return 'disconnected'
switch (ws.readyState) {
case WebSocket.CONNECTING:
return 'connecting'
case WebSocket.OPEN:
return 'connected'
case WebSocket.CLOSING:
return 'closing'
case WebSocket.CLOSED:
return 'closed'
default:
return 'unknown'
}
}