Compare commits

...

28 Commits
v1.4 ... master

Author SHA1 Message Date
c5586f9621 new ui 2024-06-03 23:41:01 +08:00
3a4a22587a new ui 2024-06-03 22:34:18 +08:00
srcrs
129b3916ab fix mobile countdown show 2024-04-28 00:59:14 +08:00
srcrs
9f30853ded page add countdown and fix autoUpdatePush zero 2024-04-20 12:55:05 +08:00
srcrs
dcbeeebc20 add AutoUpdatePush 2024-04-20 12:52:42 +08:00
srcrs
3b2fb607a3 close mobile reload 2024-04-19 04:27:29 +08:00
srcrs
a9fdd31a4e Merge branch 'main' of github.com:srcrs/rss-reader 2023-10-15 19:34:36 +08:00
srcrs
8796bcc6c7 ws close to reload 2023-10-15 19:27:35 +08:00
srcrs
79ab3e511a 更改文件权限 2023-10-14 14:36:25 -04:00
srcrs
3bf1dc961b update 2023-10-15 02:04:05 +08:00
srcrs
837f187db7 add fsnotify 2023-10-15 02:03:42 +08:00
srcrs
4b968b7c81 add 2023-10-15 01:22:30 +08:00
srcrs
d0e4e09aab 兼容代码结构改变,增加夜间模式 2023-10-15 01:18:58 +08:00
srcrs
40840821cb 添加日间时间 2023-10-15 01:18:30 +08:00
srcrs
55496722ce 移动文件位置 2023-10-15 01:18:01 +08:00
srcrs
1580b9197d 增加dark-css 2023-10-15 01:16:56 +08:00
srcrs
a1d7b02b36 调整代码结构 2023-10-15 01:16:07 +08:00
srcrs
762ca0e6ec 更新说明 2023-09-23 00:01:59 +08:00
srcrs
9215669443 更新说明 2023-09-23 00:01:38 +08:00
srcrs
a42e18b5f5 修复模版取文件index.html错误 2023-09-02 23:59:25 +08:00
srcrs
d115310bb9 添加nginx配置说明 2023-09-02 23:46:04 +08:00
srcrs
007eae0b9d 添加seo相关标签,以及seo优化 2023-09-02 23:45:35 +08:00
srcrs
6160450fcf
Merge pull request #14 from Lyric-c/main
一点优化
2023-08-28 22:49:04 +08:00
Lyric-c
57ec9939ab feed内容无更新时无需更新缓存 2023-08-18 10:28:52 +08:00
Lyric-c
c8583357cf 修改时间显示不更新 2023-08-18 10:22:04 +08:00
Lyric-c
e3250bf686 优化内存缓存;精简非必要字段 2023-08-11 16:13:46 +08:00
Lyric-c
b743521695 更新feed时串行改为并行 2023-08-11 15:27:09 +08:00
srcrs
fd30dfaedb 删除部分挂载 2023-08-09 22:25:14 +08:00
22 changed files with 694 additions and 250 deletions

View File

