/** * 知资札记 - 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>/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 = '
' + escapeHtml(a.title) + '
' + (a.created_at || '').slice(0, 10) + '
'; 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 = '' + escapeHtml(c.category) + '' + '' + '' + c.c + ''; chartContainer.appendChild(row); }); } } } catch (e) {} } function escapeHtml(str) { if (!str) return ''; return String(str).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); })();