Files
yangxiangyuan 421a8aa1f7 feat: 新增知资札记工具及相关功能
1. 新增知资札记完整站点:包含首页、详情页、管理后台
2. 添加站点图标、鉴权配置与后端API鉴权逻辑
3. 在市场页面新增知资札记入口跳转
4. 新增经典乡村音乐播放列表文件
5. 临时添加养生谚语批量导入脚本
2026-07-19 20:45:41 +08:00

594 lines
19 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 知资札记 - 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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// ── 加载分类/标签 ──
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);
})();