@ -1,12 +1,30 @@
# 简述 # 简述
RSS将信息聚合曾寻找过一些RSS客户端但觉得都太过于复杂会需要登陆、保存历史消息、 实时展示rss订阅最新消息。
使用缓存加快响应速度但我想要看到的是打开页面看到关注网站的即时消息即可一般通过RSS订阅获取到的数据即是热点
看到有感兴趣的信息,可以跳转过去再详细的了解。 ## 特性
- 打包后镜像大小仅有约20MB通过docker实现一键部署
- 支持自定义配置页面数据自动刷新
- 响应式布局,能够兼容不同的屏幕大小
- 良好的SEO首次加载使用模版引擎快速展示页面内容
- 支持添加多个RSS订阅链接
- 简洁的页面布局,可以查看每个订阅链接最后更新时间
- 支持夜间模式
- config.json配置文件支持热更新
2023年7月28日进行了界面改版和升级 2023年7月28日进行了界面改版和升级
![](demo.png) ![](pc.png)
![](mobile.png)
# 配置文件 # 配置文件
@ -28,7 +46,9 @@ RSS将信息聚合曾寻找过一些RSS客户端但觉得都太过于复
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg" "https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
], ],
"refresh": 6, "refresh": 6,
"autoUpdatePush": 7 "autoUpdatePush": 7,
"nightStartTime": "06:30:00",
"nightEndTime": "19:30:00"
} }
``` ```
@ -37,6 +57,8 @@ RSS将信息聚合曾寻找过一些RSS客户端但觉得都太过于复
values | rss订阅链接必填 values | rss订阅链接必填
refresh | rss订阅更新时间间隔单位分钟必填 refresh | rss订阅更新时间间隔单位分钟必填
autoUpdatePush | 自动刷新间隔默认为0不开启。效果为前端每autoUpdatePush分钟自动更新页面信息单位分钟非必填 autoUpdatePush | 自动刷新间隔默认为0不开启。效果为前端每autoUpdatePush分钟自动更新页面信息单位分钟非必填
nightStartTime | 日间开始时间 ,如 06:30:00
nightEndTime | 日间结束时间,如 19:30:00
# 使用方式 # 使用方式
@ -66,7 +88,7 @@ docker-compose up -d
# nginx反代 # nginx反代
这里需要注意/ws若不设置proxy_read_timeout参数则默认1分钟断开。 这里需要注意/ws若不设置proxy_read_timeout参数则默认1分钟断开。静态文件增加gzip可以大幅压缩网络传输数据
```conf ```conf
server { server {
@ -74,6 +96,8 @@ server {
server_name rss.lass.cc; server_name rss.lass.cc;
ssl_certificate fullchain.cer; ssl_certificate fullchain.cer;
ssl_certificate_key lass.cc.key; ssl_certificate_key lass.cc.key;
gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
location / { location / {
proxy_pass http://localhost:8080; proxy_pass http://localhost:8080;
} }

5
config.json Normal file → Executable file
View File

@ -5,7 +5,6 @@
"http://www.ruanyifeng.com/blog/atom.xml", "http://www.ruanyifeng.com/blog/atom.xml",
"https://feeds.appinn.com/appinns/", "https://feeds.appinn.com/appinns/",
"https://v2ex.com/feed/tab/tech.xml", "https://v2ex.com/feed/tab/tech.xml",
"https://www.cmooc.com/feed",
"http://www.sciencenet.cn/xml/blog.aspx?di=30", "http://www.sciencenet.cn/xml/blog.aspx?di=30",
"https://www.douban.com/feed/review/book", "https://www.douban.com/feed/review/book",
"https://www.douban.com/feed/review/movie", "https://www.douban.com/feed/review/movie",
@ -13,5 +12,7 @@
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg" "https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
], ],
"refresh": 6, "refresh": 6,
"autoUpdatePush": 7 "autoUpdatePush": 7,
"nightStartTime": "06:30:00",
"nightEndTime": "19:30:00"
} }

BIN
demo.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1010 KiB

View File

@ -8,6 +8,4 @@ services:
ports: ports:
- "8080:8080" - "8080:8080"
volumes: volumes:
- "$PWD/index.html:/app/index.html"
- "$PWD/db:/app/db"
- "$PWD/config.json:/app/config.json" - "$PWD/config.json:/app/config.json"

39
globals/global.go Normal file
View File

