| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426 |
- <?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><?= htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8') ?> - 管理面板</title>
- <link rel="stylesheet" href="style.css" />
- </head>
- <body class="admin-page">
- <div class="wrap">
- <header>
- <h1><?= htmlspecialchars(APP_NAME, ENT_QUOTES, 'UTF-8') ?> · 管理面板</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="editGroupLabel">分组显示名称</label>
- <input type="text" id="editGroupLabel" />
- </div>
- </div>
- <div class="form-row-3">
- <div class="form-group">
- <label for="editKeywords">关键词</label>
- <input type="text" id="editKeywords" />
- </div>
- <div class="form-group">
- <label for="editGroupHint">分组提示文字</label>
- <input type="text" id="editGroupHint" />
- </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 src="app.js"></script>
- <script>
- (function(){
- 'use strict';
- const $ = (s) => document.querySelector(s);
- const $$ = (s) => [...document.querySelectorAll(s)];
- function getCsrf(){ return $('#csrfToken').value; }
- // ===== 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;
- $('#editGroupLabel').value = l.group_label || '';
- $('#editGroupHint').value = l.group_hint || '';
- $('#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,
- group_label: $('#editGroupLabel').value,
- group_hint: $('#editGroupHint').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>
|