- 收录 32 篇经典神贴,覆盖经济金融/莲蓬鬼话/人文历史/小说连载/社会成长/玄学命理 - 支持搜索、板块筛选、热度/年代排序、详情弹窗、本地收藏 - 怀旧宣纸风设计(宋体 + 朱红印章),响应式布局 - 纯静态,无依赖,浏览器直接打开即可 - 含 README 与数据扩充说明
338 lines
10 KiB
JavaScript
338 lines
10 KiB
JavaScript
/**
|
||
* 天涯神贴 · 静态网站交互逻辑
|
||
* 依赖:data.js 中提供的 window.SHEN_TIE
|
||
*/
|
||
(function () {
|
||
'use strict';
|
||
|
||
var DATA = window.SHEN_TIE || [];
|
||
var STORAGE_KEY = 'tianya-favs';
|
||
|
||
/* ---------- 工具函数 ---------- */
|
||
function $(sel, ctx) {
|
||
return (ctx || document).querySelector(sel);
|
||
}
|
||
function $all(sel, ctx) {
|
||
return Array.prototype.slice.call((ctx || document).querySelectorAll(sel));
|
||
}
|
||
function escapeHtml(str) {
|
||
return String(str)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
function loadFavs() {
|
||
try {
|
||
return JSON.parse(localStorage.getItem(STORAGE_KEY)) || [];
|
||
} catch (e) {
|
||
return [];
|
||
}
|
||
}
|
||
function saveFavs(list) {
|
||
try {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify(list));
|
||
} catch (e) {
|
||
/* 隐私模式忽略 */
|
||
}
|
||
}
|
||
|
||
/* ---------- 全局状态 ---------- */
|
||
var state = {
|
||
section: '全部',
|
||
keyword: '',
|
||
sort: 'hot', // hot | year-asc | year-desc
|
||
favs: loadFavs(),
|
||
onlyFav: false,
|
||
};
|
||
|
||
/* ---------- 派生数据 ---------- */
|
||
function sections() {
|
||
var set = {};
|
||
DATA.forEach(function (t) {
|
||
set[t.section] = true;
|
||
});
|
||
return ['全部'].concat(Object.keys(set));
|
||
}
|
||
|
||
function filtered() {
|
||
var kw = state.keyword.trim().toLowerCase();
|
||
var list = DATA.filter(function (t) {
|
||
var okSection = state.section === '全部' || t.section === state.section;
|
||
var okFav = !state.onlyFav || state.favs.indexOf(t.id) > -1;
|
||
var okKw = true;
|
||
if (kw) {
|
||
var hay = [t.title, t.author, t.authorAlias, t.summary, t.section]
|
||
.concat(t.tags || [])
|
||
.join(' ')
|
||
.toLowerCase();
|
||
okKw = hay.indexOf(kw) > -1;
|
||
}
|
||
return okSection && okFav && okKw;
|
||
});
|
||
|
||
if (state.sort === 'hot') {
|
||
list.sort(function (a, b) {
|
||
return b.hot - a.hot;
|
||
});
|
||
} else if (state.sort === 'year-asc') {
|
||
list.sort(function (a, b) {
|
||
return a.year - b.year;
|
||
});
|
||
} else if (state.sort === 'year-desc') {
|
||
list.sort(function (a, b) {
|
||
return b.year - a.year;
|
||
});
|
||
}
|
||
return list;
|
||
}
|
||
|
||
/* ---------- 渲染:分类标签 ---------- */
|
||
function renderSections() {
|
||
var box = $('#sections');
|
||
box.innerHTML = sections()
|
||
.map(function (s) {
|
||
var active = s === state.section ? ' active' : '';
|
||
return (
|
||
'<button class="tag' + active + '" data-section="' + escapeHtml(s) + '">' +
|
||
escapeHtml(s) + '</button>'
|
||
);
|
||
})
|
||
.join('');
|
||
}
|
||
|
||
/* ---------- 渲染:卡片列表 ---------- */
|
||
function renderCards() {
|
||
var box = $('#cards');
|
||
var list = filtered();
|
||
|
||
if (!list.length) {
|
||
box.innerHTML =
|
||
'<div class="empty">没有找到相关神贴,换个关键词或分类试试。</div>';
|
||
$('#count').textContent = '0 篇';
|
||
return;
|
||
}
|
||
|
||
var html = list
|
||
.map(function (t) {
|
||
var isFav = state.favs.indexOf(t.id) > -1;
|
||
var tagsHtml = (t.tags || [])
|
||
.map(function (tag) {
|
||
return '<span class="chip">#' + escapeHtml(tag) + '</span>';
|
||
})
|
||
.join('');
|
||
var verifyBadge = t.verified
|
||
? '<span class="verified" title="网友整理认为预测大体应验">已应验</span>'
|
||
: '';
|
||
return (
|
||
'<article class="card" data-id="' + escapeHtml(t.id) + '">' +
|
||
'<div class="card-head">' +
|
||
'<span class="section-badge">' + escapeHtml(t.section) + '</span>' +
|
||
'<span class="year">' + escapeHtml(String(t.year)) + '</span>' +
|
||
'</div>' +
|
||
'<h3 class="title">' + escapeHtml(t.title) + '</h3>' +
|
||
'<div class="author">' +
|
||
'<span class="avatar">' + escapeHtml((t.authorAlias || t.author).slice(0, 1)) + '</span>' +
|
||
'<span class="name">' + escapeHtml(t.authorAlias || t.author) + '</span>' +
|
||
'<span class="dim">@' + escapeHtml(t.author) + '</span>' +
|
||
verifyBadge +
|
||
'</div>' +
|
||
'<p class="summary">' + escapeHtml(t.summary) + '</p>' +
|
||
'<div class="tagline">' + tagsHtml + '</div>' +
|
||
'<div class="card-foot">' +
|
||
'<span class="hot" title="热度">🔥 ' + escapeHtml(String(t.hot)) + '</span>' +
|
||
'<button class="fav-btn ' + (isFav ? 'on' : '') + '" data-fav="' + escapeHtml(t.id) + '">' +
|
||
(isFav ? '★ 已收藏' : '☆ 收藏') +
|
||
'</button>' +
|
||
'<span class="read-more">展开阅读 →</span>' +
|
||
'</div>' +
|
||
'</article>'
|
||
);
|
||
})
|
||
.join('');
|
||
|
||
box.innerHTML = html;
|
||
$('#count').textContent = list.length + ' 篇';
|
||
}
|
||
|
||
/* ---------- 渲染:详情弹窗 ---------- */
|
||
function openDetail(id) {
|
||
var t = DATA.filter(function (x) {
|
||
return x.id === id;
|
||
})[0];
|
||
if (!t) return;
|
||
|
||
var paras = (t.content || [])
|
||
.map(function (p) {
|
||
return '<p>' + escapeHtml(p) + '</p>';
|
||
})
|
||
.join('');
|
||
|
||
var tagsHtml = (t.tags || [])
|
||
.map(function (tag) {
|
||
return '<span class="chip">#' + escapeHtml(tag) + '</span>';
|
||
})
|
||
.join('');
|
||
|
||
var isFav = state.favs.indexOf(t.id) > -1;
|
||
|
||
var html =
|
||
'<div class="modal-mask" id="modalMask">' +
|
||
'<div class="modal" role="dialog" aria-modal="true">' +
|
||
'<button class="modal-close" id="modalClose" aria-label="关闭">×</button>' +
|
||
'<div class="modal-head">' +
|
||
'<span class="section-badge">' + escapeHtml(t.section) + '</span>' +
|
||
'<span class="year">' + escapeHtml(String(t.year)) + '</span>' +
|
||
(t.verified ? '<span class="verified">已应验</span>' : '') +
|
||
'</div>' +
|
||
'<h2 class="modal-title">' + escapeHtml(t.title) + '</h2>' +
|
||
'<div class="author">' +
|
||
'<span class="avatar lg">' + escapeHtml((t.authorAlias || t.author).slice(0, 1)) + '</span>' +
|
||
'<span class="name">' + escapeHtml(t.authorAlias || t.author) + '</span>' +
|
||
'<span class="dim">@' + escapeHtml(t.author) + '</span>' +
|
||
'<span class="hot">🔥 ' + escapeHtml(String(t.hot)) + '</span>' +
|
||
'</div>' +
|
||
'<p class="modal-summary">' + escapeHtml(t.summary) + '</p>' +
|
||
'<div class="tagline">' + tagsHtml + '</div>' +
|
||
'<div class="quotes">' + paras + '</div>' +
|
||
'<div class="modal-foot">' +
|
||
'<button class="fav-btn ' + (isFav ? 'on' : '') + '" data-fav="' + escapeHtml(t.id) + '">' +
|
||
(isFav ? '★ 已收藏' : '☆ 收藏') +
|
||
'</button>' +
|
||
'<a class="source-link" href="' + escapeHtml(t.source || '#') + '" target="_blank" rel="noopener noreferrer">' +
|
||
'查看原始出处 ↗' +
|
||
'</a>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
|
||
var old = $('#modalMask');
|
||
if (old) old.remove();
|
||
document.body.insertAdjacentHTML('beforeend', html);
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeDetail() {
|
||
var mask = $('#modalMask');
|
||
if (mask) mask.remove();
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
/* ---------- 收藏切换 ---------- */
|
||
function toggleFav(id) {
|
||
var idx = state.favs.indexOf(id);
|
||
if (idx > -1) {
|
||
state.favs.splice(idx, 1);
|
||
} else {
|
||
state.favs.push(id);
|
||
}
|
||
saveFavs(state.favs);
|
||
updateFavCount();
|
||
renderCards();
|
||
// 弹窗内的按钮也同步
|
||
var modalBtn = $('#modalMask .fav-btn');
|
||
if (modalBtn) {
|
||
var on = state.favs.indexOf(id) > -1;
|
||
modalBtn.classList.toggle('on', on);
|
||
modalBtn.textContent = on ? '★ 已收藏' : '☆ 收藏';
|
||
}
|
||
}
|
||
|
||
function updateFavCount() {
|
||
var el = $('#favCount');
|
||
if (el) el.textContent = state.favs.length;
|
||
}
|
||
|
||
/* ---------- 事件绑定 ---------- */
|
||
function bindEvents() {
|
||
// 分类
|
||
$('#sections').addEventListener('click', function (e) {
|
||
var btn = e.target.closest('[data-section]');
|
||
if (!btn) return;
|
||
state.section = btn.getAttribute('data-section');
|
||
renderSections();
|
||
renderCards();
|
||
});
|
||
|
||
// 搜索
|
||
var searchInput = $('#search');
|
||
var timer = null;
|
||
searchInput.addEventListener('input', function (e) {
|
||
clearTimeout(timer);
|
||
var val = e.target.value;
|
||
timer = setTimeout(function () {
|
||
state.keyword = val;
|
||
renderCards();
|
||
}, 150);
|
||
});
|
||
|
||
// 排序
|
||
$('#sort').addEventListener('change', function (e) {
|
||
state.sort = e.target.value;
|
||
renderCards();
|
||
});
|
||
|
||
// 只看收藏
|
||
$('#onlyFav').addEventListener('click', function (e) {
|
||
state.onlyFav = !state.onlyFav;
|
||
e.currentTarget.classList.toggle('on', state.onlyFav);
|
||
renderCards();
|
||
});
|
||
|
||
// 卡片区域:点击卡片展开 / 收藏
|
||
$('#cards').addEventListener('click', function (e) {
|
||
var favBtn = e.target.closest('[data-fav]');
|
||
if (favBtn) {
|
||
e.stopPropagation();
|
||
toggleFav(favBtn.getAttribute('data-fav'));
|
||
return;
|
||
}
|
||
var card = e.target.closest('.card');
|
||
if (card) {
|
||
openDetail(card.getAttribute('data-id'));
|
||
}
|
||
});
|
||
|
||
// 弹窗关闭(事件委托,因弹窗是后插入的)
|
||
document.addEventListener('click', function (e) {
|
||
if (e.target.id === 'modalMask' || e.target.id === 'modalClose') {
|
||
closeDetail();
|
||
return;
|
||
}
|
||
var favBtn = e.target.closest('#modalMask .fav-btn');
|
||
if (favBtn) {
|
||
toggleFav(favBtn.getAttribute('data-fav'));
|
||
}
|
||
});
|
||
|
||
// ESC 关闭弹窗
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') closeDetail();
|
||
});
|
||
|
||
// 返回顶部
|
||
$('#backTop').addEventListener('click', function () {
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
});
|
||
window.addEventListener('scroll', function () {
|
||
var btn = $('#backTop');
|
||
btn.classList.toggle('show', window.scrollY > 400);
|
||
});
|
||
}
|
||
|
||
/* ---------- 启动 ---------- */
|
||
function init() {
|
||
renderSections();
|
||
renderCards();
|
||
updateFavCount();
|
||
bindEvents();
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|