@ -0,0 +1,39 @@
package globals
import (
"embed"
"rss-reader/models"
"sync"
"github.com/gorilla/websocket"
"github.com/mmcdole/gofeed"
)
var (
DbMap map[string]models.Feed
RssUrls models.Config
Upgrader = websocket.Upgrader{}
Lock sync.RWMutex
//go:embed static
DirStatic embed.FS
HtmlContent []byte
Fp = gofeed.NewParser()
)
func Init() {
conf, err := models.ParseConf()
if err != nil {
panic(err)
}
RssUrls = conf
// 读取 index.html 内容
HtmlContent, err = DirStatic.ReadFile("static/index.html")
if err != nil {
panic(err)
}
DbMap = make(map[string]models.Feed)
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 781 B

After

Width:  |  Height:  |  Size: 781 B

114
globals/static/index-1.html Normal file

File diff suppressed because one or more lines are too long

1
globals/static/index.css Normal file

File diff suppressed because one or more lines are too long

235
globals/static/index.html Normal file
View File

@ -0,0 +1,235 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSS Reader</title>
<meta name="description" content='RSS Reader一个将订阅信息聚合展示的开源web工具便于了解近期关注的信息同时页面数据数据也实现了自动刷新。'>
<meta name="keywords" content="<< .Keywords >>">
<meta name="anthor" content="srcrs">
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link rel="stylesheet" href="static/index.css">
<< if .DarkMode >>
<!-- <link rel="stylesheet" href="static/dark-mode.css">-->
<< end >>
<link rel="icon" href="static/favicon.svg" type="image/x-icon">
<style>
body {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 30px;
}
.card-header {
font-size: 15px;
font-weight: bold;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: left;
margin-bottom: 10px;
background: linear-gradient(to bottom, #007f80, #007070);
color: white;
line-height: 2em;
border-radius: 6px;
padding: 0 0.5em;
}
.list-item {
display: flex;
align-items: center;
text-align: left;
width: 100%;
}
.list-item-title {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
flex-grow: 1;
text-align: left;
width: 100%;
margin-bottom: 10px;
font-size: 15px;
align-items: center; /* 确保内容垂直居中 */
}
.title-link {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
text-align: left;
}
a {
color: black;
text-decoration: none;
}
.feed-col {
margin-bottom: 20px;
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h1>
RSS Reader
<< if gt .AutoUpdatePush 0 >>
<span v-show="isPc"><br/>{{ countdown }} s</span>
<< end >>
</h1>
</el-header>
<el-main v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中">
<el-row :gutter="20">
<< range $index, $feed :=.RssDataList >>
<el-col v-if="showSEOFlag" :xs="24" :sm="12" :md="8" :lg="6" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>
<< $feed.Title >>
</span>
</div>
<el-scrollbar style="height: 580px;">
<< range $i, $item :=$feed.Items >>
<el-list key="<< $i >>">
<el-list-item>
<div class="list-item-title">
<span>
<< inc $i >>.
</span>
<el-link href="<< $item.Link >>" target="_blank" title="<< $item.Title >>">
<< $item.Title >>
</el-link>
</div>
</el-list-item>
</el-list>
<< end >>
</el-scrollbar>
<div slot="footer" class="card-footer" style="height: 10px;">
<time class="time">
<< $feed.Custom.lastupdate >>
</time>
</div>
</el-card>
</el-col>
<< end>>
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="(feed, index) in feeds" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>{{ feed.title }}</span>
</div>
<el-scrollbar style="height: 580px;">
<el-list v-for="(item, i) in feed.items" :key="i">
<el-list-item>
<div class="list-item-title">
<span>{{ i+1 }}. </span>
<el-link :href="item.link" target="_blank" :title="item.title">{{ item.title }}</el-link>
</div>
</el-list-item>
</el-list>
</el-scrollbar>
<!-- <div slot="footer" class="card-footer" style="height: 10px;">-->
<!-- <time class="time">{{ feed.custom.lastupdate }}</time>-->
<!-- </div>-->
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer>
<el-link href="https://github.com/srcrs/rss-reader" target="_blank">Rss-reader</el-link>
<span> | </span>
<el-link href="https://github.com/srcrs" target="_blank">By srcrs</el-link>
</el-footer>
</el-container>
</div>
<!-- <script src="https://unpkg.com/vue@next"></script> -->
<script src="static/vue.global.prod.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.full.js"></script> -->
<script src="static/index.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
feeds: [],
showSEOFlag: true,
fullscreenLoading: true,
countdown: 60,
isPc: true,
autoUpdatePush: << .AutoUpdatePush >>,
};
},
async created() {
this.fullscreenLoading = false;
// 使用媒体查询判断设备类型
this.isPc = !window.matchMedia('(max-width: 767px)').matches;
},
async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const connect = () => {
const socket = new WebSocket(protocol + window.location.host + "/ws");
socket.onmessage = event => {
const feed = JSON.parse(event.data);
const existingFeed = this.feeds.find(f => f.link === feed.link);
if (existingFeed) {
Object.assign(existingFeed, feed);
} else {
this.feeds.push(feed);
}
this.showSEOFlag = false;
};
const reloadHtml = () => {
if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
if (document.visibilityState === 'visible') {
// 刷新网页
console.log("reload...")
location.reload();
}
}
}
socket.onclose = event => {
if (this.isPc && this.autoUpdatePush > 0) {
console.log("WebSocket closed. Reconnecting...");
setInterval(reloadHtml, 3000);
}
};
// Send heartbeat message every 60 seconds
const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat");
} else if (socket.readyState === WebSocket.CLOSED || socket.readyState === WebSocket.CLOSING) {
reloadHtml()
}
};
if (this.isPc && this.autoUpdatePush > 0) {
setInterval(sendHeartbeat, 60000);
setInterval(() => {
if (this.countdown > 0) {
this.countdown--;
} else {
this.countdown = 60;
}
}, 1000);
}
};
connect();
},
beforeDestroy() {
// 在组件销毁前手动关闭 WebSocket 连接
this.socket.close();
}
});
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

