mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
pref: refactor source structure
This commit is contained in:
parent
ad2ff17a0d
commit
060bb92de2
@ -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)) {
|
||||
|
@ -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<SourceResponse> => {
|
||||
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()
|
||||
|
@ -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)
|
||||
})
|
||||
|
@ -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
|
||||
|
@ -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<string, {
|
||||
name: string
|
||||
type?: string
|
||||
/**
|
||||
* 刷新的间隔时间,复用缓存
|
||||
*/
|
||||
interval?: number
|
||||
home: string
|
||||
}>
|
||||
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"],
|
||||
},
|
||||
}
|
||||
|
113
shared/sources.ts
Normal file
113
shared/sources.ts
Normal file
@ -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<string, OriginSource>
|
||||
|
||||
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(_)
|
||||
}
|
15
shared/type.util.ts
Normal file
15
shared/type.util.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export type OmitNever<T> = { [K in keyof T as T[K] extends never ? never : K]: T[K] }
|
||||
export type UnionToIntersection<U> =
|
||||
(U extends any ? (x: U) => void : never) extends ((x: infer I) => void) ? I : never
|
||||
|
||||
export type MaybePromise<T> = Promise<T> | T
|
||||
|
||||
export function typeSafeObjectFromEntries<
|
||||
const T extends ReadonlyArray<readonly [PropertyKey, unknown]>,
|
||||
>(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<T extends Record<PropertyKey, unknown>>(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][]
|
||||
}
|
@ -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<SectionID, Section>
|
||||
|
||||
export interface OriginSource {
|
||||
name: string
|
||||
title?: string
|
||||
/**
|
||||
* 刷新的间隔时间,复用缓存
|
||||
*/
|
||||
interval?: number
|
||||
home: string
|
||||
sub?: Record<string, {
|
||||
title: string
|
||||
interval?: number
|
||||
}>
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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<SourceID[]>("focusSources", [], (stored) => {
|
||||
@ -30,7 +31,7 @@ export const currentSectionAtom = atom((get) => {
|
||||
return {
|
||||
id,
|
||||
...metadata[id],
|
||||
sourceList: get(focusSourcesAtom),
|
||||
sources: get(focusSourcesAtom),
|
||||
}
|
||||
}
|
||||
return {
|
||||
|
@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -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}
|
||||
</span>
|
||||
</div>
|
||||
{/* @ts-expect-error -_- */}
|
||||
<span className="text-xs">{sources[id]?.type}</span>
|
||||
<span className="text-xs">{sources[id]?.title}</span>
|
||||
</div>
|
||||
<OverlayScrollbarsComponent
|
||||
defer
|
||||
|
@ -43,7 +43,7 @@ export function Section({ id }: { id: SectionID }) {
|
||||
: (
|
||||
<>
|
||||
{
|
||||
metadata[id].sourceList.map(source => (
|
||||
metadata[id].sources.map(source => (
|
||||
<CardWrapper key={source} id={source} />
|
||||
))
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user