diff --git a/scripts/favicon.ts b/scripts/favicon.ts index 45c0bc7..3875cfc 100644 --- a/scripts/favicon.ts +++ b/scripts/favicon.ts @@ -4,7 +4,7 @@ import { join } from "node:path" import { Buffer } from "node:buffer" import { getLogos } from "favicons-scraper" import { consola } from "consola" -import { sources } from "../shared/data" +import { originSources } from "../shared/sources" const projectDir = fileURLToPath(new URL("..", import.meta.url)) const iconsDir = join(projectDir, "public", "icons") @@ -25,7 +25,7 @@ async function downloadImage(url: string, outputPath: string, id: string) { async function main() { await Promise.all( - Object.entries(sources).map(async ([id, source]) => { + Object.entries(originSources).map(async ([id, source]) => { try { const icon = join(iconsDir, `${id.split("-")[0]}.png`) if (fs.existsSync(icon)) { diff --git a/server/routes/[id].ts b/server/routes/[id].ts index e169558..7e383fb 100644 --- a/server/routes/[id].ts +++ b/server/routes/[id].ts @@ -1,16 +1,22 @@ import { Interval, TTL } from "@shared/consts" -import type { SourceResponse } from "@shared/types" -import { sources } from "@shared/data" +import type { SourceID, SourceResponse } from "@shared/types" +import { sources } from "@shared/sources" import { sourcesFn } from "#/sources" import { Cache } from "#/cache" export default defineEventHandler(async (event): Promise => { try { - const id = getRouterParam(event, "id") as keyof typeof sourcesFn + let id = getRouterParam(event, "id") as SourceID const query = getQuery(event) const latest = query.latest !== undefined && query.latest !== "false" + const isValid = (id: SourceID) => !id || !sources[id] || !sourcesFn[id] + + if (isValid(id)) { + const redirectID = sources[id].redirect + if (redirectID) id = redirectID + if (isValid(id)) throw new Error("Invalid source id") + } - if (!id || !sources[id] || !sourcesFn[id]) throw new Error("Invalid source id") const db = useDatabase() const cacheStore = db ? new Cache(db) : undefined const now = Date.now() diff --git a/server/sources/ithome.ts b/server/sources/ithome.ts index ea96fc6..4ebd4f9 100644 --- a/server/sources/ithome.ts +++ b/server/sources/ithome.ts @@ -27,6 +27,6 @@ export default defineSource(async () => { } } }) - return news.sort((m, n) => n.extra!.date > m.extra!.data ? 1 : -1) + return news.sort((m, n) => n.extra!.date > m.extra!.date ? 1 : -1) .slice(0, 20) }) diff --git a/server/utils/date.ts b/server/utils/date.ts index b3ca477..17b6326 100644 --- a/server/utils/date.ts +++ b/server/utils/date.ts @@ -12,3 +12,5 @@ export function tranformToUTC(date: string, format?: string, timezone: string = if (!format) return dayjs.tz(date, timezone).valueOf() return dayjs.tz(date, format, timezone).valueOf() } + +export const day = dayjs diff --git a/shared/data.ts b/shared/data.ts index 0406091..9492596 100644 --- a/shared/data.ts +++ b/shared/data.ts @@ -1,99 +1,30 @@ import type { Metadata } from "./types" -export const sectionIds = ["focus", "social", "china", "world", "digital"] as const - -export const sources = { - "wallstreetcn": { - name: "华尔街见闻", - home: "https://wallstreetcn.com/", - interval: 3 * 60 * 1000, - type: "快讯", - }, - // "36kr": { - // name: "36氪", - // type: "人气榜", - // interval: 10, - // home: "https://36kr.com", - // }, - "36kr-quick": { - name: "36氪", - type: "快讯", - interval: 3 * 60 * 1000, - home: "https://36kr.com", - }, - "douyin": { - name: "抖音", - interval: 3 * 60 * 1000, - home: "https://www.douyin.com", - }, - "hupu": { - name: "虎扑", - home: "https://hupu.com", - }, - "zhihu": { - name: "知乎", - home: "https://www.zhihu.com", - }, - "weibo": { - name: "微博", - type: "实时热搜", - interval: 1 * 60 * 1000, - home: "https://weibo.com", - }, - "tieba": { - name: "百度贴吧", - home: "https://tieba.baidu.com", - }, - "zaobao": { - name: "联合早报", - type: "实时新闻", - home: "https://www.zaobao.com", - }, - "thepaper": { - name: "澎湃新闻", - home: "https://www.thepaper.cn", - }, - "toutiao": { - name: "今日头条", - home: "https://www.toutiao.com", - }, - "cankaoxiaoxi": { - name: "参考消息", - home: "http://www.cankaoxiaoxi.com", - }, - "peopledaily": { - name: "人民日报", - home: "http://paper.people.com.cn", - }, -} as const satisfies Record +export const sectionIds = ["focus", "social", "china", "world", "tech", "code"] as const export const metadata: Metadata = { focus: { name: "关注", - sourceList: [], + sources: [], }, social: { name: "实时", - sourceList: ["douyin", "weibo", "36kr-quick", "wallstreetcn", "zaobao"], + sources: ["douyin", "weibo", "wallstreetcn", "ithome", "36kr"], }, china: { name: "国内", - sourceList: ["peopledaily", "toutiao"], + sources: ["peopledaily", "toutiao"], }, world: { name: "国外", - sourceList: [], + sources: ["aljazeeracn", "sputniknewscn", "zaobao"], }, - digital: { - name: "数码", - sourceList: [], + code: { + name: "代码", + sources: ["v2ex"], + }, + tech: { + name: "科技", + sources: ["ithome"], }, } diff --git a/shared/sources.ts b/shared/sources.ts new file mode 100644 index 0000000..748fa53 --- /dev/null +++ b/shared/sources.ts @@ -0,0 +1,113 @@ +import { typeSafeObjectFromEntries } from "./type.util" +import type { OriginSource, Source, SourceID } from "./types" + +export const originSources = { + "v2ex": { + name: "V2EX", + home: "https://v2ex.com/", + }, + "wallstreetcn": { + name: "华尔街见闻", + home: "https://wallstreetcn.com/", + title: "快讯", + }, + "sputniknewscn": { + name: "俄罗斯卫星通讯社", + home: "https://sputniknews.cn", + }, + "aljazeeracn": { + name: "半岛电视台", + interval: 30 * 60 * 1000, + home: "https://chinese.aljazeera.net", + }, + "36kr": { + name: "36氪", + home: "https://36kr.com", + sub: { + quick: { + title: "快讯", + }, + }, + }, + "douyin": { + name: "抖音", + home: "https://www.douyin.com", + }, + "hupu": { + name: "虎扑", + home: "https://hupu.com", + }, + "zhihu": { + name: "知乎", + home: "https://www.zhihu.com", + }, + "weibo": { + name: "微博", + title: "实时热搜", + interval: 5 * 60 * 1000, + home: "https://weibo.com", + }, + "tieba": { + name: "百度贴吧", + home: "https://tieba.baidu.com", + }, + "zaobao": { + name: "联合早报", + home: "https://www.zaobao.com", + }, + "thepaper": { + name: "澎湃新闻", + home: "https://www.thepaper.cn", + }, + "toutiao": { + name: "今日头条", + home: "https://www.toutiao.com", + }, + "cankaoxiaoxi": { + name: "参考消息", + home: "http://www.cankaoxiaoxi.com", + }, + "ithome": { + name: "IT之家", + interval: 1000, + home: "https://www.ithome.com", + }, + "peopledaily": { + name: "人民日报", + interval: 3 * 60 * 60 * 1000, + home: "http://paper.people.com.cn", + }, +} as const satisfies Record + +export const sources = genSources() +function genSources() { + const _: [SourceID, Source][] = [] + + Object.entries(originSources).forEach(([id, source]: [any, OriginSource]) => { + if (source.sub && Object.keys(source.sub).length) { + Object.entries(source.sub).forEach(([subId, subSource], i) => { + if (i === 0) { + _.push([id, { + redirect: `${id}-${subId}`, + name: source.name, + interval: source.interval, + ...subSource, + }] as [any, Source]) + } + _.push([`${id}-${subId}`, { + name: source.name, + interval: source.interval, + ...subSource, + }] as [any, Source]) + }) + } else { + _.push([id, { + name: source.name, + interval: source.interval, + title: source.title, + }]) + } + }) + + return typeSafeObjectFromEntries(_) +} diff --git a/shared/type.util.ts b/shared/type.util.ts new file mode 100644 index 0000000..a5178f6 --- /dev/null +++ b/shared/type.util.ts @@ -0,0 +1,15 @@ +export type OmitNever = { [K in keyof T as T[K] extends never ? never : K]: T[K] } +export type UnionToIntersection = + (U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never + +export type MaybePromise = Promise | T + +export function typeSafeObjectFromEntries< + const T extends ReadonlyArray, +>(entries: T): { [K in T[number]as K[0]]: K[1] } { + return Object.fromEntries(entries) as { [K in T[number]as K[0]]: K[1] } +} + +export function typeSafeObjectEntries>(obj: T): { [K in keyof T]: [K, T[K]] }[keyof T][] { + return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][] +} diff --git a/shared/types.ts b/shared/types.ts index 727dc8a..c45b56f 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -1,12 +1,43 @@ -import type { sectionIds, sources } from "./data" +import type { sectionIds } from "./data" +import type { originSources } from "./sources" + +type ConstSources = typeof originSources +type MainSourceID = keyof(ConstSources) + +export type SourceID = { + [Key in MainSourceID]: ConstSources[Key] extends { sub?: infer SubType } ? keyof { + // @ts-expect-error >_< + [K in keyof SubType as `${Key}-${K}` ]: never + } | Key : Key; +}[MainSourceID] -export type SourceID = keyof(typeof sources) export type SectionID = (typeof sectionIds)[number] export type Metadata = Record +export interface OriginSource { + name: string + title?: string + /** + * 刷新的间隔时间,复用缓存 + */ + interval?: number + home: string + sub?: Record +} + +export interface Source { + name: string + title?: string + interval?: number + redirect?: SourceID +} + export interface Section { name: string - sourceList: SourceID[] + sources: SourceID[] } export interface NewsItem { @@ -47,7 +78,7 @@ export interface RSSItem { } export interface CacheInfo { - id: SourceID + id: MainSourceID data: NewsItem[] updated: number } diff --git a/shared/utils.ts b/shared/utils.ts index 35ab356..27aa23f 100644 --- a/shared/utils.ts +++ b/shared/utils.ts @@ -22,3 +22,7 @@ export function relativeTime(timestamp: string | number) { return `${month}月${day}日` } } + +export function delay(ms: number) { + return new Promise(resolve => setTimeout(resolve, ms)) +} diff --git a/src/atoms.ts b/src/atoms.ts index c0c7185..b6279b2 100644 --- a/src/atoms.ts +++ b/src/atoms.ts @@ -1,6 +1,7 @@ import { atom } from "jotai" import type { SectionID, SourceID } from "@shared/types" -import { metadata, sources } from "@shared/data" +import { metadata } from "@shared/data" +import { sources } from "@shared/sources" import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage" export const focusSourcesAtom = atomWithLocalStorage("focusSources", [], (stored) => { @@ -30,7 +31,7 @@ export const currentSectionAtom = atom((get) => { return { id, ...metadata[id], - sourceList: get(focusSourcesAtom), + sources: get(focusSourcesAtom), } } return { diff --git a/src/components/header.tsx b/src/components/header.tsx index 7b7e881..418f62b 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -24,7 +24,7 @@ function RefreshButton() { const currentSection = useAtomValue(currentSectionAtom) const setRefetchSource = useSetAtom(refetchSourcesAtom) const refreshAll = useCallback(() => { - const obj = Object.fromEntries(currentSection.sourceList.map(id => [id, Date.now()])) + const obj = Object.fromEntries(currentSection.sources.map(id => [id, Date.now()])) setRefetchSource(prev => ({ ...prev, ...obj, @@ -33,7 +33,7 @@ function RefreshButton() { const isFetching = useIsFetching({ predicate: (query) => { - return currentSection.sourceList.includes(query.queryKey[0] as SourceID) + return currentSection.sources.includes(query.queryKey[0] as SourceID) }, }) diff --git a/src/components/section/card.tsx b/src/components/section/card.tsx index 82382ff..cd96926 100644 --- a/src/components/section/card.tsx +++ b/src/components/section/card.tsx @@ -6,7 +6,7 @@ import clsx from "clsx" import { useInView } from "react-intersection-observer" import { useAtom } from "jotai" import { forwardRef, useCallback, useImperativeHandle, useRef } from "react" -import { sources } from "@shared/data" +import { sources } from "@shared/sources" import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities" import { ofetch } from "ofetch" import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms" @@ -111,8 +111,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro {sources[id].name} - {/* @ts-expect-error -_- */} - {sources[id]?.type} + {sources[id]?.title} { - metadata[id].sourceList.map(source => ( + metadata[id].sources.map(source => ( )) }