2
go.mod
View File

@ -3,6 +3,7 @@ module rss-reader
go 1.18 go 1.18
require ( require (
github.com/fsnotify/fsnotify v1.6.0
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
github.com/mmcdole/gofeed v1.2.1 github.com/mmcdole/gofeed v1.2.1
) )
@ -15,5 +16,6 @@ require (
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
golang.org/x/net v0.4.0 // indirect golang.org/x/net v0.4.0 // indirect
golang.org/x/sys v0.3.0 // indirect
golang.org/x/text v0.5.0 // indirect golang.org/x/text v0.5.0 // indirect
) )

5
go.sum
View File

@ -5,6 +5,8 @@ github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEq
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
@ -29,6 +31,9 @@ golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=

View File

@ -1,155 +0,0 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RSS Reader</title>
<!-- <link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css"> -->
<link rel="stylesheet" href="static/index.min.css">
<link rel="icon" href="static/favicon.svg" type="image/x-icon">
<style>
body {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 30px;
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
font-weight: bold;
}
.list-item {
display: flex;
align-items: center;
text-align: left;
width: 100%;
}
.list-item-title {
display: flex;
/* white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis; */
flex-grow: 1;
text-align: left;
width: 100%;
margin-bottom: 10px;
}
a {
color: black;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.feed-col {
margin-bottom: 20px;
}
.time {
font-size: 12px;
color: #999;
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-header>
<h1>RSS Reader</h1>
</el-header>
<el-main
v-loading.fullscreen.lock="fullscreenLoading"
element-loading-text="拼命加载中"
>
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="(feed, index) in feeds" :key="index" class="feed-col">
<el-card class="box-card">
<div slot="header" class="card-header">
<span>{{ feed.title }}</span>
</div>
<el-scrollbar style="height: 300px;">
<el-list v-for="(item, i) in feed.items" :key="i">
<el-list-item>
<div class="list-item-title">
<span>{{ i+1 }}. </span>
<el-link :href="item.link" target="_blank" :title="item.title">{{ item.title }}</el-link>
</div>
</el-list-item>
</el-list>
</el-scrollbar>
<div slot="footer" class="card-footer" style="height: 10px;">
<time class="time">{{ feed.custom.lastupdate }}</time>
</div>
</el-card>
</el-col>
</el-row>
</el-main>
<el-footer>
<el-link href="https://github.com/srcrs/rss-reader" target="_blank">Rss-reader</el-link>
<span> | </span>
<el-link href="https://github.com/srcrs" target="_blank">By srcrs</el-link>
</el-footer>
</el-container>
</div>
<!-- <script src="https://unpkg.com/vue@next"></script> -->
<script src="static/vue.global.prod.js"></script>
<!-- <script src="https://cdn.bootcdn.net/ajax/libs/element-plus/2.3.3/index.full.js"></script> -->
<script src="static/index.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
feeds: [],
fullscreenLoading: true,
};
},
async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
const connect = () => {
const socket = new WebSocket(protocol + window.location.host + "/ws");
socket.onmessage = event => {
const feed = JSON.parse(event.data);
const existingFeed = this.feeds.find(f => f.link === feed.link);
if (existingFeed) {
Object.assign(existingFeed, feed);
} else {
this.feeds.push(feed);
}
this.fullscreenLoading = false;
};
socket.onclose = event => {
console.log("WebSocket closed. Reconnecting...");
setTimeout(connect, 300000);
};
// Send heartbeat message every 120 seconds
const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat");
}
};
setInterval(sendHeartbeat, 120000);
};
connect();
},
beforeDestroy() {
// 在组件销毁前手动关闭 WebSocket 连接
this.socket.close();
}
});
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>

