index.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794
  1. <?php require_once __DIR__ . '/../init.php'; ?><!doctype html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="utf-8" />
  5. <meta name="viewport" content="width=device-width,initial-scale=1" />
  6. <meta name="color-scheme" content="light dark" />
  7. <title>BCJD 导航</title>
  8. <style>
  9. :root{
  10. --bg: #0b1020;
  11. --panel: rgba(255,255,255,.06);
  12. --panel-2: rgba(255,255,255,.10);
  13. --text: rgba(255,255,255,.92);
  14. --muted: rgba(255,255,255,.65);
  15. --border: rgba(255,255,255,.12);
  16. --shadow: 0 10px 30px rgba(0,0,0,.35);
  17. --accent: #6ea8ff;
  18. --accent2: #7cf7d4;
  19. --danger: #ff6b6b;
  20. --ok: #3ddc97;
  21. --max: 1120px;
  22. --radius: 18px;
  23. --radius-sm: 12px;
  24. --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  25. --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", Arial, sans-serif;
  26. }
  27. html[data-theme="light"]{
  28. --bg: #f6f7fb;
  29. --panel: rgba(0,0,0,.04);
  30. --panel-2: rgba(0,0,0,.06);
  31. --text: rgba(0,0,0,.88);
  32. --muted: rgba(0,0,0,.58);
  33. --border: rgba(0,0,0,.10);
  34. --shadow: 0 10px 30px rgba(0,0,0,.12);
  35. --accent: #245bdb;
  36. --accent2: #0aa37f;
  37. --danger: #d64545;
  38. --ok: #0a7f5f;
  39. }
  40. @media (prefers-color-scheme: light){
  41. html:not([data-theme="dark"]){
  42. --bg: #f6f7fb;
  43. --panel: rgba(0,0,0,.04);
  44. --panel-2: rgba(0,0,0,.06);
  45. --text: rgba(0,0,0,.88);
  46. --muted: rgba(0,0,0,.58);
  47. --border: rgba(0,0,0,.10);
  48. --shadow: 0 10px 30px rgba(0,0,0,.12);
  49. --accent: #245bdb;
  50. --accent2: #0aa37f;
  51. --danger: #d64545;
  52. --ok: #0a7f5f;
  53. }
  54. }
  55. *{ box-sizing:border-box; }
  56. html{ scroll-behavior:smooth; }
  57. body{
  58. margin:0;
  59. min-height:100vh;
  60. font-family: var(--sans);
  61. color: var(--text);
  62. background:
  63. radial-gradient(1000px 520px at 12% 12%, rgba(110,168,255,.18), transparent 55%),
  64. radial-gradient(900px 460px at 88% 20%, rgba(124,247,212,.14), transparent 55%),
  65. radial-gradient(860px 520px at 35% 100%, rgba(255,107,107,.08), transparent 55%),
  66. var(--bg);
  67. }
  68. a{ color:inherit; text-decoration:none; }
  69. .wrap{
  70. max-width: var(--max);
  71. margin: 0 auto;
  72. padding: 30px 18px 44px;
  73. }
  74. header{
  75. display:flex;
  76. justify-content:space-between;
  77. align-items:flex-start;
  78. gap:18px;
  79. margin-bottom: 18px;
  80. }
  81. .brand{
  82. display:flex;
  83. flex-direction:column;
  84. gap:6px;
  85. }
  86. .brand h1{
  87. margin:0;
  88. font-size: 24px;
  89. font-weight: 800;
  90. letter-spacing:.2px;
  91. }
  92. .brand .sub{
  93. font-size:13px;
  94. color: var(--muted);
  95. line-height:1.45;
  96. }
  97. .toolbar{
  98. display:flex;
  99. align-items:center;
  100. justify-content:flex-end;
  101. gap:10px;
  102. flex-wrap:wrap;
  103. }
  104. .pill,
  105. button{
  106. height: 42px;
  107. display:inline-flex;
  108. align-items:center;
  109. justify-content:center;
  110. border-radius:999px;
  111. border:1px solid var(--border);
  112. background: var(--panel);
  113. box-shadow: var(--shadow);
  114. backdrop-filter: blur(10px);
  115. color: var(--text);
  116. }
  117. .pill{
  118. padding: 0 14px;
  119. font-family: var(--mono);
  120. white-space:nowrap;
  121. }
  122. button{
  123. padding: 0 14px;
  124. cursor:pointer;
  125. transition: transform .06s ease, background .15s ease, border-color .15s ease;
  126. font-family: var(--sans);
  127. font-size:13px;
  128. }
  129. button:hover{
  130. background: var(--panel-2);
  131. border-color: rgba(110,168,255,.28);
  132. }
  133. button:active{ transform: translateY(1px); }
  134. .search{
  135. display:flex;
  136. align-items:center;
  137. gap:10px;
  138. min-width: min(560px, 100%);
  139. margin-bottom: 22px;
  140. padding: 13px 14px;
  141. border-radius: 999px;
  142. border: 1px solid var(--border);
  143. background: var(--panel);
  144. backdrop-filter: blur(10px);
  145. box-shadow: var(--shadow);
  146. }
  147. .search input{
  148. width:100%;
  149. border:0;
  150. outline:none;
  151. background: transparent;
  152. color: var(--text);
  153. font-size:14px;
  154. font-family: var(--sans);
  155. }
  156. .search input::placeholder{ color: var(--muted); }
  157. .search kbd{
  158. padding: 3px 8px;
  159. border-radius: 8px;
  160. border: 1px solid var(--border);
  161. background: var(--panel-2);
  162. color: var(--muted);
  163. font-size:12px;
  164. font-family: var(--mono);
  165. }
  166. .board{
  167. display:flex;
  168. flex-direction:column;
  169. gap: 22px;
  170. }
  171. .section-block{
  172. display:flex;
  173. flex-direction:column;
  174. gap: 12px;
  175. }
  176. .section{
  177. display:flex;
  178. align-items:flex-end;
  179. justify-content:space-between;
  180. gap:10px;
  181. padding: 0 2px;
  182. }
  183. .section h2{
  184. margin:0;
  185. font-size: 13px;
  186. font-weight: 800;
  187. letter-spacing: .9px;
  188. text-transform: uppercase;
  189. color: var(--muted);
  190. }
  191. .section .hint{
  192. font-size: 12px;
  193. color: var(--muted);
  194. }
  195. .grid{
  196. display:grid;
  197. grid-template-columns: repeat(12, 1fr);
  198. gap: 14px;
  199. }
  200. .card{
  201. grid-column: span 4;
  202. min-height: 138px;
  203. padding: 14px 14px 12px;
  204. border-radius: var(--radius);
  205. border: 1px solid var(--border);
  206. background:
  207. linear-gradient(180deg, rgba(255,255,255,.06), transparent 26%),
  208. linear-gradient(180deg, var(--panel), transparent 120%);
  209. box-shadow: var(--shadow);
  210. backdrop-filter: blur(10px);
  211. display:flex;
  212. flex-direction:column;
  213. gap:10px;
  214. position:relative;
  215. overflow:hidden;
  216. transition: transform .10s ease, border-color .15s ease, background .15s ease;
  217. }
  218. .card::before{
  219. content:"";
  220. position:absolute;
  221. top:0; left:0; right:0;
  222. height: 1px;
  223. background: linear-gradient(90deg, transparent, rgba(255,255,255,.32), transparent);
  224. opacity:.75;
  225. pointer-events:none;
  226. }
  227. .card:hover{
  228. transform: translateY(-2px);
  229. border-color: rgba(110,168,255,.35);
  230. background:
  231. linear-gradient(180deg, rgba(255,255,255,.08), transparent 26%),
  232. linear-gradient(180deg, var(--panel-2), transparent 130%);
  233. }
  234. .top{
  235. display:flex;
  236. align-items:flex-start;
  237. justify-content:space-between;
  238. gap:10px;
  239. }
  240. .title{
  241. display:flex;
  242. align-items:center;
  243. gap:10px;
  244. min-width:0;
  245. }
  246. .icon{
  247. width:40px;
  248. height:40px;
  249. flex:0 0 auto;
  250. display:grid;
  251. place-items:center;
  252. border-radius: 13px;
  253. border: 1px solid var(--border);
  254. background:
  255. radial-gradient(18px 18px at 30% 30%, rgba(124,247,212,.35), transparent 60%),
  256. radial-gradient(18px 18px at 70% 70%, rgba(110,168,255,.32), transparent 60%),
  257. rgba(255,255,255,.06);
  258. }
  259. .icon svg{
  260. width:20px;
  261. height:20px;
  262. opacity:.92;
  263. }
  264. .name{
  265. font-size:15px;
  266. font-weight:780;
  267. white-space:nowrap;
  268. overflow:hidden;
  269. text-overflow:ellipsis;
  270. }
  271. .meta{
  272. margin-top:2px;
  273. color: var(--muted);
  274. font-size:11px;
  275. font-family: var(--mono);
  276. white-space:nowrap;
  277. overflow:hidden;
  278. text-overflow:ellipsis;
  279. }
  280. .tags{
  281. display:flex;
  282. flex-wrap:wrap;
  283. gap:8px;
  284. }
  285. .tag{
  286. padding: 4px 8px;
  287. border-radius:999px;
  288. border:1px solid var(--border);
  289. background: var(--panel-2);
  290. color: var(--muted);
  291. font-size:11px;
  292. line-height:1;
  293. }
  294. .tag.ok{
  295. color: var(--ok);
  296. border-color: rgba(61,220,151,.35);
  297. background: rgba(61,220,151,.10);
  298. }
  299. .tag.warn{
  300. color: var(--danger);
  301. border-color: rgba(255,107,107,.35);
  302. background: rgba(255,107,107,.08);
  303. }
  304. .desc{
  305. color: var(--muted);
  306. font-size:13px;
  307. line-height:1.4;
  308. display:-webkit-box;
  309. -webkit-line-clamp:2;
  310. -webkit-box-orient:vertical;
  311. overflow:hidden;
  312. min-height: 36px;
  313. }
  314. .actions{
  315. margin-top:auto;
  316. display:flex;
  317. align-items:center;
  318. justify-content:space-between;
  319. gap:8px;
  320. }
  321. .go{
  322. display:inline-flex;
  323. align-items:center;
  324. gap:8px;
  325. padding: 9px 12px;
  326. border-radius: var(--radius-sm);
  327. border: 1px solid rgba(110,168,255,.35);
  328. background: rgba(110,168,255,.14);
  329. font-size:13px;
  330. font-weight:730;
  331. }
  332. .go:hover{ background: rgba(110,168,255,.20); }
  333. .small{
  334. max-width:48%;
  335. white-space:nowrap;
  336. overflow:hidden;
  337. text-overflow:ellipsis;
  338. text-align:right;
  339. color: var(--muted);
  340. font-size:12px;
  341. font-family: var(--mono);
  342. }
  343. .empty{
  344. display:none;
  345. padding: 18px 16px;
  346. border-radius: 16px;
  347. border: 1px dashed var(--border);
  348. background: var(--panel);
  349. color: var(--muted);
  350. text-align:center;
  351. box-shadow: var(--shadow);
  352. }
  353. footer{
  354. margin-top: 24px;
  355. padding-top: 14px;
  356. border-top: 1px solid var(--border);
  357. display:flex;
  358. justify-content:space-between;
  359. gap:12px;
  360. flex-wrap:wrap;
  361. color: var(--muted);
  362. font-size:12px;
  363. }
  364. .mono{ font-family: var(--mono); }
  365. @media (max-width: 980px){
  366. .card{ grid-column: span 6; }
  367. .search{ min-width: 100%; }
  368. }
  369. @media (max-width: 640px){
  370. header{
  371. flex-direction:column;
  372. align-items:stretch;
  373. }
  374. .toolbar{
  375. justify-content:flex-start;
  376. }
  377. .card{ grid-column: span 12; }
  378. .small{ display:none; }
  379. }
  380. /* ===== 登录模态框 ===== */
  381. .modal-overlay{
  382. display:none;
  383. position:fixed;
  384. inset:0;
  385. background:rgba(0,0,0,.55);
  386. backdrop-filter:blur(4px);
  387. z-index:1000;
  388. align-items:center;
  389. justify-content:center;
  390. }
  391. .modal-overlay.active{ display:flex; }
  392. .modal-box{
  393. width:380px;
  394. max-width:92vw;
  395. padding:32px 28px 28px;
  396. border-radius:var(--radius);
  397. border:1px solid var(--border);
  398. background:var(--bg);
  399. box-shadow:0 20px 60px rgba(0,0,0,.5);
  400. }
  401. .modal-box h2{
  402. margin:0 0 6px;
  403. font-size:20px;
  404. font-weight:800;
  405. }
  406. .modal-box p{
  407. margin:0 0 20px;
  408. color:var(--muted);
  409. font-size:14px;
  410. }
  411. .modal-box label{
  412. display:block;
  413. font-size:13px;
  414. font-weight:600;
  415. margin-bottom:6px;
  416. color:var(--muted);
  417. }
  418. .modal-box input[type="password"]{
  419. width:100%;
  420. height:44px;
  421. padding:0 14px;
  422. border-radius:999px;
  423. border:1px solid var(--border);
  424. background:var(--panel);
  425. color:var(--text);
  426. font-size:15px;
  427. font-family:var(--sans);
  428. outline:none;
  429. transition:border-color .15s;
  430. }
  431. .modal-box input[type="password"]:focus{
  432. border-color:var(--accent);
  433. }
  434. .modal-box .btn-row{
  435. display:flex;
  436. gap:10px;
  437. margin-top:20px;
  438. }
  439. .modal-box .btn-row button{
  440. flex:1;
  441. height:44px;
  442. font-size:14px;
  443. font-weight:600;
  444. }
  445. .modal-box .btn-primary{
  446. background:var(--accent);
  447. border-color:var(--accent);
  448. color:#fff;
  449. }
  450. .modal-box .btn-primary:hover{
  451. opacity:.88;
  452. border-color:var(--accent);
  453. }
  454. .modal-box .modal-error{
  455. display:none;
  456. margin-top:12px;
  457. padding:10px 14px;
  458. border-radius:var(--radius-sm);
  459. background:rgba(255,107,107,.12);
  460. border:1px solid rgba(255,107,107,.3);
  461. color:var(--danger);
  462. font-size:13px;
  463. }
  464. /* ===== 私有链接标识 ===== */
  465. .card.private{
  466. border-left:3px solid var(--danger);
  467. }
  468. </style>
  469. </head>
  470. <body>
  471. <div class="wrap">
  472. <header>
  473. <div class="brand">
  474. <h1>BCJD 服务导航</h1>
  475. <div class="sub">统一入口 · 快速跳转 · 显示时区:<span class="mono">UTC+8</span></div>
  476. </div>
  477. <div class="toolbar">
  478. <div class="pill" id="clock">--:--:-- (UTC+8)</div>
  479. <button id="themeBtn" title="切换主题">主题:跟随系统</button>
  480. <button id="loginBtn" title="登录">登录</button>
  481. <button id="adminBtn" title="管理" style="display:none">管理</button>
  482. </div>
  483. </header>
  484. <div class="search" role="search" aria-label="Search">
  485. <span class="mono" style="opacity:.7">/</span>
  486. <input id="q" placeholder="搜索服务…(回车打开第一个)" autocomplete="off" />
  487. <kbd>Enter</kbd>
  488. </div>
  489. <!-- 加载状态 -->
  490. <div id="loading" style="text-align:center;padding:40px;color:var(--muted);font-size:14px">加载中…</div>
  491. <div class="board" id="board" style="display:none">
  492. <!-- 由 JS 动态渲染 -->
  493. <div class="empty" id="emptyState">没有找到匹配的服务。</div>
  494. </div>
  495. <footer>
  496. <div>提示:按 <span class="mono">/</span> 聚焦搜索;按 <span class="mono">Enter</span> 打开第一个结果;按 <span class="mono">Esc</span> 清空搜索。</div>
  497. <div class="mono">Updated (UTC+8): <span id="ts">--</span></div>
  498. </footer>
  499. </div>
  500. <!-- ===== 登录模态框 ===== -->
  501. <div class="modal-overlay" id="loginModal">
  502. <div class="modal-box">
  503. <h2>登录</h2>
  504. <p>请输入站点密码以解锁全部链接。</p>
  505. <label for="loginPwd">密码</label>
  506. <input type="password" id="loginPwd" placeholder="输入密码" autocomplete="current-password" />
  507. <div class="btn-row">
  508. <button id="loginCancel">取消</button>
  509. <button id="loginConfirm" class="btn-primary">登录</button>
  510. </div>
  511. <div class="modal-error" id="loginError"></div>
  512. </div>
  513. </div>
  514. <script>
  515. (function(){
  516. 'use strict';
  517. const $ = (s) => document.querySelector(s);
  518. const $$ = (s) => [...document.querySelectorAll(s)];
  519. const board = $("#board");
  520. const loading = $("#loading");
  521. const emptyState = $("#emptyState");
  522. const q = $("#q");
  523. const loginBtn = $("#loginBtn");
  524. const adminBtn = $("#adminBtn");
  525. const loginModal = $("#loginModal");
  526. const loginPwd = $("#loginPwd");
  527. const loginError = $("#loginError");
  528. const loginCancel = $("#loginCancel");
  529. const loginConfirm = $("#loginConfirm");
  530. let loggedIn = false;
  531. let allGroups = [];
  532. // ===== 时钟 =====
  533. const TZ = "Asia/Shanghai";
  534. const fmtClock = new Intl.DateTimeFormat("zh-CN", {
  535. timeZone: TZ, hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
  536. });
  537. function formatYMDHMS_UTC8(d){
  538. const parts = new Intl.DateTimeFormat("zh-CN", {
  539. timeZone: TZ, year: "numeric", month: "2-digit", day: "2-digit",
  540. hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: false,
  541. }).formatToParts(d);
  542. const get = (t) => parts.find(p => p.type === t)?.value || "";
  543. return `${get("year")}-${get("month")}-${get("day")} ${get("hour")}:${get("minute")}:${get("second")}`;
  544. }
  545. function tick(){
  546. const d = new Date();
  547. $("#clock").textContent = `${fmtClock.format(d)} (UTC+8)`;
  548. $("#ts").textContent = formatYMDHMS_UTC8(d);
  549. }
  550. tick(); setInterval(tick, 1000);
  551. // ===== 主题切换 =====
  552. const themeBtn = $("#themeBtn");
  553. const root = document.documentElement;
  554. function getThemeLabel(){
  555. const cur = root.getAttribute("data-theme");
  556. if(cur === "dark") return "主题:深色";
  557. if(cur === "light") return "主题:浅色";
  558. return "主题:跟随系统";
  559. }
  560. function refreshThemeLabel(){ themeBtn.textContent = getThemeLabel(); }
  561. function setTheme(mode){
  562. if(!mode){ root.removeAttribute("data-theme"); localStorage.removeItem("theme"); refreshThemeLabel(); return; }
  563. root.setAttribute("data-theme", mode); localStorage.setItem("theme", mode); refreshThemeLabel();
  564. }
  565. const saved = localStorage.getItem("theme");
  566. if(saved === "dark" || saved === "light") setTheme(saved); else refreshThemeLabel();
  567. themeBtn.addEventListener("click", () => {
  568. const cur = root.getAttribute("data-theme");
  569. if(cur === "dark") setTheme("light"); else if(cur === "light") setTheme(null); else setTheme("dark");
  570. });
  571. // ===== API 请求 =====
  572. async function apiGet(action){
  573. const r = await fetch(`api.php?action=${action}`);
  574. if(!r.ok) { const e = await r.json(); throw new Error(e.error || '请求失败'); }
  575. return r.json();
  576. }
  577. async function apiPost(action, data){
  578. const fd = new URLSearchParams();
  579. for(const [k,v] of Object.entries(data)) fd.append(k, v);
  580. const r = await fetch(`api.php?action=${action}`, {
  581. method: 'POST',
  582. headers: {'Content-Type':'application/x-www-form-urlencoded'},
  583. body: fd.toString(),
  584. });
  585. if(!r.ok) { const e = await r.json(); throw new Error(e.error || '请求失败'); }
  586. return r.json();
  587. }
  588. // ===== 渲染链接 =====
  589. function renderLinks(groups){
  590. board.innerHTML = '';
  591. for(const g of groups){
  592. if(!g.links || g.links.length === 0) continue;
  593. const section = document.createElement('section');
  594. section.className = 'section-block';
  595. section.dataset.group = g.name;
  596. const header = document.createElement('div');
  597. header.className = 'section';
  598. header.innerHTML = `<h2>${esc(g.label || g.name)}</h2><div class="hint">${g.hint || ''}</div>`;
  599. const grid = document.createElement('div');
  600. grid.className = 'grid';
  601. for(const link of g.links){
  602. const card = document.createElement('a');
  603. card.className = 'card' + (link.is_private ? ' private' : '');
  604. card.href = link.url;
  605. card.target = '_blank';
  606. card.rel = 'noopener';
  607. card.dataset.group = g.name;
  608. card.dataset.keywords = (link.keywords || '') + ' ' + link.title + ' ' + (link.description || '');
  609. const tagClass = link.tag_class ? `tag ${link.tag_class}` : 'tag';
  610. card.innerHTML = `
  611. <div class="top">
  612. <div class="title">
  613. <div class="icon" aria-hidden="true">${link.icon_svg || ''}</div>
  614. <div style="min-width:0">
  615. <div class="name">${esc(link.title)}</div>
  616. <div class="meta">${esc(link.meta_domain)}</div>
  617. </div>
  618. </div>
  619. <div class="tags">
  620. ${link.tag_label ? `<span class="${tagClass}">${esc(link.tag_label)}</span>` : ''}
  621. ${link.is_private ? '<span class="tag" style="color:var(--danger);border-color:rgba(255,107,107,.3);background:rgba(255,107,107,.08)">🔒 私有</span>' : ''}
  622. </div>
  623. </div>
  624. <div class="desc">${esc(link.description)}</div>
  625. <div class="actions">
  626. <span class="go">打开 →</span>
  627. <span class="small">${link.url.replace(/^https?:\/\//,'').split('/')[0]}</span>
  628. </div>
  629. `;
  630. grid.appendChild(card);
  631. }
  632. section.appendChild(header);
  633. section.appendChild(grid);
  634. board.appendChild(section);
  635. }
  636. board.appendChild(emptyState);
  637. applyFilter();
  638. }
  639. function esc(s){ return (s || '').replace(/[&<>"']/g, function(m){
  640. return {'&':'&','<':'<','>':'>','"':'"',"'":'''}[m];
  641. });}
  642. // ===== 获取并渲染链接 =====
  643. async function loadLinks(){
  644. loading.style.display = '';
  645. board.style.display = 'none';
  646. try {
  647. const data = await apiGet('get_links');
  648. loggedIn = data.logged_in;
  649. allGroups = data.groups || [];
  650. renderLinks(allGroups);
  651. loading.style.display = 'none';
  652. board.style.display = '';
  653. loginBtn.textContent = loggedIn ? '已登录' : '登录';
  654. adminBtn.style.display = loggedIn ? '' : 'none';
  655. } catch(e){
  656. loading.textContent = '加载失败:' + e.message;
  657. }
  658. }
  659. // ===== 搜索 =====
  660. function norm(s){ return (s || "").toLowerCase().trim(); }
  661. function visibleCards(){ return $$(".card").filter(c => c.style.display !== "none"); }
  662. function updateGroups(){
  663. for(const group of $$(".section-block")){
  664. const key = group.dataset.group;
  665. const count = $$(".card").filter(c => c.dataset.group === key && c.style.display !== "none").length;
  666. group.style.display = count > 0 ? "" : "none";
  667. }
  668. }
  669. function applyFilter(){
  670. const v = norm(q.value);
  671. for(const c of $$(".card")){
  672. const hay = norm((c.dataset.keywords || "") + " " + c.textContent);
  673. c.style.display = (!v || hay.includes(v)) ? "" : "none";
  674. }
  675. updateGroups();
  676. emptyState.style.display = visibleCards().length ? "none" : "block";
  677. }
  678. q.addEventListener("input", applyFilter);
  679. q.addEventListener("keydown", (e) => {
  680. if(e.key === "Enter"){ const first = visibleCards()[0]; if(first) first.click(); }
  681. if(e.key === "Escape"){ q.value = ""; applyFilter(); q.blur(); }
  682. });
  683. document.addEventListener("keydown", (e) => {
  684. if(e.key === "/" && document.activeElement !== q){ e.preventDefault(); q.focus(); }
  685. });
  686. // ===== 登录逻辑 =====
  687. function showLoginModal(){
  688. loginModal.classList.add('active'); loginPwd.value = '';
  689. loginError.style.display = 'none'; loginPwd.focus();
  690. }
  691. function hideLoginModal(){ loginModal.classList.remove('active'); }
  692. loginBtn.addEventListener('click', () => {
  693. if(loggedIn){
  694. if(!confirm('确定要退出登录吗?')) return;
  695. apiPost('logout', {}).then(() => { loggedIn = false; loadLinks(); }).catch(e => alert(e.message));
  696. } else {
  697. showLoginModal();
  698. }
  699. });
  700. function doLogin(){
  701. const pwd = loginPwd.value;
  702. if(!pwd){ loginError.textContent = '请输入密码'; loginError.style.display = ''; return; }
  703. loginConfirm.disabled = true; loginConfirm.textContent = '登录中…';
  704. apiPost('login', {password: pwd}).then(data => {
  705. if(data.success){ hideLoginModal(); loadLinks(); }
  706. else { loginError.textContent = data.error || '密码错误'; loginError.style.display = ''; loginConfirm.disabled = false; loginConfirm.textContent = '登录'; }
  707. }).catch(e => {
  708. loginError.textContent = e.message; loginError.style.display = '';
  709. loginConfirm.disabled = false; loginConfirm.textContent = '登录';
  710. });
  711. }
  712. loginConfirm.addEventListener('click', doLogin);
  713. loginPwd.addEventListener('keydown', (e) => { if(e.key === 'Enter') doLogin(); });
  714. loginCancel.addEventListener('click', hideLoginModal);
  715. loginModal.addEventListener('click', (e) => { if(e.target === loginModal) hideLoginModal(); });
  716. // ===== 管理面板 =====
  717. adminBtn.addEventListener('click', () => { window.location.href = 'admin.php'; });
  718. // ===== 启动 =====
  719. loadLinks();
  720. })();
  721. </script>
  722. </body>
  723. </html>