mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
feat: support dnd card in /focus
This commit is contained in:
parent
38d3ab5ef6
commit
346f2ee516
@ -3,6 +3,8 @@ import { defineEventHandler, getQuery, getRouterParam, sendProxy } from "h3"
|
|||||||
export default defineEventHandler(async (event) => {
|
export default defineEventHandler(async (event) => {
|
||||||
const id = getRouterParam(event, "id")
|
const id = getRouterParam(event, "id")
|
||||||
const { latest } = getQuery(event)
|
const { latest } = getQuery(event)
|
||||||
if (latest !== undefined) return await sendProxy(event, `https://smzdk.top/api/${id}/new`)
|
// https://api-hot.efefee.cn/weibo?cache=false
|
||||||
return await sendProxy(event, `https://smzdk.top/api/${id}`)
|
// https://smzdk.top/api/${id}/new
|
||||||
|
if (latest !== undefined) return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=false`)
|
||||||
|
return await sendProxy(event, `https://api-hot.efefee.cn/${id}?cache=true`)
|
||||||
})
|
})
|
||||||
|
@ -15,6 +15,7 @@ export interface NewsItem {
|
|||||||
author?: string
|
author?: string
|
||||||
desc?: string
|
desc?: string
|
||||||
url: string
|
url: string
|
||||||
|
timestamp?: number
|
||||||
mobileUrl?: string
|
mobileUrl?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -22,7 +23,6 @@ export interface NewsItem {
|
|||||||
export interface SourceInfo {
|
export interface SourceInfo {
|
||||||
name: string
|
name: string
|
||||||
title: string
|
title: string
|
||||||
subtitle?: string
|
|
||||||
type: string
|
type: string
|
||||||
description?: string
|
description?: string
|
||||||
params?: Record<string, string | object>
|
params?: Record<string, string | object>
|
||||||
|
@ -1,18 +1,20 @@
|
|||||||
export function formatTime(timestamp: string) {
|
export function relativeTime(timestamp: string | number) {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const diffInSeconds = (now.getTime() - date.getTime()) / 1000
|
const diffInSeconds = (now.getTime() - date.getTime()) / 1000
|
||||||
const diffInMinutes = diffInSeconds / 60
|
const diffInMinutes = diffInSeconds / 60
|
||||||
const diffInHours = diffInMinutes / 60
|
const diffInHours = diffInMinutes / 60
|
||||||
|
|
||||||
if (diffInSeconds < 60) {
|
if (Number.isNaN(date.getDay())) {
|
||||||
return "刚刚更新"
|
return undefined
|
||||||
|
} else if (diffInSeconds < 60) {
|
||||||
|
return "刚刚"
|
||||||
} else if (diffInMinutes < 60) {
|
} else if (diffInMinutes < 60) {
|
||||||
const minutes = Math.floor(diffInMinutes)
|
const minutes = Math.floor(diffInMinutes)
|
||||||
return `${minutes}分钟前更新`
|
return `${minutes}分钟前`
|
||||||
} else if (diffInHours < 24) {
|
} else if (diffInHours < 24) {
|
||||||
const hours = Math.floor(diffInHours)
|
const hours = Math.floor(diffInHours)
|
||||||
return `${hours}小时前更新`
|
return `${hours}小时前`
|
||||||
} else {
|
} else {
|
||||||
const month = date.getMonth() + 1
|
const month = date.getMonth() + 1
|
||||||
const day = date.getDate()
|
const day = date.getDate()
|
||||||
|
207
src/components/Card.tsx
Normal file
207
src/components/Card.tsx
Normal file
@ -0,0 +1,207 @@
|
|||||||
|
import type { SourceID, SourceInfo } from "@shared/types"
|
||||||
|
import type { UseQueryResult } from "@tanstack/react-query"
|
||||||
|
import { useQuery } from "@tanstack/react-query"
|
||||||
|
import { relativeTime } from "@shared/utils"
|
||||||
|
import clsx from "clsx"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { useInView } from "react-intersection-observer"
|
||||||
|
import { useAtom } from "jotai"
|
||||||
|
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef } from "react"
|
||||||
|
import { sourceList } from "@shared/data"
|
||||||
|
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||||
|
import { useSortable } from "@dnd-kit/sortable"
|
||||||
|
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
||||||
|
|
||||||
|
interface ItemsProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
id: SourceID
|
||||||
|
withOpacity?: boolean
|
||||||
|
isDragging?: boolean
|
||||||
|
listeners?: SyntheticListenerMap
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, withOpacity, isDragging, listeners, style, ...props }, dndRef) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
const { ref: inViewRef, inView } = useInView({
|
||||||
|
threshold: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// const bindRef = useCallback((node: HTMLDivElement) => {
|
||||||
|
// inViewRef(node)
|
||||||
|
// if (typeof dndRef === "function") {
|
||||||
|
// dndRef(node)
|
||||||
|
// } else if (dndRef) {
|
||||||
|
// dndRef.current = node
|
||||||
|
// }
|
||||||
|
// }, [inViewRef, dndRef])
|
||||||
|
|
||||||
|
useImperativeHandle(dndRef, () => ref.current as HTMLDivElement)
|
||||||
|
useImperativeHandle(inViewRef, () => ref.current as HTMLDivElement)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={clsx("flex flex-col border rounded-md px-2 h-500px", withOpacity && "op-50", isDragging ? "scale-105" : "")}
|
||||||
|
key={id}
|
||||||
|
style={{
|
||||||
|
transformOrigin: "50% 50%",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<NewsCard id={id} inView={inView} isDragging={isDragging} listeners={listeners} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
export function SortableCardWrapper(props: ItemsProps) {
|
||||||
|
const { id } = props
|
||||||
|
const {
|
||||||
|
isDragging,
|
||||||
|
attributes,
|
||||||
|
listeners,
|
||||||
|
setNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({ id })
|
||||||
|
|
||||||
|
const style = {
|
||||||
|
transform: CSS.Transform.toString(transform),
|
||||||
|
transition: transition || undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CardWrapper
|
||||||
|
ref={setNodeRef}
|
||||||
|
style={style}
|
||||||
|
withOpacity={isDragging}
|
||||||
|
isDragging={isDragging}
|
||||||
|
listeners={listeners}
|
||||||
|
{...attributes}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NewsCardProps {
|
||||||
|
id: SourceID
|
||||||
|
inView: boolean
|
||||||
|
isDragging?: boolean
|
||||||
|
listeners?: SyntheticListenerMap
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Query {
|
||||||
|
query: UseQueryResult<SourceInfo, Error>
|
||||||
|
}
|
||||||
|
|
||||||
|
function SubTitle({ query }: Query) {
|
||||||
|
const subTitle = query.data?.type
|
||||||
|
if (subTitle) return <span>{subTitle}</span>
|
||||||
|
}
|
||||||
|
|
||||||
|
function UpdateTime({ query }: Query) {
|
||||||
|
const updateTime = query.data?.updateTime
|
||||||
|
if (updateTime) return <span>{`${relativeTime(updateTime)}更新`}</span>
|
||||||
|
if (query.isError) return <span>获取失败</span>
|
||||||
|
return <span className="skeleton w-20" />
|
||||||
|
}
|
||||||
|
|
||||||
|
function Num({ num }: { num: number }) {
|
||||||
|
const color = ["bg-red-900", "bg-red-500", "bg-red-400"]
|
||||||
|
return (
|
||||||
|
<span className={clsx("bg-active min-w-6 flex justify-center items-center rounded-md", color[num - 1])}>
|
||||||
|
{num}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NewsList({ query }: Query) {
|
||||||
|
const items = query.data?.data
|
||||||
|
if (items?.length) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{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 flex flex-wrap w-full justify-between items-end flex-wrap-reverse">
|
||||||
|
<span>
|
||||||
|
{item.title}
|
||||||
|
</span>
|
||||||
|
{item.timestamp && (
|
||||||
|
<span className="text-xs">
|
||||||
|
{relativeTime(item.timestamp)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{Array.from({ length: 20 }).map((_, i) => i).map(i => (
|
||||||
|
<div key={i} className="flex gap-2 items-center">
|
||||||
|
<Num num={i + 1} />
|
||||||
|
<span className="skeleton border-b border-gray-300/20 my-1"></span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewsCard({ id, inView, isDragging, listeners }: NewsCardProps) {
|
||||||
|
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
||||||
|
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
||||||
|
const query = useQuery({
|
||||||
|
queryKey: [id, refetchSource[id]],
|
||||||
|
queryFn: async ({ queryKey }) => {
|
||||||
|
const [_id, _refetchTime] = queryKey as [SourceID, number]
|
||||||
|
let url = `/api/${_id}`
|
||||||
|
if (Date.now() - _refetchTime < 1000) {
|
||||||
|
url = `/api/${_id}?latest`
|
||||||
|
}
|
||||||
|
const response = await fetch(url)
|
||||||
|
return await response.json() as SourceInfo
|
||||||
|
},
|
||||||
|
// refetch 时显示原有的数据
|
||||||
|
placeholderData: prev => prev,
|
||||||
|
staleTime: 1000 * 60 * 5,
|
||||||
|
enabled: inView,
|
||||||
|
refetchOnWindowFocus: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const addFocusList = useCallback(() => {
|
||||||
|
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
|
||||||
|
}, [setFocusSources, focusSources, id])
|
||||||
|
const manualRefetch = useCallback(() => {
|
||||||
|
setRefetchSource(prev => ({
|
||||||
|
...prev,
|
||||||
|
[id]: Date.now(),
|
||||||
|
}))
|
||||||
|
}, [setRefetchSource, id])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div {...listeners} className={clsx("flex justify-between py-2", isDragging ? "cursor-grabbing" : "cursor-grab")}>
|
||||||
|
<span className="text-lg font-bold">
|
||||||
|
{sourceList[id]}
|
||||||
|
</span>
|
||||||
|
<SubTitle query={query} />
|
||||||
|
</div>
|
||||||
|
<div className="overflow-auto h-full">
|
||||||
|
<NewsList query={query} />
|
||||||
|
</div>
|
||||||
|
<div className="py-2 flex items-center justify-between">
|
||||||
|
<UpdateTime query={query} />
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={clsx("i-ph:arrow-clockwise", query.isFetching && "animate-spin")}
|
||||||
|
onClick={manualRefetch}
|
||||||
|
/>
|
||||||
|
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star")} onClick={addFocusList} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useMemo, useState } from "react"
|
import { useCallback, useState } from "react"
|
||||||
import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"
|
import type { DragEndEvent, DragStartEvent } from "@dnd-kit/core"
|
||||||
import {
|
import {
|
||||||
DndContext,
|
DndContext,
|
||||||
@ -9,14 +9,15 @@ import {
|
|||||||
useSensor,
|
useSensor,
|
||||||
useSensors,
|
useSensors,
|
||||||
} from "@dnd-kit/core"
|
} from "@dnd-kit/core"
|
||||||
import { SortableContext, rectSortingStrategy } from "@dnd-kit/sortable"
|
import { SortableContext, arrayMove, rectSortingStrategy } from "@dnd-kit/sortable"
|
||||||
import type { SectionID } from "@shared/types"
|
import { useAtom } from "jotai"
|
||||||
import { metadata } from "@shared/data"
|
import type { SourceID } from "@shared/types"
|
||||||
import { Item, SortableItem } from "./NewsCard"
|
import { GridContainer } from "./Pure"
|
||||||
|
import { CardWrapper, SortableCardWrapper } from "./Card"
|
||||||
|
import { focusSourcesAtom } from "~/atoms"
|
||||||
|
|
||||||
export function Main({ sectionId }: { sectionId: SectionID }) {
|
export function Main() {
|
||||||
// const [items, setItems] = useState(metadata?.[sectionId]?.sourceList ?? [])
|
const [items, setItems] = useAtom(focusSourcesAtom)
|
||||||
const items = useMemo(() => metadata?.[sectionId]?.sourceList ?? [], [sectionId])
|
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
const sensors = useSensors(useSensor(MouseSensor), useSensor(TouchSensor))
|
||||||
|
|
||||||
@ -27,16 +28,16 @@ export function Main({ sectionId }: { sectionId: SectionID }) {
|
|||||||
const { active, over } = event
|
const { active, over } = event
|
||||||
|
|
||||||
if (active.id !== over?.id) {
|
if (active.id !== over?.id) {
|
||||||
// setItems((items) => {
|
setItems((items) => {
|
||||||
// const oldIndex = items.indexOf(active.id as any)
|
const oldIndex = items.indexOf(active.id as any)
|
||||||
// const newIndex = items.indexOf(over!.id as any)
|
const newIndex = items.indexOf(over!.id as any)
|
||||||
|
|
||||||
// return arrayMove(items, oldIndex, newIndex)
|
return arrayMove(items, oldIndex, newIndex)
|
||||||
// })
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveId(null)
|
setActiveId(null)
|
||||||
}, [])
|
}, [setItems])
|
||||||
const handleDragCancel = useCallback(() => {
|
const handleDragCancel = useCallback(() => {
|
||||||
setActiveId(null)
|
setActiveId(null)
|
||||||
}, [])
|
}, [])
|
||||||
@ -50,20 +51,14 @@ export function Main({ sectionId }: { sectionId: SectionID }) {
|
|||||||
onDragCancel={handleDragCancel}
|
onDragCancel={handleDragCancel}
|
||||||
>
|
>
|
||||||
<SortableContext items={items} strategy={rectSortingStrategy}>
|
<SortableContext items={items} strategy={rectSortingStrategy}>
|
||||||
<div
|
<GridContainer>
|
||||||
id="grid-container"
|
|
||||||
className="grid w-full gap-5 mt-10"
|
|
||||||
style={{
|
|
||||||
gridTemplateColumns: "repeat(auto-fit, minmax(300px, 1fr))",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items.map(id => (
|
{items.map(id => (
|
||||||
<SortableItem key={id} id={id} />
|
<SortableCardWrapper key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</GridContainer>
|
||||||
</SortableContext>
|
</SortableContext>
|
||||||
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
<DragOverlay adjustScale style={{ transformOrigin: "0 0 " }}>
|
||||||
{!!activeId && <Item id={activeId} isDragging />}
|
{!!activeId && <CardWrapper id={activeId as SourceID} isDragging />}
|
||||||
</DragOverlay>
|
</DragOverlay>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
)
|
)
|
@ -1,4 +1,4 @@
|
|||||||
import type { CSSProperties, HTMLAttributes, PropsWithChildren } from "react"
|
import type { CSSProperties, HTMLAttributes } from "react"
|
||||||
import { useSortable } from "@dnd-kit/sortable"
|
import { useSortable } from "@dnd-kit/sortable"
|
||||||
import { CSS } from "@dnd-kit/utilities"
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
import { forwardRef } from "react"
|
import { forwardRef } from "react"
|
||||||
@ -12,14 +12,6 @@ type ItemProps = HTMLAttributes<HTMLDivElement> & {
|
|||||||
listeners?: SyntheticListenerMap
|
listeners?: SyntheticListenerMap
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GridContainer({ children }: PropsWithChildren) {
|
|
||||||
return (
|
|
||||||
<div className="grid gap-2 max-w-4xl mt-20 grid-cols-4">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Item = forwardRef<HTMLDivElement, ItemProps>(({ id, withOpacity, isDragging, listeners, style, ...props }, ref) => {
|
export const Item = forwardRef<HTMLDivElement, ItemProps>(({ id, withOpacity, isDragging, listeners, style, ...props }, ref) => {
|
||||||
const inlineStyles: CSSProperties = {
|
const inlineStyles: CSSProperties = {
|
||||||
transformOrigin: "50% 50%",
|
transformOrigin: "50% 50%",
|
||||||
@ -40,16 +32,14 @@ export const Item = forwardRef<HTMLDivElement, ItemProps>(({ id, withOpacity, is
|
|||||||
style={inlineStyles}
|
style={inlineStyles}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
|
<div
|
||||||
|
className={clsx("border-b", isDragging ? "cursor-grabbing" : "cursor-grab")}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
你好
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div
|
{id}
|
||||||
className={clsx("border-b", isDragging ? "cursor-grabbing" : "cursor-grab")}
|
|
||||||
{...listeners}
|
|
||||||
>
|
|
||||||
你好
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
{id}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@ -76,6 +66,7 @@ export function SortableItem(props: ItemProps) {
|
|||||||
ref={setNodeRef}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
withOpacity={isDragging}
|
withOpacity={isDragging}
|
||||||
|
isDragging={isDragging}
|
||||||
listeners={listeners}
|
listeners={listeners}
|
||||||
{...attributes}
|
{...attributes}
|
||||||
{...props}
|
{...props}
|
||||||
|
@ -1,116 +1,28 @@
|
|||||||
import type { SourceID, SourceInfo } from "@shared/types"
|
import { useAtomValue } from "jotai"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import type { PropsWithChildren } from "react"
|
||||||
import { formatTime } from "@shared/utils"
|
import { CardWrapper } from "./Card"
|
||||||
import clsx from "clsx"
|
import { currentSectionAtom } from "~/atoms"
|
||||||
import { useInView } from "react-intersection-observer"
|
|
||||||
import { useAtom, useAtomValue } from "jotai"
|
|
||||||
import { useCallback } from "react"
|
|
||||||
import { currentSectionAtom, focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
|
||||||
|
|
||||||
export function Main() {
|
export function GridContainer({ children }: PropsWithChildren) {
|
||||||
const currentSection = useAtomValue(currentSectionAtom)
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
id="grid-container"
|
|
||||||
className="grid w-full gap-5 mt-10"
|
className="grid w-full gap-5 mt-10"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fill, minmax(350px, 1fr))",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Main() {
|
||||||
|
const currentSection = useAtomValue(currentSectionAtom)
|
||||||
|
return (
|
||||||
|
<GridContainer>
|
||||||
{currentSection.sourceList.map(id => (
|
{currentSection.sourceList.map(id => (
|
||||||
<CardWrapper key={id} id={id} />
|
<CardWrapper key={id} id={id} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</GridContainer>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function CardWrapper({ id }: { id: SourceID }) {
|
|
||||||
const { ref, inView } = useInView({
|
|
||||||
threshold: 0,
|
|
||||||
})
|
|
||||||
return (
|
|
||||||
<div ref={ref} className="flex flex-col border rounded-md px-2 h-500px" key={id}>
|
|
||||||
<NewsCard id={id} inView={inView} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function NewsCard({ id, inView }: { id: SourceID, inView: boolean }) {
|
|
||||||
const [focusSources, setFocusSources] = useAtom(focusSourcesAtom)
|
|
||||||
const addFocusList = useCallback(() => {
|
|
||||||
setFocusSources(focusSources.includes(id) ? focusSources.filter(i => i !== id) : [...focusSources, id])
|
|
||||||
}, [setFocusSources, focusSources, id])
|
|
||||||
const [refetchSource, setRefetchSource] = useAtom(refetchSourceAtom)
|
|
||||||
const { isPending, error, isFetching, data } = useQuery({
|
|
||||||
queryKey: [id, refetchSource[id]],
|
|
||||||
queryFn: async ({ queryKey }) => {
|
|
||||||
const [_id, _refetchTime] = queryKey as [SourceID, number]
|
|
||||||
let url = `/api/${_id}`
|
|
||||||
if (Date.now() - _refetchTime < 1000) {
|
|
||||||
url = `/api/${_id}?latest`
|
|
||||||
}
|
|
||||||
const response = await fetch(url)
|
|
||||||
return await response.json() as SourceInfo
|
|
||||||
},
|
|
||||||
// refetch 时显示原有的数据
|
|
||||||
placeholderData: prev => prev,
|
|
||||||
staleTime: 1000 * 60 * 5,
|
|
||||||
enabled: inView,
|
|
||||||
refetchOnWindowFocus: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const manualRefetch = useCallback(() => {
|
|
||||||
setRefetchSource(prev => ({
|
|
||||||
...prev,
|
|
||||||
[id]: Date.now(),
|
|
||||||
}))
|
|
||||||
}, [setRefetchSource, id])
|
|
||||||
|
|
||||||
if (isPending || !data) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{Array.from({ length: 18 }).map((_, i) => i).map(i => <div key={i} className="skeleton m1"></div>)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
} else if (error) {
|
|
||||||
return <div>Error: </div>
|
|
||||||
} else if (data) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-between py-2">
|
|
||||||
<span className="text-md font-bold">
|
|
||||||
{ data?.title }
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{ data?.subtitle}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="overflow-auto">
|
|
||||||
{data?.data.slice(0, 20).map((item, index) => (
|
|
||||||
<div key={item.title} className="flex gap-2 border-b border-gray-300/20">
|
|
||||||
<span>
|
|
||||||
{ index + 1}
|
|
||||||
</span>
|
|
||||||
<a href={item.url} target="_blank">
|
|
||||||
{item.title}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="py-2 flex items-center justify-between">
|
|
||||||
<span>
|
|
||||||
{formatTime(data!.updateTime)}
|
|
||||||
</span>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={clsx("i-ph:arrow-clockwise", isFetching && "animate-spin")}
|
|
||||||
onClick={manualRefetch}
|
|
||||||
/>
|
|
||||||
<button type="button" className={clsx(focusSources.includes(id) ? "i-ph:star-fill" : "i-ph:star")} onClick={addFocusList} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -5,8 +5,8 @@ import clsx from "clsx"
|
|||||||
import { useSetAtom } from "jotai"
|
import { useSetAtom } from "jotai"
|
||||||
import { useEffect } from "react"
|
import { useEffect } from "react"
|
||||||
import { currentSectionAtom } from "~/atoms"
|
import { currentSectionAtom } from "~/atoms"
|
||||||
// import { Main } from "~/components/Main"
|
import { Main as DndMain } from "~/components/Dnd"
|
||||||
import { Main } from "~/components/Pure"
|
import { Main as PureMain } from "~/components/Pure"
|
||||||
|
|
||||||
export const Route = createFileRoute("/")({
|
export const Route = createFileRoute("/")({
|
||||||
validateSearch: (search: any) => ({
|
validateSearch: (search: any) => ({
|
||||||
@ -36,7 +36,9 @@ function IndexComponent() {
|
|||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
<Main />
|
{
|
||||||
|
id === "focus" ? <DndMain /> : <PureMain />
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user