1423 lines
61 KiB
HTML
1423 lines
61 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||
<title>AM Downloader - 音乐下载与同步</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||
<style>
|
||
:root {
|
||
--bg-base: #080c14;
|
||
--bg-surface: #0f1624;
|
||
--bg-surface-elevated: #162032;
|
||
--bg-card: rgba(17, 26, 42, 0.75);
|
||
--bg-card-hover: rgba(24, 37, 60, 0.85);
|
||
--border-subtle: rgba(255, 255, 255, 0.08);
|
||
--border-focus: rgba(16, 185, 129, 0.5);
|
||
--text-main: #f1f5f9;
|
||
--text-muted: #94a3b8;
|
||
--text-dim: #64748b;
|
||
--primary: #10b981;
|
||
--primary-hover: #059669;
|
||
--primary-glow: rgba(16, 185, 129, 0.25);
|
||
--accent: #38bdf8;
|
||
--danger: #f43f5e;
|
||
--warning: #f59e0b;
|
||
--radius-sm: 8px;
|
||
--radius-md: 12px;
|
||
--radius-lg: 16px;
|
||
--radius-full: 9999px;
|
||
--transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
|
||
--shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||
--shadow-md: 0 8px 24px rgba(0, 0, 0, 0.35);
|
||
--shadow-glow: 0 0 30px rgba(16, 185, 129, 0.15);
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
margin: 0;
|
||
background-color: var(--bg-base);
|
||
background-image:
|
||
radial-gradient(at 0% 0%, rgba(16, 185, 129, 0.07) 0px, transparent 50%),
|
||
radial-gradient(at 100% 100%, rgba(56, 189, 248, 0.06) 0px, transparent 50%);
|
||
background-attachment: fixed;
|
||
color: var(--text-main);
|
||
font-family: 'Plus Jakarta Sans', system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||
font-size: 14px;
|
||
line-height: 1.5;
|
||
-webkit-font-smoothing: antialiased;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
.wrap {
|
||
max-width: 1120px;
|
||
margin: auto;
|
||
padding: 24px 20px 96px;
|
||
}
|
||
|
||
/* Header & Top Bar */
|
||
.top {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 20px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.brand {
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
letter-spacing: -0.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.brand b {
|
||
color: var(--primary);
|
||
text-shadow: 0 0 16px var(--primary-glow);
|
||
}
|
||
.sub {
|
||
color: var(--text-muted);
|
||
font-size: 12px;
|
||
margin-top: 2px;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.nav {
|
||
display: flex;
|
||
gap: 4px;
|
||
background: rgba(15, 22, 36, 0.85);
|
||
backdrop-filter: blur(12px);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 5px;
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
|
||
.nav button {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
padding: 8px 16px;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 13.5px;
|
||
transition: var(--transition);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
.nav button:hover {
|
||
color: var(--text-main);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
.nav button.active {
|
||
background: var(--bg-surface-elevated);
|
||
color: #fff;
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||
border: 1px solid var(--border-subtle);
|
||
}
|
||
|
||
.badge {
|
||
display: inline-flex;
|
||
min-width: 18px;
|
||
height: 18px;
|
||
padding: 0 5px;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: var(--radius-full);
|
||
background: var(--primary);
|
||
color: #032014;
|
||
font-size: 11px;
|
||
font-weight: 700;
|
||
}
|
||
|
||
/* User Pill & Auth Bar */
|
||
.authbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
justify-content: flex-end;
|
||
margin: -6px 0 18px;
|
||
}
|
||
.userpill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
padding: 6px 14px;
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-full);
|
||
background: rgba(17, 26, 42, 0.6);
|
||
backdrop-filter: blur(8px);
|
||
color: var(--text-muted);
|
||
font-size: 12.5px;
|
||
font-weight: 500;
|
||
}
|
||
.userpill::before {
|
||
content: '';
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: 50%;
|
||
background: var(--primary);
|
||
box-shadow: 0 0 8px var(--primary);
|
||
}
|
||
|
||
/* Views */
|
||
.view { display: none; }
|
||
.view.active {
|
||
display: block;
|
||
animation: fadeIn 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
@keyframes fadeIn {
|
||
from { opacity: 0; transform: translateY(6px); }
|
||
to { opacity: 1; transform: translateY(0); }
|
||
}
|
||
|
||
/* Cards & Layout */
|
||
.card {
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(16px);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 22px;
|
||
margin-bottom: 18px;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: var(--transition);
|
||
}
|
||
.card:hover {
|
||
border-color: rgba(255, 255, 255, 0.12);
|
||
}
|
||
|
||
.section-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
margin-bottom: 18px;
|
||
}
|
||
.title {
|
||
font-size: 17px;
|
||
font-weight: 700;
|
||
letter-spacing: -0.3px;
|
||
color: var(--text-main);
|
||
}
|
||
.muted {
|
||
color: var(--text-muted);
|
||
font-size: 12.5px;
|
||
margin-top: 2px;
|
||
}
|
||
|
||
/* Hero Section */
|
||
.hero {
|
||
padding: 56px 16px 42px;
|
||
text-align: center;
|
||
position: relative;
|
||
}
|
||
.hero h1 {
|
||
font-size: 38px;
|
||
font-weight: 800;
|
||
letter-spacing: -1px;
|
||
margin: 0 0 10px;
|
||
background: linear-gradient(135deg, #ffffff 40%, #94a3b8 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
}
|
||
.hero p {
|
||
color: var(--text-muted);
|
||
font-size: 15px;
|
||
max-width: 500px;
|
||
margin: 0 auto 28px;
|
||
}
|
||
|
||
/* Inputs & Form Controls */
|
||
.input {
|
||
width: 100%;
|
||
padding: 11px 14px;
|
||
border-radius: var(--radius-md);
|
||
border: 1px solid var(--border-subtle);
|
||
background: rgba(10, 16, 28, 0.85);
|
||
color: #fff;
|
||
font-size: 13.5px;
|
||
outline: none;
|
||
transition: var(--transition);
|
||
font-family: inherit;
|
||
}
|
||
.input:focus {
|
||
border-color: var(--primary);
|
||
box-shadow: 0 0 0 3px var(--primary-glow);
|
||
background: rgba(12, 20, 36, 0.95);
|
||
}
|
||
select.input {
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E");
|
||
background-repeat: no-repeat;
|
||
background-position: right 12px center;
|
||
padding-right: 36px;
|
||
cursor: pointer;
|
||
}
|
||
|
||
/* Buttons */
|
||
.btn {
|
||
border: 0;
|
||
border-radius: var(--radius-md);
|
||
padding: 10px 18px;
|
||
background: var(--primary);
|
||
color: #032014;
|
||
font-weight: 700;
|
||
font-size: 13.5px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: var(--transition);
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 6px;
|
||
box-shadow: 0 2px 10px var(--primary-glow);
|
||
}
|
||
.btn:hover:not(:disabled) {
|
||
background: var(--primary-hover);
|
||
transform: translateY(-1px);
|
||
box-shadow: 0 4px 14px rgba(16, 185, 129, 0.35);
|
||
}
|
||
.btn:active:not(:disabled) {
|
||
transform: translateY(0);
|
||
}
|
||
.btn.alt {
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border: 1px solid var(--border-subtle);
|
||
color: var(--text-main);
|
||
box-shadow: none;
|
||
}
|
||
.btn.alt:hover:not(:disabled) {
|
||
background: rgba(255, 255, 255, 0.1);
|
||
border-color: rgba(255, 255, 255, 0.16);
|
||
transform: translateY(-1px);
|
||
}
|
||
.btn:disabled {
|
||
opacity: 0.45;
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
}
|
||
|
||
/* Search Bar */
|
||
.searchbar {
|
||
display: flex;
|
||
max-width: 740px;
|
||
margin: auto;
|
||
gap: 10px;
|
||
background: rgba(15, 22, 36, 0.7);
|
||
padding: 6px;
|
||
border-radius: var(--radius-lg);
|
||
border: 1px solid var(--border-subtle);
|
||
backdrop-filter: blur(12px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.searchbar .input {
|
||
border: 1px solid transparent;
|
||
background: transparent;
|
||
}
|
||
.searchbar .input:focus {
|
||
background: rgba(255, 255, 255, 0.03);
|
||
box-shadow: none;
|
||
}
|
||
.searchbar select.input {
|
||
max-width: 110px;
|
||
background-color: rgba(255, 255, 255, 0.04);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
}
|
||
|
||
/* Row & Field Layout */
|
||
.row { display: flex; gap: 10px; align-items: center; }
|
||
.row > * { min-width: 0; flex: 1; }
|
||
.field { margin-top: 14px; }
|
||
.field label {
|
||
display: block;
|
||
color: var(--text-muted);
|
||
font-size: 12.5px;
|
||
font-weight: 600;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
/* Music Result Card & List */
|
||
.results { display: grid; gap: 6px; }
|
||
.result {
|
||
display: grid;
|
||
grid-template-columns: 52px minmax(0, 1fr) auto;
|
||
gap: 14px;
|
||
align-items: center;
|
||
padding: 12px 14px;
|
||
border-radius: var(--radius-md);
|
||
background: rgba(255, 255, 255, 0.015);
|
||
border: 1px solid transparent;
|
||
transition: var(--transition);
|
||
}
|
||
.result:hover {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
border-color: var(--border-subtle);
|
||
}
|
||
.cover {
|
||
width: 52px;
|
||
height: 52px;
|
||
border-radius: var(--radius-md);
|
||
object-fit: cover;
|
||
background: #151e30;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
.name {
|
||
font-weight: 700;
|
||
font-size: 14.5px;
|
||
color: var(--text-main);
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.meta {
|
||
font-size: 12px;
|
||
color: var(--text-muted);
|
||
margin-top: 3px;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
.toolbar { display: flex; gap: 8px; align-items: center; }
|
||
.quality-select {
|
||
max-width: 136px;
|
||
padding: 8px 12px;
|
||
font-size: 12.5px;
|
||
}
|
||
|
||
/* Chips & Quality Badges */
|
||
.file-info { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 6px; }
|
||
.chip {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
padding: 3px 8px;
|
||
border-radius: var(--radius-sm);
|
||
background: rgba(255, 255, 255, 0.05);
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
font-family: 'JetBrains Mono', monospace;
|
||
font-weight: 500;
|
||
border: 1px solid var(--border-subtle);
|
||
}
|
||
.chip.actual {
|
||
color: #6ee7b7;
|
||
background: rgba(16, 185, 129, 0.1);
|
||
border-color: rgba(16, 185, 129, 0.25);
|
||
}
|
||
.chip.warn {
|
||
color: #fcd34d;
|
||
background: rgba(245, 158, 11, 0.1);
|
||
border-color: rgba(245, 158, 11, 0.25);
|
||
}
|
||
|
||
/* Progress & Status */
|
||
.progress {
|
||
height: 6px;
|
||
background: rgba(255, 255, 255, 0.06);
|
||
border-radius: var(--radius-full);
|
||
margin-top: 8px;
|
||
overflow: hidden;
|
||
}
|
||
.progress i {
|
||
display: block;
|
||
height: 100%;
|
||
background: linear-gradient(90deg, var(--primary), #34d399);
|
||
border-radius: var(--radius-full);
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
/* Task Tabs */
|
||
.task-tabs {
|
||
display: flex;
|
||
gap: 6px;
|
||
overflow-x: auto;
|
||
margin-bottom: 16px;
|
||
padding-bottom: 4px;
|
||
}
|
||
.task-tabs button {
|
||
border: 1px solid var(--border-subtle);
|
||
background: rgba(15, 22, 36, 0.6);
|
||
color: var(--text-muted);
|
||
border-radius: var(--radius-md);
|
||
padding: 7px 14px;
|
||
white-space: nowrap;
|
||
cursor: pointer;
|
||
font-weight: 600;
|
||
font-size: 13px;
|
||
transition: var(--transition);
|
||
}
|
||
.task-tabs button:hover {
|
||
color: var(--text-main);
|
||
background: rgba(255, 255, 255, 0.05);
|
||
}
|
||
.task-tabs button.active {
|
||
background: var(--bg-surface-elevated);
|
||
border-color: rgba(255, 255, 255, 0.16);
|
||
color: #fff;
|
||
}
|
||
|
||
/* Settings & Account Grids */
|
||
.settings-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 18px;
|
||
}
|
||
.setting-card h3 {
|
||
margin: 0 0 4px;
|
||
font-size: 16px;
|
||
font-weight: 700;
|
||
color: var(--text-main);
|
||
}
|
||
.setting-card p {
|
||
margin: 0 0 16px;
|
||
color: var(--text-muted);
|
||
font-size: 12.5px;
|
||
}
|
||
|
||
/* Notices & Alerts */
|
||
.notice {
|
||
padding: 12px 14px;
|
||
background: rgba(16, 26, 44, 0.6);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
color: var(--text-muted);
|
||
font-size: 12.5px;
|
||
margin-top: 14px;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Admin Shell & Dashboard */
|
||
.admin-shell { display: grid; gap: 18px; }
|
||
.admin-header { padding: 4px 2px; }
|
||
.admin-header h2 {
|
||
margin: 4px 0 6px;
|
||
font-size: 28px;
|
||
font-weight: 800;
|
||
letter-spacing: -0.6px;
|
||
}
|
||
.eyebrow {
|
||
font-size: 11px;
|
||
letter-spacing: 2px;
|
||
color: var(--primary);
|
||
font-weight: 800;
|
||
text-transform: uppercase;
|
||
}
|
||
.admin-dashboard {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||
gap: 14px;
|
||
}
|
||
.admin-tile {
|
||
border: 1px solid var(--border-subtle);
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(12px);
|
||
color: var(--text-main);
|
||
border-radius: var(--radius-lg);
|
||
padding: 22px;
|
||
text-align: left;
|
||
display: grid;
|
||
gap: 6px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
box-shadow: var(--shadow-sm);
|
||
}
|
||
.admin-tile:hover {
|
||
background: var(--bg-card-hover);
|
||
border-color: rgba(255, 255, 255, 0.16);
|
||
transform: translateY(-2px);
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.admin-tile span { font-size: 26px; }
|
||
.admin-tile strong { font-size: 16.5px; font-weight: 700; color: #fff; }
|
||
.admin-tile small { color: var(--text-muted); font-size: 12px; }
|
||
|
||
/* Admin Panels & Lists */
|
||
.admin-panel {
|
||
background: var(--bg-card);
|
||
backdrop-filter: blur(16px);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 20px;
|
||
}
|
||
.panel-head {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 14px;
|
||
margin-bottom: 20px;
|
||
}
|
||
.panel-head > div:nth-child(2) { flex: 1; }
|
||
|
||
.user-filters {
|
||
display: flex;
|
||
gap: 6px;
|
||
overflow-x: auto;
|
||
margin: 16px 0;
|
||
padding-bottom: 4px;
|
||
}
|
||
.user-filters button {
|
||
border: 1px solid var(--border-subtle);
|
||
background: rgba(15, 22, 36, 0.6);
|
||
color: var(--text-muted);
|
||
border-radius: var(--radius-md);
|
||
padding: 7px 14px;
|
||
white-space: nowrap;
|
||
font-weight: 600;
|
||
font-size: 12.5px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
}
|
||
.user-filters button.active {
|
||
background: var(--bg-surface-elevated);
|
||
border-color: rgba(255, 255, 255, 0.16);
|
||
color: #fff;
|
||
}
|
||
|
||
.user-group { margin-top: 16px; }
|
||
.group-head {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 4px 2px 10px;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
margin-bottom: 10px;
|
||
}
|
||
.group-list { display: grid; gap: 8px; }
|
||
.user-card {
|
||
background: rgba(11, 18, 30, 0.6);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
padding: 14px;
|
||
transition: var(--transition);
|
||
}
|
||
.user-card:hover {
|
||
border-color: rgba(255, 255, 255, 0.14);
|
||
background: rgba(14, 23, 38, 0.7);
|
||
}
|
||
.user-main {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
gap: 14px;
|
||
align-items: center;
|
||
}
|
||
.user-stats { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px; }
|
||
.stat-chip {
|
||
background: rgba(255, 255, 255, 0.04);
|
||
color: var(--text-muted);
|
||
padding: 3px 8px;
|
||
border-radius: var(--radius-sm);
|
||
font-size: 11.5px;
|
||
border: 1px solid var(--border-subtle);
|
||
}
|
||
.user-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; }
|
||
|
||
/* Admin Summaries & Ledgers */
|
||
.admin-summary {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 12px;
|
||
margin-top: 16px;
|
||
}
|
||
.summary-card {
|
||
background: rgba(11, 18, 30, 0.6);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-md);
|
||
padding: 16px;
|
||
}
|
||
.summary-card strong {
|
||
display: block;
|
||
font-size: 24px;
|
||
font-weight: 800;
|
||
color: var(--primary);
|
||
margin-top: 4px;
|
||
}
|
||
.ledger { display: grid; gap: 8px; margin-top: 14px; }
|
||
.ledger-item {
|
||
padding: 12px 14px;
|
||
border: 1px solid var(--border-subtle);
|
||
background: rgba(11, 18, 30, 0.6);
|
||
border-radius: var(--radius-md);
|
||
}
|
||
|
||
.detail-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 16px;
|
||
}
|
||
.detail-card {
|
||
background: rgba(11, 18, 30, 0.6);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 18px;
|
||
}
|
||
.detail-card h4 { margin: 0 0 12px; font-size: 15px; }
|
||
.detail-value { font-size: 28px; font-weight: 800; color: var(--primary); }
|
||
.detail-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px; }
|
||
|
||
/* Empty state */
|
||
.empty {
|
||
text-align: center;
|
||
padding: 48px 16px;
|
||
color: var(--text-dim);
|
||
font-size: 13.5px;
|
||
}
|
||
|
||
/* Login Modal */
|
||
.login-overlay {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(6, 10, 18, 0.85);
|
||
backdrop-filter: blur(20px);
|
||
z-index: 999;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
.login-box {
|
||
width: min(390px, 100%);
|
||
background: var(--bg-surface);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
padding: 28px;
|
||
box-shadow: var(--shadow-md);
|
||
animation: modalIn 0.3s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
@keyframes modalIn {
|
||
from { opacity: 0; transform: scale(0.96) translateY(8px); }
|
||
to { opacity: 1; transform: scale(1) translateY(0); }
|
||
}
|
||
.login-box h2 {
|
||
margin: 0 0 6px;
|
||
font-size: 22px;
|
||
font-weight: 800;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
.login-box p {
|
||
color: var(--text-muted);
|
||
font-size: 13px;
|
||
margin: 0 0 20px;
|
||
}
|
||
.login-error {
|
||
color: var(--danger);
|
||
font-size: 12px;
|
||
margin-top: 10px;
|
||
min-height: 16px;
|
||
}
|
||
|
||
/* Mobile Bottom Navigation Dock */
|
||
.mobile-nav {
|
||
display: none;
|
||
}
|
||
|
||
@media(max-width: 720px) {
|
||
.wrap { padding: 16px 14px 110px; }
|
||
.top .nav { display: none; }
|
||
.hero { padding: 36px 8px 26px; }
|
||
.hero h1 { font-size: 28px; }
|
||
.searchbar { flex-wrap: wrap; }
|
||
.searchbar .input { flex-basis: 100%; }
|
||
.searchbar select, .searchbar button { flex: 1; }
|
||
.settings-grid { grid-template-columns: 1fr; }
|
||
|
||
.result {
|
||
grid-template-columns: 44px minmax(0, 1fr);
|
||
gap: 10px;
|
||
padding: 10px;
|
||
}
|
||
.cover { width: 44px; height: 44px; }
|
||
.result .toolbar {
|
||
grid-column: 2;
|
||
display: flex;
|
||
width: 100%;
|
||
gap: 8px;
|
||
margin-top: 6px;
|
||
}
|
||
.result .quality-select { max-width: none; min-width: 0; flex: 1; }
|
||
.result .toolbar .btn { padding: 9px 14px; min-width: 76px; }
|
||
|
||
.mobile-nav {
|
||
position: fixed;
|
||
left: 12px;
|
||
right: 12px;
|
||
bottom: max(12px, env(safe-area-inset-bottom));
|
||
display: flex;
|
||
justify-content: space-around;
|
||
gap: 4px;
|
||
padding: 6px;
|
||
background: rgba(15, 22, 36, 0.88);
|
||
backdrop-filter: blur(20px);
|
||
border: 1px solid var(--border-subtle);
|
||
border-radius: var(--radius-lg);
|
||
z-index: 100;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.mobile-nav button {
|
||
flex: 1;
|
||
min-width: 0;
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
padding: 8px 2px;
|
||
border-radius: var(--radius-md);
|
||
font-size: 11px;
|
||
font-weight: 600;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 3px;
|
||
cursor: pointer;
|
||
transition: var(--transition);
|
||
}
|
||
.mobile-nav button.active {
|
||
background: var(--bg-surface-elevated);
|
||
color: #fff;
|
||
}
|
||
|
||
.admin-dashboard { grid-template-columns: 1fr; }
|
||
.admin-panel { padding: 14px; }
|
||
.admin-create > * { flex-basis: 100%; }
|
||
.user-main { align-items: flex-start; flex-direction: column; }
|
||
.user-actions .btn { flex: 1; min-width: 100px; }
|
||
.admin-summary { grid-template-columns: 1fr 1fr; }
|
||
.detail-grid { grid-template-columns: 1fr; }
|
||
.row { flex-wrap: wrap; }
|
||
.row > * { flex-basis: 100%; }
|
||
}
|
||
|
||
/* Admin Segmented Tabs & Modern Header */
|
||
.admin-tab-nav {
|
||
display: flex;
|
||
gap: 6px;
|
||
background: rgba(15, 22, 36, 0.75);
|
||
backdrop-filter: blur(12px);
|
||
padding: 5px;
|
||
border-radius: var(--radius-lg);
|
||
border: 1px solid var(--border-subtle);
|
||
overflow-x: auto;
|
||
margin-bottom: 20px;
|
||
}
|
||
.admin-tab-nav button {
|
||
border: 0;
|
||
background: transparent;
|
||
color: var(--text-muted);
|
||
padding: 9px 18px;
|
||
border-radius: var(--radius-md);
|
||
font-weight: 600;
|
||
font-size: 13.5px;
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: var(--transition);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
.admin-tab-nav button:hover {
|
||
color: var(--text-main);
|
||
background: rgba(255, 255, 255, 0.04);
|
||
}
|
||
.admin-tab-nav button.active {
|
||
background: var(--bg-surface-elevated);
|
||
color: #fff;
|
||
border: 1px solid var(--border-subtle);
|
||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
/* User Drawer Overlay & Panel */
|
||
.drawer-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
background: rgba(5, 8, 15, 0.65);
|
||
backdrop-filter: blur(6px);
|
||
z-index: 200;
|
||
opacity: 0;
|
||
pointer-events: none;
|
||
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1);
|
||
}
|
||
.drawer-backdrop.active {
|
||
opacity: 1;
|
||
pointer-events: auto;
|
||
}
|
||
.admin-drawer {
|
||
position: fixed;
|
||
top: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: min(520px, 92vw);
|
||
background: var(--bg-surface);
|
||
border-left: 1px solid var(--border-subtle);
|
||
box-shadow: -10px 0 35px rgba(0, 0, 0, 0.5);
|
||
z-index: 201;
|
||
transform: translateX(100%);
|
||
transition: transform 0.28s cubic-bezier(0.16, 1, 0.3, 1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
.admin-drawer.active {
|
||
transform: translateX(0);
|
||
}
|
||
.drawer-header {
|
||
padding: 18px 20px;
|
||
border-bottom: 1px solid var(--border-subtle);
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
background: rgba(15, 22, 36, 0.95);
|
||
}
|
||
.drawer-body {
|
||
padding: 20px;
|
||
overflow-y: auto;
|
||
flex: 1;
|
||
}
|
||
@media (max-width: 720px) {
|
||
.admin-drawer {
|
||
top: auto;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
width: 100vw;
|
||
max-height: 85vh;
|
||
border-left: 0;
|
||
border-top: 1px solid var(--border-subtle);
|
||
border-radius: 20px 20px 0 0;
|
||
transform: translateY(100%);
|
||
}
|
||
.admin-drawer.active {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="loginOverlay" class="login-overlay">
|
||
<form id="loginForm" class="login-box">
|
||
<h2>AM Downloader</h2>
|
||
<p>登录后管理你的下载任务、积分和同步设置。</p>
|
||
<div class="field">
|
||
<label>用户名</label>
|
||
<input id="loginUser" class="input" autocomplete="username" placeholder="请输入用户名">
|
||
</div>
|
||
<div class="field">
|
||
<label>密码</label>
|
||
<input id="loginPass" type="password" class="input" autocomplete="current-password" placeholder="请输入密码">
|
||
</div>
|
||
<div id="loginError" class="login-error"></div>
|
||
<button class="btn" style="width:100%;margin-top:14px">立即登录</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div class="wrap">
|
||
<header class="top">
|
||
<div>
|
||
<div class="brand">AM <b>Downloader</b></div>
|
||
<div class="sub">搜索 · 下载 · WebDAV 云端同步</div>
|
||
</div>
|
||
<nav class="nav" id="topNav">
|
||
<button data-view="home">首页</button>
|
||
<button data-view="search">搜索</button>
|
||
<button data-view="queue">下载<span id="navBadge" style="display:none">0</span></button>
|
||
<button data-view="account">账户</button>
|
||
<button id="adminNav" data-view="admin">管理</button>
|
||
</nav>
|
||
</header>
|
||
|
||
<div class="authbar">
|
||
<span id="userPill" class="userpill">加载中…</span>
|
||
<button id="logoutBtn" class="btn alt">退出</button>
|
||
</div>
|
||
|
||
<main>
|
||
<!-- 首页视图 -->
|
||
<section id="home" class="view">
|
||
<div class="hero">
|
||
<h1>找到你想听的音乐</h1>
|
||
<p>支持多源音乐检索、无损解析,一键加入下载队列并自动同步。</p>
|
||
<form id="homeSearch" class="searchbar">
|
||
<input id="homeKeyword" class="input" placeholder="输入歌曲名、歌手或专辑名称…" autocomplete="off">
|
||
<select id="homeSource" class="input">
|
||
<option value="kw">小窝</option>
|
||
<option value="kg">小狗</option>
|
||
<option value="wy">小云</option>
|
||
<option value="tx">小Q</option>
|
||
</select>
|
||
<button class="btn">开始搜索</button>
|
||
</form>
|
||
</div>
|
||
<div class="card">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="title">最近下载</div>
|
||
<div class="muted">最近提交的音频下载任务历史</div>
|
||
</div>
|
||
<button class="btn alt" data-go="queue">查看全部</button>
|
||
</div>
|
||
<div id="recentTasks"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 搜索视图 -->
|
||
<section id="search" class="view">
|
||
<div class="card">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="title">搜索音乐</div>
|
||
<div class="muted">支持歌曲名、歌手、专辑多源聚合检索</div>
|
||
</div>
|
||
</div>
|
||
<form id="searchForm" class="row">
|
||
<input id="keyword" class="input" placeholder="输入关键词" autocomplete="off">
|
||
<select id="source" class="input" style="max-width:140px">
|
||
<option value="kw">小窝</option>
|
||
<option value="kg">小狗</option>
|
||
<option value="wy">小云</option>
|
||
<option value="tx">小Q</option>
|
||
</select>
|
||
<button class="btn" style="min-width:100px">搜索</button>
|
||
</form>
|
||
</div>
|
||
|
||
<div id="resultsCard" class="card" style="display:none">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="title">搜索结果</div>
|
||
<div id="resultHint" class="muted"></div>
|
||
</div>
|
||
</div>
|
||
<div id="results" class="results"></div>
|
||
<button id="loadMore" class="btn alt loadmore" style="display:none;width:100%;margin-top:14px">加载更多</button>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 队列视图 -->
|
||
<section id="queue" class="view">
|
||
<div class="card">
|
||
<div class="section-head">
|
||
<div>
|
||
<div class="title">下载队列</div>
|
||
<div class="muted">任务会自动处理;本地文件是否保留由“设置 → 下载 → 本地文件策略”决定。</div>
|
||
</div>
|
||
</div>
|
||
<div id="taskTabs" class="task-tabs">
|
||
<button class="active" data-filter="all">全部</button>
|
||
<button data-filter="active">进行中</button>
|
||
<button data-filter="done">已完成</button>
|
||
<button data-filter="error">失败</button>
|
||
</div>
|
||
<div id="tasks"></div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 账户视图 -->
|
||
<section id="account" class="view">
|
||
<div class="settings-grid">
|
||
<div class="card setting-card">
|
||
<h3>账户与积分</h3>
|
||
<p>积分用于新的实际下载;失败会自动全额退回流水。</p>
|
||
<div class="notice" id="creditPanel">加载中…</div>
|
||
<div class="field">
|
||
<label>兑换码充值</label>
|
||
<input id="redeemCode" class="input" placeholder="输入兑换码 (例如 XXXX-XXXX-XXXX)">
|
||
<button id="redeemBtn" class="btn" style="margin-top:10px">兑换积分</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="userDavCard" class="card setting-card">
|
||
<h3>我的 WebDAV</h3>
|
||
<p>高级用户与管理员专属。文件下载后将自动同步至你的私有 WebDAV。</p>
|
||
<div class="field">
|
||
<label>服务器地址</label>
|
||
<input id="userDavUrl" class="input" placeholder="https://dav.example.com/music">
|
||
</div>
|
||
<div class="row">
|
||
<div class="field" style="margin-top:0">
|
||
<label>用户名</label>
|
||
<input id="userDavUser" class="input" placeholder="用户名">
|
||
</div>
|
||
<div class="field" style="margin-top:0">
|
||
<label>密码</label>
|
||
<input id="userDavPass" type="password" class="input" placeholder="留空不修改">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>存储根目录</label>
|
||
<input id="userDavRoot" class="input" placeholder="Music">
|
||
</div>
|
||
<div class="row" style="margin-top:16px">
|
||
<button id="testUserDav" class="btn alt">测试连接</button>
|
||
<button id="saveUserDav" class="btn">保存设置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 管理中心视图 -->
|
||
<section id="admin" class="view">
|
||
<div class="admin-shell">
|
||
<div class="admin-header">
|
||
<div>
|
||
<div class="eyebrow">Admin Center</div>
|
||
<h2>管理控制台</h2>
|
||
<p>系统多用户鉴权、积分流水记账、卡密批次、操作审计与全局设置。</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 分段 Tab 导航 (Segmented Tabs) -->
|
||
<div class="admin-tab-nav" id="adminTabNav">
|
||
<button class="admin-tab-btn active" data-admin-tab="users">
|
||
<span>👥</span> 用户管理
|
||
</button>
|
||
<button class="admin-tab-btn" data-admin-tab="credits">
|
||
<span>◎</span> 积分账本
|
||
</button>
|
||
<button class="admin-tab-btn" data-admin-tab="redeem">
|
||
<span>◇</span> 卡密批次
|
||
</button>
|
||
<button class="admin-tab-btn" data-admin-tab="audit">
|
||
<span>≡</span> 审计日志
|
||
</button>
|
||
<button class="admin-tab-btn" data-admin-tab="settings">
|
||
<span>⚙️</span> 全局设置
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 保持原有 ID 避免破坏已有绑定契约 -->
|
||
<div id="adminDashboard" style="display:none">
|
||
<button class="admin-tile" data-admin-view="users" style="display:none"></button>
|
||
<button class="admin-tile" data-admin-view="credits" style="display:none"></button>
|
||
<button class="admin-tile" data-admin-view="redeem" style="display:none"></button>
|
||
<button class="admin-tile" data-admin-view="audit" style="display:none"></button>
|
||
</div>
|
||
|
||
<!-- 1. 用户管理面板 -->
|
||
<div class="admin-panel" id="adminUsersPanel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="title">用户授权与角色列表</div>
|
||
<div class="muted">支持快速注册、角色分组分级管理、积分划拨与账户锁定。</div>
|
||
</div>
|
||
</div>
|
||
<div class="row admin-create" style="margin-bottom:14px">
|
||
<input id="newUser" class="input" placeholder="用户名">
|
||
<input id="newPass" type="password" class="input" placeholder="初始密码(至少8位)">
|
||
<select id="newRole" class="input">
|
||
<option value="user">普通用户</option>
|
||
<option value="privileged">高级用户</option>
|
||
<option value="admin">管理员</option>
|
||
</select>
|
||
<input id="newCredits" class="input" type="number" min="0" value="0" placeholder="初始积分">
|
||
<button id="createUser" class="btn">创建用户</button>
|
||
</div>
|
||
<div class="user-filters">
|
||
<button class="active" data-role-filter="all">全部</button>
|
||
<button data-role-filter="admin">管理员</button>
|
||
<button data-role-filter="privileged">高级用户</button>
|
||
<button data-role-filter="user">普通用户</button>
|
||
</div>
|
||
<div id="users"></div>
|
||
</div>
|
||
|
||
<!-- 2. 积分管理面板 -->
|
||
<div class="admin-panel" id="adminCreditsPanel" style="display:none">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="title">系统全局积分概览</div>
|
||
<div class="muted">集中查看用户额度储备与不可变流水。</div>
|
||
</div>
|
||
</div>
|
||
<div class="notice">积分充值、下载扣除、卡密兑换及失败退款均进入不可变流水记账。</div>
|
||
<div id="adminCreditSummary" class="admin-summary"></div>
|
||
<div id="creditLedger" class="ledger"></div>
|
||
</div>
|
||
|
||
<!-- 3. 兑换码面板 -->
|
||
<div class="admin-panel" id="adminRedeemPanel" style="display:none">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="title">兑换码批次与发行</div>
|
||
<div class="muted">批量生成积分卡密并支持随时吊销未消费批次。</div>
|
||
</div>
|
||
</div>
|
||
<div class="row" style="margin-bottom:14px">
|
||
<input id="batchCredits" class="input" type="number" min="1" value="10" placeholder="单码面额">
|
||
<input id="batchCount" class="input" type="number" min="1" max="10000" value="10" placeholder="生成张数">
|
||
<input id="batchNote" class="input" placeholder="批次备注说明">
|
||
<button id="createBatch" class="btn">生成卡密</button>
|
||
</div>
|
||
<pre id="batchOutput" class="notice" style="white-space:pre-wrap;max-height:220px;overflow:auto"></pre>
|
||
<div id="batches"></div>
|
||
</div>
|
||
|
||
<!-- 4. 审计日志面板 -->
|
||
<div class="admin-panel" id="adminAuditPanel" style="display:none">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="title">系统安全审计</div>
|
||
<div class="muted">记录管理员关键操作轨迹及敏感变更。</div>
|
||
</div>
|
||
<button id="loadAudit" class="btn alt">刷新审计</button>
|
||
</div>
|
||
<div id="audit" class="ledger"></div>
|
||
</div>
|
||
|
||
<!-- 5. 全局系统设置面板(内嵌在管理后台) -->
|
||
<div class="admin-panel" id="adminSettingsPanel" style="display:none">
|
||
<div class="panel-head">
|
||
<div>
|
||
<div class="title">系统全局运行设置</div>
|
||
<div class="muted">调整上游解析微服务、全局转存策略与系统级存储。</div>
|
||
</div>
|
||
</div>
|
||
<div class="settings-grid">
|
||
<div class="card setting-card" style="background:rgba(11,18,30,0.6)">
|
||
<h3>音乐解析 API</h3>
|
||
<p>用于上游音乐搜索和音源解析(独立微服务)。</p>
|
||
<div class="field">
|
||
<label>API 接口地址</label>
|
||
<input id="amApi" class="input" placeholder="http://127.0.0.1:9530">
|
||
</div>
|
||
<div class="notice">架构说明:Downloader Web 运行于端口 9540,解析 API 默认运行于端口 9530。</div>
|
||
</div>
|
||
|
||
<div class="card setting-card" style="background:rgba(11,18,30,0.6)">
|
||
<h3>下载存储策略</h3>
|
||
<p>文件保存路径、音质默认偏好与转存策略。</p>
|
||
<div class="field">
|
||
<label>本地下载目录</label>
|
||
<input id="downloadDir" class="input">
|
||
</div>
|
||
<div class="field">
|
||
<label>本地存储策略</label>
|
||
<select id="localFilePolicy" class="input">
|
||
<option value="keep">保留本地文件</option>
|
||
<option value="delete_after_upload">WebDAV 上传成功后自动清除本地副本</option>
|
||
</select>
|
||
<div class="notice">仅在 WebDAV 校验上传完整后安全删除;未开启 WebDAV 或上传失败绝不删除。</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>默认下载音质</label>
|
||
<select id="defaultQuality" class="input">
|
||
<option value="standard">标准 (128kbps)</option>
|
||
<option value="exhigh">高品质 (320kbps)</option>
|
||
<option value="lossless">无损 FLAC</option>
|
||
<option value="hires">Hi-Res 高解析</option>
|
||
<option value="sky">Sky 空间音频</option>
|
||
<option value="jyeffect">高清音效</option>
|
||
<option value="jymaster">母带级</option>
|
||
<option value="atmos">杜比全景声 Atmos</option>
|
||
<option value="atmos_plus">Atmos+ 进阶</option>
|
||
<option value="master">Master 录音室</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card setting-card" style="grid-column: 1 / -1; background:rgba(11,18,30,0.6)">
|
||
<h3>系统全局 WebDAV</h3>
|
||
<p>全局云盘存储同步配置(未配置私有 WebDAV 时兜底使用)。</p>
|
||
<div class="field">
|
||
<label>WebDAV 地址</label>
|
||
<input id="davUrl" class="input" placeholder="https://nas.example.com/dav/music">
|
||
</div>
|
||
<div class="row">
|
||
<div class="field" style="margin-top:0">
|
||
<label>用户名</label>
|
||
<input id="davUser" class="input">
|
||
</div>
|
||
<div class="field" style="margin-top:0">
|
||
<label>密码</label>
|
||
<input id="davPass" type="password" class="input">
|
||
</div>
|
||
</div>
|
||
<div class="field">
|
||
<label>远端音乐目录</label>
|
||
<input id="davPath" class="input" placeholder="Music">
|
||
</div>
|
||
<div class="field">
|
||
<label>同曲不同音质策略</label>
|
||
<select id="davVersionPolicy" class="input">
|
||
<option value="keep">保留全部版本</option>
|
||
<option value="replace_lower">高音质下载完成后清理旧的低音质</option>
|
||
</select>
|
||
</div>
|
||
<div class="row" style="margin-top:16px">
|
||
<button id="testDav" type="button" class="btn alt">测试连接</button>
|
||
<button id="saveSettings" type="button" class="btn">保存系统配置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- 用户管理:右侧/底部抽屉详情(Drawer) -->
|
||
<div class="drawer-backdrop" id="drawerBackdrop"></div>
|
||
<div class="admin-drawer" id="adminUserDetail">
|
||
<div class="drawer-header">
|
||
<div>
|
||
<div class="title" id="detailUserName">用户详情</div>
|
||
<div class="muted" id="detailUserMeta"></div>
|
||
</div>
|
||
<button class="btn alt" id="backToUsers" style="padding:6px 12px;font-size:12px">✕ 关闭</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<div id="userDetailContent"></div>
|
||
</div>
|
||
</div>
|
||
|
||
|
||
</main>
|
||
|
||
<!-- 移动端底栏导航 -->
|
||
<nav class="mobile-nav" id="mobileNav">
|
||
<button data-view="home">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/></svg>
|
||
首页
|
||
</button>
|
||
<button data-view="search">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>
|
||
搜索
|
||
</button>
|
||
<button data-view="queue">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>
|
||
下载<span id="mobileBadge" style="display:none">0</span>
|
||
</button>
|
||
<button data-view="account">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
|
||
账户
|
||
</button>
|
||
<button id="mobileAdminNav" data-view="admin" style="display:none">
|
||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z"/><circle cx="12" cy="12" r="3"/></svg>
|
||
管理
|
||
</button>
|
||
</nav>
|
||
</div>
|
||
|
||
<script>
|
||
|
||
const $=id=>document.getElementById(id);let searchPage=1,searchKeyword='',searchSource='kw',searchLoading=false,hasMore=false,taskFilter='all';
|
||
async function api(u,o){const r=await fetch('/api'+u,o);if(r.status===401){showLogin();throw new Error('请先登录')}if(!r.ok)throw new Error(await r.text());return r.status===204?null:r.json()}
|
||
function showLogin(){document.getElementById('loginOverlay').style.display='flex'}
|
||
async function loadAccount(){try{const c=await api('/credits');$('creditPanel').textContent='余额 '+c.balance+' · 预留 '+c.reserved+' · 可用 '+c.available;const u=window.currentUser||{};$('userDavCard').style.display=(u.role==='admin'||u.role==='privileged')?'':'none';if(u.role==='admin'||u.role==='privileged'){const d=await api('/webdav/config');$('userDavUrl').value=d.url||'';$('userDavUser').value=d.username||'';$('userDavRoot').value=d.rootPath||''}}catch(e){$('creditPanel').textContent=e.message}}
|
||
let adminUsersCache=[];let adminRoleFilter='all';
|
||
let currentAdminTab = 'users';
|
||
function switchAdminTab(tab){
|
||
currentAdminTab = tab;
|
||
document.querySelectorAll('.admin-tab-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.adminTab === tab);
|
||
});
|
||
const panelMap = {
|
||
users: 'adminUsersPanel',
|
||
credits: 'adminCreditsPanel',
|
||
redeem: 'adminRedeemPanel',
|
||
audit: 'adminAuditPanel',
|
||
settings: 'adminSettingsPanel'
|
||
};
|
||
['adminUsersPanel','adminCreditsPanel','adminRedeemPanel','adminAuditPanel','adminSettingsPanel'].forEach(id => {
|
||
const el = $(id);
|
||
if(el) el.style.display = 'none';
|
||
});
|
||
const activePanelId = panelMap[tab] || 'adminUsersPanel';
|
||
if($(activePanelId)) $(activePanelId).style.display = 'block';
|
||
|
||
if(tab === 'users') renderAdminUsers();
|
||
if(tab === 'credits') loadCreditLedger();
|
||
if(tab === 'redeem') loadRedeemAdmin();
|
||
if(tab === 'audit') loadAuditAdmin();
|
||
if(tab === 'settings') loadSettings();
|
||
}
|
||
|
||
function showAdminPanel(name){
|
||
if(name === 'adminUserDetail') return;
|
||
const map = {
|
||
adminDashboard: 'users',
|
||
adminUsersPanel: 'users',
|
||
adminCreditsPanel: 'credits',
|
||
adminRedeemPanel: 'redeem',
|
||
adminAuditPanel: 'audit',
|
||
adminSettingsPanel: 'settings'
|
||
};
|
||
switchAdminTab(map[name] || 'users');
|
||
}
|
||
|
||
async function loadAdmin(){
|
||
if(!window.isAdmin) return;
|
||
try {
|
||
const x = await api('/admin/users');
|
||
adminUsersCache = x.data || [];
|
||
switchAdminTab(currentAdminTab || 'users');
|
||
} catch(e) {
|
||
alert('加载管理信息失败:' + e.message);
|
||
}
|
||
}
|
||
|
||
function closeUserDrawer(){
|
||
const drawer = $('adminUserDetail');
|
||
const backdrop = $('drawerBackdrop');
|
||
if(drawer) drawer.classList.remove('active');
|
||
if(backdrop) backdrop.classList.remove('active');
|
||
}
|
||
function renderAdminUsers(){const users=adminUsersCache.filter(u=>adminRoleFilter==='all'||u.role===adminRoleFilter);const groups=[['admin','管理员'],['privileged','高级用户'],['user','普通用户']];$('users').innerHTML=groups.filter(g=>adminRoleFilter==='all'||adminRoleFilter===g[0]).map(([role,label])=>{const list=users.filter(u=>u.role===role);return `<div class="user-group"><div class="group-head"><div><div class="title">${label}</div><div class="muted">${list.length} 个账号</div></div></div><div class="group-list">${list.length?list.map(u=>`<div class="user-card"><div class="user-main"><div><div class="name">${esc(u.username)}</div><div class="meta">#${u.id} · ${esc(u.status)}</div><div class="user-stats"><span class="stat-chip" id="credit-${u.id}">积分加载中…</span></div></div><div class="user-actions"><button class="btn alt" onclick="openUserDetail(${u.id})">查看详情</button><button class="btn alt" onclick="toggleUser(${u.id},'${u.status==='active'?'disabled':'active'}')">${u.status==='active'?'禁用':'启用'}</button></div></div></div>`).join(''):'<div class="empty">暂无账号</div>'}</div></div>`}).join('');users.forEach(u=>loadUserCredit(u.id))}
|
||
async function loadUserCredit(id){try{const c=await api('/admin/users/'+id+'/credits');const e=$('credit-'+id);if(e)e.textContent='余额 '+c.balance+' · 可用 '+c.available+' · 下载 '+c.tasks}catch(e){}}
|
||
async function openUserDetail(id){const u=adminUsersCache.find(x=>x.id===id);if(!u)return;const next=u.status==='active'?'disabled':'active';$("detailUserName").textContent=u.username;$("detailUserMeta").textContent="#"+u.id+" · "+u.role+" · "+u.status;$("userDetailContent").innerHTML="<div class='detail-grid'><div class='detail-card'><h4>账户</h4><div class='muted'>角色</div><div>"+esc(u.role)+"</div><div class='muted' style='margin-top:8px'>状态</div><div>"+esc(u.status)+"</div><div class='detail-actions'><button class='btn alt' id='detailReset'>重置密码</button><button class='btn alt' id='detailToggle'>"+(u.status==='active'?'禁用账户':'启用账户')+"</button></div></div><div class='detail-card' id='detailCredits'>加载积分中…</div></div>";const drawer = $('adminUserDetail');
|
||
const backdrop = $('drawerBackdrop');
|
||
if(drawer) drawer.classList.add('active');
|
||
if(backdrop) backdrop.classList.add('active');$("detailReset").onclick=()=>resetUserPassword(id);$("detailToggle").onclick=()=>toggleUser(id,next);try{const c=await api('/admin/users/'+id+'/credits');$("detailCredits").innerHTML="<h4>积分</h4><div class='detail-value'>"+c.balance+"</div><div class='muted'>可用 "+c.available+" · 预留 "+c.reserved+" · 下载 "+c.tasks+" 次</div><div class='detail-actions'><button class='btn' id='detailAdd'>增加积分</button><button class='btn alt' id='detailSub'>扣除积分</button></div>";$("detailAdd").onclick=()=>adjustCredits(id,1);$("detailSub").onclick=()=>adjustCredits(id,-1)}catch(e){$("detailCredits").textContent=e.message}}
|
||
async function adjustCredits(id,sign){const raw=prompt(sign>0?'增加多少积分?':'扣除多少积分?','10');if(raw===null)return;const amount=Math.floor(Number(raw));if(!Number.isFinite(amount)||amount<=0){alert('请输入正整数');return}const reason=prompt('调整原因(可选)','管理员调整积分');if(reason===null)return;try{const x=await api('/admin/users/'+id+'/credits',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({amount:sign*amount,reason})});alert('调整成功,当前可用积分:'+x.available);openUserDetail(id);loadAdmin()}catch(e){alert('积分调整失败:'+e.message)}}
|
||
async function resetUserPassword(id){const pass=prompt('输入新密码(至少 8 位):');if(pass===null)return;if(pass.length<8){alert('密码至少 8 位');return}try{await api('/admin/users/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({password:pass})});alert('密码已重置,该用户的其他登录会话已失效')}catch(e){alert('密码重置失败:'+e.message)}}
|
||
async function loadCreditLedger(){try{const u=await api('/admin/users');const users=u.data||[];let total=0;for(const x of users){try{const c=await api('/admin/users/'+x.id+'/credits');total+=Number(c.balance)||0}catch(e){}}$('adminCreditSummary').innerHTML='<div class="summary-card">用户<strong>'+users.length+'</strong></div><div class="summary-card">当前余额<strong>'+total+'</strong></div><div class="summary-card">单位<strong>积分</strong></div>';$('creditLedger').innerHTML='<div class="notice">积分流水按用户查看。进入「用户管理 → 查看详情」即可调整积分并查看该用户当前余额。</div>'}catch(e){$('adminCreditSummary').textContent=e.message}}
|
||
async function loadRedeemAdmin(){const b=await api('/admin/redeem');$('batches').innerHTML=(b.data||[]).map(x=>'<div class="notice">#'+x.id+' · '+x.credits+' 积分 × '+x.count+(x.note?' · '+esc(x.note):'')+' <button class="btn alt" onclick="revokeBatch('+x.id+')">撤销未使用</button></div>').join('')}
|
||
async function loadAuditAdmin(){const l=await api('/admin/audit');$('audit').innerHTML=(l.data||[]).map(x=>'<div class="ledger-item"><b>'+esc(x.action)+'</b><div class="meta">'+esc(x.targetType)+'#'+esc(x.targetId)+' · '+esc(x.metadata||'')+' · '+esc(x.createdAt)+'</div></div>').join('')||'<div class="empty">暂无日志</div>'}
|
||
async function toggleUser(id,status){const u=adminUsersCache.find(x=>x.id===id);status=status||(u&&u.status==='active'?'disabled':'active');try{await api('/admin/users/'+id,{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({status})});const u=adminUsersCache.find(x=>x.id===id);if(u)u.status=status;renderAdminUsers()}catch(e){alert('操作失败:'+e.message)}}
|
||
async function revokeBatch(id){try{await api('/admin/redeem/'+id+'/revoke',{method:'POST'});loadRedeemAdmin()}catch(e){alert('撤销失败:'+e.message)}}
|
||
|
||
function hideLogin(){document.getElementById('loginOverlay').style.display='none'}
|
||
async function initAuth(){try{const x=await api('/auth/me');const u=x.user;window.currentUser=u;window.isAdmin=u.role==='admin';const pill=document.getElementById('userPill');if(pill)pill.textContent=u.username+' · '+u.role;const s=document.getElementById('settings');if(s)s.style.display=u.role==='admin'?'':'none';const an=document.getElementById('adminNav');if(an)an.style.display=u.role==='admin'?'':'none';const mn=document.getElementById('mobileAdminNav');if(mn)mn.style.display=u.role==='admin'?'':'none';const ae=document.getElementById('accountAdminEntry');if(ae)ae.style.display=u.role==='admin'?'':'none';document.querySelectorAll('[data-view="settings"]').forEach(x=>x.style.display=u.role==='admin'?'':'none');const c=await api('/credits');if(pill)pill.textContent=u.username+' · '+u.role+' · 积分 '+c.balance+' / 可用 '+c.available;hideLogin();return true}catch(e){console.error('initAuth error:',e);showLogin();return false}}
|
||
document.querySelectorAll('[data-admin-view]').forEach(b=>b.addEventListener('click',()=>showAdminPanel('admin'+b.dataset.adminView.charAt(0).toUpperCase()+b.dataset.adminView.slice(1)+'Panel')));document.querySelectorAll('.admin-back').forEach(b=>b.addEventListener('click',()=>showAdminPanel('adminDashboard')));document.querySelectorAll('[data-role-filter]').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('[data-role-filter]').forEach(x=>x.classList.remove('active'));b.classList.add('active');adminRoleFilter=b.dataset.roleFilter;renderAdminUsers()}));
|
||
document.querySelectorAll('.admin-tab-btn').forEach(btn => {
|
||
btn.addEventListener('click', () => switchAdminTab(btn.dataset.adminTab));
|
||
});
|
||
if($('drawerBackdrop')) $('drawerBackdrop').addEventListener('click', closeUserDrawer);
|
||
if($('backToUsers')) $('backToUsers').addEventListener('click', closeUserDrawer);
|
||
|
||
$('loginForm').addEventListener('submit',async e=>{e.preventDefault();$('loginError').textContent='登录中…';try{const x=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:$('loginUser').value.trim(),password:$('loginPass').value})});if(!x.ok)throw new Error('用户名或密码错误');await initAuth();go(location.hash.slice(1)||'home');loadRecent();loadAccount()}catch(e){$('loginError').textContent=e.message}});
|
||
$('logoutBtn').addEventListener('click',async()=>{try{await api('/auth/logout',{method:'POST'})}finally{showLogin()}});
|
||
function esc(s){return String(s??'').replace(/[&<>"']/g,m=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[m]))}
|
||
function go(view){
|
||
if(view === 'settings') {
|
||
if(!window.isAdmin) { view = 'home'; }
|
||
else { view = 'admin'; switchAdminTab('settings'); }
|
||
} else if(view === 'admin' && !window.isAdmin){
|
||
view = 'home';
|
||
}document.querySelectorAll('.view').forEach(x=>x.classList.toggle('active',x.id===view));document.querySelectorAll('[data-view]').forEach(x=>x.classList.toggle('active',x.dataset.view===view));if(view==='settings')loadSettings();if(view==='account')loadAccount();if(view==='admin')loadAdmin();if(view==='queue')loadTasks();if(view==='home')loadRecent();history.replaceState(null,'','#'+view)}
|
||
document.querySelectorAll('[data-view]').forEach(b=>b.addEventListener('click',()=>go(b.dataset.view)));document.querySelectorAll('[data-go]').forEach(b=>b.addEventListener('click',()=>go(b.dataset.go)));
|
||
function qualityRank(q){return {standard:1,exhigh:2,sq:3,lossless:4,hires:5,jyeffect:6,sky:7,jymaster:8,atmos:9,atmos_plus:10,master:11}[q]||0}
|
||
function qualityName(q){return {standard:'标准',exhigh:'高品质',sq:'SQ',lossless:'无损',hires:'Hi-Res',jyeffect:'高清音效',sky:'Sky',jymaster:'母带',atmos:'Atmos',atmos_plus:'Atmos+',master:'Master'}[q]||q}
|
||
function pickQuality(s){const qs=Array.isArray(s.qualities)?s.qualities.map(x=>typeof x==='string'?{level:x}:x).filter(x=>x.level):[];const def=$('defaultQuality').value||'standard';if(qs.some(x=>x.level===def))return def;return qs.slice().sort((a,b)=>qualityRank(b.level)-qualityRank(a.level))[0]?.level||def}
|
||
async function runSearch(reset=true){const k=$('keyword').value.trim();if(!k)return;if(reset){searchPage=1;searchKeyword=k;searchSource=$('source').value;$('results').innerHTML=''}if(searchLoading)return;searchLoading=true;$('resultsCard').style.display='block';$('resultHint').textContent=reset?'搜索中…':'加载中…';$('loadMore').disabled=true;try{const x=await api('/search?keyword='+encodeURIComponent(searchKeyword)+'&source='+encodeURIComponent(searchSource)+'&page='+searchPage+'&limit=20');const songs=x.data?.songs||[];const seen=new Set([...$('results').querySelectorAll('[data-key]')].map(x=>x.dataset.key));songs.forEach(s=>{const key=String(s.rid||s.id||s.title+'|'+s.artist);if(seen.has(key))return;const el=document.createElement('div');el.className='result';el.dataset.key=key;const qs=Array.isArray(s.qualities)?s.qualities.map(x=>typeof x==='string'?{level:x}:x).filter(x=>x.level):[];const selected=pickQuality(s);const options=qs.length?qs.slice().sort((a,b)=>qualityRank(a.level)-qualityRank(b.level)).map(q=>'<option value="'+esc(q.level)+'" '+(q.level===selected?'selected':'')+'>'+esc(qualityName(q.level))+(q.format?' · '+esc(String(q.format).toUpperCase()):'')+'</option>').join(''):'';el.innerHTML='<img class="cover" src="'+esc(s.pic||'')+'" onerror="this.style.display=\'none\'"><div><div class="name">'+esc(s.title)+'</div><div class="meta">'+esc(s.artist||'')+' · '+esc(s.album||'')+'</div></div><div class="toolbar">'+(qs.length?'<select class="input quality-select">'+options+'</select>':'')+'<button class="btn">下载</button></div>';el.querySelector('button').addEventListener('click',()=>{const q=el.querySelector('.quality-select');queueSong(s,q?q.value:selected)});$('results').appendChild(el)});hasMore=songs.length>=20;$('resultHint').textContent='第 '+searchPage+' 页 · 已显示 '+$('results').children.length+' 首';$('loadMore').style.display=hasMore?'block':'none'}catch(e){$('resultHint').textContent='搜索失败:'+e.message}finally{searchLoading=false;$('loadMore').disabled=false}}
|
||
$('searchForm').addEventListener('submit',e=>{e.preventDefault();runSearch(true)});$('loadMore').addEventListener('click',()=>{searchPage++;runSearch(false)});$('homeSearch').addEventListener('submit',e=>{e.preventDefault();$('keyword').value=$('homeKeyword').value;$('source').value=$('homeSource').value;go('search');runSearch(true)});
|
||
async function queueSong(s,quality){s.quality=quality||pickQuality(s);try{await api('/tasks',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(s)});go('queue');await loadTasks()}catch(e){alert('加入失败:'+e.message)}}
|
||
function formatSize(n){n=Number(n)||0;if(n<1024)return n+' B';const units=['KB','MB','GB','TB'];let v=n/1024;for(let i=0;i<units.length;i++){if(v<1024||i===units.length-1)return v.toFixed(v>=100?0:v>=10?1:2)+' '+units[i];v/=1024}return n+' B'}
|
||
function taskHtml(t){let pct=Math.max(0,Math.min(100,Number(t.progress)||0));let status=t.status==='done'?'已完成':t.status==='error'?'失败':t.status==='pending'?'等待中':t.status==='resolving'?'解析中':t.status==='downloading'?'下载中':t.status==='uploading'?'上传中':esc(t.status);let actual=(t.format||'').toUpperCase();let requested=qualityName(t.quality||'');let actualQ=t.actualQuality?qualityName(t.actualQuality):'';let apiQ=t.resolvedQuality?qualityName(t.resolvedQuality):'';let action=t.status==='error'?'<button class="btn alt retry">重试</button>':'';let highLossless=t.quality==='lossless'||t.quality==='hires';let mismatch=(actualQ&&actualQ!==requested)||(highLossless&&actual!=='FLAC');let info=t.status==='done'?`<div class="file-info"><span class="chip actual">格式 ${esc(actual||'未知')}</span><span class="chip">大小 ${esc(formatSize(t.totalBytes||t.downloadedBytes))}</span>${actualQ&&!mismatch?`<span class="chip actual">实际 ${esc(actualQ)}</span>`:''}${mismatch?`<span class="chip warn">请求 ${esc(requested)} · 实际 ${esc(actualQ||actual||'未知')}${apiQ&&apiQ!==actualQ?' · API '+esc(apiQ):''}</span>`:''}</div>`:'';return `<div class="result"><img class="cover" src="${esc(t.pic||'')}" onerror="this.style.display='none'"><div><div class="name">${esc(t.title)} <span class="muted">${esc(t.artist||'')}</span></div><div class="meta">${esc(t.source)} · 请求 ${esc(requested)} · ${status}${t.status==='error'?' · '+esc(t.error||''):''}</div>${info}${t.status==='downloading'||t.status==='resolving'?`<div class="progress"><i style="width:${pct}%"></i></div>`:''}</div>${action}</div>`}
|
||
async function loadTasks(){try{const x=await api('/tasks');const all=x.data||[];const active=all.filter(t=>['pending','resolving','downloading'].includes(t.status));$('navBadge').textContent=active.length;$('mobileBadge').textContent=active.length;let arr=taskFilter==='active'?active:taskFilter==='done'?all.filter(t=>t.status==='done'):taskFilter==='error'?all.filter(t=>t.status==='error'):all;$('tasks').innerHTML=arr.length?arr.map(taskHtml).join(''):'<div class="empty">暂无任务</div>';$('tasks').querySelectorAll('.retry').forEach((b,i)=>b.addEventListener('click',async()=>{const errs=arr.filter(t=>t.status==='error');await api('/tasks/'+errs[i].id+'/retry',{method:'POST'});loadTasks()}))}catch(e){$('tasks').innerHTML='<div class="empty">加载失败:'+esc(e.message)+'</div>'}}
|
||
async function loadRecent(){try{const x=await api('/tasks');const a=(x.data||[]).slice(0,5);$('recentTasks').innerHTML=a.length?a.map(taskHtml).join(''):'<div class="empty">还没有下载记录</div>'}catch(e){$('recentTasks').innerHTML='<div class="empty">加载失败</div>'}}
|
||
document.querySelectorAll('#taskTabs button').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('#taskTabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');taskFilter=b.dataset.filter;loadTasks()}));
|
||
async function loadSettings(){try{const s=await api('/admin/settings');$('amApi').value=s.amApi||'';$('downloadDir').value=s.downloadDir||'';$('localFilePolicy').value=s.localFilePolicy||'keep';$('davUrl').value=s.webdavUrl||'';$('davUser').value=s.webdavUser||'';$('davPass').value=s.webdavPass||'';$('davPath').value=s.webdavPath||'';$('defaultQuality').value=s.quality||'standard';$('davVersionPolicy').value=s.webdavVersionPolicy||'keep'}catch(e){alert('设置加载失败:'+e.message)}}
|
||
$('saveSettings').addEventListener('click',async()=>{const b={amApi:$('amApi').value,downloadDir:$('downloadDir').value,localFilePolicy:$('localFilePolicy').value,webdavUrl:$('davUrl').value,webdavUser:$('davUser').value,webdavPass:$('davPass').value,webdavPath:$('davPath').value,quality:$('defaultQuality').value,webdavVersionPolicy:$('davVersionPolicy').value};try{await api('/admin/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});alert('设置已保存')}catch(e){alert('保存失败:'+e.message)}});
|
||
$('testDav').addEventListener('click',async()=>{try{const b={url:$('davUrl').value,user:$('davUser').value,pass:$('davPass').value,path:$('davPath').value};await api('/admin/webdav/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});alert('WebDAV 连接成功')}catch(e){alert('WebDAV 连接失败:'+e.message)}});
|
||
$('redeemBtn').addEventListener('click',async()=>{try{const x=await api('/redeem',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({code:$('redeemCode').value})});alert('兑换成功:+'+x.credits+' 积分');$('redeemCode').value='';loadAccount()}catch(e){alert('兑换失败:'+e.message)}});
|
||
$('saveUserDav').addEventListener('click',async()=>{try{await api('/webdav/config',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:true,url:$('userDavUrl').value,username:$('userDavUser').value,password:$('userDavPass').value,rootPath:$('userDavRoot').value})});alert('WebDAV 已保存')}catch(e){alert('保存失败:'+e.message)}});
|
||
$('testUserDav').addEventListener('click',async()=>{try{await api('/webdav/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({url:$('userDavUrl').value,username:$('userDavUser').value,password:$('userDavPass').value,rootPath:$('userDavRoot').value})});alert('连接成功')}catch(e){alert('连接失败:'+e.message)}});
|
||
$('createUser').addEventListener('click',async()=>{try{await api('/admin/users',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:$('newUser').value,password:$('newPass').value,role:$('newRole').value,credits:Number($('newCredits').value||0)})});$('newUser').value='';$('newPass').value='';loadAdmin()}catch(e){alert('创建失败:'+e.message)}});
|
||
$('createBatch').addEventListener('click',async()=>{try{const x=await api('/admin/redeem',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({credits:Number($('batchCredits').value),count:Number($('batchCount').value),note:$('batchNote').value})});$('batchOutput').textContent=x.codes.join('\n');loadAdmin()}catch(e){alert('生成失败:'+e.message)}});
|
||
$('loadAudit').addEventListener('click',loadAdmin);
|
||
initAuth().then(ok=>{if(ok){go(location.hash.slice(1)||'home');loadRecent();loadAccount()}});setInterval(()=>{loadTasks();if(document.getElementById('home').classList.contains('active'))loadRecent()},3000);
|
||
|
||
</script>
|
||
</body>
|
||
</html> |