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 { Buffer } from "node:buffer"
|
||||||
import { getLogos } from "favicons-scraper"
|
import { getLogos } from "favicons-scraper"
|
||||||
import { consola } from "consola"
|
import { consola } from "consola"
|
||||||
import { sources } from "../shared/data"
|
import { originSources } from "../shared/sources"
|
||||||
|
|
||||||
const projectDir = fileURLToPath(new URL("..", import.meta.url))
|
const projectDir = fileURLToPath(new URL("..", import.meta.url))
|
||||||
const iconsDir = join(projectDir, "public", "icons")
|
const iconsDir = join(projectDir, "public", "icons")
|
||||||
@ -25,7 +25,7 @@ async function downloadImage(url: string, outputPath: string, id: string) {
|
|||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Object.entries(sources).map(async ([id, source]) => {
|
Object.entries(originSources).map(async ([id, source]) => {
|
||||||
try {
|
try {
|
||||||
const icon = join(iconsDir, `${id.split("-")[0]}.png`)
|
const icon = join(iconsDir, `${id.split("-")[0]}.png`)
|
||||||
if (fs.existsSync(icon)) {
|
if (fs.existsSync(icon)) {
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import { Interval, TTL } from "@shared/consts"
|
import { Interval, TTL } from "@shared/consts"
|
||||||
import type { SourceResponse } from "@shared/types"
|
import type { SourceID, SourceResponse } from "@shared/types"
|
||||||
import { sources } from "@shared/data"
|
import { sources } from "@shared/sources"
|
||||||
import { sourcesFn } from "#/sources"
|
import { sourcesFn } from "#/sources"
|
||||||
import { Cache } from "#/cache"
|
import { Cache } from "#/cache"
|
||||||
|
|
||||||
export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
||||||
try {
|
try {
|
||||||
const id = getRouterParam(event, "id") as keyof typeof sourcesFn
|
let id = getRouterParam(event, "id") as SourceID
|
||||||
const query = getQuery(event)
|
const query = getQuery(event)
|
||||||
const latest = query.latest !== undefined && query.latest !== "false"
|
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 db = useDatabase()
|
||||||
const cacheStore = db ? new Cache(db) : undefined
|
const cacheStore = db ? new Cache(db) : undefined
|
||||||
const now = Date.now()
|
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)
|
.slice(0, 20)
|
||||||
})
|
})
|
||||||
|
@ -12,3 +12,5 @@ export function tranformToUTC(date: string, format?: string, timezone: string =
|
|||||||
if (!format) return dayjs.tz(date, timezone).valueOf()
|
if (!format) return dayjs.tz(date, timezone).valueOf()
|
||||||
return dayjs.tz(date, format, timezone).valueOf()
|
return dayjs.tz(date, format, timezone).valueOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const day = dayjs
|
||||||
|
@ -1,99 +1,30 @@
|
|||||||
import type { Metadata } from "./types"
|
import type { Metadata } from "./types"
|
||||||
|
|
||||||
export const sectionIds = ["focus", "social", "china", "world", "digital"] as const
|
export const sectionIds = ["focus", "social", "china", "world", "tech", "code"] 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 metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
focus: {
|
focus: {
|
||||||
name: "关注",
|
name: "关注",
|
||||||
sourceList: [],
|
sources: [],
|
||||||
},
|
},
|
||||||
social: {
|
social: {
|
||||||
name: "实时",
|
name: "实时",
|
||||||
sourceList: ["douyin", "weibo", "36kr-quick", "wallstreetcn", "zaobao"],
|
sources: ["douyin", "weibo", "wallstreetcn", "ithome", "36kr"],
|
||||||
},
|
},
|
||||||
china: {
|
china: {
|
||||||
name: "国内",
|
name: "国内",
|
||||||
sourceList: ["peopledaily", "toutiao"],
|
sources: ["peopledaily", "toutiao"],
|
||||||
},
|
},
|
||||||
world: {
|
world: {
|
||||||
name: "国外",
|
name: "国外",
|
||||||
sourceList: [],
|
sources: ["aljazeeracn", "sputniknewscn", "zaobao"],
|
||||||
},
|
},
|
||||||
digital: {
|
code: {
|
||||||
name: "数码",
|
name: "代码",
|
||||||
sourceList: [],
|
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 SectionID = (typeof sectionIds)[number]
|
||||||
export type Metadata = Record<SectionID, Section>
|
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 {
|
export interface Section {
|
||||||
name: string
|
name: string
|
||||||
sourceList: SourceID[]
|
sources: SourceID[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface NewsItem {
|
export interface NewsItem {
|
||||||
@ -47,7 +78,7 @@ export interface RSSItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheInfo {
|
export interface CacheInfo {
|
||||||
id: SourceID
|
id: MainSourceID
|
||||||
data: NewsItem[]
|
data: NewsItem[]
|
||||||
updated: number
|
updated: number
|
||||||
}
|
}
|
||||||
|
@ -22,3 +22,7 @@ export function relativeTime(timestamp: string | number) {
|
|||||||
return `${month}月${day}日`
|
return `${month}月${day}日`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function delay(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { atom } from "jotai"
|
import { atom } from "jotai"
|
||||||
import type { SectionID, SourceID } from "@shared/types"
|
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"
|
import { atomWithLocalStorage } from "./hooks/atomWithLocalStorage"
|
||||||
|
|
||||||
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
|
export const focusSourcesAtom = atomWithLocalStorage<SourceID[]>("focusSources", [], (stored) => {
|
||||||
@ -30,7 +31,7 @@ export const currentSectionAtom = atom((get) => {
|
|||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
...metadata[id],
|
...metadata[id],
|
||||||
sourceList: get(focusSourcesAtom),
|
sources: get(focusSourcesAtom),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
@ -24,7 +24,7 @@ function RefreshButton() {
|
|||||||
const currentSection = useAtomValue(currentSectionAtom)
|
const currentSection = useAtomValue(currentSectionAtom)
|
||||||
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
const setRefetchSource = useSetAtom(refetchSourcesAtom)
|
||||||
const refreshAll = useCallback(() => {
|
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 => ({
|
setRefetchSource(prev => ({
|
||||||
...prev,
|
...prev,
|
||||||
...obj,
|
...obj,
|
||||||
@ -33,7 +33,7 @@ function RefreshButton() {
|
|||||||
|
|
||||||
const isFetching = useIsFetching({
|
const isFetching = useIsFetching({
|
||||||
predicate: (query) => {
|
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 { useInView } from "react-intersection-observer"
|
||||||
import { useAtom } from "jotai"
|
import { useAtom } from "jotai"
|
||||||
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
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 type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||||
import { ofetch } from "ofetch"
|
import { ofetch } from "ofetch"
|
||||||
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
|
import { focusSourcesAtom, refetchSourcesAtom } from "~/atoms"
|
||||||
@ -111,8 +111,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
|||||||
{sources[id].name}
|
{sources[id].name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* @ts-expect-error -_- */}
|
<span className="text-xs">{sources[id]?.title}</span>
|
||||||
<span className="text-xs">{sources[id]?.type}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
defer
|
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} />
|
<CardWrapper key={source} id={source} />
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user