diff --git a/settings.yaml b/settings.yaml index 41a5601..2115bf0 100644 --- a/settings.yaml +++ b/settings.yaml @@ -1679,6 +1679,11 @@ spec: height: 300px label: 底部显示内容 language: html + - $formkit: text + name: fmomentsPageSize + label: 友链每页数量 + help: "填写每页(滚动加载)展示的友链数量," + value: 12 - group: fcircle label: 友链鱼塘 diff --git a/templates/assets/css/fmoments.css b/templates/assets/css/fmoments.css new file mode 100644 index 0000000..c671f4a --- /dev/null +++ b/templates/assets/css/fmoments.css @@ -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; +} \ No newline at end of file diff --git a/templates/assets/js/fmoments.js b/templates/assets/js/fmoments.js new file mode 100644 index 0000000..78e7a7c --- /dev/null +++ b/templates/assets/js/fmoments.js @@ -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 = ` +
+ + + + + `; + + 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; +} \ No newline at end of file diff --git a/templates/friends.html b/templates/friends.html new file mode 100644 index 0000000..28084df --- /dev/null +++ b/templates/friends.html @@ -0,0 +1,330 @@ + + + +