mirror of
https://github.com/NeteaseCloudMusicApiEnhanced/api-enhanced.git
synced 2026-03-21 19:13:10 +00:00
Compare commits
3 Commits
d760b2960a
...
51a958936c
| Author | SHA1 | Date | |
|---|---|---|---|
| 51a958936c | |||
| 7772431cb7 | |||
| c2871322d0 |
10
CHANGELOG.MD
10
CHANGELOG.MD
@ -7,6 +7,16 @@
|
||||
- chore: 更新依赖项 (music-metadata: ^11.11.1 -> ^11.11.2, ansi-escapes: ^7.2.0 -> ^7.3.0, commander: ^14.0.2 -> ^14.0.3)
|
||||
- chore: 更新GitHub Actions (checkout: v4 -> v6, setup-node: v4 -> v6, upload-artifact: v4 -> v6, download-artifact: v4 -> v7, github-script: v7 -> v8)
|
||||
- refactor: 注释掉IP地址日志输出以提升隐私保护
|
||||
- refactor: 重构前端测试页面, 主要改进:
|
||||
- 统一使用简洁现代的设计风格
|
||||
- 去除渐变背景和复杂动画
|
||||
- 使用纯色背景(#f5f5f5)和白色卡片
|
||||
- 优化表单布局和交互体验
|
||||
- 增强错误处理和加载状态
|
||||
- 移除第三方框架依赖(Tailwind、Bootstrap、MDUI)
|
||||
- 升级Vue 2到Vue 3
|
||||
- 将硬编码的配置项移至前端表单
|
||||
- 所有页面现在都保持一致的设计语言,简洁清爽,功能完整。
|
||||
|
||||
### 4.25.0 | 2024.11.16
|
||||
- feat: 增加副歌时间、相关歌单推荐接口,原有相关歌单接口已废弃;fix: 将部分易盾白名单接口替换为eapi [#30](https://gitlab.com/Binaryify/neteasecloudmusicapi/-/merge_requests/30)
|
||||
|
||||
164
public/api.html
164
public/api.html
@ -5,82 +5,148 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>API 调试界面</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
input, button {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
input, select {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
flex: 1;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus, select:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 10px 24px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.data-result {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
.data-result > div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.data-result label {
|
||||
margin-bottom: 10px;
|
||||
margin-bottom: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
#data, #result {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
|
||||
textarea {
|
||||
flex: 1;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
min-height: 350px;
|
||||
outline: none;
|
||||
}
|
||||
#data {
|
||||
border-right: 1px solid #ccc;
|
||||
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>API 调试界面</h1>
|
||||
<form onsubmit="event.preventDefault(); sendRequest();">
|
||||
<label for="uri">uri</label>
|
||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||
<label for="crypto">crypto</label>
|
||||
<select id="crypto" name="crypto">
|
||||
<option value="weapi">weapi</option>
|
||||
<option value="eapi">eapi</option>
|
||||
<option value="api">api</option>
|
||||
<option value="linuxapi">linuxapi</option>
|
||||
<option value="" selected>(默认)</option>
|
||||
</select>
|
||||
<button type="submit">发送</button>
|
||||
<div class="form-row">
|
||||
<label for="uri">URI</label>
|
||||
<input type="text" id="uri" name="uri" value="/api/song/lyric/v1">
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label for="crypto">加密方式</label>
|
||||
<select id="crypto" name="crypto">
|
||||
<option value="weapi">weapi</option>
|
||||
<option value="eapi">eapi</option>
|
||||
<option value="api">api</option>
|
||||
<option value="linuxapi">linuxapi</option>
|
||||
<option value="" selected>(默认)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<label></label>
|
||||
<button type="submit">发送请求</button>
|
||||
</div>
|
||||
</form>
|
||||
<div class="data-result">
|
||||
<div>
|
||||
<label for="result">result</label>
|
||||
<textarea id="result" name="result"></textarea>
|
||||
<label for="result">响应结果</label>
|
||||
<textarea id="result" name="result" readonly></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label for="data">data</label>
|
||||
<label for="data">请求数据</label>
|
||||
<textarea id="data" name="data">
|
||||
{
|
||||
"cp": false,
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
'use strict'
|
||||
const WASM_BINARY_PLACEHOLDER = 'WASM_BINARY_PLACEHOLDER';
|
||||
const logger = require('../../util/logger.js')
|
||||
// See https://github.com/Distributive-Network/PythonMonkey/issues/266
|
||||
if (typeof globalThis.setInterval != 'function'){
|
||||
globalThis.setInterval = function pm$$setInterval(fn, timeout) {
|
||||
@ -1612,9 +1611,9 @@ function instantiateRuntime(){
|
||||
|
||||
function GenerateFP(floatArray) {
|
||||
let PCMBuffer = Float32Array.from(floatArray)
|
||||
logger.info('[afp] input samples n=', PCMBuffer.length)
|
||||
console.info('[afp] input samples n=', PCMBuffer.length)
|
||||
return instantiateRuntime().then((fpRuntime) => {
|
||||
logger.info('[afp] begin fingerprinting')
|
||||
console.info('[afp] begin fingerprinting')
|
||||
let fp_vector = fpRuntime.ExtractQueryFP(PCMBuffer.buffer)
|
||||
let result_buf = new Uint8Array(fp_vector.size());
|
||||
for (let t = 0; t < fp_vector.size(); t++)
|
||||
|
||||
@ -1,27 +1,142 @@
|
||||
<!DOCTYPE html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>听歌识曲 Demo</title>
|
||||
<style>
|
||||
* {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: none;
|
||||
border-top: 1px solid #eee;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
line-height: 1.6;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-family: sans-serif;
|
||||
color: #333;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input[type="file"] {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
audio {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
canvas {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
transition: all linear 0.1s;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.canvas-active {
|
||||
@ -29,39 +144,80 @@
|
||||
}
|
||||
|
||||
pre {
|
||||
overflow: scroll;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
|
||||
.warning {
|
||||
padding: 12px 16px;
|
||||
background: #fef3c7;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #92400e;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>听歌识曲 Demo (Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a>)</h1>
|
||||
<hr>
|
||||
<p><b>DISCLAIMER: </b></p>
|
||||
<p>This site uses the offical NetEase audio matcher APIs (reverse engineered from <a
|
||||
href="https://fn.music.163.com/g/chrome-extension-home-page-beta/">https://fn.music.163.com/g/chrome-extension-home-page-beta/</a>)
|
||||
</p>
|
||||
<p>And DOES NOT condone copyright infringment nor intellectual property theft.</p>
|
||||
<hr>
|
||||
<p><b>NOTE:</b></p>
|
||||
<p>Before start using the site, you may want to access this link first:</p>
|
||||
<a href="https://cors-anywhere.herokuapp.com/corsdemo">https://cors-anywhere.herokuapp.com/corsdemo</a>
|
||||
<p>Since Netease APIs do not have CORS headers, this is required to alleviate this restriction.</p>
|
||||
<hr>
|
||||
<p>Usage:</p>
|
||||
<li>Select your audio file through "Choose File" picker</li>
|
||||
<li>Hit the "Clip" button and wait for the results!</li>
|
||||
<div class="container">
|
||||
<h1>听歌识曲 Demo</h1>
|
||||
<p class="subtitle">Credit: <a href="https://github.com/mos9527/ncm-afp" target="_blank">https://github.com/mos9527/ncm-afp</a></p>
|
||||
|
||||
<audio id="audio" controls autoplay></audio>
|
||||
<canvas id="canvas"></canvas>
|
||||
<button id="invoke">Clip</button>
|
||||
<input type="file" name="picker" accept="*" id="file">
|
||||
<hr>
|
||||
<label for="use-mic">Mix in Microphone input</label>
|
||||
<input type="checkbox" name="use-mic" id="usemic">
|
||||
<hr>
|
||||
<pre id="logs"></pre>
|
||||
<hr>
|
||||
|
||||
<div class="warning">
|
||||
<strong>免责声明:</strong>本站点使用网易云音乐官方音频识别API(逆向自 <a href="https://fn.music.163.com/g/chrome-extension-home-page-beta/" target="_blank">Chrome 扩展页面</a>),不鼓励版权侵犯或知识产权盗窃。
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>使用说明</h3>
|
||||
<p>在使用本站点之前,您可能需要先访问以下链接:</p>
|
||||
<p><a href="https://cors-anywhere.herokuapp.com/corsdemo" target="_blank">https://cors-anywhere.herokuapp.com/corsdemo</a></p>
|
||||
<p>由于网易云音乐API没有CORS头,这是解决此限制的必要步骤。</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>使用方法</h3>
|
||||
<ul style="padding-left: 20px; font-size: 14px; color: #555;">
|
||||
<li>通过"选择文件"选择您的音频文件</li>
|
||||
<li>点击"识别"按钮并等待结果</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<audio id="audio" controls autoplay></audio>
|
||||
<canvas id="canvas"></canvas>
|
||||
|
||||
<div class="control-group">
|
||||
<button id="invoke">识别</button>
|
||||
<input type="file" name="picker" accept="*" id="file">
|
||||
</div>
|
||||
|
||||
<div class="control-group">
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" name="use-mic" id="usemic">
|
||||
<label for="usemic">混合麦克风输入</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 12px;">日志</h3>
|
||||
<pre id="logs"></pre>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
<script src="./afp.wasm.js"></script>
|
||||
<script src="./afp.js"></script>
|
||||
<script type="module">
|
||||
@ -76,13 +232,17 @@
|
||||
let canvas = document.getElementById('canvas')
|
||||
let canvasCtx = canvas.getContext('2d')
|
||||
let logs = document.getElementById('logs')
|
||||
logs.write = line => logs.innerHTML += line + '\n'
|
||||
logs.write = line => {
|
||||
// Append log lines as text to avoid interpreting content as HTML
|
||||
logs.appendChild(document.createTextNode(line));
|
||||
logs.appendChild(document.createElement('br'));
|
||||
}
|
||||
|
||||
function RecorderCallback(channelL) {
|
||||
let sampleBuffer = new Float32Array(channelL.subarray(0, duration * 8000))
|
||||
GenerateFP(sampleBuffer).then(FP => {
|
||||
logs.write(`[index] Generated FP ${FP}`)
|
||||
logs.write('[index] Now querying, please wait...')
|
||||
logs.write(`[index] 生成指纹 ${FP}`)
|
||||
logs.write('[index] 正在查询,请稍候...')
|
||||
fetch(
|
||||
'/audio/match?' +
|
||||
new URLSearchParams({
|
||||
@ -91,9 +251,9 @@
|
||||
method: 'POST'
|
||||
}).then(resp => resp.json()).then(resp => {
|
||||
if (!resp.data.result) {
|
||||
return logs.write('[index] Query failed with no results.')
|
||||
return logs.write('[index] 查询失败,无结果')
|
||||
}
|
||||
logs.write(`[index] Query complete. Results=${resp.data.result.length}`)
|
||||
logs.write(`[index] 查询完成。结果数量=${resp.data.result.length}`)
|
||||
for (var song of resp.data.result) {
|
||||
logs.write(
|
||||
`[result] <a target="_blank" href="https://music.163.com/song?id=${song.song.id}">${song.song.name} - ${song.song.album.name} (${song.startTime / 1000}s)</a>`
|
||||
@ -104,20 +264,19 @@
|
||||
}
|
||||
|
||||
function InitAudioCtx() {
|
||||
// AFP.wasm can't do it with anything other than 8KHz
|
||||
audioCtx = new AudioContext({ 'sampleRate': 8000 })
|
||||
if (audioCtx.state == 'suspended')
|
||||
return false
|
||||
let audioNode = audioCtx.createMediaElementSource(audio)
|
||||
audioCtx.audioWorklet.addModule('rec.js').then(() => {
|
||||
recorderNode = new AudioWorkletNode(audioCtx, 'timed-recorder')
|
||||
audioNode.connect(recorderNode) // recorderNode doesn't output anything
|
||||
audioNode.connect(recorderNode)
|
||||
audioNode.connect(audioCtx.destination)
|
||||
recorderNode.port.onmessage = event => {
|
||||
switch (event.data.message) {
|
||||
case 'finished':
|
||||
RecorderCallback(event.data.recording)
|
||||
clip.innerHTML = 'Clip'
|
||||
clip.innerHTML = '识别'
|
||||
clip.disabled = false
|
||||
canvas.classList.remove('canvas-active')
|
||||
break
|
||||
@ -130,7 +289,6 @@
|
||||
logs.write(event.data.message)
|
||||
}
|
||||
}
|
||||
// Attempt to get user's microphone and connect it to the AudioContext.
|
||||
navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
echoCancellation: false,
|
||||
@ -142,7 +300,7 @@
|
||||
micSourceNode = audioCtx.createMediaStreamSource(micStream);
|
||||
micSourceNode.connect(recorderNode)
|
||||
usemic.checked = true
|
||||
logs.write('[rec.js] Microphone attached.')
|
||||
logs.write('[rec.js] 麦克风已连接')
|
||||
});
|
||||
});
|
||||
return true
|
||||
@ -161,10 +319,20 @@
|
||||
else
|
||||
micSourceNode.connect(recorderNode)
|
||||
})
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
.replace(/\//g, '/');
|
||||
}
|
||||
file.addEventListener('change', event => {
|
||||
file.files[0].arrayBuffer().then(
|
||||
async buffer => {
|
||||
logs.write(`[index] File ${file.files[0].name} loaded.`)
|
||||
const safeName = escapeHtml(file.files[0].name)
|
||||
logs.write(`[index] 文件 ${safeName} 已加载`)
|
||||
audio.src = window.URL.createObjectURL(new Blob([buffer]))
|
||||
clip.disabled = false
|
||||
})
|
||||
@ -188,12 +356,13 @@
|
||||
UpdateCanvas()
|
||||
let requestCtx = setInterval(() => {
|
||||
try {
|
||||
if (InitAudioCtx()) { // Put this here so we don't have to deal with the 'user did not interact' thing
|
||||
if (InitAudioCtx()) {
|
||||
clearInterval(requestCtx)
|
||||
logs.write('[rec.js] Audio Context started.')
|
||||
logs.write('[rec.js] 音频上下文已启动')
|
||||
}
|
||||
} catch {
|
||||
// Fail silently
|
||||
}
|
||||
}, 100)
|
||||
</script>
|
||||
</html>
|
||||
@ -5,67 +5,320 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>更新头像</title>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.avatar-wrapper {
|
||||
position: relative;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
margin: 0 auto 32px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.avatar-wrapper.loading .avatar {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.loading-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.avatar-wrapper.loading .loading-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 3px solid #e0e0e0;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-top: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) translateY(100px);
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
opacity: 0;
|
||||
transition: all 0.3s ease;
|
||||
z-index: 1000;
|
||||
max-width: 90%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toast.show {
|
||||
transform: translateX(-50%) translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
.toast.info {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
<div class="container">
|
||||
<h1>更新头像</h1>
|
||||
<p class="subtitle">选择一张图片作为您的新头像</p>
|
||||
|
||||
<div class="avatar-wrapper" id="avatarWrapper">
|
||||
<img id="avatar" class="avatar" src="" alt="头像" />
|
||||
<div class="loading-overlay">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="upload-btn">
|
||||
选择图片
|
||||
<input id="file" type="file" accept="image/*" />
|
||||
</label>
|
||||
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">
|
||||
还没有登录?点击登录
|
||||
</a>
|
||||
|
||||
<p class="hint">支持 JPG、PNG 格式,建议尺寸 200x200</p>
|
||||
</div>
|
||||
<input id="file" type="file" />
|
||||
<img id="avatar" style="height: 200px; width: 200px; border-radius: 50%" />
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
||||
"></script>
|
||||
|
||||
<div id="toast" class="toast"></div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script>
|
||||
main()
|
||||
|
||||
async function main() {
|
||||
document.querySelector('input[type="file"]').addEventListener(
|
||||
'change',
|
||||
function (e) {
|
||||
var file = this.files[0]
|
||||
upload(file)
|
||||
},
|
||||
false,
|
||||
)
|
||||
const res = await axios({
|
||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||
withCredentials: true, //跨域的话必须设置
|
||||
})
|
||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl
|
||||
const fileInput = document.querySelector('input[type="file"]');
|
||||
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||
|
||||
fileInput.addEventListener('change', function(e) {
|
||||
const file = this.files[0];
|
||||
if (file) {
|
||||
upload(file);
|
||||
}
|
||||
}, false);
|
||||
|
||||
try {
|
||||
showToast('正在加载头像...', 'info');
|
||||
avatarWrapper.classList.add('loading');
|
||||
const res = await axios({
|
||||
url: `/user/detail?uid=32953014×tamp=${Date.now()}`,
|
||||
withCredentials: true
|
||||
});
|
||||
document.querySelector('#avatar').src = res.data.profile.avatarUrl;
|
||||
hideToast();
|
||||
} catch (error) {
|
||||
hideToast();
|
||||
showToast('加载头像失败,请刷新页面重试', 'error');
|
||||
console.error('加载头像失败:', error);
|
||||
} finally {
|
||||
avatarWrapper.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
async function upload(file) {
|
||||
var formData = new FormData()
|
||||
formData.append('imgFile', file)
|
||||
const imgSize = await getImgSize(file)
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
document.querySelector('#avatar').src = res.data.data.url
|
||||
const avatarWrapper = document.getElementById('avatarWrapper');
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
showToast('请选择图片文件', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
showToast('正在上传头像...', 'info');
|
||||
avatarWrapper.classList.add('loading');
|
||||
|
||||
var formData = new FormData();
|
||||
formData.append('imgFile', file);
|
||||
const imgSize = await getImgSize(file);
|
||||
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/avatar/upload?cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
});
|
||||
|
||||
document.querySelector('#avatar').src = res.data.data.url;
|
||||
showToast('头像更新成功!', 'success');
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error);
|
||||
const errorMsg = error.response?.data?.message || error.message || '上传失败,请重试';
|
||||
showToast(errorMsg, 'error');
|
||||
} finally {
|
||||
avatarWrapper.classList.remove('loading');
|
||||
}
|
||||
}
|
||||
|
||||
function getImgSize(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader()
|
||||
reader.readAsDataURL(file)
|
||||
reader.onload = function (theFile) {
|
||||
let image = new Image()
|
||||
image.src = theFile.target.result
|
||||
image.onload = function () {
|
||||
let reader = new FileReader();
|
||||
reader.readAsDataURL(file);
|
||||
reader.onload = function(theFile) {
|
||||
let image = new Image();
|
||||
image.src = theFile.target.result;
|
||||
image.onload = function() {
|
||||
resolve({
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
};
|
||||
image.onerror = function() {
|
||||
reject(new Error('图片加载失败'));
|
||||
};
|
||||
};
|
||||
reader.onerror = function() {
|
||||
reject(new Error('文件读取失败'));
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function showToast(message, type = 'info') {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.textContent = message;
|
||||
toast.className = `toast ${type}`;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
toast.classList.add('show');
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
toast.classList.remove('show');
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
function hideToast() {
|
||||
const toast = document.getElementById('toast');
|
||||
toast.classList.remove('show');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@ -4,26 +4,135 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>云盘上传</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.songs-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.song-item {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #eee;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.song-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
||||
</div>
|
||||
<input id="file" type="file" multiple />
|
||||
<div id="app">
|
||||
<ul>
|
||||
<li v-for="(item,index) in songs" :key="index">{{item.songName}}</li>
|
||||
</ul>
|
||||
<div class="container">
|
||||
<h1>云盘上传</h1>
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="upload-section">
|
||||
<label class="upload-btn">
|
||||
选择文件(支持多选)
|
||||
<input id="file" type="file" multiple accept="audio/*" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="app">
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<ul v-else-if="songs.length > 0" class="songs-list">
|
||||
<li v-for="(item, index) in songs" :key="index" class="song-item">
|
||||
{{ item.songName }}
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="empty-state">暂无云盘歌曲</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||
<script>
|
||||
const app = Vue.createApp({
|
||||
data() {
|
||||
return {
|
||||
songs: [],
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -31,19 +140,23 @@
|
||||
},
|
||||
methods: {
|
||||
getData() {
|
||||
console.info('getdata')
|
||||
const _this = this
|
||||
this.loading = true
|
||||
axios({
|
||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
|
||||
'cookie',
|
||||
)}`,
|
||||
}).then((res) => {
|
||||
console.info(res.data)
|
||||
_this.songs = res.data.data
|
||||
url: `/user/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
.then((res) => {
|
||||
this.songs = res.data.data || []
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('获取云盘数据失败:', err)
|
||||
})
|
||||
.finally(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
},
|
||||
}).mount('#app')
|
||||
|
||||
const fileUpdateTime = {}
|
||||
let fileLength = 0
|
||||
|
||||
@ -51,51 +164,46 @@
|
||||
document
|
||||
.querySelector('input[type="file"]')
|
||||
.addEventListener('change', function (e) {
|
||||
console.info(this.files)
|
||||
let currentIndx = 0
|
||||
fileLength = this.files.length
|
||||
for (const item of this.files) {
|
||||
currentIndx += 1
|
||||
upload(item, currentIndx)
|
||||
const files = this.files
|
||||
if (files.length === 0) return
|
||||
|
||||
fileLength = files.length
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
upload(files[i], i + 1)
|
||||
}
|
||||
})
|
||||
}
|
||||
main()
|
||||
|
||||
function upload(file, currentIndx) {
|
||||
function upload(file, currentIndex) {
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem(
|
||||
'cookie',
|
||||
)}`,
|
||||
url: `/cloud?time=${Date.now()}&cookie=${localStorage.getItem('cookie')}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
.then((res) => {
|
||||
console.info(`${file.name} 上传成功`)
|
||||
if (currentIndx >= fileLength) {
|
||||
console.info('上传完毕')
|
||||
console.log(`${file.name} 上传成功`)
|
||||
if (currentIndex >= fileLength) {
|
||||
console.log('所有文件上传完毕')
|
||||
}
|
||||
app.getData()
|
||||
})
|
||||
.catch(async (err) => {
|
||||
console.info(err)
|
||||
console.info(fileUpdateTime)
|
||||
fileUpdateTime[file.name]
|
||||
? (fileUpdateTime[file.name] += 1)
|
||||
: (fileUpdateTime[file.name] = 1)
|
||||
.catch((err) => {
|
||||
console.error(`${file.name} 上传失败:`, err)
|
||||
fileUpdateTime[file.name] = (fileUpdateTime[file.name] || 0) + 1
|
||||
if (fileUpdateTime[file.name] >= 4) {
|
||||
console.error(`丢,这首歌怎么都传不上:${file.name}`)
|
||||
console.error(`文件 ${file.name} 上传失败次数过多,已停止重试`)
|
||||
return
|
||||
} else {
|
||||
console.error(`${file.name} 失败 ${fileUpdateTime[file.name]} 次`)
|
||||
console.error(`${file.name} 上传失败 ${fileUpdateTime[file.name]} 次,正在重试...`)
|
||||
upload(file, currentIndex)
|
||||
}
|
||||
// await login()
|
||||
upload(file, currentIndx)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -36,12 +36,12 @@
|
||||
}
|
||||
</script>
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-139996012-1"></script>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-BPRGR23JEG"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
function gtag() { dataLayer.push(arguments); }
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-139996012-1');
|
||||
gtag('config', 'G-BPRGR23JEG');
|
||||
</script>
|
||||
</html>
|
||||
|
||||
@ -1,50 +1,181 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>eapi 参数和返回内容解析</title>
|
||||
<link href="https://unpkg.com/tailwindcss@^2/dist/tailwind.min.css" rel="stylesheet">
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
min-height: 200px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.radio-group {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.radio-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.radio-item input[type="radio"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.radio-item label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 12px 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.result-section {
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.result-section label {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.decode-result {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background: #f9f9f9;
|
||||
padding: 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
min-height: 200px;
|
||||
max-height: 400px;
|
||||
overflow: auto;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.example-section {
|
||||
margin-top: 32px;
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid #eee;
|
||||
}
|
||||
|
||||
.example-section h2 {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.example-section img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid #eee;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<style>
|
||||
.decode-result {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
background-color: #f0f0f0;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
height: 300px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<div id="app" class="p-5 flex flex-col">
|
||||
<h1 class="text-2xl font-bold mb-5">eapi 参数和返回内容解析</h1>
|
||||
<textarea class="border border-gray-300 p-3 mb-5" v-model="hexString" rows="10"></textarea>
|
||||
<button @click="decrypt" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
|
||||
解密
|
||||
</button>
|
||||
<div class="mt-3">
|
||||
<input type="radio" id="format" name="format" v-model="isReq" value="true">
|
||||
<label for="format" class="ml-2">请求数据request params(针对请求数据的 params)</label>
|
||||
<input type="radio" id="noFormat" name="format" v-model="isReq" value="false" class="ml-5">
|
||||
<label for="noFormat" class="ml-2">返回数据 response 二进制数据(针对返回内容解析)</label>
|
||||
</div>
|
||||
<div>
|
||||
<p>解密结果:
|
||||
<pre class="decode-result">{{ JSON.stringify(JSON.parse(result), null, 2) }}</pre>
|
||||
</p>
|
||||
<div id="app" class="container">
|
||||
<h1>eapi 参数和返回内容解析</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="hexString">十六进制字符串</label>
|
||||
<textarea id="hexString" v-model="hexString" rows="10"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="radio-group">
|
||||
<div class="radio-item">
|
||||
<input type="radio" id="req" name="format" v-model="isReq" value="true">
|
||||
<label for="req">请求数据 request params</label>
|
||||
</div>
|
||||
<div class="radio-item">
|
||||
<input type="radio" id="resp" name="format" v-model="isReq" value="false">
|
||||
<label for="resp">返回数据 response 二进制数据</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p>使用例子:</p>
|
||||
<img src="/static/eapi_params.png" />
|
||||
<img src="/static/eapi_response.png" />
|
||||
<button @click="decrypt">解密</button>
|
||||
|
||||
<div class="result-section">
|
||||
<label>解密结果:</label>
|
||||
<pre class="decode-result">{{ formatResult(result) }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="example-section">
|
||||
<h2>使用示例</h2>
|
||||
<img src="/static/eapi_params.png" alt="请求示例" />
|
||||
<img src="/static/eapi_response.png" alt="响应示例" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -64,6 +195,13 @@
|
||||
this.decrypt()
|
||||
},
|
||||
methods: {
|
||||
formatResult(result) {
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(result), null, 2)
|
||||
} catch (e) {
|
||||
return result
|
||||
}
|
||||
},
|
||||
async decrypt() {
|
||||
try {
|
||||
const res = await axios({
|
||||
@ -77,7 +215,7 @@
|
||||
console.log(res.data);
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
||||
alert(error?.response?.data?.message || '解密失败,数据格式错误')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,33 +7,33 @@
|
||||
<title>网易云音乐 API Enhanced</title>
|
||||
<style>
|
||||
:root {
|
||||
--fg: #111827; /* gray-900 */
|
||||
--muted: #6b7280; /* gray-500 */
|
||||
--border: #e5e7eb; /* gray-200 */
|
||||
--bg: #ffffff;
|
||||
--panel: #f9fafb; /* gray-50 */
|
||||
--accent: #2563eb; /* blue-600 */
|
||||
--fg: #333;
|
||||
--muted: #666;
|
||||
--border: #ddd;
|
||||
--bg: #f5f5f5;
|
||||
--panel: #ffffff;
|
||||
--accent: #333;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; font-family: system-ui, -apple-system, Segoe UI, PingFang SC, Helvetica, Arial, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||
body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; color: var(--fg); background: var(--bg); line-height: 1.6; }
|
||||
.container { max-width: 960px; margin: 40px auto; padding: 0 20px; }
|
||||
header.site-header { margin-bottom: 24px; }
|
||||
header.site-header h1 { font-size: 28px; font-weight: 700; margin: 0; }
|
||||
.badge { display: inline-block; margin-left: 8px; padding: 2px 8px; border: 1px solid var(--border); border-radius: 14px; font-size: 12px; color: var(--muted); }
|
||||
.sub { margin-top: 6px; color: var(--muted); }
|
||||
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 16px; }
|
||||
.block h2 { margin: 0 0 10px; font-size: 18px; }
|
||||
header.site-header h1 { font-size: 28px; font-weight: 600; margin: 0; }
|
||||
.badge { display: inline-block; margin-left: 8px; padding: 4px 10px; border: 1px solid var(--border); border-radius: 12px; font-size: 12px; color: var(--muted); }
|
||||
.sub { margin-top: 8px; color: var(--muted); font-size: 14px; }
|
||||
.block { background: var(--panel); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 16px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); }
|
||||
.block h2 { margin: 0 0 12px; font-size: 18px; font-weight: 600; }
|
||||
.kvs { display: grid; grid-template-columns: 140px 1fr; gap: 8px 16px; align-items: center; }
|
||||
.kvs div:first-child { color: var(--muted); }
|
||||
ul.links { list-style: none; padding: 0; margin: 0; }
|
||||
ul.links li { margin: 6px 0; }
|
||||
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); }
|
||||
ul.links li { margin: 8px 0; }
|
||||
ul.links a { color: var(--fg); text-decoration: none; border-bottom: 1px dotted var(--border); transition: all 0.2s ease; }
|
||||
ul.links a:hover { color: var(--accent); border-bottom-color: var(--accent); }
|
||||
pre { margin: 0; background: #fff; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
|
||||
code { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-size: 13px; }
|
||||
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); }
|
||||
footer.site-footer a { color: var(--fg); text-decoration: none; }
|
||||
pre { margin: 0; background: #f9f9f9; border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow: auto; }
|
||||
code { font-family: 'Courier New', monospace; font-size: 13px; }
|
||||
footer.site-footer { margin-top: 24px; padding-top: 12px; border-top: 1px solid var(--border); color: var(--muted); text-align: center; }
|
||||
footer.site-footer a { color: var(--fg); text-decoration: none; transition: color 0.2s ease; }
|
||||
footer.site-footer a:hover { color: var(--accent); }
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -1,92 +1,321 @@
|
||||
<!-- eslint-disable prettier/prettier -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>一起听</title>
|
||||
<title>一起听 - 主机模式</title>
|
||||
<script src="https://unpkg.com/petite-vue"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/mdui@1.0.2/dist/css/mdui.min.css"
|
||||
/>
|
||||
<script src="https://unpkg.com/mdui@1.0.2/dist/js/mdui.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.message {
|
||||
padding: 12px 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.audio-player {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
margin-right: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 24px;
|
||||
padding: 20px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.section h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="number"] {
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group label {
|
||||
font-size: 14px;
|
||||
color: #555;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.share-link {
|
||||
padding: 12px;
|
||||
background: #f0f0f0;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.user-list {
|
||||
list-style: none;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.user-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.user-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.user-avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.track-list {
|
||||
list-style: none;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.track-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #eee;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.track-item:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.track-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.track-cover {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.track-name {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
details {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
details[open] summary {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="mdui-container">
|
||||
<div>
|
||||
<a href="/qrlogin.html"> 如果没登录,请先登录 </a>
|
||||
</div>
|
||||
<body class="container">
|
||||
<a href="/qrlogin.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<h1>一起听 - 主机模式</h1>
|
||||
<div>消息: {{message}}</div>
|
||||
<audio id="player" autoplay controls></audio>
|
||||
<br />
|
||||
<br />
|
||||
<button v-if="!account.login" @click="login">获取登录状态</button>
|
||||
<div>您的当前登录账号为: {{account.nickname}}</div>
|
||||
<br />
|
||||
<div v-if="account.login">
|
||||
<button v-if="!roomInfo.roomId" @click="createRoom">创建房间</button>
|
||||
|
||||
<div class="message">消息: {{message}}</div>
|
||||
|
||||
<audio id="player" class="audio-player" autoplay controls></audio>
|
||||
|
||||
<div v-if="!account.login">
|
||||
<button class="btn" @click="login">获取登录状态</button>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>账号信息</h3>
|
||||
<div>当前登录账号: {{account.nickname}}</div>
|
||||
</div>
|
||||
|
||||
<div v-if="account.login" class="section">
|
||||
<h3>房间管理</h3>
|
||||
<div class="control-buttons">
|
||||
<button v-if="!roomInfo.roomId" class="btn" @click="createRoom">创建房间</button>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>加入房间</summary>
|
||||
<div><span>房间ID: </span><input v-model="roomInfo.roomId" /></div>
|
||||
<div>
|
||||
<span>邀请者 ID: </span><input v-model="roomInfo.inviterId" />
|
||||
<div class="input-group">
|
||||
<label>房间ID:</label>
|
||||
<input v-model="roomInfo.roomId" type="text" />
|
||||
</div>
|
||||
<button @click="joinRoom">点击加入</button>
|
||||
<div class="input-group">
|
||||
<label>邀请者ID:</label>
|
||||
<input v-model="roomInfo.inviterId" type="text" />
|
||||
</div>
|
||||
<button class="btn" @click="joinRoom">加入房间</button>
|
||||
</details>
|
||||
|
||||
<div v-if="roomInfo.roomId">
|
||||
<div>
|
||||
分享链接为:
|
||||
<div v-if="roomInfo.roomId" style="margin-top: 16px;">
|
||||
<h4>分享链接</h4>
|
||||
<div class="share-link">
|
||||
https://st.music.163.com/listen-together/share/?songId=1372188635&roomId={{roomInfo.roomId}}&inviterId={{roomInfo.inviterId}}
|
||||
</div>
|
||||
<br />
|
||||
<button @click="refreshRoom">刷新房间状态</button>
|
||||
<div>在线用户:</div>
|
||||
<ul class="mdui-list">
|
||||
<li
|
||||
v-for="user in roomInfo.roomUsers"
|
||||
class="mdui-list-item mdui-ripple"
|
||||
>
|
||||
<div class="mdui-list-item-avatar">
|
||||
<img :src="user.avatarUrl" />
|
||||
</div>
|
||||
<div class="mdui-list-item-content">{{user.nickname}}</div>
|
||||
<button class="btn" @click="refreshRoom">刷新房间状态</button>
|
||||
<button class="btn" @click="closeRoom">关闭房间</button>
|
||||
|
||||
<h4 style="margin-top: 16px;">在线用户</h4>
|
||||
<ul class="user-list">
|
||||
<li v-for="user in roomInfo.roomUsers" :key="user.userId" class="user-item">
|
||||
<img :src="user.avatarUrl" class="user-avatar" alt="avatar" />
|
||||
<span class="user-name">{{user.nickname}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
<button v-if="roomInfo.roomId" @click="closeRoom">关闭房间</button>
|
||||
</div>
|
||||
</div>
|
||||
<button @click="playTrack">播放</button>
|
||||
<button @click="pauseTrack">暂停</button>
|
||||
<button @click="seekTrack">同步进度</button>
|
||||
|
||||
<div class="section">
|
||||
<h3>播放控制</h3>
|
||||
<div class="control-buttons">
|
||||
<button class="btn" @click="playTrack">播放</button>
|
||||
<button class="btn" @click="pauseTrack">暂停</button>
|
||||
<button class="btn" @click="seekTrack">同步进度</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details>
|
||||
<summary>播放列表</summary>
|
||||
<br />
|
||||
<div>
|
||||
<span>歌单ID: </span><input v-model="playlistInfo.playlistId" />
|
||||
<div class="section">
|
||||
<div class="input-group">
|
||||
<label>歌单ID:</label>
|
||||
<input v-model="playlistInfo.playlistId" type="text" />
|
||||
</div>
|
||||
<button class="btn" @click="loadPlaylist">加载歌单</button>
|
||||
<div style="margin-top: 12px; font-size: 14px; color: #555;">
|
||||
歌单名称: {{playlistInfo.playlistName}}
|
||||
</div>
|
||||
|
||||
<h4 style="margin-top: 16px;">歌单内容</h4>
|
||||
<ul class="track-list">
|
||||
<li
|
||||
@click="gotoTrack(track.id)"
|
||||
v-for="track in playlistInfo.playlistTracks"
|
||||
:key="track.id"
|
||||
class="track-item"
|
||||
>
|
||||
<img :src="track.al.picUrl" class="track-cover" alt="cover" />
|
||||
<span class="track-name">{{track.name}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button @click="loadPlaylist">加载歌单到播放列表</button>
|
||||
<span>{{playlistInfo.playlistName}}</span>
|
||||
<br />
|
||||
<br />
|
||||
<div>歌单内容:</div>
|
||||
<ul class="mdui-list">
|
||||
<li
|
||||
@click="gotoTrack(track.id)"
|
||||
v-for="track in playlistInfo.playlistTracks"
|
||||
class="mdui-list-item mdui-ripple"
|
||||
>
|
||||
<div class="mdui-list-item-avatar">
|
||||
<img :src="track.al.picUrl" />
|
||||
</div>
|
||||
<div class="mdui-list-item-content">{{track.name}}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</details>
|
||||
</body>
|
||||
|
||||
<script>
|
||||
PetiteVue.createApp({
|
||||
message: '请点击获取登录状态',
|
||||
@ -126,7 +355,7 @@
|
||||
this.account.userId = res.data.data.profile.userId
|
||||
this.account.nickname = res.data.data.profile.nickname
|
||||
this.account.login = true
|
||||
this.message = '成功登录, 请创建房间'
|
||||
this.message = '成功登录,请创建房间'
|
||||
}
|
||||
},
|
||||
joinRoom: async function () {
|
||||
@ -212,7 +441,7 @@
|
||||
})
|
||||
console.info(res)
|
||||
if (res.data.code != 200 || !res.data.data.inRoom) {
|
||||
this.message = '房间状态获取失败, 可能退出了房间'
|
||||
this.message = '房间状态获取失败,可能退出了房间'
|
||||
} else {
|
||||
this.roomInfo.roomUsers = res.data.data.roomInfo.roomUsers
|
||||
}
|
||||
|
||||
@ -5,21 +5,143 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 40px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
text-align: left;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ef4444;
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #10b981;
|
||||
background: #f0fdf4;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>登录</h1>
|
||||
<p class="subtitle">使用手机号和密码登录网易云音乐</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="phone">手机号</label>
|
||||
<input type="tel" id="phone" placeholder="请输入手机号" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="password">密码</label>
|
||||
<input type="password" id="password" placeholder="请输入密码" />
|
||||
</div>
|
||||
|
||||
<button id="loginBtn" class="btn" onclick="handleLogin()">登录</button>
|
||||
|
||||
<div id="result" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script>
|
||||
const phone = '' // 这里填手机号
|
||||
const password = '' // 这里填密码
|
||||
const fileUpdateTime = {}
|
||||
if (!phone || !password) {
|
||||
const msg = '请设置你的手机号码和密码'
|
||||
alert(msg)
|
||||
throw new Error(msg)
|
||||
}
|
||||
|
||||
async function login() {
|
||||
async function login(phone, password) {
|
||||
const res = await axios({
|
||||
url: `/login/cellphone`,
|
||||
method: 'post',
|
||||
@ -30,17 +152,67 @@
|
||||
})
|
||||
return res.data.cookie
|
||||
}
|
||||
async function main() {
|
||||
const cookieToken = await login()
|
||||
const res = await axios({
|
||||
url: `/login/status`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie: cookieToken,
|
||||
},
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
const phoneInput = document.getElementById('phone')
|
||||
const passwordInput = document.getElementById('password')
|
||||
const loginBtn = document.getElementById('loginBtn')
|
||||
const resultDiv = document.getElementById('result')
|
||||
|
||||
const phone = phoneInput.value.trim()
|
||||
const password = passwordInput.value
|
||||
|
||||
if (!phone || !password) {
|
||||
showResult('请输入手机号和密码', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
loginBtn.disabled = true
|
||||
loginBtn.textContent = '登录中...'
|
||||
showResult('正在登录...', 'info')
|
||||
|
||||
try {
|
||||
const cookieToken = await login(phone, password)
|
||||
localStorage.setItem('cookie', cookieToken)
|
||||
|
||||
const res = await axios({
|
||||
url: `/login/status`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie: cookieToken,
|
||||
},
|
||||
})
|
||||
|
||||
showResult(`登录成功!\n${JSON.stringify(res.data, null, 2)}`, 'success')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
const errorMsg = error.response?.data?.message || error.message || '登录失败,请重试'
|
||||
showResult(`登录失败:${errorMsg}`, 'error')
|
||||
} finally {
|
||||
loginBtn.disabled = false
|
||||
loginBtn.textContent = '登录'
|
||||
}
|
||||
}
|
||||
main()
|
||||
|
||||
function showResult(message, type = 'info') {
|
||||
const resultDiv = document.getElementById('result')
|
||||
resultDiv.style.display = 'block'
|
||||
resultDiv.textContent = message
|
||||
resultDiv.className = 'result ' + type
|
||||
}
|
||||
|
||||
// 支持回车登录
|
||||
document.getElementById('password').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
handleLogin()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById('phone').addEventListener('keypress', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
document.getElementById('password').focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
||||
@ -5,56 +5,297 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>歌单封面上传</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 500px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.cover-wrapper {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
margin: 0 auto 24px;
|
||||
}
|
||||
|
||||
.cover {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 4px solid #fff;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.upload-btn {
|
||||
display: inline-block;
|
||||
padding: 12px 28px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.upload-btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.upload-btn input[type="file"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.loading {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.loading.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.spinner {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 4px solid #e0e0e0;
|
||||
border-top-color: #333;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.result {
|
||||
margin-top: 20px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.result.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.result.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html">
|
||||
如果没登录,请先登录
|
||||
</a>
|
||||
<div class="container">
|
||||
<h1>歌单封面上传</h1>
|
||||
<p class="subtitle">上传自定义歌单封面图片</p>
|
||||
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="playlistId">歌单 ID</label>
|
||||
<input type="text" id="playlistId" placeholder="请输入歌单ID" />
|
||||
</div>
|
||||
|
||||
<div class="cover-wrapper">
|
||||
<img id="playlist_cover" class="cover" src="" alt="歌单封面" />
|
||||
<div class="loading" id="loading">
|
||||
<div class="spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label class="upload-btn">
|
||||
选择封面图片
|
||||
<input id="file" type="file" name="filename" accept="image/*" />
|
||||
</label>
|
||||
|
||||
<div id="result" class="result" style="display: none;"></div>
|
||||
</div>
|
||||
<input id="file" type="file" name="filename" />
|
||||
<img id="playlist_cover" style="height: 200px; width: 200px; border-radius: 50%" />
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script>
|
||||
const playlist_id = ''
|
||||
if (!playlist_id) {
|
||||
const msg = '请设置你的歌单id'
|
||||
alert(msg)
|
||||
throw new Error(msg)
|
||||
const loadingOverlay = document.getElementById('loading')
|
||||
const playlistIdInput = document.getElementById('playlistId')
|
||||
const resultDiv = document.getElementById('result')
|
||||
|
||||
function showLoading() {
|
||||
loadingOverlay.classList.add('active')
|
||||
}
|
||||
|
||||
main()
|
||||
async function main() {
|
||||
document.querySelector('input[type="file"]').addEventListener(
|
||||
'change',
|
||||
function (e) {
|
||||
var file = this.files[0]
|
||||
upload(file)
|
||||
},
|
||||
false,
|
||||
)
|
||||
const res = await axios({
|
||||
url: `/playlist/detail?id=${playlist_id}×tamp=${Date.now()}`,
|
||||
function hideLoading() {
|
||||
loadingOverlay.classList.remove('active')
|
||||
}
|
||||
|
||||
function showResult(message, type) {
|
||||
resultDiv.textContent = message
|
||||
resultDiv.className = 'result ' + type
|
||||
resultDiv.style.display = 'block'
|
||||
}
|
||||
|
||||
function hideResult() {
|
||||
resultDiv.style.display = 'none'
|
||||
}
|
||||
|
||||
async function loadPlaylistCover() {
|
||||
const playlistId = playlistIdInput.value.trim()
|
||||
if (!playlistId) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading()
|
||||
hideResult()
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/detail?id=${playlistId}×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||
hideResult()
|
||||
} catch (error) {
|
||||
console.error('加载封面失败:', error)
|
||||
showResult('加载封面失败,请检查歌单ID', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
}
|
||||
|
||||
// 监听歌单ID输入变化
|
||||
playlistIdInput.addEventListener('input', function() {
|
||||
loadPlaylistCover()
|
||||
})
|
||||
|
||||
// 监听文件选择
|
||||
document
|
||||
.querySelector('input[type="file"]')
|
||||
.addEventListener('change', async function (e) {
|
||||
const file = this.files[0]
|
||||
const playlistId = playlistIdInput.value.trim()
|
||||
|
||||
if (!playlistId) {
|
||||
showResult('请先输入歌单ID', 'error')
|
||||
return
|
||||
}
|
||||
|
||||
if (!file) {
|
||||
return
|
||||
}
|
||||
|
||||
showLoading()
|
||||
hideResult()
|
||||
|
||||
try {
|
||||
await upload(file, playlistId)
|
||||
} catch (error) {
|
||||
console.error('上传失败:', error)
|
||||
showResult('上传失败,请重试', 'error')
|
||||
} finally {
|
||||
hideLoading()
|
||||
}
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.playlist.coverImgUrl
|
||||
}
|
||||
|
||||
async function upload(file) {
|
||||
async function upload(file, playlistId) {
|
||||
var formData = new FormData()
|
||||
formData.append('imgFile', file)
|
||||
const imgSize = await getImgSize(file)
|
||||
const res = await axios({
|
||||
method: 'post',
|
||||
url: `/playlist/cover/update?id=${playlist_id}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width
|
||||
}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
url: `/playlist/cover/update?id=${playlistId}&cookie=${localStorage.getItem('cookie')}&imgSize=${imgSize.width}&imgX=0&imgY=0×tamp=${Date.now()}`,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
data: formData,
|
||||
})
|
||||
document.querySelector('#playlist_cover').src = res.data.data.url
|
||||
showResult('封面上传成功!', 'success')
|
||||
}
|
||||
|
||||
function getImgSize(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let reader = new FileReader()
|
||||
@ -68,6 +309,12 @@
|
||||
height: this.height,
|
||||
})
|
||||
}
|
||||
image.onerror = function() {
|
||||
reject(new Error('图片加载失败'))
|
||||
}
|
||||
}
|
||||
reader.onerror = function() {
|
||||
reject(new Error('文件读取失败'))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,261 +2,416 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>歌单导入工具</title>
|
||||
<!-- 引入Bootstrap CSS -->
|
||||
<link href="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<!-- 引入Bootstrap JS -->
|
||||
<script src="https://fastly.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- 引入axios用于发送异步请求 -->
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-bottom: 24px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.tab-btn {
|
||||
padding: 12px 24px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tab-btn:hover {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.tab-btn.active {
|
||||
color: #333;
|
||||
border-bottom-color: #333;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-content.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"], textarea {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input[type="text"]:focus, textarea:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
table th, table td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
table th {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
table td input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 10px 20px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: #666;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #888;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.checkbox-group input[type="checkbox"] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input:disabled {
|
||||
background: #f5f5f5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container mt-5">
|
||||
<h1 class="mb-4">歌单导入工具</h1>
|
||||
<p>请选择一种导入方式并填写相关信息:</p>
|
||||
<div class="container">
|
||||
<h1>歌单导入工具</h1>
|
||||
<p class="subtitle">请选择一种导入方式并填写相关信息</p>
|
||||
|
||||
<!-- 表单开始 -->
|
||||
<form id="importForm" novalidate>
|
||||
<!-- 选项卡导航 -->
|
||||
<ul class="nav nav-tabs mb-3" id="importTabs" role="tablist">
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="tabs" id="importTabs" role="tablist">
|
||||
<li role="presentation">
|
||||
<button class="tab-btn active" id="metadata-tab" data-bs-toggle="tab" data-bs-target="#metadata" type="button" role="tab" aria-controls="metadata" aria-selected="true">元数据导入</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button class="tab-btn" id="text-tab" data-bs-toggle="tab" data-bs-target="#text" type="button" role="tab" aria-controls="text" aria-selected="false">文字导入</button>
|
||||
</li>
|
||||
<li role="presentation">
|
||||
<button class="tab-btn" id="link-tab" data-bs-toggle="tab" data-bs-target="#link" type="button" role="tab" aria-controls="link" aria-selected="false">链接导入</button>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<!-- 选项卡面板 -->
|
||||
<div class="tab-content" id="importTabContent">
|
||||
<!-- 元数据导入 -->
|
||||
<div class="tab-pane fade show active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||
<table class="table table-bordered mb-3">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">歌曲名称</th>
|
||||
<th scope="col">艺术家</th>
|
||||
<th scope="col">专辑</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="metadataTableBody">
|
||||
<!-- 默认添加一行 -->
|
||||
<tr>
|
||||
<td><input type="text" class="form-control" name="name[]" placeholder="歌曲名称"></td>
|
||||
<td><input type="text" class="form-control" name="artist[]" placeholder="艺术家"></td>
|
||||
<td><input type="text" class="form-control" name="album[]" placeholder="专辑"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-secondary mb-3" id="addMetadataRow">增加歌曲</button>
|
||||
<div class="tab-content active" id="importTabContent">
|
||||
<!-- 元数据导入 -->
|
||||
<div class="tab-content active" id="metadata" role="tabpanel" aria-labelledby="metadata-tab">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 33%">歌曲名称</th>
|
||||
<th style="width: 33%">艺术家</th>
|
||||
<th style="width: 33%">专辑</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="metadataTableBody">
|
||||
<tr>
|
||||
<td><input type="text" name="name[]" placeholder="歌曲名称"></td>
|
||||
<td><input type="text" name="artist[]" placeholder="艺术家"></td>
|
||||
<td><input type="text" name="album[]" placeholder="专辑"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<button type="button" class="btn btn-secondary" id="addMetadataRow">增加歌曲</button>
|
||||
</div>
|
||||
<!-- 文字导入 -->
|
||||
<div class="tab-content" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||
<div class="form-group">
|
||||
<label for="textInput">文字内容</label>
|
||||
<textarea id="textInput" name="text" rows="5" placeholder="请输入歌曲信息,每行一首歌曲"></textarea>
|
||||
</div>
|
||||
<!-- 文字导入 -->
|
||||
<div class="tab-pane fade" id="text" role="tabpanel" aria-labelledby="text-tab">
|
||||
<div class="mb-3">
|
||||
<label for="textInput" class="form-label">文字:</label>
|
||||
<textarea class="form-control" id="textInput" name="text" rows="5"></textarea>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="playlistNameInput" class="form-label">歌单名:</label>
|
||||
<input type="text" class="form-control" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
<!-- 链接导入 -->
|
||||
<div class="tab-pane fade" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||
<div class="mb-3">
|
||||
<label for="linkInputs" class="form-label">链接:</label>
|
||||
<div id="linkInputsContainer">
|
||||
<div class="input-group mb-3">
|
||||
<input type="text" class="form-control" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary mb-3" id="addLinkButton">增加链接</button>
|
||||
<div class="mb-3">
|
||||
<label for="playlistNameLinkInput" class="form-label">歌单名:</label>
|
||||
<input type="text" class="form-control" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="playlistNameInput">歌单名称</label>
|
||||
<input type="text" id="playlistNameInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 是否导入我喜欢的音乐 -->
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" value="" id="importStarCheckbox">
|
||||
<label class="form-check-label" for="importStarCheckbox">
|
||||
导入“我喜欢的音乐”
|
||||
</label>
|
||||
<!-- 链接导入 -->
|
||||
<div class="tab-content" id="link" role="tabpanel" aria-labelledby="link-tab">
|
||||
<div class="form-group">
|
||||
<label>链接列表</label>
|
||||
<div id="linkInputsContainer">
|
||||
<div class="input-group">
|
||||
<input type="text" id="linkInput0" name="linkInput0" placeholder="请输入链接">
|
||||
<button type="button" class="btn btn-secondary removeLinkButton" data-index="0">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-secondary" id="addLinkButton" style="margin-top: 8px;">增加链接</button>
|
||||
</div>
|
||||
<div class="form-group" style="margin-top: 20px;">
|
||||
<label for="playlistNameLinkInput">歌单名称</label>
|
||||
<input type="text" id="playlistNameLinkInput" name="playlistName" placeholder="请输入歌单名">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交按钮 -->
|
||||
<button type="submit" class="btn btn-primary mt-3">导入歌曲</button>
|
||||
</form>
|
||||
<!-- 表单结束 -->
|
||||
<div class="checkbox-group">
|
||||
<input type="checkbox" value="" id="importStarCheckbox">
|
||||
<label for="importStarCheckbox">
|
||||
导入"我喜欢的音乐"
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 动态增加链接输入框
|
||||
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||
var container = document.getElementById('linkInputsContainer');
|
||||
var newIndex = container.childElementCount - 1; // 减去非输入框元素的数量
|
||||
var newInput = document.createElement('input');
|
||||
newInput.type = 'text';
|
||||
newInput.className = 'form-control';
|
||||
newInput.id = `linkInput${newIndex}`;
|
||||
newInput.name = `linkInput${newIndex}`;
|
||||
newInput.placeholder = '请输入链接';
|
||||
|
||||
var removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'btn btn-secondary removeLinkButton';
|
||||
removeButton.textContent = '×';
|
||||
removeButton.dataset.index = newIndex.toString();
|
||||
removeButton.addEventListener('click', function() {
|
||||
var group = this.closest('.input-group');
|
||||
container.removeChild(group);
|
||||
});
|
||||
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group mb-3';
|
||||
inputGroup.appendChild(newInput);
|
||||
inputGroup.appendChild(removeButton);
|
||||
|
||||
container.appendChild(inputGroup);
|
||||
});
|
||||
|
||||
// 动态增加元数据行
|
||||
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
||||
var container = document.getElementById('metadataTableBody');
|
||||
var newRow = document.createElement('tr');
|
||||
|
||||
var nameInput = document.createElement('input');
|
||||
nameInput.type = 'text';
|
||||
nameInput.className = 'form-control';
|
||||
nameInput.name = 'name[]';
|
||||
nameInput.placeholder = '歌曲名称';
|
||||
|
||||
var artistInput = document.createElement('input');
|
||||
artistInput.type = 'text';
|
||||
artistInput.className = 'form-control';
|
||||
artistInput.name = 'artist[]';
|
||||
artistInput.placeholder = '艺术家';
|
||||
|
||||
var albumInput = document.createElement('input');
|
||||
albumInput.type = 'text';
|
||||
albumInput.className = 'form-control';
|
||||
albumInput.name = 'album[]';
|
||||
albumInput.placeholder = '专辑';
|
||||
|
||||
newRow.innerHTML = `
|
||||
<td>${nameInput.outerHTML}</td>
|
||||
<td>${artistInput.outerHTML}</td>
|
||||
<td>${albumInput.outerHTML}</td>
|
||||
`;
|
||||
|
||||
container.appendChild(newRow);
|
||||
});
|
||||
|
||||
document.getElementById('importForm').addEventListener('submit', async function(event) {
|
||||
// 阻止默认行为
|
||||
event.preventDefault();
|
||||
|
||||
// 获取表单值
|
||||
let text = document.getElementById('textInput').value;
|
||||
let links = [];
|
||||
let local = [];
|
||||
let playlistName = '';
|
||||
|
||||
// 获取所有链接输入框的值
|
||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group .form-control');
|
||||
linkInputs.forEach(function(input) {
|
||||
if (input.value.trim() !== '') {
|
||||
links.push(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取元数据
|
||||
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
||||
metadataRows.forEach(function(row) {
|
||||
let name = row.querySelector('input[name="name[]"]').value;
|
||||
let artist = row.querySelector('input[name="artist[]"]').value;
|
||||
let album = row.querySelector('input[name="album[]"]').value;
|
||||
if (name && artist && album) {
|
||||
local.push({ name, artist, album });
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有且只有一个输入字段被填写
|
||||
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
||||
if (filledCount !== 1) {
|
||||
alert("请确保仅填写了一个输入字段!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取歌单名
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
playlistName = '我喜欢的音乐';
|
||||
} else {
|
||||
playlistName = document.getElementById('playlistNameInput').value ||
|
||||
document.getElementById('playlistNameLinkInput').value ||
|
||||
'导入音乐 ' + new Date().toLocaleString();
|
||||
}
|
||||
|
||||
// 创建请求参数
|
||||
let data = {};
|
||||
if (text) {
|
||||
data.text = text;
|
||||
data.playlistName = playlistName;
|
||||
} else if (links.length > 0) {
|
||||
data.link = JSON.stringify(links);
|
||||
data.playlistName = playlistName;
|
||||
} else if (local.length > 0) {
|
||||
data.local = JSON.stringify(local);
|
||||
}
|
||||
|
||||
// 添加额外参数
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
data.importStarPlaylist = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
|
||||
let taskId = res.data?.data?.taskId
|
||||
if (taskId) {
|
||||
alert(`任务创建成功! 正在导入, 请稍等; 任务id:${taskId}`)
|
||||
// const res2 = await axios({
|
||||
// url: `/playlist/import/task/status?timestamp=${Date.now()}`,
|
||||
// method: 'post',
|
||||
// data: {
|
||||
// id: taskId
|
||||
// },
|
||||
// });
|
||||
// alert(JSON.stringify(res2.data, null, 2));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('导入失败,请检查您的输入或稍后再试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听复选框状态变化
|
||||
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
||||
let isChecked = this.checked;
|
||||
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
||||
playlistNameInputs.forEach(function(input) {
|
||||
input.disabled = isChecked;
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化时设置歌单名输入框的状态
|
||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||
</script>
|
||||
<button type="submit" class="btn" id="submitBtn">导入歌曲</button>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
|
||||
<script>
|
||||
// 选项卡切换
|
||||
const tabBtns = document.querySelectorAll('.tab-btn');
|
||||
const tabContents = document.querySelectorAll('.tab-content[id]');
|
||||
|
||||
tabBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
tabBtns.forEach(b => b.classList.remove('active'));
|
||||
tabContents.forEach(c => c.classList.remove('active'));
|
||||
|
||||
btn.classList.add('active');
|
||||
const targetId = btn.getAttribute('data-bs-target');
|
||||
document.getElementById(targetId).classList.add('active');
|
||||
});
|
||||
});
|
||||
|
||||
// 动态增加链接输入框
|
||||
document.getElementById('addLinkButton').addEventListener('click', function() {
|
||||
var container = document.getElementById('linkInputsContainer');
|
||||
var newIndex = container.children.length;
|
||||
var newInput = document.createElement('input');
|
||||
newInput.type = 'text';
|
||||
newInput.className = '';
|
||||
newInput.id = `linkInput${newIndex}`;
|
||||
newInput.name = `linkInput${newIndex}`;
|
||||
newInput.placeholder = '请输入链接';
|
||||
newInput.style.cssText = 'flex: 1; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;';
|
||||
|
||||
var removeButton = document.createElement('button');
|
||||
removeButton.type = 'button';
|
||||
removeButton.className = 'btn btn-secondary';
|
||||
removeButton.textContent = '×';
|
||||
removeButton.dataset.index = newIndex.toString();
|
||||
removeButton.addEventListener('click', function() {
|
||||
var group = this.closest('.input-group');
|
||||
container.removeChild(group);
|
||||
});
|
||||
|
||||
var inputGroup = document.createElement('div');
|
||||
inputGroup.className = 'input-group';
|
||||
inputGroup.appendChild(newInput);
|
||||
inputGroup.appendChild(removeButton);
|
||||
|
||||
container.appendChild(inputGroup);
|
||||
});
|
||||
|
||||
// 动态增加元数据行
|
||||
document.getElementById('addMetadataRow').addEventListener('click', function() {
|
||||
var container = document.getElementById('metadataTableBody');
|
||||
var newRow = document.createElement('tr');
|
||||
|
||||
newRow.innerHTML = `
|
||||
<td><input type="text" name="name[]" placeholder="歌曲名称" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
<td><input type="text" name="artist[]" placeholder="艺术家" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
<td><input type="text" name="album[]" placeholder="专辑" style="width: 100%; padding: 10px 14px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; outline: none;"></td>
|
||||
`;
|
||||
|
||||
container.appendChild(newRow);
|
||||
});
|
||||
|
||||
document.getElementById('submitBtn').addEventListener('click', async function() {
|
||||
// 获取表单值
|
||||
let text = document.getElementById('textInput').value;
|
||||
let links = [];
|
||||
let local = [];
|
||||
let playlistName = '';
|
||||
|
||||
// 获取所有链接输入框的值
|
||||
let linkInputs = document.querySelectorAll('#linkInputsContainer .input-group input[type="text"]');
|
||||
linkInputs.forEach(function(input) {
|
||||
if (input.value.trim() !== '') {
|
||||
links.push(input.value);
|
||||
}
|
||||
});
|
||||
|
||||
// 获取元数据
|
||||
let metadataRows = document.querySelectorAll('#metadataTableBody tr');
|
||||
metadataRows.forEach(function(row) {
|
||||
let name = row.querySelector('input[name="name[]"]').value;
|
||||
let artist = row.querySelector('input[name="artist[]"]').value;
|
||||
let album = row.querySelector('input[name="album[]"]').value;
|
||||
if (name && artist && album) {
|
||||
local.push({ name, artist, album });
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否有且只有一个输入字段被填写
|
||||
let filledCount = (text ? 1 : 0) + (links.length > 0 ? 1 : 0) + (local.length > 0 ? 1 : 0);
|
||||
if (filledCount !== 1) {
|
||||
alert("请确保仅填写了一个输入字段!");
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取歌单名
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
playlistName = '我喜欢的音乐';
|
||||
} else {
|
||||
playlistName = document.getElementById('playlistNameInput').value ||
|
||||
document.getElementById('playlistNameLinkInput').value ||
|
||||
'导入音乐 ' + new Date().toLocaleString();
|
||||
}
|
||||
|
||||
// 创建请求参数
|
||||
let data = {};
|
||||
if (text) {
|
||||
data.text = text;
|
||||
data.playlistName = playlistName;
|
||||
} else if (links.length > 0) {
|
||||
data.link = JSON.stringify(links);
|
||||
data.playlistName = playlistName;
|
||||
} else if (local.length > 0) {
|
||||
data.local = JSON.stringify(local);
|
||||
}
|
||||
|
||||
// 添加额外参数
|
||||
if (document.getElementById('importStarCheckbox').checked) {
|
||||
data.importStarPlaylist = true;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/playlist/import/name/task/create?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: data,
|
||||
});
|
||||
|
||||
let taskId = res.data?.data?.taskId
|
||||
if (taskId) {
|
||||
alert(`任务创建成功!正在导入,请稍等;任务id:${taskId}`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error:', error);
|
||||
alert('导入失败,请检查您的输入或稍后再试。');
|
||||
}
|
||||
});
|
||||
|
||||
// 监听复选框状态变化
|
||||
document.getElementById('importStarCheckbox').addEventListener('change', function() {
|
||||
let isChecked = this.checked;
|
||||
let playlistNameInputs = document.querySelectorAll('[name="playlistName"]');
|
||||
playlistNameInputs.forEach(function(input) {
|
||||
input.disabled = isChecked;
|
||||
});
|
||||
});
|
||||
|
||||
// 初始化时设置歌单名输入框的状态
|
||||
document.getElementById('importStarCheckbox').dispatchEvent(new Event('change'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@ -5,45 +5,164 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>二维码登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#qrImg {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.waiting {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img id="qrImg" />
|
||||
<div id="info" class="info"></div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
||||
"></script>
|
||||
<script>
|
||||
<div class="container">
|
||||
<h1>二维码登录</h1>
|
||||
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||
|
||||
<div class="qr-wrapper">
|
||||
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||
</div>
|
||||
|
||||
<div id="status" class="status waiting">等待扫描...</div>
|
||||
|
||||
<div id="info" class="info"></div>
|
||||
|
||||
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script>
|
||||
async function login() {
|
||||
let timer
|
||||
let timestamp = Date.now()
|
||||
const statusDiv = document.getElementById('status')
|
||||
const cookie = localStorage.getItem('cookie')
|
||||
getLoginStatus(cookie)
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
alert('二维码已过期,请重新获取')
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (statusRes.code === 803) {
|
||||
// 这一步会返回cookie
|
||||
clearInterval(timer)
|
||||
alert('授权登录成功')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
updateStatus('加载二维码...', 'waiting')
|
||||
getLoginStatus(cookie)
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
updateStatus('请扫描二维码', 'waiting')
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||
clearInterval(timer)
|
||||
} else if (statusRes.code === 801) {
|
||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||
} else if (statusRes.code === 802) {
|
||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||
} else if (statusRes.code === 803) {
|
||||
clearInterval(timer)
|
||||
updateStatus('授权登录成功!', 'success')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||
}
|
||||
}
|
||||
login()
|
||||
|
||||
async function checkStatus(key) {
|
||||
const res = await axios({
|
||||
@ -51,22 +170,30 @@
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function getLoginStatus(cookie = '') {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||
} catch (error) {
|
||||
console.error('获取登录状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status')
|
||||
statusDiv.textContent = message
|
||||
statusDiv.className = 'status ' + type
|
||||
}
|
||||
|
||||
login()
|
||||
</script>
|
||||
<style>
|
||||
.info {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -5,44 +5,164 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>二维码登录</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 48px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
max-width: 450px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.qr-wrapper {
|
||||
display: inline-block;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
#qrImg {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.info {
|
||||
text-align: left;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-top: 16px;
|
||||
padding: 12px;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status.waiting {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
font-size: 13px;
|
||||
color: #999;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<img id="qrImg" />
|
||||
<div id="info" class="info"></div>
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js
|
||||
"></script>
|
||||
<div class="container">
|
||||
<h1>二维码登录</h1>
|
||||
<p class="subtitle">使用网易云音乐App扫描二维码登录</p>
|
||||
|
||||
<div class="qr-wrapper">
|
||||
<img id="qrImg" src="" alt="二维码加载中..." />
|
||||
</div>
|
||||
|
||||
<div id="status" class="status waiting">等待扫描...</div>
|
||||
|
||||
<div id="info" class="info"></div>
|
||||
|
||||
<p class="hint">请打开网易云音乐App,扫描上方二维码完成登录</p>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script>
|
||||
async function login() {
|
||||
let timer
|
||||
let timestamp = Date.now()
|
||||
const statusDiv = document.getElementById('status')
|
||||
const cookie = localStorage.getItem('cookie')
|
||||
getLoginStatus(cookie)
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
alert('二维码已过期,请重新获取')
|
||||
clearInterval(timer)
|
||||
}
|
||||
if (statusRes.code === 803) {
|
||||
// 这一步会返回cookie
|
||||
clearInterval(timer)
|
||||
alert('授权登录成功')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
updateStatus('加载二维码...', 'waiting')
|
||||
getLoginStatus(cookie)
|
||||
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/qr/key?timestamp=${Date.now()}`,
|
||||
})
|
||||
const key = res.data.data.unikey
|
||||
|
||||
const res2 = await axios({
|
||||
url: `/login/qr/create?key=${key}&platform=web&qrimg=true×tamp=${Date.now()}&ua=pc`,
|
||||
})
|
||||
document.querySelector('#qrImg').src = res2.data.data.qrimg
|
||||
updateStatus('请扫描二维码', 'waiting')
|
||||
|
||||
timer = setInterval(async () => {
|
||||
const statusRes = await checkStatus(key)
|
||||
if (statusRes.code === 800) {
|
||||
updateStatus('二维码已过期,请刷新页面', 'error')
|
||||
clearInterval(timer)
|
||||
} else if (statusRes.code === 801) {
|
||||
updateStatus('二维码已扫描,请在手机上确认', 'waiting')
|
||||
} else if (statusRes.code === 802) {
|
||||
updateStatus('登录成功,正在保存信息...', 'waiting')
|
||||
} else if (statusRes.code === 803) {
|
||||
clearInterval(timer)
|
||||
updateStatus('授权登录成功!', 'success')
|
||||
await getLoginStatus(statusRes.cookie)
|
||||
localStorage.setItem('cookie', statusRes.cookie)
|
||||
}
|
||||
}, 3000)
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
updateStatus('二维码加载失败,请刷新页面重试', 'error')
|
||||
}
|
||||
}
|
||||
login()
|
||||
|
||||
async function checkStatus(key) {
|
||||
const res = await axios({
|
||||
@ -50,22 +170,30 @@
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
|
||||
async function getLoginStatus(cookie = '') {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').innerText = JSON.stringify(res.data, null, 2)
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/login/status?timestamp=${Date.now()}&ua=pc`,
|
||||
method: 'post',
|
||||
data: {
|
||||
cookie,
|
||||
},
|
||||
})
|
||||
document.querySelector('#info').textContent = JSON.stringify(res.data, null, 2)
|
||||
} catch (error) {
|
||||
console.error('获取登录状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatus(message, type) {
|
||||
const statusDiv = document.getElementById('status')
|
||||
statusDiv.textContent = message
|
||||
statusDiv.className = 'status ' + type
|
||||
}
|
||||
|
||||
login()
|
||||
</script>
|
||||
<style>
|
||||
.info {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
@ -5,79 +5,136 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>音乐解灰测试</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 20px auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.container {
|
||||
background-color: #f5f5f5;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 15px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.source-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-bottom: 15px;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.source-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
button {
|
||||
background-color: #4CAF50;
|
||||
color: white;
|
||||
padding: 10px 20px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #45a049;
|
||||
}
|
||||
#result {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
button {
|
||||
background: #333;
|
||||
color: white;
|
||||
padding: 14px 28px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: #999;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#result {
|
||||
margin-top: 24px;
|
||||
padding: 16px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #eee;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>音乐解灰测试</h1>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="songId">音乐 ID:</label>
|
||||
<input type="number" id="songId" placeholder="请输入音乐ID" required>
|
||||
<label for="songId">音乐 ID</label>
|
||||
<input type="number" id="songId" placeholder="请输入音乐ID" />
|
||||
<div class="hint">例如: 1372188635</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="sources">音源列表:</label>
|
||||
<input type="text" id="sources" placeholder="请输入音源(非必填)">
|
||||
<label for="sources">音源列表(可选)</label>
|
||||
<input type="text" id="sources" placeholder="请输入音源" />
|
||||
<div class="hint">例如: kuwo, kugou, migu</div>
|
||||
</div>
|
||||
<button onclick="testSong()">开始测试</button>
|
||||
|
||||
<button id="testBtn" onclick="testSong()">开始测试</button>
|
||||
|
||||
<div id="result"></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function testSong() {
|
||||
const songId = document.getElementById('songId').value;
|
||||
const sources = document.getElementById('sources').value;
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
if (!songId) {
|
||||
alert('请输入音乐ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const sources = document.getElementById('sources').value;
|
||||
|
||||
|
||||
|
||||
const resultDiv = document.getElementById('result');
|
||||
testBtn.disabled = true;
|
||||
testBtn.textContent = '测试中...';
|
||||
resultDiv.textContent = '正在请求...';
|
||||
|
||||
try {
|
||||
@ -86,6 +143,9 @@
|
||||
resultDiv.textContent = JSON.stringify(data, null, 2);
|
||||
} catch (error) {
|
||||
resultDiv.textContent = `请求失败: ${error.message}`;
|
||||
} finally {
|
||||
testBtn.disabled = false;
|
||||
testBtn.textContent = '开始测试';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@ -4,36 +4,230 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>播客上传声音</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 32px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.login-link {
|
||||
display: block;
|
||||
margin-bottom: 24px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.login-link:hover {
|
||||
color: #333;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.voice-list {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.voice-item {
|
||||
padding: 12px 16px;
|
||||
border: 1px solid #eee;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.voice-item:hover {
|
||||
border-color: #333;
|
||||
background: #f9f9f9;
|
||||
}
|
||||
|
||||
.voice-item.active {
|
||||
border-color: #333;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.voice-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.voice-cover {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.voice-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.voice-tracks {
|
||||
margin-top: 8px;
|
||||
padding-left: 62px;
|
||||
}
|
||||
|
||||
.voice-track {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.upload-section {
|
||||
flex: 1;
|
||||
min-width: 300px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
input[type="text"], input[type="file"] {
|
||||
width: 100%;
|
||||
padding: 10px 14px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: #333;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: #333;
|
||||
color: white;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #999;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<a href="/qrlogin-nocookie.html"> 如果没登录,请先登录 </a>
|
||||
</div>
|
||||
<div id="app">
|
||||
<ul>
|
||||
<li
|
||||
v-for="(item,index) in voicelist"
|
||||
@click="currentVoiceIndex=index"
|
||||
:class="{active:currentVoiceIndex===index}"
|
||||
>
|
||||
<img :src="item.coverUrl" style="width: 50px; width: 50px" />
|
||||
<ul>
|
||||
<li v-for="(item2,index) in item.voiceListData">
|
||||
{{item2.voiceName}}
|
||||
</li>
|
||||
</ul>
|
||||
{{item.voiceListName}}
|
||||
</li>
|
||||
</ul>
|
||||
<input v-model="songName" placeholder="请输入声音名称" />
|
||||
<input v-model="description" placeholder="请输入介绍" />
|
||||
<input type="file" name="songFile" />
|
||||
<button @click="submit">上传</button>
|
||||
<div class="container">
|
||||
<h1>播客上传声音</h1>
|
||||
<a href="/qrlogin-nocookie.html" class="login-link">还没登录?点击登录</a>
|
||||
|
||||
<div class="content">
|
||||
<div class="voice-list">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">选择播客列表</h3>
|
||||
<div v-if="loading" class="loading">加载中...</div>
|
||||
<div v-else-if="voicelist.length > 0">
|
||||
<div
|
||||
v-for="(item, index) in voicelist"
|
||||
:key="index"
|
||||
@click="currentVoiceIndex = index"
|
||||
:class="{ active: currentVoiceIndex === index }"
|
||||
class="voice-item"
|
||||
>
|
||||
<div class="voice-header">
|
||||
<img :src="item.coverUrl" class="voice-cover" alt="cover" />
|
||||
<span class="voice-name">{{ item.voiceListName }}</span>
|
||||
</div>
|
||||
<div class="voice-tracks" v-if="item.voiceListData">
|
||||
<div
|
||||
v-for="(item2, index2) in item.voiceListData"
|
||||
:key="index2"
|
||||
class="voice-track"
|
||||
>
|
||||
{{ item2.voiceName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">暂无播客列表</div>
|
||||
</div>
|
||||
|
||||
<div class="upload-section">
|
||||
<h3 style="font-size: 16px; font-weight: 600; color: #333; margin-bottom: 16px;">上传声音</h3>
|
||||
<div class="form-group">
|
||||
<label for="songName">声音名称</label>
|
||||
<input id="songName" v-model="songName" placeholder="请输入声音名称" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description">介绍</label>
|
||||
<input id="description" v-model="description" placeholder="请输入介绍" />
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>选择文件</label>
|
||||
<input type="file" name="songFile" accept="audio/*" />
|
||||
</div>
|
||||
|
||||
<button class="btn" @click="submit">上传</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="https://fastly.jsdelivr.net/npm/axios@0.26.1/dist/axios.min.js"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue"></script>
|
||||
<script src="https://fastly.jsdelivr.net/npm/vue@3"></script>
|
||||
<script>
|
||||
Vue.createApp({
|
||||
data() {
|
||||
@ -43,6 +237,7 @@
|
||||
voicelist: [],
|
||||
cookieToken: '',
|
||||
currentVoiceIndex: 0,
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
created() {
|
||||
@ -50,12 +245,6 @@
|
||||
},
|
||||
computed: {
|
||||
currentVoice() {
|
||||
// {
|
||||
// voiceListId: '',
|
||||
// coverImgId: '',
|
||||
// categoryId: '',
|
||||
// secondCategoryId: '',
|
||||
// }
|
||||
return this.voicelist[this.currentVoiceIndex]
|
||||
},
|
||||
},
|
||||
@ -63,27 +252,49 @@
|
||||
submit() {
|
||||
console.info('submit')
|
||||
const file = document.querySelector('input[type=file]').files[0]
|
||||
if (!file) {
|
||||
alert('请选择文件')
|
||||
return
|
||||
}
|
||||
this.upload(file)
|
||||
},
|
||||
|
||||
async getData() {
|
||||
const res = await axios({
|
||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
|
||||
console.info(res.data.data)
|
||||
this.voicelist = res.data.data.list
|
||||
this.voicelist.map(async (i) => {
|
||||
const res2 = await axios({
|
||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||
this.loading = true
|
||||
try {
|
||||
const res = await axios({
|
||||
url: `/voicelist/search?cookie=${localStorage.getItem('cookie')}`,
|
||||
})
|
||||
i.voiceListData = res2.data.data.list
|
||||
console.info(res2)
|
||||
})
|
||||
|
||||
console.info(res.data.data)
|
||||
this.voicelist = res.data.data.list || []
|
||||
this.voicelist.forEach(async (i) => {
|
||||
try {
|
||||
const res2 = await axios({
|
||||
url: `/voicelist/list?voiceListId=${i.voiceListId}&limit=5`,
|
||||
})
|
||||
i.voiceListData = res2.data.data.list || []
|
||||
console.info(res2)
|
||||
} catch (err) {
|
||||
console.error('获取播客详情失败:', err)
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
console.error('获取播客列表失败:', err)
|
||||
} finally {
|
||||
this.loading = false
|
||||
}
|
||||
},
|
||||
|
||||
upload(file) {
|
||||
if (!this.currentVoice) {
|
||||
alert('请先选择播客列表')
|
||||
return
|
||||
}
|
||||
|
||||
var formData = new FormData()
|
||||
formData.append('songFile', file)
|
||||
|
||||
axios({
|
||||
method: 'post',
|
||||
url: `/voice/upload?time=${Date.now()}&cookie=${localStorage.getItem(
|
||||
@ -102,26 +313,14 @@
|
||||
})
|
||||
.then((res) => {
|
||||
alert(`${file.name} 上传成功`)
|
||||
if (currentIndx >= fileLength) {
|
||||
console.info('上传完毕')
|
||||
}
|
||||
})
|
||||
.catch(async (err) => {
|
||||
console.info(err)
|
||||
.catch((err) => {
|
||||
console.error('上传失败:', err)
|
||||
alert('上传失败,请重试')
|
||||
})
|
||||
},
|
||||
},
|
||||
}).mount('#app')
|
||||
}).mount('body')
|
||||
</script>
|
||||
|
||||
<style>
|
||||
ul li {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
ul li.active {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user