-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathviewer-template.js
More file actions
108 lines (108 loc) · 9.66 KB
/
Copy pathviewer-template.js
File metadata and controls
108 lines (108 loc) · 9.66 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
// 生成"导出手机版(只读)"单文件 HTML 的模板。
// 独立成文件是因为这段是内嵌的完整迷你网页(字符串模板),体积大、和主 App 逻辑无关,单独维护更清楚。
// 纯函数:接收歌曲数组,不依赖任何全局状态。
function buildViewerHTML(songList) {
const data = songList.map(s => ({ title: s.title || '', artist: s.artist || '', lyrics: s.lyrics || '' }));
const json = JSON.stringify(data).replace(/<\/script>/gi, '<\\/script>');
return `<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" content="#145C3C">
<title>歌词集</title>
<style>
:root{--vermillion:#A32024;--sapphire:#16324F;--green:#145C3C;--gold:#C9A227;--bg:#FAF8F1;--card:#FFF;--seal-text:#FDF6E8;--title-text:#F7F1E3;--count-text:#D9EFE1;--text:#2C2A24;--text-dim:#9A9384;--placeholder:#B7AF9C;--border:#D8CDB4;--border2:#EEE8D6;--arrow:#B7AF9C;--back-detail:#8A7A63;--font-serif:"Noto Serif SC","Songti SC","STSong","SimSun",serif;--side-w:28px;--st:env(safe-area-inset-top,0px);--sb:env(safe-area-inset-bottom,0px);}
*{box-sizing:border-box;-webkit-tap-highlight-color:transparent;}
html,body{margin:0;padding:0;height:100%;background:#E7E0D2;color:var(--text);font-family:var(--font-serif);font-weight:400;line-height:1.5;overscroll-behavior-y:none;}
.app{width:100%;max-width:480px;margin:0 auto;height:100vh;height:100dvh;background:var(--bg);position:relative;overflow:hidden;}
.page{display:none;height:100%;}.page.active{display:block;}
.with-frame{display:flex;height:100%;}
.side-bar{width:var(--side-w);flex-shrink:0;background:var(--vermillion);}
.frame-main{flex:1;min-width:0;height:100%;display:flex;flex-direction:column;}
.top-bar{height:calc(104px + var(--st));flex-shrink:0;background:var(--green);border-bottom:2px solid var(--gold);padding:calc(14px + var(--st)) 18px 12px;display:flex;flex-direction:column;}
.seal{width:28px;height:28px;border-radius:3px;flex-shrink:0;background:var(--vermillion);border:1px solid var(--gold);color:var(--seal-text);font-size:16px;font-weight:500;display:flex;align-items:center;justify-content:center;line-height:1;}
.tb-row1{display:flex;align-items:center;gap:9px;}
.tb-title{font-size:18px;font-weight:500;color:var(--title-text);letter-spacing:2px;}
.tb-count{margin-left:auto;font-size:10px;color:var(--count-text);letter-spacing:1px;}
.tb-search{margin-top:12px;}
.tb-search input{width:100%;height:30px;border-radius:15px;border:none;background:var(--title-text);padding:0 14px;font-size:12px;color:var(--text);outline:none;font-family:var(--font-serif);}
.tb-search input::placeholder{color:var(--placeholder);}
.list-scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:12px 14px calc(16px + var(--sb));}
.card{background:var(--card);border-radius:6px;height:56px;margin-bottom:10px;display:flex;align-items:stretch;overflow:hidden;cursor:pointer;box-shadow:0 1px 2px rgba(60,50,30,.05);border:1px solid var(--border2);}
.card .bar{width:12px;flex-shrink:0;}
.card .card-body{flex:1;min-width:0;padding:0 12px 0 9px;display:flex;flex-direction:column;justify-content:center;}
.c-name{font-size:14px;font-weight:500;color:var(--text);letter-spacing:.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.c-artist{font-size:10px;color:var(--text-dim);margin-top:3px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.empty{text-align:center;color:var(--text-dim);padding:60px 24px;font-size:13px;line-height:2;}
#detailPage{background:var(--bg);}#detailPage.active{display:flex;flex-direction:column;}
.detail-nav{padding:calc(14px + var(--st)) 18px 12px;position:relative;flex-shrink:0;}
.d-back{background:none;border:none;color:var(--back-detail);font-size:12px;cursor:pointer;font-family:var(--font-serif);padding:2px 4px;position:absolute;left:14px;top:calc(14px + var(--st));z-index:2;}
.d-center{text-align:center;padding:0 40px;}
.d-title{font-size:15px;font-weight:500;color:var(--text);letter-spacing:1px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;}
.d-artist{font-size:11px;color:var(--text-dim);margin-top:3px;}
.d-divider{height:1px;background:var(--border);}
.lyrics-area{flex:1;display:flex;flex-direction:column;min-height:0;}
.scroll-hint{text-align:center;color:var(--arrow);font-size:11px;padding:5px 0;flex-shrink:0;user-select:none;}
.scroll-hint.hidden{visibility:hidden;}
.lyrics-scroll{flex:1;overflow-y:auto;-webkit-overflow-scrolling:touch;padding:6px 24px;}
.lyric-line{text-align:center;font-size:12px;line-height:30px;letter-spacing:2px;color:var(--text);}
.lyrics-empty{text-align:center;color:var(--placeholder);font-size:12px;padding:40px 0;font-style:italic;}
.control-area{display:flex;align-items:center;justify-content:center;gap:30px;padding:14px 20px calc(18px + var(--sb));flex-shrink:0;border-top:1px solid var(--border);}
.ctrl-btn{width:46px;height:46px;border-radius:50%;border:none;color:#fff;font-size:22px;cursor:pointer;flex-shrink:0;display:flex;align-items:center;justify-content:center;line-height:1;padding:0;}
.ctrl-btn.prev{background:var(--sapphire);}.ctrl-btn.next{background:var(--vermillion);}
.ctrl-btn:active{opacity:.82;}.ctrl-btn:disabled{opacity:.3;cursor:default;}
.ctrl-index{font-size:10px;color:var(--arrow);min-width:46px;text-align:center;letter-spacing:1px;}
</style>
</head>
<body>
<div class="app">
<div class="page active" id="listPage">
<div class="with-frame"><div class="side-bar"></div><div class="frame-main">
<div class="top-bar">
<div class="tb-row1"><div class="seal">詞</div><div class="tb-title">歌词集</div><div class="tb-count" id="count"></div></div>
<div class="tb-search"><input id="searchInput" placeholder="搜歌名 / 歌手 / 歌词片段" autocomplete="off"></div>
</div>
<div class="list-scroll" id="list"></div>
</div></div>
</div>
<div class="page" id="detailPage">
<div class="detail-nav"><button class="d-back" id="detailBack">‹ 返回</button><div class="d-center"><div class="d-title" id="dTitle"></div><div class="d-artist" id="dArtist"></div></div></div>
<div class="d-divider"></div>
<div class="lyrics-area"><div class="scroll-hint" id="hintTop">▲</div><div class="lyrics-scroll" id="lyricsScroll"></div><div class="scroll-hint" id="hintBottom">▼</div></div>
<div class="d-divider"></div>
<div class="control-area"><button class="ctrl-btn prev" id="prevBtn">‹</button><div class="ctrl-index" id="ctrlIndex"></div><button class="ctrl-btn next" id="nextBtn">›</button></div>
</div>
</div>
<script>
var SONGS=${json};var BAR=['var(--vermillion)','var(--sapphire)','var(--green)'];
var order=[],curIdx=-1,listTop=0;
function norm(s){return (s||'').trim().toLowerCase();}
function esc(s){return (s||'').replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function showPage(id){var ps=document.querySelectorAll('.page');for(var i=0;i<ps.length;i++)ps[i].classList.remove('active');document.getElementById(id).classList.add('active');}
function render(f){var q=norm(f||'');var listEl=document.getElementById('list');var items=[];for(var i=0;i<SONGS.length;i++){var s=SONGS[i];if(!q||norm(s.title).indexOf(q)>=0||norm(s.artist).indexOf(q)>=0||norm(s.lyrics).indexOf(q)>=0)items.push(i);}
order=items;document.getElementById('count').textContent=q?('找到 '+items.length+' 首'):('共 '+SONGS.length+' 首');
if(!items.length){listEl.innerHTML='<div class="empty">'+(SONGS.length?'没有匹配的歌曲':'(空)')+'</div>';return;}
var html='';for(var k=0;k<items.length;k++){var idx=items[k];var s=SONGS[idx];html+='<div class="card" data-idx="'+idx+'"><span class="bar" style="background:'+BAR[k%3]+'"></span><div class="card-body"><div class="c-name">'+esc(s.title||'(无名)')+'</div><div class="c-artist">'+esc(s.artist||'')+'</div></div></div>';}
listEl.innerHTML=html;
var cs=listEl.querySelectorAll('.card');for(var r=0;r<cs.length;r++){(function(el){el.onclick=function(){listTop=listEl.scrollTop;openDetail(parseInt(el.getAttribute('data-idx'),10));};})(cs[r]);}}
function openDetail(idx){var s=SONGS[idx];if(!s)return;curIdx=idx;
document.getElementById('dTitle').textContent=s.title||'(无名)';document.getElementById('dArtist').textContent=s.artist||'';
var scroll=document.getElementById('lyricsScroll');var lines=(s.lyrics||'').split('\\n');var has=false;for(var i=0;i<lines.length;i++)if(lines[i].trim()){has=true;break;}
if(!has){scroll.innerHTML='<div class="lyrics-empty">暂无歌词</div>';}else{var h='';for(var j=0;j<lines.length;j++)h+='<div class="lyric-line">'+(esc(lines[j])||' ')+'</div>';scroll.innerHTML=h;}
scroll.scrollTop=0;
var pos=order.indexOf(idx);document.getElementById('ctrlIndex').textContent=(pos+1)+' / '+order.length;
document.getElementById('prevBtn').disabled=pos<=0;document.getElementById('nextBtn').disabled=pos>=order.length-1;
showPage('detailPage');requestAnimationFrame(hints);}
function goto(d){var pos=order.indexOf(curIdx);var t=pos+d;if(t<0||t>=order.length)return;openDetail(order[t]);}
function hints(){var s=document.getElementById('lyricsScroll');var can=s.scrollHeight>s.clientHeight+2;document.getElementById('hintTop').classList.toggle('hidden',!can||s.scrollTop<=1);document.getElementById('hintBottom').classList.toggle('hidden',!can||s.scrollTop+s.clientHeight>=s.scrollHeight-1);}
document.getElementById('lyricsScroll').onscroll=hints;
document.getElementById('detailBack').onclick=function(){showPage('listPage');document.getElementById('list').scrollTop=listTop;};
document.getElementById('prevBtn').onclick=function(){goto(-1);};
document.getElementById('nextBtn').onclick=function(){goto(1);};
document.getElementById('searchInput').oninput=function(e){render(e.target.value);};
render('');
<\/script>
</body>
</html>`;
}