chore: bump version to 0.7.1

This commit is contained in:
ethan.chen
2025-05-27 14:16:48 +08:00
commit 63eda91fd6
103 changed files with 15868 additions and 0 deletions

View 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
};

View 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;

View 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;

View 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服务器URLws://或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;