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();
|
|||
|
|
}
|
|||
|
|
})();
|