Compare commits
No commits in common. "master" and "v1.3" have entirely different histories.
@ -18,8 +18,4 @@ WORKDIR /app
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
# 设置时区
|
||||
RUN apk add --no-cache tzdata
|
||||
ENV TZ=Asia/Shanghai
|
||||
|
||||
ENTRYPOINT ["./rss-reader"]
|
||||
|
||||
38
README.md
38
README.md
@ -1,30 +1,12 @@
|
||||
# 简述
|
||||
|
||||
实时展示rss订阅最新消息。
|
||||
|
||||
## 特性
|
||||
|
||||
- 打包后镜像大小仅有约20MB,通过docker实现一键部署
|
||||
|
||||
- 支持自定义配置页面数据自动刷新
|
||||
|
||||
- 响应式布局,能够兼容不同的屏幕大小
|
||||
|
||||
- 良好的SEO,首次加载使用模版引擎快速展示页面内容
|
||||
|
||||
- 支持添加多个RSS订阅链接
|
||||
|
||||
- 简洁的页面布局,可以查看每个订阅链接最后更新时间
|
||||
|
||||
- 支持夜间模式
|
||||
|
||||
- config.json配置文件支持热更新
|
||||
RSS将信息聚合,曾寻找过一些RSS客户端,但觉得都太过于复杂,会需要登陆、保存历史消息、
|
||||
使用缓存加快响应速度,但我想要看到的是,打开页面看到关注网站的即时消息即可(一般通过RSS订阅获取到的数据即是热点),
|
||||
看到有感兴趣的信息,可以跳转过去再详细的了解。
|
||||
|
||||
2023年7月28日,进行了界面改版和升级
|
||||
|
||||

|
||||
|
||||

|
||||

