1.适配朋友圈插件
2.更新版本号为1.0.6-ce 3.README增加朋友圈插件地址
This commit is contained in:
parent
e7b35470f0
commit
2a04f6945b
@ -1679,6 +1679,11 @@ spec:
|
||||
height: 300px
|
||||
label: 底部显示内容
|
||||
language: html
|
||||
- $formkit: text
|
||||
name: fmomentsPageSize
|
||||
label: 友链每页数量
|
||||
help: "填写每页(滚动加载)展示的友链数量,"
|
||||
value: 12
|
||||
|
||||
- group: fcircle
|
||||
label: 友链鱼塘
|
||||
|
708
templates/assets/css/fmoments.css
Normal file
708
templates/assets/css/fmoments.css
Normal file
@ -0,0 +1,708 @@
|
||||
#fMomentsMessageBoard {
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 12px;
|
||||
box-shadow: var(--heo-shadow-border);
|
||||
border: var(--style-border);
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Lato, Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
margin: 16px 0;
|
||||
padding: 16px;
|
||||
animation: slideInUp 0.6s ease-out;
|
||||
}
|
||||
|
||||
.fMomentsUpdatedTime {
|
||||
text-align: center;
|
||||
padding: 8px 0;
|
||||
margin-bottom: 16px;
|
||||
border-bottom: 1px solid var(--heo-border-color);
|
||||
font-size: 14px;
|
||||
color: var(--heo-secondtext);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fMomentsStatsGrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.fMomentsStatCard {
|
||||
background: linear-gradient(135deg, var(--heo-secondbg), var(--heo-mask-bg));
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid var(--heo-border-color);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.fMomentsStatCard::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 3px;
|
||||
background: linear-gradient(90deg, var(--heo-main), var(--heo-blue));
|
||||
transform: scaleX(0);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fMomentsStatCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--heo-shadow-lightblack);
|
||||
}
|
||||
|
||||
.fMomentsStatCard:hover::before {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
.fMomentsStatIcon {
|
||||
font-size: 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.fMomentsStatNumber {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: var(--heo-fontcolor);
|
||||
margin-bottom: 4px;
|
||||
font-family: 'SAOUI', monospace;
|
||||
}
|
||||
|
||||
.fMomentsStatLabel {
|
||||
font-size: 12px;
|
||||
color: var(--heo-secondtext);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* 控制面板 */
|
||||
.fMomentsControlPanel {
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
margin: 20px 0;
|
||||
box-shadow: var(--heo-shadow-border);
|
||||
border: var(--style-border);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.fMomentsSearchBox {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fMomentsSearchInput {
|
||||
width: 100%;
|
||||
padding: 14px 16px 14px 44px;
|
||||
border: 2px solid var(--heo-border-color);
|
||||
border-radius: 28px;
|
||||
background: var(--heo-secondbg);
|
||||
color: var(--heo-fontcolor);
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.fMomentsSearchInput:focus {
|
||||
outline: none;
|
||||
border-color: var(--heo-main);
|
||||
box-shadow: 0 0 0 3px rgba(var(--heo-main-rgb), 0.1);
|
||||
}
|
||||
|
||||
.fMomentsSearchIcon {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--heo-secondtext);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fMomentsFilterGroup {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.fMomentsSelect {
|
||||
padding: 12px 16px;
|
||||
border: 2px solid var(--heo-border-color);
|
||||
border-radius: 24px;
|
||||
background: var(--heo-secondbg);
|
||||
color: var(--heo-fontcolor);
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
min-width: 130px;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.fMomentsSelect:focus {
|
||||
outline: none;
|
||||
border-color: var(--heo-main);
|
||||
box-shadow: 0 0 0 3px rgba(var(--heo-main-rgb), 0.1);
|
||||
}
|
||||
|
||||
.fMomentsToggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 14px;
|
||||
color: var(--heo-fontcolor);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.fMomentsSwitch {
|
||||
position: relative;
|
||||
width: 52px;
|
||||
height: 28px;
|
||||
background: var(--heo-border-color);
|
||||
border-radius: 28px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fMomentsSwitch.active {
|
||||
background: var(--heo-main);
|
||||
}
|
||||
|
||||
.fMomentsSwitch::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background: white;
|
||||
border-radius: 50%;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
||||
}
|
||||
|
||||
.fMomentsSwitch.active::after {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
/* 显示数量状态 */
|
||||
.fMomentsDisplayStatus {
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 12px;
|
||||
padding: 16px 24px;
|
||||
margin: 20px 0;
|
||||
box-shadow: var(--heo-shadow-border);
|
||||
border: var(--style-border);
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: var(--heo-secondtext);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.fMomentsDisplayInfo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fMomentsDisplayCount {
|
||||
font-weight: 600;
|
||||
color: var(--heo-main);
|
||||
}
|
||||
|
||||
/* 加载状态 */
|
||||
.fMomentsLoading {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
margin: 20px 0;
|
||||
box-shadow: var(--heo-shadow-border);
|
||||
border: var(--style-border);
|
||||
}
|
||||
|
||||
.fMomentsLoadingSpinner {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 4px solid var(--heo-border-color);
|
||||
border-top: 4px solid var(--heo-main);
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.fMomentsLoadingText {
|
||||
color: var(--heo-secondtext);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 瀑布流布局 */
|
||||
#fmomentsContainer {
|
||||
font-family: 'SAOUI', -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", Lato, Roboto, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 20px 0;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem {
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--heo-shadow-border);
|
||||
border: var(--style-border);
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
animation: fadeInUp 0.6s ease-out forwards;
|
||||
height: 240px; /* 固定总高度: 72px(header) + 120px(content) + 48px(footer) */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem:hover {
|
||||
transform: translateY(-6px);
|
||||
box-shadow: var(--heo-shadow-lightblack);
|
||||
}
|
||||
|
||||
.fMomentsArticleHeader {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--heo-border-color);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
height: 72px; /* 固定高度 */
|
||||
overflow: hidden; /* 隐藏超出部分 */
|
||||
}
|
||||
|
||||
.fMomentsAvatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.fMomentsAvatar:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.fMomentsAuthorInfo {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.fMomentsAuthorName {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--heo-fontcolor);
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.fMomentsPublishTime {
|
||||
font-size: 12px;
|
||||
color: var(--heo-secondtext);
|
||||
}
|
||||
|
||||
.fMomentsArticleContent {
|
||||
padding: 16px;
|
||||
height: 120px; /* 固定高度 */
|
||||
overflow: hidden; /* 隐藏超出部分 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fMomentsArticleTitle {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--heo-fontcolor);
|
||||
line-height: 1.4;
|
||||
margin-bottom: 12px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
transition: color 0.3s ease;
|
||||
flex-shrink: 0; /* 防止标题被压缩 */
|
||||
}
|
||||
|
||||
.fMomentsArticleTitle:hover {
|
||||
color: var(--heo-main);
|
||||
}
|
||||
|
||||
.fMomentsArticleDescription {
|
||||
font-size: 14px;
|
||||
color: var(--heo-secondtext);
|
||||
line-height: 1.6;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
flex: 1; /* 占用剩余空间 */
|
||||
}
|
||||
|
||||
.fMomentsArticleFooter {
|
||||
padding: 12px 16px;
|
||||
border-top: 1px solid var(--heo-border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--heo-secondtext);
|
||||
height: 48px; /* 固定高度 */
|
||||
overflow: hidden; /* 隐藏超出部分 */
|
||||
flex-shrink: 0; /* 防止被压缩 */
|
||||
}
|
||||
|
||||
/* 加载更多按钮 */
|
||||
#fmomentsMoreBtn {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
height: 54px;
|
||||
margin: 30px auto;
|
||||
background: linear-gradient(135deg, var(--heo-main), var(--heo-blue));
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 27px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
box-shadow: 0 4px 15px rgba(var(--heo-main-rgb), 0.3);
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#fmomentsMoreBtn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 25px rgba(var(--heo-main-rgb), 0.4);
|
||||
}
|
||||
|
||||
#fmomentsMoreBtn:disabled {
|
||||
background: var(--heo-border-color);
|
||||
color: var(--heo-secondtext);
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.fMomentsNoMore {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: var(--heo-secondtext);
|
||||
font-size: 16px;
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
border: 2px dashed var(--heo-border-color);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.fMomentsNoMore i {
|
||||
font-size: 48px;
|
||||
color: var(--heo-main);
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* 错误状态 */
|
||||
.fMomentsErrorState {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--heo-secondtext);
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
border: 2px dashed #ff6b6b;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.fMomentsErrorState i {
|
||||
font-size: 64px;
|
||||
color: #ff6b6b;
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fMomentsErrorState h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--heo-fontcolor);
|
||||
}
|
||||
|
||||
.fMomentsRetryBtn {
|
||||
margin-top: 20px;
|
||||
padding: 12px 24px;
|
||||
background: var(--heo-main);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.fMomentsRetryBtn:hover {
|
||||
background: var(--heo-blue);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* 空状态 */
|
||||
.fMomentsEmptyState {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--heo-secondtext);
|
||||
background: var(--heo-card-bg);
|
||||
border-radius: 16px;
|
||||
border: 2px dashed var(--heo-border-color);
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.fMomentsEmptyState i {
|
||||
font-size: 64px;
|
||||
color: var(--heo-border-color);
|
||||
margin-bottom: 20px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fMomentsEmptyState h3 {
|
||||
font-size: 18px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--heo-fontcolor);
|
||||
}
|
||||
|
||||
.fMomentsEmptyState p {
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
#fmomentsContainer {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.fMomentsControlPanel {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.fMomentsFilterGroup {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 移动端 fMomentsMessageBoard 优化 */
|
||||
#fMomentsMessageBoard {
|
||||
padding: 12px;
|
||||
margin: 12px 0;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.fMomentsUpdatedTime {
|
||||
padding: 6px 0;
|
||||
margin-bottom: 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.fMomentsStatsGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.fMomentsStatCard {
|
||||
padding: 12px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.fMomentsStatIcon {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.fMomentsStatNumber {
|
||||
font-size: 20px;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.fMomentsStatLabel {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.fMomentsDisplayStatus {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fMomentsArticleHeader {
|
||||
height: 68px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.fMomentsArticleContent {
|
||||
height: 110px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.fMomentsArticleFooter {
|
||||
height: 44px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem {
|
||||
height: 222px; /* 调整后的总高度 */
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
/* 超小屏幕的 fMomentsMessageBoard 优化 */
|
||||
#fMomentsMessageBoard {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.fMomentsUpdatedTime {
|
||||
padding: 4px 0;
|
||||
margin-bottom: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.fMomentsStatsGrid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.fMomentsStatCard {
|
||||
padding: 10px 6px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.fMomentsStatIcon {
|
||||
font-size: 18px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.fMomentsStatNumber {
|
||||
font-size: 18px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.fMomentsStatLabel {
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.fMomentsArticleFooter {
|
||||
height: 40px;
|
||||
padding: 8px 10px;
|
||||
}
|
||||
|
||||
.fMomentsArticleContent {
|
||||
height: 100px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fMomentsArticleHeader {
|
||||
height: 64px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem {
|
||||
height: 204px; /* 调整后的总高度 */
|
||||
}
|
||||
|
||||
.fMomentsFilterGroup {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.fMomentsSelect {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.fMomentsSearchBox {
|
||||
min-width: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 超小屏幕专用(小于360px) */
|
||||
@media (max-width: 360px) {
|
||||
#fMomentsMessageBoard {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.fMomentsStatsGrid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.fMomentsStatCard {
|
||||
padding: 8px 4px;
|
||||
}
|
||||
|
||||
.fMomentsStatIcon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fMomentsStatNumber {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.fMomentsStatLabel {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 动画效果 */
|
||||
@keyframes slideInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fMomentsArticleItem:nth-child(2n) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem:nth-child(3n) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.fMomentsArticleItem:nth-child(4n) {
|
||||
animation-delay: 0.3s;
|
||||
}
|
776
templates/assets/js/fmoments.js
Normal file
776
templates/assets/js/fmoments.js
Normal file
@ -0,0 +1,776 @@
|
||||
if (typeof window.FriendMomentsApp === 'undefined') {
|
||||
class FriendMomentsApp {
|
||||
constructor() {
|
||||
|
||||
if (window._friendMomentsInstance) {
|
||||
// console.log('FriendMomentsApp instance already exists, destroying old one');
|
||||
window._friendMomentsInstance.destroy();
|
||||
// 等待销毁完成
|
||||
if (window._friendMomentsInstance) {
|
||||
return window._friendMomentsInstance;
|
||||
}
|
||||
}
|
||||
|
||||
window._friendMomentsInstance = this;
|
||||
|
||||
this.articles = [];
|
||||
this.filteredArticles = [];
|
||||
this.displayedArticles = [];
|
||||
this.currentPage = 1;
|
||||
this.pageSize = window.fmomentsConfig?.pageSize || 12;
|
||||
this.sortField = 'pubDate';
|
||||
this.sortOrder = 'desc';
|
||||
this.authorFilter = '';
|
||||
this.searchKeyword = '';
|
||||
this.loading = false;
|
||||
this.errorCount = 0;
|
||||
this.maxRetries = 3;
|
||||
|
||||
// 新增分页相关属性
|
||||
this.totalPages = 0;
|
||||
this.totalCount = 0;
|
||||
this.hasNext = false;
|
||||
this.hasPrevious = false;
|
||||
this.isFirst = true;
|
||||
this.isLast = false;
|
||||
|
||||
// 存储事件监听器引用,用于清理
|
||||
this.eventListeners = [];
|
||||
this.initialized = false;
|
||||
this.destroyed = false; // 新增:标记是否已销毁
|
||||
this.initPromise = null; // 新增:防止重复初始化
|
||||
|
||||
// 生成唯一实例ID
|
||||
this.instanceId = Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
// console.log(`Creating FriendMomentsApp instance: ${this.instanceId}`);
|
||||
|
||||
// 不在构造函数中调用init,改为外部调用
|
||||
}
|
||||
|
||||
async init() {
|
||||
// 防止重复初始化
|
||||
if (this.initPromise) {
|
||||
// console.log('Init already in progress, waiting...');
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
if (this.initialized) {
|
||||
// console.log('Already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance has been destroyed, cannot initialize');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`Initializing FriendMomentsApp instance: ${this.instanceId}`);
|
||||
|
||||
this.initPromise = this._doInit();
|
||||
return this.initPromise;
|
||||
}
|
||||
|
||||
async _doInit() {
|
||||
try {
|
||||
this.cleanup(); // 清理之前的状态
|
||||
this.showLoading();
|
||||
|
||||
// 检查是否在初始化过程中被销毁
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed during initialization');
|
||||
return;
|
||||
}
|
||||
|
||||
// 重置分页状态
|
||||
this.currentPage = 1;
|
||||
this.articles = [];
|
||||
|
||||
await this.loadArticles();
|
||||
|
||||
// 再次检查是否被销毁
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed after loading articles');
|
||||
return;
|
||||
}
|
||||
|
||||
this.setupEventListeners();
|
||||
this.setupAuthorFilter();
|
||||
this.calculateStats();
|
||||
this.displayArticles(true);
|
||||
this.updateDisplayStatus();
|
||||
this.hideLoading();
|
||||
this.initialized = true;
|
||||
|
||||
// console.log(`FriendMomentsApp instance ${this.instanceId} initialized successfully`);
|
||||
} catch (error) {
|
||||
if (!this.destroyed) {
|
||||
this.handleError(error);
|
||||
}
|
||||
} finally {
|
||||
this.initPromise = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 清理方法
|
||||
cleanup() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
// console.log(`Cleaning up FriendMomentsApp instance: ${this.instanceId}`);
|
||||
|
||||
// 清理事件监听器
|
||||
this.removeEventListeners();
|
||||
|
||||
// 清理DOM内容
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
|
||||
// 清理作者筛选选项
|
||||
const authorFilter = document.getElementById('authorFilter');
|
||||
if (authorFilter) {
|
||||
const defaultOption = authorFilter.querySelector('option[value=""]');
|
||||
authorFilter.innerHTML = '';
|
||||
if (defaultOption) {
|
||||
authorFilter.appendChild(defaultOption);
|
||||
} else {
|
||||
const option = document.createElement('option');
|
||||
option.value = '';
|
||||
option.textContent = '全部作者';
|
||||
authorFilter.appendChild(option);
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索框
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
searchInput.value = '';
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
this.articles = [];
|
||||
this.filteredArticles = [];
|
||||
this.displayedArticles = [];
|
||||
this.currentPage = 1;
|
||||
this.authorFilter = '';
|
||||
this.searchKeyword = '';
|
||||
this.loading = false;
|
||||
this.errorCount = 0;
|
||||
|
||||
// 重置分页状态
|
||||
this.totalPages = 0;
|
||||
this.totalCount = 0;
|
||||
this.hasNext = false;
|
||||
this.hasPrevious = false;
|
||||
this.isFirst = true;
|
||||
this.isLast = false;
|
||||
}
|
||||
|
||||
// 销毁实例
|
||||
destroy() {
|
||||
if (this.destroyed) {
|
||||
// console.log(`Instance ${this.instanceId} already destroyed`);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`Destroying FriendMomentsApp instance: ${this.instanceId}`);
|
||||
|
||||
this.destroyed = true;
|
||||
this.initialized = false;
|
||||
|
||||
// 取消进行中的初始化
|
||||
if (this.initPromise) {
|
||||
this.initPromise = null;
|
||||
}
|
||||
|
||||
this.cleanup();
|
||||
|
||||
// 清理全局引用
|
||||
if (window._friendMomentsInstance === this) {
|
||||
window._friendMomentsInstance = null;
|
||||
}
|
||||
|
||||
// console.log(`FriendMomentsApp instance ${this.instanceId} destroyed`);
|
||||
}
|
||||
|
||||
// 移除事件监听器
|
||||
removeEventListeners() {
|
||||
if (this.eventListeners.length > 0) {
|
||||
// console.log(`Removing ${this.eventListeners.length} event listeners for instance ${this.instanceId}`);
|
||||
this.eventListeners.forEach(({element, event, handler}) => {
|
||||
if (element && element.removeEventListener) {
|
||||
element.removeEventListener(event, handler);
|
||||
}
|
||||
});
|
||||
this.eventListeners = [];
|
||||
}
|
||||
}
|
||||
|
||||
// 添加事件监听器的辅助方法
|
||||
addEventListener(element, event, handler) {
|
||||
if (this.destroyed) return;
|
||||
|
||||
if (element) {
|
||||
element.addEventListener(event, handler);
|
||||
this.eventListeners.push({element, event, handler});
|
||||
// console.log(`Added ${event} listener for instance ${this.instanceId}`);
|
||||
}
|
||||
}
|
||||
|
||||
showLoading() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const loading = document.getElementById('loadingIndicator');
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
const errorState = document.getElementById('errorState');
|
||||
|
||||
if (loading) loading.style.display = 'block';
|
||||
if (container) container.style.display = 'none';
|
||||
if (errorState) errorState.style.display = 'none';
|
||||
}
|
||||
|
||||
hideLoading() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const loading = document.getElementById('loadingIndicator');
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (container) container.style.display = 'grid';
|
||||
}
|
||||
|
||||
async loadArticles(page = 1) {
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed, skipping loadArticles');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log(`Loading articles for instance ${this.instanceId}, page: ${page}`);
|
||||
|
||||
try {
|
||||
const baseUrl = window.fmomentsConfig?.apiUrl || '/apis/api.friend.moony.la/v1alpha1/friendposts';
|
||||
const url = new URL(baseUrl, window.location.origin);
|
||||
url.searchParams.set('page', page.toString());
|
||||
url.searchParams.set('size', this.pageSize.toString());
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
// 检查请求完成后实例是否还有效
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed after fetch, ignoring response');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 再次检查实例是否还有效
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed after parsing response, ignoring data');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.items && Array.isArray(data.items)) {
|
||||
const newArticles = data.items.map(item => ({
|
||||
id: item.metadata.name,
|
||||
author: item.spec.author,
|
||||
authorUrl: item.spec.authorUrl,
|
||||
title: item.spec.title,
|
||||
description: item.spec.description,
|
||||
postLink: item.spec.postLink,
|
||||
logo: item.spec.logo,
|
||||
pubDate: new Date(item.spec.pubDate),
|
||||
creationTime: new Date(item.metadata.creationTimestamp),
|
||||
content: `${item.spec.title} ${item.spec.author} ${item.spec.description}`.toLowerCase()
|
||||
}));
|
||||
|
||||
// 更新分页信息
|
||||
this.totalPages = data.totalPages || 0;
|
||||
this.totalCount = data.total || 0;
|
||||
this.hasNext = data.hasNext || false;
|
||||
this.hasPrevious = data.hasPrevious || false;
|
||||
this.isFirst = data.first || false;
|
||||
this.isLast = data.last || false;
|
||||
|
||||
// 如果是第一页,重置文章数组;否则追加
|
||||
if (page === 1) {
|
||||
this.articles = newArticles;
|
||||
} else {
|
||||
this.articles = this.articles.concat(newArticles);
|
||||
}
|
||||
|
||||
// console.log(`Successfully loaded ${newArticles.length} articles for page ${page}, total articles: ${this.articles.length} for instance ${this.instanceId}`);
|
||||
this.errorCount = 0;
|
||||
} else {
|
||||
throw new Error('API 返回数据格式错误');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
if (this.destroyed) {
|
||||
// console.log('Instance destroyed, ignoring loadArticles error');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.error(`Loading articles failed for instance ${this.instanceId}:`, error);
|
||||
this.errorCount++;
|
||||
|
||||
if (this.errorCount < this.maxRetries && !this.destroyed) {
|
||||
// console.log(`Retry ${this.errorCount} for instance ${this.instanceId}`);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * this.errorCount));
|
||||
return this.loadArticles(page);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
calculateStats() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const now = new Date();
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
const totalArticles = this.totalCount || this.articles.length; // 优先使用API返回的总数
|
||||
const totalAuthors = new Set(this.articles.map(a => a.author)).size;
|
||||
const todayCount = this.articles.filter(a => a.pubDate >= today).length;
|
||||
const weekCount = this.articles.filter(a => a.pubDate >= weekAgo).length;
|
||||
|
||||
this.updateStatNumber('totalArticles', totalArticles);
|
||||
this.updateStatNumber('totalAuthors', totalAuthors);
|
||||
this.updateStatNumber('todayCount', todayCount);
|
||||
this.updateStatNumber('weekCount', weekCount);
|
||||
|
||||
const lastUpdateTime = document.getElementById('lastUpdateTime');
|
||||
if (lastUpdateTime) {
|
||||
lastUpdateTime.textContent = new Date().toLocaleString('zh-CN');
|
||||
}
|
||||
}
|
||||
|
||||
updateStatNumber(elementId, value) {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const element = document.getElementById(elementId);
|
||||
if (element) {
|
||||
const startValue = 0;
|
||||
const duration = 1000;
|
||||
const startTime = performance.now();
|
||||
|
||||
const animate = (currentTime) => {
|
||||
if (this.destroyed) return; // 动画过程中检查实例状态
|
||||
|
||||
const elapsed = currentTime - startTime;
|
||||
const progress = Math.min(elapsed / duration, 1);
|
||||
const currentValue = Math.floor(startValue + (value - startValue) * progress);
|
||||
element.textContent = currentValue;
|
||||
|
||||
if (progress < 1 && !this.destroyed) {
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
};
|
||||
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
// console.log(`Setting up event listeners for instance ${this.instanceId}`);
|
||||
|
||||
// 搜索功能
|
||||
const searchInput = document.getElementById('searchInput');
|
||||
if (searchInput) {
|
||||
let searchTimeout;
|
||||
const searchHandler = (e) => {
|
||||
if (this.destroyed) return;
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
if (this.destroyed) return;
|
||||
this.searchKeyword = e.target.value.toLowerCase();
|
||||
this.resetAndDisplay();
|
||||
}, 300);
|
||||
};
|
||||
this.addEventListener(searchInput, 'input', searchHandler);
|
||||
}
|
||||
|
||||
// 作者筛选
|
||||
const authorFilter = document.getElementById('authorFilter');
|
||||
if (authorFilter) {
|
||||
const authorHandler = (e) => {
|
||||
if (this.destroyed) return;
|
||||
this.authorFilter = e.target.value;
|
||||
this.resetAndDisplay();
|
||||
};
|
||||
this.addEventListener(authorFilter, 'change', authorHandler);
|
||||
}
|
||||
|
||||
// 排序方式
|
||||
const sortSelect = document.getElementById('sortSelect');
|
||||
if (sortSelect) {
|
||||
const sortHandler = (e) => {
|
||||
if (this.destroyed) return;
|
||||
this.sortField = e.target.value;
|
||||
this.resetAndDisplay();
|
||||
};
|
||||
this.addEventListener(sortSelect, 'change', sortHandler);
|
||||
}
|
||||
|
||||
// 排序顺序
|
||||
const sortOrderSwitch = document.getElementById('sortOrderSwitch');
|
||||
if (sortOrderSwitch) {
|
||||
const sortOrderHandler = () => {
|
||||
if (this.destroyed) return;
|
||||
sortOrderSwitch.classList.toggle('active');
|
||||
this.sortOrder = sortOrderSwitch.classList.contains('active') ? 'desc' : 'asc';
|
||||
this.resetAndDisplay();
|
||||
};
|
||||
this.addEventListener(sortOrderSwitch, 'click', sortOrderHandler);
|
||||
}
|
||||
|
||||
// 加载更多
|
||||
const moreBtn = document.getElementById('fmomentsMoreBtn');
|
||||
if (moreBtn) {
|
||||
const moreBtnHandler = () => {
|
||||
if (this.destroyed) return;
|
||||
this.loadMore();
|
||||
};
|
||||
this.addEventListener(moreBtn, 'click', moreBtnHandler);
|
||||
}
|
||||
|
||||
// 重试按钮
|
||||
const retryBtn = document.getElementById('retryBtn');
|
||||
if (retryBtn) {
|
||||
const retryHandler = () => {
|
||||
if (this.destroyed) return;
|
||||
this.init();
|
||||
};
|
||||
this.addEventListener(retryBtn, 'click', retryHandler);
|
||||
}
|
||||
|
||||
// 滚动加载
|
||||
const scrollHandler = this.throttle(() => {
|
||||
if (this.destroyed) return;
|
||||
if (this.isNearBottom() && !this.loading && this.hasMoreContent()) {
|
||||
this.loadMore();
|
||||
}
|
||||
}, 200);
|
||||
this.addEventListener(window, 'scroll', scrollHandler);
|
||||
}
|
||||
|
||||
setupAuthorFilter() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const authorFilter = document.getElementById('authorFilter');
|
||||
if (!authorFilter) return;
|
||||
|
||||
const existingOptions = Array.from(authorFilter.options).map(option => option.value);
|
||||
|
||||
const authors = [...new Set(this.articles.map(article => article.author))].sort();
|
||||
authors.forEach(author => {
|
||||
if (this.destroyed) return;
|
||||
if (author && !existingOptions.includes(author)) {
|
||||
const option = document.createElement('option');
|
||||
option.value = author;
|
||||
option.textContent = author;
|
||||
authorFilter.appendChild(option);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
filterAndSortArticles() {
|
||||
if (this.destroyed) return [];
|
||||
|
||||
let filtered = this.articles.filter(article => {
|
||||
if (this.authorFilter && article.author !== this.authorFilter) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.searchKeyword && !article.content.includes(this.searchKeyword)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
filtered.sort((a, b) => {
|
||||
let valueA, valueB;
|
||||
|
||||
switch (this.sortField) {
|
||||
case 'pubDate':
|
||||
valueA = a.pubDate;
|
||||
valueB = b.pubDate;
|
||||
break;
|
||||
case 'creationTime':
|
||||
valueA = a.creationTime;
|
||||
valueB = b.creationTime;
|
||||
break;
|
||||
case 'author':
|
||||
valueA = a.author.toLowerCase();
|
||||
valueB = b.author.toLowerCase();
|
||||
break;
|
||||
case 'title':
|
||||
valueA = a.title.toLowerCase();
|
||||
valueB = b.title.toLowerCase();
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (valueA < valueB) {
|
||||
return this.sortOrder === 'asc' ? -1 : 1;
|
||||
}
|
||||
if (valueA > valueB) {
|
||||
return this.sortOrder === 'asc' ? 1 : -1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
this.filteredArticles = filtered;
|
||||
return filtered;
|
||||
}
|
||||
|
||||
displayArticles(isInitial = false) {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
if (!container) return;
|
||||
|
||||
const filtered = this.filterAndSortArticles();
|
||||
|
||||
if (filtered.length === 0) {
|
||||
this.showEmptyState();
|
||||
return;
|
||||
} else {
|
||||
this.hideEmptyState();
|
||||
}
|
||||
|
||||
if (isInitial) {
|
||||
container.innerHTML = '';
|
||||
this.displayedArticles = [];
|
||||
}
|
||||
|
||||
// 显示所有已筛选的文章
|
||||
const articlesToShow = filtered.slice(this.displayedArticles.length);
|
||||
|
||||
articlesToShow.forEach((article, index) => {
|
||||
if (this.destroyed) return;
|
||||
const articleEl = this.createArticleElement(article);
|
||||
articleEl.style.animationDelay = `${index * 0.1}s`;
|
||||
container.appendChild(articleEl);
|
||||
});
|
||||
|
||||
this.displayedArticles = filtered;
|
||||
this.updateDisplayStatus();
|
||||
this.updateLoadingStatus();
|
||||
}
|
||||
|
||||
createArticleElement(article) {
|
||||
const articleEl = document.createElement('article');
|
||||
articleEl.className = 'fMomentsArticleItem';
|
||||
|
||||
const safeLogoUrl = article.logo || window.fmomentsConfig?.errorImg || '/default-avatar.png';
|
||||
const safeAuthorUrl = article.authorUrl || '#';
|
||||
const safePostLink = article.postLink || '#';
|
||||
|
||||
const pubDateStr = article.pubDate.toLocaleDateString('zh-CN');
|
||||
const pubTimeStr = article.pubDate.toLocaleTimeString('zh-CN', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
|
||||
articleEl.innerHTML = `
|
||||
<div class="fMomentsArticleHeader">
|
||||
<img class="fMomentsAvatar" src="${safeLogoUrl}" alt="${article.author}"
|
||||
onerror="this.src='${window.fmomentsConfig?.errorImg || '/default-avatar.png'}'">
|
||||
<a href="${safeAuthorUrl}" target="_blank" rel="noopener nofollow"
|
||||
style="color: var(--heo-main); text-decoration: none;"><div class="fMomentsAuthorInfo">
|
||||
<div class="fMomentsAuthorName">${article.author}</div>
|
||||
<div class="fMomentsPublishTime">${pubTimeStr}</div>
|
||||
</div></a>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="fMomentsArticleContent">
|
||||
<a class="fMomentsArticleTitle" href="${safePostLink}" target="_blank" rel="noopener nofollow"
|
||||
title="${article.title}">${article.title}</a>
|
||||
<a class="fMomentsArticleTitle" href="${safePostLink}" target="_blank" rel="noopener nofollow"
|
||||
title="${article.title}"><div class="fMomentsArticleDescription">${article.description}</div></a>
|
||||
</div>
|
||||
|
||||
<div class="fMomentsArticleFooter">
|
||||
<span>📅 ${pubDateStr}</span>
|
||||
<span>🌐 <a href="${safeAuthorUrl}" target="_blank" rel="noopener nofollow">${article.author}</a></span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return articleEl;
|
||||
}
|
||||
|
||||
updateDisplayStatus() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const displayedCount = document.getElementById('displayedCount');
|
||||
const totalCount = document.getElementById('totalCount');
|
||||
const filterStatus = document.getElementById('filterStatus');
|
||||
const filterText = document.getElementById('filterText');
|
||||
|
||||
if (displayedCount) displayedCount.textContent = this.displayedArticles.length;
|
||||
if (totalCount) totalCount.textContent = this.totalCount || this.articles.length;
|
||||
|
||||
const hasFilter = this.authorFilter || this.searchKeyword;
|
||||
if (filterStatus) {
|
||||
filterStatus.style.display = hasFilter ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
if (filterText && hasFilter) {
|
||||
let filterParts = [];
|
||||
if (this.authorFilter) filterParts.push(`作者: ${this.authorFilter}`);
|
||||
if (this.searchKeyword) filterParts.push(`搜索: ${this.searchKeyword}`);
|
||||
filterText.textContent = filterParts.join(' | ');
|
||||
}
|
||||
}
|
||||
|
||||
resetAndDisplay() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
}
|
||||
this.displayedArticles = [];
|
||||
this.displayArticles(true);
|
||||
}
|
||||
|
||||
hasMoreContent() {
|
||||
if (this.destroyed) return false;
|
||||
|
||||
// 基于API返回的分页信息判断是否还有更多内容
|
||||
return this.hasNext && !this.isLast;
|
||||
}
|
||||
|
||||
async loadMore() {
|
||||
if (this.destroyed || this.loading || !this.hasMoreContent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
// 加载下一页
|
||||
const nextPage = Math.floor(this.articles.length / this.pageSize) + 1;
|
||||
await this.loadArticles(nextPage);
|
||||
|
||||
// 重新设置作者筛选选项(可能有新作者)
|
||||
this.setupAuthorFilter();
|
||||
|
||||
// 重新计算统计信息
|
||||
this.calculateStats();
|
||||
|
||||
// 显示新加载的文章
|
||||
this.displayArticles();
|
||||
|
||||
} catch (error) {
|
||||
if (!this.destroyed) {
|
||||
console.error('Load more failed:', error);
|
||||
// 可以选择显示错误提示,但不影响现有内容
|
||||
}
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingStatus() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const moreBtn = document.getElementById('fmomentsMoreBtn');
|
||||
const noMoreTip = document.getElementById('noMoreTip');
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
const finalCount = document.getElementById('finalCount');
|
||||
|
||||
if (!loadingStatus) return;
|
||||
|
||||
if (!this.hasMoreContent()) {
|
||||
if (moreBtn) moreBtn.style.display = 'none';
|
||||
if (noMoreTip) noMoreTip.style.display = 'block';
|
||||
if (finalCount) finalCount.textContent = this.displayedArticles.length;
|
||||
loadingStatus.style.display = 'block';
|
||||
} else {
|
||||
if (moreBtn) moreBtn.style.display = 'flex';
|
||||
if (noMoreTip) noMoreTip.style.display = 'none';
|
||||
loadingStatus.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
showEmptyState() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
const loadingStatus = document.getElementById('loadingStatus');
|
||||
|
||||
if (emptyState) emptyState.style.display = 'block';
|
||||
if (loadingStatus) loadingStatus.style.display = 'none';
|
||||
}
|
||||
|
||||
hideEmptyState() {
|
||||
if (this.destroyed) return;
|
||||
|
||||
const emptyState = document.getElementById('emptyState');
|
||||
if (emptyState) emptyState.style.display = 'none';
|
||||
}
|
||||
|
||||
handleError(error) {
|
||||
if (this.destroyed) return;
|
||||
|
||||
console.error(`Friend Moments加载失败 for instance ${this.instanceId}:`, error);
|
||||
|
||||
const loading = document.getElementById('loadingIndicator');
|
||||
const errorState = document.getElementById('errorState');
|
||||
const errorMessage = document.getElementById('errorMessage');
|
||||
const container = document.getElementById('fmomentsContainer');
|
||||
|
||||
if (loading) loading.style.display = 'none';
|
||||
if (container) container.style.display = 'none';
|
||||
if (errorState) errorState.style.display = 'block';
|
||||
|
||||
if (errorMessage) {
|
||||
let message = '加载失败,请稍后重试';
|
||||
if (error.message.includes('fetch')) {
|
||||
message = '网络连接失败,请检查网络后重试';
|
||||
} else if (error.message.includes('HTTP')) {
|
||||
message = `服务器错误: ${error.message}`;
|
||||
}
|
||||
errorMessage.textContent = message;
|
||||
}
|
||||
}
|
||||
|
||||
throttle(func, limit) {
|
||||
let inThrottle;
|
||||
return function () {
|
||||
const args = arguments;
|
||||
const context = this;
|
||||
if (!inThrottle) {
|
||||
func.apply(context, args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isNearBottom() {
|
||||
return window.innerHeight + window.scrollY >= document.body.offsetHeight - 1000;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出类
|
||||
window.FriendMomentsApp = FriendMomentsApp;
|
||||
}
|
330
templates/friends.html
Normal file
330
templates/friends.html
Normal file
@ -0,0 +1,330 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
th:replace="~{modules/layouts/layout :: layout(content = ~{::content}, htmlType = 'page',title = '朋友圈 | ' + ${site.title}, head = ~{::head})}">
|
||||
|
||||
<th:block th:fragment="head">
|
||||
<th:block th:replace="~{modules/common/open-graph :: open-graph(_title = '朋友圈',
|
||||
_permalink = '/moments',
|
||||
_cover = '',
|
||||
_excerpt = '友链朋友圈 - 发现更多精彩内容',
|
||||
_type = 'website')}"></th:block>
|
||||
<!-- 分离CSS文件 -->
|
||||
<link rel="stylesheet" th:href="@{/assets/css/fmoments.css}" data-pjax>
|
||||
</th:block>
|
||||
|
||||
<th:block th:fragment="content">
|
||||
<div class="page" id="body-wrap">
|
||||
<!-- 头部导航栏 -->
|
||||
<header class="not-top-img" id="page-header">
|
||||
<nav th:replace="~{modules/nav :: nav(title = '朋友圈')}"></nav>
|
||||
</header>
|
||||
|
||||
<main class="layout hide-aside" id="content-inner">
|
||||
<div id="page">
|
||||
<!-- 朋友圈统计信息面板 -->
|
||||
<div id="fMomentsMessageBoard">
|
||||
<div class="fMomentsUpdatedTime">
|
||||
<i class="fas fa-sync-alt"></i>
|
||||
最近更新:<span id="lastUpdateTime">加载中...</span>
|
||||
</div>
|
||||
|
||||
<div class="fMomentsStatsGrid">
|
||||
<div class="fMomentsStatCard">
|
||||
<div class="fMomentsStatIcon">📚</div>
|
||||
<div class="fMomentsStatNumber" id="totalArticles">0</div>
|
||||
<div class="fMomentsStatLabel">总文章数</div>
|
||||
</div>
|
||||
|
||||
<div class="fMomentsStatCard">
|
||||
<div class="fMomentsStatIcon">👥</div>
|
||||
<div class="fMomentsStatNumber" id="totalAuthors">0</div>
|
||||
<div class="fMomentsStatLabel">活跃作者</div>
|
||||
</div>
|
||||
|
||||
<div class="fMomentsStatCard">
|
||||
<div class="fMomentsStatIcon">🔥</div>
|
||||
<div class="fMomentsStatNumber" id="todayCount">0</div>
|
||||
<div class="fMomentsStatLabel">今日更新</div>
|
||||
</div>
|
||||
|
||||
<div class="fMomentsStatCard">
|
||||
<div class="fMomentsStatIcon">⚡</div>
|
||||
<div class="fMomentsStatNumber" id="weekCount">0</div>
|
||||
<div class="fMomentsStatLabel">本周更新</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 控制面板 -->
|
||||
<div class="fMomentsControlPanel">
|
||||
<div class="fMomentsSearchBox">
|
||||
<i class="fas fa-search fMomentsSearchIcon"></i>
|
||||
<input type="text" class="fMomentsSearchInput" placeholder="搜索文章标题或作者..." id="searchInput">
|
||||
</div>
|
||||
|
||||
<div class="fMomentsFilterGroup">
|
||||
<select class="fMomentsSelect" id="authorFilter">
|
||||
<option value="">全部作者</option>
|
||||
</select>
|
||||
|
||||
<select class="fMomentsSelect" id="sortSelect">
|
||||
<option value="pubDate">按发布时间</option>
|
||||
<option value="creationTime">按创建时间</option>
|
||||
<option value="author">按作者名称</option>
|
||||
<option value="title">按标题</option>
|
||||
</select>
|
||||
|
||||
<div class="fMomentsToggle">
|
||||
<span>升序</span>
|
||||
<div class="fMomentsSwitch active" id="sortOrderSwitch"></div>
|
||||
<span>降序</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 显示数量状态 -->
|
||||
<div class="fMomentsDisplayStatus">
|
||||
<div class="fMomentsDisplayInfo">
|
||||
<i class="fas fa-filter"></i>
|
||||
<span>显示</span>
|
||||
<span class="fMomentsDisplayCount" id="displayedCount">0</span>
|
||||
<span>/</span>
|
||||
<span class="fMomentsDisplayCount" id="totalCount">0</span>
|
||||
<span>篇文章</span>
|
||||
</div>
|
||||
<div class="fMomentsDisplayInfo" id="filterStatus" style="display: none;">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
<span id="filterText">已应用筛选条件</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 加载状态 -->
|
||||
<div id="loadingIndicator" class="fMomentsLoading">
|
||||
<div class="fMomentsLoadingSpinner"></div>
|
||||
<div class="fMomentsLoadingText">正在加载动态内容...</div>
|
||||
</div>
|
||||
|
||||
<!-- 朋友圈文章容器 -->
|
||||
<div id="fmomentsContainer"></div>
|
||||
|
||||
<!-- 加载更多按钮 -->
|
||||
<div id="loadingStatus" style="display: none;">
|
||||
<button id="fmomentsMoreBtn" type="button">
|
||||
<i class="fas fa-angle-double-down"></i>
|
||||
<span>加载更多</span>
|
||||
</button>
|
||||
<div class="fMomentsNoMore" id="noMoreTip" style="display: none;">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<div>🎉 已显示全部内容</div>
|
||||
<small>共找到 <span id="finalCount">0</span> 篇文章</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 错误状态 -->
|
||||
<div class="fMomentsErrorState" id="errorState" style="display: none;">
|
||||
<i class="fas fa-exclamation-triangle"></i>
|
||||
<h3>加载失败</h3>
|
||||
<div id="errorMessage">请检查网络连接后重试</div>
|
||||
<button class="fMomentsRetryBtn" id="retryBtn">重新加载</button>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div class="fMomentsEmptyState" id="emptyState" style="display: none;">
|
||||
<i class="fas fa-search"></i>
|
||||
<h3>没有找到匹配的文章</h3>
|
||||
<p>尝试更换搜索词或调整筛选条件</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- 底部 -->
|
||||
<footer th:replace="~{modules/footer}" />
|
||||
|
||||
<!-- 资源检查和动态加载脚本 -->
|
||||
<script data-pjax th:inline="javascript">
|
||||
// 资源动态加载器
|
||||
window.MomentsResourceLoader = {
|
||||
// 检查CSS是否已加载
|
||||
isCSSLoaded: function (href) {
|
||||
const links = document.getElementsByTagName('link');
|
||||
for (let i = 0; i < links.length; i++) {
|
||||
if (links[i].href && links[i].href.includes(href)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
// 检查JS是否已加载
|
||||
isJSLoaded: function (src) {
|
||||
const scripts = document.getElementsByTagName('script');
|
||||
for (let i = 0; i < scripts.length; i++) {
|
||||
if (scripts[i].src && scripts[i].src.includes(src)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// 也检查是否已经定义了相关的全局变量/函数
|
||||
return typeof FriendMomentsApp !== 'undefined';
|
||||
},
|
||||
|
||||
// 动态加载CSS
|
||||
loadCSS: function (href) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isCSSLoaded(href)) {
|
||||
// console.log('CSS already loaded:', href);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const link = document.createElement('link');
|
||||
link.rel = 'stylesheet';
|
||||
link.href = href;
|
||||
link.setAttribute('data-pjax', '');
|
||||
|
||||
link.onload = () => {
|
||||
// console.log('CSS loaded successfully:', href);
|
||||
resolve();
|
||||
};
|
||||
|
||||
link.onerror = () => {
|
||||
console.error('Failed to load CSS:', href);
|
||||
reject(new Error(`Failed to load CSS: ${href}`));
|
||||
};
|
||||
|
||||
document.head.appendChild(link);
|
||||
});
|
||||
},
|
||||
|
||||
// 动态加载JS
|
||||
loadJS: function (src) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isJSLoaded(src)) {
|
||||
// console.log('JS already loaded:', src);
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.setAttribute('data-pjax', '');
|
||||
|
||||
script.onload = () => {
|
||||
// console.log('JS loaded successfully:', src);
|
||||
resolve();
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
console.error('Failed to load JS:', src);
|
||||
reject(new Error(`Failed to load JS: ${src}`));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
},
|
||||
|
||||
// 加载所有必需的资源
|
||||
loadRequiredResources: async function () {
|
||||
const cssPath = /*[[@{/assets/css/fmoments.css}]]*/ '/assets/css/fmoments.css';
|
||||
const jsPath = /*[[@{/assets/js/fmoments.js}]]*/ '/assets/js/fmoments.js';
|
||||
|
||||
try {
|
||||
// console.log('Checking and loading required resources...');
|
||||
|
||||
// 并行加载CSS和JS
|
||||
await Promise.all([
|
||||
this.loadCSS(cssPath),
|
||||
this.loadJS(jsPath)
|
||||
]);
|
||||
|
||||
// console.log('All resources loaded successfully');
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error loading resources:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<!-- 初始化脚本 -->
|
||||
<script data-pjax th:inline="javascript">
|
||||
// 朋友圈配置
|
||||
// 从主题设置获取
|
||||
window.fmomentsConfig = {
|
||||
apiUrl: /*[[${theme.config.link.fmomentsApiUrl}]]*/ '/apis/api.friend.moony.la/v1alpha1/friendposts',
|
||||
pageSize: /*[[${theme.config.link.fmomentsPageSize}]]*/ 12,
|
||||
errorImg: /*[[${theme.config.other.error_404.background}]]*/ '/assets/images/404.gif'
|
||||
};
|
||||
|
||||
// 全局初始化状态追踪
|
||||
window._momentsInitializing = false;
|
||||
|
||||
// 初始化朋友圈应用
|
||||
async function initMomentsApp() {
|
||||
// 防止并发初始化
|
||||
if (window._momentsInitializing) {
|
||||
// console.log('Moments initialization already in progress');
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('Starting Moments initialization');
|
||||
window._momentsInitializing = true;
|
||||
|
||||
try {
|
||||
// 首先检查并加载必需的资源
|
||||
const resourcesLoaded = await window.MomentsResourceLoader.loadRequiredResources();
|
||||
|
||||
if (!resourcesLoaded) {
|
||||
console.error('Failed to load required resources');
|
||||
return;
|
||||
}
|
||||
|
||||
// 等待一小段时间确保资源完全加载
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 检查FriendMomentsApp是否可用
|
||||
if (typeof FriendMomentsApp !== 'undefined') {
|
||||
// 创建新实例(构造函数会处理旧实例的清理)
|
||||
window.fMomentsApp = new FriendMomentsApp();
|
||||
// 显式调用初始化
|
||||
await window.fMomentsApp.init();
|
||||
// console.log('FriendMomentsApp initialized successfully');
|
||||
} else {
|
||||
console.error('FriendMomentsApp is not available after loading resources');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error initializing FriendMomentsApp:', error);
|
||||
} finally {
|
||||
window._momentsInitializing = false;
|
||||
}
|
||||
}
|
||||
|
||||
// PJAX开始时立即清理
|
||||
document.addEventListener('pjax:start', function () {
|
||||
// console.log('PJAX start - cleaning up Moments');
|
||||
if (window._momentsInstance) {
|
||||
window._momentsInstance.destroy();
|
||||
}
|
||||
window._momentsInitializing = false;
|
||||
});
|
||||
|
||||
// DOM加载完成后初始化
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', initMomentsApp);
|
||||
} else {
|
||||
// DOM已经加载完成
|
||||
initMomentsApp();
|
||||
}
|
||||
|
||||
// PJAX完成后重新初始化
|
||||
document.addEventListener('pjax:complete', function () {
|
||||
if (document.getElementById('fmomentsContainer')) {
|
||||
// 延迟一点时间确保DOM稳定
|
||||
setTimeout(initMomentsApp, 150);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</th:block>
|
||||
|
||||
</html>
|
@ -52,7 +52,7 @@ spec:
|
||||
issues: https://gitee.com/uptoz/halo-theme-hao/issues
|
||||
settingName: "theme-hao-setting"
|
||||
configMapName: "theme-hao-configMap"
|
||||
version: "1.0.5-ce"
|
||||
version: "1.0.6-ce"
|
||||
requires: ">=2.21.0"
|
||||
license:
|
||||
- name: "CC BY-SA 4.0"
|
||||
|
Loading…
Reference in New Issue
Block a user