tianya-shentie/app.js

338 lines
10 KiB
JavaScript
Raw Permalink Normal View History

/**
* 天涯神贴 · 静态网站交互逻辑
* 依赖: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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
}
})();