1194 lines
36 KiB
HTML
1194 lines
36 KiB
HTML
<!--
|
||
发布日历:直接粘贴到 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>
|