421a8aa1f7
1. 新增知资札记完整站点:包含首页、详情页、管理后台 2. 添加站点图标、鉴权配置与后端API鉴权逻辑 3. 在市场页面新增知资札记入口跳转 4. 新增经典乡村音乐播放列表文件 5. 临时添加养生谚语批量导入脚本
594 lines
19 KiB
JavaScript
594 lines
19 KiB
JavaScript
/**
|
||
* 知资札记 - 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);
|
||
})(); |