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