171
main.go
View File

@ -1,87 +1,111 @@
package main package main
import ( import (
"embed"
"encoding/json" "encoding/json"
"io/ioutil" "html/template"
"log" "log"
"net/http" "net/http"
"sync" "rss-reader/globals"
"rss-reader/models"
"rss-reader/utils"
"time" "time"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/mmcdole/gofeed"
)
type Config struct {
Values []string `json:"values"`
ReFresh int `json:"refresh"`
AutoUpdatePush int `json:"autoUpdatePush"`
}
var (
dbMap sync.Map
rssUrls Config
upgrader = websocket.Upgrader{}
//go:embed static
dirStatic embed.FS
//go:embed index.html
fileIndex embed.FS
htmlContent []byte
) )
func init() { func init() {
// 读取配置文件 globals.Init()
data, err := ioutil.ReadFile("config.json")
if err != nil {
panic(err)
}
// 解析JSON数据到Config结构体
err = json.Unmarshal(data, &rssUrls)
if err != nil {
panic(err)
}
// 读取 index.html 内容
htmlContent, err = fileIndex.ReadFile("index.html")
if err != nil {
panic(err)
}
} }
func main() { func main() {
go updateFeeds() go utils.UpdateFeeds()
go utils.WatchConfigFileChanges("config.json")
http.HandleFunc("/feeds", getFeedsHandler) http.HandleFunc("/feeds", getFeedsHandler)
http.HandleFunc("/ws", wsHandler) http.HandleFunc("/ws", wsHandler)
http.HandleFunc("/", serveHome) // http.HandleFunc("/", serveHome)
http.HandleFunc("/", tplHandler)
//加载静态文件 //加载静态文件
fs := http.FileServer(http.FS(dirStatic)) fs := http.FileServer(http.FS(globals.DirStatic))
http.Handle("/static/", fs) http.Handle("/static/", fs)
log.Fatal(http.ListenAndServe(":8080", nil)) log.Fatal(http.ListenAndServe(":8080", nil))
} }
func serveHome(w http.ResponseWriter, r *http.Request) { func serveHome(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "text/html; charset=utf-8") w.Header().Add("Content-Type", "text/html; charset=utf-8")
w.Write(htmlContent) w.Write(globals.HtmlContent)
}
func tplHandler(w http.ResponseWriter, r *http.Request) {
// 创建一个新的模板,并设置自定义分隔符为<< >>避免与Vue的语法冲突
tmplInstance := template.New("index.html").Delims("<<", ">>")
//添加加法函数计数
funcMap := template.FuncMap{
"inc": func(i int) int {
return i + 1
},
}
// 加载模板文件
tmpl, err := tmplInstance.Funcs(funcMap).ParseFS(globals.DirStatic, "static/index.html")
if err != nil {
log.Println("模板加载错误:", err)
return
}
//判断现在是否是夜间
formattedTime := time.Now().Format("15:04:05")
darkMode := false
if globals.RssUrls.NightStartTime != "" && globals.RssUrls.NightEndTime != "" {
if globals.RssUrls.NightStartTime > formattedTime || formattedTime > globals.RssUrls.NightEndTime {
darkMode = true
}
}
// 定义一个数据对象
data := struct {
Keywords string
RssDataList []models.Feed
DarkMode bool
AutoUpdatePush int
}{
Keywords: getKeywords(),
RssDataList: utils.GetFeeds(),
DarkMode: darkMode,
AutoUpdatePush: globals.RssUrls.AutoUpdatePush,
}
// 渲染模板并将结果写入响应
err = tmpl.Execute(w, data)
if err != nil {
log.Println("模板渲染错误:", err)
}
} }
func wsHandler(w http.ResponseWriter, r *http.Request) { func wsHandler(w http.ResponseWriter, r *http.Request) {
conn, err := upgrader.Upgrade(w, r, nil) conn, err := globals.Upgrader.Upgrade(w, r, nil)
if err != nil { if err != nil {
log.Printf("Upgrade failed: %v", err) log.Printf("Upgrade failed: %v", err)
return return
} }
defer conn.Close() defer conn.Close()
for { for {
for _, url := range rssUrls.Values { for _, url := range globals.RssUrls.Values {
feedJSON, ok := dbMap.Load(url) globals.Lock.RLock()
cache, ok := globals.DbMap[url]
globals.Lock.RUnlock()
if !ok { if !ok {
log.Printf("Error getting feed from db is null %v", url) log.Printf("Error getting feed from db is null %v", url)
continue continue
} }
err = conn.WriteMessage(websocket.TextMessage, []byte(feedJSON.(string))) data, err := json.Marshal(cache)
if err != nil {
log.Printf("json marshal failure: %s", err.Error())
continue
}
err = conn.WriteMessage(websocket.TextMessage, data)
//错误直接关闭更新 //错误直接关闭更新
if err != nil { if err != nil {
log.Printf("Error sending message or Connection closed: %v", err) log.Printf("Error sending message or Connection closed: %v", err)
@ -89,55 +113,34 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
//如果未配置则不自动更新 //如果未配置则不自动更新
if rssUrls.AutoUpdatePush == 0 { if globals.RssUrls.AutoUpdatePush == 0 {
return return
} }
time.Sleep(time.Duration(rssUrls.AutoUpdatePush) * time.Minute) time.Sleep(time.Duration(globals.RssUrls.AutoUpdatePush) * time.Minute)
} }
} }
func updateFeeds() { //获取关键词也就是title
for { //获取feeds列表
for _, url := range rssUrls.Values { func getKeywords() string {
fp := gofeed.NewParser() words := ""
feed, err := fp.ParseURL(url) for _, url := range globals.RssUrls.Values {
if err != nil { globals.Lock.RLock()
log.Printf("Error fetching feed: %v | %v", url, err) cache, ok := globals.DbMap[url]
continue globals.Lock.RUnlock()
}
currentTime := time.Now()
formattedTime := currentTime.Format("2006-01-02 15:04:05")
feed.Custom = map[string]string{"lastupdate": formattedTime}
feedJSON, err := json.Marshal(feed)
if err != nil {
log.Printf("Error marshaling feed: %v", err)
continue
}
dbMap.Store(url, string(feedJSON))
}
time.Sleep(time.Duration(rssUrls.ReFresh) * time.Minute)
}
}
func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
feeds := make([]gofeed.Feed, 0, len(rssUrls.Values))
for _, url := range rssUrls.Values {
feedJSON, ok := dbMap.Load(url)
if !ok { if !ok {
log.Printf("Error getting feed from db is null %v", url) log.Printf("Error getting feed from db is null %v", url)
continue continue
} }
if cache.Title != "" {
var feed gofeed.Feed words += cache.Title + ","
if err := json.Unmarshal([]byte(feedJSON.(string)), &feed); err != nil { }
log.Printf("Error unmarshaling feed: %v", err) }
continue return words
} }
feeds = append(feeds, feed) func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
} feeds := utils.GetFeeds()
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(feeds) json.NewEncoder(w).Encode(feeds)

BIN
mobile.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

45
models/config.go Normal file
View File

@ -0,0 +1,45 @@
package models
import (
"encoding/json"
"os"
)
func ParseConf() (Config, error) {
var conf Config
data, err := os.ReadFile("config.json")
if err != nil {
return conf, err
}
// 解析JSON数据到Config结构体
err = json.Unmarshal(data, &conf)
return conf, err
}
type Config struct {
Values []string `json:"values"`
ReFresh int `json:"refresh"`
AutoUpdatePush int `json:"autoUpdatePush"`
NightStartTime string `json:"nightStartTime"`
NightEndTime string `json:"nightEndTime"`
}
func (older Config) GetIncrement(newer Config) []string {
var (
urlMap = make(map[string]struct{})
increment = make([]string, 0, len(newer.Values))
)
for _, item := range older.Values {
urlMap[item] = struct{}{}
}
for _, item := range newer.Values {
if _, ok := urlMap[item]; ok {
continue
}
increment = append(increment, item)
}
return increment
}

14
models/feed.go Normal file
View File

@ -0,0 +1,14 @@
package models
type Feed struct {
Title string `json:"title,omitempty"`
Link string `json:"link"`
Custom map[string]string `json:"custom,omitempty"`
Items []Item `json:"items,omitempty"`
}
type Item struct {
Title string `json:"title"`
Link string `json:"link"`
Description string `json:"description"`
}

BIN
pc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

117
utils/feed.go Normal file
View File

@ -0,0 +1,117 @@
package utils
import (
"log"
"rss-reader/globals"
"rss-reader/models"
"time"
"github.com/fsnotify/fsnotify"
)
func UpdateFeeds() {
var (
tick = time.Tick(time.Duration(globals.RssUrls.ReFresh) * time.Minute)
)
for {
formattedTime := time.Now().Format("2006-01-02 15:04:05")
for _, url := range globals.RssUrls.Values {
go UpdateFeed(url, formattedTime)
}
<-tick
}
}
func UpdateFeed(url, formattedTime string) {
result, err := globals.Fp.ParseURL(url)
if err != nil {
log.Printf("Error fetching feed: %v | %v", url, err)
return
}
//feed内容无更新时无需更新缓存
if cache, ok := globals.DbMap[url]; ok &&
len(result.Items) > 0 &&
len(cache.Items) > 0 &&
result.Items[0].Link == cache.Items[0].Link {
return
}
customFeed := models.Feed{
Title: result.Title,
Link: result.Link,
Custom: map[string]string{"lastupdate": formattedTime},
Items: make([]models.Item, 0, len(result.Items)),
}
for _, v := range result.Items {
customFeed.Items = append(customFeed.Items, models.Item{
Link: v.Link,
Title: v.Title,
Description: v.Description,
})
}
globals.Lock.Lock()
defer globals.Lock.Unlock()
globals.DbMap[url] = customFeed
}
//获取feeds列表
func GetFeeds() []models.Feed {
feeds := make([]models.Feed, 0, len(globals.RssUrls.Values))
for _, url := range globals.RssUrls.Values {
globals.Lock.RLock()
cache, ok := globals.DbMap[url]
globals.Lock.RUnlock()
if !ok {
log.Printf("Error getting feed from db is null %v", url)
continue
}
feeds = append(feeds, cache)
}
return feeds
}
func WatchConfigFileChanges(filePath string) {
// 创建一个新的监控器
watcher, err := fsnotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
defer watcher.Close()
// 添加要监控的文件
err = watcher.Add(filePath)
if err != nil {
log.Fatal(err)
}
// 启动一个 goroutine 来处理文件变化事件
go func() {
for {
time.Sleep(7 * time.Second)
select {
case event, ok := <-watcher.Events:
if !ok {
log.Println("通道关闭1")
return
}
if event.Op&fsnotify.Write == fsnotify.Write {
log.Println("文件已修改")
globals.Init()
formattedTime := time.Now().Format("2006-01-02 15:04:05")
for _, url := range globals.RssUrls.Values {
go UpdateFeed(url, formattedTime)
}
}
case err, ok := <-watcher.Errors:
if !ok {
log.Println("通道关闭2")
return
}
log.Println("错误:", err)
return
}
}
}()
select {}
}