admin.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605
  1. <?php
  2. /**
  3. * BCJD 导航 - 管理面板
  4. *
  5. * 安全特性:
  6. * - CSRF Token 防护所有写操作
  7. * - 请求时动态获取 Token(不与页面静态绑定)
  8. */
  9. require_once __DIR__ . '/../init.php';
  10. use BCJD\Auth;
  11. $csrfToken = Auth::getCsrfToken();
  12. ?><!doctype html>
  13. <html lang="zh-CN">
  14. <head>
  15. <meta charset="utf-8" />
  16. <meta name="viewport" content="width=device-width,initial-scale=1" />
  17. <meta name="color-scheme" content="light dark" />
  18. <title>BCJD 导航 - 管理面板</title>
  19. <style>
  20. :root{
  21. --bg: #0b1020;
  22. --panel: rgba(255,255,255,.06);
  23. --panel-2: rgba(255,255,255,.10);
  24. --text: rgba(255,255,255,.92);
  25. --muted: rgba(255,255,255,.65);
  26. --border: rgba(255,255,255,.12);
  27. --shadow: 0 10px 30px rgba(0,0,0,.35);
  28. --accent: #6ea8ff;
  29. --accent2: #7cf7d4;
  30. --danger: #ff6b6b;
  31. --ok: #3ddc97;
  32. --max: 1120px;
  33. --radius: 18px;
  34. --radius-sm: 12px;
  35. --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
  36. --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", Arial, sans-serif;
  37. }
  38. html[data-theme="light"]{
  39. --bg: #f6f7fb;
  40. --panel: rgba(0,0,0,.04);
  41. --panel-2: rgba(0,0,0,.06);
  42. --text: rgba(0,0,0,.88);
  43. --muted: rgba(0,0,0,.58);
  44. --border: rgba(0,0,0,.10);
  45. --shadow: 0 10px 30px rgba(0,0,0,.12);
  46. --accent: #245bdb;
  47. --accent2: #0aa37f;
  48. --danger: #d64545;
  49. --ok: #0a7f5f;
  50. }
  51. @media (prefers-color-scheme: light){
  52. html:not([data-theme="dark"]){
  53. --bg: #f6f7fb;
  54. --panel: rgba(0,0,0,.04);
  55. --panel-2: rgba(0,0,0,.06);
  56. --text: rgba(0,0,0,.88);
  57. --muted: rgba(0,0,0,.58);
  58. --border: rgba(0,0,0,.10);
  59. --shadow: 0 10px 30px rgba(0,0,0,.12);
  60. --accent: #245bdb;
  61. --accent2: #0aa37f;
  62. --danger: #d64545;
  63. --ok: #0a7f5f;
  64. }
  65. }
  66. *{ box-sizing:border-box; }
  67. body{
  68. margin:0;
  69. min-height:100vh;
  70. font-family: var(--sans);
  71. color: var(--text);
  72. background: var(--bg);
  73. }
  74. a{ color:var(--accent); text-decoration:none; }
  75. a:hover{ text-decoration:underline; }
  76. .wrap{
  77. max-width: var(--max);
  78. margin: 0 auto;
  79. padding: 30px 18px 44px;
  80. }
  81. header{
  82. display:flex;
  83. justify-content:space-between;
  84. align-items:center;
  85. gap:18px;
  86. margin-bottom: 24px;
  87. }
  88. header h1{ margin:0; font-size:22px; font-weight:800; }
  89. .header-actions{ display:flex; gap:10px; align-items:center; }
  90. button, .btn{
  91. height: 40px; display:inline-flex; align-items:center; justify-content:center;
  92. padding: 0 16px; border-radius:999px; border:1px solid var(--border);
  93. background: var(--panel); box-shadow: var(--shadow); color: var(--text);
  94. cursor:pointer; font-family: var(--sans); font-size:13px; font-weight:600;
  95. transition: background .15s; white-space:nowrap;
  96. }
  97. button:hover, .btn:hover{ background: var(--panel-2); border-color: rgba(110,168,255,.28); }
  98. .btn-primary{ background: var(--accent); border-color: var(--accent); color: #fff; }
  99. .btn-primary:hover{ opacity:.88; border-color:var(--accent); background:var(--accent); }
  100. .btn-danger{ background: rgba(255,107,107,.15); border-color: rgba(255,107,107,.35); color: var(--danger); }
  101. .btn-danger:hover{ background: rgba(255,107,107,.25); }
  102. .btn-sm{ height:34px; padding:0 12px; font-size:12px; }
  103. .tabs{
  104. display:flex; gap:4px; margin-bottom: 24px;
  105. border-bottom:1px solid var(--border); padding-bottom:0;
  106. }
  107. .tab{
  108. padding:10px 18px; border-radius:12px 12px 0 0; cursor:pointer;
  109. font-size:14px; font-weight:600; color: var(--muted);
  110. border:1px solid transparent; border-bottom:none; transition: all .12s; background:transparent;
  111. }
  112. .tab:hover{ color:var(--text); background:var(--panel); }
  113. .tab.active{ color:var(--accent); border-color:var(--border); background:var(--panel); }
  114. .tab-content{ display:none; }
  115. .tab-content.active{ display:block; }
  116. .link-item{
  117. display:flex; align-items:center; gap:12px; padding:12px 14px;
  118. border-radius:var(--radius-sm); border:1px solid var(--border);
  119. background:var(--panel); margin-bottom:8px; transition:background .12s;
  120. }
  121. .link-item:hover{ background:var(--panel-2); }
  122. .link-item .info{ flex:1; min-width:0; }
  123. .link-item .info .title{ font-weight:700; font-size:14px; }
  124. .link-item .info .url{
  125. font-size:12px; color:var(--muted); font-family:var(--mono);
  126. white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
  127. }
  128. .link-item .badge{
  129. font-size:11px; padding:3px 8px; border-radius:999px;
  130. border:1px solid var(--border); background:var(--panel-2); color:var(--muted); white-space:nowrap;
  131. }
  132. .link-item .badge.private{
  133. border-color:rgba(255,107,107,.35); background:rgba(255,107,107,.08); color:var(--danger);
  134. }
  135. .link-item .item-actions{ display:flex; gap:6px; flex-shrink:0; }
  136. .form-group{ margin-bottom:16px; }
  137. .form-group label{
  138. display:block; font-size:13px; font-weight:600; margin-bottom:6px; color:var(--muted);
  139. }
  140. .form-group input, .form-group select, .form-group textarea{
  141. width:100%; height:42px; padding:0 14px; border-radius:999px;
  142. border:1px solid var(--border); background:var(--panel); color:var(--text);
  143. font-size:14px; font-family:var(--sans); outline:none; transition:border-color .15s;
  144. }
  145. .form-group textarea{
  146. height:auto; min-height:60px; padding:10px 14px; border-radius:var(--radius-sm);
  147. resize:vertical; font-family:var(--mono); font-size:13px;
  148. }
  149. .form-group input:focus, .form-group select:focus, .form-group textarea:focus{ border-color:var(--accent); }
  150. .form-group select option{ background:var(--bg); color:var(--text); }
  151. .form-row{ display:grid; grid-template-columns:1fr 1fr; gap:14px; }
  152. .form-row-3{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px; }
  153. .checkbox-row{
  154. display:flex; align-items:center; gap:10px; margin-bottom:16px;
  155. }
  156. .checkbox-row input[type="checkbox"]{ width:18px; height:18px; accent-color:var(--accent); }
  157. .toast{
  158. display:none; position:fixed; bottom:30px; left:50%; transform:translateX(-50%);
  159. padding:12px 24px; border-radius:999px; background:var(--panel-2);
  160. border:1px solid var(--border); backdrop-filter:blur(10px); box-shadow:var(--shadow);
  161. font-size:14px; z-index:999;
  162. }
  163. .toast.show{ display:block; }
  164. .toast.success{ border-color:rgba(61,220,151,.35); color:var(--ok); }
  165. .toast.error{ border-color:rgba(255,107,107,.35); color:var(--danger); }
  166. .modal-overlay{
  167. display:none; position:fixed; inset:0; background:rgba(0,0,0,.55);
  168. backdrop-filter:blur(4px); z-index:1000; align-items:center; justify-content:center;
  169. }
  170. .modal-overlay.active{ display:flex; }
  171. .modal-box{
  172. width:520px; max-width:92vw; max-height:90vh; overflow-y:auto;
  173. padding:28px; border-radius:var(--radius); border:1px solid var(--border);
  174. background:var(--bg); box-shadow:0 20px 60px rgba(0,0,0,.5);
  175. }
  176. .modal-box h2{ margin:0 0 18px; font-size:20px; font-weight:800; }
  177. .modal-box .btn-row{ display:flex; gap:10px; margin-top:20px; justify-content:flex-end; }
  178. @media (max-width: 720px){
  179. .form-row, .form-row-3{ grid-template-columns:1fr; }
  180. .link-item{ flex-wrap:wrap; }
  181. .link-item .item-actions{ width:100%; justify-content:flex-end; }
  182. }
  183. </style>
  184. </head>
  185. <body>
  186. <div class="wrap">
  187. <header>
  188. <h1>BCJD 导航 · 管理面板</h1>
  189. <div class="header-actions">
  190. <a href="index.php" class="btn">← 返回首页</a>
  191. <button id="logoutBtn">退出登录</button>
  192. </div>
  193. </header>
  194. <input type="hidden" id="csrfToken" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>" />
  195. <div class="tabs">
  196. <div class="tab active" data-tab="links">链接管理</div>
  197. <div class="tab" data-tab="add">添加链接</div>
  198. <div class="tab" data-tab="password">修改密码</div>
  199. </div>
  200. <!-- ===== 链接管理 ===== -->
  201. <div class="tab-content active" id="tab-links">
  202. <div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
  203. <input id="filterLinks" placeholder="筛选链接…" style="height:40px;padding:0 14px;border-radius:999px;border:1px solid var(--border);background:var(--panel);color:var(--text);font-size:13px;font-family:var(--sans);flex:1;min-width:200px;outline:none" />
  204. <button id="refreshLinks" class="btn-primary btn-sm">刷新</button>
  205. </div>
  206. <div id="linkList"></div>
  207. <div id="linkLoading" style="text-align:center;padding:30px;color:var(--muted)">加载中…</div>
  208. </div>
  209. <!-- ===== 添加链接 ===== -->
  210. <div class="tab-content" id="tab-add">
  211. <form id="addForm" autocomplete="off">
  212. <div class="form-row">
  213. <div class="form-group">
  214. <label for="addTitle">标题 *</label>
  215. <input type="text" id="addTitle" required placeholder="如:Monica" />
  216. </div>
  217. <div class="form-group">
  218. <label for="addUrl">链接地址 *</label>
  219. <input type="url" id="addUrl" required placeholder="如:https://monica.bcjd.net" />
  220. </div>
  221. </div>
  222. <div class="form-group">
  223. <label for="addDesc">描述</label>
  224. <input type="text" id="addDesc" placeholder="简短描述该服务" />
  225. </div>
  226. <div class="form-row">
  227. <div class="form-group">
  228. <label for="addGroup">分组名(英文标识)</label>
  229. <input type="text" id="addGroup" placeholder="如:apps / ops / sites" value="apps" />
  230. </div>
  231. <div class="form-group">
  232. <label for="addGroupLabel">分组显示名称</label>
  233. <input type="text" id="addGroupLabel" placeholder="如:应用" />
  234. </div>
  235. </div>
  236. <div class="form-row-3">
  237. <div class="form-group">
  238. <label for="addKeywords">搜索关键词</label>
  239. <input type="text" id="addKeywords" placeholder="空格分隔" />
  240. </div>
  241. <div class="form-group">
  242. <label for="addTagLabel">标签文字</label>
  243. <input type="text" id="addTagLabel" placeholder="如:App" />
  244. </div>
  245. <div class="form-group">
  246. <label for="addTagClass">标签样式</label>
  247. <select id="addTagClass">
  248. <option value="">默认</option>
  249. <option value="ok">绿色 (ok)</option>
  250. <option value="warn">红色 (warn)</option>
  251. </select>
  252. </div>
  253. </div>
  254. <div class="form-row">
  255. <div class="form-group">
  256. <label for="addMetaDomain">域名/元信息</label>
  257. <input type="text" id="addMetaDomain" placeholder="如:monica.bcjd.net" />
  258. </div>
  259. <div class="form-group">
  260. <label for="addIconSvg">SVG 图标代码</label>
  261. <input type="text" id="addIconSvg" placeholder="<svg>...</svg>" />
  262. </div>
  263. </div>
  264. <div class="checkbox-row">
  265. <input type="checkbox" id="addIsPrivate" />
  266. <label for="addIsPrivate" style="margin:0;cursor:pointer">🔒 私有链接(需登录才能看到)</label>
  267. </div>
  268. <div>
  269. <button type="submit" class="btn-primary">添加链接</button>
  270. </div>
  271. </form>
  272. </div>
  273. <!-- ===== 修改密码 ===== -->
  274. <div class="tab-content" id="tab-password">
  275. <form id="pwdForm" autocomplete="off">
  276. <div class="form-group">
  277. <label for="oldPwd">当前密码</label>
  278. <input type="password" id="oldPwd" required autocomplete="current-password" />
  279. </div>
  280. <div class="form-row">
  281. <div class="form-group">
  282. <label for="newPwd">新密码(至少6位)</label>
  283. <input type="password" id="newPwd" required minlength="6" autocomplete="new-password" />
  284. </div>
  285. <div class="form-group">
  286. <label for="confirmPwd">确认新密码</label>
  287. <input type="password" id="confirmPwd" required minlength="6" autocomplete="new-password" />
  288. </div>
  289. </div>
  290. <div>
  291. <button type="submit" class="btn-primary">更新密码</button>
  292. </div>
  293. </form>
  294. </div>
  295. </div>
  296. <!-- ===== 编辑模态框 ===== -->
  297. <div class="modal-overlay" id="editModal">
  298. <div class="modal-box">
  299. <h2>编辑链接</h2>
  300. <form id="editForm" autocomplete="off">
  301. <input type="hidden" id="editId" />
  302. <div class="form-row">
  303. <div class="form-group">
  304. <label for="editTitle">标题 *</label>
  305. <input type="text" id="editTitle" required />
  306. </div>
  307. <div class="form-group">
  308. <label for="editUrl">链接地址 *</label>
  309. <input type="url" id="editUrl" required />
  310. </div>
  311. </div>
  312. <div class="form-group">
  313. <label for="editDesc">描述</label>
  314. <input type="text" id="editDesc" />
  315. </div>
  316. <div class="form-row">
  317. <div class="form-group">
  318. <label for="editGroup">分组名</label>
  319. <input type="text" id="editGroup" />
  320. </div>
  321. <div class="form-group">
  322. <label for="editKeywords">关键词</label>
  323. <input type="text" id="editKeywords" />
  324. </div>
  325. </div>
  326. <div class="form-row-3">
  327. <div class="form-group">
  328. <label for="editTagLabel">标签</label>
  329. <input type="text" id="editTagLabel" />
  330. </div>
  331. <div class="form-group">
  332. <label for="editTagClass">标签样式</label>
  333. <select id="editTagClass">
  334. <option value="">默认</option>
  335. <option value="ok">绿色 (ok)</option>
  336. <option value="warn">红色 (warn)</option>
  337. </select>
  338. </div>
  339. <div class="form-group">
  340. <label for="editMetaDomain">域名</label>
  341. <input type="text" id="editMetaDomain" />
  342. </div>
  343. </div>
  344. <div class="form-group">
  345. <label for="editIconSvg">SVG 图标</label>
  346. <textarea id="editIconSvg" rows="3"></textarea>
  347. </div>
  348. <div class="checkbox-row">
  349. <input type="checkbox" id="editIsPrivate" />
  350. <label for="editIsPrivate" style="margin:0;cursor:pointer">🔒 私有链接</label>
  351. </div>
  352. <div class="btn-row">
  353. <button type="button" id="editCancel">取消</button>
  354. <button type="submit" class="btn-primary">保存</button>
  355. </div>
  356. </form>
  357. </div>
  358. </div>
  359. <!-- Toast -->
  360. <div class="toast" id="toast"></div>
  361. <script>
  362. (function(){
  363. 'use strict';
  364. const $ = (s) => document.querySelector(s);
  365. const $$ = (s) => [...document.querySelectorAll(s)];
  366. const esc = (s) => (s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
  367. function getCsrf(){ return $('#csrfToken').value; }
  368. function toast(msg, type = 'success'){
  369. const t = $('#toast');
  370. t.textContent = msg;
  371. t.className = 'toast show ' + type;
  372. clearTimeout(t._timer);
  373. t._timer = setTimeout(() => t.classList.remove('show'), 3000);
  374. }
  375. // ===== API =====
  376. async function apiGet(action){
  377. const r = await fetch(`api.php?action=${action}`);
  378. if(!r.ok){ const e = await r.json(); throw new Error(e.error || '请求失败'); }
  379. return r.json();
  380. }
  381. async function apiPost(action, data){
  382. const fd = new URLSearchParams({csrf_token: getCsrf()});
  383. for(const [k,v] of Object.entries(data)) fd.append(k, v);
  384. const r = await fetch(`api.php?action=${action}`, {
  385. method:'POST', headers:{'Content-Type':'application/x-www-form-urlencoded'},
  386. body: fd.toString(),
  387. });
  388. if(!r.ok){ const e = await r.json(); throw new Error(e.error || '请求失败'); }
  389. return r.json();
  390. }
  391. // ===== 登录检查 =====
  392. apiGet('check_login').then(d => { if(!d.logged_in) window.location.href = 'index.php'; });
  393. // ===== 退出登录 =====
  394. $('#logoutBtn').addEventListener('click', async () => {
  395. await apiPost('logout', {});
  396. window.location.href = 'index.php';
  397. });
  398. // ===== 选项卡 =====
  399. $$('.tab').forEach(tab => {
  400. tab.addEventListener('click', () => {
  401. $$('.tab').forEach(t => t.classList.remove('active'));
  402. $$('.tab-content').forEach(t => t.classList.remove('active'));
  403. tab.classList.add('active');
  404. const target = document.getElementById('tab-' + tab.dataset.tab);
  405. if(target) target.classList.add('active');
  406. });
  407. });
  408. // ===== 加载链接列表 =====
  409. let allLinks = [];
  410. async function loadLinks(){
  411. const list = $('#linkList');
  412. const loading = $('#linkLoading');
  413. loading.style.display = '';
  414. list.innerHTML = '';
  415. try {
  416. const data = await apiGet('get_links');
  417. allLinks = [];
  418. for(const g of (data.groups || [])){
  419. for(const l of (g.links || [])){
  420. allLinks.push(l);
  421. }
  422. }
  423. renderLinks(allLinks);
  424. loading.style.display = 'none';
  425. } catch(e){
  426. loading.textContent = '加载失败:' + e.message;
  427. }
  428. }
  429. function renderLinks(links){
  430. const list = $('#linkList');
  431. if(links.length === 0){
  432. list.innerHTML = '<div style="text-align:center;padding:30px;color:var(--muted)">暂无链接</div>';
  433. return;
  434. }
  435. list.innerHTML = links.map(l => `
  436. <div class="link-item" data-id="${l.id}">
  437. <div class="info">
  438. <div class="title">${esc(l.title)}</div>
  439. <div class="url">${esc(l.url)}</div>
  440. </div>
  441. <span class="badge">${esc(l.group_name)}</span>
  442. ${l.is_private ? '<span class="badge private">🔒 私有</span>' : '<span class="badge">公开</span>'}
  443. <div class="item-actions">
  444. <button class="btn-sm edit-btn" data-id="${l.id}">编辑</button>
  445. <button class="btn-sm btn-danger delete-btn" data-id="${l.id}">删除</button>
  446. </div>
  447. </div>
  448. `).join('');
  449. list.querySelectorAll('.edit-btn').forEach(btn => {
  450. btn.addEventListener('click', () => openEdit(parseInt(btn.dataset.id)));
  451. });
  452. list.querySelectorAll('.delete-btn').forEach(btn => {
  453. btn.addEventListener('click', () => deleteLink(parseInt(btn.dataset.id)));
  454. });
  455. }
  456. // ===== 筛选 =====
  457. $('#filterLinks').addEventListener('input', () => {
  458. const v = $('#filterLinks').value.toLowerCase().trim();
  459. if(!v){ renderLinks(allLinks); return; }
  460. const filtered = allLinks.filter(l =>
  461. (l.title + ' ' + l.url + ' ' + l.group_name + ' ' + (l.keywords || '') + ' ' + (l.description || '')).toLowerCase().includes(v)
  462. );
  463. renderLinks(filtered);
  464. });
  465. $('#refreshLinks').addEventListener('click', loadLinks);
  466. // ===== 删除链接 =====
  467. async function deleteLink(id){
  468. if(!confirm('确定要删除此链接吗?')) return;
  469. try {
  470. const data = await apiPost('delete_link', {id});
  471. if(data.success){ toast('已删除'); loadLinks(); }
  472. else { toast(data.error || '删除失败', 'error'); }
  473. } catch(e){ toast(e.message, 'error'); }
  474. }
  475. // ===== 编辑链接 =====
  476. function openEdit(id){
  477. const l = allLinks.find(x => x.id === id);
  478. if(!l) return;
  479. $('#editId').value = l.id;
  480. $('#editTitle').value = l.title;
  481. $('#editUrl').value = l.url;
  482. $('#editDesc').value = l.description || '';
  483. $('#editGroup').value = l.group_name;
  484. $('#editKeywords').value = l.keywords || '';
  485. $('#editTagLabel').value = l.tag_label || '';
  486. $('#editTagClass').value = l.tag_class || '';
  487. $('#editMetaDomain').value = l.meta_domain || '';
  488. $('#editIconSvg').value = l.icon_svg || '';
  489. $('#editIsPrivate').checked = !!l.is_private;
  490. $('#editModal').classList.add('active');
  491. }
  492. $('#editCancel').addEventListener('click', () => $('#editModal').classList.remove('active'));
  493. $('#editModal').addEventListener('click', (e) => { if(e.target === $('#editModal')) $('#editModal').classList.remove('active'); });
  494. $('#editForm').addEventListener('submit', async (e) => {
  495. e.preventDefault();
  496. try {
  497. const result = await apiPost('update_link', {
  498. id: $('#editId').value,
  499. title: $('#editTitle').value,
  500. url: $('#editUrl').value,
  501. description: $('#editDesc').value,
  502. group_name: $('#editGroup').value,
  503. keywords: $('#editKeywords').value,
  504. tag_label: $('#editTagLabel').value,
  505. tag_class: $('#editTagClass').value,
  506. meta_domain: $('#editMetaDomain').value,
  507. icon_svg: $('#editIconSvg').value,
  508. is_private: $('#editIsPrivate').checked ? 1 : 0,
  509. });
  510. if(result.success){ toast('已更新'); $('#editModal').classList.remove('active'); loadLinks(); }
  511. else { toast(result.error || '更新失败', 'error'); }
  512. } catch(e){ toast(e.message, 'error'); }
  513. });
  514. // ===== 添加链接 =====
  515. $('#addForm').addEventListener('submit', async (e) => {
  516. e.preventDefault();
  517. try {
  518. const result = await apiPost('add_link', {
  519. title: $('#addTitle').value,
  520. url: $('#addUrl').value,
  521. description: $('#addDesc').value,
  522. group_name: $('#addGroup').value || 'default',
  523. group_label: $('#addGroupLabel').value,
  524. keywords: $('#addKeywords').value,
  525. tag_label: $('#addTagLabel').value,
  526. tag_class: $('#addTagClass').value,
  527. meta_domain: $('#addMetaDomain').value,
  528. icon_svg: $('#addIconSvg').value,
  529. is_private: $('#addIsPrivate').checked ? 1 : 0,
  530. });
  531. if(result.success){
  532. toast('链接已添加');
  533. $('#addForm').reset();
  534. $$('.tab').forEach(t => t.classList.remove('active'));
  535. $$('.tab-content').forEach(t => t.classList.remove('active'));
  536. document.querySelector('[data-tab="links"]').classList.add('active');
  537. document.getElementById('tab-links').classList.add('active');
  538. loadLinks();
  539. } else { toast(result.error || '添加失败', 'error'); }
  540. } catch(e){ toast(e.message, 'error'); }
  541. });
  542. // ===== 修改密码 =====
  543. $('#pwdForm').addEventListener('submit', async (e) => {
  544. e.preventDefault();
  545. const oldPwd = $('#oldPwd').value;
  546. const newPwd = $('#newPwd').value;
  547. const confirmPwd = $('#confirmPwd').value;
  548. if(newPwd !== confirmPwd){ toast('两次输入的新密码不一致', 'error'); return; }
  549. if(newPwd.length < 6){ toast('新密码至少6位', 'error'); return; }
  550. try {
  551. const result = await apiPost('update_password', {
  552. old_password: oldPwd, new_password: newPwd, confirm_password: confirmPwd,
  553. });
  554. if(result.success){ toast('密码已更新'); $('#pwdForm').reset(); }
  555. else { toast(result.error || '密码更新失败', 'error'); }
  556. } catch(e){ toast(e.message, 'error'); }
  557. });
  558. // ===== 启动 =====
  559. loadLinks();
  560. })();
  561. </script>
  562. </body>
  563. </html>