友圈分页

最近工作繁忙,等有空再看友圈时,才发现新文章早已超过 30 篇,许多内容在博客上已无法查看。以往,直接显示最近 30 篇文章已经足够了,但现在显然不再可行。为了在保证加载速度的同时,让更多友圈文章得以展示,我还是抽时间将分页功能加上了。这样,无论何时查看,都可以在博客中方便地浏览更早的友圈文章了。

实现思路就是将排序后最新的前 100 篇文章(当然也可以随意增加到上千、上万篇,随你喜欢)按每 10 篇拆分成多个文件。output_1.json,output_2.json 等包含分页数据,每个文件包含 10 篇文章,每个分页文件都包含总文章数、当前页码和文章数据。page-notes.php 中:添加了 visitPaging 来跟踪友圈文章的分页状态。修改了 fetchvisitArticles 方法以支持分页加载。更新了 handleNextPage 和 handleScroll 方法以支持友圈的无限滚动。当切换到友圈标签时会重置分页状态。当用户滚动到页面底部时,会自动加载下一页的内容。

至于友圈功能服务器方面具体如何部署就不写了,page-notes.php 中友圈代码较为分散而且很多代码也只适用于该主题,仅供参考。这里只将修改后的 rss.php 代码记录如下:

<?php // 获取所有订阅文章并保存为 JSON function getAllSubscribedArticlesAndSaveToJson($user, $password, $articleCount = 1000) { // API 基础 URL $apiUrl = 'https://你部署FreshRSS的域名或ip地址/api/greader.php'; // 获取文章的 API 地址 $articlesUrl = $apiUrl . '/reader/api/0/stream/contents/reading-list?&n=' . $articleCount; // 登录 API 地址 $loginUrl = $apiUrl . '/accounts/ClientLogin?Email=' . urlencode($user) . '&Passwd=' . urlencode($password); // 发送登录请求 $loginResponse = curlRequest($loginUrl); // 检查是否成功获取认证 token if (strpos($loginResponse, 'Auth=') !== false) { $authToken = substr($loginResponse, strpos($loginResponse, 'Auth=') + 5); // 发送请求获取文章内容 $articlesResponse = curlRequest($articlesUrl, $authToken); $articles = json_decode($articlesResponse, true); // 判断是否成功获取文章数据 if (isset($articles['items'])) { $formattedArticles = array(); // 遍历文章列表 foreach ($articles['items'] as $item) { $publishedTime = isset($item['published']) ? $item['published'] : 0; $rawSummary = isset($item['summary']['content']) ? $item['summary']['content'] : ''; $description = cleanContent($rawSummary, 200); // 获取站点 URL $site_url = ''; if (isset($item['alternate'][0]['href'])) { $parsed_url = parse_url($item['alternate'][0]['href']); if (isset($parsed_url['scheme']) && isset($parsed_url['host'])) { $site_url = $parsed_url['scheme'] . '://' . $parsed_url['host']; } } // 格式化文章数据 $formattedArticles[] = array( 'site_name' => isset($item['origin']['title']) ? $item['origin']['title'] : '', 'title' => isset($item['title']) ? $item['title'] : '', 'link' => isset($item['alternate'][0]['href']) ? $item['alternate'][0]['href'] : '', 'site_url' => $site_url, 'time' => date('Y-m-d H:i:s', $publishedTime), 'published' => $publishedTime, 'description' => $description, ); } // 按发布时间降序排序 usort($formattedArticles, function ($a, $b) { return $b['published'] - $a['published']; }); // 取前 100 篇文章 $top100 = array_slice($formattedArticles, 0, 100); // 保存到 JSON 文件 saveToJsonFile($top100); echo "\n已将按发布时间降序的前 100 篇文章保存到分页 JSON 文件。\n"; } else { echo "获取文章失败。\n"; } } else { echo "登录失败。\n"; } } // 发送 cURL 请求 function curlRequest($url, $authToken = null) { $ch = curl_init($url); if ($authToken) { $headers = array('Authorization: GoogleLogin auth=' . $authToken); curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); } curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $response = curl_exec($ch); // 检查是否有 cURL 错误 if (curl_errno($ch)) { echo 'cURL 错误: ' . curl_error($ch); } curl_close($ch); return $response; } // 保存数据到 JSON 文件 function saveToJsonFile($data, $filename = 'output.json') { // 每 10 条数据分割为一个 JSON 文件 $chunks = array_chunk($data, 10); $total = count($data); foreach ($chunks as $index => $chunk) { $pageNum = $index + 1; $pageFilename = 'output_' . $pageNum . '.json'; $pageData = [ 'total' => $total, 'page' => $pageNum, 'data' => $chunk ]; $pageJson = json_encode($pageData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); // 写入 JSON 文件 if (file_put_contents($pageFilename, $pageJson) !== false) { echo "{$pageFilename} 写入成功。\n"; } else { echo "{$pageFilename} 写入失败。\n"; } } } // 清理 HTML 内容,提取精简的描述文本 function cleanContent($rawContent, $maxLength = 200) { $patterns = [ '/\s*<img\b[^>]*>\s*/i', // 移除图片标签 '/<iframe\b[^>]*>.*?<\/iframe>/is', // 移除 iframe '/<script\b[^>]*>.*?<\/script>/is', // 移除 script '/<!--.*?-->/s', // 移除 HTML 注释 ]; $cleaned = preg_replace($patterns, '', $rawContent); $cleaned = preg_replace('/\[[^\]]*\]/', '', $cleaned); // 移除方括号内内容 $cleaned = html_entity_decode($cleaned, ENT_QUOTES, 'UTF-8'); $cleaned = strip_tags($cleaned); // 移除 HTML 标签 $cleaned = str_replace(['&nbsp;', '·'], [' ', ''], $cleaned); $cleaned = preg_replace('/[^\x{4E00}-\x{9FFF}A-Za-z0-9\p{P}\p{Z}]+/u', '', $cleaned); // 只保留汉字、字母、数字和标点符号 $cleaned = preg_replace('/[\p{Z}]+/u', ' ', $cleaned); // 规范化空格 $cleaned = trim($cleaned); $description = mb_substr($cleaned, 0, $maxLength, 'UTF-8'); // 截取指定长度的文本 return $description; } // 执行获取文章并保存的函数 getAllSubscribedArticlesAndSaveToJson('这里是FreshRSS的用户名', '这里是FreshRSS设置的api密码', 1000);

