diff --git a/index.html b/index.html index 1185d87..11faf1e 100644 --- a/index.html +++ b/index.html @@ -5,23 +5,24 @@ 文章卡片展示 - -
-
- - - - - +
+ + + \ No newline at end of file diff --git a/readme.md b/readme.md index 367913e..9d80052 100644 --- a/readme.md +++ b/readme.md @@ -2,47 +2,90 @@ # Friend-Circle-Lite -友链朋友圈简单版,实现了[友链朋友圈](https://github.com/Rock-Candy-Tea/hexo-circle-of-friends)的基本功能,能够定时爬取rss文章并输出有序内容,为了匹配,输入格式与友链朋友圈的json格式一致,暂不支持从友链页面自动爬取。 +[前端展示](https://fc.liushen.fun) | [详细文档](https://blog.qyliu.top/posts/4dc716ec/) -## 功能介绍 +友链朋友圈简单版,实现了[友链朋友圈](https://github.com/Rock-Candy-Tea/hexo-circle-of-friends)的基本功能,能够定时爬取rss文章并输出有序内容,为了较好的兼容性,输入格式与友链朋友圈的json格式一致,为了轻量化,暂不支持从友链页面自动爬取,下面会附带`hexo-theme-butterfly`主题的解决方案,其他主题可以类比。 -- **友链朋友圈**: 爬取所有友链的文章,结果放置在根目录的all.json文件中。 -- **邮件推送**: 作者可以通过邮箱订阅所有rss的更新(未来开发)。 -- **RSS 订阅**: 基于 GitHub issue 的博客更新邮件订阅功能,游客可以通过简单的提交issue进行邮箱订阅站点更新,支持删除。 +## 展示页面 + +* [清羽飞扬の友链朋友圈](https://blog.qyliu.top/fcircle-lite/) + +* [❖星港◎Star☆ 的友链朋友圈](https://blog.starsharbor.com/fcircle/) +* 欢迎更多 + +## 项目介绍 + +- **爬取文章**: 爬取所有友链的文章,结果放置在根目录的all.json文件中,方便读取并部署到前端。 +- **邮箱推送更新(对作者推送所有友链更新)**: 作者可以通过邮箱订阅所有rss的更新(未来开发)。 +- **issue邮箱订阅(对访客实时推送最新文章邮件)**: 基于`GitHub issue`的博客更新邮件订阅功能,游客可以通过简单的提交`issue`进行邮箱订阅站点更新,删除对应`issue`即可取消订阅。 + +## 特点介绍 + +* **轻量化**:对比原版友链朋友圈的功能,该友圈功能简洁,去掉了设置和fastAPI的臃肿,仅保留关键内容。 +* **无数据库**:因为内容较少,我采用`json`直接存储文章信息,减少数据库操作,提升`action`运行效率。 +* **部署简单**:原版友链朋友圈由于功能多,导致部署较为麻烦,本方案仅需简单的部署action即可使用,vercel仅用于部署前端静态页面和实时获取最新内容。 +* **文件占用**:对比原版`4MB`的`bundle.js`文件大小,本项目仅需要`5.50KB`的`fclite.min.js`文件即可轻量的展示到前端。 + +## 功能概览 + +* 文章爬取 +* 暗色适配 +* 显示作者所有文章 +* 随机钓鱼 +* 邮箱推送 +* 美观邮箱模板 ## 使用方法 +### 前置工作 + 1. **Fork 本仓库:** - 点击页面右上角的 Fork 按钮,将本仓库复制到你自己的 GitHub 账号下。 + 点击页面右上角的 Fork 按钮,将本仓库复制到你自己的`GitHub`账号下。 2. **配置 Secrets:** 在你 Fork 的仓库中,依次进入 `Settings` -> `Secrets` -> `New repository secret`,添加以下 Secrets: - `PAT_TOKEN`: GitHub 的个人访问令牌,用于访问 GitHub API。 - `SMTP_PWD`: SMTP 服务器的密码,用于发送电子邮件。 + ![](./static/1.png) + +2. **配置action权限:** + + 在设置中,点击`action`,拉到最下面,勾选`Read and write permissions`选项并保存,确保action有读写权限。 + 3. **启用 GitHub Actions:** - GitHub Actions 已经配置好在仓库的 `.github/workflows/*.yml` 文件中,当代码推送或定时触发时将自动执行。 + GitHub Actions 已经配置好在仓库的 `.github/workflows/*.yml` 文件中,当到一定时间时将自动执行,也可以手动运行。 其中,每个action功能如下: + - `friend_circle_lite.yml`实现核心功能,爬取并发送邮箱; - `deal_subscribe_issue.yml`处理固定格式的issue,打上固定标签,评论,并关闭issue; - + 4. **设置issue格式:** - 这个我已经设置好了,你只需要检查issue部分是否有对应格式即可。 + 这个我已经设置好了,你只需要检查issue部分是否有对应格式即可,可以自行修改对应参数以进行自定义。 -5. **定制配置:** - 如果需要修改爬虫设置或邮件模板等配置,可以修改仓库中的 `config.yaml` 文件: +### 配置选项 + +1. 如果需要修改爬虫设置或邮件模板等配置,需要修改仓库中的 `config.yaml` 文件: - **爬虫相关配置** 使用 `requests` 库实现友链文章的爬取,并将结果存储到根目录下的 `all.json` 文件中。 + ```yaml spider_settings: enable: true json_url: "https://blog.qyliu.top/friend.json" article_count: 5 ``` - + + `enable`:开启或关闭,默认开启; + + `json_url`:友链朋友圈通用爬取格式第一种(下方有配置方法); + + `article_count`:每个作者留存文章个数。 + - **邮箱推送功能配置** 暂未实现,预留用于将每天的友链文章更新推送给指定邮箱。 + ```yaml email_push: enable: false @@ -50,9 +93,12 @@ subject: "今天的 RSS 订阅更新" body_template: "rss_template.html" ``` - + + **暂未实现**:该部分暂未实现,由于感觉用处不大,保留接口后期酌情更新。 + - **邮箱 issue 订阅功能配置** 通过 GitHub issue 实现向提取的所有邮箱推送博客更新的功能。 + ```yaml rss_subscribe: enable: true @@ -60,9 +106,18 @@ github_repo: Friend-Circle-Lite your_blog_url: https://blog.qyliu.top/ ``` - + + `enable`:开启或关闭,默认开启,如果没有配置请关闭。 + + `github_username`:github用户名,用来拼接github api地址 + + `github_repo`:仓库名称,作用同上。 + + `your_blog_url`:用来定时检测是否有最新文章。 + - **SMTP 配置** 使用配置中的相关信息实现邮件发送功能。 + ```yaml smtp: email: 3162475700@qq.com @@ -70,10 +125,106 @@ port: 587 use_tls: true ``` + + `email`:发件人邮箱地址 + + `server`:`SMTP` 服务器地址 + + `port`:`SMTP` 端口号 + + `use_tls`:是否使用 `tls` 加密 + + 这部分配置较为复杂,请自行学习使用。 -6. **贡献与定制:** +2. **贡献与定制:** 欢迎对仓库进行贡献或根据需要进行定制。 +### 友圈json生成 + +1. 将以下文件放置到博客根目录: + + ```javascript + const YML = require('yamljs') + const fs = require('fs') + + let friends = [], + data_f = YML.parse(fs.readFileSync('source/_data/link.yml').toString().replace(/(?<=rss:)\s*\n/g, ' ""\n')); + + data_f.forEach((entry, index) => { + let lastIndex = 2; + if (index < lastIndex) { + const filteredLinkList = entry.link_list.filter(linkItem => !blacklist.includes(linkItem.name)); + friends = friends.concat(filteredLinkList); + } + }); + + // 根据规定的格式构建 JSON 数据 + const friendData = { + friends: friends.map(item => { + return [item.name, item.link, item.avatar]; + }) + }; + + // 将 JSON 对象转换为字符串 + const friendJSON = JSON.stringify(friendData, null, 2); + + // 写入 friend.json 文件 + fs.writeFileSync('./source/friend.json', friendJSON); + + console.log('friend.json 文件已生成。'); + ``` + +2. 在根目录下运行: + + ```bash + node link.js + ``` + + 你将会在source文件中发现文件`friend.json`,即为对应格式文件,下面正常hexo三件套即可放置到网站根目录。 + +3. (可选)添加运行命令到脚本中方便执行,在根目录下创建: + + ```bash + @echo off + E: + cd E:\Programming\HTML_Language\willow-God\blog + node link.js && hexo g && hexo algolia && hexo d + ``` + + 地址改成自己的,上传时仅需双击即可完成。 + + 如果是github action,可以在hexo g脚本前添加即可完整构建,注意需要安装yaml包才可解析yml文件。 + +## 部署到网页 + +首先,将该项目部署到vercel,部署到vercel的目的主要是利用vercel检测仓库并实时刷新的功能,及时获取all.json文件内容。任意平台均可。 + +部署完成后,你将获得一个地址,建议自行绑定域名。 + +在前端页面的md文件中写入: + +```html +
+ + + +``` + +其中第一个地址填入你自己的地址即可,**注意**尾部带`/`,不要遗漏。 + +然后你就可以在前端页面看到我们的结果了。效果图如上展示网站,其中两个文件你可以自行修改,在同目录下我也提供了未压缩版本,有基础的可以很便捷的进行修改。 + ## 问题与贡献 如果遇到任何问题或有建议,请[提交一个 issue](https://github.com/willow-god/Friend-Circle-Lite/issues)。欢迎贡献代码! diff --git a/static/1.png b/static/1.png new file mode 100644 index 0000000..75aef41 Binary files /dev/null and b/static/1.png differ diff --git a/static/script.js b/static/script.js deleted file mode 100644 index 13e952f..0000000 --- a/static/script.js +++ /dev/null @@ -1,132 +0,0 @@ -document.addEventListener("DOMContentLoaded", function () { - const container = document.getElementById('articles-container'); - let start = 0; - const batchSize = 20; // 每次加载的卡片数量 - - function loadMoreArticles() { - fetch('https://fc.liushen.fun/all.json') - .then(response => response.json()) - .then(data => { - allArticles = data.article_data; - const randomArticle = allArticles[Math.floor(Math.random() * allArticles.length)]; - const articles = data.article_data.slice(start, start + batchSize); - const randomArticleElement = document.getElementById('random-article'); - - randomArticleElement.innerHTML = ` -
-
随机钓鱼
-
${randomArticle.title}
-
作者: ${randomArticle.author}
-
- - `; - - articles.forEach(article => { - const card = document.createElement('div'); - card.className = 'card'; - - const title = document.createElement('div'); - title.className = 'card-title'; - title.innerText = article.title; - card.appendChild(title); - - title.onclick = () => { - window.open(article.link, '_blank'); - }; - - const author = document.createElement('div'); - author.className = 'card-author'; - const authorImg = document.createElement('img'); - authorImg.src = article.avatar; - author.appendChild(authorImg); - author.appendChild(document.createTextNode(article.author)); - card.appendChild(author); - - author.onclick = () => { - showAuthorArticles(article.author, article.avatar, article.link); - }; - - const date = document.createElement('div'); - date.className = 'card-date'; - date.appendChild(document.createTextNode(article.created.substring(0, 10))); - card.appendChild(date); - - const bgImg = document.createElement('img'); - bgImg.className = 'card-bg'; - bgImg.src = article.avatar; - card.appendChild(bgImg); - - container.appendChild(card); - }); - - start += batchSize; - - if (start >= data.article_data.length) { - // 如果加载完所有卡片,隐藏加载更多按钮 - document.getElementById('load-more-btn').style.display = 'none'; - } - }); - } - - // 显示作者文章的函数 - function showAuthorArticles(author, avatar, link) { - const modal = document.getElementById('modal'); - const modalArticlesContainer = document.getElementById('modal-articles-container'); - const modalAuthorAvatar = document.getElementById('modal-author-avatar'); - const modalAuthorNameLink = document.getElementById('modal-author-name-link'); - - modalArticlesContainer.innerHTML = ''; // 清空之前的内容 - modalAuthorAvatar.src = avatar; - modalAuthorNameLink.innerText = author; - modalAuthorNameLink.href = new URL(link).origin; - - const authorArticles = allArticles.filter(article => article.author === author); - authorArticles.forEach(article => { - const articleDiv = document.createElement('div'); - articleDiv.className = 'modal-article'; - - const title = document.createElement('a'); - title.className = 'modal-article-title'; - title.innerText = article.title; - title.href = article.link; - title.target = '_blank'; - articleDiv.appendChild(title); - - const date = document.createElement('div'); - date.className = 'modal-article-date'; - date.innerText = "--" + article.created.substring(0, 10); - articleDiv.appendChild(date); - - modalArticlesContainer.appendChild(articleDiv); - }); - - // 设置类名以触发显示动画 - modal.style.display = 'block'; - setTimeout(() => { - modal.classList.add('modal-open'); - }, 10); // 确保显示动画触发 - } - - // 隐藏模态框的函数 - function hideModal() { - const modal = document.getElementById('modal'); - modal.classList.remove('modal-open'); - modal.addEventListener('transitionend', () => { - modal.style.display = 'none'; - }, { once: true }); - } - - // 初始加载 - loadMoreArticles(); - - // 加载更多按钮点击事件 - document.getElementById('load-more-btn').addEventListener('click', loadMoreArticles); - - // 点击遮罩层关闭模态框 - window.onclick = function(event) { - const modal = document.getElementById('modal'); - if (event.target === modal) { - hideModal(); - } - }; -}); diff --git a/static/styles.css b/static/styles.css deleted file mode 100644 index 8717593..0000000 --- a/static/styles.css +++ /dev/null @@ -1,371 +0,0 @@ -/* 浅色模式颜色 */ -[data-theme=light] { - --card-title-color: #000000; - --body-background-color: #f4f4f9; - --random-container-title-color: #000000; - --random-article-bg-color: white; - --random-article-shadow-color: rgba(0, 0, 0, 0.1); - --random-container-hover-color: #3498db; - --random-author-color: gray; - --random-title-color: #000000; - --modal-bg-color: rgba(255, 255, 255, 0.5); - --modal-bg-blur: 25px; - --modal-content-bg-color: rgba(239, 250, 255, 0.7); - --modal-content-border-color: #ccc; - --modal-article-title-color: #000000; - --modal-article-date-color: #313131; - --load-more-btn-bg-color: white; - --load-more-btn-border-color: #b5b5b5; - --load-more-btn-hover-color: #0088ff; - --card-bg-color: white; - --card-border-color: #e3e8f7; - --card-title-hover-color: #007bff; - --card-author-color: #313131; - --card-author-bg-color: white; - --card-author-shadow-color: rgba(0, 0, 0, 0.1); -} - -/* 深色模式颜色 */ -[data-theme=dark] { - --card-title-color: #ffffff; - --body-background-color: #181818; - --random-container-title-color: #ffffff; - --random-article-bg-color: #2c2c2c; - --random-article-shadow-color: rgba(255, 255, 255, 0.1); - --random-container-hover-color: #3498db; - --random-author-color: #b3b3b3; - --random-title-color: #ffffff; - --modal-bg-color: rgba(0, 0, 0, 0.3); - --modal-bg-blur: 25px; - --modal-content-bg-color: rgba(20, 20, 20, 0.5); - --modal-content-border-color: #42444a; - --modal-article-title-color: #ffffff; - --modal-article-date-color: #b3b3b3; - --load-more-btn-bg-color: #2c2c2c; - --load-more-btn-border-color: #42444a; - --load-more-btn-hover-color: #0088ff; - --card-bg-color: #2c2c2c; - --card-border-color: #42444a; - --card-title-hover-color: #0088ff; - --card-author-color: #dddddd; - --card-author-bg-color: #2c2c2c; - --card-author-shadow-color: rgba(255, 255, 255, 0.1); -} - -/* 以下是原有样式,使用新定义的变量 */ -body { - font-family: Arial, sans-serif; - background-color: var(--body-background-color); - margin: 0px; - padding: 20px; - display: flex; - justify-content: center; - align-items: center; - flex-direction: column; - min-height: 100vh; -} - -/* 随机友链文章卡片 */ -#random-article { - display: flex; - position: relative; - width: 100%; - margin: 10px 20px; - background-color: var(--random-article-bg-color); - border-radius: 10px; - box-shadow: 0 4px 8px var(--random-article-shadow-color); - height: 210px; -} - -.random-container { - position: relative; - margin: 20px; - width: 100%; - height: 170px; -} - -.random-container:hover .random-title { - font-size: 32px; -} - -.random-author { - font-size: 14px; - color: var(--random-author-color); - margin-bottom: 10px; -} - -.random-container-title { - color: var(--random-container-title-color); - font-size: 20px; - font-weight: 700; - margin-bottom: 20px; -} - -.random-link-button { - position: absolute; - bottom: 20px; - right: 20px; - padding: 10px 20px; - border: none; - border-radius: 20px; - background-color: var(--random-container-hover-color); - color: #fff; - cursor: pointer; - font-size: 14px; - transition: background-color 0.3s ease-in-out; -} - -.random-title { - margin-bottom: 10px; - font-size: 30px; - color: var(--random-title-color); - transition: font-size 0.3s ease-in-out; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -/* 模态框样式 */ -.modal { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: var(--modal-bg-color); - backdrop-filter: blur(var(--modal-bg-blur)); - /* 应用高斯模糊效果,可以根据需要调整模糊程度 */ - -webkit-backdrop-filter: blur(var(--modal-bg-blur)); - /* 兼容性前缀,适用于一些旧版本的浏览器 */ - z-index: 999; - opacity: 0; - /* 初始透明度 */ - visibility: hidden; - /* 初始不可见 */ - transition: opacity 0.3s; - /* 过渡效果,持续时间为 0.3 秒 */ -} - -.modal-open { - opacity: 1; - /* 透明度变为完全不透明 */ - visibility: visible; - /* 可见 */ -} - -.modal-content { - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); - width: 320px; - /* transform: translate(-50%, -50%); */ - background-color: var(--modal-content-bg-color); - padding: 20px; - border: 1px solid var(--modal-content-border-color); - z-index: 1000; - max-height: 90%; - overflow-y: auto; - border-radius: 20px; - transition: opacity 0.3s; - /* 过渡效果,持续时间为 0.3 秒 */ -} - -@media screen and (max-width: 400px) { - .modal-content { - width: 80%; - } -} - -#modal-author-avatar { - display: block; - margin: 0 auto 10px; - /* 垂直方向上自动居中,底部留出间距 */ - border-radius: 50%; - /* 圆形图标 */ - width: 80px; - height: 80px; -} - -#modal-author-name-link { - display: block; - text-align: center; - font-size: 15px; - margin: 25px 0; - color: var(--random-container-hover-color); - text-decoration: none; -} - -#modal-author-name-link:hover { - text-decoration: underline; -} - -.modal-content hr { - margin: 20px 0; -} - -#modal-articles-container { - border-top: var(--random-container-hover-color) double 2px; - margin-top: 20px; - padding-top: 10px; -} - -.modal-article { - display: flex; - flex-wrap: wrap; - /* This property allows the elements to wrap onto multiple lines */ - margin-bottom: 10px; - padding-bottom: 10px; - border-bottom: var(--random-container-hover-color) dashed 1px; -} - -.modal-article .modal-article-title { - font-size: 18px; - cursor: pointer; - color: var(--modal-article-title-color); - height: 2.5em; /* 两行高度 */ - width: 100%; - margin-bottom: 5px; - text-decoration: none; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; - overflow: hidden; - text-overflow: ellipsis; -} - -.modal-article .modal-article-title:hover { - color: var(--random-container-hover-color); - text-decoration: underline; -} - -.modal-article .modal-article-date { - font-size: 12px; - width: 100%; - color: var(--modal-article-date-color); - padding: 5px; - cursor: default; - text-align: right; - /* Add this line to align the text to the right */ -} - -/* 其他样式... */ - -.articles-container { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); - gap: 8px; - width: 100%; -} - -#load-more-btn { - font-size: 15px; - color: var(--modal-article-date-color); - background-color: var(--load-more-btn-bg-color); - margin-top: 20px; - width: 200px; - border-radius: 20px; - padding: 3px; - border: var(--load-more-btn-border-color) solid 1px; - transition: all 0.3s; -} - -#load-more-btn:hover { - background-color: var(--load-more-btn-hover-color); - width: 300px; - color: white; -} - -.card { - background-color: var(--card-bg-color); - border-radius: 10px; - padding: 10px; - border: 1px solid var(--card-border-color); - position: relative; - overflow: hidden; - display: flex; - flex-direction: column; - justify-content: space-between; - height: 120px; -} - -.card-title { - color: var(--card-title-color); - z-index: 1; - font-size: 17px; - cursor: pointer; - margin-bottom: 10px; - line-height: 1.5; - max-height: 4.5em; - /* 三行高度 */ - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - transition: color 0.3s; - /* 添加过渡效果 */ -} - -.card-title:hover { - /* 字体变成蓝色,下划线 */ - color: var(--card-title-hover-color); - text-decoration: underline; -} - -.card-author, -.card-date { - font-size: 12px; - color: var(--card-author-color); - padding: 5px; - /* 内边距 */ - transition: box-shadow 0.2s; - /* 过渡效果 */ -} - -.card-author:hover { - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); - /* 鼠标悬停时加深阴影 */ -} - -.card-author { - cursor: pointer; - background-color: var(--card-author-bg-color); - /* 白色背景 */ - box-shadow: 0 2px 4px var(--card-author-shadow-color); - /* 阴影 */ - border-radius: 15px; - /* 圆角 */ - display: flex; - padding-right: 10px; - width: fit-content; - align-items: center; -} - -.card-author img { - border-radius: 50%; - width: 15px; - height: 15px; - margin-right: 5px; -} - -.card-date { - position: absolute; - z-index: 1; - bottom: 10px; - cursor: default; - right: 10px; - display: flex; - align-items: center; -} - -.card-bg { - z-index: 0; - border-radius: 50%; - position: absolute; - bottom: -20px; - right: -15px; - width: 140px; - height: 140px; - opacity: 0.4; -} \ No newline at end of file