| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296 |
- <?php require_once __DIR__ . '/../init.php'; ?><!doctype html>
- <html lang="zh-CN">
- <head>
- <meta charset="utf-8" />
- <meta name="viewport" content="width=device-width,initial-scale=1" />
- <meta name="color-scheme" content="light dark" />
- <title><?= htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8') ?></title>
- <link rel="stylesheet" href="style.css" />
- </head>
- <body class="index-page">
- <div class="wrap">
- <header>
- <div class="brand">
- <h1><?= htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8') ?></h1>
- <div class="sub">统一入口 · 快速跳转 · 显示时区:<span class="mono">UTC+8</span></div>
- </div>
- <div class="toolbar">
- <div class="pill" id="clock">--:--:-- (UTC+8)</div>
- <button id="themeBtn" title="切换主题">主题:跟随系统</button>
- <button id="loginBtn" title="登录">登录</button>
- <button id="adminBtn" title="管理" style="display:none">管理</button>
- </div>
- </header>
- <div class="search" role="search" aria-label="Search">
- <span class="mono" style="opacity:.7">/</span>
- <input id="q" placeholder="搜索服务…(回车打开第一个)" autocomplete="off" />
- <kbd>Enter</kbd>
- </div>
- <!-- 加载状态 -->
- <div id="loading" style="text-align:center;padding:40px;color:var(--muted);font-size:14px">加载中…</div>
- <div class="board" id="board" style="display:none">
- <!-- 由 JS 动态渲染 -->
- <div class="empty" id="emptyState">没有找到匹配的服务。</div>
- </div>
- <footer>
- <div>提示:按 <span class="mono">/</span> 聚焦搜索;按 <span class="mono">Enter</span> 打开第一个结果;按 <span class="mono">Esc</span> 清空搜索。</div>
- <div class="mono">Updated (UTC+8): <span id="ts">--</span></div>
- </footer>
- </div>
- <!-- ===== 登录模态框 ===== -->
- <div class="modal-overlay" id="loginModal">
- <div class="modal-box">
- <h2>登录</h2>
- <p>请输入站点密码以解锁全部链接。</p>
- <label for="loginPwd">密码</label>
- <input type="password" id="loginPwd" placeholder="输入密码" autocomplete="current-password" />
- <div class="btn-row">
- <button id="loginCancel">取消</button>
- <button id="loginConfirm" class="btn-primary">登录</button>
- </div>
- <div class="modal-error" id="loginError"></div>
- </div>
- </div>
- <script src="app.js"></script>
- <script>
- (function(){
- 'use strict';
- const $ = (s) => document.querySelector(s);
- const $$ = (s) => [...document.querySelectorAll(s)];
- const board = $("#board");
- const loading = $("#loading");
- const emptyState = $("#emptyState");
- const q = $("#q");
- const loginBtn = $("#loginBtn");
- const adminBtn = $("#adminBtn");
- const loginModal = $("#loginModal");
- const loginPwd = $("#loginPwd");
- const loginError = $("#loginError");
- const loginCancel = $("#loginCancel");
- const loginConfirm = $("#loginConfirm");
- let loggedIn = false;
- let allGroups = [];
- // ===== 时钟 =====
- const TZ = "Asia/Shanghai";
- const fmtClock = new Intl.DateTimeFormat("zh-CN", {
- timeZone: TZ, hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
- });
- function formatYMDHMS_UTC8(d){
- const parts = new Intl.DateTimeFormat("zh-CN", {
- timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
- hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
- }).formatToParts(d);
- const get = (t) => parts.find(p => p.type === t)?.value || "";
- return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`;
- }
- function tick(){
- const d = new Date();
- $("#clock").textContent = `${fmtClock.format(d)} (UTC+8)`;
- $("#ts").textContent = formatYMDHMS_UTC8(d);
- }
- tick(); setInterval(tick, 1000);
- // ===== 主题切换 =====
- const themeBtn = $("#themeBtn");
- const root = document.documentElement;
- function getThemeLabel(){
- const cur = root.getAttribute("data-theme");
- if(cur === "dark") return "主题:深色";
- if(cur === "light") return "主题:浅色";
- return "主题:跟随系统";
- }
- function refreshThemeLabel(){ themeBtn.textContent = getThemeLabel(); }
- function setTheme(mode){
- if(!mode){ root.removeAttribute("data-theme"); localStorage.removeItem("theme"); refreshThemeLabel(); return; }
- root.setAttribute("data-theme", mode); localStorage.setItem("theme", mode); refreshThemeLabel();
- }
- const saved = localStorage.getItem("theme");
- if(saved === "dark" || saved === "light") setTheme(saved); else refreshThemeLabel();
- themeBtn.addEventListener("click", () => {
- const cur = root.getAttribute("data-theme");
- if(cur === "dark") setTheme("light"); else if(cur === "light") setTheme(null); else setTheme("dark");
- });
- // ===== API 请求 =====
- async function apiGet(action){
- const r = await fetch(`api.php?action=${action}`);
- if(!r.ok) { const e = await r.json(); throw new Error(e.error || '请求失败'); }
- return r.json();
- }
- async function apiPost(action, data){
- const fd = new URLSearchParams();
- for(const [k,v] of Object.entries(data)) fd.append(k, v);
- const r = await fetch(`api.php?action=${action}`, {
- method: 'POST',
- headers: {'Content-Type':'application/x-www-form-urlencoded'},
- body: fd.toString(),
- });
- if(!r.ok) { const e = await r.json(); throw new Error(e.error || '请求失败'); }
- return r.json();
- }
- // ===== 渲染链接 =====
- function renderLinks(groups){
- board.innerHTML = '';
- for(const g of groups){
- if(!g.links || g.links.length === 0) continue;
- const section = document.createElement('section');
- section.className = 'section-block';
- section.dataset.group = g.name;
- const header = document.createElement('div');
- header.className = 'section';
- header.innerHTML = `<h2>${esc(g.label || g.name)}</h2><div class="hint">${g.hint || ''}</div>`;
- const grid = document.createElement('div');
- grid.className = 'grid';
- for(const link of g.links){
- const card = document.createElement('a');
- card.className = 'card' + (link.is_private ? ' private' : '');
- card.href = link.url;
- card.target = '_blank';
- card.rel = 'noopener';
- card.dataset.group = g.name;
- card.dataset.keywords = (link.keywords || '') + ' ' + link.title + ' ' + (link.description || '');
- const tagClass = link.tag_class ? `tag ${link.tag_class}` : 'tag';
- card.innerHTML = `
- <div class="top">
- <div class="title">
- <div class="icon" aria-hidden="true">${link.icon_svg || ''}</div>
- <div style="min-width:0">
- <div class="name">${esc(link.title)}</div>
- <div class="meta">${esc(link.meta_domain)}</div>
- </div>
- </div>
- <div class="tags">
- ${link.tag_label ? `<span class="${tagClass}">${esc(link.tag_label)}</span>` : ''}
- ${link.is_private ? '<span class="tag" style="color:var(--danger);border-color:rgba(255,107,107,.3);background:rgba(255,107,107,.08)">🔒 私有</span>' : ''}
- </div>
- </div>
- <div class="desc">${esc(link.description)}</div>
- <div class="actions">
- <span class="go">打开 →</span>
- <span class="small">${link.url.replace(/^https?:\/\//,'').split('/')[0]}</span>
- </div>
- `;
- grid.appendChild(card);
- }
- section.appendChild(header);
- section.appendChild(grid);
- board.appendChild(section);
- }
- board.appendChild(emptyState);
- applyFilter();
- }
- // ===== 获取并渲染链接 =====
- async function loadLinks(){
- loading.style.display = '';
- board.style.display = 'none';
- try {
- const data = await apiGet('get_links');
- loggedIn = data.logged_in;
- allGroups = data.groups || [];
- renderLinks(allGroups);
- loading.style.display = 'none';
- board.style.display = '';
- loginBtn.textContent = loggedIn ? '已登录' : '登录';
- adminBtn.style.display = loggedIn ? '' : 'none';
- } catch(e){
- loading.textContent = '加载失败:' + e.message;
- }
- }
- // ===== 搜索 =====
- function norm(s){ return (s || "").toLowerCase().trim(); }
- function visibleCards(){ return $$(".card").filter(c => c.style.display !== "none"); }
- function updateGroups(){
- for(const group of $$(".section-block")){
- const key = group.dataset.group;
- const count = $$(".card").filter(c => c.dataset.group === key && c.style.display !== "none").length;
- group.style.display = count > 0 ? "" : "none";
- }
- }
- function applyFilter(){
- const v = norm(q.value);
- for(const c of $$(".card")){
- const hay = norm((c.dataset.keywords || "") + " " + c.textContent);
- c.style.display = (!v || hay.includes(v)) ? "" : "none";
- }
- updateGroups();
- emptyState.style.display = visibleCards().length ? "none" : "block";
- }
- q.addEventListener("input", applyFilter);
- q.addEventListener("keydown", (e) => {
- if(e.key === "Enter"){ const first = visibleCards()[0]; if(first) first.click(); }
- if(e.key === "Escape"){ q.value = ""; applyFilter(); q.blur(); }
- });
- document.addEventListener("keydown", (e) => {
- if(e.key === "/" && document.activeElement !== q){ e.preventDefault(); q.focus(); }
- });
- // ===== 登录逻辑 =====
- function showLoginModal(){
- loginModal.classList.add('active'); loginPwd.value = '';
- loginError.style.display = 'none'; loginPwd.focus();
- }
- function hideLoginModal(){ loginModal.classList.remove('active'); }
- loginBtn.addEventListener('click', () => {
- if(loggedIn){
- if(!confirm('确定要退出登录吗?')) return;
- apiPost('logout', {}).then(() => { loggedIn = false; loadLinks(); }).catch(e => alert(e.message));
- } else {
- showLoginModal();
- }
- });
- function doLogin(){
- const pwd = loginPwd.value;
- if(!pwd){ loginError.textContent = '请输入密码'; loginError.style.display = ''; return; }
- loginConfirm.disabled = true; loginConfirm.textContent = '登录中…';
- apiPost('login', {password: pwd}).then(data => {
- if(data.success){ window.location.href = 'admin.php'; }
- else { loginError.textContent = data.error || '密码错误'; loginError.style.display = ''; loginConfirm.disabled = false; loginConfirm.textContent = '登录'; }
- }).catch(e => {
- loginError.textContent = e.message; loginError.style.display = '';
- loginConfirm.disabled = false; loginConfirm.textContent = '登录';
- });
- }
- loginConfirm.addEventListener('click', doLogin);
- loginPwd.addEventListener('keydown', (e) => { if(e.key === 'Enter') doLogin(); });
- loginCancel.addEventListener('click', hideLoginModal);
- loginModal.addEventListener('click', (e) => { if(e.target === loginModal) hideLoginModal(); });
- // ===== 管理面板 =====
- adminBtn.addEventListener('click', () => { window.location.href = 'admin.php'; });
- // ===== 启动 =====
- loadLinks();
- })();
- </script>
- </body>
- </html>
|