保存
This commit is contained in:
170
public/apps/music-player/README.md
Normal file
170
public/apps/music-player/README.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# 音乐播放器 - 外置应用案例
|
||||
|
||||
这是一个完整的外置应用案例,展示了如何在Vue桌面系统中开发外置应用。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🎵 音乐播放功能
|
||||
- **音频格式支持**: 支持所有浏览器兼容的音频格式(MP3、WAV、OGG等)
|
||||
- **播放控制**: 播放、暂停、上一曲、下一曲
|
||||
- **进度控制**: 可拖拽的进度条,支持跳转到任意位置
|
||||
- **音量控制**: 音量滑块调节,实时显示音量百分比
|
||||
|
||||
### 🎲 播放模式
|
||||
- **随机播放**: 支持随机播放模式切换
|
||||
- **重复播放**: 三种重复模式(关闭、单曲循环、列表循环)
|
||||
- **播放列表**: 完整的播放列表管理
|
||||
|
||||
### 🎨 用户界面
|
||||
- **现代设计**: 采用渐变色彩和毛玻璃效果
|
||||
- **响应式布局**: 支持窗口大小调整
|
||||
- **直观操作**: 清晰的视觉反馈和状态提示
|
||||
- **窗口控制**: 最小化、最大化、关闭按钮
|
||||
|
||||
### ⌨️ 交互体验
|
||||
- **键盘快捷键**:
|
||||
- `Space`: 播放/暂停
|
||||
- `←/→`: 上一曲/下一曲
|
||||
- `↑/↓`: 音量增减
|
||||
- **拖拽支持**: 支持文件拖拽添加(计划中)
|
||||
- **状态持久化**: 播放列表自动保存
|
||||
|
||||
### 🔧 系统集成
|
||||
- **系统SDK集成**: 与主系统的无缝集成
|
||||
- **窗口控制**: 通过系统API控制窗口状态
|
||||
- **系统通知**: 状态变化的系统通知
|
||||
- **数据存储**: 使用系统存储API保存用户数据
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
music-player/
|
||||
├── manifest.json # 应用清单文件
|
||||
├── index.html # 主HTML页面
|
||||
├── style.css # 样式文件
|
||||
├── app.js # 主要逻辑文件
|
||||
└── README.md # 说明文档
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 应用清单 (manifest.json)
|
||||
定义了应用的基本信息、权限要求、窗口配置等:
|
||||
- 应用ID、名称、版本信息
|
||||
- 窗口大小和行为配置
|
||||
- 所需权限(存储、文件读取、通知)
|
||||
- 分类和关键词
|
||||
|
||||
### 用户界面 (index.html + style.css)
|
||||
- **布局结构**: 采用Flexbox布局,分为头部、主要区域、播放列表和状态栏
|
||||
- **样式设计**: 使用CSS渐变、阴影、过渡动画等现代效果
|
||||
- **响应式**: 针对不同屏幕尺寸优化显示
|
||||
|
||||
### 应用逻辑 (app.js)
|
||||
核心类 `MusicPlayer` 实现了所有功能:
|
||||
|
||||
#### 初始化流程
|
||||
1. DOM就绪检测
|
||||
2. 系统SDK连接
|
||||
3. 事件监听器设置
|
||||
4. 界面状态初始化
|
||||
|
||||
#### 音频处理
|
||||
- 使用HTML5 Audio API
|
||||
- 事件驱动的状态管理
|
||||
- 错误处理和恢复机制
|
||||
|
||||
#### 播放列表管理
|
||||
- 文件选择和验证
|
||||
- 内存中的播放队列
|
||||
- 持久化存储支持
|
||||
|
||||
## 系统SDK集成
|
||||
|
||||
该应用展示了如何正确使用系统SDK:
|
||||
|
||||
### 窗口控制
|
||||
```javascript
|
||||
// 窗口操作
|
||||
this.systemSDK.window.minimize();
|
||||
this.systemSDK.window.toggleMaximize();
|
||||
this.systemSDK.window.close();
|
||||
|
||||
// 关闭事件监听
|
||||
this.systemSDK.window.onBeforeClose(() => {
|
||||
this.cleanup();
|
||||
return true;
|
||||
});
|
||||
```
|
||||
|
||||
### 系统通知
|
||||
```javascript
|
||||
this.systemSDK.notification.show({
|
||||
title: '音乐播放器',
|
||||
message: '应用已启动,准备播放音乐!',
|
||||
type: 'info'
|
||||
});
|
||||
```
|
||||
|
||||
### 数据存储
|
||||
```javascript
|
||||
// 保存数据
|
||||
this.systemSDK.storage.setItem('music-player-playlist', data);
|
||||
|
||||
// 读取数据
|
||||
const data = this.systemSDK.storage.getItem('music-player-playlist');
|
||||
```
|
||||
|
||||
## 安全考虑
|
||||
|
||||
- **文件访问**: 通过文件选择器安全地访问用户文件
|
||||
- **内存管理**: 及时释放对象URL避免内存泄漏
|
||||
- **错误处理**: 完整的错误捕获和用户友好的错误提示
|
||||
- **权限控制**: 仅请求必要的系统权限
|
||||
|
||||
## 性能优化
|
||||
|
||||
- **懒加载**: 音频文件仅在需要时加载
|
||||
- **事件防抖**: 避免频繁的状态更新
|
||||
- **内存回收**: 应用关闭时清理所有资源
|
||||
- **DOM优化**: 高效的DOM操作和事件委托
|
||||
|
||||
## 扩展可能
|
||||
|
||||
这个案例可以进一步扩展:
|
||||
|
||||
1. **音频可视化**: 添加频谱显示
|
||||
2. **歌词显示**: 支持LRC歌词文件
|
||||
3. **均衡器**: 音频效果控制
|
||||
4. **在线音乐**: 集成在线音乐服务
|
||||
5. **播放历史**: 记录播放统计
|
||||
6. **主题切换**: 多种UI主题
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 本地测试
|
||||
1. 将整个文件夹放置到 `public/apps/` 目录下
|
||||
2. 启动Vue桌面系统
|
||||
3. 通过应用管理器安装或直接打开应用
|
||||
|
||||
### 调试技巧
|
||||
- 使用浏览器开发者工具调试
|
||||
- 检查控制台日志了解应用状态
|
||||
- 利用系统SDK的调试功能
|
||||
|
||||
### 部署注意事项
|
||||
- 确保所有文件路径正确
|
||||
- 验证清单文件格式
|
||||
- 测试在不同窗口大小下的表现
|
||||
|
||||
## 总结
|
||||
|
||||
这个音乐播放器应用是一个完整的外置应用开发示例,展示了:
|
||||
|
||||
- 如何构建功能完整的外置应用
|
||||
- 系统SDK的正确使用方法
|
||||
- 现代Web技术的应用
|
||||
- 良好的用户体验设计
|
||||
- 安全和性能的最佳实践
|
||||
|
||||
通过学习这个案例,开发者可以了解外置应用的完整开发流程,并以此为基础开发自己的应用。
|
||||
751
public/apps/music-player/app.js
Normal file
751
public/apps/music-player/app.js
Normal file
@@ -0,0 +1,751 @@
|
||||
/**
|
||||
* 音乐播放器 - 外置应用案例
|
||||
* 展示了如何创建一个功能完整的外置应用
|
||||
*/
|
||||
|
||||
class MusicPlayer {
|
||||
constructor() {
|
||||
// 应用状态
|
||||
this.isPlaying = false;
|
||||
this.currentTrackIndex = 0;
|
||||
this.playlist = [];
|
||||
this.isShuffleMode = false;
|
||||
this.repeatMode = 'none'; // none, one, all
|
||||
this.volume = 0.7;
|
||||
|
||||
// DOM 元素
|
||||
this.audioPlayer = null;
|
||||
this.playPauseBtn = null;
|
||||
this.progressBar = null;
|
||||
this.volumeBar = null;
|
||||
this.playlist_element = null;
|
||||
|
||||
// 系统SDK
|
||||
this.systemSDK = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化应用
|
||||
*/
|
||||
async init() {
|
||||
console.log('[音乐播放器] 初始化开始');
|
||||
|
||||
// 等待DOM加载完成
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => this.setupApp());
|
||||
} else {
|
||||
this.setupApp();
|
||||
}
|
||||
|
||||
// 初始化系统SDK
|
||||
await this.initSystemSDK();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化系统SDK
|
||||
*/
|
||||
async initSystemSDK() {
|
||||
try {
|
||||
console.log('[音乐播放器] 开始初始化系统SDK');
|
||||
|
||||
// 等待系统SDK可用
|
||||
let attempts = 0;
|
||||
const maxAttempts = 100; // 增加尝试次数
|
||||
|
||||
while (!window.SystemSDK && attempts < maxAttempts) {
|
||||
console.log(`[音乐播放器] 等待SystemSDK可用... (尝试 ${attempts + 1}/${maxAttempts})`);
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (window.SystemSDK) {
|
||||
console.log('[音乐播放器] SystemSDK对象已找到,开始初始化');
|
||||
|
||||
// 初始化SDK
|
||||
const initResult = await window.SystemSDK.init({
|
||||
appId: 'music-player',
|
||||
appName: '音乐播放器',
|
||||
version: '1.0.0',
|
||||
permissions: ['storage.read', 'storage.write']
|
||||
});
|
||||
|
||||
if (initResult.success) {
|
||||
this.systemSDK = window.SystemSDK;
|
||||
console.log('[音乐播放器] 系统SDK初始化成功');
|
||||
|
||||
// 显示系统通知
|
||||
if (this.systemSDK.ui) {
|
||||
try {
|
||||
await this.systemSDK.ui.showNotification({
|
||||
title: '音乐播放器',
|
||||
message: '应用已启动,准备播放音乐!',
|
||||
type: 'info'
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('[音乐播放器] 显示通知失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 从本地存储恢复播放列表
|
||||
await this.loadPlaylistFromStorage();
|
||||
|
||||
} else {
|
||||
console.error('[音乐播放器] 系统SDK初始化失败:', initResult.error);
|
||||
}
|
||||
} else {
|
||||
console.error('[音乐播放器] 系统SDK不可用,已达到最大尝试次数');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[音乐播放器] 系统SDK初始化失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置应用界面和事件
|
||||
*/
|
||||
setupApp() {
|
||||
console.log('[音乐播放器] 设置界面');
|
||||
|
||||
// 获取DOM元素
|
||||
this.audioPlayer = document.getElementById('audioPlayer');
|
||||
this.playPauseBtn = document.getElementById('playPauseBtn');
|
||||
this.progressBar = document.getElementById('progressBar');
|
||||
this.volumeBar = document.getElementById('volumeBar');
|
||||
this.playlist_element = document.getElementById('playlist');
|
||||
|
||||
// 设置音频事件
|
||||
this.setupAudioEvents();
|
||||
|
||||
// 设置控制按钮事件
|
||||
this.setupControlEvents();
|
||||
|
||||
// 设置窗口控制事件
|
||||
this.setupWindowControls();
|
||||
|
||||
// 设置键盘快捷键
|
||||
this.setupKeyboardShortcuts();
|
||||
|
||||
// 初始化音量
|
||||
this.setVolume(this.volume * 100);
|
||||
|
||||
console.log('[音乐播放器] 应用设置完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置音频播放器事件
|
||||
*/
|
||||
setupAudioEvents() {
|
||||
if (!this.audioPlayer) return;
|
||||
|
||||
// 播放开始
|
||||
this.audioPlayer.addEventListener('play', () => {
|
||||
this.isPlaying = true;
|
||||
this.updatePlayButton();
|
||||
this.updateStatus('正在播放');
|
||||
});
|
||||
|
||||
// 播放暂停
|
||||
this.audioPlayer.addEventListener('pause', () => {
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
this.updateStatus('已暂停');
|
||||
});
|
||||
|
||||
// 播放结束
|
||||
this.audioPlayer.addEventListener('ended', () => {
|
||||
this.handleTrackEnded();
|
||||
});
|
||||
|
||||
// 时间更新
|
||||
this.audioPlayer.addEventListener('timeupdate', () => {
|
||||
this.updateProgress();
|
||||
});
|
||||
|
||||
// 加载完成
|
||||
this.audioPlayer.addEventListener('loadedmetadata', () => {
|
||||
this.updateTotalTime();
|
||||
});
|
||||
|
||||
// 加载错误
|
||||
this.audioPlayer.addEventListener('error', (e) => {
|
||||
console.error('[音乐播放器] 播放错误:', e);
|
||||
this.updateStatus('播放出错');
|
||||
this.nextTrack();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置控制按钮事件
|
||||
*/
|
||||
setupControlEvents() {
|
||||
// 播放/暂停
|
||||
document.getElementById('playPauseBtn')?.addEventListener('click', () => {
|
||||
this.togglePlayPause();
|
||||
});
|
||||
|
||||
// 上一曲
|
||||
document.getElementById('prevBtn')?.addEventListener('click', () => {
|
||||
this.prevTrack();
|
||||
});
|
||||
|
||||
// 下一曲
|
||||
document.getElementById('nextBtn')?.addEventListener('click', () => {
|
||||
this.nextTrack();
|
||||
});
|
||||
|
||||
// 随机播放
|
||||
document.getElementById('shuffleBtn')?.addEventListener('click', () => {
|
||||
this.toggleShuffle();
|
||||
});
|
||||
|
||||
// 重复播放
|
||||
document.getElementById('repeatBtn')?.addEventListener('click', () => {
|
||||
this.toggleRepeat();
|
||||
});
|
||||
|
||||
// 进度条
|
||||
this.progressBar?.addEventListener('input', () => {
|
||||
this.seekTo(this.progressBar.value);
|
||||
});
|
||||
|
||||
// 音量控制
|
||||
this.volumeBar?.addEventListener('input', () => {
|
||||
this.setVolume(this.volumeBar.value);
|
||||
});
|
||||
|
||||
// 文件选择
|
||||
document.getElementById('addFilesBtn')?.addEventListener('click', () => {
|
||||
document.getElementById('fileInput').click();
|
||||
});
|
||||
|
||||
document.getElementById('fileInput')?.addEventListener('change', (e) => {
|
||||
this.handleFileSelection(e.target.files);
|
||||
});
|
||||
|
||||
// 清空播放列表
|
||||
document.getElementById('clearPlaylistBtn')?.addEventListener('click', () => {
|
||||
this.clearPlaylist();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置窗口控制事件
|
||||
*/
|
||||
setupWindowControls() {
|
||||
// 最小化
|
||||
document.getElementById('minimizeBtn')?.addEventListener('click', () => {
|
||||
if (this.systemSDK) {
|
||||
this.systemSDK.window.minimize();
|
||||
}
|
||||
});
|
||||
|
||||
// 最大化/还原
|
||||
document.getElementById('maximizeBtn')?.addEventListener('click', () => {
|
||||
if (this.systemSDK) {
|
||||
this.systemSDK.window.toggleMaximize();
|
||||
}
|
||||
});
|
||||
|
||||
// 关闭
|
||||
document.getElementById('closeBtn')?.addEventListener('click', () => {
|
||||
if (this.systemSDK) {
|
||||
this.systemSDK.window.close();
|
||||
} else {
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置键盘快捷键
|
||||
*/
|
||||
setupKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.target.tagName === 'INPUT') return;
|
||||
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
e.preventDefault();
|
||||
this.togglePlayPause();
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
this.prevTrack();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
this.nextTrack();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.setVolume(Math.min(100, this.volume * 100 + 5));
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.setVolume(Math.max(0, this.volume * 100 - 5));
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理文件选择
|
||||
*/
|
||||
handleFileSelection(files) {
|
||||
const audioFiles = Array.from(files).filter(file =>
|
||||
file.type.startsWith('audio/')
|
||||
);
|
||||
|
||||
if (audioFiles.length === 0) {
|
||||
this.updateStatus('未选择音频文件');
|
||||
return;
|
||||
}
|
||||
|
||||
audioFiles.forEach(file => {
|
||||
const track = {
|
||||
id: Date.now() + Math.random(),
|
||||
name: file.name.replace(/\.[^/.]+$/, ""),
|
||||
file: file,
|
||||
url: URL.createObjectURL(file),
|
||||
duration: 0
|
||||
};
|
||||
|
||||
this.playlist.push(track);
|
||||
});
|
||||
|
||||
this.updatePlaylist();
|
||||
this.savePlaylistToStorage();
|
||||
this.updateStatus(`添加了 ${audioFiles.length} 首歌曲`);
|
||||
|
||||
// 如果是第一次添加歌曲,自动播放
|
||||
if (this.playlist.length === audioFiles.length) {
|
||||
this.loadTrack(0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放/暂停切换
|
||||
*/
|
||||
togglePlayPause() {
|
||||
if (!this.audioPlayer || this.playlist.length === 0) {
|
||||
this.updateStatus('播放列表为空');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.audioPlayer.pause();
|
||||
} else {
|
||||
this.audioPlayer.play().catch(error => {
|
||||
console.error('[音乐播放器] 播放失败:', error);
|
||||
this.updateStatus('播放失败');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上一曲
|
||||
*/
|
||||
prevTrack() {
|
||||
if (this.playlist.length === 0) return;
|
||||
|
||||
let newIndex;
|
||||
if (this.isShuffleMode) {
|
||||
newIndex = Math.floor(Math.random() * this.playlist.length);
|
||||
} else {
|
||||
newIndex = this.currentTrackIndex - 1;
|
||||
if (newIndex < 0) {
|
||||
newIndex = this.playlist.length - 1;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadTrack(newIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下一曲
|
||||
*/
|
||||
nextTrack() {
|
||||
if (this.playlist.length === 0) return;
|
||||
|
||||
let newIndex;
|
||||
if (this.isShuffleMode) {
|
||||
newIndex = Math.floor(Math.random() * this.playlist.length);
|
||||
} else {
|
||||
newIndex = this.currentTrackIndex + 1;
|
||||
if (newIndex >= this.playlist.length) {
|
||||
newIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
this.loadTrack(newIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载指定曲目
|
||||
*/
|
||||
loadTrack(index) {
|
||||
if (index < 0 || index >= this.playlist.length) return;
|
||||
|
||||
this.currentTrackIndex = index;
|
||||
const track = this.playlist[index];
|
||||
|
||||
this.audioPlayer.src = track.url;
|
||||
this.updateCurrentTrackInfo(track);
|
||||
this.updatePlaylistHighlight();
|
||||
|
||||
if (this.isPlaying) {
|
||||
this.audioPlayer.play().catch(error => {
|
||||
console.error('[音乐播放器] 播放失败:', error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理曲目播放结束
|
||||
*/
|
||||
handleTrackEnded() {
|
||||
switch (this.repeatMode) {
|
||||
case 'one':
|
||||
this.audioPlayer.currentTime = 0;
|
||||
this.audioPlayer.play();
|
||||
break;
|
||||
case 'all':
|
||||
this.nextTrack();
|
||||
break;
|
||||
default:
|
||||
if (this.currentTrackIndex < this.playlist.length - 1) {
|
||||
this.nextTrack();
|
||||
} else {
|
||||
this.isPlaying = false;
|
||||
this.updatePlayButton();
|
||||
this.updateStatus('播放完成');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换随机播放
|
||||
*/
|
||||
toggleShuffle() {
|
||||
this.isShuffleMode = !this.isShuffleMode;
|
||||
const btn = document.getElementById('shuffleBtn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', this.isShuffleMode);
|
||||
}
|
||||
this.updateStatus(this.isShuffleMode ? '随机播放已开启' : '随机播放已关闭');
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换重复播放模式
|
||||
*/
|
||||
toggleRepeat() {
|
||||
const modes = ['none', 'one', 'all'];
|
||||
const currentIndex = modes.indexOf(this.repeatMode);
|
||||
this.repeatMode = modes[(currentIndex + 1) % modes.length];
|
||||
|
||||
const btn = document.getElementById('repeatBtn');
|
||||
if (btn) {
|
||||
btn.classList.toggle('active', this.repeatMode !== 'none');
|
||||
switch (this.repeatMode) {
|
||||
case 'one':
|
||||
btn.textContent = '🔂';
|
||||
break;
|
||||
case 'all':
|
||||
btn.textContent = '🔁';
|
||||
break;
|
||||
default:
|
||||
btn.textContent = '🔁';
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const modeNames = { none: '关闭', one: '单曲循环', all: '列表循环' };
|
||||
this.updateStatus(`重复播放: ${modeNames[this.repeatMode]}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置音量
|
||||
*/
|
||||
setVolume(value) {
|
||||
this.volume = value / 100;
|
||||
if (this.audioPlayer) {
|
||||
this.audioPlayer.volume = this.volume;
|
||||
}
|
||||
|
||||
const volumeValue = document.getElementById('volumeValue');
|
||||
if (volumeValue) {
|
||||
volumeValue.textContent = `${Math.round(value)}%`;
|
||||
}
|
||||
|
||||
if (this.volumeBar) {
|
||||
this.volumeBar.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定时间
|
||||
*/
|
||||
seekTo(percentage) {
|
||||
if (this.audioPlayer && this.audioPlayer.duration) {
|
||||
const time = (percentage / 100) * this.audioPlayer.duration;
|
||||
this.audioPlayer.currentTime = time;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新播放按钮状态
|
||||
*/
|
||||
updatePlayButton() {
|
||||
if (this.playPauseBtn) {
|
||||
this.playPauseBtn.textContent = this.isPlaying ? '⏸️' : '▶️';
|
||||
this.playPauseBtn.classList.toggle('playing', this.isPlaying);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新进度条
|
||||
*/
|
||||
updateProgress() {
|
||||
if (this.audioPlayer && this.progressBar && this.audioPlayer.duration) {
|
||||
const progress = (this.audioPlayer.currentTime / this.audioPlayer.duration) * 100;
|
||||
this.progressBar.value = progress;
|
||||
|
||||
const currentTime = document.getElementById('currentTime');
|
||||
if (currentTime) {
|
||||
currentTime.textContent = this.formatTime(this.audioPlayer.currentTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新总时长显示
|
||||
*/
|
||||
updateTotalTime() {
|
||||
if (this.audioPlayer) {
|
||||
const totalTime = document.getElementById('totalTime');
|
||||
if (totalTime) {
|
||||
totalTime.textContent = this.formatTime(this.audioPlayer.duration || 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前曲目信息
|
||||
*/
|
||||
updateCurrentTrackInfo(track) {
|
||||
const titleElement = document.getElementById('trackTitle');
|
||||
const artistElement = document.getElementById('trackArtist');
|
||||
const albumElement = document.getElementById('trackAlbum');
|
||||
|
||||
if (titleElement) titleElement.textContent = track.name;
|
||||
if (artistElement) artistElement.textContent = '未知艺术家';
|
||||
if (albumElement) albumElement.textContent = '未知专辑';
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新播放列表显示
|
||||
*/
|
||||
updatePlaylist() {
|
||||
if (!this.playlist_element) return;
|
||||
|
||||
if (this.playlist.length === 0) {
|
||||
this.playlist_element.innerHTML = '<li class="playlist-empty">暂无音乐文件</li>';
|
||||
this.updateTrackCount();
|
||||
return;
|
||||
}
|
||||
|
||||
this.playlist_element.innerHTML = this.playlist.map((track, index) => `
|
||||
<li class="playlist-item ${index === this.currentTrackIndex ? 'playing' : ''}"
|
||||
data-index="${index}">
|
||||
<span class="track-number">${index + 1}</span>
|
||||
<div class="track-details">
|
||||
<div class="track-name">${track.name}</div>
|
||||
<div class="track-duration">${this.formatTime(track.duration)}</div>
|
||||
</div>
|
||||
</li>
|
||||
`).join('');
|
||||
|
||||
// 添加点击事件
|
||||
this.playlist_element.querySelectorAll('.playlist-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.dataset.index);
|
||||
this.loadTrack(index);
|
||||
if (!this.isPlaying) {
|
||||
this.togglePlayPause();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.updateTrackCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新播放列表高亮
|
||||
*/
|
||||
updatePlaylistHighlight() {
|
||||
if (!this.playlist_element) return;
|
||||
|
||||
this.playlist_element.querySelectorAll('.playlist-item').forEach((item, index) => {
|
||||
item.classList.toggle('playing', index === this.currentTrackIndex);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空播放列表
|
||||
*/
|
||||
clearPlaylist() {
|
||||
this.playlist.forEach(track => {
|
||||
if (track.url) {
|
||||
URL.revokeObjectURL(track.url);
|
||||
}
|
||||
});
|
||||
|
||||
this.playlist = [];
|
||||
this.currentTrackIndex = 0;
|
||||
this.isPlaying = false;
|
||||
|
||||
if (this.audioPlayer) {
|
||||
this.audioPlayer.pause();
|
||||
this.audioPlayer.src = '';
|
||||
}
|
||||
|
||||
this.updatePlaylist();
|
||||
this.updatePlayButton();
|
||||
this.updateCurrentTrackInfo({ name: '选择音乐文件开始播放' });
|
||||
this.updateStatus('播放列表已清空');
|
||||
this.savePlaylistToStorage();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新状态栏
|
||||
*/
|
||||
updateStatus(message) {
|
||||
const statusText = document.getElementById('statusText');
|
||||
if (statusText) {
|
||||
statusText.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新歌曲数量
|
||||
*/
|
||||
updateTrackCount() {
|
||||
const trackCount = document.getElementById('trackCount');
|
||||
if (trackCount) {
|
||||
trackCount.textContent = `${this.playlist.length} 首歌曲`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间显示
|
||||
*/
|
||||
formatTime(seconds) {
|
||||
if (!seconds || isNaN(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')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存播放列表到本地存储
|
||||
*/
|
||||
savePlaylistToStorage() {
|
||||
try {
|
||||
const playlistData = this.playlist.map(track => ({
|
||||
id: track.id,
|
||||
name: track.name,
|
||||
duration: track.duration
|
||||
}));
|
||||
|
||||
// 使用系统SDK进行存储操作
|
||||
if (this.systemSDK && this.systemSDK.storage) {
|
||||
console.log('[音乐播放器] 保存播放列表到系统存储');
|
||||
this.systemSDK.storage.set('music-player-playlist', JSON.stringify(playlistData))
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
console.log('[音乐播放器] 播放列表保存成功');
|
||||
} else {
|
||||
console.warn('[音乐播放器] 保存播放列表到系统存储失败:', result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('[音乐播放器] 保存播放列表失败:', error);
|
||||
});
|
||||
} else {
|
||||
console.warn('[音乐播放器] 系统SDK未初始化,无法保存播放列表');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[音乐播放器] 保存播放列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从本地存储加载播放列表
|
||||
*/
|
||||
async loadPlaylistFromStorage() {
|
||||
try {
|
||||
// 使用系统SDK进行存储操作
|
||||
if (this.systemSDK && this.systemSDK.storage) {
|
||||
console.log('[音乐播放器] 从系统存储加载播放列表');
|
||||
const result = await this.systemSDK.storage.get('music-player-playlist');
|
||||
|
||||
if (result.success && result.data) {
|
||||
try {
|
||||
const playlistData = JSON.parse(result.data);
|
||||
console.log(`[音乐播放器] 从系统存储加载了 ${playlistData.length} 首歌曲`);
|
||||
// 这里可以恢复播放列表
|
||||
} catch (parseError) {
|
||||
console.warn('[音乐播放器] 解析播放列表数据失败:', parseError);
|
||||
}
|
||||
} else if (!result.success) {
|
||||
console.warn('[音乐播放器] 从系统存储加载播放列表失败:', result.error);
|
||||
}
|
||||
} else {
|
||||
console.warn('[音乐播放器] 系统SDK未初始化,无法加载播放列表');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[音乐播放器] 加载播放列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
cleanup() {
|
||||
console.log('[音乐播放器] 清理资源');
|
||||
|
||||
// 暂停播放
|
||||
if (this.audioPlayer) {
|
||||
this.audioPlayer.pause();
|
||||
}
|
||||
|
||||
// 释放对象URL
|
||||
this.playlist.forEach(track => {
|
||||
if (track.url) {
|
||||
URL.revokeObjectURL(track.url);
|
||||
}
|
||||
});
|
||||
|
||||
// 保存状态
|
||||
this.savePlaylistToStorage();
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动
|
||||
let musicPlayerApp;
|
||||
|
||||
// 确保在DOM加载完成后启动应用
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
musicPlayerApp = new MusicPlayer();
|
||||
});
|
||||
} else {
|
||||
musicPlayerApp = new MusicPlayer();
|
||||
}
|
||||
|
||||
// 导出供外部使用
|
||||
window.MusicPlayerApp = musicPlayerApp;
|
||||
88
public/apps/music-player/index.html
Normal file
88
public/apps/music-player/index.html
Normal file
@@ -0,0 +1,88 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>音乐播放器</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="music-player">
|
||||
<!-- 头部标题栏 -->
|
||||
<header class="player-header">
|
||||
<h1>🎵 音乐播放器</h1>
|
||||
<div class="header-controls">
|
||||
<button id="minimizeBtn" class="control-btn">➖</button>
|
||||
<button id="maximizeBtn" class="control-btn">🔲</button>
|
||||
<button id="closeBtn" class="control-btn">❌</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- 主要播放区域 -->
|
||||
<main class="player-main">
|
||||
<!-- 当前播放信息 -->
|
||||
<section class="current-track">
|
||||
<div class="track-art">
|
||||
<img id="trackImage" src="data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTAwIiBoZWlnaHQ9IjEwMCIgdmlld0JveD0iMCAwIDEwMCAxMDAiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxyZWN0IHdpZHRoPSIxMDAiIGhlaWdodD0iMTAwIiBmaWxsPSIjRkY2NzMxIi8+CjxwYXRoIGQ9Ik0zNSA3NVYyNUw2NSA1MEwzNSA3NVoiIGZpbGw9IndoaXRlIi8+Cjwvc3ZnPg==" alt="音乐封面">
|
||||
</div>
|
||||
<div class="track-info">
|
||||
<h2 id="trackTitle">选择音乐文件开始播放</h2>
|
||||
<p id="trackArtist">未知艺术家</p>
|
||||
<p id="trackAlbum">未知专辑</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 进度控制 -->
|
||||
<section class="progress-section">
|
||||
<div class="time-display">
|
||||
<span id="currentTime">00:00</span>
|
||||
<input type="range" id="progressBar" min="0" max="100" value="0" class="progress-bar">
|
||||
<span id="totalTime">00:00</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 播放控制 -->
|
||||
<section class="controls">
|
||||
<button id="shuffleBtn" class="control-btn secondary">🔀</button>
|
||||
<button id="prevBtn" class="control-btn">⏮️</button>
|
||||
<button id="playPauseBtn" class="control-btn primary">▶️</button>
|
||||
<button id="nextBtn" class="control-btn">⏭️</button>
|
||||
<button id="repeatBtn" class="control-btn secondary">🔁</button>
|
||||
</section>
|
||||
|
||||
<!-- 音量控制 -->
|
||||
<section class="volume-section">
|
||||
<span class="volume-icon">🔊</span>
|
||||
<input type="range" id="volumeBar" min="0" max="100" value="70" class="volume-bar">
|
||||
<span id="volumeValue">70%</span>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<!-- 播放列表 -->
|
||||
<aside class="playlist-section">
|
||||
<div class="playlist-header">
|
||||
<h3>播放列表</h3>
|
||||
<div class="playlist-controls">
|
||||
<input type="file" id="fileInput" accept="audio/*" multiple style="display: none;">
|
||||
<button id="addFilesBtn" class="btn-secondary">添加文件</button>
|
||||
<button id="clearPlaylistBtn" class="btn-secondary">清空列表</button>
|
||||
</div>
|
||||
</div>
|
||||
<ul id="playlist" class="playlist">
|
||||
<li class="playlist-empty">暂无音乐文件</li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- 状态栏 -->
|
||||
<footer class="status-bar">
|
||||
<span id="statusText">就绪</span>
|
||||
<span id="trackCount">0 首歌曲</span>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<!-- 隐藏的音频元素 -->
|
||||
<audio id="audioPlayer" preload="none"></audio>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
27
public/apps/music-player/manifest.json
Normal file
27
public/apps/music-player/manifest.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"id": "music-player",
|
||||
"name": "音乐播放器",
|
||||
"version": "1.0.0",
|
||||
"description": "一个功能丰富的音乐播放器应用,支持播放本地音乐文件",
|
||||
"author": "外置应用开发者",
|
||||
"entryPoint": "index.html",
|
||||
"icon": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iMjQiIGN5PSIyNCIgcj0iMjIiIGZpbGw9IiNGRjY3MzEiLz4KPHBhdGggZD0iTTE5IDMyVjE2TDMxIDI0TDE5IDMyWiIgZmlsbD0id2hpdGUiLz4KPC9zdmc+",
|
||||
"permissions": [
|
||||
"storage.read",
|
||||
"storage.write",
|
||||
"file.read",
|
||||
"system.notification"
|
||||
],
|
||||
"window": {
|
||||
"width": 600,
|
||||
"height": 400,
|
||||
"minWidth": 400,
|
||||
"minHeight": 300,
|
||||
"resizable": true,
|
||||
"minimizable": true,
|
||||
"maximizable": true,
|
||||
"closable": true
|
||||
},
|
||||
"category": "多媒体",
|
||||
"keywords": ["音乐", "播放器", "媒体", "音频"]
|
||||
}
|
||||
430
public/apps/music-player/style.css
Normal file
430
public/apps/music-player/style.css
Normal file
@@ -0,0 +1,430 @@
|
||||
/* 音乐播放器样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: #333;
|
||||
overflow: hidden;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.music-player {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
/* 头部标题栏 */
|
||||
.player-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
background: linear-gradient(90deg, #FF6B35, #F7931E);
|
||||
color: white;
|
||||
user-select: none;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.player-header h1 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-controls .control-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.header-controls .control-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
/* 主播放区域 */
|
||||
.player-main {
|
||||
flex: 1;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 当前播放信息 */
|
||||
.current-track {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.track-art {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.track-art img {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.track-info h2 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.track-info p {
|
||||
font-size: 14px;
|
||||
color: #7f8c8d;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* 进度控制 */
|
||||
.progress-section {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
padding: 15px 20px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.time-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.time-display span {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
background: #e1e1e1;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.progress-bar::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #FF6B35;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.progress-bar::-webkit-slider-thumb:hover {
|
||||
transform: scale(1.2);
|
||||
box-shadow: 0 3px 8px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
.progress-bar::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: #FF6B35;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 6px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
/* 播放控制 */
|
||||
.controls {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
padding: 12px;
|
||||
border-radius: 50%;
|
||||
transition: all 0.2s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.control-btn:hover {
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.control-btn.primary {
|
||||
background: linear-gradient(135deg, #FF6B35, #F7931E);
|
||||
color: white;
|
||||
font-size: 32px;
|
||||
box-shadow: 0 4px 15px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.control-btn.primary:hover {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 6px 20px rgba(255, 107, 53, 0.4);
|
||||
}
|
||||
|
||||
.control-btn.secondary {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.control-btn.active {
|
||||
color: #FF6B35;
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
/* 音量控制 */
|
||||
.volume-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 15px 20px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.volume-icon {
|
||||
font-size: 18px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.volume-bar {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: #e1e1e1;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.volume-bar::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #FF6B35;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
.volume-bar::-moz-range-thumb {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
background: #FF6B35;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 2px 4px rgba(255, 107, 53, 0.3);
|
||||
}
|
||||
|
||||
#volumeValue {
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
min-width: 30px;
|
||||
}
|
||||
|
||||
/* 播放列表 */
|
||||
.playlist-section {
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
max-height: 200px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.playlist-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.playlist-header h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.playlist-controls {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
padding: 6px 12px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #e9ecef;
|
||||
border-color: #adb5bd;
|
||||
}
|
||||
|
||||
.playlist {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.playlist-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 20px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s ease;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.playlist-item:hover {
|
||||
background: rgba(255, 107, 53, 0.1);
|
||||
}
|
||||
|
||||
.playlist-item.playing {
|
||||
background: rgba(255, 107, 53, 0.15);
|
||||
color: #FF6B35;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.playlist-item .track-number {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
min-width: 20px;
|
||||
margin-right: 15px;
|
||||
}
|
||||
|
||||
.playlist-item .track-details {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.playlist-item .track-name {
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.playlist-item .track-duration {
|
||||
font-size: 11px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.playlist-empty {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* 状态栏 */
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 20px;
|
||||
background: #f8f9fa;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.05);
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 500px) {
|
||||
.current-track {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.track-art img {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.controls {
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.control-btn {
|
||||
font-size: 20px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.control-btn.primary {
|
||||
font-size: 28px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #999;
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes pulse {
|
||||
0% { transform: scale(1); }
|
||||
50% { transform: scale(1.05); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
.control-btn.primary.playing {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
Reference in New Issue
Block a user