mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
pref: ui
This commit is contained in:
parent
7a49cc7eec
commit
42d8653d3d
@ -61,6 +61,7 @@
|
|||||||
"@types/md5": "^2.3.5",
|
"@types/md5": "^2.3.5",
|
||||||
"@types/react": "^18.3.11",
|
"@types/react": "^18.3.11",
|
||||||
"@types/react-dom": "^18.3.0",
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@unocss/rule-utils": "^0.63.4",
|
||||||
"@vitejs/plugin-react-swc": "^3.7.1",
|
"@vitejs/plugin-react-swc": "^3.7.1",
|
||||||
"bumpp": "^9.7.1",
|
"bumpp": "^9.7.1",
|
||||||
"eslint": "^9.12.0",
|
"eslint": "^9.12.0",
|
||||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -126,6 +126,9 @@ importers:
|
|||||||
'@types/react-dom':
|
'@types/react-dom':
|
||||||
specifier: ^18.3.0
|
specifier: ^18.3.0
|
||||||
version: 18.3.0
|
version: 18.3.0
|
||||||
|
'@unocss/rule-utils':
|
||||||
|
specifier: ^0.63.4
|
||||||
|
version: 0.63.4
|
||||||
'@vitejs/plugin-react-swc':
|
'@vitejs/plugin-react-swc':
|
||||||
specifier: ^3.7.1
|
specifier: ^3.7.1
|
||||||
version: 3.7.1(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))
|
version: 3.7.1(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0))
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import { TTL } from "@shared/consts"
|
import { TTL } from "@shared/consts"
|
||||||
import type { SourceID, SourceResponse } from "@shared/types"
|
import type { SourceID, SourceResponse } from "@shared/types"
|
||||||
import { sources } from "@shared/sources"
|
import { sources } from "@shared/sources"
|
||||||
import { delay } from "@shared/utils"
|
|
||||||
import { sourcesFn } from "#/sources"
|
import { sourcesFn } from "#/sources"
|
||||||
import { Cache } from "#/cache"
|
import { Cache } from "#/cache"
|
||||||
|
|
||||||
@ -29,9 +28,6 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
|||||||
// 默认 10 分钟,是低于 TTL 的,但部分 Source 的间隔会超过 TTL,甚至有的一天刷新一次。
|
// 默认 10 分钟,是低于 TTL 的,但部分 Source 的间隔会超过 TTL,甚至有的一天刷新一次。
|
||||||
const interval = sources[id].interval
|
const interval = sources[id].interval
|
||||||
if (now - cache.updated < interval) {
|
if (now - cache.updated < interval) {
|
||||||
if (id === "cankaoxiaoxi") {
|
|
||||||
await delay(2000)
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
data: {
|
data: {
|
||||||
|
@ -1,26 +1,6 @@
|
|||||||
export const colors = [
|
import { colors as c } from "unocss/preset-mini"
|
||||||
"blue", // #0000FF
|
import { typeSafeObjectKeys } from "./type.util"
|
||||||
"indigo", // #4B0082
|
|
||||||
"violet", // #EE82EE
|
|
||||||
"purple", // #800080
|
|
||||||
"fuchsia", // #FF00FF
|
|
||||||
"pink", // #FFC0CB
|
|
||||||
"rose", // #FF007F
|
|
||||||
"amber", // #FFBF00
|
|
||||||
"yellow", // #FFFF00
|
|
||||||
"lime", // #00FF00
|
|
||||||
"green", // #008000
|
|
||||||
"emerald", // #50C878
|
|
||||||
"teal", // #008080
|
|
||||||
"cyan", // #00FFFF
|
|
||||||
"sky", // #87CEEB
|
|
||||||
"slate", // #708090
|
|
||||||
"gray", // #808080
|
|
||||||
"zinc", // #A0A0A0
|
|
||||||
"neutral", // #828282
|
|
||||||
"stone", // #D2B48C
|
|
||||||
"red", // #FF0000
|
|
||||||
"orange", // #FFA500
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type Color = typeof colors[number]
|
export const colors = typeSafeObjectKeys(c)
|
||||||
|
|
||||||
|
export type Color = Exclude<typeof colors[number], "current" | "inherit" | "transparent" | "black" | "white">
|
||||||
|
@ -13,3 +13,7 @@ export function typeSafeObjectFromEntries<
|
|||||||
export function typeSafeObjectEntries<T extends Record<PropertyKey, unknown>>(obj: T): { [K in keyof T]: [K, T[K]] }[keyof T][] {
|
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][]
|
return Object.entries(obj) as { [K in keyof T]: [K, T[K]] }[keyof T][]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function typeSafeObjectKeys<T extends Record<PropertyKey, unknown>>(obj: T): (keyof T)[] {
|
||||||
|
return Object.keys(obj) as (keyof T)[]
|
||||||
|
}
|
||||||
|
@ -59,14 +59,17 @@ export function Header() {
|
|||||||
<img src={logo} alt="logo" className="h-12" />
|
<img src={logo} alt="logo" className="h-12" />
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span>News</span>
|
<span>News</span>
|
||||||
<span>Now</span>
|
<span>
|
||||||
|
<span className="color-red-6">N</span>
|
||||||
|
<span>ow</span>
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
<a className="btn-pure text-sm ml-1">
|
<a className="btn-pure text-sm ml-1">
|
||||||
{`v${Version}`}
|
{`v${Version}`}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex gap-2 items-center text-2xl text-primary">
|
<span className="flex gap-2 items-center text-xl text-primary-600 dark:text-primary ">
|
||||||
<RefreshButton />
|
<RefreshButton />
|
||||||
<ThemeToggle />
|
<ThemeToggle />
|
||||||
<GithubIcon />
|
<GithubIcon />
|
||||||
|
@ -4,7 +4,7 @@ import { useQuery } from "@tanstack/react-query"
|
|||||||
import clsx from "clsx"
|
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, useMemo, useRef } from "react"
|
||||||
import { sources } from "@shared/sources"
|
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"
|
||||||
@ -42,7 +42,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col h-500px rounded-2xl bg-op-50 p-4 backdrop-blur-5",
|
"flex flex-col h-500px rounded-2xl bg-op-40 p-4 backdrop-blur-5",
|
||||||
isDragged && "op-50",
|
isDragged && "op-50",
|
||||||
`bg-${sources[id].color}`,
|
`bg-${sources[id].color}`,
|
||||||
)}
|
)}
|
||||||
@ -78,19 +78,19 @@ export function CardOverlay({ id }: { id: SourceID }) {
|
|||||||
<span className="text-xl font-bold">
|
<span className="text-xl font-bold">
|
||||||
{sources[id].name}
|
{sources[id].name}
|
||||||
</span>
|
</span>
|
||||||
{sources[id]?.title && <span className="text-sm">{sources[id].title}</span>}
|
{sources[id]?.title && <span className={clsx("text-sm", `color-${sources[id].color} bg-base op-80 bg-op-50! px-1 rounded`)}>{sources[id].title}</span>}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-xs">正在刷新</span>
|
<span className="text-xs">拖拽中</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 op-80">
|
<div className={clsx("flex gap-2 text-lg", `color-${sources[id].color}`)}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
|
className={clsx("i-ph:dots-six-vertical-duotone", "cursor-grabbing")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-full p-2 overflow-x-auto bg-base bg-op-70! rounded-2xl" />
|
<div className={`h-full p-2 overflow-x-auto bg-base bg-op-50! rounded-2xl sprinkle-${sources[id].color}`} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -129,6 +129,8 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
|
|||||||
}))
|
}))
|
||||||
}, [setRefetchSource, id])
|
}, [setRefetchSource, id])
|
||||||
|
|
||||||
|
const isFreshFetching = useMemo(() => query.isFetching && !query.isPlaceholderData, [query])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
|
<div className={clsx("flex justify-between mx-2 mt-0 mb-2 items-center")}>
|
||||||
@ -169,12 +171,18 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<OverlayScrollbar
|
<OverlayScrollbar
|
||||||
className={clsx("h-full p-2 overflow-x-auto bg-base bg-op-70! rounded-2xl transition-filter duration-500", query.isFetching && "blur-3px")}
|
className={clsx([
|
||||||
|
"h-full p-2 overflow-x-auto rounded-2xl bg-base bg-op-70!",
|
||||||
|
isFreshFetching && `animate-pulse`,
|
||||||
|
`sprinkle-${sources[id].color}`,
|
||||||
|
])}
|
||||||
options={{
|
options={{
|
||||||
overflow: { x: "hidden" },
|
overflow: { x: "hidden" },
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sources[id].type === "hottest" ? <NewsList query={query} /> : <NewsListTimeLine query={query} />}
|
<div className={clsx("duration-500 transition-opacity", isFreshFetching && "op-20")}>
|
||||||
|
{sources[id].type === "hottest" ? <NewsList query={query} /> : <NewsListTimeLine query={query} />}
|
||||||
|
</div>
|
||||||
</OverlayScrollbar>
|
</OverlayScrollbar>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -17,8 +17,8 @@ export function Section({ id }: { id: SectionID }) {
|
|||||||
<>
|
<>
|
||||||
<div className="w-full flex justify-center">
|
<div className="w-full flex justify-center">
|
||||||
<span className={clsx([
|
<span className={clsx([
|
||||||
"flex gap-2 mb-6 py-3 px-6 rounded-2xl bg-primary bg-op-10 dark:bg-op-5",
|
"flex gap-2 mb-4 py-3 px-6 rounded-2xl bg-primary/1 shadow shadow-red/20 hover:shadow-red/50 transition-shadow duration-500",
|
||||||
"md:(z-100)",
|
"md:(z-100 mb-6)",
|
||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
{sectionIds.map(section => (
|
{sectionIds.map(section => (
|
||||||
@ -27,8 +27,8 @@ export function Section({ id }: { id: SectionID }) {
|
|||||||
to="/s/$section"
|
to="/s/$section"
|
||||||
params={{ section }}
|
params={{ section }}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"op-90",
|
"text-sm",
|
||||||
id === section && "color-primary font-bold op-100!",
|
id === section ? "color-primary font-bold" : "op-70 dark:op-90",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{metadata[section].name}
|
{metadata[section].name}
|
||||||
|
@ -6,11 +6,9 @@ import "virtual:uno.css"
|
|||||||
import type { QueryClient } from "@tanstack/react-query"
|
import type { QueryClient } from "@tanstack/react-query"
|
||||||
import { Author, Homepage } from "@shared/consts"
|
import { Author, Homepage } from "@shared/consts"
|
||||||
import clsx from "clsx"
|
import clsx from "clsx"
|
||||||
import { useEffect } from "react"
|
|
||||||
import { Header } from "~/components/header"
|
import { Header } from "~/components/header"
|
||||||
import { useOnReload } from "~/hooks/useOnReload"
|
import { useOnReload } from "~/hooks/useOnReload"
|
||||||
import { OverlayScrollbar } from "~/components/common/overlay-scrollbar"
|
import { OverlayScrollbar } from "~/components/common/overlay-scrollbar"
|
||||||
import { useSticky } from "~/hooks/useSticky"
|
|
||||||
|
|
||||||
export const Route = createRootRouteWithContext<{
|
export const Route = createRootRouteWithContext<{
|
||||||
queryClient: QueryClient
|
queryClient: QueryClient
|
||||||
@ -28,23 +26,28 @@ function NotFoundComponent() {
|
|||||||
|
|
||||||
function RootComponent() {
|
function RootComponent() {
|
||||||
useOnReload()
|
useOnReload()
|
||||||
const { ref, isSticky } = useSticky()
|
|
||||||
useEffect(() => {
|
|
||||||
console.log(isSticky)
|
|
||||||
}, [isSticky])
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="absolute top-0 z-[-2] h-screen w-screen bg"></div>
|
<OverlayScrollbar className={clsx([
|
||||||
<OverlayScrollbar className="h-full overflow-x-auto relative">
|
"h-full overflow-x-auto relative px-4",
|
||||||
<header
|
"md:(px-10)",
|
||||||
ref={ref}
|
"lg:(px-24)",
|
||||||
className={
|
])}
|
||||||
clsx("flex justify-between items-center py-4 px-8 md:(py-6 px-20)", "sticky top-0 z-100 backdrop-blur-md")
|
>
|
||||||
}
|
<header className={clsx([
|
||||||
|
"flex justify-between items-center py-4 px-5",
|
||||||
|
"lg:(py-6)",
|
||||||
|
"sticky top-0 z-100 backdrop-blur-md",
|
||||||
|
])}
|
||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-[calc(100vh-12rem)] px-4 md:(px-16 mt--16 min-h-[calc(100vh-8rem)] )">
|
<main className={clsx([
|
||||||
|
"min-h-[calc(100vh-170px)] transition-margin",
|
||||||
|
"md:(min-h-[calc(100vh-105px)] mt--16)",
|
||||||
|
"lg:(min-h-[calc(100vh-120px)] mt--17)",
|
||||||
|
])}
|
||||||
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<footer className="py-6 flex flex-col items-center justify-center text-sm text-neutral-500">
|
<footer className="py-6 flex flex-col items-center justify-center text-sm text-neutral-500">
|
||||||
|
@ -15,7 +15,7 @@ html.dark {
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
--at-apply: color-base bg-base;
|
--at-apply: color-base bg-base sprinkle-red;
|
||||||
-moz-user-select: none;
|
-moz-user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { defineConfig, presetIcons, presetUno, transformerDirectives, transformerVariantGroup } from "unocss"
|
import { defineConfig, presetIcons, presetUno, transformerDirectives, transformerVariantGroup } from "unocss"
|
||||||
import { colors } from "./shared/colors"
|
import { hex2rgba } from "@unocss/rule-utils"
|
||||||
|
import { colors } from "unocss/preset-mini"
|
||||||
|
import { sources } from "./shared/sources"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mergeSelectors: false,
|
mergeSelectors: false,
|
||||||
@ -10,24 +12,33 @@ export default defineConfig({
|
|||||||
scale: 1.2,
|
scale: 1.2,
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
rules: [],
|
rules: [
|
||||||
|
[/^sprinkle-(.+)$/, ([_, d]) => {
|
||||||
|
if (d in colors) {
|
||||||
|
// @ts-expect-error >_<
|
||||||
|
const hex: any = colors[d]?.[400]
|
||||||
|
if (hex) {
|
||||||
|
return {
|
||||||
|
"background-image": `radial-gradient(ellipse 80% 80% at 50% -30%,
|
||||||
|
rgba(${hex2rgba(hex)?.join(", ")}, 0.3),
|
||||||
|
rgba(255, 255, 255, 0));`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
],
|
||||||
shortcuts: {
|
shortcuts: {
|
||||||
"color-base": "color-neutral-800 dark:color-neutral-300",
|
"color-base": "color-neutral-800 dark:color-neutral-300",
|
||||||
"bg-base": "bg-white dark:bg-dark-600",
|
"bg-base": "bg-white dark:bg-dark-600",
|
||||||
"shadow-base": "shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)] dark:shadow-[0px_2px_3px_-1px_rgba(0,0,0,0.1),0px_1px_0px_0px_rgba(25,28,33,0.02),0px_0px_0px_1px_rgba(25,28,33,0.08)]",
|
|
||||||
|
|
||||||
"bg-hover": "bg-primary-400/5",
|
|
||||||
|
|
||||||
"color-active": "color-primary-600 dark:color-primary-400",
|
"color-active": "color-primary-600 dark:color-primary-400",
|
||||||
"border-active": "border-primary-600/25 dark:border-primary-400/25",
|
"border-active": "border-primary-600/25 dark:border-primary-400/25",
|
||||||
"bg-active": "bg-primary-400/10",
|
"bg-active": "bg-primary-400/10",
|
||||||
|
|
||||||
"btn-pure": "op50 hover:op75",
|
"btn-pure": "op50 hover:op75",
|
||||||
"btn-action": "border border-base rounded flex gap-2 items-center px2 py1 op75 hover:op100 hover:bg-hover",
|
|
||||||
"btn-action-active": "color-active border-active! bg-active op100!",
|
|
||||||
},
|
},
|
||||||
safelist: [
|
safelist: [
|
||||||
...["bg", "color", "border"].map(t => colors.map(c => `${t}-${c}`)).flat(1),
|
...["bg", "color", "border", "sprinkle", "shadow"].map(t => Object.values(sources).map(c => `${t}-${c.color}`)).flat(1),
|
||||||
],
|
],
|
||||||
extendTheme: (theme) => {
|
extendTheme: (theme) => {
|
||||||
// @ts-expect-error >_<
|
// @ts-expect-error >_<
|
||||||
|
Loading…
x
Reference in New Issue
Block a user