743 lines
23 KiB
HTML
743 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<title>list.txt Manager</title>
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg-body: #09090b;
|
|
--bg-panel: #121214;
|
|
--bg-hover: #27272a;
|
|
--bg-border: #27272a;
|
|
--bg-input: #09090b;
|
|
--text-primary: #f4f4f5;
|
|
--text-muted: #a1a1aa;
|
|
--danger: #ef4444;
|
|
--success: #10b981;
|
|
--radius: 8px;
|
|
--font-ui: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
--ring-color: rgba(255, 255, 255, 0.2);
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: var(--font-ui);
|
|
background: radial-gradient(circle at 50% 0%, #18181b 0%, var(--bg-body) 100%);
|
|
color: var(--text-primary);
|
|
margin: 0;
|
|
padding: 30px 20px;
|
|
min-height: 100vh;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
}
|
|
|
|
.layout-wrapper {
|
|
width: 100%;
|
|
max-width: 980px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 18px;
|
|
}
|
|
|
|
.header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
padding-bottom: 14px;
|
|
border-bottom: 1px solid var(--bg-border);
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.header h2 {
|
|
margin: 0;
|
|
font-weight: 600;
|
|
letter-spacing: -0.025em;
|
|
background: linear-gradient(to right, #fff, #a1a1aa);
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
}
|
|
|
|
.header-actions { display: flex; gap: 10px; flex-wrap: wrap; }
|
|
|
|
.panel {
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--bg-border);
|
|
border-radius: var(--radius);
|
|
padding: 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
|
|
}
|
|
|
|
.panel-subtitle {
|
|
font-size: 0.85rem;
|
|
color: var(--text-muted);
|
|
padding-bottom: 12px;
|
|
border-bottom: 1px solid var(--bg-border);
|
|
}
|
|
|
|
.toolbar {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
.btn {
|
|
background: transparent;
|
|
border: 1px solid var(--bg-border);
|
|
color: var(--text-primary);
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
font-size: 0.9rem;
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
font-weight: 500;
|
|
user-select: none;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
|
|
.btn-primary {
|
|
background: var(--text-primary);
|
|
color: var(--bg-body);
|
|
border-color: var(--text-primary);
|
|
font-weight: 600;
|
|
}
|
|
|
|
.btn-primary:hover:not(:disabled) {
|
|
background: #d4d4d8;
|
|
border-color: #d4d4d8;
|
|
}
|
|
|
|
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.list-container {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
max-height: 65vh;
|
|
overflow-y: auto;
|
|
padding-right: 4px;
|
|
}
|
|
|
|
.list-item {
|
|
display: grid;
|
|
grid-template-columns: 110px 1fr auto;
|
|
gap: 14px;
|
|
align-items: center;
|
|
background: var(--bg-input);
|
|
padding: 12px 14px;
|
|
border: 1px solid var(--bg-border);
|
|
border-radius: 6px;
|
|
transition: border-color 0.2s;
|
|
}
|
|
|
|
.list-item:hover { border-color: #52525b; }
|
|
|
|
.code {
|
|
font-family: var(--font-mono);
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.tag {
|
|
display: inline-flex;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
background: var(--bg-hover);
|
|
font-size: 0.8rem;
|
|
margin-right: 4px;
|
|
margin-bottom: 4px;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
.btn-icon {
|
|
padding: 8px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
border-radius: 6px;
|
|
transition: all 0.2s;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.btn-icon:hover { color: var(--text-primary); background: var(--bg-hover); }
|
|
.btn-icon.danger:hover { color: var(--danger); background: rgba(239, 68, 68, 0.1); }
|
|
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
background: rgba(0,0,0,0.8);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 100;
|
|
opacity: 0;
|
|
pointer-events: none;
|
|
transition: opacity 0.2s ease;
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
|
|
.modal-overlay.active {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.modal {
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--bg-border);
|
|
border-radius: 12px;
|
|
width: 92%;
|
|
max-width: 540px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
box-shadow: 0 25px 50px -12px rgba(0,0,0,0.7);
|
|
}
|
|
|
|
.modal-header {
|
|
padding: 18px 20px;
|
|
border-bottom: 1px solid var(--bg-border);
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
font-weight: 600;
|
|
font-size: 1.05rem;
|
|
}
|
|
|
|
.modal-body {
|
|
padding: 18px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 14px;
|
|
}
|
|
|
|
.modal-footer {
|
|
padding: 14px 20px;
|
|
border-top: 1px solid var(--bg-border);
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
gap: 10px;
|
|
background: rgba(0,0,0,0.2);
|
|
border-bottom-left-radius: 12px;
|
|
border-bottom-right-radius: 12px;
|
|
}
|
|
|
|
.form-group { display: flex; flex-direction: column; gap: 8px; }
|
|
label { font-size: 0.875rem; font-weight: 500; }
|
|
|
|
input[type="text"] {
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--bg-border);
|
|
color: var(--text-primary);
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.95rem;
|
|
outline: none;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
input[type="text"]:focus {
|
|
border-color: #52525b;
|
|
box-shadow: 0 0 0 3px var(--ring-color);
|
|
}
|
|
|
|
.edit-files-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--bg-border);
|
|
border-radius: 6px;
|
|
padding: 12px;
|
|
min-height: 120px;
|
|
max-height: 260px;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.edit-file-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
background: var(--bg-panel);
|
|
padding: 10px 12px;
|
|
border-radius: 6px;
|
|
border: 1px solid var(--bg-border);
|
|
transition: all 0.2s;
|
|
gap: 10px;
|
|
}
|
|
|
|
.edit-file-name {
|
|
font-family: var(--font-mono);
|
|
font-size: 0.85rem;
|
|
color: var(--text-primary);
|
|
word-break: break-all;
|
|
}
|
|
|
|
.edit-file-actions { display: flex; gap: 6px; }
|
|
|
|
.custom-select-container {
|
|
position: relative;
|
|
width: 100%;
|
|
font-family: var(--font-ui);
|
|
font-size: 0.95rem;
|
|
user-select: none;
|
|
}
|
|
|
|
.custom-select-trigger {
|
|
background: var(--bg-input);
|
|
border: 1px solid var(--bg-border);
|
|
color: var(--text-primary);
|
|
padding: 10px 14px;
|
|
border-radius: 6px;
|
|
cursor: pointer;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.custom-select-trigger.active {
|
|
border-color: #52525b;
|
|
box-shadow: 0 0 0 3px var(--ring-color);
|
|
}
|
|
|
|
.custom-select-options {
|
|
position: absolute;
|
|
top: calc(100% + 4px);
|
|
left: 0;
|
|
right: 0;
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--bg-border);
|
|
border-radius: 6px;
|
|
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.5);
|
|
z-index: 150;
|
|
max-height: 240px;
|
|
overflow-y: auto;
|
|
display: none;
|
|
flex-direction: column;
|
|
padding: 4px;
|
|
}
|
|
|
|
.custom-select-options.active { display: flex; }
|
|
|
|
.custom-select-option {
|
|
padding: 8px 12px;
|
|
cursor: pointer;
|
|
border-radius: 4px;
|
|
color: var(--text-primary);
|
|
transition: background 0.1s;
|
|
font-family: var(--font-mono);
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
.custom-select-option:hover { background: var(--bg-hover); }
|
|
.custom-select-option.selected { background: var(--bg-hover); font-weight: 500; }
|
|
|
|
.toast-container {
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
z-index: 200;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.toast {
|
|
background: var(--bg-panel);
|
|
border: 1px solid var(--bg-border);
|
|
padding: 14px 18px;
|
|
border-radius: 8px;
|
|
box-shadow: 0 10px 30px -10px rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
min-width: 280px;
|
|
animation: slideIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
|
}
|
|
|
|
@keyframes slideIn {
|
|
from { transform: translateX(120%); opacity: 0; }
|
|
to { transform: translateX(0); opacity: 1; }
|
|
}
|
|
|
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
|
::-webkit-scrollbar-track { background: var(--bg-input); border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb { background: #52525b; border-radius: 4px; }
|
|
::-webkit-scrollbar-thumb:hover { background: #71717a; }
|
|
* { scrollbar-width: thin; scrollbar-color: #52525b var(--bg-input); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="layout-wrapper">
|
|
<div class="header">
|
|
<h2>list.txt 管理</h2>
|
|
<div class="header-actions">
|
|
<a class="btn" href="/">控制台</a>
|
|
<a class="btn" href="/generator.html">Generator</a>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="panel">
|
|
<div class="panel-subtitle">对 uploads/list.txt 进行增删改;每条记录格式:站号==音频文件1,音频文件2,... (支持多个站号用 / 分隔)</div>
|
|
<div class="toolbar">
|
|
<button class="btn" onclick="openAddModal()">添加</button>
|
|
<div style="display:flex; gap:10px; flex-wrap:wrap;">
|
|
<button class="btn" onclick="reloadAll()">刷新</button>
|
|
<button class="btn btn-primary" id="btn-save" onclick="saveList()">保存到服务器</button>
|
|
</div>
|
|
</div>
|
|
<div class="list-container" id="list-container">
|
|
<div style="text-align:center; padding: 40px; color:var(--text-muted)">Loading...</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-overlay" id="edit-modal">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<span id="modal-title">Edit</span>
|
|
<button class="btn-icon" onclick="closeModal()" aria-label="Close">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
|
</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>站号</label>
|
|
<input type="text" id="edit-code" placeholder="例如:01-01 / 01-01/01-02" />
|
|
</div>
|
|
<div class="form-group">
|
|
<label>播放序列(从上到下)</label>
|
|
<div class="edit-files-list" id="edit-files-list"></div>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>添加音频文件</label>
|
|
<div style="display:flex; gap:10px; align-items:center;">
|
|
<div class="custom-select-container" id="custom-file-select">
|
|
<div class="custom-select-trigger" onclick="toggleCustomSelect()">
|
|
<span id="custom-select-label">-- Select an audio file --</span>
|
|
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#a1a1aa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>
|
|
</div>
|
|
<div class="custom-select-options" id="custom-select-options"></div>
|
|
</div>
|
|
<button class="btn" onclick="addFileToSequence()">添加</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button class="btn" onclick="closeModal()">取消</button>
|
|
<button class="btn btn-primary" onclick="confirmEdit()">确定</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toast-container" id="toast-container"></div>
|
|
|
|
<script>
|
|
const ICONS = {
|
|
up: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="18 15 12 9 6 15"></polyline></svg>`,
|
|
down: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"></polyline></svg>`,
|
|
trash: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`,
|
|
edit: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>`
|
|
};
|
|
|
|
let listData = [];
|
|
let availableFiles = [];
|
|
let editingIndex = -1;
|
|
let currentSequence = [];
|
|
let selectedFileValue = '';
|
|
|
|
function showToast(title, message) {
|
|
const container = document.getElementById('toast-container');
|
|
const toast = document.createElement('div');
|
|
toast.className = 'toast';
|
|
toast.innerHTML = `
|
|
<div>
|
|
<div style="font-weight:600; font-size:0.9rem; margin-bottom:2px">${title}</div>
|
|
<div style="font-size:0.8rem; color:var(--text-muted)">${message}</div>
|
|
</div>
|
|
`;
|
|
container.appendChild(toast);
|
|
setTimeout(() => {
|
|
toast.style.opacity = '0';
|
|
setTimeout(() => toast.remove(), 250);
|
|
}, 2500);
|
|
}
|
|
|
|
function toggleCustomSelect() {
|
|
const trigger = document.querySelector('#custom-file-select .custom-select-trigger');
|
|
const options = document.getElementById('custom-select-options');
|
|
if (!trigger || !options) return;
|
|
const next = !options.classList.contains('active');
|
|
trigger.classList.toggle('active', next);
|
|
options.classList.toggle('active', next);
|
|
}
|
|
|
|
function closeCustomSelect() {
|
|
const trigger = document.querySelector('#custom-file-select .custom-select-trigger');
|
|
const options = document.getElementById('custom-select-options');
|
|
if (!trigger || !options) return;
|
|
trigger.classList.remove('active');
|
|
options.classList.remove('active');
|
|
}
|
|
|
|
function selectCustomOption(value, label) {
|
|
selectedFileValue = value || '';
|
|
const labelEl = document.getElementById('custom-select-label');
|
|
if (labelEl) labelEl.textContent = label || '-- Select an audio file --';
|
|
|
|
const optionsContainer = document.getElementById('custom-select-options');
|
|
if (optionsContainer) {
|
|
Array.from(optionsContainer.querySelectorAll('.custom-select-option')).forEach(el => {
|
|
el.classList.toggle('selected', (el.getAttribute('data-value') || '') === selectedFileValue);
|
|
});
|
|
}
|
|
closeCustomSelect();
|
|
}
|
|
|
|
document.addEventListener('click', (e) => {
|
|
const container = document.getElementById('custom-file-select');
|
|
if (!container) return;
|
|
if (!container.contains(e.target)) closeCustomSelect();
|
|
});
|
|
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') closeCustomSelect();
|
|
});
|
|
|
|
async function fetchAvailableFiles() {
|
|
const res = await fetch('/api/files');
|
|
const data = await res.json();
|
|
availableFiles = (data.files || [])
|
|
.filter(f => f.type === 'station' || f.type === 'general')
|
|
.map(f => f.name)
|
|
.sort();
|
|
populateFileSelect();
|
|
}
|
|
|
|
function populateFileSelect() {
|
|
const optionsContainer = document.getElementById('custom-select-options');
|
|
if (!optionsContainer) return;
|
|
let html = `<div class="custom-select-option ${selectedFileValue === '' ? 'selected' : ''}" data-value="" onclick="selectCustomOption('', '-- Select an audio file --')">-- Select an audio file --</div>`;
|
|
availableFiles.forEach(f => {
|
|
const isSelected = selectedFileValue === f ? 'selected' : '';
|
|
html += `<div class="custom-select-option ${isSelected}" data-value="${f}" onclick="selectCustomOption('${f}', '${f}')">${f}</div>`;
|
|
});
|
|
optionsContainer.innerHTML = html;
|
|
}
|
|
|
|
async function loadList() {
|
|
const res = await fetch('/api/files/content?path=uploads/list.txt');
|
|
const data = await res.json();
|
|
listData = [];
|
|
if (data.success && data.content) {
|
|
const lines = String(data.content).split('\n');
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (!trimmed) continue;
|
|
const match = trimmed.match(/^([^=]+)==(.+)$/);
|
|
if (match) {
|
|
listData.push({ code: match[1], files: match[2].split(',').filter(Boolean) });
|
|
}
|
|
}
|
|
}
|
|
renderList();
|
|
}
|
|
|
|
function renderList() {
|
|
const container = document.getElementById('list-container');
|
|
container.innerHTML = '';
|
|
listData.sort((a, b) => String(a.code).localeCompare(String(b.code)));
|
|
|
|
if (listData.length === 0) {
|
|
container.innerHTML = '<div style="text-align:center; padding: 40px; color:var(--text-muted)">No entries</div>';
|
|
return;
|
|
}
|
|
|
|
listData.forEach((item, index) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'list-item';
|
|
|
|
const tagsHtml = (item.files || []).map(f => `<span class="tag">${f}</span>`).join(' ');
|
|
div.innerHTML = `
|
|
<div class="code">${item.code}</div>
|
|
<div>${tagsHtml || '<span style="color:var(--text-muted); font-style: italic;">No files</span>'}</div>
|
|
<div style="display:flex; gap:6px;">
|
|
<button class="btn-icon" onclick="openEditModal(${index})" aria-label="Edit">${ICONS.edit}</button>
|
|
<button class="btn-icon danger" onclick="removeListItem(${index})" aria-label="Delete">${ICONS.trash}</button>
|
|
</div>
|
|
`;
|
|
container.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function removeListItem(index) {
|
|
const code = listData[index]?.code;
|
|
if (!code) return;
|
|
if (!confirm(`删除 ${code} ?`)) return;
|
|
listData.splice(index, 1);
|
|
renderList();
|
|
}
|
|
|
|
function openAddModal() {
|
|
editingIndex = -1;
|
|
currentSequence = [];
|
|
document.getElementById('modal-title').textContent = '添加站号配置';
|
|
document.getElementById('edit-code').value = '';
|
|
renderSequence();
|
|
selectCustomOption('', '-- Select an audio file --');
|
|
document.getElementById('edit-modal').classList.add('active');
|
|
}
|
|
|
|
function openEditModal(index) {
|
|
editingIndex = index;
|
|
const item = listData[index];
|
|
currentSequence = [...(item?.files || [])];
|
|
document.getElementById('modal-title').textContent = `编辑:${item?.code || ''}`;
|
|
document.getElementById('edit-code').value = item?.code || '';
|
|
renderSequence();
|
|
selectCustomOption('', '-- Select an audio file --');
|
|
document.getElementById('edit-modal').classList.add('active');
|
|
}
|
|
|
|
function closeModal() {
|
|
closeCustomSelect();
|
|
document.getElementById('edit-modal').classList.remove('active');
|
|
}
|
|
|
|
function renderSequence() {
|
|
const list = document.getElementById('edit-files-list');
|
|
list.innerHTML = '';
|
|
|
|
if (currentSequence.length === 0) {
|
|
list.innerHTML = '<div style="color:var(--text-muted); font-size:0.85rem; text-align:center; padding:10px;">No files added yet</div>';
|
|
return;
|
|
}
|
|
|
|
currentSequence.forEach((file, idx) => {
|
|
const div = document.createElement('div');
|
|
div.className = 'edit-file-item';
|
|
div.innerHTML = `
|
|
<div class="edit-file-name">${file}</div>
|
|
<div class="edit-file-actions">
|
|
<button class="btn-icon" onclick="moveFile(${idx}, -1)" ${idx === 0 ? 'disabled style="opacity:0.35"' : ''} aria-label="Up">${ICONS.up}</button>
|
|
<button class="btn-icon" onclick="moveFile(${idx}, 1)" ${idx === currentSequence.length - 1 ? 'disabled style="opacity:0.35"' : ''} aria-label="Down">${ICONS.down}</button>
|
|
<button class="btn-icon danger" onclick="removeFileFromSequence(${idx})" aria-label="Remove">${ICONS.trash}</button>
|
|
</div>
|
|
`;
|
|
list.appendChild(div);
|
|
});
|
|
}
|
|
|
|
function addFileToSequence() {
|
|
const file = selectedFileValue;
|
|
if (!file) return;
|
|
currentSequence.push(file);
|
|
renderSequence();
|
|
selectCustomOption('', '-- Select an audio file --');
|
|
}
|
|
|
|
function removeFileFromSequence(idx) {
|
|
currentSequence.splice(idx, 1);
|
|
renderSequence();
|
|
}
|
|
|
|
function moveFile(idx, dir) {
|
|
if (idx + dir < 0 || idx + dir >= currentSequence.length) return;
|
|
const temp = currentSequence[idx];
|
|
currentSequence[idx] = currentSequence[idx + dir];
|
|
currentSequence[idx + dir] = temp;
|
|
renderSequence();
|
|
}
|
|
|
|
function confirmEdit() {
|
|
const code = document.getElementById('edit-code').value.trim();
|
|
if (!code) {
|
|
alert('站号不能为空');
|
|
return;
|
|
}
|
|
|
|
if (editingIndex === -1) {
|
|
if (listData.some(item => item.code === code)) {
|
|
alert(`站号 ${code} 已存在`);
|
|
return;
|
|
}
|
|
listData.push({ code, files: [...currentSequence] });
|
|
} else {
|
|
listData[editingIndex].code = code;
|
|
listData[editingIndex].files = [...currentSequence];
|
|
}
|
|
|
|
renderList();
|
|
closeModal();
|
|
}
|
|
|
|
async function saveList() {
|
|
const btn = document.getElementById('btn-save');
|
|
btn.disabled = true;
|
|
btn.textContent = 'Saving...';
|
|
|
|
const content = listData
|
|
.filter(item => item.code)
|
|
.map(item => `${item.code}==${(item.files || []).join(',')}`)
|
|
.join('\n');
|
|
|
|
try {
|
|
const res = await fetch('/api/files/content', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ path: 'uploads/list.txt', content: content + '\n' })
|
|
});
|
|
const data = await res.json();
|
|
if (!data.success) throw new Error(data.message || 'Save failed');
|
|
showToast('Success', 'list.txt saved');
|
|
} catch (e) {
|
|
showToast('Error', e?.message || String(e));
|
|
} finally {
|
|
btn.disabled = false;
|
|
btn.textContent = '保存到服务器';
|
|
}
|
|
}
|
|
|
|
async function reloadAll() {
|
|
try {
|
|
await fetchAvailableFiles();
|
|
await loadList();
|
|
showToast('OK', 'Reloaded');
|
|
} catch (e) {
|
|
showToast('Error', e?.message || String(e));
|
|
}
|
|
}
|
|
|
|
async function init() {
|
|
try {
|
|
await fetchAvailableFiles();
|
|
await loadList();
|
|
} catch (e) {
|
|
showToast('Error', e?.message || String(e));
|
|
}
|
|
}
|
|
|
|
init();
|
|
</script>
|
|
</body>
|
|
</html>
|