| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- <?php
- /**
- * BCJD 导航 - 管理面板
- *
- * 安全特性:
- * - CSRF Token 防护所有写操作
- * - 请求时动态获取 Token(不与页面静态绑定)
- */
- require_once __DIR__ . '/../init.php';
- use BCJD\Auth;
- $csrfToken = Auth::getCsrfToken();
- ?><!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>BCJD 导航 - 管理面板</title>
- <style>
- :root{
- --bg: #0b1020;
- --panel: rgba(255,255,255,.06);
- --panel-2: rgba(255,255,255,.10);
- --text: rgba(255,255,255,.92);
- --muted: rgba(255,255,255,.65);
- --border: rgba(255,255,255,.12);
- --shadow: 0 10px 30px rgba(0,0,0,.35);
- --accent: #6ea8ff;
- --accent2: #7cf7d4;
- --danger: #ff6b6b;
- --ok: #3ddc97;
- --max: 1120px;
- --radius: 18px;
- --radius-sm: 12px;
- --mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
- --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "PingFang SC", "Noto Sans CJK SC", "Microsoft YaHei", Arial, sans-serif;
- }
- html[data-theme="light"]{
- --bg: #f6f7fb;
- --panel: rgba(0,0,0,.04);
- --panel-2: rgba(0,0,0,.06);
- --text: rgba(0,0,0,.88);
- --muted: rgba(0,0,0,.58);
- --border: rgba(0,0,0,.10);
- --shadow: 0 10px 30px rgba(0,0,0,.12);
- --accent: #245bdb;
- --accent2: #0aa37f;
- --danger: #d64545;
- --ok: #0a7f5f;
- }
- @media (prefers-color-scheme: light){
- html:not([data-theme="dark"]){
- --bg: #f6f7fb;
- --panel: rgba(0,0,0,.04);
- --panel-2: rgba(0,0,0,.06);
- --text: rgba(0,0,0,.88);
- --muted: rgba(0,0,0,.58);
- --border: rgba(0,0,0,.10);
- --shadow: 0 10px 30px rgba(0,0,0,.12);
- --accent: #245bdb;
- --accent2: #0aa37f;
- --danger: #d64545;
- --ok: #0a7f5f;
- }
- }
- *{ box-sizing:border-box; }
- body{
- margin:0;
- min-height:100vh;
- font-family: var(--sans);
- color: var(--text);
- background: var(--bg);
- }
- a{ color:var(--accent); text-decoration:none; }
- a:hover{ text-decoration:underline; }
- .wrap{
- max-width: var(--max);
- margin: 0 auto;
- padding: 30px 18px 44px;
- }
- header{
- display:flex;
- justify-content:space-between;
- align-items:center;
- gap:18px;
- margin-bottom: 24px;
- }
- header h1{ margin:0; font-size:22px; font-weight:800; }
- .header-actions{ display:flex; gap:10px; align-items:center; }
- button, .btn{
- height: 40px; display:inline-flex; align-items:center; justify-content:center;
- padding: 0 16px; border-radius:999px; border:1px solid var(--border);
- background: var(--panel); box-shadow: var(--shadow); color: var(--text);
- cursor:pointer; font-family: var(--sans); font-size:13px; font-weight:600;
- transition: background .15s; white-space:nowrap;
- }
- button:hover, .btn:hover{ background: var(--panel-2); border-color: rgba(110,168,255,.28); }
- .btn-primary{ background: var(--accent); border-color: var(--accent); color: #fff; }
- .btn-primary:hover{ opacity:.88; border-color:var(--accent); background:var(--accent); }
- .btn-danger{ background: rgba(255,107,107,.15); border-color: rgba(255,107,107,.35); color: var(--danger); }
- .btn-danger:hover{ background: rgba(255,107,107,.25); }
- .btn-sm{ height:34px; padding:0 12px; font-size:12px; }
- .tabs{
- display:flex; gap:4px; margin-bottom: 24px;
- border-bottom:1px solid var(--border); padding-bottom:0;
- }
- .tab{
- padding:10px 18px; border-radius:12px 12px 0 0; cursor:pointer;
- font-size:14px; font-weight:600; color: var(--muted);
- border:1px solid transparent; border-bottom:none; transition: all .12s; background:transparent;
- }
- .tab:hover{ color:var(--text); background:var(--panel); }
- .tab.active{ color:var(--accent); border-color:var(--border); background:var(--panel); }
- .tab-content{ display:none; }
- .tab-content.active{ display:block; }
- .link-item{
- display:flex; align-items:center; gap:12px; padding:12px 14px;
- border-radius:var(--radius-sm); border:1px solid var(--border);
- background:var(--panel); margin-bottom:8px; transition:background .12s;
- }
- .link-item:hover{ background:var(--panel-2); }
- .link-item .info{ flex:1; min-width:0; }
- .link-item .info .title{ font-weight:700; font-size:14px; }
- .link-item .info .url{
- font-size:12px; color:var(--muted); font-family:var(--mono);
- white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
- }
- .link-item .badge{
- font-size:11px; padding:3px 8px; border-radius:999px;
- border:1px solid var(--border); background:var(--panel-2); color:var(--muted); white-space:nowrap;
- }
- .link-item .badge.private{
- border-color:rgba(255,107,107,.35); background:rgba(255,107,107,.08); color:var(--danger);
- }
- .link-item .item-actions{ display:flex; gap:6px; flex-shrink:0; }
- .form-group{ margin-bottom:16px; }
- .form-group label{
- display:block; font-size:13px; font-weight:600; margin-bottom:6px; color:var(--muted);
- }
- .form-group input, .form-group select, .form-group textarea{
- width:100%; height:42px; padding:0 14px; border-radius:999px;
- border:1px solid var(--border); background:var(--panel); color:var(--text);
- font-size:14px; font-family:var(--sans); outline:none; transition:border-color .15s;
- }
- .form-group textarea{
- height:auto; min-height:60px; padding:10px 14px; border-radius:var(--radius-sm);
- resize:vertical; font-family:var(--mono); font-size:13px;
- }
- .form-group input:focus, .form-group select:focus, .form-group textarea:focus{ border-color:var(--accent); }
- .form-group select option{ background:var(--bg); color:var(--text); }
- .form-row{ display:grid; grid-template-columns:1fr 1fr; gap:14px; }
- .form-row-3{ display:grid; grid-template-columns:1fr 1fr 1fr; gap:14px; }
- .checkbox-row{
- display:flex; align-items:center; gap:10px; margin-bottom:16px;
- }
- .checkbox-row input[type="checkbox"]{ width:18px; height:18px; accent-color:var(--accent); }
- .toast{
- display:none; position:fixed; bottom:30px; left:50%; transform:translateX(-50%);
- padding:12px 24px; border-radius:999px; background:var(--panel-2);
- border:1px solid var(--border); backdrop-filter:blur(10px); box-shadow:var(--shadow);
- font-size:14px; z-index:999;
- }
- .toast.show{ display:block; }
- .toast.success{ border-color:rgba(61,220,151,.35); color:var(--ok); }
- .toast.error{ border-color:rgba(255,107,107,.35); color:var(--danger); }
- .modal-overlay{
- display:none; position:fixed; inset:0; background:rgba(0,0,0,.55);
- backdrop-filter:blur(4px); z-index:1000; align-items:center; justify-content:center;
- }
- .modal-overlay.active{ display:flex; }
- .modal-box{
- width:520px; max-width:92vw; max-height:90vh; overflow-y:auto;
- padding:28px; border-radius:var(--radius); border:1px solid var(--border);
- background:var(--bg); box-shadow:0 20px 60px rgba(0,0,0,.5);
- }
- .modal-box h2{ margin:0 0 18px; font-size:20px; font-weight:800; }
- .modal-box .btn-row{ display:flex; gap:10px; margin-top:20px; justify-content:flex-end; }
- @media (max-width: 720px){
- .form-row, .form-row-3{ grid-template-columns:1fr; }
- .link-item{ flex-wrap:wrap; }
- .link-item .item-actions{ width:100%; justify-content:flex-end; }
- }
- </style>
- </head>
- <body>
- <div class="wrap">
- <header>
- <h1>BCJD 导航 · 管理面板</h1>
- <div class="header-actions">
- <a href="index.php" class="btn">← 返回首页</a>
- <button id="logoutBtn">退出登录</button>
- </div>
- </header>
- <input type="hidden" id="csrfToken" value="<?= htmlspecialchars($csrfToken, ENT_QUOTES, 'UTF-8') ?>" />
- <div class="tabs">
- <div class="tab active" data-tab="links">链接管理</div>
- <div class="tab" data-tab="add">添加链接</div>
- <div class="tab" data-tab="password">修改密码</div>
- </div>
- <!-- ===== 链接管理 ===== -->
- <div class="tab-content active" id="tab-links">
- <div style="margin-bottom:14px;display:flex;gap:10px;align-items:center;flex-wrap:wrap">
- <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" />
- <button id="refreshLinks" class="btn-primary btn-sm">刷新</button>
- </div>
- <div id="linkList"></div>
- <div id="linkLoading" style="text-align:center;padding:30px;color:var(--muted)">加载中…</div>
- </div>
- <!-- ===== 添加链接 ===== -->
- <div class="tab-content" id="tab-add">
- <form id="addForm" autocomplete="off">
- <div class="form-row">
- <div class="form-group">
- <label for="addTitle">标题 *</label>
- <input type="text" id="addTitle" required placeholder="如:Monica" />
- </div>
- <div class="form-group">
- <label for="addUrl">链接地址 *</label>
- <input type="url" id="addUrl" required placeholder="如:https://monica.bcjd.net" />
- </div>
- </div>
- <div class="form-group">
- <label for="addDesc">描述</label>
- <input type="text" id="addDesc" placeholder="简短描述该服务" />
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="addGroup">分组名(英文标识)</label>
- <input type="text" id="addGroup" placeholder="如:apps / ops / sites" value="apps" />
- </div>
- <div class="form-group">
- <label for="addGroupLabel">分组显示名称</label>
- <input type="text" id="addGroupLabel" placeholder="如:应用" />
- </div>
- </div>
- <div class="form-row-3">
- <div class="form-group">
- <label for="addKeywords">搜索关键词</label>
- <input type="text" id="addKeywords" placeholder="空格分隔" />
- </div>
- <div class="form-group">
- <label for="addTagLabel">标签文字</label>
- <input type="text" id="addTagLabel" placeholder="如:App" />
- </div>
- <div class="form-group">
- <label for="addTagClass">标签样式</label>
- <select id="addTagClass">
- <option value="">默认</option>
- <option value="ok">绿色 (ok)</option>
- <option value="warn">红色 (warn)</option>
- </select>
- </div>
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="addMetaDomain">域名/元信息</label>
- <input type="text" id="addMetaDomain" placeholder="如:monica.bcjd.net" />
- </div>
- <div class="form-group">
- <label for="addIconSvg">SVG 图标代码</label>
- <input type="text" id="addIconSvg" placeholder="<svg>...</svg>" />
- </div>
- </div>
- <div class="checkbox-row">
- <input type="checkbox" id="addIsPrivate" />
- <label for="addIsPrivate" style="margin:0;cursor:pointer">🔒 私有链接(需登录才能看到)</label>
- </div>
- <div>
- <button type="submit" class="btn-primary">添加链接</button>
- </div>
- </form>
- </div>
- <!-- ===== 修改密码 ===== -->
- <div class="tab-content" id="tab-password">
- <form id="pwdForm" autocomplete="off">
- <div class="form-group">
- <label for="oldPwd">当前密码</label>
- <input type="password" id="oldPwd" required autocomplete="current-password" />
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="newPwd">新密码(至少6位)</label>
- <input type="password" id="newPwd" required minlength="6" autocomplete="new-password" />
- </div>
- <div class="form-group">
- <label for="confirmPwd">确认新密码</label>
- <input type="password" id="confirmPwd" required minlength="6" autocomplete="new-password" />
- </div>
- </div>
- <div>
- <button type="submit" class="btn-primary">更新密码</button>
- </div>
- </form>
- </div>
- </div>
- <!-- ===== 编辑模态框 ===== -->
- <div class="modal-overlay" id="editModal">
- <div class="modal-box">
- <h2>编辑链接</h2>
- <form id="editForm" autocomplete="off">
- <input type="hidden" id="editId" />
- <div class="form-row">
- <div class="form-group">
- <label for="editTitle">标题 *</label>
- <input type="text" id="editTitle" required />
- </div>
- <div class="form-group">
- <label for="editUrl">链接地址 *</label>
- <input type="url" id="editUrl" required />
- </div>
- </div>
- <div class="form-group">
- <label for="editDesc">描述</label>
- <input type="text" id="editDesc" />
- </div>
- <div class="form-row">
- <div class="form-group">
- <label for="editGroup">分组名</label>
- <input type="text" id="editGroup" />
- </div>
- <div class="form-group">
- <label for="editKeywords">关键词</label>
- <input type="text" id="editKeywords" />
- </div>
- </div>
- <div class="form-row-3">
- <div class="form-group">
- <label for="editTagLabel">标签</label>
- <input type="text" id="editTagLabel" />
- </div>
- <div class="form-group">
- <label for="editTagClass">标签样式</label>
- <select id="editTagClass">
- <option value="">默认</option>
- <option value="ok">绿色 (ok)</option>
- <option value="warn">红色 (warn)</option>
- </select>
- </div>
- <div class="form-group">
- <label for="editMetaDomain">域名</label>
- <input type="text" id="editMetaDomain" />
- </div>
- </div>
- <div class="form-group">
- <label for="editIconSvg">SVG 图标</label>
- <textarea id="editIconSvg" rows="3"></textarea>
- </div>
- <div class="checkbox-row">
- <input type="checkbox" id="editIsPrivate" />
- <label for="editIsPrivate" style="margin:0;cursor:pointer">🔒 私有链接</label>
- </div>
- <div class="btn-row">
- <button type="button" id="editCancel">取消</button>
- <button type="submit" class="btn-primary">保存</button>
- </div>
- </form>
- </div>
- </div>
- <!-- Toast -->
- <div class="toast" id="toast"></div>
- <script>
- (function(){
- 'use strict';
- const $ = (s) => document.querySelector(s);
- const $$ = (s) => [...document.querySelectorAll(s)];
- const esc = (s) => (s || '').replace(/[&<>"']/g, m => ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]);
- function getCsrf(){ return $('#csrfToken').value; }
- function toast(msg, type = 'success'){
- const t = $('#toast');
- t.textContent = msg;
- t.className = 'toast show ' + type;
- clearTimeout(t._timer);
- t._timer = setTimeout(() => t.classList.remove('show'), 3000);
- }
- // ===== 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({csrf_token: getCsrf()});
- 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();
- }
- // ===== 登录检查 =====
- apiGet('check_login').then(d => { if(!d.logged_in) window.location.href = 'index.php'; });
- // ===== 退出登录 =====
- $('#logoutBtn').addEventListener('click', async () => {
- await apiPost('logout', {});
- window.location.href = 'index.php';
- });
- // ===== 选项卡 =====
- $$('.tab').forEach(tab => {
- tab.addEventListener('click', () => {
- $$('.tab').forEach(t => t.classList.remove('active'));
- $$('.tab-content').forEach(t => t.classList.remove('active'));
- tab.classList.add('active');
- const target = document.getElementById('tab-' + tab.dataset.tab);
- if(target) target.classList.add('active');
- });
- });
- // ===== 加载链接列表 =====
- let allLinks = [];
- async function loadLinks(){
- const list = $('#linkList');
- const loading = $('#linkLoading');
- loading.style.display = '';
- list.innerHTML = '';
- try {
- const data = await apiGet('get_links');
- allLinks = [];
- for(const g of (data.groups || [])){
- for(const l of (g.links || [])){
- allLinks.push(l);
- }
- }
- renderLinks(allLinks);
- loading.style.display = 'none';
- } catch(e){
- loading.textContent = '加载失败:' + e.message;
- }
- }
- function renderLinks(links){
- const list = $('#linkList');
- if(links.length === 0){
- list.innerHTML = '<div style="text-align:center;padding:30px;color:var(--muted)">暂无链接</div>';
- return;
- }
- list.innerHTML = links.map(l => `
- <div class="link-item" data-id="${l.id}">
- <div class="info">
- <div class="title">${esc(l.title)}</div>
- <div class="url">${esc(l.url)}</div>
- </div>
- <span class="badge">${esc(l.group_name)}</span>
- ${l.is_private ? '<span class="badge private">🔒 私有</span>' : '<span class="badge">公开</span>'}
- <div class="item-actions">
- <button class="btn-sm edit-btn" data-id="${l.id}">编辑</button>
- <button class="btn-sm btn-danger delete-btn" data-id="${l.id}">删除</button>
- </div>
- </div>
- `).join('');
- list.querySelectorAll('.edit-btn').forEach(btn => {
- btn.addEventListener('click', () => openEdit(parseInt(btn.dataset.id)));
- });
- list.querySelectorAll('.delete-btn').forEach(btn => {
- btn.addEventListener('click', () => deleteLink(parseInt(btn.dataset.id)));
- });
- }
- // ===== 筛选 =====
- $('#filterLinks').addEventListener('input', () => {
- const v = $('#filterLinks').value.toLowerCase().trim();
- if(!v){ renderLinks(allLinks); return; }
- const filtered = allLinks.filter(l =>
- (l.title + ' ' + l.url + ' ' + l.group_name + ' ' + (l.keywords || '') + ' ' + (l.description || '')).toLowerCase().includes(v)
- );
- renderLinks(filtered);
- });
- $('#refreshLinks').addEventListener('click', loadLinks);
- // ===== 删除链接 =====
- async function deleteLink(id){
- if(!confirm('确定要删除此链接吗?')) return;
- try {
- const data = await apiPost('delete_link', {id});
- if(data.success){ toast('已删除'); loadLinks(); }
- else { toast(data.error || '删除失败', 'error'); }
- } catch(e){ toast(e.message, 'error'); }
- }
- // ===== 编辑链接 =====
- function openEdit(id){
- const l = allLinks.find(x => x.id === id);
- if(!l) return;
- $('#editId').value = l.id;
- $('#editTitle').value = l.title;
- $('#editUrl').value = l.url;
- $('#editDesc').value = l.description || '';
- $('#editGroup').value = l.group_name;
- $('#editKeywords').value = l.keywords || '';
- $('#editTagLabel').value = l.tag_label || '';
- $('#editTagClass').value = l.tag_class || '';
- $('#editMetaDomain').value = l.meta_domain || '';
- $('#editIconSvg').value = l.icon_svg || '';
- $('#editIsPrivate').checked = !!l.is_private;
- $('#editModal').classList.add('active');
- }
- $('#editCancel').addEventListener('click', () => $('#editModal').classList.remove('active'));
- $('#editModal').addEventListener('click', (e) => { if(e.target === $('#editModal')) $('#editModal').classList.remove('active'); });
- $('#editForm').addEventListener('submit', async (e) => {
- e.preventDefault();
- try {
- const result = await apiPost('update_link', {
- id: $('#editId').value,
- title: $('#editTitle').value,
- url: $('#editUrl').value,
- description: $('#editDesc').value,
- group_name: $('#editGroup').value,
- keywords: $('#editKeywords').value,
- tag_label: $('#editTagLabel').value,
- tag_class: $('#editTagClass').value,
- meta_domain: $('#editMetaDomain').value,
- icon_svg: $('#editIconSvg').value,
- is_private: $('#editIsPrivate').checked ? 1 : 0,
- });
- if(result.success){ toast('已更新'); $('#editModal').classList.remove('active'); loadLinks(); }
- else { toast(result.error || '更新失败', 'error'); }
- } catch(e){ toast(e.message, 'error'); }
- });
- // ===== 添加链接 =====
- $('#addForm').addEventListener('submit', async (e) => {
- e.preventDefault();
- try {
- const result = await apiPost('add_link', {
- title: $('#addTitle').value,
- url: $('#addUrl').value,
- description: $('#addDesc').value,
- group_name: $('#addGroup').value || 'default',
- group_label: $('#addGroupLabel').value,
- keywords: $('#addKeywords').value,
- tag_label: $('#addTagLabel').value,
- tag_class: $('#addTagClass').value,
- meta_domain: $('#addMetaDomain').value,
- icon_svg: $('#addIconSvg').value,
- is_private: $('#addIsPrivate').checked ? 1 : 0,
- });
- if(result.success){
- toast('链接已添加');
- $('#addForm').reset();
- $$('.tab').forEach(t => t.classList.remove('active'));
- $$('.tab-content').forEach(t => t.classList.remove('active'));
- document.querySelector('[data-tab="links"]').classList.add('active');
- document.getElementById('tab-links').classList.add('active');
- loadLinks();
- } else { toast(result.error || '添加失败', 'error'); }
- } catch(e){ toast(e.message, 'error'); }
- });
- // ===== 修改密码 =====
- $('#pwdForm').addEventListener('submit', async (e) => {
- e.preventDefault();
- const oldPwd = $('#oldPwd').value;
- const newPwd = $('#newPwd').value;
- const confirmPwd = $('#confirmPwd').value;
- if(newPwd !== confirmPwd){ toast('两次输入的新密码不一致', 'error'); return; }
- if(newPwd.length < 6){ toast('新密码至少6位', 'error'); return; }
- try {
- const result = await apiPost('update_password', {
- old_password: oldPwd, new_password: newPwd, confirm_password: confirmPwd,
- });
- if(result.success){ toast('密码已更新'); $('#pwdForm').reset(); }
- else { toast(result.error || '密码更新失败', 'error'); }
- } catch(e){ toast(e.message, 'error'); }
- });
- // ===== 启动 =====
- loadLinks();
- })();
- </script>
- </body>
- </html>
|