release: v0.4.0
This commit is contained in:
@@ -1,923 +0,0 @@
|
||||
<!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" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>CloudSearch - 搜索</title>
|
||||
<script>
|
||||
// 替换标题为网站名称
|
||||
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){
|
||||
if(cfg.site_name) document.title = cfg.site_name + ' - 搜索';
|
||||
}).catch(function(){});
|
||||
</script>
|
||||
<script src="/h5/qrcode.min.js"></script>
|
||||
<style>
|
||||
/* ===== Reset & Base ===== */
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html{font-size:16px;-webkit-text-size-adjust:100%}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;background:#f5f5f5;color:#303133;min-height:100vh;overflow-x:hidden}
|
||||
:root{--primary:#409eff;--primary-dark:#337ecc;--primary-light:rgba(64,158,255,0.08);--text:#303133;--text2:#909399;--border:#ebeef5;--bg:#f5f5f5;--white:#fff;--radius:10px;--shadow:0 1px 4px rgba(0,0,0,0.04);--safe-bottom:env(safe-area-inset-bottom,0px)}
|
||||
a{color:var(--primary);text-decoration:none}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
/* ===== Home Page ===== */
|
||||
.home-page{padding-bottom:calc(30px + var(--safe-bottom))}
|
||||
.home-hero{display:flex;flex-direction:column;align-items:center;padding:36px 16px 20px}
|
||||
.home-logo{font-size:32px;font-weight:700;color:var(--primary);margin-bottom:20px;text-align:center}
|
||||
.home-logo-img{max-width:360px;max-height:80px;width:auto;height:auto;object-fit:contain}
|
||||
.home-search-box{display:flex;width:100%;max-width:500px;border:1px solid var(--border);border-radius:20px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.home-search-box:focus-within{border-color:var(--primary);background:var(--white);box-shadow:0 0 0 3px rgba(64,158,255,.1)}
|
||||
.home-search-box input{flex:1;height:40px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.home-search-box button{flex-shrink:0;height:32px;margin:4px;padding:0 22px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer}
|
||||
.home-search-box button:active{background:var(--primary-dark)}
|
||||
.home-quote{margin-top:12px;font-size:12px;color:#b0b8c4;font-style:italic;text-align:center;max-width:500px;line-height:1.5}
|
||||
.home-quote-author{font-size:11px;color:#c0c4cc;display:inline-block;margin-top:2px}
|
||||
|
||||
/* ===== Home Rankings ===== */
|
||||
.home-rankings{padding:8px 12px;display:flex;flex-direction:column;gap:10px}
|
||||
.rank-block{background:var(--white);border-radius:var(--radius);padding:12px;border:1px solid var(--border);box-shadow:var(--shadow)}
|
||||
.rank-block-hdr{display:flex;align-items:center;justify-content:space-between;padding-bottom:8px;border-bottom:2px solid #f0f0f0;margin-bottom:4px}
|
||||
.rank-block-title{font-size:14px;font-weight:700;color:var(--text);white-space:nowrap}
|
||||
.rank-block-tabs{display:flex;gap:2px;background:#f0f2f5;border-radius:5px;padding:2px}
|
||||
.rank-tab{font-size:11px;padding:2px 9px;border-radius:4px;cursor:pointer;color:#909399;font-weight:500;transition:all .2s;user-select:none}
|
||||
.rank-tab.active{background:var(--white);color:var(--primary);font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
||||
.rank-item{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:6px;cursor:pointer;transition:background .15s}
|
||||
.rank-item:active{background:#f0f5ff}
|
||||
.rank-idx{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#909399;background:#f0f0f0;flex-shrink:0}
|
||||
.rank-idx.top3{background:var(--primary);color:var(--white);font-size:12px}
|
||||
.rank-name{flex:1;min-width:0;font-size:13px;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rank-cnt{font-size:11px;color:#c0c4cc;white-space:nowrap;flex-shrink:0}
|
||||
.rank-expand{text-align:center;padding:5px;margin-top:2px;font-size:12px;color:var(--primary);cursor:pointer;border-radius:5px;user-select:none}
|
||||
.rank-expand:active{background:#ecf5ff}
|
||||
.rank-block-ftr{margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;align-items:center;justify-content:space-between;font-size:10px;color:#c0c4cc}
|
||||
.ftr-time{font-family:monospace;font-size:9px}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.app{max-width:100%;margin:0 auto;padding-bottom:calc(20px + var(--safe-bottom))}
|
||||
.header{position:sticky;top:0;z-index:50;background:var(--white);border-bottom:1px solid var(--border);padding:8px 12px}
|
||||
.header-row{display:flex;align-items:center;gap:10px}
|
||||
.header-title{font-size:18px;font-weight:700;color:var(--primary);flex-shrink:0}
|
||||
.header-title-link{text-decoration:none;flex-shrink:0;display:flex;align-items:center}
|
||||
.header-logo-img{max-width:160px;max-height:36px;width:auto;height:auto;object-fit:contain;display:block}
|
||||
.header-actions{margin-left:auto;display:flex;gap:6px;align-items:center}
|
||||
|
||||
/* ===== Search Bar ===== */
|
||||
.search-wrap{flex:1;display:flex;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.search-wrap:focus-within{border-color:var(--primary);background:var(--white)}
|
||||
.search-wrap input{flex:1;height:36px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.search-wrap button{flex-shrink:0;height:28px;margin:4px;padding:0 18px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.search-wrap button:active{background:var(--primary-dark)}
|
||||
.search-wrap button:disabled{opacity:.5}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.site-footer{margin-top:30px;padding:16px 12px 24px;background:#f9fafb;border-top:1px solid var(--border)}
|
||||
.footer-inner{max-width:500px;margin:0 auto;font-size:11px;line-height:1.8;color:#909399;text-align:center;white-space:pre-line}
|
||||
.footer-actions{display:flex;justify-content:center;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||||
.footer-btn{padding:8px 20px;border:1px solid var(--border);border-radius:8px;background:var(--white);color:var(--text);font-size:13px;cursor:pointer;transition:all .2s}
|
||||
.footer-btn:active{background:var(--primary-light);border-color:var(--primary);color:var(--primary)}
|
||||
|
||||
/* ===== Info Bar ===== */
|
||||
.info-bar{display:flex;align-items:center;gap:8px;padding:10px 12px 0;font-size:12px;color:var(--text2);flex-wrap:wrap}
|
||||
.info-bar .count{font-weight:600;color:var(--text)}
|
||||
.info-bar .time{font-family:monospace;background:#f4f4f5;padding:1px 6px;border-radius:4px}
|
||||
.info-bar .badge-err{background:#fef0f0;color:#f56c6c;padding:1px 6px;border-radius:4px}
|
||||
|
||||
/* ===== Loading ===== */
|
||||
.loading{padding:24px 12px;text-align:center;font-size:13px;color:var(--text2)}
|
||||
.loading-bar{width:100%;height:3px;background:#e8e8e8;border-radius:2px;overflow:hidden;margin-top:8px}
|
||||
.loading-bar-inner{height:100%;background:linear-gradient(90deg,var(--primary),#67c23a);border-radius:2px;transition:width .3s ease;width:0%}
|
||||
|
||||
/* ===== Tabs ===== */
|
||||
.tabs{display:flex;gap:4px;padding:8px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
||||
.tabs::-webkit-scrollbar{display:none}
|
||||
.tab{flex-shrink:0;padding:5px 12px;border-radius:16px;font-size:12px;color:#606266;background:#f0f2f5;cursor:pointer;white-space:nowrap;transition:all .2s;user-select:none}
|
||||
.tab:active{transform:scale(.95)}
|
||||
.tab.active{background:var(--primary-light);color:var(--primary);font-weight:600}
|
||||
|
||||
/* ===== Results ===== */
|
||||
.results{display:flex;flex-direction:column;gap:10px;padding:8px 12px}
|
||||
.empty{padding:40px 12px;text-align:center;color:var(--text2);font-size:14px}
|
||||
|
||||
/* ===== Card ===== */
|
||||
.card{display:flex;gap:10px;background:var(--white);border-radius:var(--radius);padding:10px;border:1px solid var(--border);transition:border-color .2s}
|
||||
.card:active{border-color:#c0c4cc}
|
||||
.card-cover{flex-shrink:0;width:90px;height:120px;border-radius:8px;overflow:hidden;background:var(--bg);position:relative}
|
||||
.card-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.card-cover .placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px;background:linear-gradient(135deg,#667eea,#764ba2);color:rgba(255,255,255,.6)}
|
||||
.card-cover .tag{position:absolute;bottom:3px;left:3px;padding:1px 5px;border-radius:3px;color:#fff;font-size:10px;font-weight:600;backdrop-filter:blur(2px)}
|
||||
.card-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.card-title{font-size:14px;font-weight:700;color:var(--text);line-height:1.3;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.card-meta{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:8px}
|
||||
.card-meta .size{color:#67c23a}
|
||||
.card-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
|
||||
.card-tags span{font-size:10px;padding:1px 6px;border-radius:4px;background:#ecf5ff;color:#409eff;white-space:nowrap}
|
||||
.card-tags .quality{background:#fef0f0;color:#e74c3c}
|
||||
.card-actions{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:6px}
|
||||
.card-source{font-size:10px;color:var(--text2);background:#f4f4f5;padding:1px 6px;border-radius:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}
|
||||
.card-btn{padding:4px 10px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background .2s}
|
||||
.card-btn:active{background:var(--primary-dark)}
|
||||
.card-btn:disabled{opacity:.5}
|
||||
|
||||
/* ===== Toast ===== */
|
||||
.toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.75);color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:200;pointer-events:none;opacity:0;transition:opacity .3s}
|
||||
.toast.show{opacity:1}
|
||||
.toast.error{background:rgba(245,108,108,.9)}
|
||||
|
||||
/* ===== Overlay / Modal ===== */
|
||||
.overlay{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;animation:fadeIn .2s}
|
||||
.modal{background:var(--white);border-radius:14px;width:100%;max-width:420px;max-height:90vh;overflow-y:auto;padding:0;animation:slideUp .25s}
|
||||
.modal-hdr{padding:14px 16px;border-bottom:1px solid var(--border);font-size:15px;font-weight:700;color:var(--text)}
|
||||
.modal-body{padding:14px 16px}
|
||||
.modal-ftr{padding:10px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
||||
.modal-ftr button{height:36px;padding:0 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}
|
||||
.modal-ftr .btn-close{background:#f4f4f5;color:#606266}
|
||||
.modal-ftr .btn-disclaimer{background:#fdf6ec;color:#d46b08;margin-right:auto}
|
||||
.modal-ftr .btn-primary{background:var(--primary);color:var(--white)}
|
||||
.modal-ftr .btn-primary:active{background:var(--primary-dark)}
|
||||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes slideUp{from{transform:translateY(40px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
|
||||
/* ===== Share Modal ===== */
|
||||
.share-section{display:flex;flex-direction:column;gap:10px}
|
||||
.share-row{display:flex;gap:8px}
|
||||
.share-row input{flex:1;height:36px;border:1px solid var(--border);border-radius:6px;padding:0 10px;font-size:13px;outline:none;background:var(--bg);color:var(--text)}
|
||||
.share-row input:focus{border-color:var(--primary)}
|
||||
.share-row .copy-btn{height:36px;padding:0 12px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
|
||||
.share-pwd{display:flex;align-items:center;gap:6px;font-size:13px}
|
||||
.share-pwd .pwd-tag{padding:2px 8px;background:#fdf6ec;color:#e6a23c;border-radius:4px;font-weight:700}
|
||||
.share-pwd .pwd-hint{font-size:11px;color:var(--text2)}
|
||||
.share-tip{padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;line-height:1.5;color:#d46b08;display:flex;gap:6px;align-items:flex-start}
|
||||
.share-tip .warn-icon{font-size:18px;line-height:1.5;flex-shrink:0}
|
||||
.share-tip .tip-text{flex:1;min-width:0}
|
||||
.share-tip strong{font-weight:700}
|
||||
.warning-box{background:#fff2f0;border:1px solid #ffccc7;border-radius:8px;padding:8px 10px;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.warning-item{margin:0;font-size:12px;line-height:1.8;font-weight:700;white-space:nowrap}
|
||||
.warning-item:nth-child(odd){color:#cf1322}
|
||||
.warning-item:nth-child(even){color:#d46b08}
|
||||
.warning-item:last-child{color:#b71c1c;font-size:13px}
|
||||
.share-qr{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px}
|
||||
.share-qr canvas{border-radius:8px}
|
||||
.share-qr .qr-label{font-size:12px;font-weight:600;color:var(--primary)}
|
||||
.share-qr .qr-sub{font-size:11px;color:var(--text2)}
|
||||
.share-disclaimer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:10px;padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;color:#d46b08;flex-wrap:wrap}
|
||||
|
||||
/* ===== Login Modal ===== */
|
||||
.login-form{display:flex;flex-direction:column;gap:12px}
|
||||
.login-form input{height:40px;border:1px solid var(--border);border-radius:8px;padding:0 12px;font-size:14px;outline:none}
|
||||
.login-form input:focus{border-color:var(--primary)}
|
||||
.login-form .login-btn{height:40px;border:none;border-radius:8px;background:var(--primary);color:var(--white);font-size:14px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.login-form .login-btn:active{background:var(--primary-dark)}
|
||||
.login-form .login-btn:disabled{opacity:.5}
|
||||
.login-form .login-err{font-size:12px;color:#f56c6c;text-align:center}
|
||||
|
||||
/* ===== Progress Steps ===== */
|
||||
.steps{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
||||
.step{display:flex;align-items:flex-start;gap:10px;opacity:.4;transition:opacity .3s}
|
||||
.step.active{opacity:1}
|
||||
.step.done{opacity:.7}
|
||||
.step-dot{flex-shrink:0;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;background:#e4e7ed;color:#909399}
|
||||
.step.active .step-dot{background:var(--primary);color:#fff;box-shadow:0 0 0 3px rgba(64,158,255,.2)}
|
||||
.step.done .step-dot{background:#67c23a;color:#fff}
|
||||
.step-body{flex:1;padding-top:3px;display:flex;align-items:center;gap:8px}
|
||||
.step-title{font-size:13px;color:var(--text);font-weight:500}
|
||||
.step-status{font-size:11px;padding:1px 7px;border-radius:10px;white-space:nowrap}
|
||||
.step-status.doing{background:#ecf5ff;color:var(--primary)}
|
||||
.step-status.done{background:#f0f9eb;color:#67c23a}
|
||||
.step-status.wait{background:#f4f4f5;color:#c0c4cc}
|
||||
.error-alert{padding:12px 16px;background:#fef0f0;border:1px solid #fde2e2;border-radius:8px;display:flex;align-items:center;gap:8px;font-size:14px;color:#f56c6c}
|
||||
|
||||
/* ===== User Badge ===== */
|
||||
.user-badge{font-size:12px;color:var(--primary);font-weight:600;white-space:nowrap}
|
||||
.login-btn-small{height:30px;padding:0 10px;border:none;border-radius:6px;background:var(--primary-light);color:var(--primary);font-size:12px;font-weight:600;cursor:pointer}
|
||||
.logout-btn-small{height:30px;padding:0 8px;border:none;border-radius:6px;background:transparent;color:var(--text2);font-size:12px;cursor:pointer}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<!-- ===== Home Page (shown when no search) ===== -->
|
||||
<div id="homePage" class="home-page">
|
||||
<div class="home-hero">
|
||||
<div class="home-logo" id="homeLogo" style="display:none"></div>
|
||||
<div class="home-search-box">
|
||||
<input id="homeSearchInput" type="text" placeholder="搜索网盘资源..." />
|
||||
<button id="homeSearchBtn" onclick="homeSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="home-quote" id="homeQuote"></div>
|
||||
<div class="home-quote-author" id="homeQuoteAuthor"></div>
|
||||
</div>
|
||||
<div class="home-rankings" id="homeRankings"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Search Results View ===== -->
|
||||
<div id="searchView" style="display:none">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-row">
|
||||
<a href="/h5" class="header-title-link"><div class="header-title" id="headerTitle" style="display:none">CloudSearch</div></a>
|
||||
<div class="search-wrap">
|
||||
<input id="searchInput" type="text" placeholder="搜索网盘资源..." @keydown="handleKeydown" />
|
||||
<button id="searchBtn" onclick="doSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="header-actions" id="userArea">
|
||||
<template id="userLoggedIn">
|
||||
<span class="user-badge" id="usernameDisplay"></span>
|
||||
<button class="logout-btn-small" onclick="logout()">退出</button>
|
||||
</template>
|
||||
<template id="userLoggedOut">
|
||||
<button class="login-btn-small" onclick="showLogin()">登录</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Bar -->
|
||||
<div id="infoBar" class="info-bar" style="display:none">
|
||||
<span id="infoCount" class="count"></span>
|
||||
<span id="infoTime" class="time"></span>
|
||||
<span id="infoFiltered" class="badge-err"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="loading" class="loading" style="display:none">
|
||||
<div id="loadingText">🔍 正在搜索中...</div>
|
||||
<div class="loading-bar"><div class="loading-bar-inner" id="loadingBar"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div id="tabs" class="tabs" style="display:none"></div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" class="results"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="overlay" id="overlay" style="display:none" onclick="closeModal()"></div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<div class="modal" id="shareModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr" id="shareTitle">分享链接</div>
|
||||
<div class="modal-body">
|
||||
<div id="progressSteps" class="steps" style="display:none">
|
||||
<div class="step" id="step1"><div class="step-dot"><span>1</span></div><div class="step-body"><span class="step-title">正在转存...</span><span class="step-status doing">进行中</span></div></div>
|
||||
<div class="step" id="step2"><div class="step-dot"><span>2</span></div><div class="step-body"><span class="step-title">重命名文件(防和谐)...</span><span class="step-status wait">等待中</span></div></div>
|
||||
<div class="step" id="step3"><div class="step-dot"><span>3</span></div><div class="step-body"><span class="step-title">生成分享链接...</span><span class="step-status wait">等待中</span></div></div>
|
||||
</div>
|
||||
<div id="saveError" class="error-alert" style="display:none"></div>
|
||||
<div id="shareContent" style="display:none">
|
||||
<div class="share-qr">
|
||||
<div id="qrContainer"></div>
|
||||
<div class="qr-label" id="qrLabel"></div>
|
||||
<div class="qr-sub">保存到你自己的网盘</div>
|
||||
</div>
|
||||
<div class="share-section">
|
||||
<div class="share-row">
|
||||
<input id="shareLinkInput" type="text" readonly />
|
||||
</div>
|
||||
<div id="sharePwdRow" class="share-pwd" style="display:none">
|
||||
<span>🔑 提取密码:</span>
|
||||
<span class="pwd-tag" id="sharePwdTag"></span>
|
||||
<span class="pwd-hint">打开链接后需输入密码</span>
|
||||
</div>
|
||||
<div class="share-tip">
|
||||
<span class="warn-icon">⚠️</span>
|
||||
<div class="tip-text">
|
||||
<strong>请尽快复制链接到浏览器打开</strong> 或 <strong>用夸克APP扫码</strong><br>
|
||||
<strong>转存至您的网盘,以免资源被官方和谐</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warning-box">
|
||||
<p class="warning-item">郑重警告一:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告二:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告三:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告四:以上警告说三遍,你还要明知故犯吗?</p>
|
||||
</div>
|
||||
<div class="share-disclaimer">
|
||||
<span>⚠️ 本站资源仅供学习交流,请于24h内删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-disclaimer" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
<button class="btn-close" onclick="closeModal()">关闭</button>
|
||||
<button class="btn-primary" id="copyBtn2" onclick="copyShareLink()" style="display:none">一键复制链接</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div class="modal" id="loginModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr">登录</div>
|
||||
<div class="modal-body">
|
||||
<div class="login-form">
|
||||
<input id="loginUser" type="text" placeholder="用户名" />
|
||||
<input id="loginPass" type="password" placeholder="密码" />
|
||||
<button class="login-btn" id="loginBtn" onclick="handleLogin()">登录</button>
|
||||
<div class="login-err" id="loginErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-close" onclick="closeLogin()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// ===== Anime keywords for categorization =====
|
||||
const ANIME_KWS=['仙逆','凡人修仙传','斗破苍穹','斗破','盘龙','完美世界','一念永恒','妖神记','星辰变','遮天','神墓','吞噬星空','武动乾坤','大主宰','全职高手','鬼灭之刃','海贼王','火影忍者','死神','龙珠','进击的巨人','咒术回战','一人之下','狐妖小红娘','魔道祖师','天官赐福','时光代理人','大王饶命','斗罗大陆','绝世唐门','不良人','秦时明月','全职法师','牧神记','三体','灵笼','雾山五行','凡人','仙王的日常生活','百妖谱','眷思量','镖人','伍六七','刺客伍六七','葬送的芙莉莲','间谍过家家']
|
||||
|
||||
// ===== Quotes =====
|
||||
const QUOTES=['学而时习之,不亦说乎。','温故而知新,可以为师矣。','三人行,必有我师焉。','学而不思则罔,思而不学则殆。','博学之,审问之,慎思之,明辨之,笃行之。','千里之行,始于足下。','不积跬步,无以至千里。','知之为知之,不知为不知,是知也。','工欲善其事,必先利其器。','玉不琢,不成器;人不学,不知道。','学以致用,知行合一。','学海无涯,勤作舟。','书山有路,勤为径。','宝剑锋从磨砺出,梅花香自苦寒来。','锲而不舍,金石可镂。','业精于勤,荒于嬉。','读书破万卷,下笔如有神。','路漫漫其修远兮,吾将上下而求索。','采菊东篱下,悠然见南山。','海内存知己,天涯若比邻。','长风破浪会有时,直挂云帆济沧海。','会当凌绝顶,一览众山小。','山重水复疑无路,柳暗花明又一村。']
|
||||
|
||||
// ===== Home Page =====
|
||||
function homeSearch(){
|
||||
const q=document.getElementById('homeSearchInput').value.trim()
|
||||
if(q)doSearchFromHome(q)
|
||||
}
|
||||
|
||||
function doSearchFromHome(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
doSearch()
|
||||
}
|
||||
function renderHomePage(data){
|
||||
fetch("/api/site-config").then(r=>r.json()).then(cfg=>{
|
||||
// 显示 Logo(优先图片,其次文字)
|
||||
var logoEl=document.getElementById("homeLogo");
|
||||
var headerEl=document.getElementById("headerTitle");
|
||||
if(cfg.site_logo){
|
||||
logoEl.innerHTML='<img src="'+cfg.site_logo+'" class="home-logo-img" alt="logo" />';
|
||||
logoEl.style.display="";
|
||||
headerEl.innerHTML='<img src="'+cfg.site_logo+'" class="header-logo-img" alt="logo" />';
|
||||
headerEl.style.display="";
|
||||
}else if(cfg.site_name){
|
||||
logoEl.textContent=cfg.site_name;
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent=cfg.site_name;
|
||||
headerEl.style.display="";
|
||||
}else{
|
||||
logoEl.textContent="CloudSearch";
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent="CloudSearch";
|
||||
headerEl.style.display="";
|
||||
}
|
||||
if(cfg.site_disclaimer){
|
||||
document.getElementById("footerContent").innerHTML=cfg.site_disclaimer.replace(/\n/g,'<br>');
|
||||
document.getElementById("siteFooter").style.display="block";
|
||||
}
|
||||
}).catch(()=>{})
|
||||
const categories=data.categories||[]
|
||||
const fetchedAt=data.fetchedAt||''
|
||||
// Quote
|
||||
fetch('https://v1.hitokoto.cn/').then(r=>r.json()).then(d=>{
|
||||
document.getElementById('homeQuote').textContent='「 '+d.hitokoto+' 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---'+(d.from_who||d.from||'')
|
||||
}).catch(()=>{
|
||||
document.getElementById('homeQuote').textContent='「 学而时习之,不亦说乎。 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---孔子'
|
||||
})
|
||||
|
||||
// Store expanded state per category
|
||||
window.__expanded=window.__expanded||{}
|
||||
window.__activeTab=window.__activeTab||{}
|
||||
|
||||
const el=document.getElementById('homeRankings')
|
||||
let html=''
|
||||
for(const cat of categories){
|
||||
const icons={movie:'🎬',tv:'📺',western_movie:'🎥',western:'🌍',donghua:'🐉',global_anime:'🌐',variety:'🎤',niche:'💎',hotsite:'🏆'}
|
||||
const icon=icons[cat.category]||'📋'
|
||||
const key=cat.category
|
||||
if(!window.__activeTab[key])window.__activeTab[key]='hot'
|
||||
|
||||
html+='<div class="rank-block">'
|
||||
html+='<div class="rank-block-hdr">'+
|
||||
'<span class="rank-block-title">'+icon+' '+cat.label+'</span>'+
|
||||
'<div class="rank-block-tabs" id="rtabs-'+key+'">'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='hot'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'hot\')">热榜</span>'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='newest'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'newest\')">最新</span>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
html+='<div class="rank-block-items" id="ritems-'+key+'" data-hot=\''+JSON.stringify({items:cat.hot||[]}).replace(/'/g,"'")+'\' data-newest=\''+JSON.stringify({items:cat.newest||[]}).replace(/'/g,"'")+'\'>'
|
||||
const items=window.__activeTab[key]==='hot'?(cat.hot||[]):(cat.newest||[])
|
||||
html+=renderRankItems(items,key,false)
|
||||
html+='</div>'
|
||||
// 数据来源
|
||||
html+='<div class="rank-block-ftr">'+
|
||||
'<span>'+(cat.category!=='hotsite'?'数据来源:TMDB':'本站搜索数据')+'</span>'+
|
||||
'<span class="ftr-time">'+fetchedAt+'</span>'+
|
||||
'</div></div>'
|
||||
}
|
||||
el.innerHTML=html
|
||||
}
|
||||
|
||||
function renderRankItems(items,key,expanded){
|
||||
if(!items||items.length===0)return'<div style="padding:10px;text-align:center;color:#c0c4cc;font-size:12px">暂无数据</div>'
|
||||
const limit=3
|
||||
const show=expanded?items.length:Math.min(limit,items.length)
|
||||
let html=items.slice(0,show).map((item,i)=>{
|
||||
const c=i<3?' rank-idx top3':' rank-idx'
|
||||
return '<div class="rank-item" onclick="doSearchFromHome(\''+item.keyword.replace(/'/g,"\\'")+'\')">'+
|
||||
'<span class="'+c+'">'+(i+1)+'</span>'+
|
||||
'<span class="rank-name">'+item.keyword+'</span>'+
|
||||
'<span class="rank-cnt">'+(item.rating?'⭐'+item.rating:item.searchCount)+'</span>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
if(items.length>limit&&!expanded){
|
||||
html+='<div class="rank-expand" onclick="expandRank(\''+key+'\')">展开全部 ▼</div>'
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
function expandRank(key){
|
||||
const container=document.getElementById('ritems-'+key)
|
||||
if(!container)return
|
||||
const tab=window.__activeTab[key]||'hot'
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,key,true)
|
||||
}
|
||||
|
||||
function switchRankTab(category,tab){
|
||||
window.__activeTab[category]=tab
|
||||
const tabsContainer=document.getElementById('rtabs-'+category)
|
||||
if(tabsContainer){
|
||||
tabsContainer.querySelectorAll('.rank-tab').forEach(t=>t.className='rank-tab')
|
||||
tabsContainer.querySelector(tab==='hot'?'.rank-tab:first-child':'.rank-tab:last-child').className='rank-tab active'
|
||||
}
|
||||
const container=document.getElementById('ritems-'+category)
|
||||
if(container){
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,category,false)
|
||||
}
|
||||
}
|
||||
let userInfo = null
|
||||
let allResults = []
|
||||
let allChannels = []
|
||||
let activeTab = ''
|
||||
let currentSaveItem = null
|
||||
const CLOUD_ICONS = {quark:'☁️',baidu:'🔵',aliyun:'🟠','115':'🟣',tianyi:'🔷','123pan':'🔴',uc:'🟡',xunlei:'🟢',pikpak:'🟤',magnet:'🧲',ed2k:'🔗',others:'📁'}
|
||||
const CLOUD_LABELS = {quark:'夸克网盘',baidu:'百度网盘',aliyun:'阿里云盘','115':'115网盘',tianyi:'天翼云盘','123pan':'123云盘',uc:'UC网盘',xunlei:'迅雷云盘',pikpak:'PikPak',magnet:'磁力链接',ed2k:'电驴链接',others:'其他'}
|
||||
const CLOUD_COLORS = {quark:'#07c160',baidu:'#4e6ef2',aliyun:'#ff6a00','115':'#9b59b6',tianyi:'#00a1d6','123pan':'#e74c3c',uc:'#f39c12',xunlei:'#2ecc71',pikpak:'#8e44ad',magnet:'#95a5a6',ed2k:'#7f8c8d',others:'#95a5a6'}
|
||||
const CLOUD_ORDER = {quark:1,baidu:2,aliyun:3,'115':4,tianyi:5,'123pan':6,uc:7,xunlei:8,pikpak:9,magnet:10,ed2k:11,others:12}
|
||||
|
||||
// ===== Fetch helpers =====
|
||||
function getToken(){return localStorage.getItem('h5_admin_token')}
|
||||
function apiHeaders(){const h={'Content-Type':'application/json'};const t=getToken();if(t)h['Authorization']='Bearer '+t;return h}
|
||||
|
||||
// ===== Toast =====
|
||||
let toastTimer
|
||||
function showToast(msg,isError){
|
||||
const el=document.getElementById('toast')
|
||||
el.textContent=msg
|
||||
el.className='toast show'+(isError?' error':'')
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer=setTimeout(()=>el.className='toast',2000)
|
||||
}
|
||||
|
||||
// ===== User =====
|
||||
async function checkLogin(){
|
||||
try{
|
||||
const res=await fetch('/api/me',{headers:apiHeaders()})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
if(data.loggedIn){
|
||||
userInfo=data
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+data.username+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function logout(){
|
||||
localStorage.removeItem('h5_admin_token')
|
||||
userInfo=null
|
||||
document.getElementById('userArea').innerHTML='<button class="login-btn-small" onclick="showLogin()">登录</button>'
|
||||
showToast('已退出')
|
||||
}
|
||||
|
||||
function showLogin(){
|
||||
document.getElementById('loginErr').textContent=''
|
||||
document.getElementById('loginUser').value=''
|
||||
document.getElementById('loginPass').value=''
|
||||
document.getElementById('loginModal').style.display='block'
|
||||
document.getElementById('overlay').style.display='block'
|
||||
}
|
||||
|
||||
function closeLogin(){
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
document.getElementById('overlay').style.display='none'
|
||||
}
|
||||
|
||||
async function handleLogin(){
|
||||
const user=document.getElementById('loginUser').value.trim()
|
||||
const pass=document.getElementById('loginPass').value
|
||||
if(!user||!pass){showToast('请输入用户名和密码',true);return}
|
||||
const btn=document.getElementById('loginBtn')
|
||||
btn.disabled=true;btn.textContent='登录中...'
|
||||
try{
|
||||
const res=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:user,password:pass})})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
localStorage.setItem('h5_admin_token',data.token)
|
||||
userInfo={username:user}
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+user+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
closeLogin()
|
||||
showToast('登录成功')
|
||||
}else{
|
||||
const err=await res.json().catch(()=>({}))
|
||||
document.getElementById('loginErr').textContent=err.error||'登录失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('loginErr').textContent='网络错误'
|
||||
}finally{
|
||||
btn.disabled=false;btn.textContent='登录'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Search =====
|
||||
function handleKeydown(e){if(e.key==='Enter')doSearch()}
|
||||
|
||||
let searchTimer
|
||||
function doSearch(){
|
||||
const q=document.getElementById('searchInput').value.trim()
|
||||
if(!q)return
|
||||
// Update URL
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
// Show loading
|
||||
document.getElementById('results').innerHTML=''
|
||||
document.getElementById('tabs').style.display='none'
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
document.getElementById('loading').style.display='block'
|
||||
document.getElementById('loadingText').textContent='🔍 正在搜索中...'
|
||||
document.getElementById('searchBtn').disabled=true
|
||||
|
||||
let progress=0
|
||||
const bar=document.getElementById('loadingBar')
|
||||
const progressTimer=setInterval(()=>{
|
||||
if(progress<60)progress+=1+Math.random()*3
|
||||
else if(progress<85)progress+=0.5+Math.random()
|
||||
bar.style.width=progress+'%'
|
||||
},200)
|
||||
|
||||
// Use streaming search for live updates
|
||||
streamSearch(q,progressTimer,bar)
|
||||
}
|
||||
|
||||
async function streamSearch(q,progressTimer,bar){
|
||||
const startTime=Date.now()
|
||||
try{
|
||||
const response=await fetch('/api/query',{method:'POST',headers:apiHeaders(),body:JSON.stringify({q})})
|
||||
if(!response.ok)throw new Error('搜索失败 ('+response.status+')')
|
||||
|
||||
const reader=response.body.getReader()
|
||||
const decoder=new TextDecoder()
|
||||
let buffer=''
|
||||
let allItems=[]
|
||||
let channels=[]
|
||||
let totalCount=0
|
||||
let filteredCount=0
|
||||
|
||||
while(true){
|
||||
const {done,value}=await reader.read()
|
||||
if(done)break
|
||||
|
||||
buffer+=decoder.decode(value,{stream:true})
|
||||
const lines=buffer.split('\n')
|
||||
buffer=lines.pop()||''
|
||||
|
||||
for(const line of lines){
|
||||
if(!line.trim())continue
|
||||
try{
|
||||
const msg=JSON.parse(line)
|
||||
if(msg.type==='stats'){
|
||||
totalCount=msg.total||0
|
||||
filteredCount=msg.filtered||0
|
||||
document.getElementById('loadingText').textContent='🔍 搜索到 '+totalCount+' 条,正在验证...'
|
||||
}else if(msg.type==='result'){
|
||||
if(msg.valid&&msg.id){
|
||||
allItems.push(msg.id)
|
||||
}
|
||||
}else if(msg.type==='complete'){
|
||||
const results=msg.results||[]
|
||||
channels=msg.channels||[]
|
||||
clearInterval(progressTimer)
|
||||
bar.style.width='100%'
|
||||
setTimeout(()=>renderResults(results,channels,totalCount,filteredCount,Date.now()-startTime),300)
|
||||
return
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
clearInterval(progressTimer)
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
document.getElementById('results').innerHTML='<div class="empty">搜索失败:'+e.message+'</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Render =====
|
||||
function renderResults(results,channels,totalCount,filteredCount,time){
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
allResults=results
|
||||
allChannels=channels||[]
|
||||
|
||||
// Info bar
|
||||
if(totalCount>0){
|
||||
document.getElementById('infoBar').style.display='flex'
|
||||
document.getElementById('infoCount').textContent='已为您挑选到最符合 '+totalCount+' 条结果'
|
||||
document.getElementById('infoTime').textContent='⏱ '+time+'ms'
|
||||
if(filteredCount>0)document.getElementById('infoFiltered').textContent='❌ 失效 '+filteredCount
|
||||
else document.getElementById('infoFiltered').textContent=''
|
||||
}else{
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
}
|
||||
|
||||
// Build tabs
|
||||
const tabsEl=document.getElementById('tabs')
|
||||
tabsEl.innerHTML=''
|
||||
const typeCounts={}
|
||||
for(const r of results){const ct=r.cloud_type||'others';typeCounts[ct]=(typeCounts[ct]||0)+1}
|
||||
const sorted=Object.keys(typeCounts).sort((a,b)=>(CLOUD_ORDER[a]||99)-(CLOUD_ORDER[b]||99))
|
||||
// "全部" tab
|
||||
const allTab=document.createElement('div')
|
||||
allTab.className='tab active'
|
||||
allTab.textContent='📋 全部 ('+results.length+')'
|
||||
allTab.onclick=()=>{setActiveTab('');renderCardList(results)}
|
||||
tabsEl.appendChild(allTab)
|
||||
for(const ct of sorted){
|
||||
const tab=document.createElement('div')
|
||||
tab.className='tab'
|
||||
tab.textContent=(CLOUD_ICONS[ct]||'📁')+' '+(CLOUD_LABELS[ct]||ct)+' ('+typeCounts[ct]+')'
|
||||
tab.onclick=()=>{setActiveTab(ct);renderCardList(results.filter(r=>(r.cloud_type||'others')===ct))}
|
||||
tabsEl.appendChild(tab)
|
||||
}
|
||||
tabsEl.style.display=results.length>0?'flex':'none'
|
||||
activeTab=''
|
||||
|
||||
// Render cards
|
||||
renderCardList(results)
|
||||
}
|
||||
|
||||
function setActiveTab(ct){
|
||||
activeTab=ct
|
||||
document.querySelectorAll('.tab').forEach((t,i)=>{
|
||||
const isAll=i===0&&!ct
|
||||
const active=i>0&&ct&&t.textContent.includes(CLOUD_LABELS[ct])
|
||||
t.className='tab'+(active||isAll?' active':'')
|
||||
})
|
||||
}
|
||||
|
||||
function renderCardList(items){
|
||||
const el=document.getElementById('results')
|
||||
if(items.length===0){
|
||||
el.innerHTML='<div class="empty">暂无结果</div>'
|
||||
return
|
||||
}
|
||||
el.innerHTML=items.map((item,idx)=>{
|
||||
const coverHtml=item.cover
|
||||
? '<img src="'+escapeHtml(item.cover)+'" alt="" onerror="this.parentElement.innerHTML=\'<div class=placeholder>'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>\'" loading="lazy" />'
|
||||
: '<div class="placeholder">'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>'
|
||||
const cloudLabel=CLOUD_LABELS[item.cloud_type]||item.cloud_type||''
|
||||
const cloudColor=CLOUD_COLORS[item.cloud_type]||'#95a5a6'
|
||||
const tags=extractTags(item.title||'')
|
||||
const cleanTitle=(item.title||'').replace(/【[^】]+】/g,'').trim()
|
||||
const relativeTime=formatTime(item.update_time||item.datetime||'')
|
||||
return '<div class="card" onclick="saveItem('+idx+')">'+
|
||||
'<div class="card-cover">'+coverHtml+'<span class="tag" style="background:'+cloudColor+'">'+cloudLabel+'</span></div>'+
|
||||
'<div class="card-body">'+
|
||||
'<div class="card-title">'+escapeHtml(cleanTitle)+'</div>'+
|
||||
'<div class="card-meta"><span>🕐 '+relativeTime+'</span>'+(item.file_size?'<span class="size">📦 '+escapeHtml(item.file_size)+'</span>':'')+'</div>'+
|
||||
(tags.length>0?'<div class="card-tags">'+tags.map(t=>'<span'+(isQualityTag(t)?' class="quality"':'')+'>'+escapeHtml(t)+'</span>').join('')+'</div>':'')+
|
||||
'<div class="card-actions">'+
|
||||
'<span class="card-source">'+(item.source?escapeHtml(item.source):'网盘')+'</span>'+
|
||||
'<button class="card-btn" onclick="event.stopPropagation();saveItem('+idx+')">🔗 获取分享链接</button>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
|
||||
// Store items for save reference
|
||||
window.__h5Results=items
|
||||
}
|
||||
|
||||
function escapeHtml(s){if(!s)return '';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function extractTags(title){
|
||||
const tags=[]
|
||||
// Quality tags
|
||||
const quality=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','WEB-DL','WEBRip']
|
||||
for(const q of quality){if(title.includes(q)&&!tags.includes(q))tags.push(q)}
|
||||
const kw=['杜比视界','杜比全景声','高码率','内封简繁英字幕','内嵌字幕','中文字幕','中英字幕']
|
||||
for(const k of kw){if(title.includes(k)&&!tags.includes(k))tags.push(k)}
|
||||
return tags.slice(0,6)
|
||||
}
|
||||
|
||||
function isQualityTag(t){const q=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','臻彩','高清','WEB-DL','WEBRip'];return q.includes(t)}
|
||||
|
||||
function formatTime(s){
|
||||
if(!s)return ''
|
||||
const d=new Date(s)
|
||||
if(isNaN(d.getTime()))return s.slice(0,10)
|
||||
const diff=Date.now()-d.getTime()
|
||||
if(diff<0)return s.slice(0,10)
|
||||
const mins=Math.floor(diff/60000)
|
||||
if(mins<60)return mins<=1?'刚刚':mins+' 分钟前'
|
||||
const hours=Math.floor(mins/60)
|
||||
if(hours<24)return hours+' 小时前'
|
||||
const days=Math.floor(hours/24)
|
||||
if(days<30)return days+' 天前'
|
||||
return Math.floor(days/30)+' 个月前'
|
||||
}
|
||||
|
||||
// ===== Save / Share =====
|
||||
function saveItem(idx){
|
||||
const items=window.__h5Results||[]
|
||||
currentSaveItem=items[idx]
|
||||
if(!currentSaveItem)return
|
||||
|
||||
document.getElementById('progressSteps').style.display='block'
|
||||
document.getElementById('shareContent').style.display='none'
|
||||
document.getElementById('saveError').style.display='none'
|
||||
document.getElementById('copyBtn2').style.display='none'
|
||||
|
||||
const title=(currentSaveItem.title||'').replace(/【[^】]+】/g,'').trim()||'资源'
|
||||
document.getElementById('shareTitle').textContent=title
|
||||
|
||||
// Show modal
|
||||
document.getElementById('overlay').style.display='block'
|
||||
document.getElementById('shareModal').style.display='block'
|
||||
|
||||
// Reset steps
|
||||
resetSteps()
|
||||
advanceStep(1)
|
||||
|
||||
// Call save API
|
||||
doSave()
|
||||
}
|
||||
|
||||
async function doSave(){
|
||||
try{
|
||||
const res=await fetch('/api/save',{method:'POST',headers:apiHeaders(),body:JSON.stringify({type:'search',source:currentSaveItem,target_cloud:currentSaveItem.cloud_type||'quark'})})
|
||||
const data=await res.json()
|
||||
|
||||
if(!data.success){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=data.message||data.error||'保存失败'
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2
|
||||
advanceStep(2)
|
||||
await sleep(500)
|
||||
|
||||
// Step 3
|
||||
advanceStep(3)
|
||||
await sleep(300)
|
||||
|
||||
if(data.share_url){
|
||||
advanceStep(4)
|
||||
await sleep(200)
|
||||
showShareResult(data)
|
||||
}else{
|
||||
advanceStep(4)
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent='生成分享链接失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=e.message||'保存请求失败'
|
||||
}
|
||||
}
|
||||
|
||||
function showShareResult(data){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('shareContent').style.display='block'
|
||||
|
||||
const link=data.share_url
|
||||
document.getElementById('shareLinkInput').value=link
|
||||
|
||||
const diskLabel=CLOUD_LABELS[currentSaveItem.cloud_type]||'夸克网盘'
|
||||
document.getElementById('qrLabel').textContent=diskLabel+' APP扫码转存'
|
||||
|
||||
// Generate QR
|
||||
const qrContainer=document.getElementById('qrContainer')
|
||||
qrContainer.innerHTML=''
|
||||
new QRCode(qrContainer,{text:link,width:140,height:140})
|
||||
|
||||
// Password
|
||||
const pwd=data.share_pwd||data.sharePwd||''
|
||||
if(pwd){
|
||||
document.getElementById('sharePwdRow').style.display='flex'
|
||||
document.getElementById('sharePwdTag').textContent=pwd
|
||||
}else{
|
||||
document.getElementById('sharePwdRow').style.display='none'
|
||||
}
|
||||
|
||||
document.getElementById('copyBtn2').style.display='inline-block'
|
||||
}
|
||||
|
||||
function resetSteps(){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
el.className='step'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
el.querySelector('.step-status').textContent='等待中'
|
||||
el.querySelector('.step-status').className='step-status wait'
|
||||
}
|
||||
}
|
||||
|
||||
function advanceStep(n){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
if(i<n){
|
||||
el.className='step done'
|
||||
el.querySelector('.step-dot').innerHTML='<span class="step-check">✓</span>'
|
||||
el.querySelector('.step-status').textContent='已完成'
|
||||
el.querySelector('.step-status').className='step-status done'
|
||||
}else if(i===n){
|
||||
el.className='step active'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
const titles=['正在转存到','正在重命名文件(防和谐)','正在生成分享链接']
|
||||
el.querySelector('.step-title').textContent=titles[i-1]+'...'
|
||||
el.querySelector('.step-status').textContent='进行中'
|
||||
el.querySelector('.step-status').className='step-status doing'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms){return new Promise(r=>setTimeout(r,ms))}
|
||||
|
||||
function copyShareLink(){
|
||||
const input=document.getElementById('shareLinkInput')
|
||||
if(!input.value)return
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(input.value).then(()=>showToast('链接已复制')).catch(()=>fallbackCopy(input.value))
|
||||
}else{
|
||||
fallbackCopy(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text){
|
||||
const ta=document.createElement('textarea')
|
||||
ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try{document.execCommand('copy');showToast('链接已复制')}catch{showToast('复制失败',true)}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
|
||||
function openDisclaimer(){
|
||||
window.open('/disclaimer/','_blank')
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
document.getElementById('overlay').style.display='none'
|
||||
document.getElementById('shareModal').style.display='none'
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
}
|
||||
|
||||
// ===== Init =====
|
||||
checkLogin()
|
||||
|
||||
// Add Enter key handler for home search
|
||||
document.getElementById('homeSearchInput').addEventListener('keydown',function(e){if(e.key==='Enter')homeSearch()})
|
||||
// Also add for search view input
|
||||
document.getElementById('searchInput').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch()})
|
||||
|
||||
// Fetch home page data
|
||||
fetch('/api/rankings/categorized').then(r=>r.json()).then(data=>{
|
||||
renderHomePage(data)
|
||||
}).catch(()=>{
|
||||
document.getElementById('homeQuote').textContent='「 学而时习之,不亦说乎。 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---孔子'
|
||||
})
|
||||
|
||||
// Check URL for query
|
||||
const params=new URLSearchParams(window.location.search)
|
||||
const q=params.get('q')
|
||||
if(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
doSearch()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
<!-- Footer -->
|
||||
<div id="siteFooter" class="site-footer" style="display:none">
|
||||
<div id="footerContent" class="footer-inner"></div>
|
||||
<div class="footer-actions" id="footerActions">
|
||||
<button class="footer-btn" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
@@ -1,599 +0,0 @@
|
||||
// ===== Anime keywords for categorization =====
|
||||
const ANIME_KWS=['仙逆','凡人修仙传','斗破苍穹','斗破','盘龙','完美世界','一念永恒','妖神记','星辰变','遮天','神墓','吞噬星空','武动乾坤','大主宰','全职高手','鬼灭之刃','海贼王','火影忍者','死神','龙珠','进击的巨人','咒术回战','一人之下','狐妖小红娘','魔道祖师','天官赐福','时光代理人','大王饶命','斗罗大陆','绝世唐门','不良人','秦时明月','全职法师','牧神记','三体','灵笼','雾山五行','凡人','仙王的日常生活','百妖谱','眷思量','镖人','伍六七','刺客伍六七','葬送的芙莉莲','间谍过家家']
|
||||
|
||||
// ===== Quotes =====
|
||||
const QUOTES=['学而时习之,不亦说乎。','温故而知新,可以为师矣。','三人行,必有我师焉。','学而不思则罔,思而不学则殆。','博学之,审问之,慎思之,明辨之,笃行之。','千里之行,始于足下。','不积跬步,无以至千里。','知之为知之,不知为不知,是知也。','工欲善其事,必先利其器。','玉不琢,不成器;人不学,不知道。','学以致用,知行合一。','学海无涯,勤作舟。','书山有路,勤为径。','宝剑锋从磨砺出,梅花香自苦寒来。','锲而不舍,金石可镂。','业精于勤,荒于嬉。','读书破万卷,下笔如有神。','路漫漫其修远兮,吾将上下而求索。','采菊东篱下,悠然见南山。','海内存知己,天涯若比邻。','长风破浪会有时,直挂云帆济沧海。','会当凌绝顶,一览众山小。','山重水复疑无路,柳暗花明又一村。']
|
||||
|
||||
// ===== Home Page =====
|
||||
function homeSearch(){
|
||||
const q=document.getElementById('homeSearchInput').value.trim()
|
||||
if(q)doSearchFromHome(q)
|
||||
}
|
||||
|
||||
function doSearchFromHome(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
doSearch()
|
||||
}
|
||||
function renderHomePage(data){
|
||||
fetch("/api/site-config").then(r=>r.json()).then(cfg=>{
|
||||
// 显示 Logo(优先图片,其次文字)
|
||||
var logoEl=document.getElementById("homeLogo");
|
||||
var headerEl=document.getElementById("headerTitle");
|
||||
if(cfg.site_logo){
|
||||
logoEl.innerHTML='<img src="'+cfg.site_logo+'" class="home-logo-img" alt="logo" />';
|
||||
logoEl.style.display="";
|
||||
headerEl.innerHTML='<img src="'+cfg.site_logo+'" class="header-logo-img" alt="logo" />';
|
||||
headerEl.style.display="";
|
||||
}else if(cfg.site_name){
|
||||
logoEl.textContent=cfg.site_name;
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent=cfg.site_name;
|
||||
headerEl.style.display="";
|
||||
}else{
|
||||
logoEl.textContent="CloudSearch";
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent="CloudSearch";
|
||||
headerEl.style.display="";
|
||||
}
|
||||
if(cfg.site_disclaimer){
|
||||
document.getElementById("footerContent").innerHTML=cfg.site_disclaimer.replace(/\n/g,'<br>');
|
||||
document.getElementById("siteFooter").style.display="block";
|
||||
}
|
||||
}).catch(()=>{})
|
||||
const categories=data.categories||[]
|
||||
const fetchedAt=data.fetchedAt||''
|
||||
// Quote
|
||||
fetch('https://v1.(function(){
|
||||
var q=QUOTES[Math.floor(Math.random()*QUOTES.length)];
|
||||
document.getElementById('homeQuote').textContent='「 '+q+' 」';
|
||||
document.getElementById('homeQuoteAuthor').textContent='——古籍经典';
|
||||
})()
|
||||
|
||||
// Store expanded state per category
|
||||
window.__expanded=window.__expanded||{}
|
||||
window.__activeTab=window.__activeTab||{}
|
||||
|
||||
const el=document.getElementById('homeRankings')
|
||||
let html=''
|
||||
for(const cat of categories){
|
||||
const icons={movie:'🎬',tv:'📺',western_movie:'🎥',western:'🌍',donghua:'🐉',global_anime:'🌐',variety:'🎤',niche:'💎',hotsite:'🏆'}
|
||||
const icon=icons[cat.category]||'📋'
|
||||
const key=cat.category
|
||||
if(!window.__activeTab[key])window.__activeTab[key]='hot'
|
||||
|
||||
html+='<div class="rank-block">'
|
||||
html+='<div class="rank-block-hdr">'+
|
||||
'<span class="rank-block-title">'+icon+' '+cat.label+'</span>'+
|
||||
'<div class="rank-block-tabs" id="rtabs-'+key+'">'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='hot'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'hot\')">热榜</span>'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='newest'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'newest\')">最新</span>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
html+='<div class="rank-block-items" id="ritems-'+key+'" data-hot=\''+JSON.stringify({items:cat.hot||[]}).replace(/'/g,"'")+'\' data-newest=\''+JSON.stringify({items:cat.newest||[]}).replace(/'/g,"'")+'\'>'
|
||||
const items=window.__activeTab[key]==='hot'?(cat.hot||[]):(cat.newest||[])
|
||||
html+=renderRankItems(items,key,false)
|
||||
html+='</div>'
|
||||
// 数据来源
|
||||
html+='<div class="rank-block-ftr">'+
|
||||
'<span>'+(cat.category!=='hotsite'?'数据来源:TMDB':'本站搜索数据')+'</span>'+
|
||||
'<span class="ftr-time">'+fetchedAt+'</span>'+
|
||||
'</div></div>'
|
||||
}
|
||||
el.innerHTML=html
|
||||
}
|
||||
|
||||
function renderRankItems(items,key,expanded){
|
||||
if(!items||items.length===0)return'<div style="padding:10px;text-align:center;color:#c0c4cc;font-size:12px">暂无数据</div>'
|
||||
const limit=3
|
||||
const show=expanded?items.length:Math.min(limit,items.length)
|
||||
let html=items.slice(0,show).map((item,i)=>{
|
||||
const c=i<3?' rank-idx top3':' rank-idx'
|
||||
return '<div class="rank-item" onclick="doSearchFromHome(\''+item.keyword.replace(/'/g,"\\'")+'\')">'+
|
||||
'<span class="'+c+'">'+(i+1)+'</span>'+
|
||||
'<span class="rank-name">'+item.keyword+'</span>'+
|
||||
'<span class="rank-cnt">'+(item.rating?'⭐'+item.rating:item.searchCount)+'</span>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
if(items.length>limit&&!expanded){
|
||||
html+='<div class="rank-expand" onclick="expandRank(\''+key+'\')">展开全部 ▼</div>'
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
function expandRank(key){
|
||||
const container=document.getElementById('ritems-'+key)
|
||||
if(!container)return
|
||||
const tab=window.__activeTab[key]||'hot'
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,key,true)
|
||||
}
|
||||
|
||||
function switchRankTab(category,tab){
|
||||
window.__activeTab[category]=tab
|
||||
const tabsContainer=document.getElementById('rtabs-'+category)
|
||||
if(tabsContainer){
|
||||
tabsContainer.querySelectorAll('.rank-tab').forEach(t=>t.className='rank-tab')
|
||||
tabsContainer.querySelector(tab==='hot'?'.rank-tab:first-child':'.rank-tab:last-child').className='rank-tab active'
|
||||
}
|
||||
const container=document.getElementById('ritems-'+category)
|
||||
if(container){
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,category,false)
|
||||
}
|
||||
}
|
||||
let userInfo = null
|
||||
let allResults = []
|
||||
let allChannels = []
|
||||
let activeTab = ''
|
||||
let currentSaveItem = null
|
||||
const CLOUD_ICONS = {quark:'☁️',baidu:'🔵',aliyun:'🟠','115':'🟣',tianyi:'🔷','123pan':'🔴',uc:'🟡',xunlei:'🟢',pikpak:'🟤',magnet:'🧲',ed2k:'🔗',others:'📁'}
|
||||
const CLOUD_LABELS = {quark:'夸克网盘',baidu:'百度网盘',aliyun:'阿里云盘','115':'115网盘',tianyi:'天翼云盘','123pan':'123云盘',uc:'UC网盘',xunlei:'迅雷云盘',pikpak:'PikPak',magnet:'磁力链接',ed2k:'电驴链接',others:'其他'}
|
||||
const CLOUD_COLORS = {quark:'#07c160',baidu:'#4e6ef2',aliyun:'#ff6a00','115':'#9b59b6',tianyi:'#00a1d6','123pan':'#e74c3c',uc:'#f39c12',xunlei:'#2ecc71',pikpak:'#8e44ad',magnet:'#95a5a6',ed2k:'#7f8c8d',others:'#95a5a6'}
|
||||
const CLOUD_ORDER = {quark:1,baidu:2,aliyun:3,'115':4,tianyi:5,'123pan':6,uc:7,xunlei:8,pikpak:9,magnet:10,ed2k:11,others:12}
|
||||
|
||||
// ===== Fetch helpers =====
|
||||
function getToken(){return localStorage.getItem('h5_admin_token')}
|
||||
function apiHeaders(){const h={'Content-Type':'application/json'};const t=getToken();if(t)h['Authorization']='Bearer '+t;return h}
|
||||
|
||||
// ===== Toast =====
|
||||
let toastTimer
|
||||
function showToast(msg,isError){
|
||||
const el=document.getElementById('toast')
|
||||
el.textContent=msg
|
||||
el.className='toast show'+(isError?' error':'')
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer=setTimeout(()=>el.className='toast',2000)
|
||||
}
|
||||
|
||||
// ===== User =====
|
||||
async function checkLogin(){
|
||||
try{
|
||||
const res=await fetch('/api/me',{headers:apiHeaders()})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
if(data.loggedIn){
|
||||
userInfo=data
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+data.username+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function logout(){
|
||||
localStorage.removeItem('h5_admin_token')
|
||||
userInfo=null
|
||||
document.getElementById('userArea').innerHTML='<button class="login-btn-small" onclick="showLogin()">登录</button>'
|
||||
showToast('已退出')
|
||||
}
|
||||
|
||||
function showLogin(){
|
||||
document.getElementById('loginErr').textContent=''
|
||||
document.getElementById('loginUser').value=''
|
||||
document.getElementById('loginPass').value=''
|
||||
document.getElementById('loginModal').style.display='block'
|
||||
document.getElementById('overlay').style.display='block'
|
||||
}
|
||||
|
||||
function closeLogin(){
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
document.getElementById('overlay').style.display='none'
|
||||
}
|
||||
|
||||
async function handleLogin(){
|
||||
const user=document.getElementById('loginUser').value.trim()
|
||||
const pass=document.getElementById('loginPass').value
|
||||
if(!user||!pass){showToast('请输入用户名和密码',true);return}
|
||||
const btn=document.getElementById('loginBtn')
|
||||
btn.disabled=true;btn.textContent='登录中...'
|
||||
try{
|
||||
const res=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:user,password:pass})})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
localStorage.setItem('h5_admin_token',data.token)
|
||||
userInfo={username:user}
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+user+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
closeLogin()
|
||||
showToast('登录成功')
|
||||
}else{
|
||||
const err=await res.json().catch(()=>({}))
|
||||
document.getElementById('loginErr').textContent=err.error||'登录失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('loginErr').textContent='网络错误'
|
||||
}finally{
|
||||
btn.disabled=false;btn.textContent='登录'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Search =====
|
||||
function handleKeydown(e){if(e.key==='Enter')doSearch()}
|
||||
|
||||
let searchTimer
|
||||
function doSearch(){
|
||||
const q=document.getElementById('searchInput').value.trim()
|
||||
if(!q)return
|
||||
// Update URL
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
// Show loading
|
||||
document.getElementById('results').innerHTML=''
|
||||
document.getElementById('tabs').style.display='none'
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
document.getElementById('loading').style.display='block'
|
||||
document.getElementById('loadingText').textContent='🔍 正在搜索中...'
|
||||
document.getElementById('searchBtn').disabled=true
|
||||
|
||||
let progress=0
|
||||
const bar=document.getElementById('loadingBar')
|
||||
const progressTimer=setInterval(()=>{
|
||||
if(progress<60)progress+=1+Math.random()*3
|
||||
else if(progress<85)progress+=0.5+Math.random()
|
||||
bar.style.width=progress+'%'
|
||||
},200)
|
||||
|
||||
// Use streaming search for live updates
|
||||
streamSearch(q,progressTimer,bar)
|
||||
}
|
||||
|
||||
async function streamSearch(q,progressTimer,bar){
|
||||
const startTime=Date.now()
|
||||
try{
|
||||
const response=await fetch('/api/query',{method:'POST',headers:apiHeaders(),body:JSON.stringify({q})})
|
||||
if(!response.ok)throw new Error('搜索失败 ('+response.status+')')
|
||||
|
||||
const reader=response.body.getReader()
|
||||
const decoder=new TextDecoder()
|
||||
let buffer=''
|
||||
let allItems=[]
|
||||
let channels=[]
|
||||
let totalCount=0
|
||||
let filteredCount=0
|
||||
|
||||
while(true){
|
||||
const {done,value}=await reader.read()
|
||||
if(done)break
|
||||
|
||||
buffer+=decoder.decode(value,{stream:true})
|
||||
const lines=buffer.split('\n')
|
||||
buffer=lines.pop()||''
|
||||
|
||||
for(const line of lines){
|
||||
if(!line.trim())continue
|
||||
try{
|
||||
const msg=JSON.parse(line)
|
||||
if(msg.type==='stats'){
|
||||
totalCount=msg.total||0
|
||||
filteredCount=msg.filtered||0
|
||||
document.getElementById('loadingText').textContent='🔍 搜索到 '+totalCount+' 条,正在验证...'
|
||||
}else if(msg.type==='result'){
|
||||
if(msg.valid&&msg.id){
|
||||
allItems.push(msg.id)
|
||||
}
|
||||
}else if(msg.type==='complete'){
|
||||
const results=msg.results||[]
|
||||
channels=msg.channels||[]
|
||||
clearInterval(progressTimer)
|
||||
bar.style.width='100%'
|
||||
setTimeout(()=>renderResults(results,channels,totalCount,filteredCount,Date.now()-startTime),300)
|
||||
return
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
clearInterval(progressTimer)
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
document.getElementById('results').innerHTML='<div class="empty">搜索失败:'+e.message+'</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Render =====
|
||||
function renderResults(results,channels,totalCount,filteredCount,time){
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
allResults=results
|
||||
allChannels=channels||[]
|
||||
|
||||
// Info bar
|
||||
if(totalCount>0){
|
||||
document.getElementById('infoBar').style.display='flex'
|
||||
document.getElementById('infoCount').textContent='已为您挑选到最符合 '+totalCount+' 条结果'
|
||||
document.getElementById('infoTime').textContent='⏱ '+time+'ms'
|
||||
if(filteredCount>0)document.getElementById('infoFiltered').textContent='❌ 失效 '+filteredCount
|
||||
else document.getElementById('infoFiltered').textContent=''
|
||||
}else{
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
}
|
||||
|
||||
// Build tabs
|
||||
const tabsEl=document.getElementById('tabs')
|
||||
tabsEl.innerHTML=''
|
||||
const typeCounts={}
|
||||
for(const r of results){const ct=r.cloud_type||'others';typeCounts[ct]=(typeCounts[ct]||0)+1}
|
||||
const sorted=Object.keys(typeCounts).sort((a,b)=>(CLOUD_ORDER[a]||99)-(CLOUD_ORDER[b]||99))
|
||||
// "全部" tab
|
||||
const allTab=document.createElement('div')
|
||||
allTab.className='tab active'
|
||||
allTab.textContent='📋 全部 ('+results.length+')'
|
||||
allTab.onclick=()=>{setActiveTab('');renderCardList(results)}
|
||||
tabsEl.appendChild(allTab)
|
||||
for(const ct of sorted){
|
||||
const tab=document.createElement('div')
|
||||
tab.className='tab'
|
||||
tab.textContent=(CLOUD_ICONS[ct]||'📁')+' '+(CLOUD_LABELS[ct]||ct)+' ('+typeCounts[ct]+')'
|
||||
tab.onclick=()=>{setActiveTab(ct);renderCardList(results.filter(r=>(r.cloud_type||'others')===ct))}
|
||||
tabsEl.appendChild(tab)
|
||||
}
|
||||
tabsEl.style.display=results.length>0?'flex':'none'
|
||||
activeTab=''
|
||||
|
||||
// Render cards
|
||||
renderCardList(results)
|
||||
}
|
||||
|
||||
function setActiveTab(ct){
|
||||
activeTab=ct
|
||||
document.querySelectorAll('.tab').forEach((t,i)=>{
|
||||
const isAll=i===0&&!ct
|
||||
const active=i>0&&ct&&t.textContent.includes(CLOUD_LABELS[ct])
|
||||
t.className='tab'+(active||isAll?' active':'')
|
||||
})
|
||||
}
|
||||
|
||||
function renderCardList(items){
|
||||
const el=document.getElementById('results')
|
||||
if(items.length===0){
|
||||
el.innerHTML='<div class="empty">暂无结果</div>'
|
||||
return
|
||||
}
|
||||
el.innerHTML=items.map((item,idx)=>{
|
||||
const coverHtml=item.cover
|
||||
? '<img src="'+escapeHtml(item.cover)+'" alt="" onerror="this.parentElement.innerHTML=\'<div class=placeholder>'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>\'" loading="lazy" />'
|
||||
: '<div class="placeholder">'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>'
|
||||
const cloudLabel=CLOUD_LABELS[item.cloud_type]||item.cloud_type||''
|
||||
const cloudColor=CLOUD_COLORS[item.cloud_type]||'#95a5a6'
|
||||
const tags=extractTags(item.title||'')
|
||||
const cleanTitle=(item.title||'').replace(/【[^】]+】/g,'').trim()
|
||||
const relativeTime=formatTime(item.update_time||item.datetime||'')
|
||||
return '<div class="card" onclick="saveItem('+idx+')">'+
|
||||
'<div class="card-cover">'+coverHtml+'<span class="tag" style="background:'+cloudColor+'">'+cloudLabel+'</span></div>'+
|
||||
'<div class="card-body">'+
|
||||
'<div class="card-title">'+escapeHtml(cleanTitle)+'</div>'+
|
||||
'<div class="card-meta"><span>🕐 '+relativeTime+'</span>'+(item.file_size?'<span class="size">📦 '+escapeHtml(item.file_size)+'</span>':'')+'</div>'+
|
||||
(tags.length>0?'<div class="card-tags">'+tags.map(t=>'<span'+(isQualityTag(t)?' class="quality"':'')+'>'+escapeHtml(t)+'</span>').join('')+'</div>':'')+
|
||||
'<div class="card-actions">'+
|
||||
'<span class="card-source">'+(item.source?escapeHtml(item.source):'网盘')+'</span>'+
|
||||
'<button class="card-btn" onclick="event.stopPropagation();saveItem('+idx+')">🔗 获取分享链接</button>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
|
||||
// Store items for save reference
|
||||
window.__h5Results=items
|
||||
}
|
||||
|
||||
function escapeHtml(s){if(!s)return '';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function extractTags(title){
|
||||
const tags=[]
|
||||
// Quality tags
|
||||
const quality=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','WEB-DL','WEBRip']
|
||||
for(const q of quality){if(title.includes(q)&&!tags.includes(q))tags.push(q)}
|
||||
const kw=['杜比视界','杜比全景声','高码率','内封简繁英字幕','内嵌字幕','中文字幕','中英字幕']
|
||||
for(const k of kw){if(title.includes(k)&&!tags.includes(k))tags.push(k)}
|
||||
return tags.slice(0,6)
|
||||
}
|
||||
|
||||
function isQualityTag(t){const q=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','臻彩','高清','WEB-DL','WEBRip'];return q.includes(t)}
|
||||
|
||||
function formatTime(s){
|
||||
if(!s)return ''
|
||||
const d=new Date(s)
|
||||
if(isNaN(d.getTime()))return s.slice(0,10)
|
||||
const diff=Date.now()-d.getTime()
|
||||
if(diff<0)return s.slice(0,10)
|
||||
const mins=Math.floor(diff/60000)
|
||||
if(mins<60)return mins<=1?'刚刚':mins+' 分钟前'
|
||||
const hours=Math.floor(mins/60)
|
||||
if(hours<24)return hours+' 小时前'
|
||||
const days=Math.floor(hours/24)
|
||||
if(days<30)return days+' 天前'
|
||||
return Math.floor(days/30)+' 个月前'
|
||||
}
|
||||
|
||||
// ===== Save / Share =====
|
||||
function saveItem(idx){
|
||||
const items=window.__h5Results||[]
|
||||
currentSaveItem=items[idx]
|
||||
if(!currentSaveItem)return
|
||||
|
||||
document.getElementById('progressSteps').style.display='block'
|
||||
document.getElementById('shareContent').style.display='none'
|
||||
document.getElementById('saveError').style.display='none'
|
||||
document.getElementById('copyBtn2').style.display='none'
|
||||
|
||||
const title=(currentSaveItem.title||'').replace(/【[^】]+】/g,'').trim()||'资源'
|
||||
document.getElementById('shareTitle').textContent=title
|
||||
|
||||
// Show modal
|
||||
document.getElementById('overlay').style.display='block'
|
||||
document.getElementById('shareModal').style.display='block'
|
||||
|
||||
// Reset steps
|
||||
resetSteps()
|
||||
advanceStep(1)
|
||||
|
||||
// Call save API
|
||||
doSave()
|
||||
}
|
||||
|
||||
async function doSave(){
|
||||
try{
|
||||
const res=await fetch('/api/save',{method:'POST',headers:apiHeaders(),body:JSON.stringify({type:'search',source:currentSaveItem,target_cloud:currentSaveItem.cloud_type||'quark'})})
|
||||
const data=await res.json()
|
||||
|
||||
if(!data.success){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=data.message||data.error||'保存失败'
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2
|
||||
advanceStep(2)
|
||||
await sleep(500)
|
||||
|
||||
// Step 3
|
||||
advanceStep(3)
|
||||
await sleep(300)
|
||||
|
||||
if(data.share_url){
|
||||
advanceStep(4)
|
||||
await sleep(200)
|
||||
showShareResult(data)
|
||||
}else{
|
||||
advanceStep(4)
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent='生成分享链接失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=e.message||'保存请求失败'
|
||||
}
|
||||
}
|
||||
|
||||
function showShareResult(data){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('shareContent').style.display='block'
|
||||
|
||||
const link=data.share_url
|
||||
document.getElementById('shareLinkInput').value=link
|
||||
|
||||
const diskLabel=CLOUD_LABELS[currentSaveItem.cloud_type]||'夸克网盘'
|
||||
document.getElementById('qrLabel').textContent=diskLabel+' APP扫码转存'
|
||||
|
||||
// Generate QR
|
||||
const qrContainer=document.getElementById('qrContainer')
|
||||
qrContainer.innerHTML=''
|
||||
new QRCode(qrContainer,{text:link,width:140,height:140})
|
||||
|
||||
// Password
|
||||
const pwd=data.share_pwd||data.sharePwd||''
|
||||
if(pwd){
|
||||
document.getElementById('sharePwdRow').style.display='flex'
|
||||
document.getElementById('sharePwdTag').textContent=pwd
|
||||
}else{
|
||||
document.getElementById('sharePwdRow').style.display='none'
|
||||
}
|
||||
|
||||
document.getElementById('copyBtn2').style.display='inline-block'
|
||||
}
|
||||
|
||||
function resetSteps(){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
el.className='step'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
el.querySelector('.step-status').textContent='等待中'
|
||||
el.querySelector('.step-status').className='step-status wait'
|
||||
}
|
||||
}
|
||||
|
||||
function advanceStep(n){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
if(i<n){
|
||||
el.className='step done'
|
||||
el.querySelector('.step-dot').innerHTML='<span class="step-check">✓</span>'
|
||||
el.querySelector('.step-status').textContent='已完成'
|
||||
el.querySelector('.step-status').className='step-status done'
|
||||
}else if(i===n){
|
||||
el.className='step active'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
const titles=['正在转存到','正在重命名文件(防和谐)','正在生成分享链接']
|
||||
el.querySelector('.step-title').textContent=titles[i-1]+'...'
|
||||
el.querySelector('.step-status').textContent='进行中'
|
||||
el.querySelector('.step-status').className='step-status doing'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms){return new Promise(r=>setTimeout(r,ms))}
|
||||
|
||||
function copyShareLink(){
|
||||
const input=document.getElementById('shareLinkInput')
|
||||
if(!input.value)return
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(input.value).then(()=>showToast('链接已复制')).catch(()=>fallbackCopy(input.value))
|
||||
}else{
|
||||
fallbackCopy(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text){
|
||||
const ta=document.createElement('textarea')
|
||||
ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try{document.execCommand('copy');showToast('链接已复制')}catch{showToast('复制失败',true)}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
|
||||
function openDisclaimer(){
|
||||
window.open('/disclaimer/','_blank')
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
document.getElementById('overlay').style.display='none'
|
||||
document.getElementById('shareModal').style.display='none'
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
}
|
||||
|
||||
// ===== Init =====
|
||||
checkLogin()
|
||||
|
||||
// Add Enter key handler for home search
|
||||
document.getElementById('homeSearchInput').addEventListener('keydown',function(e){if(e.key==='Enter')homeSearch()})
|
||||
// Also add for search view input
|
||||
document.getElementById('searchInput').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch()})
|
||||
|
||||
// Fetch home page data
|
||||
fetch('/api/rankings/categorized').then(r=>r.json()).then(data=>{
|
||||
renderHomePage(data)
|
||||
}).catch(()=>{
|
||||
document.getElementById('homeQuote').textContent='「 学而时习之,不亦说乎。 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---孔子'
|
||||
})
|
||||
|
||||
// Check URL for query
|
||||
const params=new URLSearchParams(window.location.search)
|
||||
const q=params.get('q')
|
||||
if(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
doSearch()
|
||||
}
|
||||
|
||||
// ===== Dark Mode Toggle =====
|
||||
(function() {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'theme-btn';
|
||||
btn.title = '切换暗色模式';
|
||||
var isDark = localStorage.getItem('h5_theme') === 'dark';
|
||||
if (!isDark && window.matchMedia('(prefers-color-scheme: dark)').matches) isDark = true;
|
||||
btn.textContent = isDark ? '☀️' : '🌙';
|
||||
if (isDark) document.documentElement.setAttribute('data-theme', 'dark');
|
||||
btn.onclick = function() {
|
||||
var dark = document.documentElement.getAttribute('data-theme') !== 'dark';
|
||||
document.documentElement.setAttribute('data-theme', dark ? 'dark' : '');
|
||||
localStorage.setItem('h5_theme', dark ? 'dark' : 'light');
|
||||
btn.textContent = dark ? '☀️' : '🌙';
|
||||
};
|
||||
document.body.appendChild(btn);
|
||||
})();
|
||||
@@ -1,160 +0,0 @@
|
||||
<!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" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>CloudSearch - 搜索</title>
|
||||
<script>
|
||||
// 替换标题为网站名称
|
||||
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){
|
||||
if(cfg.site_name) document.title = cfg.site_name + ' - 搜索';
|
||||
}).catch(function(){});
|
||||
</script>
|
||||
<script src="/h5/qrcode.min.js"></script>
|
||||
<link rel="stylesheet" href="/h5/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<!-- ===== Home Page (shown when no search) ===== -->
|
||||
<div id="homePage" class="home-page">
|
||||
<div class="home-hero">
|
||||
<div class="home-logo" id="homeLogo" style="display:none"></div>
|
||||
<div class="home-search-box">
|
||||
<input id="homeSearchInput" type="text" placeholder="搜索网盘资源..." />
|
||||
<button id="homeSearchBtn" onclick="homeSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="home-quote" id="homeQuote"></div>
|
||||
<div class="home-quote-author" id="homeQuoteAuthor"></div>
|
||||
</div>
|
||||
<div class="home-rankings" id="homeRankings"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Search Results View ===== -->
|
||||
<div id="searchView" style="display:none">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-row">
|
||||
<a href="/h5" class="header-title-link"><div class="header-title" id="headerTitle" style="display:none">CloudSearch</div></a>
|
||||
<div class="search-wrap">
|
||||
<input id="searchInput" type="text" placeholder="搜索网盘资源..." @keydown="handleKeydown" />
|
||||
<button id="searchBtn" onclick="doSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="header-actions" id="userArea">
|
||||
<template id="userLoggedIn">
|
||||
<span class="user-badge" id="usernameDisplay"></span>
|
||||
<button class="logout-btn-small" onclick="logout()">退出</button>
|
||||
</template>
|
||||
<template id="userLoggedOut">
|
||||
<button class="login-btn-small" onclick="showLogin()">登录</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Bar -->
|
||||
<div id="infoBar" class="info-bar" style="display:none">
|
||||
<span id="infoCount" class="count"></span>
|
||||
<span id="infoTime" class="time"></span>
|
||||
<span id="infoFiltered" class="badge-err"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="loading" class="loading" style="display:none">
|
||||
<div id="loadingText">🔍 正在搜索中...</div>
|
||||
<div class="loading-bar"><div class="loading-bar-inner" id="loadingBar"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div id="tabs" class="tabs" style="display:none"></div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" class="results"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="overlay" id="overlay" style="display:none" onclick="closeModal()"></div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<div class="modal" id="shareModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr" id="shareTitle">分享链接</div>
|
||||
<div class="modal-body">
|
||||
<div id="progressSteps" class="steps" style="display:none">
|
||||
<div class="step" id="step1"><div class="step-dot"><span>1</span></div><div class="step-body"><span class="step-title">正在转存...</span><span class="step-status doing">进行中</span></div></div>
|
||||
<div class="step" id="step2"><div class="step-dot"><span>2</span></div><div class="step-body"><span class="step-title">重命名文件(防和谐)...</span><span class="step-status wait">等待中</span></div></div>
|
||||
<div class="step" id="step3"><div class="step-dot"><span>3</span></div><div class="step-body"><span class="step-title">生成分享链接...</span><span class="step-status wait">等待中</span></div></div>
|
||||
</div>
|
||||
<div id="saveError" class="error-alert" style="display:none"></div>
|
||||
<div id="shareContent" style="display:none">
|
||||
<div class="share-qr">
|
||||
<div id="qrContainer"></div>
|
||||
<div class="qr-label" id="qrLabel"></div>
|
||||
<div class="qr-sub">保存到你自己的网盘</div>
|
||||
</div>
|
||||
<div class="share-section">
|
||||
<div class="share-row">
|
||||
<input id="shareLinkInput" type="text" readonly />
|
||||
</div>
|
||||
<div id="sharePwdRow" class="share-pwd" style="display:none">
|
||||
<span>🔑 提取密码:</span>
|
||||
<span class="pwd-tag" id="sharePwdTag"></span>
|
||||
<span class="pwd-hint">打开链接后需输入密码</span>
|
||||
</div>
|
||||
<div class="share-tip">
|
||||
<span class="warn-icon">⚠️</span>
|
||||
<div class="tip-text">
|
||||
<strong>请尽快复制链接到浏览器打开</strong> 或 <strong>用夸克APP扫码</strong><br>
|
||||
<strong>转存至您的网盘,以免资源被官方和谐</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warning-box">
|
||||
<p class="warning-item">郑重警告一:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告二:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告三:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告四:以上警告说三遍,你还要明知故犯吗?</p>
|
||||
</div>
|
||||
<div class="share-disclaimer">
|
||||
<span>⚠️ 本站资源仅供学习交流,请于24h内删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-disclaimer" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
<button class="btn-close" onclick="closeModal()">关闭</button>
|
||||
<button class="btn-primary" id="copyBtn2" onclick="copyShareLink()" style="display:none">一键复制链接</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div class="modal" id="loginModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr">登录</div>
|
||||
<div class="modal-body">
|
||||
<div class="login-form">
|
||||
<input id="loginUser" type="text" placeholder="用户名" />
|
||||
<input id="loginPass" type="password" placeholder="密码" />
|
||||
<button class="login-btn" id="loginBtn" onclick="handleLogin()">登录</button>
|
||||
<div class="login-err" id="loginErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-close" onclick="closeLogin()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="/h5/qrcode.min.js"></script>
|
||||
<script src="/h5/app.js"></script>
|
||||
<!-- Footer -->
|
||||
<div id="siteFooter" class="site-footer" style="display:none">
|
||||
<div id="footerContent" class="footer-inner"></div>
|
||||
<div class="footer-actions" id="footerActions">
|
||||
<button class="footer-btn" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
1
source_clean/frontend-src/h5/qrcode.min.js
vendored
1
source_clean/frontend-src/h5/qrcode.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,179 +0,0 @@
|
||||
/* ===== Dark Mode ===== */[data-theme="dark"] body { background:#1a1a1a;color:#e5e5e5 }[data-theme="dark"] .rank-block,[data-theme="dark"] .card,[data-theme="dark"] .modal,[data-theme="dark"] .header { background:#1f1f1f;border-color:#333 }[data-theme="dark"] input,[data-theme="dark"] .home-search-box,[data-theme="dark"] .search-wrap { background:#2a2a2a;color:#e5e5e5;border-color:#333 }[data-theme="dark"] .rank-block-title,[data-theme="dark"] .card-title,[data-theme="dark"] .rank-name,[data-theme="dark"] .modal-hdr { color:#e5e5e5 }[data-theme="dark"] .card-meta,[data-theme="dark"] .rank-cnt,[data-theme="dark"] .info-bar { color:#999 }[data-theme="dark"] .rank-tab,[data-theme="dark"] .tab { background:#333;color:#999 }[data-theme="dark"] .rank-tab.active,[data-theme="dark"] .tab.active { background:#409eff;color:#fff }[data-theme="dark"] .site-footer { background:#1a1a1a;border-color:#333;color:#999 }[data-theme="dark"] .rank-block-ftr { border-color:#333;color:#666 }[data-theme="dark"] .rank-idx { background:#333;color:#999 }[data-theme="dark"] .theme-btn { position:fixed;bottom:20px;right:20px;z-index:99;width:40px;height:40px;border-radius:50%;border:1px solid #555;background:#1f1f1f;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 2px 8px rgba(0,0,0,.3) }
|
||||
/* ===== Reset & Base ===== */
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html{font-size:16px;-webkit-text-size-adjust:100%}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;background:#f5f5f5;color:#303133;min-height:100vh;overflow-x:hidden}
|
||||
:root{--primary:#409eff;--primary-dark:#337ecc;--primary-light:rgba(64,158,255,0.08);--text:#303133;--text2:#909399;--border:#ebeef5;--bg:#f5f5f5;--white:#fff;--radius:10px;--shadow:0 1px 4px rgba(0,0,0,0.04);--safe-bottom:env(safe-area-inset-bottom,0px)}
|
||||
a{color:var(--primary);text-decoration:none}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
/* ===== Home Page ===== */
|
||||
.home-page{padding-bottom:calc(30px + var(--safe-bottom))}
|
||||
.home-hero{display:flex;flex-direction:column;align-items:center;padding:36px 16px 20px}
|
||||
.home-logo{font-size:32px;font-weight:700;color:var(--primary);margin-bottom:20px;text-align:center}
|
||||
.home-logo-img{max-width:360px;max-height:80px;width:auto;height:auto;object-fit:contain}
|
||||
.home-search-box{display:flex;width:100%;max-width:500px;border:1px solid var(--border);border-radius:20px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.home-search-box:focus-within{border-color:var(--primary);background:var(--white);box-shadow:0 0 0 3px rgba(64,158,255,.1)}
|
||||
.home-search-box input{flex:1;height:40px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.home-search-box button{flex-shrink:0;height:32px;margin:4px;padding:0 22px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer}
|
||||
.home-search-box button:active{background:var(--primary-dark)}
|
||||
.home-quote{margin-top:12px;font-size:12px;color:#b0b8c4;font-style:italic;text-align:center;max-width:500px;line-height:1.5}
|
||||
.home-quote-author{font-size:11px;color:#c0c4cc;display:inline-block;margin-top:2px}
|
||||
|
||||
/* ===== Home Rankings ===== */
|
||||
.home-rankings{padding:8px 12px;display:flex;flex-direction:column;gap:10px}
|
||||
.rank-block{background:var(--white);border-radius:var(--radius);padding:12px;border:1px solid var(--border);box-shadow:var(--shadow)}
|
||||
.rank-block-hdr{display:flex;align-items:center;justify-content:space-between;padding-bottom:8px;border-bottom:2px solid #f0f0f0;margin-bottom:4px}
|
||||
.rank-block-title{font-size:14px;font-weight:700;color:var(--text);white-space:nowrap}
|
||||
.rank-block-tabs{display:flex;gap:2px;background:#f0f2f5;border-radius:5px;padding:2px}
|
||||
.rank-tab{font-size:11px;padding:2px 9px;border-radius:4px;cursor:pointer;color:#909399;font-weight:500;transition:all .2s;user-select:none}
|
||||
.rank-tab.active{background:var(--white);color:var(--primary);font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
||||
.rank-item{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:6px;cursor:pointer;transition:background .15s}
|
||||
.rank-item:active{background:#f0f5ff}
|
||||
.rank-idx{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#909399;background:#f0f0f0;flex-shrink:0}
|
||||
.rank-idx.top3{background:var(--primary);color:var(--white);font-size:12px}
|
||||
.rank-name{flex:1;min-width:0;font-size:13px;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rank-cnt{font-size:11px;color:#c0c4cc;white-space:nowrap;flex-shrink:0}
|
||||
.rank-expand{text-align:center;padding:5px;margin-top:2px;font-size:12px;color:var(--primary);cursor:pointer;border-radius:5px;user-select:none}
|
||||
.rank-expand:active{background:#ecf5ff}
|
||||
.rank-block-ftr{margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;align-items:center;justify-content:space-between;font-size:10px;color:#c0c4cc}
|
||||
.ftr-time{font-family:monospace;font-size:9px}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.app{max-width:100%;margin:0 auto;padding-bottom:calc(20px + var(--safe-bottom))}
|
||||
.header{position:sticky;top:0;z-index:50;background:var(--white);border-bottom:1px solid var(--border);padding:8px 12px}
|
||||
.header-row{display:flex;align-items:center;gap:10px}
|
||||
.header-title{font-size:18px;font-weight:700;color:var(--primary);flex-shrink:0}
|
||||
.header-title-link{text-decoration:none;flex-shrink:0;display:flex;align-items:center}
|
||||
.header-logo-img{max-width:120px;max-height:28px;width:auto;height:auto;object-fit:contain;display:block}
|
||||
.header-actions{margin-left:auto;display:flex;gap:6px;align-items:center}
|
||||
|
||||
/* ===== Search Bar ===== */
|
||||
.search-wrap{flex:1;display:flex;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.search-wrap:focus-within{border-color:var(--primary);background:var(--white)}
|
||||
.search-wrap input{flex:1;height:36px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.search-wrap button{flex-shrink:0;height:28px;margin:4px;padding:0 18px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.search-wrap button:active{background:var(--primary-dark)}
|
||||
.search-wrap button:disabled{opacity:.5}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.site-footer{margin-top:30px;padding:16px 12px 24px;background:#f9fafb;border-top:1px solid var(--border)}
|
||||
.footer-inner{max-width:500px;margin:0 auto;font-size:11px;line-height:1.8;color:#909399;text-align:center;white-space:pre-line}
|
||||
.footer-actions{display:flex;justify-content:center;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||||
.footer-btn{padding:8px 20px;border:1px solid var(--border);border-radius:8px;background:var(--white);color:var(--text);font-size:13px;cursor:pointer;transition:all .2s}
|
||||
.footer-btn:active{background:var(--primary-light);border-color:var(--primary);color:var(--primary)}
|
||||
|
||||
/* ===== Info Bar ===== */
|
||||
.info-bar{display:flex;align-items:center;gap:8px;padding:10px 12px 0;font-size:12px;color:var(--text2);flex-wrap:wrap}
|
||||
.info-bar .count{font-weight:600;color:var(--text)}
|
||||
.info-bar .time{font-family:monospace;background:#f4f4f5;padding:1px 6px;border-radius:4px}
|
||||
.info-bar .badge-err{background:#fef0f0;color:#f56c6c;padding:1px 6px;border-radius:4px}
|
||||
|
||||
/* ===== Loading ===== */
|
||||
.loading{padding:24px 12px;text-align:center;font-size:13px;color:var(--text2)}
|
||||
.loading-bar{width:100%;height:3px;background:#e8e8e8;border-radius:2px;overflow:hidden;margin-top:8px}
|
||||
.loading-bar-inner{height:100%;background:linear-gradient(90deg,var(--primary),#67c23a);border-radius:2px;transition:width .3s ease;width:0%}
|
||||
|
||||
/* ===== Tabs ===== */
|
||||
.tabs{display:flex;gap:4px;padding:8px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
||||
.tabs::-webkit-scrollbar{display:none}
|
||||
.tab{flex-shrink:0;padding:5px 12px;border-radius:16px;font-size:12px;color:#606266;background:#f0f2f5;cursor:pointer;white-space:nowrap;transition:all .2s;user-select:none}
|
||||
.tab:active{transform:scale(.95)}
|
||||
.tab.active{background:var(--primary-light);color:var(--primary);font-weight:600}
|
||||
|
||||
/* ===== Results ===== */
|
||||
.results{display:flex;flex-direction:column;gap:10px;padding:8px 12px}
|
||||
.empty{padding:40px 12px;text-align:center;color:var(--text2);font-size:14px}
|
||||
|
||||
/* ===== Card ===== */
|
||||
.card{display:flex;gap:10px;background:var(--white);border-radius:var(--radius);padding:10px;border:1px solid var(--border);transition:border-color .2s}
|
||||
.card:active{border-color:#c0c4cc}
|
||||
.card-cover{flex-shrink:0;width:90px;height:120px;border-radius:8px;overflow:hidden;background:var(--bg);position:relative}
|
||||
.card-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.card-cover .placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px;background:linear-gradient(135deg,#667eea,#764ba2);color:rgba(255,255,255,.6)}
|
||||
.card-cover .tag{position:absolute;bottom:3px;left:3px;padding:1px 5px;border-radius:3px;color:#fff;font-size:10px;font-weight:600;backdrop-filter:blur(2px)}
|
||||
.card-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.card-title{font-size:14px;font-weight:700;color:var(--text);line-height:1.3;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.card-meta{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:8px}
|
||||
.card-meta .size{color:#67c23a}
|
||||
.card-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
|
||||
.card-tags span{font-size:10px;padding:1px 6px;border-radius:4px;background:#ecf5ff;color:#409eff;white-space:nowrap}
|
||||
.card-tags .quality{background:#fef0f0;color:#e74c3c}
|
||||
.card-actions{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:6px}
|
||||
.card-source{font-size:10px;color:var(--text2);background:#f4f4f5;padding:1px 6px;border-radius:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}
|
||||
.card-btn{padding:4px 10px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background .2s}
|
||||
.card-btn:active{background:var(--primary-dark)}
|
||||
.card-btn:disabled{opacity:.5}
|
||||
|
||||
/* ===== Toast ===== */
|
||||
.toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.75);color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:200;pointer-events:none;opacity:0;transition:opacity .3s}
|
||||
.toast.show{opacity:1}
|
||||
.toast.error{background:rgba(245,108,108,.9)}
|
||||
|
||||
/* ===== Overlay / Modal ===== */
|
||||
.overlay{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;animation:fadeIn .2s}
|
||||
.modal{background:var(--white);border-radius:14px;width:100%;max-width:420px;max-height:90vh;overflow-y:auto;padding:0;animation:slideUp .25s}
|
||||
.modal-hdr{padding:14px 16px;border-bottom:1px solid var(--border);font-size:15px;font-weight:700;color:var(--text)}
|
||||
.modal-body{padding:14px 16px}
|
||||
.modal-ftr{padding:10px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
||||
.modal-ftr button{height:36px;padding:0 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}
|
||||
.modal-ftr .btn-close{background:#f4f4f5;color:#606266}
|
||||
.modal-ftr .btn-disclaimer{background:#fdf6ec;color:#d46b08;margin-right:auto}
|
||||
.modal-ftr .btn-primary{background:var(--primary);color:var(--white)}
|
||||
.modal-ftr .btn-primary:active{background:var(--primary-dark)}
|
||||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes slideUp{from{transform:translateY(40px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
|
||||
/* ===== Share Modal ===== */
|
||||
.share-section{display:flex;flex-direction:column;gap:10px}
|
||||
.share-row{display:flex;gap:8px}
|
||||
.share-row input{flex:1;height:36px;border:1px solid var(--border);border-radius:6px;padding:0 10px;font-size:13px;outline:none;background:var(--bg);color:var(--text)}
|
||||
.share-row input:focus{border-color:var(--primary)}
|
||||
.share-row .copy-btn{height:36px;padding:0 12px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
|
||||
.share-pwd{display:flex;align-items:center;gap:6px;font-size:13px}
|
||||
.share-pwd .pwd-tag{padding:2px 8px;background:#fdf6ec;color:#e6a23c;border-radius:4px;font-weight:700}
|
||||
.share-pwd .pwd-hint{font-size:11px;color:var(--text2)}
|
||||
.share-tip{padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;line-height:1.5;color:#d46b08;display:flex;gap:6px;align-items:flex-start}
|
||||
.share-tip .warn-icon{font-size:18px;line-height:1.5;flex-shrink:0}
|
||||
.share-tip .tip-text{flex:1;min-width:0}
|
||||
.share-tip strong{font-weight:700}
|
||||
.warning-box{background:#fff2f0;border:1px solid #ffccc7;border-radius:8px;padding:8px 10px;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.warning-item{margin:0;font-size:12px;line-height:1.8;font-weight:700;white-space:nowrap}
|
||||
.warning-item:nth-child(odd){color:#cf1322}
|
||||
.warning-item:nth-child(even){color:#d46b08}
|
||||
.warning-item:last-child{color:#b71c1c;font-size:13px}
|
||||
.share-qr{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px}
|
||||
.share-qr canvas{border-radius:8px}
|
||||
.share-qr .qr-label{font-size:12px;font-weight:600;color:var(--primary)}
|
||||
.share-qr .qr-sub{font-size:11px;color:var(--text2)}
|
||||
.share-disclaimer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:10px;padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;color:#d46b08;flex-wrap:wrap}
|
||||
|
||||
/* ===== Login Modal ===== */
|
||||
.login-form{display:flex;flex-direction:column;gap:12px}
|
||||
.login-form input{height:40px;border:1px solid var(--border);border-radius:8px;padding:0 12px;font-size:14px;outline:none}
|
||||
.login-form input:focus{border-color:var(--primary)}
|
||||
.login-form .login-btn{height:40px;border:none;border-radius:8px;background:var(--primary);color:var(--white);font-size:14px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.login-form .login-btn:active{background:var(--primary-dark)}
|
||||
.login-form .login-btn:disabled{opacity:.5}
|
||||
.login-form .login-err{font-size:12px;color:#f56c6c;text-align:center}
|
||||
|
||||
/* ===== Progress Steps ===== */
|
||||
.steps{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
||||
.step{display:flex;align-items:flex-start;gap:10px;opacity:.4;transition:opacity .3s}
|
||||
.step.active{opacity:1}
|
||||
.step.done{opacity:.7}
|
||||
.step-dot{flex-shrink:0;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;background:#e4e7ed;color:#909399}
|
||||
.step.active .step-dot{background:var(--primary);color:#fff;box-shadow:0 0 0 3px rgba(64,158,255,.2)}
|
||||
.step.done .step-dot{background:#67c23a;color:#fff}
|
||||
.step-body{flex:1;padding-top:3px;display:flex;align-items:center;gap:8px}
|
||||
.step-title{font-size:13px;color:var(--text);font-weight:500}
|
||||
.step-status{font-size:11px;padding:1px 7px;border-radius:10px;white-space:nowrap}
|
||||
.step-status.doing{background:#ecf5ff;color:var(--primary)}
|
||||
.step-status.done{background:#f0f9eb;color:#67c23a}
|
||||
.step-status.wait{background:#f4f4f5;color:#c0c4cc}
|
||||
.error-alert{padding:12px 16px;background:#fef0f0;border:1px solid #fde2e2;border-radius:8px;display:flex;align-items:center;gap:8px;font-size:14px;color:#f56c6c}
|
||||
|
||||
/* ===== User Badge ===== */
|
||||
.user-badge{font-size:12px;color:var(--primary);font-weight:600;white-space:nowrap}
|
||||
.login-btn-small{height:30px;padding:0 10px;border:none;border-radius:6px;background:var(--primary-light);color:var(--primary);font-size:12px;font-weight:600;cursor:pointer}
|
||||
.logout-btn-small{height:30px;padding:0 8px;border:none;border-radius:6px;background:transparent;color:var(--text2);font-size:12px;cursor:pointer}
|
||||
@@ -2,23 +2,17 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>CloudSearch - 网盘资源搜索</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<script>
|
||||
(function() {
|
||||
// 替换标题为网站名称
|
||||
fetch('/api/site-config').then(r=>r.json()).then(cfg=>{
|
||||
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){
|
||||
if(cfg.site_name) document.title = cfg.site_name + ' - 网盘资源搜索';
|
||||
}).catch(function(){});
|
||||
// 跳过:参数 ?desktop=1 强制使用桌面版
|
||||
if (window.location.search.includes('desktop=1')) return;
|
||||
var ua = navigator.userAgent;
|
||||
var isMobile = /Android|webOS|iPhone|iPod|BlackBerry|IEMobile|Opera Mini|Mobile/i.test(ua);
|
||||
var isTablet = /iPad|Android(?!.*Mobile)/i.test(ua);
|
||||
if (isMobile || isTablet) {
|
||||
window.location.replace(window.location.origin + '/h5');
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
@@ -26,4 +20,4 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -1,923 +0,0 @@
|
||||
<!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" />
|
||||
<meta http-equiv="Cache-Control" content="no-cache, no-store, must-revalidate" />
|
||||
<meta http-equiv="Pragma" content="no-cache" />
|
||||
<meta http-equiv="Expires" content="0" />
|
||||
<title>CloudSearch - 搜索</title>
|
||||
<script>
|
||||
// 替换标题为网站名称
|
||||
fetch('/api/site-config').then(function(r){return r.json()}).then(function(cfg){
|
||||
if(cfg.site_name) document.title = cfg.site_name + ' - 搜索';
|
||||
}).catch(function(){});
|
||||
</script>
|
||||
<script src="/h5/qrcode.min.js"></script>
|
||||
<style>
|
||||
/* ===== Reset & Base ===== */
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
html{font-size:16px;-webkit-text-size-adjust:100%}
|
||||
body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;background:#f5f5f5;color:#303133;min-height:100vh;overflow-x:hidden}
|
||||
:root{--primary:#409eff;--primary-dark:#337ecc;--primary-light:rgba(64,158,255,0.08);--text:#303133;--text2:#909399;--border:#ebeef5;--bg:#f5f5f5;--white:#fff;--radius:10px;--shadow:0 1px 4px rgba(0,0,0,0.04);--safe-bottom:env(safe-area-inset-bottom,0px)}
|
||||
a{color:var(--primary);text-decoration:none}
|
||||
img{display:block;max-width:100%}
|
||||
|
||||
/* ===== Home Page ===== */
|
||||
.home-page{padding-bottom:calc(30px + var(--safe-bottom))}
|
||||
.home-hero{display:flex;flex-direction:column;align-items:center;padding:36px 16px 20px}
|
||||
.home-logo{font-size:32px;font-weight:700;color:var(--primary);margin-bottom:20px;text-align:center}
|
||||
.home-logo-img{max-width:360px;max-height:80px;width:auto;height:auto;object-fit:contain}
|
||||
.home-search-box{display:flex;width:100%;max-width:500px;border:1px solid var(--border);border-radius:20px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.home-search-box:focus-within{border-color:var(--primary);background:var(--white);box-shadow:0 0 0 3px rgba(64,158,255,.1)}
|
||||
.home-search-box input{flex:1;height:40px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.home-search-box button{flex-shrink:0;height:32px;margin:4px;padding:0 22px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer}
|
||||
.home-search-box button:active{background:var(--primary-dark)}
|
||||
.home-quote{margin-top:12px;font-size:12px;color:#b0b8c4;font-style:italic;text-align:center;max-width:500px;line-height:1.5}
|
||||
.home-quote-author{font-size:11px;color:#c0c4cc;display:inline-block;margin-top:2px}
|
||||
|
||||
/* ===== Home Rankings ===== */
|
||||
.home-rankings{padding:8px 12px;display:flex;flex-direction:column;gap:10px}
|
||||
.rank-block{background:var(--white);border-radius:var(--radius);padding:12px;border:1px solid var(--border);box-shadow:var(--shadow)}
|
||||
.rank-block-hdr{display:flex;align-items:center;justify-content:space-between;padding-bottom:8px;border-bottom:2px solid #f0f0f0;margin-bottom:4px}
|
||||
.rank-block-title{font-size:14px;font-weight:700;color:var(--text);white-space:nowrap}
|
||||
.rank-block-tabs{display:flex;gap:2px;background:#f0f2f5;border-radius:5px;padding:2px}
|
||||
.rank-tab{font-size:11px;padding:2px 9px;border-radius:4px;cursor:pointer;color:#909399;font-weight:500;transition:all .2s;user-select:none}
|
||||
.rank-tab.active{background:var(--white);color:var(--primary);font-weight:600;box-shadow:0 1px 2px rgba(0,0,0,.06)}
|
||||
.rank-item{display:flex;align-items:center;gap:7px;padding:5px 6px;border-radius:6px;cursor:pointer;transition:background .15s}
|
||||
.rank-item:active{background:#f0f5ff}
|
||||
.rank-idx{width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:700;color:#909399;background:#f0f0f0;flex-shrink:0}
|
||||
.rank-idx.top3{background:var(--primary);color:var(--white);font-size:12px}
|
||||
.rank-name{flex:1;min-width:0;font-size:13px;font-weight:500;color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.rank-cnt{font-size:11px;color:#c0c4cc;white-space:nowrap;flex-shrink:0}
|
||||
.rank-expand{text-align:center;padding:5px;margin-top:2px;font-size:12px;color:var(--primary);cursor:pointer;border-radius:5px;user-select:none}
|
||||
.rank-expand:active{background:#ecf5ff}
|
||||
.rank-block-ftr{margin-top:6px;padding-top:6px;border-top:1px solid #f0f0f0;display:flex;align-items:center;justify-content:space-between;font-size:10px;color:#c0c4cc}
|
||||
.ftr-time{font-family:monospace;font-size:9px}
|
||||
|
||||
/* ===== Layout ===== */
|
||||
.app{max-width:100%;margin:0 auto;padding-bottom:calc(20px + var(--safe-bottom))}
|
||||
.header{position:sticky;top:0;z-index:50;background:var(--white);border-bottom:1px solid var(--border);padding:8px 12px}
|
||||
.header-row{display:flex;align-items:center;gap:10px}
|
||||
.header-title{font-size:18px;font-weight:700;color:var(--primary);flex-shrink:0}
|
||||
.header-title-link{text-decoration:none;flex-shrink:0;display:flex;align-items:center}
|
||||
.header-logo-img{max-width:160px;max-height:36px;width:auto;height:auto;object-fit:contain;display:block}
|
||||
.header-actions{margin-left:auto;display:flex;gap:6px;align-items:center}
|
||||
|
||||
/* ===== Search Bar ===== */
|
||||
.search-wrap{flex:1;display:flex;border:1px solid var(--border);border-radius:18px;overflow:hidden;background:var(--bg);transition:border-color .2s}
|
||||
.search-wrap:focus-within{border-color:var(--primary);background:var(--white)}
|
||||
.search-wrap input{flex:1;height:36px;border:none;padding:0 14px;font-size:14px;outline:none;background:transparent}
|
||||
.search-wrap button{flex-shrink:0;height:28px;margin:4px;padding:0 18px;border:none;border-radius:999px;background:var(--primary);color:var(--white);font-size:13px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.search-wrap button:active{background:var(--primary-dark)}
|
||||
.search-wrap button:disabled{opacity:.5}
|
||||
|
||||
/* ===== Footer ===== */
|
||||
.site-footer{margin-top:30px;padding:16px 12px 24px;background:#f9fafb;border-top:1px solid var(--border)}
|
||||
.footer-inner{max-width:500px;margin:0 auto;font-size:11px;line-height:1.8;color:#909399;text-align:center;white-space:pre-line}
|
||||
.footer-actions{display:flex;justify-content:center;gap:10px;margin-top:12px;flex-wrap:wrap}
|
||||
.footer-btn{padding:8px 20px;border:1px solid var(--border);border-radius:8px;background:var(--white);color:var(--text);font-size:13px;cursor:pointer;transition:all .2s}
|
||||
.footer-btn:active{background:var(--primary-light);border-color:var(--primary);color:var(--primary)}
|
||||
|
||||
/* ===== Info Bar ===== */
|
||||
.info-bar{display:flex;align-items:center;gap:8px;padding:10px 12px 0;font-size:12px;color:var(--text2);flex-wrap:wrap}
|
||||
.info-bar .count{font-weight:600;color:var(--text)}
|
||||
.info-bar .time{font-family:monospace;background:#f4f4f5;padding:1px 6px;border-radius:4px}
|
||||
.info-bar .badge-err{background:#fef0f0;color:#f56c6c;padding:1px 6px;border-radius:4px}
|
||||
|
||||
/* ===== Loading ===== */
|
||||
.loading{padding:24px 12px;text-align:center;font-size:13px;color:var(--text2)}
|
||||
.loading-bar{width:100%;height:3px;background:#e8e8e8;border-radius:2px;overflow:hidden;margin-top:8px}
|
||||
.loading-bar-inner{height:100%;background:linear-gradient(90deg,var(--primary),#67c23a);border-radius:2px;transition:width .3s ease;width:0%}
|
||||
|
||||
/* ===== Tabs ===== */
|
||||
.tabs{display:flex;gap:4px;padding:8px 12px;overflow-x:auto;-webkit-overflow-scrolling:touch;scrollbar-width:none}
|
||||
.tabs::-webkit-scrollbar{display:none}
|
||||
.tab{flex-shrink:0;padding:5px 12px;border-radius:16px;font-size:12px;color:#606266;background:#f0f2f5;cursor:pointer;white-space:nowrap;transition:all .2s;user-select:none}
|
||||
.tab:active{transform:scale(.95)}
|
||||
.tab.active{background:var(--primary-light);color:var(--primary);font-weight:600}
|
||||
|
||||
/* ===== Results ===== */
|
||||
.results{display:flex;flex-direction:column;gap:10px;padding:8px 12px}
|
||||
.empty{padding:40px 12px;text-align:center;color:var(--text2);font-size:14px}
|
||||
|
||||
/* ===== Card ===== */
|
||||
.card{display:flex;gap:10px;background:var(--white);border-radius:var(--radius);padding:10px;border:1px solid var(--border);transition:border-color .2s}
|
||||
.card:active{border-color:#c0c4cc}
|
||||
.card-cover{flex-shrink:0;width:90px;height:120px;border-radius:8px;overflow:hidden;background:var(--bg);position:relative}
|
||||
.card-cover img{width:100%;height:100%;object-fit:cover}
|
||||
.card-cover .placeholder{width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px;background:linear-gradient(135deg,#667eea,#764ba2);color:rgba(255,255,255,.6)}
|
||||
.card-cover .tag{position:absolute;bottom:3px;left:3px;padding:1px 5px;border-radius:3px;color:#fff;font-size:10px;font-weight:600;backdrop-filter:blur(2px)}
|
||||
.card-body{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
|
||||
.card-title{font-size:14px;font-weight:700;color:var(--text);line-height:1.3;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden}
|
||||
.card-meta{font-size:11px;color:var(--text2);display:flex;align-items:center;gap:8px}
|
||||
.card-meta .size{color:#67c23a}
|
||||
.card-tags{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}
|
||||
.card-tags span{font-size:10px;padding:1px 6px;border-radius:4px;background:#ecf5ff;color:#409eff;white-space:nowrap}
|
||||
.card-tags .quality{background:#fef0f0;color:#e74c3c}
|
||||
.card-actions{margin-top:auto;display:flex;align-items:center;justify-content:space-between;gap:6px}
|
||||
.card-source{font-size:10px;color:var(--text2);background:#f4f4f5;padding:1px 6px;border-radius:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:120px}
|
||||
.card-btn{padding:4px 10px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap;transition:background .2s}
|
||||
.card-btn:active{background:var(--primary-dark)}
|
||||
.card-btn:disabled{opacity:.5}
|
||||
|
||||
/* ===== Toast ===== */
|
||||
.toast{position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:rgba(0,0,0,.75);color:#fff;padding:10px 20px;border-radius:8px;font-size:14px;z-index:200;pointer-events:none;opacity:0;transition:opacity .3s}
|
||||
.toast.show{opacity:1}
|
||||
.toast.error{background:rgba(245,108,108,.9)}
|
||||
|
||||
/* ===== Overlay / Modal ===== */
|
||||
.overlay{position:fixed;inset:0;z-index:100;background:rgba(0,0,0,.5);display:flex;align-items:center;justify-content:center;padding:16px;animation:fadeIn .2s}
|
||||
.modal{background:var(--white);border-radius:14px;width:100%;max-width:420px;max-height:90vh;overflow-y:auto;padding:0;animation:slideUp .25s}
|
||||
.modal-hdr{padding:14px 16px;border-bottom:1px solid var(--border);font-size:15px;font-weight:700;color:var(--text)}
|
||||
.modal-body{padding:14px 16px}
|
||||
.modal-ftr{padding:10px 16px;border-top:1px solid var(--border);display:flex;gap:8px;justify-content:flex-end}
|
||||
.modal-ftr button{height:36px;padding:0 16px;border:none;border-radius:8px;font-size:13px;font-weight:600;cursor:pointer}
|
||||
.modal-ftr .btn-close{background:#f4f4f5;color:#606266}
|
||||
.modal-ftr .btn-disclaimer{background:#fdf6ec;color:#d46b08;margin-right:auto}
|
||||
.modal-ftr .btn-primary{background:var(--primary);color:var(--white)}
|
||||
.modal-ftr .btn-primary:active{background:var(--primary-dark)}
|
||||
@keyframes fadeIn{from{opacity:0}to{opacity:1}}
|
||||
@keyframes slideUp{from{transform:translateY(40px);opacity:0}to{transform:translateY(0);opacity:1}}
|
||||
|
||||
/* ===== Share Modal ===== */
|
||||
.share-section{display:flex;flex-direction:column;gap:10px}
|
||||
.share-row{display:flex;gap:8px}
|
||||
.share-row input{flex:1;height:36px;border:1px solid var(--border);border-radius:6px;padding:0 10px;font-size:13px;outline:none;background:var(--bg);color:var(--text)}
|
||||
.share-row input:focus{border-color:var(--primary)}
|
||||
.share-row .copy-btn{height:36px;padding:0 12px;border:none;border-radius:6px;background:var(--primary);color:var(--white);font-size:12px;font-weight:600;cursor:pointer;white-space:nowrap}
|
||||
.share-pwd{display:flex;align-items:center;gap:6px;font-size:13px}
|
||||
.share-pwd .pwd-tag{padding:2px 8px;background:#fdf6ec;color:#e6a23c;border-radius:4px;font-weight:700}
|
||||
.share-pwd .pwd-hint{font-size:11px;color:var(--text2)}
|
||||
.share-tip{padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;line-height:1.5;color:#d46b08;display:flex;gap:6px;align-items:flex-start}
|
||||
.share-tip .warn-icon{font-size:18px;line-height:1.5;flex-shrink:0}
|
||||
.share-tip .tip-text{flex:1;min-width:0}
|
||||
.share-tip strong{font-weight:700}
|
||||
.warning-box{background:#fff2f0;border:1px solid #ffccc7;border-radius:8px;padding:8px 10px;overflow-x:auto;-webkit-overflow-scrolling:touch}
|
||||
.warning-item{margin:0;font-size:12px;line-height:1.8;font-weight:700;white-space:nowrap}
|
||||
.warning-item:nth-child(odd){color:#cf1322}
|
||||
.warning-item:nth-child(even){color:#d46b08}
|
||||
.warning-item:last-child{color:#b71c1c;font-size:13px}
|
||||
.share-qr{display:flex;flex-direction:column;align-items:center;gap:4px;padding:8px}
|
||||
.share-qr canvas{border-radius:8px}
|
||||
.share-qr .qr-label{font-size:12px;font-weight:600;color:var(--primary)}
|
||||
.share-qr .qr-sub{font-size:11px;color:var(--text2)}
|
||||
.share-disclaimer{display:flex;align-items:center;justify-content:center;gap:8px;margin-top:10px;padding:8px 10px;background:#fdf6ec;border-radius:6px;font-size:12px;color:#d46b08;flex-wrap:wrap}
|
||||
|
||||
/* ===== Login Modal ===== */
|
||||
.login-form{display:flex;flex-direction:column;gap:12px}
|
||||
.login-form input{height:40px;border:1px solid var(--border);border-radius:8px;padding:0 12px;font-size:14px;outline:none}
|
||||
.login-form input:focus{border-color:var(--primary)}
|
||||
.login-form .login-btn{height:40px;border:none;border-radius:8px;background:var(--primary);color:var(--white);font-size:14px;font-weight:600;cursor:pointer;transition:background .2s}
|
||||
.login-form .login-btn:active{background:var(--primary-dark)}
|
||||
.login-form .login-btn:disabled{opacity:.5}
|
||||
.login-form .login-err{font-size:12px;color:#f56c6c;text-align:center}
|
||||
|
||||
/* ===== Progress Steps ===== */
|
||||
.steps{display:flex;flex-direction:column;gap:10px;padding:8px 0}
|
||||
.step{display:flex;align-items:flex-start;gap:10px;opacity:.4;transition:opacity .3s}
|
||||
.step.active{opacity:1}
|
||||
.step.done{opacity:.7}
|
||||
.step-dot{flex-shrink:0;width:26px;height:26px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;background:#e4e7ed;color:#909399}
|
||||
.step.active .step-dot{background:var(--primary);color:#fff;box-shadow:0 0 0 3px rgba(64,158,255,.2)}
|
||||
.step.done .step-dot{background:#67c23a;color:#fff}
|
||||
.step-body{flex:1;padding-top:3px;display:flex;align-items:center;gap:8px}
|
||||
.step-title{font-size:13px;color:var(--text);font-weight:500}
|
||||
.step-status{font-size:11px;padding:1px 7px;border-radius:10px;white-space:nowrap}
|
||||
.step-status.doing{background:#ecf5ff;color:var(--primary)}
|
||||
.step-status.done{background:#f0f9eb;color:#67c23a}
|
||||
.step-status.wait{background:#f4f4f5;color:#c0c4cc}
|
||||
.error-alert{padding:12px 16px;background:#fef0f0;border:1px solid #fde2e2;border-radius:8px;display:flex;align-items:center;gap:8px;font-size:14px;color:#f56c6c}
|
||||
|
||||
/* ===== User Badge ===== */
|
||||
.user-badge{font-size:12px;color:var(--primary);font-weight:600;white-space:nowrap}
|
||||
.login-btn-small{height:30px;padding:0 10px;border:none;border-radius:6px;background:var(--primary-light);color:var(--primary);font-size:12px;font-weight:600;cursor:pointer}
|
||||
.logout-btn-small{height:30px;padding:0 8px;border:none;border-radius:6px;background:transparent;color:var(--text2);font-size:12px;cursor:pointer}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app" id="app">
|
||||
<!-- ===== Home Page (shown when no search) ===== -->
|
||||
<div id="homePage" class="home-page">
|
||||
<div class="home-hero">
|
||||
<div class="home-logo" id="homeLogo" style="display:none"></div>
|
||||
<div class="home-search-box">
|
||||
<input id="homeSearchInput" type="text" placeholder="搜索网盘资源..." />
|
||||
<button id="homeSearchBtn" onclick="homeSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="home-quote" id="homeQuote"></div>
|
||||
<div class="home-quote-author" id="homeQuoteAuthor"></div>
|
||||
</div>
|
||||
<div class="home-rankings" id="homeRankings"></div>
|
||||
</div>
|
||||
|
||||
<!-- ===== Search Results View ===== -->
|
||||
<div id="searchView" style="display:none">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<div class="header-row">
|
||||
<a href="/h5" class="header-title-link"><div class="header-title" id="headerTitle" style="display:none">CloudSearch</div></a>
|
||||
<div class="search-wrap">
|
||||
<input id="searchInput" type="text" placeholder="搜索网盘资源..." @keydown="handleKeydown" />
|
||||
<button id="searchBtn" onclick="doSearch()">搜 索</button>
|
||||
</div>
|
||||
<div class="header-actions" id="userArea">
|
||||
<template id="userLoggedIn">
|
||||
<span class="user-badge" id="usernameDisplay"></span>
|
||||
<button class="logout-btn-small" onclick="logout()">退出</button>
|
||||
</template>
|
||||
<template id="userLoggedOut">
|
||||
<button class="login-btn-small" onclick="showLogin()">登录</button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Info Bar -->
|
||||
<div id="infoBar" class="info-bar" style="display:none">
|
||||
<span id="infoCount" class="count"></span>
|
||||
<span id="infoTime" class="time"></span>
|
||||
<span id="infoFiltered" class="badge-err"></span>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div id="loading" class="loading" style="display:none">
|
||||
<div id="loadingText">🔍 正在搜索中...</div>
|
||||
<div class="loading-bar"><div class="loading-bar-inner" id="loadingBar"></div></div>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div id="tabs" class="tabs" style="display:none"></div>
|
||||
|
||||
<!-- Results -->
|
||||
<div id="results" class="results"></div>
|
||||
|
||||
<!-- Overlay -->
|
||||
<div class="overlay" id="overlay" style="display:none" onclick="closeModal()"></div>
|
||||
|
||||
<!-- Share Modal -->
|
||||
<div class="modal" id="shareModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr" id="shareTitle">分享链接</div>
|
||||
<div class="modal-body">
|
||||
<div id="progressSteps" class="steps" style="display:none">
|
||||
<div class="step" id="step1"><div class="step-dot"><span>1</span></div><div class="step-body"><span class="step-title">正在转存...</span><span class="step-status doing">进行中</span></div></div>
|
||||
<div class="step" id="step2"><div class="step-dot"><span>2</span></div><div class="step-body"><span class="step-title">重命名文件(防和谐)...</span><span class="step-status wait">等待中</span></div></div>
|
||||
<div class="step" id="step3"><div class="step-dot"><span>3</span></div><div class="step-body"><span class="step-title">生成分享链接...</span><span class="step-status wait">等待中</span></div></div>
|
||||
</div>
|
||||
<div id="saveError" class="error-alert" style="display:none"></div>
|
||||
<div id="shareContent" style="display:none">
|
||||
<div class="share-qr">
|
||||
<div id="qrContainer"></div>
|
||||
<div class="qr-label" id="qrLabel"></div>
|
||||
<div class="qr-sub">保存到你自己的网盘</div>
|
||||
</div>
|
||||
<div class="share-section">
|
||||
<div class="share-row">
|
||||
<input id="shareLinkInput" type="text" readonly />
|
||||
</div>
|
||||
<div id="sharePwdRow" class="share-pwd" style="display:none">
|
||||
<span>🔑 提取密码:</span>
|
||||
<span class="pwd-tag" id="sharePwdTag"></span>
|
||||
<span class="pwd-hint">打开链接后需输入密码</span>
|
||||
</div>
|
||||
<div class="share-tip">
|
||||
<span class="warn-icon">⚠️</span>
|
||||
<div class="tip-text">
|
||||
<strong>请尽快复制链接到浏览器打开</strong> 或 <strong>用夸克APP扫码</strong><br>
|
||||
<strong>转存至您的网盘,以免资源被官方和谐</strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="warning-box">
|
||||
<p class="warning-item">郑重警告一:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告二:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告三:网盘内除您所需资源外,不要打开任何不相关内容。</p>
|
||||
<p class="warning-item">郑重警告四:以上警告说三遍,你还要明知故犯吗?</p>
|
||||
</div>
|
||||
<div class="share-disclaimer">
|
||||
<span>⚠️ 本站资源仅供学习交流,请于24h内删除</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-disclaimer" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
<button class="btn-close" onclick="closeModal()">关闭</button>
|
||||
<button class="btn-primary" id="copyBtn2" onclick="copyShareLink()" style="display:none">一键复制链接</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Login Modal -->
|
||||
<div class="modal" id="loginModal" style="display:none;position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);z-index:101">
|
||||
<div class="modal-hdr">登录</div>
|
||||
<div class="modal-body">
|
||||
<div class="login-form">
|
||||
<input id="loginUser" type="text" placeholder="用户名" />
|
||||
<input id="loginPass" type="password" placeholder="密码" />
|
||||
<button class="login-btn" id="loginBtn" onclick="handleLogin()">登录</button>
|
||||
<div class="login-err" id="loginErr"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-ftr">
|
||||
<button class="btn-close" onclick="closeLogin()">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Toast -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script>
|
||||
// ===== Anime keywords for categorization =====
|
||||
const ANIME_KWS=['仙逆','凡人修仙传','斗破苍穹','斗破','盘龙','完美世界','一念永恒','妖神记','星辰变','遮天','神墓','吞噬星空','武动乾坤','大主宰','全职高手','鬼灭之刃','海贼王','火影忍者','死神','龙珠','进击的巨人','咒术回战','一人之下','狐妖小红娘','魔道祖师','天官赐福','时光代理人','大王饶命','斗罗大陆','绝世唐门','不良人','秦时明月','全职法师','牧神记','三体','灵笼','雾山五行','凡人','仙王的日常生活','百妖谱','眷思量','镖人','伍六七','刺客伍六七','葬送的芙莉莲','间谍过家家']
|
||||
|
||||
// ===== Quotes =====
|
||||
const QUOTES=['学而时习之,不亦说乎。','温故而知新,可以为师矣。','三人行,必有我师焉。','学而不思则罔,思而不学则殆。','博学之,审问之,慎思之,明辨之,笃行之。','千里之行,始于足下。','不积跬步,无以至千里。','知之为知之,不知为不知,是知也。','工欲善其事,必先利其器。','玉不琢,不成器;人不学,不知道。','学以致用,知行合一。','学海无涯,勤作舟。','书山有路,勤为径。','宝剑锋从磨砺出,梅花香自苦寒来。','锲而不舍,金石可镂。','业精于勤,荒于嬉。','读书破万卷,下笔如有神。','路漫漫其修远兮,吾将上下而求索。','采菊东篱下,悠然见南山。','海内存知己,天涯若比邻。','长风破浪会有时,直挂云帆济沧海。','会当凌绝顶,一览众山小。','山重水复疑无路,柳暗花明又一村。']
|
||||
|
||||
// ===== Home Page =====
|
||||
function homeSearch(){
|
||||
const q=document.getElementById('homeSearchInput').value.trim()
|
||||
if(q)doSearchFromHome(q)
|
||||
}
|
||||
|
||||
function doSearchFromHome(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
doSearch()
|
||||
}
|
||||
function renderHomePage(data){
|
||||
fetch("/api/site-config").then(r=>r.json()).then(cfg=>{
|
||||
// 显示 Logo(优先图片,其次文字)
|
||||
var logoEl=document.getElementById("homeLogo");
|
||||
var headerEl=document.getElementById("headerTitle");
|
||||
if(cfg.site_logo){
|
||||
logoEl.innerHTML='<img src="'+cfg.site_logo+'" class="home-logo-img" alt="logo" />';
|
||||
logoEl.style.display="";
|
||||
headerEl.innerHTML='<img src="'+cfg.site_logo+'" class="header-logo-img" alt="logo" />';
|
||||
headerEl.style.display="";
|
||||
}else if(cfg.site_name){
|
||||
logoEl.textContent=cfg.site_name;
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent=cfg.site_name;
|
||||
headerEl.style.display="";
|
||||
}else{
|
||||
logoEl.textContent="CloudSearch";
|
||||
logoEl.style.display="";
|
||||
headerEl.textContent="CloudSearch";
|
||||
headerEl.style.display="";
|
||||
}
|
||||
if(cfg.site_disclaimer){
|
||||
document.getElementById("footerContent").innerHTML=cfg.site_disclaimer.replace(/\n/g,'<br>');
|
||||
document.getElementById("siteFooter").style.display="block";
|
||||
}
|
||||
}).catch(()=>{})
|
||||
const categories=data.categories||[]
|
||||
const fetchedAt=data.fetchedAt||''
|
||||
// Quote
|
||||
fetch('https://v1.hitokoto.cn/').then(r=>r.json()).then(d=>{
|
||||
document.getElementById('homeQuote').textContent='「 '+d.hitokoto+' 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---'+(d.from_who||d.from||'')
|
||||
}).catch(()=>{
|
||||
document.getElementById('homeQuote').textContent='「 学而时习之,不亦说乎。 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---孔子'
|
||||
})
|
||||
|
||||
// Store expanded state per category
|
||||
window.__expanded=window.__expanded||{}
|
||||
window.__activeTab=window.__activeTab||{}
|
||||
|
||||
const el=document.getElementById('homeRankings')
|
||||
let html=''
|
||||
for(const cat of categories){
|
||||
const icons={movie:'🎬',tv:'📺',western_movie:'🎥',western:'🌍',donghua:'🐉',global_anime:'🌐',variety:'🎤',niche:'💎',hotsite:'🏆'}
|
||||
const icon=icons[cat.category]||'📋'
|
||||
const key=cat.category
|
||||
if(!window.__activeTab[key])window.__activeTab[key]='hot'
|
||||
|
||||
html+='<div class="rank-block">'
|
||||
html+='<div class="rank-block-hdr">'+
|
||||
'<span class="rank-block-title">'+icon+' '+cat.label+'</span>'+
|
||||
'<div class="rank-block-tabs" id="rtabs-'+key+'">'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='hot'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'hot\')">热榜</span>'+
|
||||
'<span class="rank-tab'+(window.__activeTab[key]==='newest'?' active':'')+'" onclick="switchRankTab(\''+key+'\',\'newest\')">最新</span>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
html+='<div class="rank-block-items" id="ritems-'+key+'" data-hot=\''+JSON.stringify({items:cat.hot||[]}).replace(/'/g,"'")+'\' data-newest=\''+JSON.stringify({items:cat.newest||[]}).replace(/'/g,"'")+'\'>'
|
||||
const items=window.__activeTab[key]==='hot'?(cat.hot||[]):(cat.newest||[])
|
||||
html+=renderRankItems(items,key,false)
|
||||
html+='</div>'
|
||||
// 数据来源
|
||||
html+='<div class="rank-block-ftr">'+
|
||||
'<span>'+(cat.category!=='hotsite'?'数据来源:TMDB':'本站搜索数据')+'</span>'+
|
||||
'<span class="ftr-time">'+fetchedAt+'</span>'+
|
||||
'</div></div>'
|
||||
}
|
||||
el.innerHTML=html
|
||||
}
|
||||
|
||||
function renderRankItems(items,key,expanded){
|
||||
if(!items||items.length===0)return'<div style="padding:10px;text-align:center;color:#c0c4cc;font-size:12px">暂无数据</div>'
|
||||
const limit=3
|
||||
const show=expanded?items.length:Math.min(limit,items.length)
|
||||
let html=items.slice(0,show).map((item,i)=>{
|
||||
const c=i<3?' rank-idx top3':' rank-idx'
|
||||
return '<div class="rank-item" onclick="doSearchFromHome(\''+item.keyword.replace(/'/g,"\\'")+'\')">'+
|
||||
'<span class="'+c+'">'+(i+1)+'</span>'+
|
||||
'<span class="rank-name">'+item.keyword+'</span>'+
|
||||
'<span class="rank-cnt">'+(item.rating?'⭐'+item.rating:item.searchCount)+'</span>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
if(items.length>limit&&!expanded){
|
||||
html+='<div class="rank-expand" onclick="expandRank(\''+key+'\')">展开全部 ▼</div>'
|
||||
}
|
||||
return html
|
||||
}
|
||||
|
||||
function expandRank(key){
|
||||
const container=document.getElementById('ritems-'+key)
|
||||
if(!container)return
|
||||
const tab=window.__activeTab[key]||'hot'
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,key,true)
|
||||
}
|
||||
|
||||
function switchRankTab(category,tab){
|
||||
window.__activeTab[category]=tab
|
||||
const tabsContainer=document.getElementById('rtabs-'+category)
|
||||
if(tabsContainer){
|
||||
tabsContainer.querySelectorAll('.rank-tab').forEach(t=>t.className='rank-tab')
|
||||
tabsContainer.querySelector(tab==='hot'?'.rank-tab:first-child':'.rank-tab:last-child').className='rank-tab active'
|
||||
}
|
||||
const container=document.getElementById('ritems-'+category)
|
||||
if(container){
|
||||
const data=JSON.parse(tab==='hot'?container.dataset.hot:container.dataset.newest)
|
||||
container.innerHTML=renderRankItems(data.items,category,false)
|
||||
}
|
||||
}
|
||||
let userInfo = null
|
||||
let allResults = []
|
||||
let allChannels = []
|
||||
let activeTab = ''
|
||||
let currentSaveItem = null
|
||||
const CLOUD_ICONS = {quark:'☁️',baidu:'🔵',aliyun:'🟠','115':'🟣',tianyi:'🔷','123pan':'🔴',uc:'🟡',xunlei:'🟢',pikpak:'🟤',magnet:'🧲',ed2k:'🔗',others:'📁'}
|
||||
const CLOUD_LABELS = {quark:'夸克网盘',baidu:'百度网盘',aliyun:'阿里云盘','115':'115网盘',tianyi:'天翼云盘','123pan':'123云盘',uc:'UC网盘',xunlei:'迅雷云盘',pikpak:'PikPak',magnet:'磁力链接',ed2k:'电驴链接',others:'其他'}
|
||||
const CLOUD_COLORS = {quark:'#07c160',baidu:'#4e6ef2',aliyun:'#ff6a00','115':'#9b59b6',tianyi:'#00a1d6','123pan':'#e74c3c',uc:'#f39c12',xunlei:'#2ecc71',pikpak:'#8e44ad',magnet:'#95a5a6',ed2k:'#7f8c8d',others:'#95a5a6'}
|
||||
const CLOUD_ORDER = {quark:1,baidu:2,aliyun:3,'115':4,tianyi:5,'123pan':6,uc:7,xunlei:8,pikpak:9,magnet:10,ed2k:11,others:12}
|
||||
|
||||
// ===== Fetch helpers =====
|
||||
function getToken(){return localStorage.getItem('h5_admin_token')}
|
||||
function apiHeaders(){const h={'Content-Type':'application/json'};const t=getToken();if(t)h['Authorization']='Bearer '+t;return h}
|
||||
|
||||
// ===== Toast =====
|
||||
let toastTimer
|
||||
function showToast(msg,isError){
|
||||
const el=document.getElementById('toast')
|
||||
el.textContent=msg
|
||||
el.className='toast show'+(isError?' error':'')
|
||||
clearTimeout(toastTimer)
|
||||
toastTimer=setTimeout(()=>el.className='toast',2000)
|
||||
}
|
||||
|
||||
// ===== User =====
|
||||
async function checkLogin(){
|
||||
try{
|
||||
const res=await fetch('/api/me',{headers:apiHeaders()})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
if(data.loggedIn){
|
||||
userInfo=data
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+data.username+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
}
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
|
||||
function logout(){
|
||||
localStorage.removeItem('h5_admin_token')
|
||||
userInfo=null
|
||||
document.getElementById('userArea').innerHTML='<button class="login-btn-small" onclick="showLogin()">登录</button>'
|
||||
showToast('已退出')
|
||||
}
|
||||
|
||||
function showLogin(){
|
||||
document.getElementById('loginErr').textContent=''
|
||||
document.getElementById('loginUser').value=''
|
||||
document.getElementById('loginPass').value=''
|
||||
document.getElementById('loginModal').style.display='block'
|
||||
document.getElementById('overlay').style.display='block'
|
||||
}
|
||||
|
||||
function closeLogin(){
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
document.getElementById('overlay').style.display='none'
|
||||
}
|
||||
|
||||
async function handleLogin(){
|
||||
const user=document.getElementById('loginUser').value.trim()
|
||||
const pass=document.getElementById('loginPass').value
|
||||
if(!user||!pass){showToast('请输入用户名和密码',true);return}
|
||||
const btn=document.getElementById('loginBtn')
|
||||
btn.disabled=true;btn.textContent='登录中...'
|
||||
try{
|
||||
const res=await fetch('/api/admin/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({username:user,password:pass})})
|
||||
if(res.ok){
|
||||
const data=await res.json()
|
||||
localStorage.setItem('h5_admin_token',data.token)
|
||||
userInfo={username:user}
|
||||
document.getElementById('userArea').innerHTML='<span class="user-badge">'+user+'</span><button class="logout-btn-small" onclick="logout()">退出</button>'
|
||||
closeLogin()
|
||||
showToast('登录成功')
|
||||
}else{
|
||||
const err=await res.json().catch(()=>({}))
|
||||
document.getElementById('loginErr').textContent=err.error||'登录失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('loginErr').textContent='网络错误'
|
||||
}finally{
|
||||
btn.disabled=false;btn.textContent='登录'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Search =====
|
||||
function handleKeydown(e){if(e.key==='Enter')doSearch()}
|
||||
|
||||
let searchTimer
|
||||
function doSearch(){
|
||||
const q=document.getElementById('searchInput').value.trim()
|
||||
if(!q)return
|
||||
// Update URL
|
||||
window.history.replaceState({},'','/h5?q='+encodeURIComponent(q))
|
||||
// Show loading
|
||||
document.getElementById('results').innerHTML=''
|
||||
document.getElementById('tabs').style.display='none'
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
document.getElementById('loading').style.display='block'
|
||||
document.getElementById('loadingText').textContent='🔍 正在搜索中...'
|
||||
document.getElementById('searchBtn').disabled=true
|
||||
|
||||
let progress=0
|
||||
const bar=document.getElementById('loadingBar')
|
||||
const progressTimer=setInterval(()=>{
|
||||
if(progress<60)progress+=1+Math.random()*3
|
||||
else if(progress<85)progress+=0.5+Math.random()
|
||||
bar.style.width=progress+'%'
|
||||
},200)
|
||||
|
||||
// Use streaming search for live updates
|
||||
streamSearch(q,progressTimer,bar)
|
||||
}
|
||||
|
||||
async function streamSearch(q,progressTimer,bar){
|
||||
const startTime=Date.now()
|
||||
try{
|
||||
const response=await fetch('/api/query',{method:'POST',headers:apiHeaders(),body:JSON.stringify({q})})
|
||||
if(!response.ok)throw new Error('搜索失败 ('+response.status+')')
|
||||
|
||||
const reader=response.body.getReader()
|
||||
const decoder=new TextDecoder()
|
||||
let buffer=''
|
||||
let allItems=[]
|
||||
let channels=[]
|
||||
let totalCount=0
|
||||
let filteredCount=0
|
||||
|
||||
while(true){
|
||||
const {done,value}=await reader.read()
|
||||
if(done)break
|
||||
|
||||
buffer+=decoder.decode(value,{stream:true})
|
||||
const lines=buffer.split('\n')
|
||||
buffer=lines.pop()||''
|
||||
|
||||
for(const line of lines){
|
||||
if(!line.trim())continue
|
||||
try{
|
||||
const msg=JSON.parse(line)
|
||||
if(msg.type==='stats'){
|
||||
totalCount=msg.total||0
|
||||
filteredCount=msg.filtered||0
|
||||
document.getElementById('loadingText').textContent='🔍 搜索到 '+totalCount+' 条,正在验证...'
|
||||
}else if(msg.type==='result'){
|
||||
if(msg.valid&&msg.id){
|
||||
allItems.push(msg.id)
|
||||
}
|
||||
}else if(msg.type==='complete'){
|
||||
const results=msg.results||[]
|
||||
channels=msg.channels||[]
|
||||
clearInterval(progressTimer)
|
||||
bar.style.width='100%'
|
||||
setTimeout(()=>renderResults(results,channels,totalCount,filteredCount,Date.now()-startTime),300)
|
||||
return
|
||||
}
|
||||
}catch(e){}
|
||||
}
|
||||
}
|
||||
}catch(e){
|
||||
clearInterval(progressTimer)
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
document.getElementById('results').innerHTML='<div class="empty">搜索失败:'+e.message+'</div>'
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Render =====
|
||||
function renderResults(results,channels,totalCount,filteredCount,time){
|
||||
document.getElementById('loading').style.display='none'
|
||||
document.getElementById('searchBtn').disabled=false
|
||||
allResults=results
|
||||
allChannels=channels||[]
|
||||
|
||||
// Info bar
|
||||
if(totalCount>0){
|
||||
document.getElementById('infoBar').style.display='flex'
|
||||
document.getElementById('infoCount').textContent='已为您挑选到最符合 '+totalCount+' 条结果'
|
||||
document.getElementById('infoTime').textContent='⏱ '+time+'ms'
|
||||
if(filteredCount>0)document.getElementById('infoFiltered').textContent='❌ 失效 '+filteredCount
|
||||
else document.getElementById('infoFiltered').textContent=''
|
||||
}else{
|
||||
document.getElementById('infoBar').style.display='none'
|
||||
}
|
||||
|
||||
// Build tabs
|
||||
const tabsEl=document.getElementById('tabs')
|
||||
tabsEl.innerHTML=''
|
||||
const typeCounts={}
|
||||
for(const r of results){const ct=r.cloud_type||'others';typeCounts[ct]=(typeCounts[ct]||0)+1}
|
||||
const sorted=Object.keys(typeCounts).sort((a,b)=>(CLOUD_ORDER[a]||99)-(CLOUD_ORDER[b]||99))
|
||||
// "全部" tab
|
||||
const allTab=document.createElement('div')
|
||||
allTab.className='tab active'
|
||||
allTab.textContent='📋 全部 ('+results.length+')'
|
||||
allTab.onclick=()=>{setActiveTab('');renderCardList(results)}
|
||||
tabsEl.appendChild(allTab)
|
||||
for(const ct of sorted){
|
||||
const tab=document.createElement('div')
|
||||
tab.className='tab'
|
||||
tab.textContent=(CLOUD_ICONS[ct]||'📁')+' '+(CLOUD_LABELS[ct]||ct)+' ('+typeCounts[ct]+')'
|
||||
tab.onclick=()=>{setActiveTab(ct);renderCardList(results.filter(r=>(r.cloud_type||'others')===ct))}
|
||||
tabsEl.appendChild(tab)
|
||||
}
|
||||
tabsEl.style.display=results.length>0?'flex':'none'
|
||||
activeTab=''
|
||||
|
||||
// Render cards
|
||||
renderCardList(results)
|
||||
}
|
||||
|
||||
function setActiveTab(ct){
|
||||
activeTab=ct
|
||||
document.querySelectorAll('.tab').forEach((t,i)=>{
|
||||
const isAll=i===0&&!ct
|
||||
const active=i>0&&ct&&t.textContent.includes(CLOUD_LABELS[ct])
|
||||
t.className='tab'+(active||isAll?' active':'')
|
||||
})
|
||||
}
|
||||
|
||||
function renderCardList(items){
|
||||
const el=document.getElementById('results')
|
||||
if(items.length===0){
|
||||
el.innerHTML='<div class="empty">暂无结果</div>'
|
||||
return
|
||||
}
|
||||
el.innerHTML=items.map((item,idx)=>{
|
||||
const coverHtml=item.cover
|
||||
? '<img src="'+escapeHtml(item.cover)+'" alt="" onerror="this.parentElement.innerHTML=\'<div class=placeholder>'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>\'" loading="lazy" />'
|
||||
: '<div class="placeholder">'+escapeHtml(CLOUD_ICONS[item.cloud_type||'others'])+'</div>'
|
||||
const cloudLabel=CLOUD_LABELS[item.cloud_type]||item.cloud_type||''
|
||||
const cloudColor=CLOUD_COLORS[item.cloud_type]||'#95a5a6'
|
||||
const tags=extractTags(item.title||'')
|
||||
const cleanTitle=(item.title||'').replace(/【[^】]+】/g,'').trim()
|
||||
const relativeTime=formatTime(item.update_time||item.datetime||'')
|
||||
return '<div class="card" onclick="saveItem('+idx+')">'+
|
||||
'<div class="card-cover">'+coverHtml+'<span class="tag" style="background:'+cloudColor+'">'+cloudLabel+'</span></div>'+
|
||||
'<div class="card-body">'+
|
||||
'<div class="card-title">'+escapeHtml(cleanTitle)+'</div>'+
|
||||
'<div class="card-meta"><span>🕐 '+relativeTime+'</span>'+(item.file_size?'<span class="size">📦 '+escapeHtml(item.file_size)+'</span>':'')+'</div>'+
|
||||
(tags.length>0?'<div class="card-tags">'+tags.map(t=>'<span'+(isQualityTag(t)?' class="quality"':'')+'>'+escapeHtml(t)+'</span>').join('')+'</div>':'')+
|
||||
'<div class="card-actions">'+
|
||||
'<span class="card-source">'+(item.source?escapeHtml(item.source):'网盘')+'</span>'+
|
||||
'<button class="card-btn" onclick="event.stopPropagation();saveItem('+idx+')">🔗 获取分享链接</button>'+
|
||||
'</div>'+
|
||||
'</div>'+
|
||||
'</div>'
|
||||
}).join('')
|
||||
|
||||
// Store items for save reference
|
||||
window.__h5Results=items
|
||||
}
|
||||
|
||||
function escapeHtml(s){if(!s)return '';return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"')}
|
||||
|
||||
function extractTags(title){
|
||||
const tags=[]
|
||||
// Quality tags
|
||||
const quality=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','WEB-DL','WEBRip']
|
||||
for(const q of quality){if(title.includes(q)&&!tags.includes(q))tags.push(q)}
|
||||
const kw=['杜比视界','杜比全景声','高码率','内封简繁英字幕','内嵌字幕','中文字幕','中英字幕']
|
||||
for(const k of kw){if(title.includes(k)&&!tags.includes(k))tags.push(k)}
|
||||
return tags.slice(0,6)
|
||||
}
|
||||
|
||||
function isQualityTag(t){const q=['4K','1080P','2160P','720P','480P','HDR','HDR10','BluRay','REMUX','HEVC','x264','x265','臻彩','高清','WEB-DL','WEBRip'];return q.includes(t)}
|
||||
|
||||
function formatTime(s){
|
||||
if(!s)return ''
|
||||
const d=new Date(s)
|
||||
if(isNaN(d.getTime()))return s.slice(0,10)
|
||||
const diff=Date.now()-d.getTime()
|
||||
if(diff<0)return s.slice(0,10)
|
||||
const mins=Math.floor(diff/60000)
|
||||
if(mins<60)return mins<=1?'刚刚':mins+' 分钟前'
|
||||
const hours=Math.floor(mins/60)
|
||||
if(hours<24)return hours+' 小时前'
|
||||
const days=Math.floor(hours/24)
|
||||
if(days<30)return days+' 天前'
|
||||
return Math.floor(days/30)+' 个月前'
|
||||
}
|
||||
|
||||
// ===== Save / Share =====
|
||||
function saveItem(idx){
|
||||
const items=window.__h5Results||[]
|
||||
currentSaveItem=items[idx]
|
||||
if(!currentSaveItem)return
|
||||
|
||||
document.getElementById('progressSteps').style.display='block'
|
||||
document.getElementById('shareContent').style.display='none'
|
||||
document.getElementById('saveError').style.display='none'
|
||||
document.getElementById('copyBtn2').style.display='none'
|
||||
|
||||
const title=(currentSaveItem.title||'').replace(/【[^】]+】/g,'').trim()||'资源'
|
||||
document.getElementById('shareTitle').textContent=title
|
||||
|
||||
// Show modal
|
||||
document.getElementById('overlay').style.display='block'
|
||||
document.getElementById('shareModal').style.display='block'
|
||||
|
||||
// Reset steps
|
||||
resetSteps()
|
||||
advanceStep(1)
|
||||
|
||||
// Call save API
|
||||
doSave()
|
||||
}
|
||||
|
||||
async function doSave(){
|
||||
try{
|
||||
const res=await fetch('/api/save',{method:'POST',headers:apiHeaders(),body:JSON.stringify({type:'search',source:currentSaveItem,target_cloud:currentSaveItem.cloud_type||'quark'})})
|
||||
const data=await res.json()
|
||||
|
||||
if(!data.success){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=data.message||data.error||'保存失败'
|
||||
return
|
||||
}
|
||||
|
||||
// Step 2
|
||||
advanceStep(2)
|
||||
await sleep(500)
|
||||
|
||||
// Step 3
|
||||
advanceStep(3)
|
||||
await sleep(300)
|
||||
|
||||
if(data.share_url){
|
||||
advanceStep(4)
|
||||
await sleep(200)
|
||||
showShareResult(data)
|
||||
}else{
|
||||
advanceStep(4)
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent='生成分享链接失败'
|
||||
}
|
||||
}catch(e){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('saveError').style.display='flex'
|
||||
document.getElementById('saveError').textContent=e.message||'保存请求失败'
|
||||
}
|
||||
}
|
||||
|
||||
function showShareResult(data){
|
||||
document.getElementById('progressSteps').style.display='none'
|
||||
document.getElementById('shareContent').style.display='block'
|
||||
|
||||
const link=data.share_url
|
||||
document.getElementById('shareLinkInput').value=link
|
||||
|
||||
const diskLabel=CLOUD_LABELS[currentSaveItem.cloud_type]||'夸克网盘'
|
||||
document.getElementById('qrLabel').textContent=diskLabel+' APP扫码转存'
|
||||
|
||||
// Generate QR
|
||||
const qrContainer=document.getElementById('qrContainer')
|
||||
qrContainer.innerHTML=''
|
||||
new QRCode(qrContainer,{text:link,width:140,height:140})
|
||||
|
||||
// Password
|
||||
const pwd=data.share_pwd||data.sharePwd||''
|
||||
if(pwd){
|
||||
document.getElementById('sharePwdRow').style.display='flex'
|
||||
document.getElementById('sharePwdTag').textContent=pwd
|
||||
}else{
|
||||
document.getElementById('sharePwdRow').style.display='none'
|
||||
}
|
||||
|
||||
document.getElementById('copyBtn2').style.display='inline-block'
|
||||
}
|
||||
|
||||
function resetSteps(){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
el.className='step'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
el.querySelector('.step-status').textContent='等待中'
|
||||
el.querySelector('.step-status').className='step-status wait'
|
||||
}
|
||||
}
|
||||
|
||||
function advanceStep(n){
|
||||
for(let i=1;i<=3;i++){
|
||||
const el=document.getElementById('step'+i)
|
||||
if(i<n){
|
||||
el.className='step done'
|
||||
el.querySelector('.step-dot').innerHTML='<span class="step-check">✓</span>'
|
||||
el.querySelector('.step-status').textContent='已完成'
|
||||
el.querySelector('.step-status').className='step-status done'
|
||||
}else if(i===n){
|
||||
el.className='step active'
|
||||
el.querySelector('.step-dot').innerHTML='<span>'+i+'</span>'
|
||||
const titles=['正在转存到','正在重命名文件(防和谐)','正在生成分享链接']
|
||||
el.querySelector('.step-title').textContent=titles[i-1]+'...'
|
||||
el.querySelector('.step-status').textContent='进行中'
|
||||
el.querySelector('.step-status').className='step-status doing'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function sleep(ms){return new Promise(r=>setTimeout(r,ms))}
|
||||
|
||||
function copyShareLink(){
|
||||
const input=document.getElementById('shareLinkInput')
|
||||
if(!input.value)return
|
||||
if(navigator.clipboard&&navigator.clipboard.writeText){
|
||||
navigator.clipboard.writeText(input.value).then(()=>showToast('链接已复制')).catch(()=>fallbackCopy(input.value))
|
||||
}else{
|
||||
fallbackCopy(input.value)
|
||||
}
|
||||
}
|
||||
|
||||
function fallbackCopy(text){
|
||||
const ta=document.createElement('textarea')
|
||||
ta.value=text;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta)
|
||||
ta.select()
|
||||
try{document.execCommand('copy');showToast('链接已复制')}catch{showToast('复制失败',true)}
|
||||
document.body.removeChild(ta)
|
||||
}
|
||||
|
||||
function openDisclaimer(){
|
||||
window.open('/disclaimer/','_blank')
|
||||
}
|
||||
|
||||
function closeModal(){
|
||||
document.getElementById('overlay').style.display='none'
|
||||
document.getElementById('shareModal').style.display='none'
|
||||
document.getElementById('loginModal').style.display='none'
|
||||
}
|
||||
|
||||
// ===== Init =====
|
||||
checkLogin()
|
||||
|
||||
// Add Enter key handler for home search
|
||||
document.getElementById('homeSearchInput').addEventListener('keydown',function(e){if(e.key==='Enter')homeSearch()})
|
||||
// Also add for search view input
|
||||
document.getElementById('searchInput').addEventListener('keydown',function(e){if(e.key==='Enter')doSearch()})
|
||||
|
||||
// Fetch home page data
|
||||
fetch('/api/rankings/categorized').then(r=>r.json()).then(data=>{
|
||||
renderHomePage(data)
|
||||
}).catch(()=>{
|
||||
document.getElementById('homeQuote').textContent='「 学而时习之,不亦说乎。 」'
|
||||
document.getElementById('homeQuoteAuthor').textContent='---孔子'
|
||||
})
|
||||
|
||||
// Check URL for query
|
||||
const params=new URLSearchParams(window.location.search)
|
||||
const q=params.get('q')
|
||||
if(q){
|
||||
document.getElementById('homePage').style.display='none'
|
||||
document.getElementById('searchView').style.display='block'
|
||||
document.getElementById('searchInput').value=q
|
||||
doSearch()
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
<!-- Footer -->
|
||||
<div id="siteFooter" class="site-footer" style="display:none">
|
||||
<div id="footerContent" class="footer-inner"></div>
|
||||
<div class="footer-actions" id="footerActions">
|
||||
<button class="footer-btn" onclick="openDisclaimer()">📜 免责声明</button>
|
||||
</div>
|
||||
</div>
|
||||
</html>
|
||||
@@ -1,28 +1,8 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="isDark ? '切换亮色' : '切换暗色'">
|
||||
{{ isDark ? '☀️' : '🌙' }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const isDark = ref(false)
|
||||
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : '')
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem('theme')
|
||||
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
isDark.value = true
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@@ -68,86 +48,6 @@ onMounted(() => {
|
||||
--sidebar-w: 240px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #141414;
|
||||
--bg-card: #1d1d1d;
|
||||
--bg-input: #2a2a2a;
|
||||
--bg-page: #0f0f0f;
|
||||
--bg-card-header: linear-gradient(135deg, #1d1d1d 0%, #1a1a1a 100%);
|
||||
|
||||
--text: #e5e5e5;
|
||||
--text-secondary: #999999;
|
||||
--text-tertiary: #666666;
|
||||
--text-placeholder: #555555;
|
||||
|
||||
--border: #333333;
|
||||
--border-light: #2a2a2a;
|
||||
|
||||
--primary: #409eff;
|
||||
--primary-hover: #66b1ff;
|
||||
--primary-light: rgba(64, 158, 255, 0.15);
|
||||
--primary-soft: rgba(64, 158, 255, 0.1);
|
||||
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] body { background: var(--bg); color: var(--text); }
|
||||
[data-theme="dark"] .el-card,
|
||||
[data-theme="dark"] .el-dialog,
|
||||
[data-theme="dark"] .el-menu,
|
||||
[data-theme="dark"] .el-table,
|
||||
[data-theme="dark"] .el-select-dropdown,
|
||||
[data-theme="dark"] .el-popover {
|
||||
--el-bg-color: var(--bg-card) !important;
|
||||
--el-border-color: var(--border) !important;
|
||||
--el-text-color-primary: var(--text) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-dialog__body,
|
||||
[data-theme="dark"] .el-table__body td {
|
||||
background-color: var(--bg-card) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-table__header th {
|
||||
background-color: #262626 !important;
|
||||
}
|
||||
[data-theme="dark"] .el-input__wrapper,
|
||||
[data-theme="dark"] .el-select .el-input__wrapper {
|
||||
background-color: var(--bg-input) !important;
|
||||
border-color: var(--border) !important;
|
||||
box-shadow: 0 0 0 1px var(--border) inset !important;
|
||||
}
|
||||
[data-theme="dark"] .el-input__inner,
|
||||
[data-theme="dark"] .el-textarea__inner {
|
||||
background-color: var(--bg-input) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-button--default {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 99;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.theme-toggle:hover { transform: scale(1.1); border-color: var(--primary); }
|
||||
|
||||
#app { min-height: 100vh; background: var(--bg-page); color: var(--text); }
|
||||
|
||||
/* ── Global element-plus overrides ── */
|
||||
@@ -157,35 +57,19 @@ onMounted(() => {
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
.el-card:hover {
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
.el-card:hover { box-shadow: var(--shadow-md) !important; }
|
||||
.el-card__header {
|
||||
padding: 16px 20px !important;
|
||||
border-bottom: 1px solid var(--border-light) !important;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-weight: 600; font-size: 14px;
|
||||
background: var(--bg-card-header);
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
.el-card__body { padding: 20px !important; }
|
||||
|
||||
/* ── Typography helpers ── */
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin: 20px 0;
|
||||
}
|
||||
.form-tip { font-size: 12px; color: var(--text-tertiary); line-height: 1.6; }
|
||||
.page-title { font-size: 20px; font-weight: 700; color: var(--text); }
|
||||
.section-divider { border-top: 1px solid var(--border-light); margin: 20px 0; }
|
||||
|
||||
/* ── Fade transition ── */
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
|
||||
|
||||
193
source_clean/frontend-src/src/App.vue.bak
Executable file
193
source_clean/frontend-src/src/App.vue.bak
Executable file
@@ -0,0 +1,193 @@
|
||||
<template>
|
||||
<router-view />
|
||||
<button class="theme-toggle" @click="toggleTheme" :title="isDark ? '切换亮色' : '切换暗色'">
|
||||
{{ isDark ? '☀️' : '🌙' }}
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const isDark = ref(false)
|
||||
|
||||
function toggleTheme() {
|
||||
isDark.value = !isDark.value
|
||||
document.documentElement.setAttribute('data-theme', isDark.value ? 'dark' : '')
|
||||
localStorage.setItem('theme', isDark.value ? 'dark' : 'light')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const saved = localStorage.getItem('theme')
|
||||
if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
|
||||
isDark.value = true
|
||||
document.documentElement.setAttribute('data-theme', 'dark')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* =============================================
|
||||
CloudSearch Design System — Global Tokens
|
||||
============================================= */
|
||||
:root {
|
||||
/* ── Backgrounds ── */
|
||||
--bg: #f0f2f5;
|
||||
--bg-card: #ffffff;
|
||||
--bg-input: #f5f7fa;
|
||||
--bg-page: #f0f2f5;
|
||||
--bg-card-header: linear-gradient(135deg, #f8f9fc 0%, #eef1f8 100%);
|
||||
|
||||
/* ── Text ── */
|
||||
--text: #1d2129;
|
||||
--text-secondary: #4e5969;
|
||||
--text-tertiary: #86909c;
|
||||
--text-placeholder: #c9cdd4;
|
||||
|
||||
/* ── Borders ── */
|
||||
--border: #e5e6eb;
|
||||
--border-light: #f2f3f5;
|
||||
|
||||
/* ── Primary ── */
|
||||
--primary: #409eff;
|
||||
--primary-hover: #66b1ff;
|
||||
--primary-light: rgba(64, 158, 255, 0.08);
|
||||
--primary-soft: #ecf5ff;
|
||||
|
||||
/* ── Shadows ── */
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.06);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.08);
|
||||
|
||||
/* ── Radius ── */
|
||||
--radius-sm: 6px;
|
||||
--radius-md: 10px;
|
||||
--radius-lg: 14px;
|
||||
--radius-xl: 20px;
|
||||
|
||||
/* ── Sizing ── */
|
||||
--sidebar-w: 240px;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg: #141414;
|
||||
--bg-card: #1d1d1d;
|
||||
--bg-input: #2a2a2a;
|
||||
--bg-page: #0f0f0f;
|
||||
--bg-card-header: linear-gradient(135deg, #1d1d1d 0%, #1a1a1a 100%);
|
||||
|
||||
--text: #e5e5e5;
|
||||
--text-secondary: #999999;
|
||||
--text-tertiary: #666666;
|
||||
--text-placeholder: #555555;
|
||||
|
||||
--border: #333333;
|
||||
--border-light: #2a2a2a;
|
||||
|
||||
--primary: #409eff;
|
||||
--primary-hover: #66b1ff;
|
||||
--primary-light: rgba(64, 158, 255, 0.15);
|
||||
--primary-soft: rgba(64, 158, 255, 0.1);
|
||||
|
||||
--shadow-sm: 0 1px 4px rgba(0, 0, 0, 0.2);
|
||||
--shadow-md: 0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
--shadow-lg: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
[data-theme="dark"] body { background: var(--bg); color: var(--text); }
|
||||
[data-theme="dark"] .el-card,
|
||||
[data-theme="dark"] .el-dialog,
|
||||
[data-theme="dark"] .el-menu,
|
||||
[data-theme="dark"] .el-table,
|
||||
[data-theme="dark"] .el-select-dropdown,
|
||||
[data-theme="dark"] .el-popover {
|
||||
--el-bg-color: var(--bg-card) !important;
|
||||
--el-border-color: var(--border) !important;
|
||||
--el-text-color-primary: var(--text) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-dialog__body,
|
||||
[data-theme="dark"] .el-table__body td {
|
||||
background-color: var(--bg-card) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-table__header th {
|
||||
background-color: #262626 !important;
|
||||
}
|
||||
[data-theme="dark"] .el-input__wrapper,
|
||||
[data-theme="dark"] .el-select .el-input__wrapper {
|
||||
background-color: var(--bg-input) !important;
|
||||
border-color: var(--border) !important;
|
||||
box-shadow: 0 0 0 1px var(--border) inset !important;
|
||||
}
|
||||
[data-theme="dark"] .el-input__inner,
|
||||
[data-theme="dark"] .el-textarea__inner {
|
||||
background-color: var(--bg-input) !important;
|
||||
color: var(--text) !important;
|
||||
}
|
||||
[data-theme="dark"] .el-button--default {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.theme-toggle {
|
||||
position: fixed;
|
||||
bottom: 24px;
|
||||
right: 24px;
|
||||
z-index: 99;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-card);
|
||||
cursor: pointer;
|
||||
font-size: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-md);
|
||||
transition: all 0.3s;
|
||||
}
|
||||
.theme-toggle:hover { transform: scale(1.1); border-color: var(--primary); }
|
||||
|
||||
#app { min-height: 100vh; background: var(--bg-page); color: var(--text); }
|
||||
|
||||
/* ── Global element-plus overrides ── */
|
||||
.el-card {
|
||||
border-radius: var(--radius-lg) !important;
|
||||
border: 1px solid var(--border) !important;
|
||||
box-shadow: var(--shadow-sm) !important;
|
||||
transition: box-shadow 0.2s ease;
|
||||
}
|
||||
.el-card:hover {
|
||||
box-shadow: var(--shadow-md) !important;
|
||||
}
|
||||
.el-card__header {
|
||||
padding: 16px 20px !important;
|
||||
border-bottom: 1px solid var(--border-light) !important;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
background: var(--bg-card-header);
|
||||
}
|
||||
.el-card__body {
|
||||
padding: 20px !important;
|
||||
}
|
||||
|
||||
/* ── Typography helpers ── */
|
||||
.form-tip {
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
}
|
||||
.section-divider {
|
||||
border-top: 1px solid var(--border-light);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
/* ── Fade transition ── */
|
||||
.fade-enter-active, .fade-leave-active { transition: opacity 0.2s ease; }
|
||||
.fade-enter-from, .fade-leave-to { opacity: 0; }
|
||||
</style>
|
||||
@@ -6,354 +6,117 @@
|
||||
<div v-else class="logo-text">{{ siteName || 'CloudSearch' }}</div>
|
||||
</template>
|
||||
<div class="search-box">
|
||||
<el-input
|
||||
v-model="query"
|
||||
placeholder="搜索网盘资源,或粘贴视频/网盘链接..."
|
||||
size="large"
|
||||
clearable
|
||||
@keyup.enter="handleSearch"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-icon><Search /></el-icon>
|
||||
</template>
|
||||
<el-input v-model="query" placeholder="搜索网盘资源..." size="large" clearable @keyup.enter="handleSearch">
|
||||
<template #prefix><el-icon><Search /></el-icon></template>
|
||||
</el-input>
|
||||
<el-button type="primary" size="large" @click="handleSearch" class="search-btn">
|
||||
搜 索
|
||||
</el-button>
|
||||
<el-button type="primary" size="large" @click="handleSearch" class="search-btn">搜索</el-button>
|
||||
</div>
|
||||
<div class="quote-section" v-if="currentQuote">
|
||||
<span class="quote-text">「 {{ currentQuote }} 」</span>
|
||||
<span class="quote-author">---{{ quoteAuthor }}</span>
|
||||
<div class="quote-line" v-if="currentQuote">「 {{ currentQuote }} 」 ---{{ quoteAuthor }}</div>
|
||||
</div>
|
||||
|
||||
<div class="cloud-zone" v-if="words.length > 0">
|
||||
<p class="cloud-title">大家都在搜</p>
|
||||
<div class="word-cloud">
|
||||
<span v-for="w in words" :key="w.k" class="wc-word" :style="{fontSize:w.s+'px',color:w.c,fontWeight:w.w,transform:'rotate('+w.r+'deg)',opacity:w.o,animationDelay:w.d+'s'}" @click="searchTag(w.k)">{{ w.k }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<div v-if="categories.length > 0" class="rankings-grid">
|
||||
<div
|
||||
v-for="cat in categories"
|
||||
:key="cat.category"
|
||||
class="rank-panel"
|
||||
>
|
||||
<div class="panel-header">
|
||||
<span class="panel-title">{{ getCategoryIcon(cat.category) }} {{ cat.label }}</span>
|
||||
<div class="panel-tabs">
|
||||
<span class="panel-tab" :class="{ active: activeTab[cat.category] === 'hot' }" @click="switchTab(cat.category, 'hot')">热榜</span>
|
||||
<span class="panel-tab" :class="{ active: activeTab[cat.category] === 'newest' }" @click="switchTab(cat.category, 'newest')">最新</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div
|
||||
v-for="(item, idx) in visibleItems(cat)"
|
||||
:key="cat.category + '-' + idx"
|
||||
class="rank-item"
|
||||
@click="searchTag(item.keyword)"
|
||||
>
|
||||
<span class="rank-idx" :class="{ 'top-three': idx < 3 }">{{ idx + 1 }}</span>
|
||||
<span class="rank-name">{{ item.keyword }}</span>
|
||||
<span class="rank-cnt">{{ formatCount(item) }}</span>
|
||||
</div>
|
||||
<!-- 展开按钮 -->
|
||||
<div v-if="hasMoreItems(cat)" class="rank-expand" @click="expandCategory(cat.category)">
|
||||
展开全部 ▼
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span v-if="cat.category === 'hotsite'">基于本站搜索数据</span>
|
||||
<span v-else-if="cat.category === 'donghua' || cat.category === 'global_anime'">数据来源:Bilibili</span>
|
||||
<span v-else-if="cat.category === 'movie' || cat.category === 'tv'">数据来源:百度</span>
|
||||
<span v-else>数据来源:TMDB</span>
|
||||
<span class="footer-time">{{ fetchedAt }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="configLoaded" class="empty-area"><p>搜索脉搏采集中...</p></div>
|
||||
|
||||
<div v-if="siteDisclaimer" class="site-footer">
|
||||
<div class="footer-inner">{{ siteDisclaimer }}</div>
|
||||
<div class="footer-actions">
|
||||
<el-button class="footer-disclaimer-btn" size="small" @click="openDisclaimer">📜 免责声明</el-button>
|
||||
</div>
|
||||
<el-button class="footer-btn" size="small" @click="openDisclaimer">免责声明</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Search } from '@element-plus/icons-vue'
|
||||
import { getCategorizedRankings, getSiteConfig } from '../api'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const query = ref('')
|
||||
const categories = ref<any[]>([])
|
||||
const expanded = reactive<Record<string, boolean>>({})
|
||||
const activeTab = reactive<Record<string, string>>({})
|
||||
const configLoaded = ref(false)
|
||||
const siteLogo = ref('')
|
||||
const siteName = ref('')
|
||||
const siteDisclaimer = ref('')
|
||||
const configLoaded = ref(false)
|
||||
const currentQuote = ref('')
|
||||
const quoteAuthor = ref('')
|
||||
const QUOTES = ['学而时习之,不亦说乎。','温故而知新,可以为师矣。','三人行,必有我师焉。','学而不思则罔,思而不学则殆。','博学之,审问之,慎思之,明辨之,笃行之。','千里之行,始于足下。','不积跬步,无以至千里。','知之为知之,不知为不知,是知也。','工欲善其事,必先利其器。','玉不琢,不成器;人不学,不知道。','学以致用,知行合一。','学海无涯,勤作舟。','书山有路,勤为径。','宝剑锋从磨砺出,梅花香自苦寒来。','锲而不舍,金石可镂。','业精于勤,荒于嬉。','读书破万卷,下笔如有神。','路漫漫其修远兮,吾将上下而求索。','采菊东篱下,悠然见南山。','海内存知己,天涯若比邻。','长风破浪会有时,直挂云帆济沧海。','会当凌绝顶,一览众山小。','山重水复疑无路,柳暗花明又一村。']
|
||||
const words = ref<any[]>([])
|
||||
|
||||
const fetchedAt = ref('')
|
||||
const QS = ['学而时习之,不亦说乎。', '温故而知新,可以为师矣。', '千里之行,始于足下。', '书山有路,勤为径。', '学以致用,知行合一。', '路漫漫其修远兮,吾将上下而求索。']
|
||||
const CS = ['#e74c3c', '#e67e22', '#f39c12', '#27ae60', '#2980b9', '#8e44ad', '#2c3e50', '#16a085', '#d35400', '#c0392b', '#1abc9c', '#3498db', '#9b59b6', '#34495e', '#e91e63', '#009688', '#ff5722', '#795548', '#607d8b', '#673ab7']
|
||||
const SS = [48,42,38,34,30,28,26,24,22,20,18,17,16,15,14,13,12]
|
||||
|
||||
const INITIAL_SHOW = 8
|
||||
|
||||
const CATEGORY_ICONS: Record<string, string> = {
|
||||
movie: '🎬', western_movie: '🎥', western_tv: '🌍',
|
||||
donghua: '🐉', global_anime: '🌐',
|
||||
tv: '📺',
|
||||
niche: '💎', hotsite: '🏆',
|
||||
}
|
||||
|
||||
function getCategoryIcon(cat: string): string {
|
||||
return CATEGORY_ICONS[cat] || '📋'
|
||||
}
|
||||
|
||||
function formatCount(item: any): string {
|
||||
const parts: string[] = []
|
||||
if (item.rating) {
|
||||
parts.push(`⭐${item.rating}`)
|
||||
}
|
||||
if (item.searchCount > 0) {
|
||||
const n = item.searchCount
|
||||
if (n >= 100000000) {
|
||||
parts.push(`${(n / 100000000).toFixed(1)}亿`)
|
||||
} else if (n >= 10000) {
|
||||
parts.push(`${(n / 10000).toFixed(0)}万`)
|
||||
} else {
|
||||
parts.push(String(n))
|
||||
}
|
||||
}
|
||||
return parts.join(' ') || ''
|
||||
}
|
||||
|
||||
function getItems(cat: any): any[] {
|
||||
const tab = activeTab[cat.category] || 'hot'
|
||||
return tab === 'hot' ? (cat.hot || []) : (cat.newest || [])
|
||||
}
|
||||
|
||||
function visibleItems(cat: any): any[] {
|
||||
const items = getItems(cat)
|
||||
return expanded[cat.category] ? items : items.slice(0, INITIAL_SHOW)
|
||||
}
|
||||
|
||||
function hasMoreItems(cat: any): boolean {
|
||||
const items = getItems(cat)
|
||||
return items.length > INITIAL_SHOW && !expanded[cat.category]
|
||||
}
|
||||
|
||||
function expandCategory(category: string) {
|
||||
expanded[category] = true
|
||||
}
|
||||
|
||||
function switchTab(category: string, tab: string) {
|
||||
activeTab[category] = tab
|
||||
// 切换标签时收起展开
|
||||
expanded[category] = false
|
||||
}
|
||||
|
||||
function openDisclaimer() {
|
||||
window.open('/disclaimer/', '_blank')
|
||||
}
|
||||
function handleSearch() { const q = query.value.trim(); if(q) router.push('/search?q='+encodeURIComponent(q)) }
|
||||
function searchTag(t:string) { router.push('/search?q='+encodeURIComponent(t)) }
|
||||
function openDisclaimer() { window.open('/disclaimer/', '_blank') }
|
||||
|
||||
onMounted(async () => {
|
||||
// 一言(内置格言)
|
||||
const q = QUOTES[Math.floor(Math.random() * QUOTES.length)]
|
||||
currentQuote.value = q
|
||||
currentQuote.value = QS[Math.floor(Math.random()*QS.length)]
|
||||
quoteAuthor.value = '古籍经典'
|
||||
|
||||
try {
|
||||
const [catsData, siteCfg] = await Promise.all([
|
||||
getCategorizedRankings(),
|
||||
getSiteConfig(),
|
||||
])
|
||||
// 新格式: { fetchedAt, categories }
|
||||
if (catsData.fetchedAt) {
|
||||
fetchedAt.value = catsData.fetchedAt
|
||||
categories.value = catsData.categories || []
|
||||
} else {
|
||||
// 兼容旧格式
|
||||
categories.value = Array.isArray(catsData) ? catsData : []
|
||||
const [catsData, siteCfg] = await Promise.all([getCategorizedRankings(), getSiteConfig()])
|
||||
if(siteCfg.site_logo) siteLogo.value=siteCfg.site_logo
|
||||
if(siteCfg.site_name) siteName.value=siteCfg.site_name
|
||||
if(siteCfg.site_disclaimer) siteDisclaimer.value=siteCfg.site_disclaimer
|
||||
configLoaded.value=true
|
||||
if(catsData?.categories){
|
||||
const hotCat=catsData.categories.find((c:any)=>c.category==='hot')
|
||||
if(hotCat?.hot?.length){
|
||||
const arr=hotCat.hot.slice(0,25).map((item:any,idx:number)=>({
|
||||
k:item.keyword, s:SS[Math.min(idx,SS.length-1)],
|
||||
c:CS[idx%CS.length], w:idx<5?800:idx<10?600:400,
|
||||
r:(Math.random()-.5)*6, o:1-idx*.02, d:idx*.08
|
||||
}))
|
||||
for(let i=arr.length-1;i>0;i--){const j=Math.floor(Math.random()*(i+1));[arr[i],arr[j]]=[arr[j],arr[i]]}
|
||||
words.value=arr
|
||||
}
|
||||
}
|
||||
for (const cat of categories.value) {
|
||||
activeTab[cat.category] = 'hot'
|
||||
expanded[cat.category] = false
|
||||
}
|
||||
if (siteCfg.site_logo) {
|
||||
siteLogo.value = siteCfg.site_logo
|
||||
}
|
||||
if (siteCfg.site_name) {
|
||||
siteName.value = siteCfg.site_name
|
||||
}
|
||||
if (siteCfg.site_disclaimer) {
|
||||
siteDisclaimer.value = siteCfg.site_disclaimer
|
||||
}
|
||||
configLoaded.value = true
|
||||
} catch (e) {
|
||||
console.error('加载首页数据失败', e)
|
||||
}
|
||||
}catch(e){console.error(e)}
|
||||
})
|
||||
|
||||
function handleSearch() {
|
||||
const q = query.value.trim()
|
||||
if (q) router.push('/search?q=' + encodeURIComponent(q))
|
||||
}
|
||||
|
||||
function searchTag(tag: string) {
|
||||
router.push('/search?q=' + encodeURIComponent(tag))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.home-page { min-height: 100vh; display: flex; flex-direction: column; }
|
||||
.home-page{min-height:100vh;background:#f5f7fa}
|
||||
.hero-section{text-align:center;padding:56px 24px 36px;background:#f5f7fa}
|
||||
.logo-text{font-size:48px;font-weight:700;color:#1d2129;margin-bottom:24px}
|
||||
.logo-img{display:block;max-width:400px;max-height:100px;margin:0 auto 24px}
|
||||
.search-box{display:inline-flex;align-items:center;width:100%;max-width:600px;border-radius:28px;background:#fff;overflow:hidden;box-shadow:0 2px 12px rgba(0,0,0,.08)}
|
||||
.search-box :deep(.el-input__wrapper){border:none;box-shadow:none;background:transparent;padding:4px 20px}
|
||||
.search-box :deep(.el-input__inner){font-size:15px}
|
||||
.search-btn{flex-shrink:0;border:none;border-radius:24px;padding:0 28px;height:40px;font-size:14px;font-weight:600;background:#409eff;color:#fff;margin:4px;cursor:pointer;transition:all .2s}
|
||||
.search-btn:hover{background:#337ecc;transform:scale(1.02)}
|
||||
.quote-line{margin-top:14px;font-size:13px;color:#86909c;font-style:italic}
|
||||
.cloud-zone{max-width:1100px;margin:0 auto;padding:32px 24px 60px}
|
||||
.cloud-title{text-align:center;font-size:14px;color:#86909c;letter-spacing:4px;margin-bottom:40px}
|
||||
.word-cloud{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:12px 24px;padding:20px;line-height:1.4}
|
||||
.wc-word{display:inline-block;cursor:pointer;transition:all .25s cubic-bezier(.4,0,.2,1);animation:pop-in .8s ease-out both;padding:2px 6px;border-radius:6px;user-select:none}
|
||||
.wc-word:hover{transform:scale(1.3) rotate(0deg)!important;opacity:1!important}
|
||||
@keyframes pop-in{from{opacity:0;transform:translateY(20px) scale(.8)}to{opacity:1;transform:translateY(0) scale(1)}}
|
||||
.empty-area{text-align:center;padding:80px 20px;color:#c0c4cc}
|
||||
.site-footer{text-align:center;padding:20px 16px 32px;background:#f5f7fa;border-top:1px solid #e5e6eb}
|
||||
.footer-inner{max-width:800px;margin:0 auto 12px;font-size:12px;line-height:1.8;color:#86909c;white-space:pre-line}
|
||||
.footer-btn{font-size:12px!important;color:#86909c!important}
|
||||
|
||||
.hero-section {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
padding: 60px 24px 40px;
|
||||
/* Responsive */
|
||||
@media(max-width:768px){
|
||||
.hero-section{padding:36px 16px 24px}
|
||||
.logo-text{font-size:28px}
|
||||
.search-box{border-radius:24px}
|
||||
.search-btn{padding:0 18px;height:36px;font-size:13px}
|
||||
.quote-line{font-size:12px}
|
||||
.cloud-zone{padding:24px 12px 40px}
|
||||
.cloud-title{margin-bottom:24px;font-size:13px;letter-spacing:2px}
|
||||
.word-cloud{gap:8px 16px;padding:12px}
|
||||
.wc-word{padding:1px 4px}
|
||||
}
|
||||
.logo-text { font-size: 64px; font-weight: 700; color: var(--primary-color); margin-bottom: 32px; letter-spacing: -2px; }
|
||||
.logo-img { max-width: 500px; max-height: 120px; width: auto; height: auto; object-fit: contain; margin-bottom: 32px; }
|
||||
.search-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%; max-width: 640px;
|
||||
border: 1px solid #dfe1e5;
|
||||
border-radius: 24px;
|
||||
background: #fff;
|
||||
box-shadow: none;
|
||||
transition: box-shadow .2s, border-color .2s;
|
||||
overflow: hidden;
|
||||
@media(max-width:480px){
|
||||
.hero-section{padding:28px 12px 20px}
|
||||
.logo-text{font-size:22px;margin-bottom:16px}
|
||||
.search-box{max-width:100%}
|
||||
.search-box :deep(.el-input__inner){font-size:13px}
|
||||
}
|
||||
.search-box:focus-within {
|
||||
box-shadow: 0 1px 6px rgba(32,33,36,.28);
|
||||
border-color: rgba(223,225,229,0);
|
||||
}
|
||||
.search-box :deep(.el-input__wrapper) {
|
||||
border: none; box-shadow: none; background: transparent;
|
||||
padding: 4px 20px; border-radius: 0;
|
||||
}
|
||||
.search-box :deep(.el-input__inner) {
|
||||
font-size: 15px;
|
||||
}
|
||||
.search-btn {
|
||||
flex-shrink: 0;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 0 24px;
|
||||
height: 38px;
|
||||
line-height: 38px;
|
||||
margin: 4px;
|
||||
font-size: 14px; font-weight: 600;
|
||||
background: var(--primary-color); color: #fff;
|
||||
cursor: pointer; transition: all .2s;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.search-btn:hover {
|
||||
background: #3a7be0;
|
||||
}
|
||||
.search-btn:active {
|
||||
background: #2d6ccf;
|
||||
}
|
||||
|
||||
.quote-section { margin-top: 18px; max-width: 640px; text-align: center; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.quote-text { font-size: 14px; color: #aab0b8; font-style: italic; letter-spacing: 0.5px; }
|
||||
.quote-author { font-size: 12px; color: #c0c4cc; display: inline-block; margin-left: 4px; }
|
||||
|
||||
.content-section { max-width: 1500px; width: 100%; margin: 0 auto; padding: 0 16px 60px; }
|
||||
|
||||
.rankings-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.rank-panel {
|
||||
background: var(--bg-white,#fff);
|
||||
border-radius: 12px; padding: 14px; border: 1px solid #ebeef5;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,0.04); display: flex; flex-direction: column;
|
||||
}
|
||||
|
||||
.panel-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding-bottom: 10px; border-bottom: 2px solid #f0f0f0; margin-bottom: 4px;
|
||||
}
|
||||
.panel-title { font-size: 15px; font-weight: 700; color: #303133; white-space: nowrap; }
|
||||
.panel-tabs { display: flex; gap: 2px; background: #f0f2f5; border-radius: 6px; padding: 2px; }
|
||||
.panel-tab {
|
||||
font-size: 11px; padding: 3px 10px; border-radius: 5px; cursor: pointer;
|
||||
color: #909399; font-weight: 500; transition: all .2s; user-select: none;
|
||||
}
|
||||
.panel-tab.active { background: #fff; color: var(--primary-color); font-weight: 600; box-shadow: 0 1px 3px rgba(0,0,0,0.06); }
|
||||
|
||||
.panel-body { flex: 1; display: flex; flex-direction: column; gap: 1px; }
|
||||
|
||||
.rank-item {
|
||||
display: flex; align-items: center; gap: 8px; padding: 5px 6px;
|
||||
border-radius: 6px; cursor: pointer; transition: background .15s;
|
||||
}
|
||||
.rank-item:hover { background: #f0f5ff; }
|
||||
.rank-item:active { background: #e6f0ff; }
|
||||
|
||||
.rank-idx {
|
||||
width: 22px; height: 22px; border-radius: 50%; display: flex; align-items: center;
|
||||
justify-content: center; font-size: 12px; font-weight: 700;
|
||||
color: #909399; background: #f0f0f0; flex-shrink: 0;
|
||||
}
|
||||
.rank-idx.top-three { background: var(--primary-color); color: #fff; }
|
||||
.rank-name { flex: 1; min-width: 0; font-size: 13px; font-weight: 500; color: #303133; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rank-cnt { font-size: 11px; color: #c0c4cc; white-space: nowrap; flex-shrink: 0; }
|
||||
|
||||
.rank-expand {
|
||||
text-align: center; padding: 6px; margin-top: 2px; font-size: 12px;
|
||||
color: var(--primary-color); cursor: pointer; border-radius: 6px;
|
||||
transition: background .15s; user-select: none;
|
||||
}
|
||||
.rank-expand:hover { background: #ecf5ff; }
|
||||
|
||||
.panel-footer {
|
||||
margin-top: 8px; padding-top: 8px; border-top: 1px solid #f0f0f0;
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 11px; color: #c0c4cc;
|
||||
}
|
||||
.footer-time { font-family: monospace; font-size: 10px; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-section { padding: 36px 16px 24px; }
|
||||
.logo-text { font-size: 36px; margin-bottom: 20px; }
|
||||
.logo-img { max-width: 360px; max-height: 100px; margin-bottom: 20px; }
|
||||
.rankings-scroll { gap: 12px; }
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
margin-top: auto;
|
||||
padding: 20px 16px 32px;
|
||||
background: #f9fafb;
|
||||
border-top: 1px solid #ebeef5;
|
||||
}
|
||||
.footer-inner {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
font-size: 12px;
|
||||
line-height: 1.8;
|
||||
color: #909399;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
}
|
||||
.footer-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.footer-disclaimer-btn {
|
||||
font-size: 12px !important;
|
||||
color: #909399 !important;
|
||||
}
|
||||
.footer-disclaimer-btn:hover {
|
||||
color: #409eff !important;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -28,14 +28,6 @@
|
||||
<el-button type="primary" size="large" @click="handleSearch" class="result-search-btn">搜 索</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 右上角登录/用户信息,独立于搜索栏 -->
|
||||
<div class="top-right-user">
|
||||
<template v-if="userInfo">
|
||||
<span class="user-badge">{{ userInfo.username }}</span>
|
||||
<el-button size="small" text @click="handleLogout">退出</el-button>
|
||||
</template>
|
||||
<el-button v-else size="small" @click="showLogin = true">登录</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 滚动通知条(跑马灯) -->
|
||||
@@ -151,7 +143,7 @@
|
||||
:data="item"
|
||||
:fallbackTags="contentTags"
|
||||
:fallbackImage="fallbackImage"
|
||||
:loggedIn="userInfo !== null"
|
||||
:loggedIn="false"
|
||||
:cloudTypeMap="cloudTypeMap"
|
||||
@save="handleSave"
|
||||
/>
|
||||
@@ -183,7 +175,7 @@
|
||||
:data="item"
|
||||
:fallbackTags="contentTags"
|
||||
:fallbackImage="fallbackImage"
|
||||
:loggedIn="userInfo !== null"
|
||||
:loggedIn="false"
|
||||
:cloudTypeMap="cloudTypeMap"
|
||||
@save="handleSave"
|
||||
/>
|
||||
@@ -346,21 +338,6 @@
|
||||
</el-dialog>
|
||||
</div>
|
||||
|
||||
<!-- 登录弹窗 -->
|
||||
<el-dialog v-model="showLogin" title="登录" width="380px" :close-on-click-modal="false" top="25vh">
|
||||
<el-form ref="loginFormRef" :model="loginForm" :rules="loginRules" label-width="0" @keyup.enter="handleLogin">
|
||||
<el-form-item prop="username">
|
||||
<el-input v-model="loginForm.username" placeholder="用户名" prefix-icon="User" />
|
||||
</el-form-item>
|
||||
<el-form-item prop="password">
|
||||
<el-input v-model="loginForm.password" type="password" placeholder="密码" prefix-icon="Lock" show-password />
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button type="primary" :loading="loginLoading" style="width: 100%" @click="handleLogin">登录</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<p v-if="loginError" class="login-error">{{ loginError }}</p>
|
||||
</el-dialog>
|
||||
<div v-if="siteDisclaimer" class="site-footer">
|
||||
<div class="footer-inner">{{ siteDisclaimer }}</div>
|
||||
<div class="footer-actions">
|
||||
@@ -376,7 +353,7 @@ import { Search, Loading, CircleCheckFilled } from '@element-plus/icons-vue'
|
||||
import QRCode from 'qrcode'
|
||||
import ResultCard from '../components/ResultCard.vue'
|
||||
import VideoResultCard from '../components/VideoResultCard.vue'
|
||||
import { query as searchQuery, saveToCloud, saveVideoToCloud, getSystemConfigs, getCloudTypes, streamSearch, adminLogin, getMe } from '../api'
|
||||
import { query as searchQuery, saveToCloud, saveVideoToCloud, getSystemConfigs, getCloudTypes, streamSearch, getSiteConfig } from '../api'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import type { SearchResult, VideoParseResult, SaveResult, IntentType, CloudType, ChannelGroup } from '../types'
|
||||
@@ -417,17 +394,6 @@ const siteDisclaimer = ref('')
|
||||
const siteMarquee = ref('')
|
||||
const coverError = ref(false)
|
||||
|
||||
// 登录状态
|
||||
const userInfo = ref<{ username: string } | null>(null)
|
||||
const showLogin = ref(false)
|
||||
const loginFormRef = ref<FormInstance>()
|
||||
const loginForm = reactive({ username: '', password: '' })
|
||||
const loginLoading = ref(false)
|
||||
const loginError = ref('')
|
||||
const loginRules = {
|
||||
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
}
|
||||
|
||||
const allResultsMap = ref(new Map<string, any>())
|
||||
const validResults = ref<any[]>([])
|
||||
@@ -456,43 +422,22 @@ async function loadCloudTypes() {
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
// 预加载网站配置(logo/名称/公告等)
|
||||
try {
|
||||
const sc = await getSiteConfig()
|
||||
if (sc.site_logo) siteLogo.value = sc.site_logo
|
||||
if (sc.site_name) siteName.value = sc.site_name
|
||||
if (sc.site_disclaimer) siteDisclaimer.value = sc.site_disclaimer
|
||||
if (sc.site_marquee) siteMarquee.value = sc.site_marquee
|
||||
} catch {}
|
||||
const q = (route.query.q as string) || ''
|
||||
if (q) {
|
||||
query.value = q
|
||||
doSearch(q)
|
||||
}
|
||||
// 检查登录状态
|
||||
const me: any = await getMe().catch(() => ({ loggedIn: false }))
|
||||
if (me.loggedIn && me.username) {
|
||||
userInfo.value = { username: me.username }
|
||||
}
|
||||
loadCloudTypes()
|
||||
})
|
||||
|
||||
async function handleLogin() {
|
||||
const valid = await loginFormRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
loginLoading.value = true
|
||||
loginError.value = ''
|
||||
try {
|
||||
const res = await adminLogin(loginForm.username, loginForm.password)
|
||||
localStorage.setItem('admin_token', res.token)
|
||||
userInfo.value = { username: loginForm.username }
|
||||
showLogin.value = false
|
||||
loginForm.password = ''
|
||||
ElMessage.success('登录成功')
|
||||
} catch (e: any) {
|
||||
loginError.value = e?.response?.data?.error || e?.message || '登录失败'
|
||||
} finally {
|
||||
loginLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function handleLogout() {
|
||||
localStorage.removeItem('admin_token')
|
||||
userInfo.value = null
|
||||
ElMessage.success('已退出')
|
||||
}
|
||||
|
||||
// 网盘分类标签 — 始终展示所有已知云盘类型(0 也显示)
|
||||
const cloudTabs = computed(() => {
|
||||
|
||||
@@ -237,6 +237,7 @@ watch(() => stats.value.trendTrend, () => {
|
||||
}, { deep: true })
|
||||
|
||||
// Reload chart when switching back to dashboard
|
||||
const activeMenu = ref("dashboard")
|
||||
watch(() => activeMenu.value, (val, oldVal) => {
|
||||
if (val === 'dashboard' && oldVal !== 'dashboard') {
|
||||
nextTick(() => {
|
||||
|
||||
@@ -2,7 +2,10 @@
|
||||
<div class="admin-layout">
|
||||
<aside class="admin-sidebar">
|
||||
<div class="sidebar-brand">
|
||||
<div class="sidebar-logo">☁️</div>
|
||||
<div class="sidebar-logo">
|
||||
<img v-if="siteLogo" :src="siteLogo" :alt="siteName || 'CloudSearch'" class="sidebar-logo-img" @error="(e: any) => { (e.target as HTMLElement).style.display='none'; siteLogo='' }" />
|
||||
<span v-else class="sidebar-logo-fallback">☁️</span>
|
||||
</div>
|
||||
<div class="sidebar-brand-text">
|
||||
<h2>{{ siteName || 'CloudSearch' }}</h2>
|
||||
<p>管理控制台</p>
|
||||
@@ -86,6 +89,7 @@ const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const siteName = ref('')
|
||||
const siteLogo = ref('')
|
||||
const appVersion = ref('')
|
||||
|
||||
const pageTitles: Record<string, string> = {
|
||||
@@ -142,6 +146,7 @@ onMounted(async () => {
|
||||
try {
|
||||
const cfg = await getSiteConfig()
|
||||
siteName.value = cfg.site_name || ''
|
||||
siteLogo.value = cfg.site_logo || ''
|
||||
} catch {}
|
||||
try {
|
||||
const h = await fetch('/health')
|
||||
@@ -180,7 +185,14 @@ onMounted(async () => {
|
||||
font-size: 28px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.sidebar-logo-img { width: 36px; height: 36px; object-fit: contain; border-radius: 6px; }
|
||||
.sidebar-logo-fallback { font-size: 28px; }
|
||||
.sidebar-brand-text h2 {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -828,6 +828,7 @@ const searchAllChannels = computed({
|
||||
set: (val: boolean) => { configs.search_all_channels = val ? 'true' : 'false' },
|
||||
})
|
||||
|
||||
const autoUpdateEnabled = computed({
|
||||
get: () => String(configs.auto_update_enabled) === 'true',
|
||||
set: (val: boolean) => { configs.auto_update_enabled = val ? 'true' : 'false' },
|
||||
})
|
||||
|
||||
2042
source_clean/frontend-src/src/pages/admin/SystemConfig.vue.bak
Normal file
2042
source_clean/frontend-src/src/pages/admin/SystemConfig.vue.bak
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user