Files
vipsong/templates/index.html
T

262 lines
9.4 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>vipsong Web — 音乐加密格式转码</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f6fa; color: #2d3436; min-height: 100vh; padding: 2rem;
}
.container { max-width: 720px; margin: 0 auto; }
h1 { font-size: 1.5rem; margin-bottom: 0.5rem; }
.subtitle { color: #636e72; font-size: 0.9rem; margin-bottom: 1.5rem; }
.upload-area {
border: 2px dashed #dfe6e9; border-radius: 12px; padding: 2.5rem 1.5rem;
text-align: center; cursor: pointer; transition: all .2s; background: #fff;
}
.upload-area:hover { border-color: #0984e3; background: #f8f9fa; }
.upload-area.dragover { border-color: #0984e3; background: #e3f2fd; }
.upload-area i { font-size: 2rem; color: #b2bec3; margin-bottom: 0.5rem; display: block; }
.upload-area p { color: #636e72; font-size: 0.9rem; }
.format-selector {
margin: 1.5rem 0; display: flex; align-items: center; gap: 1rem;
}
.format-selector label { font-weight: 600; color: #2d3436; }
select {
padding: 0.5rem 1rem; border: 1px solid #dfe6e9; border-radius: 8px;
font-size: 0.9rem; background: #fff; cursor: pointer;
}
.btn-convert {
display: block; width: 100%; padding: 0.8rem; margin-top: 1rem;
background: #0984e3; color: #fff; border: none; border-radius: 8px;
font-size: 1rem; font-weight: 600; cursor: pointer; transition: .2s;
}
.btn-convert:hover:not(:disabled) { background: #074c8a; }
.btn-convert:disabled { background: #b2bec3; cursor: not-allowed; }
.file-list { margin-top: 1.5rem; }
.file-item {
display: flex; align-items: center; justify-content: space-between;
padding: 0.8rem 1rem; border-radius: 8px; margin-bottom: 0.5rem;
background: #fff; border: 1px solid #dfe6e9;
}
.file-info { display: flex; align-items: center; gap: 0.8rem; }
.file-name { font-size: 0.9rem; max-width: 320px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.file-status { font-size: 0.8rem; padding: 0.2rem 0.6rem; border-radius: 12px; }
.status-pending { background: #f1f2f6; color: #636e72; }
.status-upload { background: #fff3cd; color: #856404; }
.status-convert { background: #fff3cd; color: #856404; }
.status-done { background: #d4edda; color: #155724; }
.status-error { background: #f8d7da; color: #721c24; }
.btn-download { padding: 0.3rem 0.8rem; background: #00b894; color: #fff; border: none; border-radius: 6px; font-size: 0.8rem; cursor: pointer; }
.btn-download:hover { background: #0984e3; }
.supported { margin-top: 1rem; font-size: 0.8rem; color: #636e72; }
.supported span { color: #0984e3; font-weight: 600; }
/* 弹窗模态框 - 毛玻璃居中效果 */
.modal-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0, 0, 0, 0.45);
display: flex; align-items: center; justify-content: center;
z-index: 10000;
}
.modal-content {
background: rgba(255, 255, 255, 0.25);
border-radius: 16px; padding: 1.8rem 1.5rem;
text-align: center; width: 340px; max-width: 90%;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.35);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
.modal-content h3 { color: #2d3436; font-size: 1.1rem; margin-bottom: 0.5rem; }
.modal-content p { color: #636e72; font-size: 0.85rem; margin-bottom: 1.2rem; line-height: 1.4; }
.modal-btn {
padding: 0.5rem 1.5rem; background: #0984e3; color: #fff;
border: none; border-radius: 8px; font-size: 0.9rem; cursor: pointer;
transition: background .2s;
}
.modal-btn:hover { background: #074c8a; }
@media (max-width: 480px) {
body { padding: 1rem; }
h1 { font-size: 1.2rem; }
}
</style>
</head>
<body>
<div class="container">
<h1>vipsong — 音乐加密格式转码</h1>
<p class="subtitle">支持 .ncm (网易云) / .kgm .vpr (酷狗) → mp3 / flac / wav / ogg / m4a</p>
<form id="uploadForm">
<div class="upload-area" id="dropZone">
<i>📁</i>
<p>点击选择文件,或拖拽到这里</p>
<input type="file" id="fileInput" multiple accept=".ncm,.kgm,.kgma,.vpr" style="display:none">
</div>
<div class="format-selector">
<label>输出格式:</label>
<select id="formatSelect">
<option value="mp3">MP3 (默认)</option>
<option value="flac">FLAC (无损)</option>
<option value="wav">WAV (无损)</option>
<option value="ogg">OGG</option>
<option value="m4a">M4A</option>
</select>
</div>
<button type="button" id="btnConvert" class="btn-convert" disabled>开始转码</button>
</form>
<div class="file-list" id="fileList"></div>
<p class="supported">
<span>支持格式:</span> .ncm / .kgm / .kgma / .vpr
&nbsp;|&nbsp; <span>输出格式:</span> mp3 / flac / wav / ogg / m4a
</p>
</div>
<script>
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const formatSelect = document.getElementById('formatSelect');
const btnConvert = document.getElementById('btnConvert');
const fileList = document.getElementById('fileList');
let files = [];
const SUPPORTED_EXTENSIONS = ['.ncm', '.kgm', '.kgma', '.vpr'];
function showModal(message) {
const existing = document.querySelector('.modal-overlay');
if (existing) existing.remove();
const overlay = document.createElement('div');
overlay.className = 'modal-overlay';
overlay.innerHTML =
'<div class="modal-content">' +
'<h3>⚠️ 文件格式不支持</h3>' +
'<p>' + message + '</p>' +
'<button class="modal-btn" onclick="this.closest(\'.modal-overlay\').remove()">确定</button>' +
'</div>';
document.body.appendChild(overlay);
}
// 点击上传区域选择文件
dropZone.addEventListener('click', () => fileInput.click());
// 拖拽
['dragenter', 'dragover'].forEach(evt => {
dropZone.addEventListener(evt, e => {
e.preventDefault(); e.stopPropagation();
dropZone.classList.add('dragover');
});
});
['dragleave', 'drop'].forEach(evt => {
dropZone.addEventListener(evt, e => {
e.preventDefault(); e.stopPropagation();
dropZone.classList.remove('dragover');
});
});
// 拖拽放下
dropZone.addEventListener('drop', e => {
const dt = e.dataTransfer;
handleFiles(dt.files);
});
// 文件选择
fileInput.addEventListener('change', e => {
handleFiles(e.target.files);
});
function handleFiles(fileListObj) {
for (const f of fileListObj) {
const ext = f.name.slice(f.name.lastIndexOf('.')).toLowerCase();
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
showModal('文件 "' + f.name + '" 格式不受支持<br>仅支持: .ncm / .kgm / .kgma / .vpr 格式的音乐文件');
continue;
}
if (files.some(existing => existing.name === f.name)) continue;
files.push(f);
}
renderFileList();
}
function renderFileList() {
fileList.innerHTML = '';
if (files.length === 0) {
btnConvert.disabled = true;
return;
}
btnConvert.disabled = false;
files.forEach((f, idx) => {
const item = document.createElement('div');
item.className = 'file-item';
item.innerHTML = `
<div class="file-info">
<span class="file-name">${f.name}</span>
<span class="file-status status-pending" id="status-${idx}">等待</span>
</div>
<div id="actions-${idx}"></div>
`;
fileList.appendChild(item);
});
}
function setStatus(idx, text, cls) {
const el = document.getElementById(`status-${idx}`);
if (el) { el.textContent = text; el.className = 'file-status status-' + cls; }
}
async function convertFile(file, idx) {
const formData = new FormData();
formData.append('file', file);
formData.append('format', formatSelect.value);
setStatus(idx, '上传中', 'upload');
try {
const resp = await fetch('/convert', { method: 'POST', body: formData });
const data = await resp.json();
if (!resp.ok) throw new Error(data.detail || '转换失败');
setStatus(idx, '完成', 'done');
const actions = document.getElementById(`actions-${idx}`);
const a = document.createElement('a');
a.href = data.download_url;
a.textContent = '下载';
a.className = 'btn-download';
a.download = data.output_name;
actions.appendChild(a);
} catch (error) {
setStatus(idx, '失败: ' + error.message, 'error');
}
}
// 开始转码
btnConvert.addEventListener('click', async () => {
btnConvert.disabled = true;
btnConvert.textContent = '转码中...';
for (let i = 0; i < files.length; i++) {
await convertFile(files[i], i);
}
btnConvert.disabled = false;
btnConvert.textContent = '开始转码';
});
</script>
</body>
</html>