chore: bump version to 0.7.1
This commit is contained in:
119
frontend/src/utils/musicControl.js
Normal file
119
frontend/src/utils/musicControl.js
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* 音乐播放控制模块
|
||||
* 负责与后端交互控制Apple Music播放
|
||||
*/
|
||||
|
||||
/**
|
||||
* 发送播放控制请求到后端
|
||||
* @param {string} action - 控制动作 ('playpause'|'previous'|'next'|'seek')
|
||||
* @param {number} [position] - 当seek时的目标位置(秒)
|
||||
* @returns {Promise<Object>} - 请求结果
|
||||
*/
|
||||
import request from './request';
|
||||
async function sendControlCommand(action, position = null) {
|
||||
try {
|
||||
const payload = { action };
|
||||
|
||||
// 如果是seek操作且提供了position参数
|
||||
if (action === 'seek' && position !== null) {
|
||||
payload.position = position;
|
||||
}
|
||||
|
||||
const response = await request({
|
||||
url: '/control',
|
||||
method: 'POST',
|
||||
data: payload
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`控制请求失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
// 检查结果状态,如果Apple Music未运行,提供明确的错误信息
|
||||
if (result.status === 'error' && result.message === 'notrunning') {
|
||||
console.warn('Apple Music未运行,无法执行控制命令');
|
||||
throw new Error('Apple Music未运行,请先启动Apple Music应用');
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error(`${action}控制请求错误:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换播放/暂停状态
|
||||
* @returns {Promise<Object>} - 请求结果
|
||||
*/
|
||||
function togglePlayPause() {
|
||||
return sendControlCommand('playpause');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到上一首曲目
|
||||
* @returns {Promise<Object>} - 请求结果
|
||||
*/
|
||||
function previousTrack() {
|
||||
return sendControlCommand('previous');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到下一首曲目
|
||||
* @returns {Promise<Object>} - 请求结果
|
||||
*/
|
||||
function nextTrack(baseUrl) {
|
||||
return sendControlCommand('next');
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定播放位置
|
||||
* @param {number} position - 目标位置(秒)
|
||||
* @returns {Promise<Object>} - 请求结果
|
||||
*/
|
||||
function seekTo(position) {
|
||||
return sendControlCommand('seek', position);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据进度条点击位置计算新的播放位置并进行跳转
|
||||
* @param {Event} event - 点击事件对象
|
||||
* @param {number} duration - 当前曲目总时长(秒)
|
||||
* @returns {Promise<Object>|null} - 请求结果或null(如果duration无效)
|
||||
*/
|
||||
function seekToByClick(event, duration) {
|
||||
if (duration <= 0) return null;
|
||||
|
||||
// 计算点击位置占进度条的比例
|
||||
const progressBar = event.target.closest('.progress-bar');
|
||||
const clickX = event.clientX - progressBar.getBoundingClientRect().left;
|
||||
const percentage = clickX / progressBar.clientWidth;
|
||||
|
||||
// 计算对应的播放位置
|
||||
const newPosition = percentage * duration;
|
||||
|
||||
return seekTo(newPosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化秒数为mm:ss格式
|
||||
* @param {number} seconds - 秒数
|
||||
* @returns {string} - 格式化后的时间字符串
|
||||
*/
|
||||
function formatTime(seconds) {
|
||||
if (!seconds) return '00:00';
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
export {
|
||||
togglePlayPause,
|
||||
previousTrack,
|
||||
nextTrack,
|
||||
seekTo,
|
||||
seekToByClick,
|
||||
formatTime
|
||||
};
|
||||
103
frontend/src/utils/positionTracker.js
Normal file
103
frontend/src/utils/positionTracker.js
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* 播放位置跟踪模块
|
||||
* 负责在WebSocket更新之间本地更新播放位置
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
class PositionTracker {
|
||||
constructor() {
|
||||
this.position = ref(0);
|
||||
this.duration = ref(0);
|
||||
this.playbackStatus = ref('stopped');
|
||||
this.updateTimer = null;
|
||||
this.updateInterval = 100; // 每100毫秒更新一次位置
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新播放状态和位置信息
|
||||
* @param {string} status - 播放状态 ('playing'|'paused'|'stopped'|'notrunning')
|
||||
* @param {number} pos - 当前播放位置(秒)
|
||||
* @param {number} dur - 曲目总时长(秒)
|
||||
*/
|
||||
updateState(status, pos, dur) {
|
||||
this.playbackStatus.value = status;
|
||||
|
||||
if (pos !== undefined && pos !== null) {
|
||||
this.position.value = pos;
|
||||
}
|
||||
|
||||
if (dur !== undefined && dur !== null) {
|
||||
this.duration.value = dur;
|
||||
}
|
||||
|
||||
// 如果状态变化,管理定时器
|
||||
if (status === 'playing') {
|
||||
this.startPositionUpdate();
|
||||
} else {
|
||||
this.stopPositionUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地更新播放位置
|
||||
*/
|
||||
updatePositionLocally() {
|
||||
if (this.playbackStatus.value === 'playing') {
|
||||
// 每次更新位置(基于更新间隔计算增量)
|
||||
const increment = this.updateInterval / 1000; // 转换为秒
|
||||
this.position.value += increment;
|
||||
|
||||
// 确保不超过总时长
|
||||
if (this.position.value > this.duration.value && this.duration.value > 0) {
|
||||
this.position.value = this.duration.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动位置更新定时器
|
||||
*/
|
||||
startPositionUpdate() {
|
||||
// 先停止可能存在的定时器
|
||||
this.stopPositionUpdate();
|
||||
|
||||
// 创建新的定时器
|
||||
this.updateTimer = setInterval(() => {
|
||||
this.updatePositionLocally();
|
||||
}, this.updateInterval);
|
||||
|
||||
console.log('开始本地位置更新计时器');
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止位置更新定时器
|
||||
*/
|
||||
stopPositionUpdate() {
|
||||
if (this.updateTimer) {
|
||||
clearInterval(this.updateTimer);
|
||||
this.updateTimer = null;
|
||||
console.log('停止本地位置更新计时器');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算播放进度百分比
|
||||
* @returns {number} 播放进度百分比 (0-100)
|
||||
*/
|
||||
getProgressPercentage() {
|
||||
if (this.duration.value <= 0) return 0;
|
||||
return (this.position.value / this.duration.value) * 100;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
cleanup() {
|
||||
this.stopPositionUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const positionTracker = new PositionTracker();
|
||||
|
||||
export default positionTracker;
|
||||
33
frontend/src/utils/request.js
Normal file
33
frontend/src/utils/request.js
Normal file
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* @Date: 2025-05-22 13:56:20
|
||||
* @LastEditors: 陈子健
|
||||
* @LastEditTime: 2025-05-22 17:08:29
|
||||
* @FilePath: /mac-lyric-vue/frontend/src/utils/request.js
|
||||
*/
|
||||
import axios from 'axios';
|
||||
import { useLyricsStore } from '../store';
|
||||
|
||||
const request = (options) => {
|
||||
const lyricsStore = useLyricsStore();
|
||||
const config = {
|
||||
baseURL: `${lyricsStore.baseUrl}`,
|
||||
url: `${options.url}`,
|
||||
method: options.method,
|
||||
data: options.data,
|
||||
params: options.params
|
||||
};
|
||||
const axiosInstance = axios.create();
|
||||
axiosInstance.interceptors.request.use(config => {
|
||||
console.log('config', config);
|
||||
return config;
|
||||
});
|
||||
axiosInstance.interceptors.response.use(response => {
|
||||
if (response.status === 200) {
|
||||
return response.data;
|
||||
}
|
||||
return response;
|
||||
});
|
||||
return axiosInstance(config);
|
||||
};
|
||||
|
||||
export default request;
|
||||
236
frontend/src/utils/websocketService.js
Normal file
236
frontend/src/utils/websocketService.js
Normal file
@@ -0,0 +1,236 @@
|
||||
/**
|
||||
* WebSocket通信服务模块
|
||||
* 负责与后端WebSocket通信、连接管理和消息处理
|
||||
*/
|
||||
import { ref } from 'vue';
|
||||
|
||||
class WebSocketService {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.wsUrl = ref('ws://127.0.0.1:5000/ws');
|
||||
this.reconnectInterval = 1000; // 重连间隔(毫秒)
|
||||
this.reconnectTimer = null;
|
||||
this.pingInterval = null;
|
||||
this.messageHandlers = new Map(); // 消息处理器映射
|
||||
this.isConnecting = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置WebSocket服务器URL
|
||||
* @param {string} url - WebSocket服务器URL(ws://或wss://协议)
|
||||
*/
|
||||
setWsUrl(url) {
|
||||
this.wsUrl.value = url;
|
||||
console.log('WebSocket URL已设置为:', url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册消息处理器
|
||||
* @param {string} messageType - 消息类型
|
||||
* @param {Function} handler - 处理函数,接收消息数据参数
|
||||
*/
|
||||
registerHandler(messageType, handler) {
|
||||
this.messageHandlers.set(messageType, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除消息处理器
|
||||
* @param {string} messageType - 要移除处理器的消息类型
|
||||
*/
|
||||
removeHandler(messageType) {
|
||||
this.messageHandlers.delete(messageType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有消息处理器
|
||||
*/
|
||||
clearHandlers() {
|
||||
this.messageHandlers.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接到WebSocket服务器
|
||||
*/
|
||||
connect() {
|
||||
if (this.isConnecting || (this.socket && this.socket.readyState === WebSocket.OPEN)) {
|
||||
console.log('WebSocket已连接或正在连接中,不需要重新连接');
|
||||
return;
|
||||
}
|
||||
|
||||
this.isConnecting = true;
|
||||
|
||||
try {
|
||||
console.log('尝试连接WebSocket:', this.wsUrl.value);
|
||||
this.socket = new WebSocket(this.wsUrl.value);
|
||||
|
||||
this.socket.onopen = () => {
|
||||
console.log('WebSocket连接成功');
|
||||
this.isConnecting = false;
|
||||
|
||||
// 发送初始化请求,请求当前状态
|
||||
this.sendMessage({ type: 'request_status' });
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
// 启动定期ping,保持连接活跃
|
||||
this.startPingInterval();
|
||||
};
|
||||
|
||||
this.socket.onmessage = (event) => {
|
||||
this.handleMessage(event);
|
||||
};
|
||||
|
||||
this.socket.onclose = (event) => {
|
||||
console.log(`WebSocket连接关闭,代码: ${event.code},原因: ${event.reason}`);
|
||||
|
||||
this.isConnecting = false;
|
||||
this.socket = null;
|
||||
|
||||
// 清理ping定时器
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
|
||||
// 尝试重连
|
||||
this.scheduleReconnect();
|
||||
};
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
console.error('WebSocket错误:', error);
|
||||
this.isConnecting = false;
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('创建WebSocket连接错误:', error);
|
||||
this.isConnecting = false;
|
||||
|
||||
// 连接失败时也应设置重连
|
||||
this.scheduleReconnect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重新连接
|
||||
*/
|
||||
scheduleReconnect() {
|
||||
if (!this.reconnectTimer) {
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
console.log('尝试重新连接WebSocket...');
|
||||
this.reconnectTimer = null;
|
||||
this.connect();
|
||||
}, this.reconnectInterval);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动定期发送ping消息的定时器
|
||||
*/
|
||||
startPingInterval() {
|
||||
// 清理现有的ping定时器
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
}
|
||||
|
||||
// 每10秒发送一次ping消息保持连接
|
||||
this.pingInterval = setInterval(() => {
|
||||
this.sendPing();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送ping消息
|
||||
*/
|
||||
sendPing() {
|
||||
if (this.socket?.readyState === WebSocket.OPEN) {
|
||||
console.log('发送ping消息保持连接');
|
||||
this.sendMessage({ type: 'ping' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息到WebSocket服务器
|
||||
* @param {Object} data - 要发送的消息对象
|
||||
* @returns {boolean} - 是否成功发送
|
||||
*/
|
||||
sendMessage(data) {
|
||||
if (!this.socket || this.socket.readyState !== WebSocket.OPEN) {
|
||||
console.warn('WebSocket未连接,无法发送消息');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
this.socket.send(JSON.stringify(data));
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('发送WebSocket消息失败:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的WebSocket消息
|
||||
* @param {MessageEvent} event - WebSocket消息事件
|
||||
*/
|
||||
handleMessage(event) {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
console.log('收到WebSocket消息类型:', data.type);
|
||||
|
||||
// 调用对应类型的消息处理器
|
||||
if (data.type && this.messageHandlers.has(data.type)) {
|
||||
this.messageHandlers.get(data.type)(data);
|
||||
} else {
|
||||
console.log('没有对应的处理器,消息类型:', data.type);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('处理WebSocket消息错误:', error, '原始数据:', event.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并恢复WebSocket连接
|
||||
*/
|
||||
checkAndRestoreConnection() {
|
||||
console.log('检查WebSocket连接状态');
|
||||
|
||||
if (!this.socket || this.socket.readyState === WebSocket.CLOSED || this.socket.readyState === WebSocket.CLOSING) {
|
||||
console.log('WebSocket未连接或已关闭,尝试重新连接');
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭WebSocket连接
|
||||
*/
|
||||
disconnect() {
|
||||
// 清理ping定时器
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval);
|
||||
this.pingInterval = null;
|
||||
}
|
||||
|
||||
// 清理重连定时器
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer);
|
||||
this.reconnectTimer = null;
|
||||
}
|
||||
|
||||
// 关闭WebSocket连接
|
||||
if (this.socket) {
|
||||
try {
|
||||
this.socket.close();
|
||||
} catch (error) {
|
||||
console.error('关闭WebSocket连接错误:', error);
|
||||
}
|
||||
this.socket = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const websocketService = new WebSocketService();
|
||||
|
||||
export default websocketService;
|
||||
Reference in New Issue
Block a user