feat: 新增知资札记工具及相关功能
1. 新增知资札记完整站点:包含首页、详情页、管理后台 2. 添加站点图标、鉴权配置与后端API鉴权逻辑 3. 在市场页面新增知资札记入口跳转 4. 新增经典乡村音乐播放列表文件 5. 临时添加养生谚语批量导入脚本
This commit is contained in:
@@ -0,0 +1,245 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
知资札记 - 后台CMS管理样式
|
||||
主色:#162A46 墨蓝 | 删除红:#D94452
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { font-size: 16px; }
|
||||
body {
|
||||
font-family: 'Microsoft YaHei', 'Source Han Sans CN', -apple-system, sans-serif;
|
||||
color: #333; background: #F5F6FA; line-height: 1.6;
|
||||
}
|
||||
a { text-decoration: none; color: inherit; }
|
||||
button { cursor: pointer; font-family: inherit; border: none; outline: none; }
|
||||
input, select { font-family: inherit; outline: none; }
|
||||
|
||||
/* ── 布局 ── */
|
||||
.za-layout { display: flex; min-height: 100vh; }
|
||||
|
||||
/* ── 左侧菜单 ── */
|
||||
.za-sidebar {
|
||||
width: 220px; min-width: 220px; background: #162A46; color: #fff;
|
||||
padding: 20px 0; position: fixed; top: 0; left: 0; bottom: 0;
|
||||
overflow-y: auto; z-index: 100;
|
||||
}
|
||||
.za-sidebar .sb-logo {
|
||||
font-size: 1.1rem; font-weight: 700; letter-spacing: 2px;
|
||||
padding: 0 20px 20px; border-bottom: 1px solid rgba(255,255,255,0.1);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.za-sidebar .sb-menu { list-style: none; }
|
||||
.za-sidebar .sb-menu li a {
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
padding: 10px 20px; color: rgba(255,255,255,0.7);
|
||||
font-size: 0.9rem; transition: all 0.2s;
|
||||
}
|
||||
.za-sidebar .sb-menu li a:hover,
|
||||
.za-sidebar .sb-menu li a.active { color: #fff; background: rgba(255,255,255,0.08); }
|
||||
.za-sidebar .sb-menu .sub-menu { padding-left: 16px; }
|
||||
.za-sidebar .sb-menu .sub-menu a { font-size: 0.82rem; padding: 7px 20px; }
|
||||
|
||||
/* ── 主内容区 ── */
|
||||
.za-main { flex: 1; margin-left: 220px; padding: 20px; }
|
||||
|
||||
/* ── 顶部操作栏 ── */
|
||||
.za-toolbar {
|
||||
background: #fff; border-radius: 8px; padding: 16px;
|
||||
margin-bottom: 16px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 10px;
|
||||
}
|
||||
.za-toolbar .search-box {
|
||||
display: flex; align-items: center; border: 1px solid #E0E4E8;
|
||||
border-radius: 6px; padding: 6px 12px; flex: 1; min-width: 200px;
|
||||
}
|
||||
.za-toolbar .search-box input {
|
||||
border: none; flex: 1; font-size: 0.85rem; background: transparent;
|
||||
}
|
||||
.za-toolbar .search-box button { background: transparent; color: #999; font-size: 0.9rem; }
|
||||
.za-toolbar select {
|
||||
border: 1px solid #E0E4E8; border-radius: 6px;
|
||||
padding: 7px 12px; font-size: 0.85rem; background: #fff;
|
||||
}
|
||||
.za-toolbar .btn {
|
||||
padding: 7px 16px; border-radius: 6px; font-size: 0.85rem;
|
||||
font-weight: 500; transition: all 0.2s;
|
||||
}
|
||||
.za-toolbar .btn-primary { background: #162A46; color: #fff; }
|
||||
.za-toolbar .btn-primary:hover { background: #1E3A5F; }
|
||||
.za-toolbar .btn-danger { background: #D94452; color: #fff; }
|
||||
.za-toolbar .btn-danger:hover { background: #C0392B; }
|
||||
.za-toolbar .btn-outline { background: #fff; color: #162A46; border: 1px solid #162A46; }
|
||||
.za-toolbar .btn-outline:hover { background: #F0F4F8; }
|
||||
.za-toolbar .btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||
|
||||
/* ── 表格 ── */
|
||||
.za-table-wrap {
|
||||
background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.06);
|
||||
overflow: hidden;
|
||||
}
|
||||
.za-table {
|
||||
width: 100%; border-collapse: collapse; font-size: 0.85rem;
|
||||
}
|
||||
.za-table thead { background: #F8FAFB; }
|
||||
.za-table th {
|
||||
padding: 12px 14px; text-align: left; font-weight: 600;
|
||||
color: #666; border-bottom: 2px solid #E0E4E8;
|
||||
white-space: nowrap; font-size: 0.8rem;
|
||||
}
|
||||
.za-table td {
|
||||
padding: 10px 14px; border-bottom: 1px solid #F0F2F5;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.za-table tbody tr:hover { background: #F8FAFB; }
|
||||
.za-table .cell-title {
|
||||
max-width: 240px; white-space: nowrap; overflow: hidden;
|
||||
text-overflow: ellipsis; cursor: default;
|
||||
}
|
||||
.za-table .cell-cover {
|
||||
width: 60px; height: 40px; border-radius: 4px; overflow: hidden;
|
||||
background: #E8EDF2; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.za-table .cell-cover img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.za-table .cell-cover .text-icon {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
background: #162A46; color: #fff; font-size: 0.8rem; font-weight: 700;
|
||||
}
|
||||
.za-table .type-badge {
|
||||
display: inline-block; padding: 2px 8px; border-radius: 10px;
|
||||
font-size: 0.72rem; font-weight: 500;
|
||||
}
|
||||
.za-table .type-badge.text { background: #E8F0FE; color: #1E3A5F; }
|
||||
.za-table .type-badge.image { background: #E8F5F0; color: #467974; }
|
||||
.za-table .status-badge {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 10px;
|
||||
font-size: 0.72rem; font-weight: 500;
|
||||
}
|
||||
.za-table .status-badge.normal { background: #E8F5E9; color: #2E7D32; }
|
||||
.za-table .status-badge.offline { background: #FFF3E0; color: #E65100; }
|
||||
.za-table .actions { display: flex; gap: 6px; }
|
||||
.za-table .actions button {
|
||||
padding: 4px 10px; border-radius: 4px; font-size: 0.75rem;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.za-table .actions .act-preview { background: #E8F0FE; color: #1E3A5F; }
|
||||
.za-table .actions .act-preview:hover { background: #D0E0FC; }
|
||||
.za-table .actions .act-edit { background: #E8F5F0; color: #467974; }
|
||||
.za-table .actions .act-edit:hover { background: #D0EBE0; }
|
||||
.za-table .actions .act-toggle { background: #FFF3E0; color: #E65100; }
|
||||
.za-table .actions .act-toggle:hover { background: #FFE0B2; }
|
||||
.za-table .actions .act-delete { background: #FFEBEE; color: #D94452; }
|
||||
.za-table .actions .act-delete:hover { background: #FFCDD2; }
|
||||
|
||||
/* ── 分页 ── */
|
||||
.za-pagination {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
padding: 12px 16px; background: #fff; border-top: 1px solid #F0F2F5;
|
||||
font-size: 0.85rem; color: #666;
|
||||
}
|
||||
.za-pagination .page-btns { display: flex; gap: 4px; align-items: center; }
|
||||
.za-pagination .page-btns button {
|
||||
padding: 5px 10px; border-radius: 4px; font-size: 0.8rem;
|
||||
background: #fff; border: 1px solid #E0E4E8; color: #333;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.za-pagination .page-btns button:hover:not(:disabled) { background: #F0F4F8; }
|
||||
.za-pagination .page-btns button.active { background: #162A46; color: #fff; border-color: #162A46; }
|
||||
.za-pagination .page-btns button:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||
.za-pagination select { border: 1px solid #E0E4E8; border-radius: 4px; padding: 4px 8px; }
|
||||
|
||||
/* ── 弹窗 ── */
|
||||
.za-modal-overlay {
|
||||
position: fixed; inset: 0; z-index: 5000;
|
||||
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.za-modal-overlay.hidden { display: none; }
|
||||
.za-modal {
|
||||
background: #fff; border-radius: 12px; padding: 24px;
|
||||
max-width: 460px; width: 90%; box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
}
|
||||
.za-modal h3 { margin-bottom: 12px; color: #333; font-size: 1.05rem; }
|
||||
.za-modal .modal-body { font-size: 0.85rem; color: #666; margin-bottom: 20px; line-height: 1.6; }
|
||||
.za-modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.za-modal .btn {
|
||||
padding: 8px 20px; border-radius: 6px; font-size: 0.85rem; font-weight: 500;
|
||||
}
|
||||
.za-modal .btn-primary { background: #162A46; color: #fff; }
|
||||
.za-modal .btn-primary:hover { background: #1E3A5F; }
|
||||
.za-modal .btn-danger { background: #D94452; color: #fff; }
|
||||
.za-modal .btn-danger:hover { background: #C0392B; }
|
||||
.za-modal .btn-cancel { background: #F0F2F5; color: #666; }
|
||||
.za-modal .btn-cancel:hover { background: #E0E4E8; }
|
||||
|
||||
/* ── Toast ── */
|
||||
.za-toast {
|
||||
position: fixed; top: 20px; left: 50%; transform: translateX(-50%);
|
||||
z-index: 9999; padding: 10px 24px; border-radius: 8px;
|
||||
font-size: 0.85rem; color: #fff; opacity: 0;
|
||||
transition: opacity 0.3s; pointer-events: none;
|
||||
}
|
||||
.za-toast.show { opacity: 1; }
|
||||
.za-toast.info { background: #3B82F6; }
|
||||
.za-toast.success { background: #10B981; }
|
||||
.za-toast.error { background: #EF4444; }
|
||||
|
||||
/* ── 新增/编辑表单弹窗 ── */
|
||||
.za-modal.form-modal { max-width: 700px; max-height: 85vh; overflow-y: auto; }
|
||||
.za-modal .form-group { margin-bottom: 14px; }
|
||||
.za-modal .form-group label {
|
||||
display: block; font-size: 0.82rem; font-weight: 600;
|
||||
color: #555; margin-bottom: 4px;
|
||||
}
|
||||
.za-modal .form-group input,
|
||||
.za-modal .form-group select,
|
||||
.za-modal .form-group textarea {
|
||||
width: 100%; padding: 8px 12px; border: 1px solid #E0E4E8;
|
||||
border-radius: 6px; font-size: 0.85rem; transition: border-color 0.2s;
|
||||
}
|
||||
.za-modal .form-group input:focus,
|
||||
.za-modal .form-group textarea:focus,
|
||||
.za-modal .form-group select:focus { border-color: #162A46; }
|
||||
.za-modal .form-group textarea { min-height: 200px; resize: vertical; font-family: inherit; }
|
||||
.za-modal .form-group .tag-input {
|
||||
display: flex; flex-wrap: wrap; gap: 4px; align-items: center;
|
||||
border: 1px solid #E0E4E8; border-radius: 6px; padding: 6px;
|
||||
min-height: 38px;
|
||||
}
|
||||
.za-modal .form-group .tag-input .tag-chip {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 2px 8px; border-radius: 10px; font-size: 0.75rem;
|
||||
background: #E8F0FE; color: #1E3A5F;
|
||||
}
|
||||
.za-modal .form-group .tag-input .tag-chip .remove-tag {
|
||||
background: transparent; color: #1E3A5F; font-size: 0.8rem; padding: 0; cursor: pointer;
|
||||
}
|
||||
.za-modal .form-group .tag-input input {
|
||||
border: none; flex: 1; min-width: 80px; padding: 4px; font-size: 0.82rem;
|
||||
}
|
||||
|
||||
/* ── 图片上传预览 ── */
|
||||
.za-modal .form-group .upload-area { margin-top: 4px; }
|
||||
.za-modal .form-group .upload-preview {
|
||||
display: flex; flex-wrap: wrap; gap: 8px; margin-top: 10px;
|
||||
}
|
||||
.za-modal .form-group .preview-item {
|
||||
position: relative; width: 80px; height: 80px;
|
||||
border-radius: 6px; overflow: hidden; border: 1px solid #E0E4E8;
|
||||
}
|
||||
.za-modal .form-group .preview-item img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
.za-modal .form-group .preview-item .preview-remove {
|
||||
position: absolute; top: 2px; right: 2px;
|
||||
width: 18px; height: 18px; border-radius: 50%;
|
||||
background: rgba(0,0,0,0.5); color: #fff;
|
||||
font-size: 0.7rem; display: flex; align-items: center; justify-content: center;
|
||||
padding: 0; line-height: 1;
|
||||
}
|
||||
|
||||
/* ── 响应式 ── */
|
||||
@media (max-width: 900px) {
|
||||
.za-sidebar { display: none; }
|
||||
.za-main { margin-left: 0; }
|
||||
.za-table { font-size: 0.78rem; }
|
||||
.za-table th, .za-table td { padding: 8px 8px; }
|
||||
.za-table .cell-title { max-width: 120px; }
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>知资札记 · 后台管理</title>
|
||||
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/zhizi_notes/admin.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Toast -->
|
||||
<div id="za-toast" class="za-toast"></div>
|
||||
|
||||
<!-- ═══════════════ 布局 ═══════════════ -->
|
||||
<div class="za-layout">
|
||||
<!-- 左侧菜单 -->
|
||||
<aside class="za-sidebar">
|
||||
<div class="sb-logo">知资札记</div>
|
||||
<ul class="sb-menu">
|
||||
<li><a href="#">📊 数据仪表盘</a></li>
|
||||
<li><a href="#" class="active">📄 图文内容管理</a>
|
||||
<ul class="sub-menu">
|
||||
<li><a href="#" class="active">全部存档列表</a></li>
|
||||
<li><a href="#" id="za-menu-new">+ 新增存档</a></li>
|
||||
<li><a href="#">🖼 图片资源库</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a href="#">🏷 分类标签管理</a></li>
|
||||
<li><a href="#">👤 用户管理</a></li>
|
||||
<li><a href="#">🕷 爬虫任务日志</a></li>
|
||||
<li><a href="#">⚙ 系统设置</a></li>
|
||||
</ul>
|
||||
</aside>
|
||||
|
||||
<!-- 主内容区 -->
|
||||
<main class="za-main">
|
||||
<!-- 顶部操作栏 -->
|
||||
<div class="za-toolbar">
|
||||
<div class="search-box">
|
||||
<input id="za-search" type="text" placeholder="搜索标题/标签…" />
|
||||
<button id="za-search-btn">🔍</button>
|
||||
</div>
|
||||
<select id="za-filter-category">
|
||||
<option value="">全部分类</option>
|
||||
</select>
|
||||
<select id="za-filter-type">
|
||||
<option value="">全部类型</option>
|
||||
<option value="text">纯文字</option>
|
||||
<option value="image">图文</option>
|
||||
</select>
|
||||
<select id="za-filter-status">
|
||||
<option value="">全部状态</option>
|
||||
<option value="normal">正常</option>
|
||||
<option value="offline">已下架</option>
|
||||
</select>
|
||||
<button class="btn btn-outline" id="za-btn-batch-offline" disabled>批量下架</button>
|
||||
<button class="btn btn-danger" id="za-btn-batch-delete" disabled>批量删除</button>
|
||||
<button class="btn btn-outline" id="za-btn-batch-export" disabled>批量导出</button>
|
||||
<button class="btn btn-primary" id="za-btn-new">+ 新增存档</button>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="za-table-wrap">
|
||||
<table class="za-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width:40px"><input type="checkbox" id="za-select-all" title="全选" /></th>
|
||||
<th style="width:70px">封面</th>
|
||||
<th>标题</th>
|
||||
<th style="width:90px">分类</th>
|
||||
<th style="width:90px">类型</th>
|
||||
<th style="width:100px">来源</th>
|
||||
<th style="width:130px">创建时间</th>
|
||||
<th style="width:60px">阅读</th>
|
||||
<th style="width:70px">状态</th>
|
||||
<th style="width:200px">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="za-tbody"></tbody>
|
||||
</table>
|
||||
|
||||
<!-- 分页 -->
|
||||
<div class="za-pagination" id="za-pagination">
|
||||
<div>
|
||||
<span id="za-page-info">共 0 条</span>
|
||||
<span style="margin-left:12px">每页</span>
|
||||
<select id="za-page-size">
|
||||
<option value="10">10</option>
|
||||
<option value="20" selected>20</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
<span>条</span>
|
||||
</div>
|
||||
<div class="page-btns" id="za-page-btns"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/tools/zhizi_notes/admin.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,696 @@
|
||||
/**
|
||||
* 知资札记 - 后台CMS管理逻辑
|
||||
* 图文内容管理列表页面
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API_BASE = '/api/zhizi_notes';
|
||||
const $ = function (sel) { return document.querySelector(sel); };
|
||||
const $$ = function (sel) { return document.querySelectorAll(sel); };
|
||||
|
||||
// ── 状态 ──
|
||||
var adminState = {
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
totalPages: 0,
|
||||
total: 0,
|
||||
filter: { search: '', category: '', contentType: '', status: '' },
|
||||
selectedIds: new Set(),
|
||||
editingId: null
|
||||
};
|
||||
|
||||
// ── Toast ──
|
||||
function showToast(msg, type) {
|
||||
type = type || 'info';
|
||||
var t = $('#za-toast');
|
||||
if (!t) return;
|
||||
t.textContent = msg;
|
||||
t.className = 'za-toast ' + type + ' show';
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(function () { t.classList.remove('show'); }, type === 'error' ? 4000 : 2000);
|
||||
}
|
||||
|
||||
// ── 链接清洗 ──
|
||||
function clearAllLinks(html) {
|
||||
if (!html || typeof html !== 'string') return '';
|
||||
return html.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '$1');
|
||||
}
|
||||
|
||||
// ── API ──
|
||||
async function apiGet(path) {
|
||||
try {
|
||||
var res = await fetch(API_BASE + path, { credentials: 'same-origin' });
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
async function apiPost(path, body) {
|
||||
try {
|
||||
var res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
async function apiPut(path, body) {
|
||||
try {
|
||||
var res = await fetch(API_BASE + path, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
async function apiDelete(path) {
|
||||
try {
|
||||
var res = await fetch(API_BASE + path, { method: 'DELETE', credentials: 'same-origin' });
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
// ── 加载列表 ──
|
||||
async function loadTableData() {
|
||||
var params = new URLSearchParams();
|
||||
params.set('page', adminState.page);
|
||||
params.set('pageSize', adminState.pageSize);
|
||||
if (adminState.filter.search) params.set('search', adminState.filter.search);
|
||||
if (adminState.filter.category) params.set('category', adminState.filter.category);
|
||||
if (adminState.filter.contentType) params.set('contentType', adminState.filter.contentType);
|
||||
if (adminState.filter.status) params.set('status', adminState.filter.status);
|
||||
|
||||
var data = await apiGet('/articles?' + params.toString());
|
||||
if (!data.ok) { showToast('加载失败:' + (data.error || '未知错误'), 'error'); return; }
|
||||
|
||||
adminState.totalPages = data.pagination ? data.pagination.totalPages : 0;
|
||||
adminState.total = data.pagination ? data.pagination.total : 0;
|
||||
adminState.selectedIds.clear();
|
||||
renderTable(data.data || []);
|
||||
renderPagination();
|
||||
}
|
||||
|
||||
// ── 渲染表格 ──
|
||||
function renderTable(articles) {
|
||||
var tbody = $('#za-tbody');
|
||||
if (!tbody) return;
|
||||
tbody.innerHTML = '';
|
||||
|
||||
if (articles.length === 0) {
|
||||
tbody.innerHTML = '<tr><td colspan="10" style="text-align:center;padding:40px;color:#999">暂无数据</td></tr>';
|
||||
return;
|
||||
}
|
||||
|
||||
articles.forEach(function (a) {
|
||||
var tr = document.createElement('tr');
|
||||
|
||||
// 复选框
|
||||
var tdCb = document.createElement('td');
|
||||
var cb = document.createElement('input');
|
||||
cb.type = 'checkbox';
|
||||
cb.setAttribute('data-id', a.id);
|
||||
cb.addEventListener('change', function () { toggleSelect(a.id, cb.checked); });
|
||||
tdCb.appendChild(cb);
|
||||
|
||||
// 封面
|
||||
var tdCover = document.createElement('td');
|
||||
var coverDiv = document.createElement('div');
|
||||
coverDiv.className = 'cell-cover';
|
||||
if (a.cover_url) {
|
||||
var img = document.createElement('img');
|
||||
img.src = a.cover_url; img.alt = '';
|
||||
coverDiv.appendChild(img);
|
||||
} else {
|
||||
coverDiv.innerHTML = '<div class="text-icon">文</div>';
|
||||
}
|
||||
tdCover.appendChild(coverDiv);
|
||||
|
||||
// 标题
|
||||
var tdTitle = document.createElement('td');
|
||||
var titleDiv = document.createElement('div');
|
||||
titleDiv.className = 'cell-title';
|
||||
titleDiv.textContent = a.title;
|
||||
titleDiv.title = a.title;
|
||||
tdTitle.appendChild(titleDiv);
|
||||
|
||||
// 分类
|
||||
var tdCat = document.createElement('td');
|
||||
tdCat.textContent = a.category;
|
||||
|
||||
// 内容类型
|
||||
var tdType = document.createElement('td');
|
||||
var typeSpan = document.createElement('span');
|
||||
typeSpan.className = 'type-badge ' + (a.content_type === 'text' ? 'text' : 'image');
|
||||
var imgCount = a.image_count || 0;
|
||||
typeSpan.textContent = a.content_type === 'text' ? '纯文字' : '图文(' + imgCount + '张)';
|
||||
tdType.appendChild(typeSpan);
|
||||
|
||||
// 来源
|
||||
var tdSrc = document.createElement('td');
|
||||
tdSrc.textContent = a.source || '-';
|
||||
tdSrc.style.maxWidth = '100px';
|
||||
tdSrc.style.overflow = 'hidden';
|
||||
tdSrc.style.textOverflow = 'ellipsis';
|
||||
tdSrc.style.whiteSpace = 'nowrap';
|
||||
|
||||
// 创建时间
|
||||
var tdTime = document.createElement('td');
|
||||
tdTime.textContent = (a.created_at || '').slice(0, 16);
|
||||
tdTime.style.whiteSpace = 'nowrap';
|
||||
|
||||
// 阅读量
|
||||
var tdViews = document.createElement('td');
|
||||
tdViews.textContent = a.views || 0;
|
||||
|
||||
// 状态
|
||||
var tdStatus = document.createElement('td');
|
||||
var statusSpan = document.createElement('span');
|
||||
statusSpan.className = 'status-badge ' + (a.status === 'normal' ? 'normal' : 'offline');
|
||||
statusSpan.textContent = a.status === 'normal' ? '正常' : '已下架';
|
||||
tdStatus.appendChild(statusSpan);
|
||||
|
||||
// 操作
|
||||
var tdActions = document.createElement('td');
|
||||
tdActions.className = 'actions';
|
||||
|
||||
var btnPreview = document.createElement('button');
|
||||
btnPreview.className = 'act-preview';
|
||||
btnPreview.textContent = '预览';
|
||||
btnPreview.addEventListener('click', function () { window.open('/tools/zhizi_notes/detail.html?id=' + a.id, '_blank'); });
|
||||
|
||||
var btnEdit = document.createElement('button');
|
||||
btnEdit.className = 'act-edit';
|
||||
btnEdit.textContent = '编辑';
|
||||
btnEdit.addEventListener('click', function () { openEditForm(a.id); });
|
||||
|
||||
var btnToggle = document.createElement('button');
|
||||
btnToggle.className = 'act-toggle';
|
||||
btnToggle.textContent = a.status === 'normal' ? '下架' : '上架';
|
||||
btnToggle.addEventListener('click', function () { toggleStatus(a.id, a.status); });
|
||||
|
||||
var btnDel = document.createElement('button');
|
||||
btnDel.className = 'act-delete';
|
||||
btnDel.textContent = '删除';
|
||||
btnDel.addEventListener('click', function () { confirmDelete(a.id, a.title); });
|
||||
|
||||
tdActions.appendChild(btnPreview);
|
||||
tdActions.appendChild(btnEdit);
|
||||
tdActions.appendChild(btnToggle);
|
||||
tdActions.appendChild(btnDel);
|
||||
|
||||
tr.appendChild(tdCb);
|
||||
tr.appendChild(tdCover);
|
||||
tr.appendChild(tdTitle);
|
||||
tr.appendChild(tdCat);
|
||||
tr.appendChild(tdType);
|
||||
tr.appendChild(tdSrc);
|
||||
tr.appendChild(tdTime);
|
||||
tr.appendChild(tdViews);
|
||||
tr.appendChild(tdStatus);
|
||||
tr.appendChild(tdActions);
|
||||
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 分页 ──
|
||||
function renderPagination() {
|
||||
var container = $('#za-pagination');
|
||||
if (!container) return;
|
||||
var info = $('#za-page-info');
|
||||
if (info) info.textContent = '共 ' + adminState.total + ' 条,第 ' + adminState.page + '/' + adminState.totalPages + ' 页';
|
||||
|
||||
var btns = $('#za-page-btns');
|
||||
if (!btns) return;
|
||||
btns.innerHTML = '';
|
||||
|
||||
var total = adminState.totalPages;
|
||||
var cur = adminState.page;
|
||||
|
||||
var prevBtn = document.createElement('button');
|
||||
prevBtn.textContent = '上一页';
|
||||
prevBtn.disabled = cur <= 1;
|
||||
prevBtn.addEventListener('click', function () { if (cur > 1) { adminState.page--; loadTableData(); } });
|
||||
btns.appendChild(prevBtn);
|
||||
|
||||
var start = Math.max(1, cur - 2);
|
||||
var end = Math.min(total, cur + 2);
|
||||
for (var i = start; i <= end; i++) {
|
||||
var pageBtn = document.createElement('button');
|
||||
pageBtn.textContent = i;
|
||||
if (i === cur) pageBtn.classList.add('active');
|
||||
pageBtn.addEventListener('click', function (p) {
|
||||
return function () { adminState.page = p; loadTableData(); };
|
||||
}(i));
|
||||
btns.appendChild(pageBtn);
|
||||
}
|
||||
|
||||
var nextBtn = document.createElement('button');
|
||||
nextBtn.textContent = '下一页';
|
||||
nextBtn.disabled = cur >= total;
|
||||
nextBtn.addEventListener('click', function () { if (cur < total) { adminState.page++; loadTableData(); } });
|
||||
btns.appendChild(nextBtn);
|
||||
}
|
||||
|
||||
// ── 选择行 ──
|
||||
function toggleSelect(id, checked) {
|
||||
if (checked) adminState.selectedIds.add(id);
|
||||
else adminState.selectedIds.delete(id);
|
||||
updateBatchBtns();
|
||||
}
|
||||
|
||||
function toggleSelectAll(checked) {
|
||||
$$('#za-tbody input[type="checkbox"]').forEach(function (cb) {
|
||||
cb.checked = checked;
|
||||
var id = parseInt(cb.getAttribute('data-id'));
|
||||
if (checked) adminState.selectedIds.add(id);
|
||||
else adminState.selectedIds.delete(id);
|
||||
});
|
||||
updateBatchBtns();
|
||||
}
|
||||
|
||||
function updateBatchBtns() {
|
||||
var hasSelection = adminState.selectedIds.size > 0;
|
||||
var btns = $$('.za-toolbar .btn:disabled, .za-toolbar .btn:not(:disabled)');
|
||||
$$('#za-btn-batch-offline, #za-btn-batch-delete, #za-btn-batch-export').forEach(function (btn) {
|
||||
btn.disabled = !hasSelection;
|
||||
});
|
||||
}
|
||||
|
||||
// ── 批量操作 ──
|
||||
async function batchAction(action) {
|
||||
if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; }
|
||||
var ids = Array.from(adminState.selectedIds);
|
||||
var labels = { delete: '删除', offline: '下架', online: '上架' };
|
||||
var label = labels[action] || action;
|
||||
|
||||
if (action === 'delete') {
|
||||
showConfirm('确认批量' + label, '确定要批量' + label + ' ' + ids.length + ' 篇文章吗?此操作不可恢复。', async function () {
|
||||
var data = await apiPost('/articles/batch', { ids: ids, action: action });
|
||||
if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); }
|
||||
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
|
||||
});
|
||||
} else {
|
||||
var data = await apiPost('/articles/batch', { ids: ids, action: action });
|
||||
if (data.ok) { showToast('批量' + label + '成功', 'success'); loadTableData(); }
|
||||
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 状态切换 ──
|
||||
async function toggleStatus(id, currentStatus) {
|
||||
var newStatus = currentStatus === 'normal' ? 'offline' : 'normal';
|
||||
var label = newStatus === 'normal' ? '上架' : '下架';
|
||||
var data = await apiPut('/articles/' + id, { status: newStatus });
|
||||
if (data.ok) { showToast(label + '成功', 'success'); loadTableData(); }
|
||||
else showToast('操作失败:' + (data.error || '未知错误'), 'error');
|
||||
}
|
||||
|
||||
// ── 删除确认 ──
|
||||
function confirmDelete(id, title) {
|
||||
showConfirm('确认删除', '确定要永久删除文章「' + title + '」吗?关联图片也会一并删除,此操作不可恢复。', async function () {
|
||||
var data = await apiDelete('/articles/' + id);
|
||||
if (data.ok) { showToast('删除成功', 'success'); loadTableData(); }
|
||||
else showToast('删除失败:' + (data.error || '未知错误'), 'error');
|
||||
});
|
||||
}
|
||||
|
||||
// ── 确认弹窗 ──
|
||||
function showConfirm(title, message, onConfirm) {
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'za-modal-overlay';
|
||||
overlay.innerHTML = '<div class="za-modal">' +
|
||||
'<h3>' + escapeHtml(title) + '</h3>' +
|
||||
'<div class="modal-body">' + escapeHtml(message) + '</div>' +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-cancel" id="za-confirm-cancel">取消</button>' +
|
||||
'<button class="btn btn-danger" id="za-confirm-ok">确认删除</button>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
overlay.querySelector('#za-confirm-cancel').addEventListener('click', function () { overlay.remove(); });
|
||||
overlay.querySelector('#za-confirm-ok').addEventListener('click', function () {
|
||||
overlay.remove();
|
||||
if (onConfirm) onConfirm();
|
||||
});
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
|
||||
}
|
||||
|
||||
// ── 上传文件 ──
|
||||
async function uploadFiles(files, articleId) {
|
||||
var formData = new FormData();
|
||||
formData.append('articleId', articleId || 0);
|
||||
for (var i = 0; i < files.length; i++) {
|
||||
formData.append('images', files[i]);
|
||||
}
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/upload', { method: 'POST', credentials: 'same-origin', body: formData });
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
// ── 新增/编辑表单 ──
|
||||
async function openEditForm(id) {
|
||||
adminState.editingId = id;
|
||||
var article = null;
|
||||
if (id) {
|
||||
var data = await apiGet('/articles/' + id);
|
||||
if (data.ok) article = data.data;
|
||||
else { showToast('加载文章失败', 'error'); return; }
|
||||
}
|
||||
|
||||
var overlay = document.createElement('div');
|
||||
overlay.className = 'za-modal-overlay';
|
||||
overlay.innerHTML = '<div class="za-modal form-modal">' +
|
||||
'<h3>' + (id ? '编辑存档' : '新增存档') + '</h3>' +
|
||||
'<div class="form-group"><label>标题</label><input id="za-form-title" value="' + escapeHtml(article ? article.title : '') + '" placeholder="输入文章标题" /></div>' +
|
||||
'<div class="form-group"><label>分类</label><select id="za-form-category"><option value="">选择分类</option></select></div>' +
|
||||
'<div class="form-group"><label>来源备注</label><input id="za-form-source" value="' + escapeHtml(article ? article.source : '') + '" placeholder="来源信息(纯文字)" /></div>' +
|
||||
'<div class="form-group"><label>标签</label><div class="tag-input" id="za-form-tags"><input id="za-tag-input" placeholder="输入标签后回车" /></div></div>' +
|
||||
'<div class="form-group"><label>正文</label><textarea id="za-form-content" placeholder="粘贴正文内容(自动清除外链)">' + escapeHtml(article ? article.content : '') + '</textarea></div>' +
|
||||
'<div class="form-group"><label>内容类型</label><select id="za-form-contentType"><option value="text">纯文字</option><option value="image">图文</option></select></div>' +
|
||||
'<div class="form-group"><label>置顶</label><select id="za-form-pinned"><option value="0">否</option><option value="1">是</option></select></div>' +
|
||||
'<div class="form-group"><label>图片上传</label>' +
|
||||
'<div class="upload-area" id="za-upload-area">' +
|
||||
'<input type="file" id="za-file-input" accept="image/*" multiple style="display:none" />' +
|
||||
'<button type="button" class="btn btn-outline" id="za-upload-btn">选择图片(可多选)</button>' +
|
||||
'<div id="za-upload-preview" class="upload-preview"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="modal-actions">' +
|
||||
'<button class="btn btn-cancel" id="za-form-cancel">取消</button>' +
|
||||
'<button class="btn btn-primary" id="za-form-save">保存</button>' +
|
||||
'</div></div>';
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// 填充分类
|
||||
var cats = await apiGet('/categories');
|
||||
var catSelect = overlay.querySelector('#za-form-category');
|
||||
if (cats.ok && cats.data) {
|
||||
cats.data.forEach(function (c) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = c.name; opt.textContent = c.name;
|
||||
if (article && article.category === c.name) opt.selected = true;
|
||||
catSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
|
||||
if (article) {
|
||||
overlay.querySelector('#za-form-contentType').value = article.content_type || 'text';
|
||||
overlay.querySelector('#za-form-pinned').value = article.is_pinned ? '1' : '0';
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
var tagContainer = overlay.querySelector('#za-form-tags');
|
||||
var tagInput = overlay.querySelector('#za-tag-input');
|
||||
var currentTags = [];
|
||||
try { if (article && article.tags_json) currentTags = JSON.parse(article.tags_json); } catch (e) {}
|
||||
|
||||
function renderTagChips() {
|
||||
var existing = tagContainer.querySelectorAll('.tag-chip');
|
||||
existing.forEach(function (el) { el.remove(); });
|
||||
currentTags.forEach(function (t, i) {
|
||||
var chip = document.createElement('span');
|
||||
chip.className = 'tag-chip';
|
||||
chip.innerHTML = t + '<button class="remove-tag" data-idx="' + i + '">×</button>';
|
||||
chip.querySelector('.remove-tag').addEventListener('click', function () {
|
||||
currentTags.splice(i, 1);
|
||||
renderTagChips();
|
||||
});
|
||||
tagContainer.insertBefore(chip, tagInput);
|
||||
});
|
||||
}
|
||||
renderTagChips();
|
||||
tagInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
var val = tagInput.value.trim();
|
||||
if (val && currentTags.indexOf(val) === -1) {
|
||||
currentTags.push(val);
|
||||
renderTagChips();
|
||||
}
|
||||
tagInput.value = '';
|
||||
}
|
||||
});
|
||||
|
||||
// ── 图片上传逻辑 ──
|
||||
var selectedFiles = [];
|
||||
var removedImageIds = []; // 编辑时删除的已有图片ID
|
||||
var uploadPreview = overlay.querySelector('#za-upload-preview');
|
||||
var fileInput = overlay.querySelector('#za-file-input');
|
||||
var uploadBtn = overlay.querySelector('#za-upload-btn');
|
||||
|
||||
// 编辑时加载已有图片
|
||||
if (article && article.images) {
|
||||
article.images.forEach(function (img) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'preview-item';
|
||||
item.innerHTML = '<img src="/uploads/zhizi_notes/' + img.filename + '" alt="" />' +
|
||||
'<button class="preview-remove" data-filename="' + img.filename + '" data-id="' + (img.id || '') + '">×</button>';
|
||||
uploadPreview.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
// 为所有已有图片的删除按钮绑定事件
|
||||
uploadPreview.querySelectorAll('.preview-remove').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var id = btn.getAttribute('data-id');
|
||||
if (id) removedImageIds.push(parseInt(id));
|
||||
btn.closest('.preview-item').remove();
|
||||
});
|
||||
});
|
||||
|
||||
uploadBtn.addEventListener('click', function () { fileInput.click(); });
|
||||
fileInput.addEventListener('change', function () {
|
||||
if (!fileInput.files) return;
|
||||
Array.from(fileInput.files).forEach(function (file, idx) {
|
||||
if (!file.type.startsWith('image/')) return;
|
||||
selectedFiles.push(file);
|
||||
var reader = new FileReader();
|
||||
reader.onload = (function (i) {
|
||||
return function (e) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'preview-item';
|
||||
item.innerHTML = '<img src="' + e.target.result + '" alt="" />' +
|
||||
'<button class="preview-remove">×</button>';
|
||||
item.querySelector('.preview-remove').addEventListener('click', function () {
|
||||
selectedFiles[i] = null;
|
||||
item.remove();
|
||||
});
|
||||
uploadPreview.appendChild(item);
|
||||
};
|
||||
})(idx);
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
fileInput.value = '';
|
||||
});
|
||||
|
||||
overlay.querySelector('#za-form-cancel').addEventListener('click', function () { overlay.remove(); });
|
||||
overlay.querySelector('#za-form-save').addEventListener('click', async function () {
|
||||
var saveBtn = overlay.querySelector('#za-form-save');
|
||||
saveBtn.disabled = true;
|
||||
saveBtn.textContent = '保存中…';
|
||||
|
||||
var title = overlay.querySelector('#za-form-title').value.trim();
|
||||
if (!title) { showToast('标题不能为空', 'error'); saveBtn.disabled = false; saveBtn.textContent = '保存'; return; }
|
||||
|
||||
// 先保存文章获取 ID
|
||||
var rawContent = overlay.querySelector('#za-form-content').value;
|
||||
var content = clearAllLinks(rawContent);
|
||||
var body = {
|
||||
title: title,
|
||||
content: content,
|
||||
category: overlay.querySelector('#za-form-category').value || '未分类',
|
||||
source: overlay.querySelector('#za-form-source').value.trim(),
|
||||
tags: currentTags,
|
||||
contentType: overlay.querySelector('#za-form-contentType').value,
|
||||
isPinned: overlay.querySelector('#za-form-pinned').value === '1'
|
||||
};
|
||||
|
||||
var result;
|
||||
if (id) {
|
||||
result = await apiPut('/articles/' + id, body);
|
||||
result.data = { id: id };
|
||||
} else {
|
||||
result = await apiPost('/articles', body);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
showToast('保存失败:' + (result.error || '未知错误'), 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
return;
|
||||
}
|
||||
|
||||
var savedId = result.data ? result.data.id : id;
|
||||
if (!savedId) savedId = id;
|
||||
|
||||
// 编辑时删除已被移除的图片
|
||||
if (id && removedImageIds.length > 0) {
|
||||
for (var k = 0; k < removedImageIds.length; k++) {
|
||||
await apiDelete('/images/' + removedImageIds[k]);
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤已删除的文件(标记为 null)
|
||||
var filesToUpload = selectedFiles.filter(function (f) { return f !== null; });
|
||||
|
||||
// 上传图片
|
||||
if (filesToUpload.length > 0 && savedId) {
|
||||
saveBtn.textContent = '上传图片中…';
|
||||
var uploadResult = await uploadFiles(filesToUpload, savedId);
|
||||
if (!uploadResult.ok) {
|
||||
showToast('图片上传失败:' + (uploadResult.error || '未知错误'), 'error');
|
||||
saveBtn.disabled = false;
|
||||
saveBtn.textContent = '保存';
|
||||
return;
|
||||
}
|
||||
// 更新封面为新上传的第一张图
|
||||
if (uploadResult.data && uploadResult.data.length > 0) {
|
||||
body.coverUrl = uploadResult.data[0].url;
|
||||
await apiPut('/articles/' + savedId, body);
|
||||
}
|
||||
}
|
||||
|
||||
showToast(id ? '更新成功' : '创建成功', 'success');
|
||||
overlay.remove();
|
||||
loadTableData();
|
||||
});
|
||||
|
||||
overlay.addEventListener('click', function (e) { if (e.target === overlay) overlay.remove(); });
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── 导出 ──
|
||||
async function exportSelected() {
|
||||
if (adminState.selectedIds.size === 0) { showToast('请先选择文章', 'info'); return; }
|
||||
var ids = Array.from(adminState.selectedIds);
|
||||
try {
|
||||
var res = await fetch(API_BASE + '/export', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify({ ids: ids })
|
||||
});
|
||||
if (res.ok) {
|
||||
var blob = await res.blob();
|
||||
var url = URL.createObjectURL(blob);
|
||||
var a = document.createElement('a');
|
||||
a.href = url; a.download = '知资札记_导出.txt'; a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
showToast('导出成功', 'success');
|
||||
} else {
|
||||
showToast('导出失败', 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('导出失败:' + e.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 初始化 ──
|
||||
function init() {
|
||||
// 全选
|
||||
var selectAllCb = $('#za-select-all');
|
||||
if (selectAllCb) {
|
||||
selectAllCb.addEventListener('change', function () { toggleSelectAll(selectAllCb.checked); });
|
||||
}
|
||||
|
||||
// 搜索
|
||||
var searchInput = $('#za-search');
|
||||
var searchBtn = $('#za-search-btn');
|
||||
if (searchBtn && searchInput) {
|
||||
searchBtn.addEventListener('click', function () {
|
||||
adminState.filter.search = searchInput.value.trim();
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
});
|
||||
searchInput.addEventListener('keydown', function (e) {
|
||||
if (e.key === 'Enter') {
|
||||
adminState.filter.search = searchInput.value.trim();
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 分类筛选
|
||||
var catFilter = $('#za-filter-category');
|
||||
if (catFilter) {
|
||||
catFilter.addEventListener('change', function () {
|
||||
adminState.filter.category = catFilter.value;
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
});
|
||||
}
|
||||
|
||||
// 内容类型筛选
|
||||
var typeFilter = $('#za-filter-type');
|
||||
if (typeFilter) {
|
||||
typeFilter.addEventListener('change', function () {
|
||||
adminState.filter.contentType = typeFilter.value;
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
});
|
||||
}
|
||||
|
||||
// 状态筛选
|
||||
var statusFilter = $('#za-filter-status');
|
||||
if (statusFilter) {
|
||||
statusFilter.addEventListener('change', function () {
|
||||
adminState.filter.status = statusFilter.value;
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
});
|
||||
}
|
||||
|
||||
// 每页条数
|
||||
var pageSizeSel = $('#za-page-size');
|
||||
if (pageSizeSel) {
|
||||
pageSizeSel.addEventListener('change', function () {
|
||||
adminState.pageSize = parseInt(pageSizeSel.value) || 20;
|
||||
adminState.page = 1;
|
||||
loadTableData();
|
||||
});
|
||||
}
|
||||
|
||||
// 批量操作按钮
|
||||
var btnBatchOffline = $('#za-btn-batch-offline');
|
||||
var btnBatchDelete = $('#za-btn-batch-delete');
|
||||
var btnBatchExport = $('#za-btn-batch-export');
|
||||
if (btnBatchOffline) btnBatchOffline.addEventListener('click', function () { batchAction('offline'); });
|
||||
if (btnBatchDelete) btnBatchDelete.addEventListener('click', function () { batchAction('delete'); });
|
||||
if (btnBatchExport) btnBatchExport.addEventListener('click', function () { exportSelected(); });
|
||||
|
||||
// 新增按钮
|
||||
var btnNew = $('#za-btn-new');
|
||||
if (btnNew) btnNew.addEventListener('click', function () { openEditForm(null); });
|
||||
|
||||
// 加载分类选项
|
||||
loadFilterOptions();
|
||||
loadTableData();
|
||||
}
|
||||
|
||||
async function loadFilterOptions() {
|
||||
var cats = await apiGet('/categories');
|
||||
var catFilter = $('#za-filter-category');
|
||||
if (catFilter && cats.ok && cats.data) {
|
||||
cats.data.forEach(function (c) {
|
||||
var opt = document.createElement('option');
|
||||
opt.value = c.name; opt.textContent = c.name;
|
||||
catFilter.appendChild(opt);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"_comment": "知资札记工具的前端鉴权配置",
|
||||
"enable_auth_comment": "true:启用鉴权;false:暂时对所有访问者开放",
|
||||
"enable_auth": true,
|
||||
"max_age_days_comment": "鉴权Cookie有效期(天),范围建议1~365",
|
||||
"max_age_days": 30,
|
||||
"cookieName_comment": "鉴权Cookie名称",
|
||||
"cookieName": "zhizi_notes_gate",
|
||||
"guidParam_comment": "URL中的GUID参数名",
|
||||
"guidParam": "guid",
|
||||
"guidValue_comment": "GUID参数值(留空表示不启用GUID)",
|
||||
"guidValue": "b1c4ejKa0TAD7MQSIEBf94RMOG6SJ2-nUTJNUEiw",
|
||||
"requireGuid_comment": "true:强制需要GUID参数才能访问;false:允许仅凭token或已有会话访问",
|
||||
"requireGuid": false
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/* ═══════════════════════════════════════════════════════════
|
||||
知资札记 - 文章详情页样式
|
||||
复用 zhizi_notes.css 变量体系
|
||||
═══════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── 布局 ── */
|
||||
.zd-layout {
|
||||
margin-top: 56px; min-height: calc(100vh - 56px);
|
||||
max-width: 900px; margin-left: auto; margin-right: auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
.zd-main { padding: 30px 0 60px; }
|
||||
|
||||
/* ── 文章头部 ── */
|
||||
.zd-header { margin-bottom: 24px; padding-bottom: 20px; border-bottom: 1px solid var(--border); }
|
||||
.zd-header h1 {
|
||||
font-size: 1.6rem; font-weight: 700; color: var(--text);
|
||||
line-height: 1.5; margin-bottom: 12px;
|
||||
}
|
||||
.zd-meta {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 12px;
|
||||
font-size: 0.82rem; color: var(--text-light);
|
||||
}
|
||||
.zd-meta .zd-cat {
|
||||
display: inline-block; padding: 2px 10px; border-radius: 10px;
|
||||
background: var(--primary); color: #fff; font-size: 0.75rem;
|
||||
}
|
||||
.zd-meta .fav-btn {
|
||||
background: transparent; color: var(--text-light); font-size: 1.1rem;
|
||||
padding: 2px 4px; transition: all var(--transition); border: none; cursor: pointer;
|
||||
}
|
||||
.zd-meta .fav-btn.favorited { color: #E74C3C; }
|
||||
.zd-meta .fav-btn:hover { color: #E74C3C; }
|
||||
|
||||
/* ── 标签 ── */
|
||||
.zd-tags { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
|
||||
.zd-tag {
|
||||
padding: 3px 10px; border-radius: 12px; font-size: 0.75rem;
|
||||
background: var(--bg-tag); color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* ── 封面图 ── */
|
||||
#zd-cover {
|
||||
margin-bottom: 24px; border-radius: var(--radius);
|
||||
overflow: hidden; max-height: 400px;
|
||||
}
|
||||
#zd-cover img { width: 100%; height: 100%; object-fit: cover; }
|
||||
|
||||
/* ── 正文 ── */
|
||||
.zd-content {
|
||||
font-size: 0.95rem; line-height: 1.85; color: var(--text);
|
||||
word-break: break-word; white-space: pre-wrap;
|
||||
}
|
||||
.zd-content p { margin-bottom: 1em; }
|
||||
.zd-content img { max-width: 100%; border-radius: 6px; margin: 12px 0; }
|
||||
.zd-content blockquote {
|
||||
border-left: 3px solid var(--accent); padding: 8px 16px;
|
||||
margin: 12px 0; color: var(--text-secondary);
|
||||
background: var(--bg);
|
||||
}
|
||||
.zd-content h1,.zd-content h2,.zd-content h3,.zd-content h4 {
|
||||
margin: 1.2em 0 0.6em; font-weight: 600;
|
||||
}
|
||||
.zd-content h2 { font-size: 1.2rem; }
|
||||
.zd-content h3 { font-size: 1.05rem; }
|
||||
.zd-content ul,.zd-content ol { padding-left: 1.5em; margin-bottom: 1em; }
|
||||
.zd-content li { margin-bottom: 0.3em; }
|
||||
.zd-content pre {
|
||||
background: var(--bg); padding: 12px 16px; border-radius: 6px;
|
||||
overflow-x: auto; font-size: 0.85rem; margin: 12px 0;
|
||||
}
|
||||
.zd-content code {
|
||||
background: var(--bg-tag); padding: 2px 6px; border-radius: 4px;
|
||||
font-size: 0.88em;
|
||||
}
|
||||
.zd-content pre code { background: transparent; padding: 0; }
|
||||
|
||||
/* ── 图片集 ── */
|
||||
.zd-gallery { margin-top: 30px; padding-top: 20px; border-top: 1px solid var(--border); }
|
||||
.zd-gallery h3 { font-size: 1rem; color: var(--text); margin-bottom: 14px; }
|
||||
.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 12px; }
|
||||
.gallery-item {
|
||||
border-radius: var(--radius); overflow: hidden;
|
||||
background: var(--bg-tag); cursor: pointer;
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
.gallery-item:hover { transform: scale(1.02); }
|
||||
.gallery-item img { width: 100%; height: 180px; object-fit: cover; display: block; }
|
||||
|
||||
/* ── 响应式 ── */
|
||||
@media (max-width: 767px) {
|
||||
.zd-layout { padding: 0 12px; }
|
||||
.zd-header h1 { font-size: 1.3rem; }
|
||||
.zd-content { font-size: 0.9rem; }
|
||||
.gallery-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); }
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>知资札记 · 文章详情</title>
|
||||
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/zhizi_notes/zhizi_notes.css" />
|
||||
<link rel="stylesheet" href="/tools/zhizi_notes/detail.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Toast -->
|
||||
<div id="zd-toast" class="zs-toast"></div>
|
||||
|
||||
<!-- ═══════════════ 顶部 Header ═══════════════ -->
|
||||
<header class="zs-header">
|
||||
<button class="hamburger" id="zd-back" title="返回" style="display:flex">←</button>
|
||||
<div class="logo">知资札记</div>
|
||||
<nav class="nav-menu">
|
||||
<button data-nav="home">首页</button>
|
||||
</nav>
|
||||
<div class="header-right">
|
||||
<button id="zs-theme-btn" class="theme-btn" title="切换暗黑模式">🌙</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════════ 主体 ═══════════════ -->
|
||||
<div class="zd-layout">
|
||||
<main class="zd-main">
|
||||
<!-- 加载中 -->
|
||||
<div id="zd-loading" class="zs-loading">
|
||||
<div class="spinner"></div>
|
||||
<div>加载中…</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<article id="zd-article" style="display:none">
|
||||
<!-- 标题区 -->
|
||||
<header class="zd-header">
|
||||
<h1 id="zd-title"></h1>
|
||||
<div class="zd-meta">
|
||||
<span id="zd-category" class="zd-cat"></span>
|
||||
<span id="zd-source"></span>
|
||||
<span id="zd-date"></span>
|
||||
<span id="zd-views"></span>
|
||||
<button id="zd-fav-btn" class="fav-btn" title="收藏">♡</button>
|
||||
</div>
|
||||
<div id="zd-tags" class="zd-tags"></div>
|
||||
</header>
|
||||
|
||||
<!-- 封面图 -->
|
||||
<div id="zd-cover" style="display:none">
|
||||
<img id="zd-cover-img" alt="" />
|
||||
</div>
|
||||
|
||||
<!-- 正文 -->
|
||||
<div id="zd-content" class="zd-content"></div>
|
||||
|
||||
<!-- 图片集 -->
|
||||
<div id="zd-gallery" class="zd-gallery" style="display:none">
|
||||
<h3>附图</h3>
|
||||
<div id="zd-gallery-grid" class="gallery-grid"></div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<!-- 错误 -->
|
||||
<div id="zd-error" class="zs-empty" style="display:none">
|
||||
<div id="zd-error-msg">文章不存在或已被删除</div>
|
||||
<div style="margin-top:12px">
|
||||
<a href="/tools/zhizi_notes/index.html" style="color:var(--accent)">← 返回首页</a>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="/tools/zhizi_notes/detail.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 知资札记 - 文章详情页逻辑
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API_BASE = '/api/zhizi_notes';
|
||||
const STORAGE_KEY = 'zhizi_notes_theme';
|
||||
const $ = function (sel) { return document.querySelector(sel); };
|
||||
|
||||
let articleId = null;
|
||||
let isFavorited = false;
|
||||
|
||||
// ── 解析 URL 参数 ──
|
||||
function getParam(name) {
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
return params.get(name);
|
||||
}
|
||||
|
||||
// ── Toast ──
|
||||
function showToast(msg, type) {
|
||||
type = type || 'info';
|
||||
const t = $('#zd-toast');
|
||||
if (!t) return;
|
||||
t.textContent = msg;
|
||||
t.className = 'zs-toast ' + type + ' show';
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(function () { t.classList.remove('show'); }, type === 'error' ? 4000 : 2000);
|
||||
}
|
||||
|
||||
// ── 暗黑模式 ──
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
const btn = $('#zs-theme-btn');
|
||||
if (btn) {
|
||||
btn.textContent = saved === 'dark' ? '☀️' : '🌙';
|
||||
btn.addEventListener('click', function () {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
if (isDark) {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
localStorage.setItem(STORAGE_KEY, 'light');
|
||||
btn.textContent = '🌙';
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem(STORAGE_KEY, 'dark');
|
||||
btn.textContent = '☀️';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── API ──
|
||||
async function apiGet(path) {
|
||||
try {
|
||||
const res = await fetch(API_BASE + path, { credentials: 'same-origin' });
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
async function apiPost(path, body) {
|
||||
try {
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return await res.json();
|
||||
} catch (e) { return { ok: false, error: e.message }; }
|
||||
}
|
||||
|
||||
// ── 加载详情 ──
|
||||
async function loadDetail() {
|
||||
articleId = getParam('id');
|
||||
if (!articleId) {
|
||||
showError('缺少文章ID');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await apiGet('/articles/' + articleId);
|
||||
const loading = $('#zd-loading');
|
||||
if (loading) loading.style.display = 'none';
|
||||
|
||||
if (!data.ok) {
|
||||
showError(data.error || '文章不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
renderArticle(data.data);
|
||||
}
|
||||
|
||||
function showError(msg) {
|
||||
const loading = $('#zd-loading');
|
||||
const error = $('#zd-error');
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (error) {
|
||||
error.style.display = 'block';
|
||||
$('#zd-error-msg').textContent = msg;
|
||||
}
|
||||
}
|
||||
|
||||
// ── 渲染文章 ──
|
||||
function renderArticle(article) {
|
||||
const articleEl = $('#zd-article');
|
||||
if (!articleEl) return;
|
||||
articleEl.style.display = 'block';
|
||||
|
||||
// 标题
|
||||
$('#zd-title').textContent = article.title;
|
||||
document.title = article.title + ' · 知资札记';
|
||||
|
||||
// 元信息
|
||||
$('#zd-category').textContent = article.category || '未分类';
|
||||
$('#zd-source').textContent = article.source ? '来源:' + article.source : '';
|
||||
$('#zd-date').textContent = (article.created_at || '').slice(0, 10);
|
||||
$('#zd-views').textContent = (article.views || 0) + ' 阅读';
|
||||
|
||||
// 收藏按钮
|
||||
isFavorited = article.fav_count > 0;
|
||||
updateFavBtn();
|
||||
const favBtn = $('#zd-fav-btn');
|
||||
if (favBtn) {
|
||||
favBtn.addEventListener('click', function () { toggleFavorite(); });
|
||||
}
|
||||
|
||||
// 标签
|
||||
const tagsEl = $('#zd-tags');
|
||||
try {
|
||||
const tags = JSON.parse(article.tags_json || '[]');
|
||||
if (tags.length > 0 && tagsEl) {
|
||||
tagsEl.innerHTML = '';
|
||||
tags.forEach(function (t) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'zd-tag';
|
||||
span.textContent = t;
|
||||
tagsEl.appendChild(span);
|
||||
});
|
||||
} else if (tagsEl) {
|
||||
tagsEl.style.display = 'none';
|
||||
}
|
||||
} catch (e) { if (tagsEl) tagsEl.style.display = 'none'; }
|
||||
|
||||
// 封面图
|
||||
if (article.cover_url) {
|
||||
const cover = $('#zd-cover');
|
||||
const coverImg = $('#zd-cover-img');
|
||||
if (cover) cover.style.display = 'block';
|
||||
if (coverImg) coverImg.src = article.cover_url;
|
||||
}
|
||||
|
||||
// 正文
|
||||
const contentEl = $('#zd-content');
|
||||
if (contentEl) {
|
||||
contentEl.innerHTML = article.content || article.content_plain || '';
|
||||
}
|
||||
|
||||
// 图片集
|
||||
if (article.images && article.images.length > 0) {
|
||||
const gallery = $('#zd-gallery');
|
||||
const grid = $('#zd-gallery-grid');
|
||||
if (gallery) gallery.style.display = 'block';
|
||||
if (grid) {
|
||||
grid.innerHTML = '';
|
||||
article.images.forEach(function (img) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'gallery-item';
|
||||
const imgEl = document.createElement('img');
|
||||
imgEl.src = '/uploads/zhizi_notes/' + img.filename;
|
||||
imgEl.alt = img.original_name || '';
|
||||
imgEl.loading = 'lazy';
|
||||
item.appendChild(imgEl);
|
||||
grid.appendChild(item);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 收藏切换 ──
|
||||
async function toggleFavorite() {
|
||||
const data = await apiPost('/articles/' + articleId + '/toggle_favorite', {});
|
||||
if (data.ok) {
|
||||
isFavorited = data.data ? data.data.favorited : !isFavorited;
|
||||
updateFavBtn();
|
||||
showToast(isFavorited ? '已收藏' : '已取消收藏', isFavorited ? 'success' : 'info');
|
||||
} else {
|
||||
showToast('操作失败:' + (data.error || '未知错误'), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function updateFavBtn() {
|
||||
const btn = $('#zd-fav-btn');
|
||||
if (!btn) return;
|
||||
if (isFavorited) {
|
||||
btn.classList.add('favorited');
|
||||
btn.textContent = '♥';
|
||||
} else {
|
||||
btn.classList.remove('favorited');
|
||||
btn.textContent = '♡';
|
||||
}
|
||||
}
|
||||
|
||||
// ── 初始化 ──
|
||||
function init() {
|
||||
initTheme();
|
||||
loadDetail();
|
||||
|
||||
// 返回按钮
|
||||
const backBtn = $('#zd-back');
|
||||
if (backBtn) {
|
||||
backBtn.addEventListener('click', function () {
|
||||
if (document.referrer && document.referrer.indexOf('/tools/zhizi_notes') !== -1) {
|
||||
history.back();
|
||||
} else {
|
||||
location.href = '/tools/zhizi_notes/index.html';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
@@ -0,0 +1,147 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>知资札记 · 证券投资图文存档站</title>
|
||||
<link rel="icon" href="/icons/zhizi_notes.svg" type="image/svg+xml" />
|
||||
<link rel="stylesheet" href="/common/common.css" />
|
||||
<link rel="stylesheet" href="/tools/zhizi_notes/zhizi_notes.css" />
|
||||
</head>
|
||||
<body>
|
||||
<!-- Toast -->
|
||||
<div id="zs-toast" class="zs-toast"></div>
|
||||
|
||||
<!-- 侧边栏遮罩(手机) -->
|
||||
<div id="zs-sidebar-overlay" class="zs-sidebar-overlay"></div>
|
||||
|
||||
<!-- ═══════════════ 顶部 Header ═══════════════ -->
|
||||
<header class="zs-header">
|
||||
<button id="zs-hamburger" class="hamburger" title="菜单">☰</button>
|
||||
<div class="logo">知资札记</div>
|
||||
<nav class="nav-menu">
|
||||
<button class="active" data-nav="home">首页</button>
|
||||
<button data-nav="archive">分类归档</button>
|
||||
<button data-nav="favorites">我的收藏</button>
|
||||
<button data-nav="submit">提交存档</button>
|
||||
</nav>
|
||||
<div class="header-right">
|
||||
<div class="search-box">
|
||||
<input id="zs-search" type="text" placeholder="搜索标题/标签…" />
|
||||
<button id="zs-search-btn" title="搜索">🔍</button>
|
||||
</div>
|
||||
<button id="zs-theme-btn" class="theme-btn" title="切换暗黑模式">🌙</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- ═══════════════ 主体布局 ═══════════════ -->
|
||||
<div class="zs-layout">
|
||||
<!-- 左侧筛选栏 -->
|
||||
<aside id="zs-sidebar" class="zs-sidebar">
|
||||
<!-- 内容分类 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">内容分类</div>
|
||||
<div id="zs-categories">
|
||||
<button class="cat-item active" data-cat="">全部</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 热门标签 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">热门标签</div>
|
||||
<div id="zs-tags" class="tag-cloud"></div>
|
||||
</div>
|
||||
|
||||
<!-- 时间筛选 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">时间筛选</div>
|
||||
<div class="time-filter">
|
||||
<button class="time-item active" data-time="">全部</button>
|
||||
<button class="time-item" data-time="week">本周存档</button>
|
||||
<button class="time-item" data-time="month">本月</button>
|
||||
<button class="time-item" data-time="quarter">季度</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容类型 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">内容类型</div>
|
||||
<div class="type-toggle">
|
||||
<button class="type-btn active" data-type="">全部</button>
|
||||
<button class="type-btn" data-type="text">纯文字</button>
|
||||
<button class="type-btn" data-type="image">图文多图</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 站点统计 -->
|
||||
<div class="sidebar-section">
|
||||
<div class="sidebar-title">站点统计</div>
|
||||
<div class="stats-card">
|
||||
<div class="stat-row"><span>总存档文章</span><span class="stat-val" id="zs-stat-articles">0</span></div>
|
||||
<div class="stat-row"><span>总存档图片</span><span class="stat-val" id="zs-stat-images">0</span></div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- 中间主内容区 -->
|
||||
<main class="zs-main">
|
||||
<!-- 视图切换 + 筛选标签 -->
|
||||
<div class="view-toggle">
|
||||
<button class="active" data-view="card">📐 瀑布卡片</button>
|
||||
<button data-view="list">📋 简洁列表</button>
|
||||
<div id="zs-filter-tags" class="filter-tags" style="flex:1;margin-left:8px"></div>
|
||||
</div>
|
||||
|
||||
<!-- 卡片网格 -->
|
||||
<div id="zs-card-grid" class="card-grid"></div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div id="zs-loading" class="zs-loading" style="display:none">
|
||||
<div class="spinner"></div>
|
||||
<div>加载中…</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div id="zs-empty" class="zs-empty" style="display:none">
|
||||
<div>暂无存档内容</div>
|
||||
<div style="font-size:0.8rem;margin-top:4px">点击右侧「提交新存档」开始记录</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 右侧辅助栏 -->
|
||||
<aside class="zs-rightbar">
|
||||
<!-- 站长精选 -->
|
||||
<div class="right-section">
|
||||
<div class="right-title">站长精选</div>
|
||||
<div id="zs-pinned">
|
||||
<div class="pinned-item" style="color:var(--text-light)">暂无置顶</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 最新入库 -->
|
||||
<div class="right-section">
|
||||
<div class="right-title">最新入库</div>
|
||||
<div id="zs-latest">
|
||||
<div class="latest-item" style="color:var(--text-light)">暂无存档</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 分类占比 -->
|
||||
<div class="right-section">
|
||||
<div class="right-title">分类占比</div>
|
||||
<div id="zs-chart">
|
||||
<div style="color:var(--text-light);font-size:0.8rem">暂无数据</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 提交存档 -->
|
||||
<div class="right-section">
|
||||
<button id="zs-submit-btn" class="submit-btn">提交新存档</button>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<!-- 脚本 -->
|
||||
<script src="/tools/zhizi_notes/zhizi_notes.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,416 @@
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
知资札记 - 全局样式
|
||||
主色:#162A46 墨蓝 | 辅助色:#467974 青灰
|
||||
日间背景:#F7F9FC | 暗黑背景:#0F1726
|
||||
═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* ── CSS 变量(日间默认) ── */
|
||||
:root {
|
||||
--primary: #162A46;
|
||||
--primary-light: #1E3A5F;
|
||||
--primary-dark: #0F1D33;
|
||||
--accent: #467974;
|
||||
--accent-light: #5A9690;
|
||||
--text: #333;
|
||||
--text-secondary: #666;
|
||||
--text-light: #999;
|
||||
--bg: #F7F9FC;
|
||||
--bg-card: #FFFFFF;
|
||||
--bg-sidebar: #FFFFFF;
|
||||
--bg-header: #162A46;
|
||||
--bg-tag: #E8EDF2;
|
||||
--bg-tag-hover: #D0D8E0;
|
||||
--border: #E0E4E8;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.08);
|
||||
--shadow-hover: 0 4px 16px rgba(0,0,0,0.12);
|
||||
--radius: 8px;
|
||||
--transition: 0.2s ease;
|
||||
--font: 'Microsoft YaHei', 'Source Han Sans CN', -apple-system, sans-serif;
|
||||
--line-height: 1.75;
|
||||
}
|
||||
|
||||
/* ── 暗黑模式 ── */
|
||||
[data-theme="dark"] {
|
||||
--primary: #1E3A5F;
|
||||
--primary-light: #2A4D7A;
|
||||
--primary-dark: #0F1D33;
|
||||
--accent: #5A9690;
|
||||
--accent-light: #6DB0A9;
|
||||
--text: #E0E4E8;
|
||||
--text-secondary: #A0A8B4;
|
||||
--text-light: #6B7280;
|
||||
--bg: #0F1726;
|
||||
--bg-card: #1A2332;
|
||||
--bg-sidebar: #1A2332;
|
||||
--bg-header: #0F1D33;
|
||||
--bg-tag: #1E2D3D;
|
||||
--bg-tag-hover: #2A3D52;
|
||||
--border: #2A3D52;
|
||||
--shadow: 0 1px 3px rgba(0,0,0,0.3);
|
||||
--shadow-hover: 0 4px 16px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* ── 全局重置 ── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
html { font-size: 16px; scroll-behavior: smooth; }
|
||||
body {
|
||||
font-family: var(--font);
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
line-height: var(--line-height);
|
||||
min-height: 100vh;
|
||||
transition: background var(--transition), color var(--transition);
|
||||
}
|
||||
a { text-decoration: none; color: inherit; }
|
||||
img { max-width: 100%; display: block; }
|
||||
button { cursor: pointer; font-family: inherit; border: none; outline: none; }
|
||||
input, select, textarea { font-family: inherit; outline: none; }
|
||||
|
||||
/* ── 顶部 Header ── */
|
||||
.zs-header {
|
||||
position: fixed; top: 0; left: 0; right: 0; z-index: 1000;
|
||||
background: var(--bg-header); color: #fff;
|
||||
height: 56px; display: flex; align-items: center;
|
||||
padding: 0 20px; gap: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
.zs-header .logo {
|
||||
font-size: 1.25rem; font-weight: 700;
|
||||
letter-spacing: 2px; white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
.zs-header .nav-menu {
|
||||
display: flex; gap: 4px; flex: 1; justify-content: center;
|
||||
}
|
||||
.zs-header .nav-menu button {
|
||||
background: transparent; color: rgba(255,255,255,0.75);
|
||||
padding: 6px 16px; border-radius: 6px; font-size: 0.9rem;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-header .nav-menu button:hover,
|
||||
.zs-header .nav-menu button.active { color: #fff; background: rgba(255,255,255,0.12); }
|
||||
.zs-header .header-right { display: flex; align-items: center; gap: 10px; }
|
||||
.zs-header .search-box {
|
||||
display: flex; align-items: center; background: rgba(255,255,255,0.12);
|
||||
border-radius: 20px; padding: 4px 12px; gap: 6px;
|
||||
}
|
||||
.zs-header .search-box input {
|
||||
background: transparent; border: none; color: #fff;
|
||||
width: 160px; font-size: 0.85rem;
|
||||
}
|
||||
.zs-header .search-box input::placeholder { color: rgba(255,255,255,0.5); }
|
||||
.zs-header .search-box button {
|
||||
background: transparent; color: rgba(255,255,255,0.7);
|
||||
font-size: 1rem; padding: 2px;
|
||||
}
|
||||
.zs-header .theme-btn {
|
||||
background: rgba(255,255,255,0.12); color: #fff;
|
||||
width: 34px; height: 34px; border-radius: 50%;
|
||||
font-size: 1.1rem; display: flex; align-items: center; justify-content: center;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-header .theme-btn:hover { background: rgba(255,255,255,0.2); }
|
||||
|
||||
/* ── 汉堡菜单按钮(手机) ── */
|
||||
.hamburger {
|
||||
display: none; background: transparent; color: #fff;
|
||||
font-size: 1.5rem; width: 36px; height: 36px;
|
||||
align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
/* ── 主体布局 ── */
|
||||
.zs-layout {
|
||||
display: flex; margin-top: 56px; min-height: calc(100vh - 56px);
|
||||
max-width: 1400px; margin-left: auto; margin-right: auto;
|
||||
}
|
||||
|
||||
/* ── 左侧筛选栏 ── */
|
||||
.zs-sidebar {
|
||||
width: 220px; min-width: 220px; background: var(--bg-sidebar);
|
||||
border-right: 1px solid var(--border);
|
||||
padding: 16px 12px; overflow-y: auto;
|
||||
position: sticky; top: 56px; height: calc(100vh - 56px);
|
||||
transition: transform var(--transition);
|
||||
}
|
||||
.zs-sidebar .sidebar-section { margin-bottom: 20px; }
|
||||
.zs-sidebar .sidebar-title {
|
||||
font-size: 0.8rem; color: var(--text-light);
|
||||
text-transform: uppercase; letter-spacing: 1px;
|
||||
margin-bottom: 8px; padding-left: 4px;
|
||||
}
|
||||
.zs-sidebar .cat-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 8px 12px; border-radius: 6px; font-size: 0.9rem;
|
||||
color: var(--text-secondary); background: transparent;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-sidebar .cat-item:hover { background: var(--bg-tag); color: var(--text); }
|
||||
.zs-sidebar .cat-item.active { background: var(--primary); color: #fff; }
|
||||
.zs-sidebar .tag-cloud { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.zs-sidebar .tag-item {
|
||||
padding: 4px 10px; border-radius: 12px; font-size: 0.78rem;
|
||||
background: var(--bg-tag); color: var(--text-secondary);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-sidebar .tag-item:hover { background: var(--bg-tag-hover); color: var(--text); }
|
||||
.zs-sidebar .tag-item.active { background: var(--accent); color: #fff; }
|
||||
.zs-sidebar .time-filter { display: flex; flex-direction: column; gap: 4px; }
|
||||
.zs-sidebar .time-item {
|
||||
display: block; width: 100%; text-align: left;
|
||||
padding: 6px 12px; border-radius: 6px; font-size: 0.85rem;
|
||||
color: var(--text-secondary); background: transparent;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-sidebar .time-item:hover { background: var(--bg-tag); }
|
||||
.zs-sidebar .time-item.active { background: var(--accent); color: #fff; }
|
||||
.zs-sidebar .type-toggle { display: flex; gap: 6px; }
|
||||
.zs-sidebar .type-btn {
|
||||
flex: 1; padding: 6px 8px; border-radius: 6px; font-size: 0.8rem;
|
||||
background: var(--bg-tag); color: var(--text-secondary);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-sidebar .type-btn.active { background: var(--accent); color: #fff; }
|
||||
.zs-sidebar .stats-card {
|
||||
background: var(--bg); border-radius: var(--radius);
|
||||
padding: 12px; font-size: 0.85rem;
|
||||
}
|
||||
.zs-sidebar .stats-card .stat-row {
|
||||
display: flex; justify-content: space-between;
|
||||
padding: 4px 0; color: var(--text-secondary);
|
||||
}
|
||||
.zs-sidebar .stats-card .stat-val { font-weight: 700; color: var(--primary); }
|
||||
|
||||
/* ── 中间主内容区 ── */
|
||||
.zs-main {
|
||||
flex: 1; padding: 20px; min-width: 0;
|
||||
}
|
||||
.zs-main .view-toggle {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 16px;
|
||||
}
|
||||
.zs-main .view-toggle button {
|
||||
padding: 6px 14px; border-radius: 6px; font-size: 0.85rem;
|
||||
background: var(--bg-tag); color: var(--text-secondary);
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-main .view-toggle button.active { background: var(--primary); color: #fff; }
|
||||
.zs-main .filter-tags {
|
||||
display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px;
|
||||
}
|
||||
.zs-main .filter-tag {
|
||||
display: inline-flex; align-items: center; gap: 4px;
|
||||
padding: 3px 10px; border-radius: 12px; font-size: 0.78rem;
|
||||
background: var(--accent); color: #fff;
|
||||
}
|
||||
.zs-main .filter-tag .remove-filter {
|
||||
background: transparent; color: rgba(255,255,255,0.7);
|
||||
font-size: 0.85rem; line-height: 1; padding: 0;
|
||||
}
|
||||
.zs-main .filter-tag .remove-filter:hover { color: #fff; }
|
||||
|
||||
/* ── 卡片网格视图 ── */
|
||||
.card-grid {
|
||||
display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card-grid.list-view {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* ── 图文卡片 ── */
|
||||
.zs-card {
|
||||
background: var(--bg-card); border-radius: var(--radius);
|
||||
box-shadow: var(--shadow); overflow: hidden;
|
||||
transition: all var(--transition); cursor: pointer;
|
||||
position: relative;
|
||||
}
|
||||
.zs-card:hover { box-shadow: var(--shadow-hover); transform: translateY(-2px); }
|
||||
.zs-card .card-cover {
|
||||
height: 180px; overflow: hidden; position: relative;
|
||||
background: var(--bg-tag);
|
||||
}
|
||||
.zs-card .card-cover img {
|
||||
width: 100%; height: 100%; object-fit: cover;
|
||||
}
|
||||
.zs-card .card-cover .text-badge {
|
||||
width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, var(--primary), var(--accent));
|
||||
color: #fff; font-size: 2.5rem; font-weight: 700;
|
||||
}
|
||||
.zs-card .card-badge {
|
||||
position: absolute; bottom: 8px; right: 8px;
|
||||
background: rgba(0,0,0,0.6); color: #fff;
|
||||
padding: 2px 8px; border-radius: 10px; font-size: 0.72rem;
|
||||
}
|
||||
.zs-card .card-body { padding: 14px; }
|
||||
.zs-card .card-title {
|
||||
font-size: 0.95rem; font-weight: 600; color: var(--text);
|
||||
line-height: 1.5; display: -webkit-box;
|
||||
-webkit-line-clamp: 2; -webkit-box-orient: vertical;
|
||||
overflow: hidden; margin-bottom: 8px;
|
||||
}
|
||||
.zs-card .card-source {
|
||||
font-size: 0.78rem; color: var(--text-light); margin-bottom: 6px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.zs-card .card-tags { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 8px; }
|
||||
.zs-card .card-tag {
|
||||
padding: 2px 8px; border-radius: 10px; font-size: 0.7rem;
|
||||
background: var(--bg-tag); color: var(--text-secondary);
|
||||
}
|
||||
.zs-card .card-footer {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
font-size: 0.75rem; color: var(--text-light);
|
||||
border-top: 1px solid var(--border); padding-top: 10px;
|
||||
}
|
||||
.zs-card .card-footer .fav-btn {
|
||||
background: transparent; color: var(--text-light); font-size: 1rem;
|
||||
padding: 2px 4px; transition: all var(--transition);
|
||||
}
|
||||
.zs-card .card-footer .fav-btn.favorited { color: #E74C3C; }
|
||||
.zs-card .card-footer .fav-btn:hover { color: #E74C3C; }
|
||||
|
||||
/* ── 列表视图卡片 ── */
|
||||
.list-view .zs-card {
|
||||
display: flex; flex-direction: row;
|
||||
}
|
||||
.list-view .zs-card .card-cover {
|
||||
width: 240px; min-width: 240px; height: auto; min-height: 160px;
|
||||
}
|
||||
.list-view .zs-card .card-body { flex: 1; }
|
||||
|
||||
/* ── 右侧辅助栏 ── */
|
||||
.zs-rightbar {
|
||||
width: 260px; min-width: 260px; background: var(--bg-sidebar);
|
||||
border-left: 1px solid var(--border); padding: 16px 12px;
|
||||
overflow-y: auto; position: sticky; top: 56px;
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
.zs-rightbar .right-section { margin-bottom: 20px; }
|
||||
.zs-rightbar .right-title {
|
||||
font-size: 0.82rem; font-weight: 600; color: var(--text);
|
||||
margin-bottom: 10px; padding-bottom: 6px;
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
.zs-rightbar .pinned-item {
|
||||
padding: 8px 0; border-bottom: 1px solid var(--border);
|
||||
cursor: pointer; transition: color var(--transition);
|
||||
}
|
||||
.zs-rightbar .pinned-item:hover { color: var(--accent); }
|
||||
.zs-rightbar .pinned-item .pinned-title {
|
||||
font-size: 0.85rem; font-weight: 500;
|
||||
display: -webkit-box; -webkit-line-clamp: 1; -webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
}
|
||||
.zs-rightbar .pinned-item .pinned-date {
|
||||
font-size: 0.72rem; color: var(--text-light); margin-top: 2px;
|
||||
}
|
||||
.zs-rightbar .latest-item {
|
||||
padding: 6px 0; font-size: 0.82rem; color: var(--text-secondary);
|
||||
cursor: pointer; transition: color var(--transition);
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.zs-rightbar .latest-item:hover { color: var(--accent); }
|
||||
.zs-rightbar .chart-bar {
|
||||
display: flex; align-items: center; gap: 8px; margin-bottom: 6px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
.zs-rightbar .chart-bar .bar-label { width: 60px; text-align: right; color: var(--text-secondary); }
|
||||
.zs-rightbar .chart-bar .bar-track {
|
||||
flex: 1; height: 8px; background: var(--bg-tag); border-radius: 4px; overflow: hidden;
|
||||
}
|
||||
.zs-rightbar .chart-bar .bar-fill {
|
||||
height: 100%; background: var(--accent); border-radius: 4px;
|
||||
transition: width 0.5s ease;
|
||||
}
|
||||
.zs-rightbar .chart-bar .bar-count { width: 30px; color: var(--text-light); }
|
||||
.zs-rightbar .submit-btn {
|
||||
display: block; width: 100%; padding: 10px; border-radius: var(--radius);
|
||||
background: var(--primary); color: #fff; font-size: 0.9rem; font-weight: 600;
|
||||
text-align: center; transition: all var(--transition);
|
||||
}
|
||||
.zs-rightbar .submit-btn:hover { background: var(--primary-light); }
|
||||
|
||||
/* ── 加载状态 ── */
|
||||
.zs-loading {
|
||||
text-align: center; padding: 40px; color: var(--text-light);
|
||||
}
|
||||
.zs-loading .spinner {
|
||||
width: 32px; height: 32px; border: 3px solid var(--border);
|
||||
border-top-color: var(--accent); border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite; margin: 0 auto 12px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
.zs-empty {
|
||||
text-align: center; padding: 60px 20px; color: var(--text-light); font-size: 0.95rem;
|
||||
}
|
||||
|
||||
/* ── 提示 Toast ── */
|
||||
.zs-toast {
|
||||
position: fixed; top: 70px; left: 50%; transform: translateX(-50%);
|
||||
z-index: 9999; padding: 10px 24px; border-radius: 8px;
|
||||
font-size: 0.9rem; color: #fff; opacity: 0;
|
||||
transition: opacity 0.3s ease; pointer-events: none;
|
||||
}
|
||||
.zs-toast.show { opacity: 1; }
|
||||
.zs-toast.info { background: #3B82F6; }
|
||||
.zs-toast.success { background: #10B981; }
|
||||
.zs-toast.error { background: #EF4444; }
|
||||
|
||||
/* ── 模态弹窗(交互详情/确认) ── */
|
||||
.zs-modal-overlay {
|
||||
position: fixed; inset: 0; z-index: 5000;
|
||||
background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center;
|
||||
transition: opacity var(--transition);
|
||||
}
|
||||
.zs-modal-overlay.hidden { display: none; }
|
||||
.zs-modal {
|
||||
background: var(--bg-card); border-radius: 12px; padding: 24px;
|
||||
max-width: 500px; width: 90%; max-height: 80vh; overflow-y: auto;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.2);
|
||||
}
|
||||
.zs-modal h3 { margin-bottom: 16px; color: var(--text); }
|
||||
.zs-modal .modal-body { font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 20px; }
|
||||
.zs-modal .modal-body pre {
|
||||
background: var(--bg); padding: 12px; border-radius: 6px;
|
||||
font-size: 0.78rem; overflow-x: auto; white-space: pre-wrap; word-break: break-all;
|
||||
}
|
||||
.zs-modal .modal-actions { display: flex; gap: 10px; justify-content: flex-end; }
|
||||
.zs-modal .btn {
|
||||
padding: 8px 20px; border-radius: 6px; font-size: 0.85rem; font-weight: 500;
|
||||
transition: all var(--transition);
|
||||
}
|
||||
.zs-modal .btn-primary { background: var(--primary); color: #fff; }
|
||||
.zs-modal .btn-primary:hover { background: var(--primary-light); }
|
||||
.zs-modal .btn-danger { background: #D94452; color: #fff; }
|
||||
.zs-modal .btn-danger:hover { background: #C0392B; }
|
||||
.zs-modal .btn-cancel { background: var(--bg-tag); color: var(--text-secondary); }
|
||||
.zs-modal .btn-cancel:hover { background: var(--bg-tag-hover); }
|
||||
|
||||
/* ── 响应式 ── */
|
||||
@media (max-width: 1199px) {
|
||||
.zs-rightbar { display: none; }
|
||||
.zs-sidebar { width: 200px; min-width: 200px; }
|
||||
.card-grid { grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); }
|
||||
.list-view .zs-card .card-cover { width: 180px; min-width: 180px; }
|
||||
}
|
||||
@media (max-width: 767px) {
|
||||
.zs-layout { flex-direction: column; }
|
||||
.zs-sidebar {
|
||||
position: fixed; top: 56px; left: 0; bottom: 0; z-index: 900;
|
||||
width: 260px; transform: translateX(-100%);
|
||||
box-shadow: 4px 0 16px rgba(0,0,0,0.15);
|
||||
}
|
||||
.zs-sidebar.open { transform: translateX(0); }
|
||||
.zs-sidebar-overlay {
|
||||
display: none; position: fixed; inset: 0; z-index: 899;
|
||||
background: rgba(0,0,0,0.4);
|
||||
}
|
||||
.zs-sidebar-overlay.show { display: block; }
|
||||
.hamburger { display: flex; }
|
||||
.zs-header .nav-menu { display: none; }
|
||||
.zs-header .search-box input { width: 100px; }
|
||||
.zs-main { padding: 12px; }
|
||||
.card-grid { grid-template-columns: 1fr; }
|
||||
.list-view .zs-card { flex-direction: column; }
|
||||
.list-view .zs-card .card-cover { width: 100%; min-width: auto; height: 180px; }
|
||||
}
|
||||
@@ -0,0 +1,594 @@
|
||||
/**
|
||||
* 知资札记 - C端前端逻辑
|
||||
* 证券投资图文存档站
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── 常量 ──
|
||||
const API_BASE = '/api/zhizi_notes';
|
||||
const STORAGE_KEY = 'zhizi_notes_theme';
|
||||
const STORAGE_VIEW = 'zhizi_notes_view';
|
||||
const STORAGE_FAVORITES = 'zhizi_notes_favorites';
|
||||
|
||||
// ── 状态 ──
|
||||
let state = {
|
||||
page: 1,
|
||||
pageSize: 12,
|
||||
totalPages: 0,
|
||||
loading: false,
|
||||
done: false,
|
||||
viewMode: localStorage.getItem(STORAGE_VIEW) || 'card',
|
||||
filter: { category: '', tag: '', timeRange: '', contentType: '', search: '' },
|
||||
favorites: new Set()
|
||||
};
|
||||
|
||||
// ── DOM 缓存 ──
|
||||
const $ = (sel) => document.querySelector(sel);
|
||||
const $$ = (sel) => document.querySelectorAll(sel);
|
||||
|
||||
// ── 链接清洗函数 ──
|
||||
window.clearAllLink = function (htmlStr) {
|
||||
if (!htmlStr || typeof htmlStr !== 'string') return '';
|
||||
return htmlStr.replace(/<a\b[^>]*>(.*?)<\/a>/gi, '$1');
|
||||
};
|
||||
|
||||
// ── Toast 提示 ──
|
||||
function showToast(msg, type) {
|
||||
type = type || 'info';
|
||||
const t = $('#zs-toast');
|
||||
if (!t) return;
|
||||
t.textContent = msg;
|
||||
t.className = 'zs-toast ' + type + ' show';
|
||||
const duration = type === 'error' ? 4000 : 2000;
|
||||
clearTimeout(t._timer);
|
||||
t._timer = setTimeout(function () { t.classList.remove('show'); }, duration);
|
||||
}
|
||||
|
||||
// ── 暗黑模式 ──
|
||||
function initTheme() {
|
||||
const saved = localStorage.getItem(STORAGE_KEY);
|
||||
if (saved === 'dark') {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
}
|
||||
const btn = $('#zs-theme-btn');
|
||||
if (btn) {
|
||||
btn.textContent = saved === 'dark' ? '☀️' : '🌙';
|
||||
btn.addEventListener('click', toggleTheme);
|
||||
}
|
||||
}
|
||||
|
||||
function toggleTheme() {
|
||||
const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
||||
if (isDark) {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
localStorage.setItem(STORAGE_KEY, 'light');
|
||||
$('#zs-theme-btn').textContent = '🌙';
|
||||
} else {
|
||||
document.documentElement.setAttribute('data-theme', 'dark');
|
||||
localStorage.setItem(STORAGE_KEY, 'dark');
|
||||
$('#zs-theme-btn').textContent = '☀️';
|
||||
}
|
||||
}
|
||||
|
||||
// ── API 请求 ──
|
||||
async function apiGet(path) {
|
||||
try {
|
||||
const res = await fetch(API_BASE + path, { credentials: 'same-origin' });
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
async function apiPost(path, body) {
|
||||
try {
|
||||
const res = await fetch(API_BASE + path, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'same-origin',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
return await res.json();
|
||||
} catch (e) {
|
||||
return { ok: false, error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
// ── 加载文章列表 ──
|
||||
async function loadArticles(reset) {
|
||||
if (reset) {
|
||||
state.page = 1; state.done = false;
|
||||
const grid = $('#zs-card-grid');
|
||||
if (grid) grid.innerHTML = '';
|
||||
}
|
||||
if (state.loading || state.done) return;
|
||||
state.loading = true;
|
||||
showLoading(true);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
params.set('page', state.page);
|
||||
params.set('pageSize', state.pageSize);
|
||||
if (state.filter.category) params.set('category', state.filter.category);
|
||||
if (state.filter.tag) params.set('tag', state.filter.tag);
|
||||
if (state.filter.timeRange) params.set('timeRange', state.filter.timeRange);
|
||||
if (state.filter.contentType) params.set('contentType', state.filter.contentType);
|
||||
if (state.filter.search) params.set('search', state.filter.search);
|
||||
|
||||
const data = await apiGet('/articles?' + params.toString());
|
||||
showLoading(false);
|
||||
state.loading = false;
|
||||
|
||||
if (!data.ok) {
|
||||
showToast('加载失败:' + (data.error || '未知错误'), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const articles = data.data || [];
|
||||
const pag = data.pagination || {};
|
||||
state.totalPages = pag.totalPages || 0;
|
||||
if (state.page >= state.totalPages) state.done = true;
|
||||
if (articles.length === 0 && state.page === 1) {
|
||||
showEmpty(true);
|
||||
} else {
|
||||
showEmpty(false);
|
||||
renderCards(articles);
|
||||
state.page++;
|
||||
}
|
||||
}
|
||||
|
||||
function showLoading(show) {
|
||||
const el = $('#zs-loading');
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function showEmpty(show) {
|
||||
const el = $('#zs-empty');
|
||||
if (el) el.style.display = show ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// ── 渲染卡片 ──
|
||||
function renderCards(articles) {
|
||||
const grid = $('#zs-card-grid');
|
||||
if (!grid) return;
|
||||
articles.forEach(function (article) {
|
||||
const card = createCard(article);
|
||||
grid.appendChild(card);
|
||||
});
|
||||
lazyLoadImages();
|
||||
}
|
||||
|
||||
function createCard(article) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'zs-card';
|
||||
card.setAttribute('data-id', article.id);
|
||||
card.addEventListener('click', function (e) {
|
||||
if (e.target.closest('.fav-btn')) return;
|
||||
openDetail(article.id);
|
||||
});
|
||||
|
||||
// 封面区
|
||||
const cover = document.createElement('div');
|
||||
cover.className = 'card-cover';
|
||||
if (article.cover_url) {
|
||||
const img = document.createElement('img');
|
||||
img.setAttribute('data-src', article.cover_url);
|
||||
img.setAttribute('alt', article.title);
|
||||
img.className = 'lazy-img';
|
||||
cover.appendChild(img);
|
||||
} else {
|
||||
const badge = document.createElement('div');
|
||||
badge.className = 'text-badge';
|
||||
badge.textContent = '文';
|
||||
cover.appendChild(badge);
|
||||
}
|
||||
|
||||
// 角标
|
||||
const imgCount = article.image_count || 0;
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'card-badge';
|
||||
badge.textContent = article.content_type === 'image' ? '含' + imgCount + '张附图' : '纯文字';
|
||||
cover.appendChild(badge);
|
||||
|
||||
// 正文区
|
||||
const body = document.createElement('div');
|
||||
body.className = 'card-body';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'card-title';
|
||||
title.textContent = article.title;
|
||||
|
||||
const source = document.createElement('div');
|
||||
source.className = 'card-source';
|
||||
source.textContent = article.source || '';
|
||||
|
||||
// 标签
|
||||
const tagsRow = document.createElement('div');
|
||||
tagsRow.className = 'card-tags';
|
||||
try {
|
||||
const tags = JSON.parse(article.tags_json || '[]');
|
||||
tags.forEach(function (t) {
|
||||
const span = document.createElement('span');
|
||||
span.className = 'card-tag';
|
||||
span.textContent = t;
|
||||
tagsRow.appendChild(span);
|
||||
});
|
||||
} catch (e) {}
|
||||
|
||||
// 底部
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'card-footer';
|
||||
|
||||
const meta = document.createElement('span');
|
||||
meta.textContent = (article.created_at || '').slice(0, 10) + ' · ' + (article.views || 0) + ' 阅读';
|
||||
|
||||
const favBtn = document.createElement('button');
|
||||
favBtn.className = 'fav-btn';
|
||||
favBtn.setAttribute('data-id', article.id);
|
||||
favBtn.title = '收藏';
|
||||
if (state.favorites.has(article.id)) {
|
||||
favBtn.classList.add('favorited');
|
||||
favBtn.textContent = '♥';
|
||||
} else {
|
||||
favBtn.textContent = '♡';
|
||||
}
|
||||
favBtn.addEventListener('click', function (e) { e.stopPropagation(); toggleFavorite(article.id, favBtn); });
|
||||
|
||||
footer.appendChild(meta);
|
||||
footer.appendChild(favBtn);
|
||||
|
||||
body.appendChild(title);
|
||||
body.appendChild(source);
|
||||
body.appendChild(tagsRow);
|
||||
body.appendChild(footer);
|
||||
|
||||
card.appendChild(cover);
|
||||
card.appendChild(body);
|
||||
return card;
|
||||
}
|
||||
|
||||
// ── 图片懒加载 ──
|
||||
function lazyLoadImages() {
|
||||
const imgs = document.querySelectorAll('img.lazy-img[data-src]');
|
||||
if (!('IntersectionObserver' in window)) {
|
||||
imgs.forEach(function (img) { img.src = img.getAttribute('data-src'); img.removeAttribute('data-src'); });
|
||||
return;
|
||||
}
|
||||
const observer = new IntersectionObserver(function (entries) {
|
||||
entries.forEach(function (entry) {
|
||||
if (entry.isIntersecting) {
|
||||
const img = entry.target;
|
||||
img.src = img.getAttribute('data-src');
|
||||
img.removeAttribute('data-src');
|
||||
img.classList.remove('lazy-img');
|
||||
observer.unobserve(img);
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '100px' });
|
||||
imgs.forEach(function (img) { observer.observe(img); });
|
||||
}
|
||||
|
||||
// ── 滚动触底加载 ──
|
||||
let scrollTimer = null;
|
||||
function onScroll() {
|
||||
if (scrollTimer) return;
|
||||
scrollTimer = setTimeout(function () {
|
||||
scrollTimer = null;
|
||||
if (state.loading || state.done) return;
|
||||
const scrollY = window.scrollY + window.innerHeight;
|
||||
const docH = document.documentElement.scrollHeight;
|
||||
if (scrollY >= docH - 300) loadArticles(false);
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// ── 收藏切换 ──
|
||||
async function toggleFavorite(articleId, btn) {
|
||||
if (btn._pending) return;
|
||||
btn._pending = true;
|
||||
btn.style.opacity = '0.5';
|
||||
try {
|
||||
const data = await apiPost('/articles/' + articleId + '/toggle_favorite', {});
|
||||
if (data.ok) {
|
||||
if (data.data && data.data.favorited) {
|
||||
state.favorites.add(articleId);
|
||||
btn.classList.add('favorited');
|
||||
btn.textContent = '♥';
|
||||
showToast('已收藏', 'success');
|
||||
} else {
|
||||
state.favorites.delete(articleId);
|
||||
btn.classList.remove('favorited');
|
||||
btn.textContent = '♡';
|
||||
showToast('已取消收藏', 'info');
|
||||
}
|
||||
} else {
|
||||
showToast('操作失败:' + (data.error || '未知错误'), 'error');
|
||||
}
|
||||
} catch (e) {
|
||||
showToast('网络错误', 'error');
|
||||
}
|
||||
btn._pending = false;
|
||||
btn.style.opacity = '1';
|
||||
}
|
||||
|
||||
// ── 打开详情页 ──
|
||||
function openDetail(id) {
|
||||
window.open('/tools/zhizi_notes/detail.html?id=' + id, '_blank');
|
||||
}
|
||||
|
||||
// ── 筛选逻辑 ──
|
||||
function setFilter(key, value) {
|
||||
if (key === 'category') {
|
||||
if (state.filter.category === value) value = '';
|
||||
} else if (key === 'tag') {
|
||||
if (state.filter.tag === value) value = '';
|
||||
} else if (key === 'timeRange') {
|
||||
if (state.filter.timeRange === value) value = '';
|
||||
} else if (key === 'contentType') {
|
||||
if (state.filter.contentType === value) value = '';
|
||||
}
|
||||
state.filter[key] = value;
|
||||
updateFilterUI();
|
||||
loadArticles(true);
|
||||
}
|
||||
|
||||
function setSearch(value) {
|
||||
state.filter.search = value;
|
||||
loadArticles(true);
|
||||
}
|
||||
|
||||
function updateFilterUI() {
|
||||
// 分类
|
||||
$$('.zs-sidebar .cat-item').forEach(function (el) {
|
||||
var cat = el.getAttribute('data-cat');
|
||||
el.classList.toggle('active', cat === state.filter.category);
|
||||
});
|
||||
// 标签
|
||||
$$('.zs-sidebar .tag-item').forEach(function (el) {
|
||||
var tag = el.getAttribute('data-tag');
|
||||
el.classList.toggle('active', tag === state.filter.tag);
|
||||
});
|
||||
// 时间
|
||||
$$('.zs-sidebar .time-item').forEach(function (el) {
|
||||
var tr = el.getAttribute('data-time');
|
||||
el.classList.toggle('active', tr === state.filter.timeRange);
|
||||
});
|
||||
// 图文类型
|
||||
$$('.zs-sidebar .type-btn').forEach(function (el) {
|
||||
var ct = el.getAttribute('data-type');
|
||||
el.classList.toggle('active', ct === state.filter.contentType);
|
||||
});
|
||||
// 筛选标签显示
|
||||
renderFilterTags();
|
||||
}
|
||||
|
||||
function renderFilterTags() {
|
||||
var container = $('#zs-filter-tags');
|
||||
if (!container) return;
|
||||
container.innerHTML = '';
|
||||
var items = [];
|
||||
if (state.filter.category) items.push({ label: state.filter.category, key: 'category' });
|
||||
if (state.filter.tag) items.push({ label: state.filter.tag, key: 'tag' });
|
||||
if (state.filter.timeRange) {
|
||||
var labels = { week: '本周', month: '本月', quarter: '本季度' };
|
||||
items.push({ label: labels[state.filter.timeRange] || state.filter.timeRange, key: 'timeRange' });
|
||||
}
|
||||
if (state.filter.contentType) {
|
||||
items.push({ label: state.filter.contentType === 'text' ? '纯文字' : '图文多图', key: 'contentType' });
|
||||
}
|
||||
items.forEach(function (item) {
|
||||
var span = document.createElement('span');
|
||||
span.className = 'filter-tag';
|
||||
span.textContent = item.label;
|
||||
var rm = document.createElement('button');
|
||||
rm.className = 'remove-filter';
|
||||
rm.textContent = '×';
|
||||
rm.addEventListener('click', function () { setFilter(item.key, ''); });
|
||||
span.appendChild(rm);
|
||||
container.appendChild(span);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 视图切换 ──
|
||||
function setViewMode(mode) {
|
||||
state.viewMode = mode;
|
||||
localStorage.setItem(STORAGE_VIEW, mode);
|
||||
var grid = $('#zs-card-grid');
|
||||
if (grid) grid.className = mode === 'list' ? 'card-grid list-view' : 'card-grid';
|
||||
$$('.zs-main .view-toggle button').forEach(function (btn) {
|
||||
btn.classList.toggle('active', btn.getAttribute('data-view') === mode);
|
||||
});
|
||||
}
|
||||
|
||||
// ── 加载右侧栏 ──
|
||||
async function loadRightbar() {
|
||||
// 置顶
|
||||
try {
|
||||
var pinned = await apiGet('/articles/pinned');
|
||||
var pinnedContainer = $('#zs-pinned');
|
||||
if (pinnedContainer && pinned.ok && pinned.data) {
|
||||
pinnedContainer.innerHTML = '';
|
||||
pinned.data.forEach(function (a) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'pinned-item';
|
||||
div.innerHTML = '<div class="pinned-title">' + escapeHtml(a.title) + '</div><div class="pinned-date">' + (a.created_at || '').slice(0, 10) + '</div>';
|
||||
div.addEventListener('click', function () { openDetail(a.id); });
|
||||
pinnedContainer.appendChild(div);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 最新
|
||||
try {
|
||||
var latest = await apiGet('/articles/latest');
|
||||
var latestContainer = $('#zs-latest');
|
||||
if (latestContainer && latest.ok && latest.data) {
|
||||
latestContainer.innerHTML = '';
|
||||
latest.data.forEach(function (a) {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'latest-item';
|
||||
div.textContent = a.title;
|
||||
div.addEventListener('click', function () { openDetail(a.id); });
|
||||
latestContainer.appendChild(div);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 统计数据
|
||||
try {
|
||||
var stats = await apiGet('/stats');
|
||||
if (stats.ok && stats.data) {
|
||||
var totalArticles = $('#zs-stat-articles');
|
||||
var totalImages = $('#zs-stat-images');
|
||||
if (totalArticles) totalArticles.textContent = stats.data.totalArticles || 0;
|
||||
if (totalImages) totalImages.textContent = stats.data.totalImages || 0;
|
||||
// 分类占比
|
||||
var chartContainer = $('#zs-chart');
|
||||
if (chartContainer && stats.data.catStats) {
|
||||
var maxCount = Math.max.apply(null, stats.data.catStats.map(function (c) { return c.c; }));
|
||||
chartContainer.innerHTML = '';
|
||||
stats.data.catStats.forEach(function (c) {
|
||||
var pct = maxCount > 0 ? Math.round((c.c / maxCount) * 100) : 0;
|
||||
var row = document.createElement('div');
|
||||
row.className = 'chart-bar';
|
||||
row.innerHTML = '<span class="bar-label">' + escapeHtml(c.category) + '</span>' +
|
||||
'<span class="bar-track"><span class="bar-fill" style="width:' + pct + '%"></span></span>' +
|
||||
'<span class="bar-count">' + c.c + '</span>';
|
||||
chartContainer.appendChild(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
||||
}
|
||||
|
||||
// ── 加载分类/标签 ──
|
||||
async function loadSidebarFilters() {
|
||||
// 分类
|
||||
try {
|
||||
var cats = await apiGet('/categories');
|
||||
var catContainer = $('#zs-categories');
|
||||
if (catContainer && cats.ok && cats.data) {
|
||||
catContainer.innerHTML = '';
|
||||
cats.data.forEach(function (c) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'cat-item';
|
||||
btn.setAttribute('data-cat', c.name);
|
||||
btn.textContent = c.name;
|
||||
btn.addEventListener('click', function () { setFilter('category', c.name); });
|
||||
catContainer.appendChild(btn);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
// 标签
|
||||
try {
|
||||
var tags = await apiGet('/tags');
|
||||
var tagContainer = $('#zs-tags');
|
||||
if (tagContainer && tags.ok && tags.data) {
|
||||
tagContainer.innerHTML = '';
|
||||
tags.data.forEach(function (t) {
|
||||
var btn = document.createElement('button');
|
||||
btn.className = 'tag-item';
|
||||
btn.setAttribute('data-tag', t.name);
|
||||
btn.textContent = t.name + '(' + (t.article_count || 0) + ')';
|
||||
btn.addEventListener('click', function () { setFilter('tag', t.name); });
|
||||
tagContainer.appendChild(btn);
|
||||
});
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── 加载收藏状态 ──
|
||||
async function loadFavorites() {
|
||||
try {
|
||||
var data = await apiGet('/favorites');
|
||||
if (data.ok && data.data) {
|
||||
state.favorites = new Set();
|
||||
data.data.forEach(function (a) { state.favorites.add(a.id); });
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// ── 初始化 ──
|
||||
function init() {
|
||||
initTheme();
|
||||
|
||||
// 视图切换
|
||||
setViewMode(state.viewMode);
|
||||
$$('.zs-main .view-toggle button').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { setViewMode(btn.getAttribute('data-view')); });
|
||||
});
|
||||
|
||||
// 分类筛选
|
||||
$$('.zs-sidebar .cat-item').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { setFilter('category', btn.getAttribute('data-cat')); });
|
||||
});
|
||||
|
||||
// 标签筛选
|
||||
// (动态生成的,在 loadSidebarFilters 中绑定)
|
||||
|
||||
// 时间筛选
|
||||
$$('.zs-sidebar .time-item').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { setFilter('timeRange', btn.getAttribute('data-time')); });
|
||||
});
|
||||
|
||||
// 图文类型筛选
|
||||
$$('.zs-sidebar .type-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () { setFilter('contentType', btn.getAttribute('data-type')); });
|
||||
});
|
||||
|
||||
// 搜索
|
||||
var searchInput = $('#zs-search');
|
||||
var searchBtn = $('#zs-search-btn');
|
||||
if (searchBtn && searchInput) {
|
||||
searchBtn.addEventListener('click', function () { setSearch(searchInput.value.trim()); });
|
||||
searchInput.addEventListener('keydown', function (e) { if (e.key === 'Enter') setSearch(searchInput.value.trim()); });
|
||||
}
|
||||
|
||||
// 滚动加载
|
||||
window.addEventListener('scroll', onScroll, { passive: true });
|
||||
|
||||
// 汉堡菜单
|
||||
var hamburger = $('#zs-hamburger');
|
||||
var sidebar = $('#zs-sidebar');
|
||||
var overlay = $('#zs-sidebar-overlay');
|
||||
if (hamburger && sidebar) {
|
||||
hamburger.addEventListener('click', function () {
|
||||
var open = sidebar.classList.contains('open');
|
||||
if (open) {
|
||||
sidebar.classList.remove('open');
|
||||
if (overlay) overlay.classList.remove('show');
|
||||
} else {
|
||||
sidebar.classList.add('open');
|
||||
if (overlay) overlay.classList.add('show');
|
||||
}
|
||||
});
|
||||
if (overlay) {
|
||||
overlay.addEventListener('click', function () {
|
||||
sidebar.classList.remove('open');
|
||||
overlay.classList.remove('show');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 提交存档按钮
|
||||
var submitBtn = $('#zs-submit-btn');
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', function () {
|
||||
window.open('/tools/zhizi_notes/admin.html', '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
// 初始加载
|
||||
loadSidebarFilters();
|
||||
loadFavorites().then(function () { loadArticles(true); });
|
||||
loadRightbar();
|
||||
}
|
||||
|
||||
// ── 启动 ──
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
})();
|
||||
Reference in New Issue
Block a user