调整代码结构
This commit is contained in:
parent
762ca0e6ec
commit
a1d7b02b36
39
globals/global.go
Normal file
39
globals/global.go
Normal 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)
|
||||||
|
}
|
||||||
1
globals/static/favicon.svg
Normal file
1
globals/static/favicon.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1690474700709" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1055" xmlns:xlink="http://www.w3.org/1999/xlink" width="128" height="128"><path d="M512 729.984a218.048 218.048 0 1 1 218.048-218.048 43.584 43.584 0 0 1-87.232 0A130.816 130.816 0 1 0 512 642.752a43.584 43.584 0 0 1 0 87.232z m0 174.4a392.448 392.448 0 1 1 392.448-392.448 43.584 43.584 0 0 1-87.232 0 305.216 305.216 0 1 0-122.496 244.608L481.088 542.912a43.584 43.584 0 0 1 61.504-61.504l246.784 246.784a43.584 43.584 0 0 1 0 61.504A392.448 392.448 0 0 1 512 904.448z" fill="#000000" fill-opacity="0.9" p-id="1056"></path></svg>
|
||||||
|
After Width: | Height: | Size: 781 B |
78
globals/static/index.full.min.js
vendored
Normal file
78
globals/static/index.full.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
196
globals/static/index.html
Normal file
196
globals/static/index.html
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
<!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.min.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 {
|
||||||
|
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">
|
||||||
|
<< 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: 300px;">
|
||||||
|
<< 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: 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: [],
|
||||||
|
showSEOFlag: true,
|
||||||
|
fullscreenLoading: true,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async created() {
|
||||||
|
this.fullscreenLoading = false;
|
||||||
|
},
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
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>
|
||||||
1
globals/static/index.min.css
vendored
Normal file
1
globals/static/index.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
globals/static/vue.global.prod.js
Normal file
1
globals/static/vue.global.prod.js
Normal file
File diff suppressed because one or more lines are too long
45
models/config.go
Normal file
45
models/config.go
Normal 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
14
models/feed.go
Normal 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"`
|
||||||
|
}
|
||||||
102
utils/feed.go
Normal file
102
utils/feed.go
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
package utils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"rss-reader/globals"
|
||||||
|
"rss-reader/models"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
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) {
|
||||||
|
// 获取初始文件信息
|
||||||
|
initialFileInfo, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("无法获取文件信息:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
// 每隔一段时间检查文件是否有变化
|
||||||
|
time.Sleep(7 * time.Second)
|
||||||
|
|
||||||
|
// 获取最新的文件信息
|
||||||
|
currentFileInfo, err := os.Stat(filePath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("无法获取文件信息:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查文件的修改时间是否有变化
|
||||||
|
if currentFileInfo.ModTime() != initialFileInfo.ModTime() {
|
||||||
|
log.Println("文件已修改")
|
||||||
|
initialFileInfo = currentFileInfo
|
||||||
|
globals.Init()
|
||||||
|
formattedTime := time.Now().Format("2006-01-02 15:04:05")
|
||||||
|
for _, url := range globals.RssUrls.Values {
|
||||||
|
go UpdateFeed(url, formattedTime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user