page-notes.php 中与友圈(visit)功能相关的主要代码部分:

1.模板中的友圈文章列表渲染部分:

<div v-if="search.type === 'visit'" class="visit-articles notes-list"> <div v-for="article in visitArticles" :key="article.link" class="notes-item visititem"> <div class="tile d-block"> <div class="tile-header flex-center justify-between"> <div class="d-flex align-center justify-between w-100"> <h3 class="text-dark h5 mb-2 mt-2 text-ellipsis flex-auto mr-2"> <a :href="article.link" target="_blank" rel="nofollow">{{ article.title }}</a> </h3> <span class="rss-icon mb-2 mt-2"> <i class="czs-rss"></i> </span> </div> </div> <div class="tile-content p-0"> <div class="flex-wrap d-flex"> <div class="article-content w-100"> <p>{{ article.description }}</p> </div> </div> </div> <div class="tile-footer text-gray text-tiny flex-center justify-between"> <time>{{ article.time }}</time> <span class="author-name text-ellipsis"> <i class="czs-user-l"></i> <a :href="article.site_url" target="_blank" rel="nofollow">{{ article.site_name }}</a> </span> </div> </div> </div> </div>

2.数据相关的属性:

data() { return { visitArticles: [], // 存储友圈文章列表 visitPaging: { // 友圈分页控制 page: 1, rows: 10, total: 0 } } }

3.友圈文章获取方法:

fetchvisitArticles() { if (this.loading) return Promise.resolve(); this.loading = true; return fetch(`https://kkn.me/wp-content/rss/output_${this.visitPaging.page}.json`) .then(res => res.json()) .then(response => { if (this.visitPaging.page === 1) { this.visitArticles = response.data; } else { this.visitArticles.push(...response.data); } this.visitPaging.total = response.total; }) .catch(error => { console.error('Error fetching visit articles:', error); }) .finally(() => (this.loading = false)); }

4.标签切换相关代码:

handleTabs(item) { // ... else if (targetType === "visit") { this.visitPaging.page = 1; this.visitArticles = []; this.fetchvisitArticles().then(() => { this.search.type = targetType; this.$nextTick(() => { this.scrollToTop(); }); }); } // ... }

5.分页加载相关代码:

handleNextPage() { if (this.loading || this.theEnd) return; if (this.search.type === "visit") { if (!this.loading && !this.isLoadingNextPage && this.visitArticles.length < this.visitPaging.total) { this.isLoadingNextPage = true; this.visitPaging.page++; this.fetchvisitArticles().finally(() => { this.isLoadingNextPage = false; }); } return; } // ... }

6.滚动加载检测:

handleScroll() { if (!["beauty", "visit"].includes(this.search.type)) return; if (this.scrollThrottleTimer) return; this.scrollThrottleTimer = setTimeout(() => { const scrollHeight = document.documentElement.scrollHeight; const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const clientHeight = window.innerHeight || document.documentElement.clientHeight; if (scrollHeight - scrollTop - clientHeight < 100) { this.handleNextPage(); } this.scrollThrottleTimer = null; }, 200); }

7.在导航标签中的友圈选项:

tabs: [ { name: "全部", id: "all" }, { name: "文章", id: "post" }, { name: "笔记", id: "note" }, { name: "收藏", id: "beauty" }, { name: "友圈", id: "visit" } ],