mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: support timeline
This commit is contained in:
parent
da64d6dbe0
commit
17146bd865
@ -15,7 +15,7 @@ export default defineSource(async () => {
|
||||
const date = $a.find(".lenta__item-date").attr("data-unixtime")
|
||||
if (url && title && date) {
|
||||
news.push({
|
||||
url,
|
||||
url: `https://sputniknews.cn${url}`,
|
||||
title,
|
||||
id: url,
|
||||
extra: {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Metadata } from "./types"
|
||||
|
||||
export const sectionIds = ["focus", "realtime", "china", "world", "tech", "code"] as const
|
||||
export const sectionIds = ["focus", "realtime", "hottest", "china", "world", "tech"] as const
|
||||
|
||||
export const metadata: Metadata = {
|
||||
focus: {
|
||||
@ -9,22 +9,22 @@ export const metadata: Metadata = {
|
||||
},
|
||||
realtime: {
|
||||
name: "实时",
|
||||
sources: ["weibo", "douyin", "zhihu", "toutiao", "wallstreetcn", "ithome", "36kr"],
|
||||
sources: ["weibo", "wallstreetcn", "ithome", "36kr", "zaobao"],
|
||||
},
|
||||
hottest: {
|
||||
name: "最热",
|
||||
sources: ["weibo", "douyin", "zhihu", "toutiao"],
|
||||
},
|
||||
china: {
|
||||
name: "国内",
|
||||
sources: ["toutiao", "zhihu"],
|
||||
sources: ["weibo", "douyin", "toutiao", "zhihu"],
|
||||
},
|
||||
world: {
|
||||
name: "国际",
|
||||
sources: ["sputniknewscn", "zaobao", "cankaoxiaoxi"],
|
||||
},
|
||||
code: {
|
||||
name: "代码",
|
||||
sources: ["v2ex"],
|
||||
},
|
||||
tech: {
|
||||
name: "科技",
|
||||
sources: ["ithome", "coolapk", "36kr-quick"],
|
||||
sources: ["ithome", "v2ex", "coolapk", "36kr-quick", "wallstreetcn"],
|
||||
},
|
||||
}
|
||||
|
@ -18,6 +18,7 @@ export const originSources = {
|
||||
},
|
||||
"coolapk": {
|
||||
name: "酷安",
|
||||
type: "hottest",
|
||||
home: "https://coolapk.com",
|
||||
},
|
||||
"wallstreetcn": {
|
||||
@ -46,6 +47,7 @@ export const originSources = {
|
||||
},
|
||||
"douyin": {
|
||||
name: "抖音",
|
||||
type: "hottest",
|
||||
home: "https://www.douyin.com",
|
||||
},
|
||||
"hupu": {
|
||||
@ -54,11 +56,13 @@ export const originSources = {
|
||||
},
|
||||
"zhihu": {
|
||||
name: "知乎",
|
||||
type: "hottest",
|
||||
home: "https://www.zhihu.com",
|
||||
},
|
||||
"weibo": {
|
||||
name: "微博",
|
||||
title: "实时热搜",
|
||||
type: "hottest",
|
||||
interval: Time.Realtime,
|
||||
home: "https://weibo.com",
|
||||
},
|
||||
@ -78,6 +82,7 @@ export const originSources = {
|
||||
},
|
||||
"toutiao": {
|
||||
name: "今日头条",
|
||||
type: "hottest",
|
||||
home: "https://www.toutiao.com",
|
||||
},
|
||||
"ithome": {
|
||||
@ -97,12 +102,14 @@ function genSources() {
|
||||
_.push([id, {
|
||||
redirect: `${id}-${subId}`,
|
||||
name: source.name,
|
||||
type: source.type,
|
||||
interval: source.interval,
|
||||
...subSource,
|
||||
}] as [any, Source])
|
||||
}
|
||||
_.push([`${id}-${subId}`, {
|
||||
name: source.name,
|
||||
type: source.type,
|
||||
interval: source.interval,
|
||||
...subSource,
|
||||
}] as [any, Source])
|
||||
@ -110,6 +117,7 @@ function genSources() {
|
||||
} else {
|
||||
_.push([id, {
|
||||
name: source.name,
|
||||
type: source.type,
|
||||
interval: source.interval,
|
||||
title: source.title,
|
||||
}])
|
||||
|
@ -21,9 +21,17 @@ export interface OriginSource {
|
||||
* 刷新的间隔时间,复用缓存
|
||||
*/
|
||||
interval?: number
|
||||
/**
|
||||
* @default latest
|
||||
*/
|
||||
type?: "hottest" | "latest"
|
||||
home: string
|
||||
sub?: Record<string, {
|
||||
title: string
|
||||
/**
|
||||
* @default latest
|
||||
*/
|
||||
type?: "hottest" | "latest"
|
||||
interval?: number
|
||||
}>
|
||||
}
|
||||
@ -31,6 +39,7 @@ export interface OriginSource {
|
||||
export interface Source {
|
||||
name: string
|
||||
title?: string
|
||||
type?: "hottest" | "latest"
|
||||
interval?: number
|
||||
redirect?: SourceID
|
||||
}
|
||||
|
@ -46,8 +46,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
||||
<div
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
"flex flex-col h-550px rounded-2xl bg-blue bg-op-50 p-4 backdrop-blur-5",
|
||||
"shadow-base",
|
||||
"flex flex-col h-500px rounded-2xl bg-blue bg-op-50 p-4 backdrop-blur-5",
|
||||
isDragged && "op-50",
|
||||
isOverlay ? "backdrop-blur-5 bg-op-40" : "",
|
||||
)}
|
||||
@ -62,6 +61,52 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
||||
)
|
||||
})
|
||||
|
||||
export function CardOverlay({ id }: { id: SourceID }) {
|
||||
const [focusSources] = useAtom(focusSourcesAtom)
|
||||
return (
|
||||
<div className={clsx(
|
||||
"flex flex-col h-500px rounded-2xl bg-blue bg-op-50 p-4 backdrop-blur-5",
|
||||
"backdrop-blur-5 bg-op-40",
|
||||
)}
|
||||
>
|
||||
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img
|
||||
src={`/icons/${id.split("-")[0]}.png`}
|
||||
className={clsx("h-8 rounded-full")}
|
||||
alt={id}
|
||||
onError={e => e.currentTarget.src = "/icons/default.png"}
|
||||
/>
|
||||
<span className="flex flex-col">
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-xl font-bold">
|
||||
{sources[id].name}
|
||||
</span>
|
||||
{sources[id]?.title && <span className="text-sm">{sources[id].title}</span>}
|
||||
</span>
|
||||
<span className="text-xs op-0">刚刚刷新</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2 op-80">
|
||||
<button
|
||||
type="button"
|
||||
className={clsx("i-ph:arrow-counter-clockwise-duotone")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star-duotone")}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="h-full p-2 overflow-x-auto bg-base bg-op-70! rounded-2xl" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardProps) {
|
||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourcesAtom)
|
||||
@ -98,7 +143,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={clsx("flex justify-between m-4 mt-0 items-center")}>
|
||||
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
|
||||
<div className="flex gap-2 items-center">
|
||||
<img
|
||||
src={`/icons/${id.split("-")[0]}.png`}
|
||||
@ -116,7 +161,7 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
||||
<span className="text-xs"><UpdateTime query={query} /></span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="flex gap-2 op-80">
|
||||
<button
|
||||
type="button"
|
||||
className={clsx("i-ph:arrow-counter-clockwise-duotone", query.isFetching && "animate-spin i-ph:circle-dashed-duotone")}
|
||||
@ -134,7 +179,15 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NewsList query={query} />
|
||||
|
||||
<OverlayScrollbar
|
||||
className="h-full p-2 overflow-x-auto bg-base bg-op-70! rounded-2xl"
|
||||
options={{
|
||||
overflow: { x: "hidden" },
|
||||
}}
|
||||
>
|
||||
{sources[id].type === "hottest" ? <NewsList query={query} /> : <NewsListTimeLine query={query} />}
|
||||
</OverlayScrollbar>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@ -146,42 +199,25 @@ function UpdateTime({ query }: Query) {
|
||||
return "加载中..."
|
||||
}
|
||||
|
||||
function Num({ num }: { num: number }) {
|
||||
return (
|
||||
<span className={clsx("bg-gray/10 min-w-6 flex justify-center items-center rounded-md")}>
|
||||
{num}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
function ExtraInfo({ item }: { item: NewsItem }) {
|
||||
const relativeTime = useRelativeTime(item?.extra?.date)
|
||||
if (item?.extra?.info) {
|
||||
return <>{item.extra.info}</>
|
||||
}
|
||||
|
||||
if (item?.extra?.icon) {
|
||||
return <img src={item.extra.icon} className="w-5 inline" onError={e => e.currentTarget.hidden = true} />
|
||||
}
|
||||
|
||||
if (relativeTime) {
|
||||
return <>{relativeTime}</>
|
||||
}
|
||||
}
|
||||
|
||||
function NewsList({ query }: Query) {
|
||||
const items = query.data?.items
|
||||
return (
|
||||
<OverlayScrollbar
|
||||
className="h-full pl-2 pr-3 mr-1 py-2 overflow-x-auto bg-base rounded-2xl"
|
||||
options={{
|
||||
overflow: { x: "hidden" },
|
||||
}}
|
||||
>
|
||||
{items?.slice(0, 20).map((item, i) => (
|
||||
<div key={item.title} className="flex gap-2 items-center">
|
||||
<Num num={i + 1} />
|
||||
<a href={item.url} target="_blank" className="my-1">
|
||||
<ol>
|
||||
{items?.map((item, i) => (
|
||||
<li key={item.title} className="flex gap-2 items-center mb-2 items-stretch">
|
||||
<span className={clsx("bg-gray-4/10 min-w-6 flex justify-center items-center rounded-md text-sm")}>
|
||||
{i + 1}
|
||||
</span>
|
||||
<a href={item.url} target="_blank" className="self-start">
|
||||
<span className="mr-2">
|
||||
{item.title}
|
||||
</span>
|
||||
@ -189,8 +225,39 @@ function NewsList({ query }: Query) {
|
||||
<ExtraInfo item={item} />
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</li>
|
||||
))}
|
||||
</OverlayScrollbar>
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
||||
function UpdatedTime({ item }: { item: NewsItem }) {
|
||||
const relativeTime = useRelativeTime(item?.extra?.date)
|
||||
return <>{relativeTime}</>
|
||||
}
|
||||
|
||||
function NewsListTimeLine({ query }: Query) {
|
||||
const items = query.data?.items
|
||||
return (
|
||||
<ol className="relative border-s border-dash border-gray-4/30">
|
||||
{items?.map(item => (
|
||||
<li key={item.title} className="flex gap-2 mb-2 ms-4">
|
||||
<div className={clsx("absolute w-2 h-2 bg-gray-4/50 rounded-full ml-0.5 mt-1 -start-1.5")} />
|
||||
<span className="flex flex-col">
|
||||
<span className="text-xs text-gray-4/80 truncate align-middle">
|
||||
<UpdatedTime item={item} />
|
||||
</span>
|
||||
<a href={item.url} target="_blank">
|
||||
<span>
|
||||
{item.title}
|
||||
</span>
|
||||
<span className="text-xs text-gray-4/80 truncate align-middle">
|
||||
<ExtraInfo item={item} />
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
)
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import type { SourceID } from "@shared/types"
|
||||
import { CSS } from "@dnd-kit/utilities"
|
||||
import { motion } from "framer-motion"
|
||||
import type { ItemsProps } from "./card"
|
||||
import { CardWrapper } from "./card"
|
||||
import { CardOverlay, CardWrapper } from "./card"
|
||||
import { currentSectionAtom } from "~/atoms"
|
||||
|
||||
export function Dnd() {
|
||||
@ -98,7 +98,7 @@ export function DndWrapper({ items, setItems, children }: PropsWithChildren<DndP
|
||||
{children}
|
||||
</SortableContext>
|
||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
||||
{!!activeId && <CardWrapper id={activeId as SourceID} isOverlay />}
|
||||
{!!activeId && <CardOverlay id={activeId as SourceID} />}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
)
|
||||
|
@ -31,7 +31,7 @@ function RootComponent() {
|
||||
<header className="flex justify-between items-center sticky top-0 z-100 bg-base py-4 px-6 md:(pt-8 px-16)">
|
||||
<Header />
|
||||
</header>
|
||||
<main className="min-h-[calc(100vh-11rem)] px-6 md:(px-16)">
|
||||
<main className="min-h-[calc(100vh-12rem)] px-6 md:(px-16)">
|
||||
<Outlet />
|
||||
</main>
|
||||
<footer className="py-6 flex flex-col items-center justify-center text-sm">
|
||||
|
Loading…
x
Reference in New Issue
Block a user