Files
StarlightRadio/public/index.html
T
Xiao-no-love 16df0a7d08 v2.4: UI 规范化(token体系) + 复制直播链接 + 设置页间距
- 建立完整 token 体系: 色板/字号阶梯(9档)/圆角阶梯/阴影/动效/间距
- 字号 42→9 种、圆角 20→8 种、正文硬编码色清零
- 补丁区(v2/v2.1/v2.2 追加段)并入主体系: 统一缩进 + 全用 token
- header「打开直播流」改为「复制直播流链接」(navigator.clipboard + 降级)
- header 图标按钮统一为胶囊形(20px 圆角)
- 设置页: .page.active 加 gap, section-body 内边距对齐, 缓解拥挤

测试: api 16/16, e2e 17/17, write 5/5 全绿
2026-09-24 22:20:55 +08:00

515 lines
24 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover, maximum-scale=1"
/>
<meta name="theme-color" content="#f4f5f9" />
<title>Xiao の 电台</title>
<link rel="icon" href="/favicon.ico" type="image/x-icon" />
<link rel="stylesheet" href="vendor/bootstrap.min.css" />
<link rel="stylesheet" href="vendor/bootstrap-icons.css" />
<script src="vendor/Sortable.min.js"></script>
<script src="vendor/hls.min.js"></script>
<link rel="stylesheet" href="css/style.css" />
</head>
<body>
<div class="page-bg"></div>
<div class="bg-shapes">
<div class="s"></div>
<div class="s"></div>
<div class="s"></div>
</div>
<div class="toast-container" id="toastContainer"></div>
<!-- Header -->
<header class="top-header">
<div class="header-inner">
<div class="brand">
<div class="brand-icon">
<img src="favicon.png" alt="logo" />
</div>
<span>Xiao の 电台</span>
<span class="live-pill"
><span class="live-dot"></span>LIVE</span
>
</div>
<div class="header-actions">
<span class="cache-pill" id="cacheInfo"><i class="bi bi-database"></i> --</span>
<button class="channel-btn" id="btnStreamMgr" title="频道与切换"><i class="bi bi-broadcast"></i><span id="currentChannelName">主电台</span></button>
<button class="btn-link-sm" id="liveLink" title="复制直播流链接"
><i class="bi bi-link-45deg"></i></button
>
<button class="btn-link-sm" id="btnLogout" title="退出登录"
><i class="bi bi-box-arrow-right"></i></button>
</div>
</div>
</header>
<!-- Pages -->
<div class="page-container">
<!-- 0. Live 收听 -->
<div class="page active" id="pageLive">
<div class="live-card">
<div class="live-now-playing">
<div class="live-cover-wrap">
<img class="live-cover" id="liveCover" src="" alt="" />
<div class="live-cover-ph" id="liveCoverPh">
<i class="bi bi-broadcast"></i>
</div>
<span class="live-badge"><span class="live-dot"></span>LIVE</span>
</div>
<div class="live-meta">
<div class="live-station" id="liveStation">主电台</div>
<div class="live-now" id="liveNow">正在加载直播流...</div>
<div class="live-sub" id="liveSub">--</div>
</div>
</div>
<div class="live-controls">
<button class="live-play-btn" id="livePlayBtn"><i class="bi bi-play-fill"></i></button>
<div class="live-progress-wrap">
<div class="live-progress-bar"><div class="live-progress-fill" id="liveProgressFill"></div></div>
<div class="live-status" id="liveStatus">未连接</div>
</div>
</div>
<div class="live-hint">
<i class="bi bi-info-circle"></i>
点击播放按钮收听 <b id="liveHintStation">主电台</b> 的实时直播流
</div>
</div>
</div>
<!-- 1. Search -->
<div class="page" id="pageSearch">
<div class="search-card">
<div class="search-header">
<h2>导入音源</h2>
<p>输入网易云歌曲 ID、链接或歌名查询并下载</p>
</div>
<div class="search-input-wrap">
<div class="search-field">
<input
type="text"
id="musicId"
placeholder="歌曲 ID、链接或歌名,如 33894312 / 知我"
inputmode="text"
autocomplete="off"
spellcheck="false"
/>
<button class="btn-search" id="btnSearch">
查询
</button>
</div>
</div>
<div class="parse-hint" id="parseHint">
<i class="bi bi-magic"></i>
已自动解析为 ID:<code id="parsedId"></code>
</div>
<!-- Preview: info only -->
<div id="musicPreview" style="display: none">
<div class="preview-card">
<div class="preview-cover-section">
<div class="preview-cover-wrap">
<img
id="musicCover"
class="preview-cover"
src=""
alt=""
/>
</div>
<div class="preview-info">
<div
class="preview-title"
id="musicName"
></div>
<div
class="preview-artist"
id="musicArtist"
></div>
</div>
</div>
<div
class="preview-details"
id="musicDetails"
></div>
<div class="preview-actions">
<button class="btn-download" id="btnDownload">
<i class="bi bi-download"></i> 下载到资源库
</button>
</div>
</div>
</div>
<div
id="searchResults"
class="searchResults"
style="display: none"
></div>
<div class="search-tips" id="searchTips">
<div class="tips-icon">
<i class="bi bi-music-note-beamed"></i>
</div>
<div class="tips-title">搜索并下载音乐</div>
<div class="tips-desc">
在网易云音乐中复制歌曲 ID 或完整链接<br />粘贴到上方输入框即可自动识别
</div>
</div>
<!-- Import local audio -->
<div class="import-card" id="importCard">
<div class="import-header">
<h3><i class="bi bi-upload"></i> 本地导入</h3>
<p>上传音频文件,自动识别作者与封面(封面存本地)</p>
</div>
<input type="file" id="importFile" accept="audio/*,.mp3,.m4a,.flac,.wav,.aac,.ogg" hidden />
<input type="text" id="importTitle" class="import-input" placeholder="歌曲名(选填,留空则用文件名)" />
<input type="text" id="importArtist" class="import-input" placeholder="作者(选填,留空自动匹配)" />
<button class="btn-download" id="btnImport"><i class="bi bi-upload"></i> 选择文件并导入</button>
<div class="import-progress" id="importProgress"></div>
</div>
</div>
</div>
<!-- 2. Resources -->
<div class="page" id="pageResources">
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i
class="bi bi-folder2-open"
style="color: var(--accent)"
></i>
已有音乐
<span class="section-count" id="resourceCount"
>0</span
>
</div>
<div class="resource-tools">
<select class="resource-sort" id="resourceSort" title="排序方式">
<option value="created_desc">加入时间 ↓</option>
<option value="created_asc">加入时间 ↑</option>
<option value="heat_desc">热度 ↓</option>
<option value="heat_asc">热度 ↑</option>
<option value="name_asc">名称 ↑</option>
<option value="name_desc">名称 ↓</option>
</select>
<input
type="text"
class="resource-search"
id="resourceSearch"
placeholder="搜索已有音乐..."
autocomplete="off"
spellcheck="false"
/>
</div>
</div>
<div class="section-body" id="resourceList">
<div class="empty-state">
<div class="empty-icon">
<i class="bi bi-arrow-repeat"></i>
</div>
<div class="empty-title">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 3. Playlist -->
<div class="page" id="pagePlaylist">
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i
class="bi bi-music-note-list"
style="color: var(--accent)"
></i>
播放列表
<span class="section-count" id="playlistCount"
>0</span
>
</div>
<button class="btn-clear" id="btnClear">
<i class="bi bi-trash3"></i> 清空
</button>
</div>
<div class="section-body" id="playlistList">
<div class="empty-state">
<div class="empty-icon">
<i class="bi bi-arrow-repeat"></i>
</div>
<div class="empty-title">加载中...</div>
</div>
</div>
</div>
</div>
<!-- 5. Settings -->
<div class="page" id="pageSettings">
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i class="bi bi-database" style="color: var(--warning)"></i>
缓存管理
</div>
<button class="btn-clear" id="btnCleanup"><i class="bi bi-trash3"></i> 立即清理</button>
</div>
<div class="section-body">
<div class="cache-stat">
<div class="cache-stat-row">
<span>已用 / 上限</span>
<b id="setCacheText">-- / --</b>
</div>
<div class="cache-bar"><div class="cache-bar-fill" id="setCacheBar"></div></div>
<div class="cache-stat-note" id="setCacheNote">超限时自动按热度 LRU 淘汰</div>
</div>
</div>
</div>
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i class="bi bi-gear" style="color: var(--accent)"></i>
运行配置
</div>
</div>
<div class="section-body">
<div class="config-grid" id="setConfigGrid"></div>
</div>
</div>
<div class="section-card">
<div class="section-header">
<div class="section-title">
<i class="bi bi-file-text" style="color: var(--success)"></i>
运行日志
</div>
<select class="log-tail-select" id="logTail">
<option value="50">50 行</option>
<option value="100" selected>100 行</option>
<option value="200">200 行</option>
<option value="500">500 行</option>
</select>
</div>
<div class="section-body">
<pre class="log-view" id="logView">加载中...</pre>
</div>
</div>
</div>
<!-- 4. About — 保持原有风格体系 -->
<div class="page" id="pageAbout">
<div class="about-card">
<!-- Hero -->
<div class="about-hero">
<div class="about-logo">
<img src="favicon.png" alt="logo" />
</div>
<h2>Xiao の 电台</h2>
<p>基于 HLS 的个人在线音乐电台</p>
</div>
<!-- Body -->
<div class="about-body">
<!-- 项目简介 -->
<div class="about-section">
<div class="about-section-icon">
<i class="bi bi-book"></i>
</div>
<div class="about-section-content">
<h3>项目简介</h3>
<p>
一个基于 Web 的个人在线音乐电台,支持通过网易云音乐歌曲 ID、链接或歌名进行搜索、试听与下载,提供 HLS 直播流播放、资源管理与播放列表拖拽排序等功能。界面采用简约玻璃拟态风格,适配移动端与桌面端多种屏幕尺寸。
</p>
</div>
</div>
<!-- 核心功能 -->
<div class="about-section">
<div class="about-section-icon">
<i class="bi bi-lightning-charge"></i>
</div>
<div class="about-section-content">
<h3>核心功能</h3>
<div class="about-feature-list">
<div class="about-feat">
<i class="bi bi-search" style="color: var(--accent)"></i>
<span>智能搜索 — 歌曲 ID、链接或歌名关键词,自动解析并一键导入</span>
</div>
<div class="about-feat">
<i class="bi bi-broadcast" style="color: var(--success)"></i>
<span>直播流 — FFmpeg HLS 切片 + 滑动窗口,5 秒精度连续输出</span>
</div>
<div class="about-feat">
<i class="bi bi-database-check" style="color: var(--warning)"></i>
<span>智能缓存 — 预生成切片、增量追踪、LRU 淘汰与自动清理</span>
</div>
<div class="about-feat">
<i class="bi bi-hand-index-thumb" style="color: #8b5cf6"></i>
<span>播放列表 — 拖拽排序、添加/移除/跳转,SSE 实时状态推送</span>
</div>
<div class="about-feat">
<i class="bi bi-phone" style="color: #06b6d4"></i>
<span>全端适配 — 玻璃拟态 UI,手势滑动、键盘快捷键、拖拽进度条</span>
</div>
</div>
</div>
</div>
<!-- 技术栈 -->
<div class="about-section">
<div class="about-section-icon">
<i class="bi bi-tools"></i>
</div>
<div class="about-section-content">
<h3>技术栈</h3>
<div class="tech-tags">
<span>HTML5</span>
<span>CSS3</span>
<span>JavaScript ES6+</span>
<span>Bootstrap 5</span>
<span>SortableJS</span>
<span>HLS.js</span>
<span>Node.js</span>
<span>Express</span>
<span>FFmpeg</span>
<span>Chokidar</span>
</div>
</div>
</div>
<!-- 开发语言 -->
<div class="about-section">
<div class="about-section-icon">
<i class="bi bi-translate"></i>
</div>
<div class="about-section-content">
<h3>开发语言</h3>
<p>前端:HTML / CSS / JavaScript &nbsp;|&nbsp; 后端:Node.js</p>
</div>
</div>
<!-- 作者 -->
<div class="about-section">
<div class="about-section-icon">
<i class="bi bi-person"></i>
</div>
<div class="about-section-content">
<h3>作者</h3>
<p style="margin-bottom: 0.45rem">Xiao — 独立开发者</p>
<a
href="https://github.com/Xiao-no-love/"
target="_blank"
rel="noopener"
class="about-gh-link"
>
<i class="bi bi-github"></i>
<span>Xiao-no-love</span>
<i class="bi bi-box-arrow-up-right" style="font-size: 0.6rem; opacity: 0.5"></i>
</a>
</div>
</div>
<!-- 免责声明 -->
<div class="about-section disclaimer-section">
<div class="about-section-icon">
<i class="bi bi-exclamation-triangle"></i>
</div>
<div class="about-section-content">
<h3>免责声明</h3>
<p>
本项目所有音源均来自网易云音乐,仅供个人学习与技术研究使用,严禁用于任何商业用途。音乐版权归原创作者及网易云音乐平台所有。如有侵权,请联系删除。请于下载后 24 小时内清除,支持正版音乐。
</p>
</div>
</div>
</div>
<!-- Footer -->
<div class="about-version">
v2.0.0 &middot; Built with &hearts; &middot;
<a
href="https://github.com/Xiao-no-love/"
target="_blank"
rel="noopener"
style="color: var(--accent); text-decoration: none"
>GitHub</a>
</div>
</div>
</div>
</div>
<!-- 频道管理模态框 -->
<div class="modal-mask" id="streamModal">
<div class="modal-box">
<div class="modal-head">
<h3><i class="bi bi-sliders"></i> 频道管理</h3>
<button class="modal-close" id="streamModalClose" title="关闭"><i class="bi bi-x-lg"></i></button>
</div>
<div class="modal-body">
<div class="stream-list" id="streamMgrList"></div>
<div class="stream-create">
<input type="text" id="newStreamId" placeholder="频道 ID(字母数字_-,如 chill)" autocomplete="off" spellcheck="false" />
<input type="text" id="newStreamName" placeholder="显示名称(选填)" autocomplete="off" />
<button id="btnCreateStream"><i class="bi bi-plus-lg"></i> 新建频道</button>
</div>
</div>
</div>
</div>
<!-- Mini Player — full background progress + drag seek -->
<div class="mini-player" id="miniPlayer">
<div class="mp-progress" id="mpProgress"></div>
<div class="mp-body" id="mpBody">
<img
class="mp-cover"
id="mpCover"
src=""
alt=""
onerror="this.style.display = 'none'"
/>
<div class="mp-info">
<div class="mp-name" id="mpName">未在播放</div>
<div class="mp-artist" id="mpArtist">--</div>
</div>
<div class="mp-controls">
<span class="mp-time" id="mpTime">0:00</span>
<button class="mp-play" id="mpPlay">
<i class="bi bi-play-fill"></i>
</button>
<button class="mp-expand" id="mpExpand" title="查看详情">
<i class="bi bi-chevron-up"></i>
</button>
</div>
</div>
</div>
<!-- Bottom Nav — 4 tabs, full-width aligned with content -->
<nav class="bottom-nav">
<div class="bottom-nav-inner">
<button class="nav-tab active" data-page="pageLive">
<i class="bi bi-broadcast"></i><span>收听</span>
</button>
<button class="nav-tab" data-page="pageSearch">
<i class="bi bi-search"></i><span>搜索</span>
</button>
<button class="nav-tab" data-page="pageResources">
<i class="bi bi-folder"></i><span>资源</span>
</button>
<button class="nav-tab" data-page="pagePlaylist">
<i class="bi bi-list-ul"></i><span>列表</span>
</button>
<button class="nav-tab" data-page="pageSettings">
<i class="bi bi-gear"></i><span>设置</span>
</button>
<button class="nav-tab" data-page="pageAbout">
<i class="bi bi-info-circle"></i><span>关于</span>
</button>
</div>
</nav>
<!-- Global audio element -->
<audio id="musicPlayer" preload="none" style="display: none"></audio>
<script src="js/app.js"></script>
</body>
</html>