|
|
@ -1,7 +1,16 @@ |
|
|
<script setup lang="ts"> |
|
|
<script setup lang="ts"> |
|
|
definePageMeta({ title: 'RSS' }) |
|
|
definePageMeta({ title: 'RSS' }) |
|
|
|
|
|
|
|
|
type Feed = { id: number; feedUrl: string; title: string | null; lastError: string | null } |
|
|
type Feed = { |
|
|
|
|
|
id: number |
|
|
|
|
|
feedUrl: string |
|
|
|
|
|
title: string | null |
|
|
|
|
|
lastError: string | null |
|
|
|
|
|
lastFetchedAt: string | null |
|
|
|
|
|
pollIntervalMinutes: number |
|
|
|
|
|
nextSyncAt: string | null |
|
|
|
|
|
} |
|
|
|
|
|
type RssSyncMeta = { serverCheckIntervalMinutes: number } |
|
|
type Item = { |
|
|
type Item = { |
|
|
id: number |
|
|
id: number |
|
|
title: string | null |
|
|
title: string | null |
|
|
@ -12,6 +21,7 @@ type Item = { |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const feeds = ref<Feed[]>([]) |
|
|
const feeds = ref<Feed[]>([]) |
|
|
|
|
|
const syncMeta = ref<RssSyncMeta>({ serverCheckIntervalMinutes: 60 }) |
|
|
const items = ref<Item[]>([]) |
|
|
const items = ref<Item[]>([]) |
|
|
const feedUrl = ref('') |
|
|
const feedUrl = ref('') |
|
|
const loading = ref(true) |
|
|
const loading = ref(true) |
|
|
@ -38,6 +48,25 @@ const selectedFeedLabel = computed(() => { |
|
|
return f?.title || f?.feedUrl || '当前订阅' |
|
|
return f?.title || f?.feedUrl || '当前订阅' |
|
|
}) |
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
function formatNextSync(iso: string) { |
|
|
|
|
|
return new Date(iso).toLocaleString('zh-CN', { dateStyle: 'short', timeStyle: 'short' }) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const earliestNextSyncLabel = computed(() => { |
|
|
|
|
|
const dates = feeds.value.map((f) => f.nextSyncAt).filter(Boolean) as string[] |
|
|
|
|
|
if (!dates.length) { |
|
|
|
|
|
return null |
|
|
|
|
|
} |
|
|
|
|
|
return formatNextSync(dates.reduce((a, b) => (a < b ? a : b))) |
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
|
|
function feedNextLine(f: Feed) { |
|
|
|
|
|
if (!f.nextSyncAt) { |
|
|
|
|
|
return '尚未完成首次抓取,后台将自动重试' |
|
|
|
|
|
} |
|
|
|
|
|
return `预计下次同步:${formatNextSync(f.nextSyncAt)}` |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
function countForFeed(feedId: number) { |
|
|
function countForFeed(feedId: number) { |
|
|
return items.value.filter((it) => it.feedId === feedId).length |
|
|
return items.value.filter((it) => it.feedId === feedId).length |
|
|
} |
|
|
} |
|
|
@ -57,9 +86,10 @@ async function load() { |
|
|
loading.value = true |
|
|
loading.value = true |
|
|
try { |
|
|
try { |
|
|
const [f, i] = await Promise.all([ |
|
|
const [f, i] = await Promise.all([ |
|
|
fetchData<{ feeds: Feed[] }>('/api/me/rss/feeds'), |
|
|
fetchData<{ feeds: Feed[]; sync: RssSyncMeta }>('/api/me/rss/feeds'), |
|
|
fetchData<{ items: Item[] }>('/api/me/rss/items'), |
|
|
fetchData<{ items: Item[] }>('/api/me/rss/items'), |
|
|
]) |
|
|
]) |
|
|
|
|
|
syncMeta.value = f.sync |
|
|
feeds.value = f.feeds |
|
|
feeds.value = f.feeds |
|
|
items.value = i.items |
|
|
items.value = i.items |
|
|
if (selectedFeedId.value !== null && !feeds.value.some((x) => x.id === selectedFeedId.value)) { |
|
|
if (selectedFeedId.value !== null && !feeds.value.some((x) => x.id === selectedFeedId.value)) { |
|
|
@ -128,6 +158,12 @@ async function copyUnlistedLink(it: Item) { |
|
|
<h1 class="text-2xl font-semibold"> |
|
|
<h1 class="text-2xl font-semibold"> |
|
|
RSS 收件箱 |
|
|
RSS 收件箱 |
|
|
</h1> |
|
|
</h1> |
|
|
|
|
|
<p v-if="!loading" class="text-sm text-muted -mt-4"> |
|
|
|
|
|
服务器约每 {{ syncMeta.serverCheckIntervalMinutes }} 分钟检查一次到期的订阅;添加新订阅后会立即抓取一次。 |
|
|
|
|
|
<template v-if="feeds.length && earliestNextSyncLabel"> |
|
|
|
|
|
当前全部订阅中,预计最早下次同步时间为 {{ earliestNextSyncLabel }}(按各源上次抓取时间推算,仅供参考)。 |
|
|
|
|
|
</template> |
|
|
|
|
|
</p> |
|
|
|
|
|
|
|
|
<UCard> |
|
|
<UCard> |
|
|
<div class="flex flex-col sm:flex-row gap-2"> |
|
|
<div class="flex flex-col sm:flex-row gap-2"> |
|
|
@ -186,6 +222,9 @@ async function copyUnlistedLink(it: Item) { |
|
|
<div class="text-xs text-muted mt-0.5"> |
|
|
<div class="text-xs text-muted mt-0.5"> |
|
|
{{ countForFeed(f.id) }} 条 |
|
|
{{ countForFeed(f.id) }} 条 |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="text-[11px] text-muted mt-1 leading-snug"> |
|
|
|
|
|
{{ feedNextLine(f) }} |
|
|
|
|
|
</div> |
|
|
</button> |
|
|
</button> |
|
|
<div v-if="f.lastError" class="px-3 text-error text-xs"> |
|
|
<div v-if="f.lastError" class="px-3 text-error text-xs"> |
|
|
{{ f.lastError }} |
|
|
{{ f.lastError }} |
|
|
|