RSS订阅
https://files.wowood.cn/feeds/MP_WXS_3945205084.atom.rss?limit=50
https://files.wowood.cn/feeds/MP_WXS_3922264679.atom.rss?limit=50
https://files.wowood.cn/feeds/MP_WXS_3888889046.atom.rss?limit=50
https://files.wowood.cn/feeds/MP_WXS_3898080835.atom.rss?limit=50
获取前50
https://github.com/cooderl/wewe-rss/issues/213
Docker部署
环境要求:Git、Docker、Docker-Compose
克隆项目
git clone https://github.com/srcrs/rss-reader
进入rss-reader文件夹,运行项目
docker-compose up -d
国内服务器将Dockerfile中取消下面注释使用 go mod 镜像
#RUN go env -w GO111MODULE=on && \
# go env -w GOPROXY=https://goproxy.cn,direct
部署成功后,通过ip+端口号访问
如果要修改主题
1.拷贝项目后,直接修改/globals/static/index.html 文件
2.修改完成后,重新打包镜像
docker build -t srcrs/rss-reader:latest .
如果连接不上docker服务,则使用https://docker.1ms.run
想起之前的问题所在了,小猫咪开全局的话宝塔面板会直接崩溃,只能在进入终端之后再打开全局进行拉取,要不断换节点试试,有时候直接规则就能拉到了
curl -s https://static.1ms.run/1ms-helper/scripts/install.sh | sudo bash /dev/stdin config
3.重新启动项目即可
docker compose down
docker compose up -d
代码备份
<!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="RSS, news, feed, reader, open-source">
<meta name="anthor" content="srcrs">
<style>
/* * Element Plus V2.x CSS - Condensed for inline use
* Note: This is a very simplified version of Element Plus CSS.
* For a complete and accurate inline CSS, you'd typically need to extract
* all relevant styles from 'index.min.css' and 'dark-mode.css'
* and potentially their dependencies, which is a massive undertaking.
* For demonstration purposes, only essential layout styles are included.
*/
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
font-family: var(--el-font-family);
font-size: var(--el-font-size-base);
line-height: var(--el-line-height-base);
color: var(--el-text-color-primary);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root {
--el-color-white: #ffffff;
--el-color-black: #000000;
--el-color-primary: #409eff;
--el-color-primary-light-3: #79bbff;
--el-color-primary-light-5: #a0cfff;
--el-color-primary-light-7: #c6e2ff;
--el-color-primary-light-8: #d9ecff;
--el-color-primary-light-9: #ecf5ff;
--el-color-primary-dark-2: #337ecc;
--el-color-success: #67c23a;
--el-color-warning: #e6a23c;
--el-color-danger: #f56c6c;
--el-color-info: #909399;
--el-bg-color: #ffffff;
--el-bg-color-overlay: #ffffff;
--el-text-color-primary: #303133;
--el-text-color-regular: #606266;
--el-text-color-secondary: #909399;
--el-text-color-placeholder: #a8abb2;
--el-border-color: #dcdfe6;
--el-border-color-light: #e4e7ed;
--el-border-color-lighter: #ebeef5;
--el-border-color-extra-light: #f2f6fc;
--el-border-color-dark: #d4d7de;
--el-border-color-darker: #cdd0d6;
--el-fill-color: #f0f2f5;
--el-fill-color-light: #f5f7fa;
--el-fill-color-lighter: #fafafa;
--el-fill-color-extra-light: #edf2f6;
--el-fill-color-dark: #ebedef;
--el-fill-color-darker: #e6e8eb;
--el-box-shadow: 0px 12px 32px 4px rgba(0, 0, 0, 0.04), 0px 8px 20px rgba(0, 0, 0, 0.08);
--el-box-shadow-light: 0px 0px 12px rgba(0, 0, 0, 0.12);
--el-box-shadow-lighter: 0px 0px 6px rgba(0, 0, 0, 0.12);
--el-box-shadow-dark: 0px 16px 48px 16px rgba(0, 0, 0, 0.08), 0px 12px 32px rgba(0, 0, 0, 0.12), 0px 8px 16px -8px rgba(0, 0, 0, 0.16);
--el-font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "\5FAE\8F6F\96C5\9ED1", Arial, sans-serif;
--el-font-size-extra-large: 20px;
--el-font-size-large: 18px;
--el-font-size-medium: 16px;
--el-font-size-base: 14px;
--el-font-size-small: 13px;
--el-font-size-extra-small: 12px;
--el-font-weight-primary: 500;
--el-font-line-height-primary: 24px;
--el-index-normal: 1;
--el-index-top: 1000;
--el-index-popper: 2000;
--el-border-radius-base: 4px;
--el-border-radius-small: 2px;
--el-border-radius-round: 20px;
--el-border-radius-circle: 100%;
--el-transition-duration: 0.3s;
--el-transition-duration-fast: 0.2s;
--el-transition-function-ease-in-out-bezier: cubic-bezier(0.645, 0.045, 0.355, 1);
--el-transition-function-linear: linear;
--el-disabled-opacity: 0.5;
--el-box-shadow-base: var(--el-box-shadow-light);
}
.el-container {
display: flex;
flex-direction: column;
height: 100%;
}
.el-header {
padding: var(--el-header-padding);
box-sizing: border-box;
flex-shrink: 0;
}
.el-main {
flex-grow: 1;
padding: var(--el-main-padding);
}
.el-row {
display: flex;
flex-wrap: wrap;
position: relative;
box-sizing: border-box;
}
.el-row::before,
.el-row::after {
display: table;
content: "";
}
.el-row::after {
clear: both;
}
.el-col {
float: left;
box-sizing: border-box;
}
.el-card {
border-radius: var(--el-border-radius-base);
border: 1px solid var(--el-border-color-light);
background-color: var(--el-color-white);
overflow: hidden;
color: var(--el-text-color-primary);
transition: 0.3s;
}
.el-card__header {
padding: 18px 20px;
border-bottom: 1px solid var(--el-border-color-light);
box-sizing: border-box;
}
.el-card__body {
padding: 20px; /* Adjusted padding for 432px height */
}
.el-card__footer {
padding: 18px 20px;
border-top: 1px solid var(--el-border-color-light);
box-sizing: border-box;
}
.el-link {
display: inline-flex;
flex-direction: row;
align-items: center;
vertical-align: middle;
position: relative;
text-decoration: none;
outline: none;
cursor: pointer;
padding: 0;
font-size: inherit;
font-weight: inherit;
}
/* Override Element Plus's custom underline behavior for el-link to use standard text-decoration */
.el-link.is-underline:hover:after {
content: none; /* Remove the custom pseudo-element underline */
}
.el-scrollbar {
overflow: hidden;
position: relative;
}
.el-scrollbar__wrap {
overflow: auto;
height: 100%;
}
.el-scrollbar__view {
box-sizing: content-box;
}
.el-loading-mask {
position: absolute;
z-index: 2000;
background-color: var(--el-overlay-color-light);
margin: 0;
top: 0;
right: 0;
bottom: 0;
left: 0;
transition: opacity 0.3s;
}
.el-loading-spinner {
top: 50%;
margin-top: -21px;
width: 100%;
text-align: center;
position: absolute;
}
.el-loading-text {
color: var(--el-color-primary);
margin: 3px 0;
font-size: 14px;
}
/* Custom Styles */
body {
font-family: "Avenir", Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 0; /* Remove body top margin, controlled by el-main padding */
background-color: #f2f2f2; /* Light gray background */
}
#app {
max-width: 1800px; /* Increased max-width to allow more content horizontally */
margin: 0 auto; /* Center the app */
}
.card-header {
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
font-weight: bold;
color: #333; /* Darker header text */
background-color: #e6e6e6; /* Light gray background for header */
padding: 10px 0;
border-bottom: 1px solid #ddd;
}
.el-card {
height: 100%; /* Make cards fill the column height */
display: flex;
flex-direction: column;
margin-bottom: 0px; /* Add margin between cards */
border: none; /* Remove default border */
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); /* Subtle shadow */
}
.el-card__header {
padding: 0; /* Remove default padding as card-header handles it */
border-bottom: none;
}
/* el-card__body padding is already adjusted in Element Plus styles */
.list-item {
display: flex;
align-items: flex-start; /* Align items to the start */
text-align: left; /* Ensure text alignment is left */
width: 100%;
margin-bottom: 10px; /* Space between list items */
}
.list-item:last-child {
margin-bottom: 0; /* No margin for last item */
}
.list-item-title {
display: flex;
flex-grow: 1;
text-align: left; /* Ensure text alignment is left */
width: 100%;
line-height: 1.4; /* Improve readability */
}
.list-item-title span {
margin-right: 0; /* Removed space after number */
color: #666; /* Gray out numbers */
font-size: 14px;
/* Prevent number from wrapping if link is very short */
flex-shrink: 0;
}
.el-link {
color: #333; /* Darker link color */
text-decoration: none;
font-size: 14px;
flex-grow: 1; /* Allow link to take full width */
overflow: hidden; /* Hide overflow text */
text-overflow: ellipsis; /* Add ellipsis */
white-space: nowrap; /* Prevent text wrapping */
}
.el-link:hover {
color: #409eff !important; /* Element Plus primary color on hover, !important to ensure override */
text-decoration: underline !important; /* Add underline on hover, !important to ensure override */
}
.feed-col {
margin-bottom: 20px;
}
.time {
font-size: 12px;
color: #999;
display: block; /* Ensure it takes full width */
}
.el-card__footer {
height: auto; /* Allow footer to adjust height */
padding: 10px 20px; /* Adjust footer padding */
border-top: 1px solid #eee; /* Light border on footer */
background-color: #f9f9f9; /* Slightly different background for footer */
display: flex; /* Added for centering */
justify-content: center; /* Added for horizontal centering */
align-items: center; /* Added for vertical centering */
}
/* Scrollbar styles for better appearance */
.el-scrollbar__wrap {
margin-right: -17px; /* Hide default scrollbar space */
}
.el-scrollbar__bar.is-vertical>div {
background-color: rgba(0, 0, 0, 0.2); /* Darker scrollbar thumb */
border-radius: 4px;
}
/* Responsive adjustments */
/* Mobile (xs): 1 column */
@media (max-width: 767px) {
.card-header {
font-size: 16px;
}
.list-item-title .el-link {
font-size: 13px;
}
.time {
font-size: 11px;
}
.el-col-xs-24 {
width: 100%;
}
.el-main {
padding: 20px 10px; /* Adjust padding for small screens */
}
}
/* Small screens (sm): 2 columns */
@media (min-width: 768px) and (max-width: 991px) {
.el-col-sm-12 {
width: 50%;
}
.el-main {
padding: 20px;
}
}
/* Medium screens (md): 3 columns */
@media (min-width: 992px) and (max-width: 1199px) {
.el-col-md-8 {
width: 33.33333%;
}
.el-main {
padding: 20px;
}
}
/* Large screens (lg) and up: 4 columns */
@media (min-width: 1200px) {
.el-col-lg-6 {
width: 25%;
}
.el-main {
padding: 20px;
}
}
</style>
</head>
<body>
<div id="app">
<el-container>
<el-main v-loading.fullscreen.lock="fullscreenLoading" element-loading-text="拼命加载中" style="padding-top: 30px;">
<el-row :gutter="20">
<el-col v-if="showSEOFlag" :xs="24" :sm="12" :md="8" :lg="6" :key="0" class="feed-col">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>Example Feed 1</span>
</div>
</template>
<el-scrollbar style="height: 392px;">
<div>
<div class="list-item">
<div class="list-item-title">
<span>1.</span>
<el-link href="#" target="_blank" title="Example Item 1 Title longer text that should now be cut off if too long">
Example Item 1 Title longer text that should now be cut off if too long
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>2.</span>
<el-link href="#" target="_blank" title="Example Item 2 Title">
Example Item 2 Title
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>3.</span>
<el-link href="#" target="_blank" title="Another Example Item Title">
Another Example Item Title that is very long and should definitely be truncated now instead of wrapping.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>4.</span>
<el-link href="#" target="_blank" title="Short Title">
Short Title
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>5.</span>
<el-link href="#" target="_blank" title="Another one with some description">
Another one with some description to make it longer.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>6.</span>
<el-link href="#" target="_blank" title="More Content Here">
More Content Here, allowing for more lines to be visible in the taller card.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>7.</span>
<el-link href="#" target="_blank" title="Seventh Item Example">
Seventh Item Example for demonstration.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>8.</span>
<el-link href="#" target="_blank" title="Eighth Item, still more">
Eighth Item, still more to fill the space.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>9.</span>
<el-link href="#" target="_blank" title="Ninth Article of Interest">
Ninth Article of Interest.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>10.</span>
<el-link href="#" target="_blank" title="Tenth Item in the List">
Tenth Item in the List. This should fill about half of the scrollable area now.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>11.</span>
<el-link href="#" target="_blank" title="Eleventh Entry">
Eleventh Entry to fill the view.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>12.</span>
<el-link href="#" target="_blank" title="Another long entry that tests the two-line clamp and overall card height to make sure it doesn't overflow unnaturally.">
Another long entry that tests the two-line clamp and overall card height to make sure it doesn't overflow unnaturally.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>13.</span>
<el-link href="#" target="_blank" title="Thirteenth item, almost there.">
Thirteenth item, almost there.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>14.</span>
<el-link href="#" target="_blank" title="Fourteenth content piece.">
Fourteenth content piece.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>15.</span>
<el-link href="#" target="_blank" title="Fifteenth item, final one visible before scrolling.">
Fifteenth item, final one visible before scrolling.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>16.</span>
<el-link href="#" target="_blank" title="Sixteenth item, getting closer to 20.">
Sixteenth item, getting closer to 20.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>17.</span>
<el-link href="#" target="_blank" title="Seventeenth item.">
Seventeenth item.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>18.</span>
<el-link href="#" target="_blank" title="Eighteenth item.">
Eighteenth item.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>19.</span>
<el-link href="#" target="_blank" title="Nineteenth item.">
Nineteenth item.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>20.</span>
<el-link href="#" target="_blank" title="Twentieth item - this should be near the bottom of the visible area.">
Twentieth item - this should be near the bottom of the visible area.
</el-link>
</div>
</div>
</div>
<div>
<div class="list-item">
<div class="list-item-title">
<span>21.</span>
<el-link href="#" target="_blank" title="Twenty-first item - this should now be scrolled.">
Twenty-first item - this should now be scrolled.
</el-link>
</div>
</div>
</div>
</el-scrollbar>
<template #footer>
<div class="card-footer">
<time class="time">
2023-10-27 10:00:00
</time>
</div>
</template>
</el-card>
</el-col>
<el-col :xs="24" :sm="12" :md="8" :lg="6" v-for="(feed, index) in feeds" :key="feed.link || index" class="feed-col">
<el-card class="box-card">
<template #header>
<div class="card-header">
<span>{{ feed.title }}</span>
</div>
</template>
<el-scrollbar style="height: 392px;">
<div v-for="(item, i) in feed.items" :key="item.link || i">
<div class="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>
</div>
</div>
</el-scrollbar>
<template #footer>
<div class="card-footer">
<time class="time">{{ feed.custom ? feed.custom.lastupdate : 'N/A' }}</time>
</div>
</template>
</el-card>
</el-col>
</el-row>
</el-main>
</el-container>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@3.4.21/dist/vue.global.prod.js"></script>
<script src="https://cdn.jsdelivr.net/npm/element-plus@2.7.2/dist/index.full.min.js"></script>
<script>
const app = Vue.createApp({
data() {
return {
feeds: [],
showSEOFlag: true,
fullscreenLoading: true,
isPc: true,
autoUpdatePush: 60,
heartbeatInterval: null,
};
},
async created() {
this.fullscreenLoading = false;
// Determine if on PC based on screen width
this.isPc = !window.matchMedia('(max-width: 767px)').matches;
},
async mounted() {
const protocol = window.location.protocol === 'https:' ? 'wss://' : 'ws://';
let socket;
// Function to establish WebSocket connection
const connectWebSocket = () => {
socket = new WebSocket(protocol + window.location.host + "/ws");
socket.onopen = () => {
console.log("WebSocket connected.");
// Clear any previous reconnection attempts and intervals
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
// Start heartbeat only if on PC and auto-update is enabled
if (this.isPc && this.autoUpdatePush > 0) {
this.heartbeatInterval = setInterval(sendHeartbeat, 60000); // Heartbeat every 60 seconds
}
};
socket.onmessage = event => {
const feed = JSON.parse(event.data);
const existingFeedIndex = this.feeds.findIndex(f => f.link === feed.link);
if (existingFeedIndex !== -1) {
// Update existing feed in place to prevent full re-render of the card
this.feeds[existingFeedIndex] = feed;
} else {
// Add new feed
this.feeds.push(feed);
}
// Once data comes from WebSocket, hide the static SEO content
this.showSEOFlag = false;
};
// Function to send heartbeat message
const sendHeartbeat = () => {
if (socket.readyState === WebSocket.OPEN) {
socket.send("heartbeat");
}
};
socket.onclose = event => {
console.log("WebSocket closed. Attempting to reconnect...");
// Clear intervals on close
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
// Attempt to reconnect only if on PC and auto-update is enabled
if (this.isPc && this.autoUpdatePush > 0) {
// Only attempt reconnection, no full page reload
setTimeout(connectWebSocket, 3000); // Reconnect after 3 seconds
}
};
socket.onerror = error => {
console.error("WebSocket error:", error);
// Force close to trigger onclose and reconnect logic
socket.close();
};
};
// Initial WebSocket connection
connectWebSocket();
},
beforeUnmount() {
// Clean up intervals when component is unmounted
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
}
}
});
// Register ElementPlus components globally (assuming ElementPlus is properly loaded)
app.use(ElementPlus);
app.mount("#app");
</script>
</body>
</html>