Files
halo-theme-pix/embed-code/page/publication-calendar.html
T
2026-07-30 19:27:07 +08:00

1194 lines
36 KiB
HTML
Raw 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.
<!--
发布日历:直接粘贴到 Halo 自定义页面即可。
API: /apis/api.publication-calendar.anian.dev/v1alpha1/publications
API 返回的相对链接会自动保留为 PJAX 链接。
-->
<section class="publication-calendar" id="publication-calendar" aria-label="发布日历">
<style>
#publication-calendar,
#publication-calendar * {
box-sizing: border-box;
}
#publication-calendar {
--pc-theme: #22bb6d;
--pc-theme-strong: #00bb5e;
--pc-ink: #294338;
--pc-muted: #789083;
--pc-line: #dcebe1;
--pc-soft: #e5ede8;
--pc-page: #f6faf8;
--pc-empty: #edf4ef;
--pc-shadow: 0 14px 35px rgba(63, 117, 84, .08);
width: 100%;
max-width: 1180px;
margin: 30px auto 0;
color: var(--pc-ink);
font-family: inherit;
line-height: 1.6;
}
#publication-calendar button,
#publication-calendar input,
#publication-calendar select {
font: inherit;
}
#publication-calendar button,
#publication-calendar a {
-webkit-tap-highlight-color: transparent;
}
.pub-summary,
.pub-calendar-card,
.pub-details {
background: var(--pc-page);
border: 1px solid rgba(34, 187, 109, .12);
border-radius: 14px;
box-shadow: var(--pc-shadow);
}
.pub-summary {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 24px 28px;
margin-bottom: 14px;
}
.pub-eyebrow {
margin: 0 0 4px;
color: var(--pc-muted);
font-size: 12px;
letter-spacing: .12em;
}
.pub-summary h1 {
margin: 0;
color: var(--pc-ink);
font-size: clamp(24px, 3vw, 34px);
font-weight: 700;
line-height: 1.2;
}
.pub-summary h1 span {
color: var(--pc-theme-strong);
font-variant-numeric: tabular-nums;
}
.pub-summary__breakdown {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 7px 12px;
max-width: 570px;
color: var(--pc-muted);
font-size: 12px;
}
.pub-summary__breakdown span {
white-space: nowrap;
}
.pub-summary__breakdown b {
color: var(--pc-theme-strong);
font-variant-numeric: tabular-nums;
}
.pub-calendar-card {
padding: 22px 24px 24px;
margin-bottom: 16px;
}
.pub-toolbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.pub-month-switch,
.pub-category-switch {
display: flex;
align-items: center;
gap: 8px;
}
.pub-month-switch {
min-width: 150px;
height: 36px;
justify-content: space-between;
border: 1px solid var(--pc-line);
border-radius: 9px;
background: #fff;
}
.pub-month-switch button {
display: inline-grid;
width: 32px;
height: 100%;
padding: 0;
place-items: center;
border: 0;
background: transparent;
color: var(--pc-theme-strong);
cursor: pointer;
font-size: 22px;
line-height: 1;
transition: .2s ease;
}
.pub-month-switch button:hover,
.pub-month-switch button:focus-visible {
border-radius: 7px;
background: #ecfbf2;
outline: none;
}
.pub-month-label {
flex: 1;
color: var(--pc-ink);
font-size: 13px;
font-weight: 700;
text-align: center;
white-space: nowrap;
}
.pub-category-picker {
position: relative;
min-width: 124px;
}
.pub-category-trigger {
display: flex;
position: relative;
width: 100%;
height: 36px;
align-items: center;
justify-content: center;
padding: 0 32px 0 12px;
border: 1px solid var(--pc-line);
border-radius: 9px;
background: #fff;
color: var(--pc-theme-strong);
cursor: pointer;
font-size: 13px;
font-weight: 600;
outline: none;
transition: border-color .2s ease, box-shadow .2s ease;
}
.pub-category-trigger:hover,
.pub-category-trigger:focus-visible,
.pub-category-trigger[aria-expanded="true"] {
border-color: var(--pc-theme);
box-shadow: 0 0 0 3px rgba(34, 187, 109, .12);
outline: none;
}
.pub-category-arrow {
position: absolute;
right: 11px;
color: var(--pc-theme);
font-size: 16px;
line-height: 1;
transform: rotate(180deg);
transition: transform .2s ease;
}
.pub-category-trigger[aria-expanded="true"] .pub-category-arrow {
transform: none;
}
.pub-category-menu {
position: absolute;
z-index: 20;
top: calc(100% + 6px);
right: 0;
min-width: 100%;
padding: 5px;
border: 1px solid var(--pc-line);
border-radius: 10px;
background: #fff;
box-shadow: 0 12px 28px rgba(63, 117, 84, .16);
}
.pub-category-menu[hidden] {
display: none;
}
.pub-category-option {
display: block;
width: 100%;
padding: 7px 9px;
border: 0;
border-radius: 7px;
background: transparent;
color: var(--pc-ink);
cursor: pointer;
font-size: 12px;
text-align: center;
white-space: nowrap;
}
.pub-category-option:hover,
.pub-category-option:focus-visible,
.pub-category-option[aria-selected="true"] {
background: #ecfbf2;
color: var(--pc-theme-strong);
outline: none;
}
.pub-calendar-wrap {
display: block;
overflow: visible;
padding-bottom: 2px;
}
.pub-weekday-row,
.pub-calendar-grid {
display: grid;
grid-template-columns: repeat(7, minmax(18px, 1fr));
gap: 6px;
}
.pub-weekday-row {
color: var(--pc-muted);
font-size: 10px;
text-align: center;
margin-bottom: 6px;
}
.pub-weekday-row span {
min-width: 18px;
}
.pub-calendar-grid {
grid-auto-rows: minmax(22px, 1fr);
}
.pub-calendar-grid.is-loading {
display: flex;
min-height: 142px;
align-items: center;
justify-content: center;
}
.pub-calendar-grid.is-loading .pub-loading {
padding: 20px 0;
white-space: nowrap;
}
.pub-day.is-blank {
pointer-events: none;
}
.pub-day {
position: relative;
min-width: 18px;
min-height: 22px;
padding: 0;
border: 0;
border-radius: 5px;
background: var(--pc-empty);
cursor: pointer;
transition: transform .18s ease, box-shadow .18s ease, outline .18s ease;
}
.pub-day[data-level="1"] {
background: #ccefd9;
}
.pub-day[data-level="2"] {
background: #8bddad;
}
.pub-day[data-level="3"] {
background: #42ca7d;
}
.pub-day[data-level="4"] {
background: var(--pc-theme-strong);
}
.pub-day:hover,
.pub-day:focus-visible {
z-index: 4;
outline: 2px solid var(--pc-theme-strong);
outline-offset: 2px;
box-shadow: 0 4px 12px rgba(0, 187, 94, .22);
transform: translateY(-1px);
}
.pub-day.is-selected {
outline: 2px solid var(--pc-theme-strong);
outline-offset: 2px;
}
.pub-day__number {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: rgba(41, 67, 56, .72);
font-size: 10px;
pointer-events: none;
}
.pub-day[data-level="3"] .pub-day__number,
.pub-day[data-level="4"] .pub-day__number {
color: #fff;
}
.pub-day__tooltip {
position: absolute;
z-index: 10;
bottom: calc(100% + 9px);
left: 50%;
display: none;
width: max-content;
max-width: 190px;
padding: 8px 10px;
border: 1px solid rgba(34, 187, 109, .24);
border-radius: 8px;
background: #294338;
box-shadow: 0 8px 18px rgba(41, 67, 56, .16);
color: #fff;
font-size: 11px;
line-height: 1.7;
opacity: 0;
pointer-events: none;
transform: translate(-50%, 4px);
transition: opacity .16s ease, transform .16s ease;
}
.pub-day__tooltip::after {
position: absolute;
bottom: -5px;
left: 50%;
width: 9px;
height: 9px;
background: #294338;
content: "";
transform: translateX(-50%) rotate(45deg);
}
.pub-day:hover .pub-day__tooltip,
.pub-day:focus-visible .pub-day__tooltip {
display: block;
opacity: 1;
transform: translate(-50%, 0);
}
.pub-day.is-edge-start .pub-day__tooltip {
left: 0;
transform: translate(0, 4px);
}
.pub-day.is-edge-end .pub-day__tooltip {
left: 100%;
transform: translate(-100%, 4px);
}
.pub-day.is-top-row .pub-day__tooltip {
top: calc(100% + 9px);
bottom: auto;
}
.pub-day:hover.is-edge-start .pub-day__tooltip,
.pub-day:focus-visible.is-edge-start .pub-day__tooltip {
transform: translate(0, 0);
}
.pub-day:hover.is-edge-end .pub-day__tooltip,
.pub-day:focus-visible.is-edge-end .pub-day__tooltip {
transform: translate(-100%, 0);
}
.pub-day.is-edge-start .pub-day__tooltip::after {
left: 14px;
transform: rotate(45deg);
}
.pub-day.is-edge-end .pub-day__tooltip::after {
left: calc(100% - 14px);
transform: rotate(45deg);
}
.pub-day.is-top-row .pub-day__tooltip::after {
top: -5px;
bottom: auto;
}
.pub-tooltip-line {
position: relative;
z-index: 1;
display: flex;
justify-content: space-between;
gap: 16px;
}
.pub-tooltip-line b {
color: #9af2bd;
font-variant-numeric: tabular-nums;
}
.pub-calendar-legend {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 12px;
color: var(--pc-muted);
font-size: 10px;
}
.pub-calendar-legend i {
display: inline-block;
width: 13px;
height: 13px;
border-radius: 3px;
}
.pub-calendar-legend i:nth-of-type(1) { background: var(--pc-empty); }
.pub-calendar-legend i:nth-of-type(2) { background: #ccefd9; }
.pub-calendar-legend i:nth-of-type(3) { background: #8bddad; }
.pub-calendar-legend i:nth-of-type(4) { background: #42ca7d; }
.pub-calendar-legend i:nth-of-type(5) { background: var(--pc-theme-strong); }
.pub-details {
padding: 22px 24px 26px;
}
.pub-details__head {
display: flex;
position: relative;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 26px;
padding-right: 52px;
margin-bottom: 18px;
}
.pub-details__head > div {
min-width: 0;
}
.pub-details__head h2 {
margin: 0;
color: var(--pc-ink);
font-size: 20px;
line-height: 1.3;
}
.pub-view-all {
display: inline-flex;
position: absolute;
top: 50%;
right: 0;
z-index: 1;
width: auto;
height: 26px;
min-height: 26px;
align-items: center;
justify-content: center;
margin: 0;
padding: 0 8px;
border: 1px solid var(--pc-line);
border-radius: 6px;
background: #fff;
color: var(--pc-theme-strong);
cursor: pointer;
line-height: 1 !important;
transform: translateY(-50%);
}
.pub-view-all:hover,
.pub-view-all:focus-visible {
border-color: var(--pc-theme);
background: #ecfbf2;
outline: none;
}
.pub-sections {
display: grid;
grid-template-columns: minmax(0, 1fr);
gap: 14px;
}
.pub-section {
min-width: 0;
overflow: hidden;
border: 1px solid var(--pc-line);
border-radius: 11px;
background: rgba(255, 255, 255, .62);
}
.pub-section__title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 12px 14px;
border-bottom: 1px solid var(--pc-line);
background: rgba(229, 237, 232, .6);
color: var(--pc-ink);
font-size: 14px;
font-weight: 700;
}
.pub-section__title small {
color: var(--pc-theme-strong);
font-size: 13px;
font-weight: 600;
}
.pub-section__body {
max-height: 300px;
overflow-y: auto;
padding: 5px 14px 9px;
scrollbar-color: #9ad9b4 transparent;
scrollbar-width: thin;
}
.pub-item {
display: block;
padding: 9px 0;
border-bottom: 1px dashed var(--pc-line);
color: var(--pc-theme-strong) !important;
font-size: 13px;
text-decoration: none !important;
transition: color .18s ease, padding-left .18s ease;
}
a.pub-item,
a.pub-item:visited {
color: var(--pc-theme-strong) !important;
text-decoration: none !important;
}
.pub-item:last-child {
border-bottom: 0;
}
a.pub-item:hover,
a.pub-item:focus-visible {
padding-left: 4px;
color: var(--pc-theme-strong) !important;
outline: none;
}
.pub-item__text {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pub-item__author {
display: inline;
margin: 0;
color: var(--pc-muted);
font-size: inherit;
line-height: inherit;
}
.pub-item__meta {
display: block;
margin-top: 2px;
color: var(--pc-muted);
font-size: 10px;
}
.pub-empty {
padding: 20px 0;
color: var(--pc-muted);
font-size: 12px;
text-align: center;
}
.pub-loading {
padding: 40px 16px;
color: var(--pc-muted);
text-align: center;
}
.pub-loading.is-error {
color: #a24d51;
}
@media only screen and (min-width: 1366px) {
.pub-sections {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media only screen and (max-width: 600px) {
.pub-summary,
.pub-calendar-card,
.pub-details {
border-radius: 11px;
}
.pub-summary {
align-items: flex-start;
flex-direction: column;
gap: 12px;
padding: 18px;
}
.pub-summary__breakdown {
justify-content: flex-start;
gap: 5px 10px;
}
.pub-calendar-card,
.pub-details {
padding: 16px;
}
.pub-toolbar {
flex-wrap: wrap;
gap: 8px;
margin-bottom: 15px;
}
#publication-calendar {
margin-top: 38px;
}
.pub-month-switch {
flex: 1 1 140px;
min-width: 140px;
}
.pub-category-picker {
min-width: 112px;
}
.pub-calendar-grid,
.pub-weekday-row {
gap: 4px;
}
.pub-details__head {
align-items: flex-start;
}
.pub-details__head h2 {
font-size: 18px;
}
.pub-view-all {
padding: 0 8px;
font-size: 12px !important;
}
.pub-section__body {
max-height: 250px;
}
}
@media (prefers-reduced-motion: reduce) {
#publication-calendar *,
#publication-calendar *::before,
#publication-calendar *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
}
}
</style>
<div class="pub-summary">
<div>
<p class="pub-eyebrow">PUBLICATION CALENDAR</p>
<h1>本月总览 <span id="pub-total"></span></h1>
</div>
<div class="pub-summary__breakdown" id="pub-breakdown" aria-label="分类统计"></div>
</div>
<div class="pub-calendar-card">
<div class="pub-toolbar">
<div class="pub-month-switch" aria-label="切换月份">
<button type="button" id="pub-prev-month" title="上个月" aria-label="上个月"></button>
<span class="pub-month-label" id="pub-month-label" aria-live="polite"></span>
<button type="button" id="pub-next-month" title="下个月" aria-label="下个月"></button>
</div>
<div class="pub-category-switch">
<div class="pub-category-picker" id="pub-category-picker">
<button class="pub-category-trigger" id="pub-category-trigger" type="button"
aria-haspopup="listbox" aria-expanded="false" aria-label="选择贡献类别">
<span id="pub-category-label">全部类别</span>
<i class="pub-category-arrow ri-arrow-up-s-line" aria-hidden="true"></i>
</button>
<div class="pub-category-menu" id="pub-category-menu" role="listbox" hidden></div>
</div>
</div>
</div>
<div class="pub-calendar-wrap">
<div class="pub-weekday-row" aria-hidden="true">
<span></span><span></span><span></span><span></span><span></span><span></span><span></span>
</div>
<div class="pub-calendar-grid" id="pub-calendar-grid" aria-live="polite"></div>
</div>
<div class="pub-calendar-legend" aria-label="数量等级">
<i></i><i></i><i></i><i></i><i></i>
</div>
</div>
<div class="pub-details">
<div class="pub-details__head">
<div>
<h2 id="pub-details-title">本月发布</h2>
</div>
<button class="pub-view-all" id="pub-view-all" type="button" hidden>全部</button>
</div>
<div class="pub-sections" id="pub-sections"></div>
</div>
<script>
(function () {
'use strict';
function initPublicationCalendar() {
var root = document.getElementById('publication-calendar');
if (!root || root.dataset.mounted === 'true') return;
root.dataset.mounted = 'true';
var API = '/apis/api.publication-calendar.anian.dev/v1alpha1/publications';
var TIMEZONE = (window.Intl && Intl.DateTimeFormat().resolvedOptions().timeZone) || 'Asia/Shanghai';
var categories = [
{ key: 'moments', label: '日记' },
{ key: 'posts', label: '作文' },
{ key: 'friends', label: '朋友圈' },
{ key: 'photos', label: '图库' },
{ key: 'douban', label: '课外书' },
{ key: 'equipments', label: '奇遇' }
];
var monthLabelElement = root.querySelector('#pub-month-label');
var categoryTrigger = root.querySelector('#pub-category-trigger');
var categoryLabelElement = root.querySelector('#pub-category-label');
var categoryPicker = root.querySelector('#pub-category-picker');
var categoryMenu = root.querySelector('#pub-category-menu');
var grid = root.querySelector('#pub-calendar-grid');
var sections = root.querySelector('#pub-sections');
var total = root.querySelector('#pub-total');
var breakdown = root.querySelector('#pub-breakdown');
var detailsTitle = root.querySelector('#pub-details-title');
var viewAll = root.querySelector('#pub-view-all');
var currentData = null;
var selectedDay = null;
var requestId = 0;
var currentMonthValue = '';
var selectedCategory = 'all';
[{ key: 'all', label: '全部类别' }].concat(categories).forEach(function (category) {
var option = document.createElement('button');
option.type = 'button';
option.className = 'pub-category-option';
option.setAttribute('role', 'option');
option.setAttribute('aria-selected', category.key === 'all' ? 'true' : 'false');
option.dataset.category = category.key;
option.textContent = category.label;
categoryMenu.appendChild(option);
});
function pad(number) {
return String(number).padStart(2, '0');
}
function currentMonth() {
var now = new Date();
return now.getFullYear() + '-' + pad(now.getMonth() + 1);
}
function monthLabel(month) {
var parts = month.split('-');
return Number(parts[0]) + '年' + Number(parts[1]) + '月';
}
function monthShortLabel(month) {
var parts = month.split('-');
return Number(parts[0]) + '年' + Number(parts[1]) + '月';
}
function daysInMonth(month) {
var parts = month.split('-');
return new Date(Number(parts[0]), Number(parts[1]), 0).getDate();
}
function firstWeekday(month) {
var parts = month.split('-');
return new Date(Number(parts[0]), Number(parts[1]) - 1, 1).getDay();
}
function escapeHtml(value) {
var node = document.createElement('div');
node.textContent = value == null ? '' : String(value);
return node.innerHTML;
}
function text(value, fallback) {
var result = value == null ? '' : String(value).trim();
return result || fallback || '';
}
function listText(value) {
if (Array.isArray(value)) {
return value.map(function (entry) {
return text(entry, '');
}).filter(Boolean).join('、');
}
return text(value, '');
}
function cleanMomentContent(value) {
var result = text(value, '');
// 兼容 API 已解析和仍保留字面量 \\u003C 的两种情况。
result = result.replace(/\\u003c/gi, '<').replace(/\\u003e/gi, '>');
result = result.split(/<p\b/i)[0];
result = result.replace(/\\n/g, '\n').replace(/\r?\n+/g, ' ');
var node = document.createElement('div');
node.innerHTML = result;
return (node.textContent || node.innerText || result).replace(/\s+/g, ' ').trim();
}
function getItemsForDay(day) {
var item = (currentData && currentData.items || []).find(function (entry) {
return Number(entry.day) === day;
});
return item || categories.reduce(function (result, category) {
result[category.key] = [];
return result;
}, { day: day });
}
function countItems(dayItems, key) {
return Array.isArray(dayItems && dayItems[key]) ? dayItems[key].length : 0;
}
function countForDay(day, key) {
var daily = currentData && currentData.dailyCounts && currentData.dailyCounts[key];
if (Array.isArray(daily) && daily[day - 1] != null) return Number(daily[day - 1]) || 0;
return countItems(getItemsForDay(day), key);
}
function countAllForDay(day) {
return categories.reduce(function (sum, category) {
return sum + countForDay(day, category.key);
}, 0);
}
function getDayCount(day) {
var key = selectedCategory;
return key === 'all' ? countAllForDay(day) : countForDay(day, key);
}
function levelForCount(value, max) {
if (!value) return 0;
if (max <= 1) return 4;
var ratio = value / max;
if (ratio <= .25) return 1;
if (ratio <= .5) return 2;
if (ratio <= .75) return 3;
return 4;
}
function safeLink(link) {
if (!link) return null;
try {
var url = new URL(String(link), window.location.origin);
if (url.protocol !== 'http:' && url.protocol !== 'https:') return null;
return url.href;
} catch (error) {
return null;
}
}
// 统一识别日记媒体类型,兼容 type、originType 和常见文件后缀。
function mediaKind(media) {
var rawTypeValue = media && media.type && typeof media.type === 'object' ? media.type.name : media && media.type;
var rawType = String(rawTypeValue || '').toUpperCase();
var originType = String(media && media.originType || '').toUpperCase();
var url = String(media && media.url || '').split('?')[0].toLowerCase();
var typeText = rawType + ' ' + originType;
if (typeText.indexOf('VIDEO') !== -1 || /\.(mp4|webm|ogv|mov|m4v)$/.test(url)) return 'video';
if (typeText.indexOf('AUDIO') !== -1 || /\.(mp3|wav|ogg|m4a|flac|aac)$/.test(url)) return 'audio';
if (rawType.indexOf('POST') !== -1 || (!rawType && (originType.indexOf('POST') !== -1 || originType === 'PAGE'))) return 'post';
if (typeText.indexOf('PHOTO') !== -1 || typeText.indexOf('IMAGE') !== -1) return 'photo';
return 'photo';
}
// 汇总媒体数量,避免 AUDIO/POST 被原有兜底逻辑误统计为图片。
function countMediaTypes(mediaItems) {
return mediaItems.reduce(function (result, media) {
var kind = mediaKind(media);
if (kind === 'video') result.videos += 1;
if (kind === 'audio') result.audios += 1;
if (kind === 'post') result.posts += 1;
if (kind === 'photo') result.photos += 1;
return result;
}, { photos: 0, videos: 0, audios: 0, posts: 0 });
}
function itemText(item, key) {
if (key === 'friends') {
return text(item.title || '无标题');
}
if (key === 'moments') {
return cleanMomentContent(item.title) || '无内容';
}
if (key === 'posts') return text(item.title, text(item.name, '无标题'));
if (key === 'photos') return text(item.title, text(item.name, '图库记录'));
if (key === 'douban') return text(item.title, text(item.name, '课外书记录'));
if (key === 'equipments') return text(item.title, text(item.name, '奇遇记录'));
return text(item.title, text(item.name, '发布记录'));
}
function itemMeta(item, key, day) {
var rawDate = text(item.date, currentData.month + '-' + pad(day));
var date = rawDate.slice(0, 10); // 只保留前10字符 YYYY-MM-DD
var media = '';
var itemCategories = listText(item.categories);
var itemTags = listText(item.tags);
var metaParts = [date];
if (key === 'photos' || key === 'douban' || key === 'equipments' || key === 'posts') {
if (itemCategories) metaParts.push(itemCategories);
}
if (key === 'moments' || key === 'posts') {
if (itemTags) metaParts.push(itemTags);
}
if ((key === 'photos' || key === 'friends' || key === 'moments') && Array.isArray(item.media) && item.media.length) {
var mediaCounts = countMediaTypes(item.media);
var mediaLabels = [];
// 图库列表保持原有短文案,日记/朋友圈展示具体数量。
if (mediaCounts.photos) mediaLabels.push(key === 'photos' ? '图片' : mediaCounts.photos + '张图片');
if (mediaCounts.videos) mediaLabels.push(key === 'photos' ? '视频' : mediaCounts.videos + '个视频');
if (mediaCounts.audios) mediaLabels.push(key === 'photos' ? '音频' : mediaCounts.audios + '首歌曲');
if (mediaCounts.posts) mediaLabels.push(key === 'photos' ? '卡片' : mediaCounts.posts + '张卡片');
media = mediaLabels.join(' · ');
}
return { parts: metaParts, media: media };
}
function sortEntries(entries) {
return entries.sort(function (left, right) {
var leftTime = Date.parse(left.item.date || '') || 0;
var rightTime = Date.parse(right.item.date || '') || 0;
return rightTime - leftTime || right.day - left.day;
});
}
function renderSummary() {
var fallbackTotal = categories.reduce(function (sum, category) {
return sum + Number(currentData.counts && currentData.counts[category.key] || 0);
}, 0);
total.textContent = Number.isFinite(Number(currentData.total)) ? Number(currentData.total) : fallbackTotal;
breakdown.innerHTML = categories.map(function (category) {
var value = Number(currentData.counts && currentData.counts[category.key] || 0);
return '<span>' + escapeHtml(category.label) + ' <b>' + value + '</b></span>';
}).join('');
}
function tooltipHtml(day) {
return categories.map(function (category) {
return '<div class="pub-tooltip-line"><span>' + escapeHtml(category.label) + '</span><b>' + countForDay(day, category.key) + '</b></div>';
}).join('');
}
function renderCalendar() {
grid.classList.remove('is-loading');
var totalDays = daysInMonth(currentData.month);
var offset = firstWeekday(currentData.month);
var values = [];
for (var day = 1; day <= totalDays; day += 1) values.push(getDayCount(day));
var max = Math.max.apply(null, values.concat([1]));
var html = '';
for (var blank = 0; blank < offset; blank += 1) {
html += '<span class="pub-day is-blank" aria-hidden="true"></span>';
}
for (var currentDay = 1; currentDay <= totalDays; currentDay += 1) {
var selected = selectedDay === currentDay ? ' is-selected' : '';
var cellIndex = offset + currentDay - 1;
var column = cellIndex % 7;
var row = Math.floor(cellIndex / 7);
var position = (column === 0 ? ' is-edge-start' : '') + (column === 6 ? ' is-edge-end' : '') + (row === 0 ? ' is-top-row' : '');
var count = values[currentDay - 1];
var tooltipTitle = categories.map(function (category) {
return category.label + ': ' + countForDay(currentDay, category.key);
}).join('\n');
html += '<button class="pub-day' + selected + position + '" type="button" data-day="' + currentDay +
'" data-level="' + levelForCount(count, max) + '" title="' + escapeHtml(tooltipTitle) + '" aria-label="' + escapeHtml(monthShortLabel(currentData.month) + currentDay + '日,共 ' + countAllForDay(currentDay) + ' 条') + '">' +
'<span class="pub-day__number">' + currentDay + '</span>' +
'<span class="pub-day__tooltip" role="tooltip">' + tooltipHtml(currentDay) + '</span></button>';
}
grid.innerHTML = html;
}
function renderItem(item, key, day) {
var content = escapeHtml(itemText(item, key));
var meta = itemMeta(item, key, day);
var metaParts = meta.parts.map(function (part) {
return escapeHtml(part);
});
if (key === 'friends') {
metaParts.push('<span class="pub-item__author">' + escapeHtml(text(item.author, '未知作者')) + '</span>');
}
if (meta.media) metaParts.push(escapeHtml(meta.media));
var link = safeLink(item.link);
var tag = link ? 'a' : 'div';
var attrs = link ? ' href="' + escapeHtml(link) + '" data-pjax=""' : '';
return '<' + tag + ' class="pub-item"' + attrs + '>' +
'<span class="pub-item__text">' + content + '</span>' +
'<span class="pub-item__meta">' + metaParts.join(' · ') + '</span>' +
'</' + tag + '>';
}
function renderDetails() {
var showingDay = selectedDay != null;
var sourceItems = showingDay ? getItemsForDay(selectedDay) : null;
detailsTitle.textContent = showingDay ? monthShortLabel(currentData.month) + selectedDay + '日发布' : monthShortLabel(currentData.month) + '发布';
viewAll.hidden = !showingDay;
sections.innerHTML = categories.map(function (category) {
var items = showingDay ? sourceItems[category.key] : (currentData.items || []).reduce(function (all, dayItems, index) {
var day = Number(dayItems.day) || index + 1;
return all.concat(Array.isArray(dayItems[category.key]) ? dayItems[category.key].map(function (item) {
return { item: item, day: day };
}) : []);
}, []);
var normalized = showingDay ? (Array.isArray(items) ? items.map(function (item) {
return { item: item, day: selectedDay };
}) : []) : items;
sortEntries(normalized);
var list = normalized.length ? normalized.map(function (entry) {
return renderItem(entry.item, category.key, entry.day);
}).join('') : '<div class="pub-empty">暂无' + escapeHtml(category.label) + '</div>';
return '<section class="pub-section" aria-labelledby="pub-section-' + category.key + '">' +
'<div class="pub-section__title" id="pub-section-' + category.key + '"><span>' + escapeHtml(category.label) + '</span><small>' + normalized.length + ' 条</small></div>' +
'<div class="pub-section__body">' + list + '</div></section>';
}).join('');
}
function render() {
renderSummary();
renderCalendar();
renderDetails();
}
function loadMonth(month) {
if (!/^\d{4}-\d{2}$/.test(month)) return;
var thisRequest = ++requestId;
selectedDay = null;
currentMonthValue = month;
monthLabelElement.textContent = monthLabel(month);
grid.classList.add('is-loading');
grid.innerHTML = '<div class="pub-loading">加载中……</div>';
sections.innerHTML = '';
var url = API + '?month=' + encodeURIComponent(month) + '&timezone=' + encodeURIComponent(TIMEZONE);
fetch(url, { headers: { Accept: 'application/json' } })
.then(function (response) {
if (!response.ok) throw new Error('HTTP ' + response.status);
return response.json();
})
.then(function (data) {
if (thisRequest !== requestId) return;
currentData = data || {};
currentData.month = /^\d{4}-\d{2}$/.test(currentData.month) ? currentData.month : month;
currentData.items = Array.isArray(currentData.items) ? currentData.items : [];
currentData.counts = currentData.counts || {};
currentData.dailyCounts = currentData.dailyCounts || {};
render();
})
.catch(function (error) {
if (thisRequest !== requestId) return;
currentData = null;
total.textContent = '—';
breakdown.innerHTML = '';
grid.classList.add('is-loading');
grid.innerHTML = '<div class="pub-loading is-error">发布日历加载失败:' + escapeHtml(error.message) + '</div>';
sections.innerHTML = '';
});
}
function shiftMonth(offset) {
var parts = currentMonthValue.split('-').map(Number);
var date = new Date(parts[0], parts[1] - 1 + offset, 1);
loadMonth(date.getFullYear() + '-' + pad(date.getMonth() + 1));
}
function closeCategoryMenu() {
categoryMenu.hidden = true;
categoryTrigger.setAttribute('aria-expanded', 'false');
}
function setCategory(key) {
var option = categoryMenu.querySelector('[data-category="' + key + '"]');
if (!option) return;
selectedCategory = key;
categoryLabelElement.textContent = option.textContent;
categoryMenu.querySelectorAll('.pub-category-option').forEach(function (item) {
item.setAttribute('aria-selected', item === option ? 'true' : 'false');
});
closeCategoryMenu();
if (currentData) renderCalendar();
}
function navigatePjax(event) {
var link = event.target.closest('a.pub-item[data-pjax]');
if (!link || !root.contains(link) || (event.button != null && event.button !== 0) || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
event.preventDefault();
event.stopPropagation();
if (window.pjax && typeof window.pjax.loadUrl === 'function') {
window.pjax.loadUrl(link.href);
} else if (window.$ && typeof window.$.pjax === 'function') {
window.$.pjax({ url: link.href, container: '#pjax-container' });
} else {
window.location.href = link.href;
}
}
root.querySelector('#pub-prev-month').addEventListener('click', function () { shiftMonth(-1); });
root.querySelector('#pub-next-month').addEventListener('click', function () { shiftMonth(1); });
categoryTrigger.addEventListener('click', function (event) {
event.stopPropagation();
var isOpen = categoryMenu.hidden;
categoryMenu.hidden = !isOpen;
categoryTrigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
categoryMenu.addEventListener('click', function (event) {
var option = event.target.closest('.pub-category-option');
if (option) setCategory(option.dataset.category);
});
document.addEventListener('click', function (event) {
if (!categoryPicker.contains(event.target)) closeCategoryMenu();
});
root.addEventListener('click', navigatePjax, true);
grid.addEventListener('click', function (event) {
var dayButton = event.target.closest('.pub-day[data-day]');
if (!dayButton || !currentData) return;
selectedDay = Number(dayButton.dataset.day);
renderCalendar();
renderDetails();
});
viewAll.addEventListener('click', function () {
selectedDay = null;
renderCalendar();
renderDetails();
});
currentMonthValue = currentMonth();
loadMonth(currentMonthValue);
}
document.addEventListener('pjax:complete', initPublicationCalendar);
initPublicationCalendar();
})();
</script>
</section>