|
||||
|
||||
# 配置文件
|
||||
|
||||
@ -43,12 +25,11 @@
|
||||
"https://www.douban.com/feed/review/book",
|
||||
"https://www.douban.com/feed/review/movie",
|
||||
"https://www.geekpark.net/rss",
|
||||
"https://cn.nytimes.com/rss.html",
|
||||
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
|
||||
],
|
||||
"refresh": 6,
|
||||
"autoUpdatePush": 7,
|
||||
"nightStartTime": "06:30:00",
|
||||
"nightEndTime": "19:30:00"
|
||||
"autoUpdatePush": 1
|
||||
}
|
||||
```
|
||||
|
||||
@ -57,8 +38,6 @@
|
||||
values | rss订阅链接(必填)
|
||||
refresh | rss订阅更新时间间隔,单位分钟(必填)
|
||||
autoUpdatePush | 自动刷新间隔,默认为0,不开启。效果为前端每autoUpdatePush分钟自动更新页面信息,单位分钟(非必填)
|
||||
nightStartTime | 日间开始时间 ,如 06:30:00
|
||||
nightEndTime | 日间结束时间,如 19:30:00
|
||||
|
||||
# 使用方式
|
||||
|
||||
@ -88,16 +67,12 @@ docker-compose up -d
|
||||
|
||||
# nginx反代
|
||||
|
||||
这里需要注意/ws,若不设置proxy_read_timeout参数,则默认1分钟断开。静态文件增加gzip可以大幅压缩网络传输数据
|
||||
|
||||
```conf
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name rss.lass.cc;
|
||||
ssl_certificate fullchain.cer;
|
||||
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 / {
|
||||
proxy_pass http://localhost:8080;
|
||||
}
|
||||
@ -107,7 +82,6 @@ server {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "Upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 300s;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
5
config.json
Executable file → Normal file
5
config.json
Executable file → Normal file
@ -5,6 +5,7 @@
|
||||
"http://www.ruanyifeng.com/blog/atom.xml",
|
||||
"https://feeds.appinn.com/appinns/",
|
||||
"https://v2ex.com/feed/tab/tech.xml",
|
||||
"https://www.cmooc.com/feed",
|
||||
"http://www.sciencenet.cn/xml/blog.aspx?di=30",
|
||||
"https://www.douban.com/feed/review/book",
|
||||
"https://www.douban.com/feed/review/movie",
|
||||
@ -12,7 +13,5 @@
|
||||
"https://hostloc.com/forum.php?mod=rss&fid=45&auth=389ec3vtQanmEuRoghE%2FpZPWnYCPmvwWgSa7RsfjbQ%2BJpA%2F6y6eHAx%2FKqtmPOg"
|
||||
],
|
||||
"refresh": 6,
|
||||
"autoUpdatePush": 7,
|
||||
"nightStartTime": "06:30:00",
|
||||
"nightEndTime": "19:30:00"
|
||||
"autoUpdatePush": 7
|
||||
}
|
||||
|
||||
@ -8,4 +8,6 @@ services:
|
||||
ports:
|
||||
- "8080:8080"
|
||||
volumes:
|
||||
- "$PWD/index.html:/app/index.html"
|
||||
- "$PWD/db:/app/db"
|
||||
- "$PWD/config.json:/app/config.json"
|
||||
|
||||
@ -1,39 +0,0 @@
|
||||
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
File diff suppressed because one or more lines are too long
@ -1,235 +0,0 @@
|
||||
<!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
2
go.mod
@ -3,7 +3,6 @@ module rss-reader
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
github.com/fsnotify/fsnotify v1.6.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/mmcdole/gofeed v1.2.1
|
||||
)
|
||||
@ -16,6 +15,5 @@ require (
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // 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
|
||||
)
|
||||
|
||||
5
go.sum
5
go.sum
@ -5,8 +5,6 @@ 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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
@ -31,9 +29,6 @@ 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/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-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/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM=
|
||||
|
||||
148
index.html
Normal file
148
index.html
Normal file
@ -0,0 +1,148 @@
|
||||
<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, 120000);
|
||||
};
|
||||
};
|
||||
connect();
|
||||
},
|
||||
beforeDestroy() {
|
||||
// 在组件销毁前手动关闭 WebSocket 连接
|
||||
this.socket.close();
|
||||
}
|
||||
});
|
||||
|
||||
app.use(ElementPlus);
|
||||
app.mount("#app");
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
173
main.go
173
main.go
@ -1,111 +1,87 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"rss-reader/globals"
|
||||
"rss-reader/models"
|
||||
|
||||
"rss-reader/utils"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"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() {
|
||||
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() {
|
||||
go utils.UpdateFeeds()
|
||||
go utils.WatchConfigFileChanges("config.json")
|
||||
go updateFeeds()
|
||||
http.HandleFunc("/feeds", getFeedsHandler)
|
||||
http.HandleFunc("/ws", wsHandler)
|
||||
// http.HandleFunc("/", serveHome)
|
||||
http.HandleFunc("/", tplHandler)
|
||||
http.HandleFunc("/", serveHome)
|
||||
|
||||
//加载静态文件
|
||||
fs := http.FileServer(http.FS(globals.DirStatic))
|
||||
fs := http.FileServer(http.FS(dirStatic))
|
||||
http.Handle("/static/", fs)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
func serveHome(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("Content-Type", "text/html; charset=utf-8")
|
||||
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)
|
||||
}
|
||||
w.Write(htmlContent)
|
||||
}
|
||||
|
||||
func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := globals.Upgrader.Upgrade(w, r, nil)
|
||||
conn, err := upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
log.Printf("Upgrade failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
for {
|
||||
for _, url := range globals.RssUrls.Values {
|
||||
globals.Lock.RLock()
|
||||
cache, ok := globals.DbMap[url]
|
||||
globals.Lock.RUnlock()
|
||||
for _, url := range rssUrls.Values {
|
||||
feedJSON, ok := dbMap.Load(url)
|
||||
if !ok {
|
||||
log.Printf("Error getting feed from db is null %v", url)
|
||||
continue
|
||||
}
|
||||
data, err := json.Marshal(cache)
|
||||
if err != nil {
|
||||
log.Printf("json marshal failure: %s", err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
err = conn.WriteMessage(websocket.TextMessage, data)
|
||||
err = conn.WriteMessage(websocket.TextMessage, []byte(feedJSON.(string)))
|
||||
//错误直接关闭更新
|
||||
if err != nil {
|
||||
log.Printf("Error sending message or Connection closed: %v", err)
|
||||
@ -113,34 +89,55 @@ func wsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
//如果未配置则不自动更新
|
||||
if globals.RssUrls.AutoUpdatePush == 0 {
|
||||
if rssUrls.AutoUpdatePush == 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Duration(globals.RssUrls.AutoUpdatePush) * time.Minute)
|
||||
time.Sleep(time.Duration(rssUrls.AutoUpdatePush) * time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
//获取关键词也就是title
|
||||
//获取feeds列表
|
||||
func getKeywords() string {
|
||||
words := ""
|
||||
for _, url := range globals.RssUrls.Values {
|
||||
globals.Lock.RLock()
|
||||
cache, ok := globals.DbMap[url]
|
||||
globals.Lock.RUnlock()
|
||||
func updateFeeds() {
|
||||
for {
|
||||
for _, url := range rssUrls.Values {
|
||||
fp := gofeed.NewParser()
|
||||
feed, err := fp.ParseURL(url)
|
||||
if err != nil {
|
||||
log.Printf("Error fetching feed: %v | %v", url, err)
|
||||
continue
|
||||
}
|
||||
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 {
|
||||
log.Printf("Error getting feed from db is null %v", url)
|
||||
continue
|
||||
}
|
||||
if cache.Title != "" {
|
||||
words += cache.Title + ","
|
||||
}
|
||||
}
|
||||
return words
|
||||
}
|
||||
|
||||
func getFeedsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
feeds := utils.GetFeeds()
|
||||
var feed gofeed.Feed
|
||||
if err := json.Unmarshal([]byte(feedJSON.(string)), &feed); err != nil {
|
||||
log.Printf("Error unmarshaling feed: %v", err)
|
||||
continue
|
||||
}
|
||||
|
||||
feeds = append(feeds, feed)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(feeds)
|
||||
|
||||
BIN
mobile.png
BIN
mobile.png
Binary file not shown.
|
Before Width: | Height: | Size: 122 KiB |
@ -1,45 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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"`
|
||||
}
|
||||
|
Before Width: | Height: | Size: 781 B After Width: | Height: | Size: 781 B |
File diff suppressed because one or more lines are too long
117
utils/feed.go
117
utils/feed.go
@ -1,117 +0,0 @@
|
||||
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 {}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user