- 响应式设计,支持桌面端和手机端自适应 - 首页展示轮播图、推荐歌单、热门歌曲、新碟上架、热门歌手 - 歌曲/歌手/专辑/歌单详情页 - 底部播放器:播放/暂停、上下首、进度拖动、音量控制 - 歌词同步滚动显示 - 搜索功能 - 后台管理 /admin - JSON文件存储
908 lines
29 KiB
JavaScript
908 lines
29 KiB
JavaScript
// ============ 后台管理 JS ============
|
|
|
|
// 当前页面
|
|
let currentPage = 'dashboard';
|
|
|
|
// API 工具
|
|
const api = {
|
|
async get(url) {
|
|
const res = await fetch(url);
|
|
return res.json();
|
|
},
|
|
async post(url, data) {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return res.json();
|
|
},
|
|
async put(url, data) {
|
|
const res = await fetch(url, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(data)
|
|
});
|
|
return res.json();
|
|
},
|
|
async delete(url) {
|
|
const res = await fetch(url, { method: 'DELETE' });
|
|
return res.json();
|
|
},
|
|
async upload(url, formData) {
|
|
const res = await fetch(url, {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
return res.json();
|
|
}
|
|
};
|
|
|
|
// 通知
|
|
function toast(msg, type = 'info') {
|
|
const el = document.createElement('div');
|
|
el.className = `toast ${type}`;
|
|
el.textContent = msg;
|
|
document.body.appendChild(el);
|
|
setTimeout(() => el.remove(), 3000);
|
|
}
|
|
|
|
// 模态框
|
|
const modal = {
|
|
open(title, bodyHtml) {
|
|
document.getElementById('modalTitle').textContent = title;
|
|
document.getElementById('modalBody').innerHTML = bodyHtml;
|
|
document.getElementById('modal').style.display = 'flex';
|
|
},
|
|
close() {
|
|
document.getElementById('modal').style.display = 'none';
|
|
}
|
|
};
|
|
|
|
// ============ 页面渲染 ============
|
|
const pages = {
|
|
// 仪表盘
|
|
async dashboard() {
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const stats = await api.get('/api/admin/stats');
|
|
|
|
content.innerHTML = `
|
|
<div class="stats-grid">
|
|
<div class="stat-card">
|
|
<div class="icon blue"><i class="fas fa-music"></i></div>
|
|
<div class="value">${stats.songCount}</div>
|
|
<div class="label">歌曲总数</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="icon green"><i class="fas fa-user"></i></div>
|
|
<div class="value">${stats.artistCount}</div>
|
|
<div class="label">歌手数量</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="icon orange"><i class="fas fa-compact-disc"></i></div>
|
|
<div class="value">${stats.albumCount}</div>
|
|
<div class="label">专辑数量</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="icon purple"><i class="fas fa-list"></i></div>
|
|
<div class="value">${stats.playlistCount}</div>
|
|
<div class="label">歌单数量</div>
|
|
</div>
|
|
<div class="stat-card">
|
|
<div class="icon red"><i class="fas fa-play-circle"></i></div>
|
|
<div class="value">${formatNumber(stats.totalPlays)}</div>
|
|
<div class="label">总播放次数</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>快捷操作</h3>
|
|
</div>
|
|
<div style="padding: 20px; display: flex; gap: 12px; flex-wrap: wrap;">
|
|
<button class="btn btn-primary" onclick="pages.songs()"><i class="fas fa-music"></i> 管理歌曲</button>
|
|
<button class="btn btn-primary" onclick="pages.artists()"><i class="fas fa-user"></i> 管理歌手</button>
|
|
<button class="btn btn-primary" onclick="pages.albums()"><i class="fas fa-compact-disc"></i> 管理专辑</button>
|
|
<button class="btn btn-primary" onclick="pages.playlists()"><i class="fas fa-list"></i> 管理歌单</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
},
|
|
|
|
// 歌曲管理
|
|
async songs() {
|
|
currentPage = 'songs';
|
|
document.getElementById('pageTitle').textContent = '歌曲管理';
|
|
updateNav();
|
|
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const songs = await api.get('/api/admin/songs');
|
|
const artists = await api.get('/api/artists');
|
|
const albums = await api.get('/api/albums');
|
|
|
|
let html = `
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>歌曲列表 (${songs.length})</h3>
|
|
<button class="btn btn-primary" onclick="songForm.open()"><i class="fas fa-plus"></i> 添加歌曲</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>封面</th>
|
|
<th>歌曲名</th>
|
|
<th>歌手</th>
|
|
<th>专辑</th>
|
|
<th>时长</th>
|
|
<th>播放量</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
if (songs.length === 0) {
|
|
html += '<tr><td colspan="7" style="text-align:center;padding:40px;color:#999">暂无歌曲</td></tr>';
|
|
} else {
|
|
songs.forEach(song => {
|
|
html += `
|
|
<tr>
|
|
<td><img class="cover-thumb" src="${song.cover || ''}" onerror="this.style.display='none'" alt=""></td>
|
|
<td>${song.title}</td>
|
|
<td>${song.artist_name || '-'}</td>
|
|
<td>${song.album_title || '-'}</td>
|
|
<td>${formatTime(song.duration)}</td>
|
|
<td>${formatNumber(song.play_count)}</td>
|
|
<td class="actions-cell">
|
|
<button class="btn btn-secondary btn-sm" onclick='songForm.edit(${JSON.stringify(song)})'>编辑</button>
|
|
<button class="btn btn-danger btn-sm" onclick="songForm.delete(${song.id})">删除</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += '</tbody></table></div>';
|
|
content.innerHTML = html;
|
|
|
|
// 保存歌手和专辑数据供表单使用
|
|
window._artists = artists;
|
|
window._albums = albums;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
},
|
|
|
|
// 歌手管理
|
|
async artists() {
|
|
currentPage = 'artists';
|
|
document.getElementById('pageTitle').textContent = '歌手管理';
|
|
updateNav();
|
|
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const artists = await api.get('/api/admin/artists');
|
|
|
|
let html = `
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>歌手列表 (${artists.length})</h3>
|
|
<button class="btn btn-primary" onclick="artistForm.open()"><i class="fas fa-plus"></i> 添加歌手</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>头像</th>
|
|
<th>姓名</th>
|
|
<th>简介</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
if (artists.length === 0) {
|
|
html += '<tr><td colspan="4" style="text-align:center;padding:40px;color:#999">暂无歌手</td></tr>';
|
|
} else {
|
|
artists.forEach(artist => {
|
|
html += `
|
|
<tr>
|
|
<td><img class="cover-thumb" src="${artist.avatar || ''}" onerror="this.style.display='none'" alt="" style="border-radius:50%"></td>
|
|
<td>${artist.name}</td>
|
|
<td>${artist.bio || '-'}</td>
|
|
<td class="actions-cell">
|
|
<button class="btn btn-secondary btn-sm" onclick='artistForm.edit(${JSON.stringify(artist)})'>编辑</button>
|
|
<button class="btn btn-danger btn-sm" onclick="artistForm.delete(${artist.id})">删除</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += '</tbody></table></div>';
|
|
content.innerHTML = html;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
},
|
|
|
|
// 专辑管理
|
|
async albums() {
|
|
currentPage = 'albums';
|
|
document.getElementById('pageTitle').textContent = '专辑管理';
|
|
updateNav();
|
|
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const albums = await api.get('/api/admin/albums');
|
|
const artists = await api.get('/api/artists');
|
|
|
|
let html = `
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>专辑列表 (${albums.length})</h3>
|
|
<button class="btn btn-primary" onclick="albumForm.open()"><i class="fas fa-plus"></i> 添加专辑</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>封面</th>
|
|
<th>专辑名</th>
|
|
<th>歌手</th>
|
|
<th>发行日期</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
if (albums.length === 0) {
|
|
html += '<tr><td colspan="5" style="text-align:center;padding:40px;color:#999">暂无专辑</td></tr>';
|
|
} else {
|
|
albums.forEach(album => {
|
|
html += `
|
|
<tr>
|
|
<td><img class="cover-thumb" src="${album.cover || ''}" onerror="this.style.display='none'" alt=""></td>
|
|
<td>${album.title}</td>
|
|
<td>${album.artist_name || '-'}</td>
|
|
<td>${album.release_date || '-'}</td>
|
|
<td class="actions-cell">
|
|
<button class="btn btn-secondary btn-sm" onclick='albumForm.edit(${JSON.stringify(album)})'>编辑</button>
|
|
<button class="btn btn-danger btn-sm" onclick="albumForm.delete(${album.id})">删除</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += '</tbody></table></div>';
|
|
content.innerHTML = html;
|
|
|
|
window._artists = artists;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
},
|
|
|
|
// 歌单管理
|
|
async playlists() {
|
|
currentPage = 'playlists';
|
|
document.getElementById('pageTitle').textContent = '歌单管理';
|
|
updateNav();
|
|
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const playlists = await api.get('/api/admin/playlists');
|
|
|
|
let html = `
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>歌单列表 (${playlists.length})</h3>
|
|
<button class="btn btn-primary" onclick="playlistForm.open()"><i class="fas fa-plus"></i> 添加歌单</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>封面</th>
|
|
<th>歌单名</th>
|
|
<th>描述</th>
|
|
<th>推荐</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
if (playlists.length === 0) {
|
|
html += '<tr><td colspan="5" style="text-align:center;padding:40px;color:#999">暂无歌单</td></tr>';
|
|
} else {
|
|
playlists.forEach(pl => {
|
|
html += `
|
|
<tr>
|
|
<td><img class="cover-thumb" src="${pl.cover || ''}" onerror="this.style.display='none'" alt=""></td>
|
|
<td>${pl.title}</td>
|
|
<td>${pl.description || '-'}</td>
|
|
<td>${pl.is_featured ? '✅' : '-'}</td>
|
|
<td class="actions-cell">
|
|
<button class="btn btn-secondary btn-sm" onclick='playlistForm.edit(${JSON.stringify(pl)})'>编辑</button>
|
|
<button class="btn btn-danger btn-sm" onclick="playlistForm.delete(${pl.id})">删除</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += '</tbody></table></div>';
|
|
content.innerHTML = html;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
},
|
|
|
|
// 轮播图管理
|
|
async banners() {
|
|
currentPage = 'banners';
|
|
document.getElementById('pageTitle').textContent = '轮播图管理';
|
|
updateNav();
|
|
|
|
const content = document.getElementById('adminContent');
|
|
content.innerHTML = '<div class="loading">加载中...</div>';
|
|
|
|
try {
|
|
const banners = await api.get('/api/admin/banners');
|
|
|
|
let html = `
|
|
<div class="table-container">
|
|
<div class="table-header">
|
|
<h3>轮播图列表 (${banners.length})</h3>
|
|
<button class="btn btn-primary" onclick="bannerForm.open()"><i class="fas fa-plus"></i> 添加轮播图</button>
|
|
</div>
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>图片</th>
|
|
<th>标题</th>
|
|
<th>链接</th>
|
|
<th>排序</th>
|
|
<th>状态</th>
|
|
<th>操作</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
`;
|
|
|
|
if (banners.length === 0) {
|
|
html += '<tr><td colspan="6" style="text-align:center;padding:40px;color:#999">暂无轮播图</td></tr>';
|
|
} else {
|
|
banners.forEach(b => {
|
|
html += `
|
|
<tr>
|
|
<td><img class="cover-thumb" src="${b.image || ''}" onerror="this.style.display='none'" alt="" style="width:80px;height:40px"></td>
|
|
<td>${b.title}</td>
|
|
<td>${b.link || '-'}</td>
|
|
<td>${b.sort_order}</td>
|
|
<td>${b.is_active ? '启用' : '禁用'}</td>
|
|
<td class="actions-cell">
|
|
<button class="btn btn-secondary btn-sm" onclick='bannerForm.edit(${JSON.stringify(b)})'>编辑</button>
|
|
<button class="btn btn-danger btn-sm" onclick="bannerForm.delete(${b.id})">删除</button>
|
|
</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
}
|
|
|
|
html += '</tbody></table></div>';
|
|
content.innerHTML = html;
|
|
} catch (err) {
|
|
content.innerHTML = '<div class="empty-state"><i class="fas fa-exclamation-circle"></i><p>加载失败</p></div>';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============ 表单模块 ============
|
|
|
|
// 歌曲表单
|
|
const songForm = {
|
|
open(song = null) {
|
|
const artists = window._artists || [];
|
|
const albums = window._albums || [];
|
|
|
|
let artistOptions = '<option value="">选择歌手</option>';
|
|
artists.forEach(a => {
|
|
artistOptions += `<option value="${a.id}" ${song && song.artist_id == a.id ? 'selected' : ''}>${a.name}</option>`;
|
|
});
|
|
|
|
let albumOptions = '<option value="">选择专辑</option>';
|
|
albums.forEach(a => {
|
|
albumOptions += `<option value="${a.id}" ${song && song.album_id == a.id ? 'selected' : ''}>${a.title}</option>`;
|
|
});
|
|
|
|
const html = `
|
|
<form onsubmit="songForm.submit(event, ${song ? song.id : 'null'})">
|
|
<div class="form-group">
|
|
<label>歌曲名 *</label>
|
|
<input type="text" name="title" value="${song ? song.title : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>歌手</label>
|
|
<select name="artist_id">${artistOptions}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>专辑</label>
|
|
<select name="album_id">${albumOptions}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>音乐文件 *</label>
|
|
<input type="file" name="music_file" accept="audio/*">
|
|
${song ? `<small>当前: ${song.file_path}</small>` : ''}
|
|
<input type="hidden" name="file_path" value="${song ? song.file_path : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>封面图片</label>
|
|
<input type="file" name="cover_file" accept="image/*">
|
|
${song ? `<small>当前: ${song.cover || '无'}</small>` : ''}
|
|
<input type="hidden" name="cover" value="${song ? song.cover : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>时长 (秒)</label>
|
|
<input type="number" name="duration" value="${song ? song.duration : '0'}" step="0.1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>歌词 (LRC格式)</label>
|
|
<textarea name="lyrics" rows="6">${song ? song.lyrics || '' : ''}</textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="modal.close()">取消</button>
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
modal.open(song ? '编辑歌曲' : '添加歌曲', html);
|
|
|
|
// 处理文件上传
|
|
this.setupFileUpload('music_file', 'file_path', '/api/admin/upload/music');
|
|
this.setupFileUpload('cover_file', 'cover', '/api/admin/upload/cover');
|
|
},
|
|
|
|
edit(song) {
|
|
this.open(song);
|
|
},
|
|
|
|
async submit(e, id) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const data = {
|
|
title: form.title.value,
|
|
artist_id: form.artist_id.value || null,
|
|
album_id: form.album_id.value || null,
|
|
file_path: form.file_path.value,
|
|
cover: form.cover.value,
|
|
duration: parseFloat(form.duration.value) || 0,
|
|
lyrics: form.lyrics.value
|
|
};
|
|
|
|
if (!data.file_path) {
|
|
toast('请上传音乐文件', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (id) {
|
|
await api.put(`/api/admin/songs/${id}`, data);
|
|
toast('歌曲已更新', 'success');
|
|
} else {
|
|
await api.post('/api/admin/songs', data);
|
|
toast('歌曲已添加', 'success');
|
|
}
|
|
modal.close();
|
|
pages.songs();
|
|
} catch (err) {
|
|
toast('操作失败', 'error');
|
|
}
|
|
},
|
|
|
|
async delete(id) {
|
|
if (!confirm('确定要删除这首歌曲吗?')) return;
|
|
try {
|
|
await api.delete(`/api/admin/songs/${id}`);
|
|
toast('歌曲已删除', 'success');
|
|
pages.songs();
|
|
} catch (err) {
|
|
toast('删除失败', 'error');
|
|
}
|
|
},
|
|
|
|
setupFileUpload(inputName, hiddenName, uploadUrl) {
|
|
const input = document.querySelector(`input[name="${inputName}"]`);
|
|
if (!input) return;
|
|
|
|
input.addEventListener('change', async (e) => {
|
|
const file = e.target.files[0];
|
|
if (!file) return;
|
|
|
|
const formData = new FormData();
|
|
formData.append(inputName.replace('_file', ''), file);
|
|
|
|
try {
|
|
const result = await api.upload(uploadUrl, formData);
|
|
document.querySelector(`input[name="${hiddenName}"]`).value = result.path;
|
|
toast('文件已上传', 'success');
|
|
} catch (err) {
|
|
toast('上传失败', 'error');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// 歌手表单
|
|
const artistForm = {
|
|
open(artist = null) {
|
|
const html = `
|
|
<form onsubmit="artistForm.submit(event, ${artist ? artist.id : 'null'})">
|
|
<div class="form-group">
|
|
<label>歌手名 *</label>
|
|
<input type="text" name="name" value="${artist ? artist.name : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>头像</label>
|
|
<input type="file" name="avatar_file" accept="image/*">
|
|
${artist ? `<small>当前: ${artist.avatar || '无'}</small>` : ''}
|
|
<input type="hidden" name="avatar" value="${artist ? artist.avatar : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>简介</label>
|
|
<textarea name="bio" rows="4">${artist ? artist.bio || '' : ''}</textarea>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="modal.close()">取消</button>
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
modal.open(artist ? '编辑歌手' : '添加歌手', html);
|
|
songForm.setupFileUpload('avatar_file', 'avatar', '/api/admin/upload/cover');
|
|
},
|
|
|
|
edit(artist) {
|
|
this.open(artist);
|
|
},
|
|
|
|
async submit(e, id) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const data = {
|
|
name: form.name.value,
|
|
avatar: form.avatar.value,
|
|
bio: form.bio.value
|
|
};
|
|
|
|
try {
|
|
if (id) {
|
|
await api.put(`/api/admin/artists/${id}`, data);
|
|
toast('歌手已更新', 'success');
|
|
} else {
|
|
await api.post('/api/admin/artists', data);
|
|
toast('歌手已添加', 'success');
|
|
}
|
|
modal.close();
|
|
pages.artists();
|
|
} catch (err) {
|
|
toast('操作失败', 'error');
|
|
}
|
|
},
|
|
|
|
async delete(id) {
|
|
if (!confirm('确定要删除这个歌手吗?')) return;
|
|
try {
|
|
await api.delete(`/api/admin/artists/${id}`);
|
|
toast('歌手已删除', 'success');
|
|
pages.artists();
|
|
} catch (err) {
|
|
toast('删除失败', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 专辑表单
|
|
const albumForm = {
|
|
open(album = null) {
|
|
const artists = window._artists || [];
|
|
|
|
let artistOptions = '<option value="">选择歌手</option>';
|
|
artists.forEach(a => {
|
|
artistOptions += `<option value="${a.id}" ${album && album.artist_id == a.id ? 'selected' : ''}>${a.name}</option>`;
|
|
});
|
|
|
|
const html = `
|
|
<form onsubmit="albumForm.submit(event, ${album ? album.id : 'null'})">
|
|
<div class="form-group">
|
|
<label>专辑名 *</label>
|
|
<input type="text" name="title" value="${album ? album.title : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>歌手</label>
|
|
<select name="artist_id">${artistOptions}</select>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>封面</label>
|
|
<input type="file" name="cover_file" accept="image/*">
|
|
${album ? `<small>当前: ${album.cover || '无'}</small>` : ''}
|
|
<input type="hidden" name="cover" value="${album ? album.cover : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>描述</label>
|
|
<textarea name="description" rows="3">${album ? album.description || '' : ''}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>发行日期</label>
|
|
<input type="date" name="release_date" value="${album ? album.release_date || '' : ''}">
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="modal.close()">取消</button>
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
modal.open(album ? '编辑专辑' : '添加专辑', html);
|
|
songForm.setupFileUpload('cover_file', 'cover', '/api/admin/upload/cover');
|
|
},
|
|
|
|
edit(album) {
|
|
this.open(album);
|
|
},
|
|
|
|
async submit(e, id) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const data = {
|
|
title: form.title.value,
|
|
artist_id: form.artist_id.value || null,
|
|
cover: form.cover.value,
|
|
description: form.description.value,
|
|
release_date: form.release_date.value
|
|
};
|
|
|
|
try {
|
|
if (id) {
|
|
await api.put(`/api/admin/albums/${id}`, data);
|
|
toast('专辑已更新', 'success');
|
|
} else {
|
|
await api.post('/api/admin/albums', data);
|
|
toast('专辑已添加', 'success');
|
|
}
|
|
modal.close();
|
|
pages.albums();
|
|
} catch (err) {
|
|
toast('操作失败', 'error');
|
|
}
|
|
},
|
|
|
|
async delete(id) {
|
|
if (!confirm('确定要删除这个专辑吗?')) return;
|
|
try {
|
|
await api.delete(`/api/admin/albums/${id}`);
|
|
toast('专辑已删除', 'success');
|
|
pages.albums();
|
|
} catch (err) {
|
|
toast('删除失败', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 歌单表单
|
|
const playlistForm = {
|
|
open(playlist = null) {
|
|
const html = `
|
|
<form onsubmit="playlistForm.submit(event, ${playlist ? playlist.id : 'null'})">
|
|
<div class="form-group">
|
|
<label>歌单名 *</label>
|
|
<input type="text" name="title" value="${playlist ? playlist.title : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>封面</label>
|
|
<input type="file" name="cover_file" accept="image/*">
|
|
${playlist ? `<small>当前: ${playlist.cover || '无'}</small>` : ''}
|
|
<input type="hidden" name="cover" value="${playlist ? playlist.cover : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>描述</label>
|
|
<textarea name="description" rows="3">${playlist ? playlist.description || '' : ''}</textarea>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_featured" ${playlist && playlist.is_featured ? 'checked' : ''}> 首页推荐
|
|
</label>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="modal.close()">取消</button>
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
modal.open(playlist ? '编辑歌单' : '添加歌单', html);
|
|
songForm.setupFileUpload('cover_file', 'cover', '/api/admin/upload/cover');
|
|
},
|
|
|
|
edit(playlist) {
|
|
this.open(playlist);
|
|
},
|
|
|
|
async submit(e, id) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const data = {
|
|
title: form.title.value,
|
|
cover: form.cover.value,
|
|
description: form.description.value,
|
|
is_featured: form.is_featured.checked ? 1 : 0
|
|
};
|
|
|
|
try {
|
|
if (id) {
|
|
await api.put(`/api/admin/playlists/${id}`, data);
|
|
toast('歌单已更新', 'success');
|
|
} else {
|
|
await api.post('/api/admin/playlists', data);
|
|
toast('歌单已添加', 'success');
|
|
}
|
|
modal.close();
|
|
pages.playlists();
|
|
} catch (err) {
|
|
toast('操作失败', 'error');
|
|
}
|
|
},
|
|
|
|
async delete(id) {
|
|
if (!confirm('确定要删除这个歌单吗?')) return;
|
|
try {
|
|
await api.delete(`/api/admin/playlists/${id}`);
|
|
toast('歌单已删除', 'success');
|
|
pages.playlists();
|
|
} catch (err) {
|
|
toast('删除失败', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
// 轮播图表单
|
|
const bannerForm = {
|
|
open(banner = null) {
|
|
const html = `
|
|
<form onsubmit="bannerForm.submit(event, ${banner ? banner.id : 'null'})">
|
|
<div class="form-group">
|
|
<label>标题 *</label>
|
|
<input type="text" name="title" value="${banner ? banner.title : ''}" required>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>图片 *</label>
|
|
<input type="file" name="image_file" accept="image/*">
|
|
${banner ? `<small>当前: ${banner.image || '无'}</small>` : ''}
|
|
<input type="hidden" name="image" value="${banner ? banner.image : ''}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>链接</label>
|
|
<input type="text" name="link" value="${banner ? banner.link || '' : ''}" placeholder="/song/1">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>排序</label>
|
|
<input type="number" name="sort_order" value="${banner ? banner.sort_order : '0'}">
|
|
</div>
|
|
<div class="form-group">
|
|
<label>
|
|
<input type="checkbox" name="is_active" ${!banner || banner.is_active ? 'checked' : ''}> 启用
|
|
</label>
|
|
</div>
|
|
<div class="form-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="modal.close()">取消</button>
|
|
<button type="submit" class="btn btn-primary">保存</button>
|
|
</div>
|
|
</form>
|
|
`;
|
|
|
|
modal.open(banner ? '编辑轮播图' : '添加轮播图', html);
|
|
songForm.setupFileUpload('image_file', 'image', '/api/admin/upload/cover');
|
|
},
|
|
|
|
edit(banner) {
|
|
this.open(banner);
|
|
},
|
|
|
|
async submit(e, id) {
|
|
e.preventDefault();
|
|
const form = e.target;
|
|
const data = {
|
|
title: form.title.value,
|
|
image: form.image.value,
|
|
link: form.link.value,
|
|
sort_order: parseInt(form.sort_order.value) || 0,
|
|
is_active: form.is_active.checked ? 1 : 0
|
|
};
|
|
|
|
if (!data.image) {
|
|
toast('请上传图片', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (id) {
|
|
await api.put(`/api/admin/banners/${id}`, data);
|
|
toast('轮播图已更新', 'success');
|
|
} else {
|
|
await api.post('/api/admin/banners', data);
|
|
toast('轮播图已添加', 'success');
|
|
}
|
|
modal.close();
|
|
pages.banners();
|
|
} catch (err) {
|
|
toast('操作失败', 'error');
|
|
}
|
|
},
|
|
|
|
async delete(id) {
|
|
if (!confirm('确定要删除这个轮播图吗?')) return;
|
|
try {
|
|
await api.delete(`/api/admin/banners/${id}`);
|
|
toast('轮播图已删除', 'success');
|
|
pages.banners();
|
|
} catch (err) {
|
|
toast('删除失败', 'error');
|
|
}
|
|
}
|
|
};
|
|
|
|
// ============ 辅助函数 ============
|
|
function formatTime(seconds) {
|
|
if (!seconds || isNaN(seconds)) return '0:00';
|
|
const min = Math.floor(seconds / 60);
|
|
const sec = Math.floor(seconds % 60);
|
|
return `${min}:${sec.toString().padStart(2, '0')}`;
|
|
}
|
|
|
|
function formatNumber(num) {
|
|
if (!num) return '0';
|
|
if (num >= 10000) return (num / 10000).toFixed(1) + '万';
|
|
return num.toString();
|
|
}
|
|
|
|
function updateNav() {
|
|
document.querySelectorAll('.nav-item').forEach(item => {
|
|
item.classList.toggle('active', item.dataset.page === currentPage);
|
|
});
|
|
}
|
|
|
|
// ============ 初始化 ============
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// 导航点击
|
|
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
|
|
item.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const page = item.dataset.page;
|
|
if (pages[page]) {
|
|
pages[page]();
|
|
}
|
|
});
|
|
});
|
|
|
|
// 默认显示仪表盘
|
|
pages.dashboard();
|
|
});
|