feat: support timeline

This commit is contained in:
Ou 2024-10-11 23:44:50 +08:00
parent da64d6dbe0
commit 17146bd865
7 changed files with 127 additions and 43 deletions

View File

@ -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: {

View File

@ -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"],
},
}

View File

@ -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,
}])

View File

@ -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
}

View File

@ -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>
)
}

View File

@ -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>
)

View File

@ -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">