mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 03:09:14 +08:00
pref: colorful card
This commit is contained in:
parent
5d87ce3ea4
commit
698253e4ae
@ -1,6 +1,7 @@
|
|||||||
import { Interval, 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"
|
||||||
|
|
||||||
@ -26,8 +27,11 @@ export default defineEventHandler(async (event): Promise<SourceResponse> => {
|
|||||||
if (cache) {
|
if (cache) {
|
||||||
// interval 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,这个间隔内可能内容压根不会更新。
|
// interval 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,这个间隔内可能内容压根不会更新。
|
||||||
// 默认 10 分钟,是低于 TTL 的,但部分 Source 的间隔会超过 TTL,甚至有的一天刷新一次。
|
// 默认 10 分钟,是低于 TTL 的,但部分 Source 的间隔会超过 TTL,甚至有的一天刷新一次。
|
||||||
const interval = sources[id]?.interval ?? 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: {
|
||||||
|
@ -29,7 +29,7 @@ export default defineSource(async () => {
|
|||||||
const res: Res = await $fetch(url)
|
const res: Res = await $fetch(url)
|
||||||
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
|
if (!res.ok || res.data.realtime.length === 0) throw new Error("Cannot fetch data")
|
||||||
return res.data.realtime
|
return res.data.realtime
|
||||||
.filter(k => !k.icon_desc || k.icon_desc !== "荐")
|
.filter(k => !k.icon_desc || !/[荐促商宣]/.test(k.icon_desc))
|
||||||
.slice(0, 20)
|
.slice(0, 20)
|
||||||
.map((k) => {
|
.map((k) => {
|
||||||
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
||||||
|
26
shared/colors.ts
Normal file
26
shared/colors.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
export const colors = [
|
||||||
|
"blue", // #0000FF
|
||||||
|
"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]
|
@ -14,11 +14,13 @@ const Time = {
|
|||||||
export const originSources = {
|
export const originSources = {
|
||||||
"v2ex": {
|
"v2ex": {
|
||||||
name: "V2EX",
|
name: "V2EX",
|
||||||
|
color: "slate",
|
||||||
home: "https://v2ex.com/",
|
home: "https://v2ex.com/",
|
||||||
},
|
},
|
||||||
"coolapk": {
|
"coolapk": {
|
||||||
name: "酷安",
|
name: "酷安",
|
||||||
type: "hottest",
|
type: "hottest",
|
||||||
|
color: "green",
|
||||||
home: "https://coolapk.com",
|
home: "https://coolapk.com",
|
||||||
},
|
},
|
||||||
"wallstreetcn": {
|
"wallstreetcn": {
|
||||||
@ -29,10 +31,12 @@ export const originSources = {
|
|||||||
},
|
},
|
||||||
"sputniknewscn": {
|
"sputniknewscn": {
|
||||||
name: "俄罗斯卫星通讯社",
|
name: "俄罗斯卫星通讯社",
|
||||||
|
color: "yellow",
|
||||||
home: "https://sputniknews.cn",
|
home: "https://sputniknews.cn",
|
||||||
},
|
},
|
||||||
"cankaoxiaoxi": {
|
"cankaoxiaoxi": {
|
||||||
name: "参考消息",
|
name: "参考消息",
|
||||||
|
color: "red",
|
||||||
interval: Time.Common,
|
interval: Time.Common,
|
||||||
home: "https://china.cankaoxiaoxi.com",
|
home: "https://china.cankaoxiaoxi.com",
|
||||||
},
|
},
|
||||||
@ -48,6 +52,7 @@ export const originSources = {
|
|||||||
"douyin": {
|
"douyin": {
|
||||||
name: "抖音",
|
name: "抖音",
|
||||||
type: "hottest",
|
type: "hottest",
|
||||||
|
color: "gray",
|
||||||
home: "https://www.douyin.com",
|
home: "https://www.douyin.com",
|
||||||
},
|
},
|
||||||
"hupu": {
|
"hupu": {
|
||||||
@ -63,6 +68,7 @@ export const originSources = {
|
|||||||
name: "微博",
|
name: "微博",
|
||||||
title: "实时热搜",
|
title: "实时热搜",
|
||||||
type: "hottest",
|
type: "hottest",
|
||||||
|
color: "red",
|
||||||
interval: Time.Realtime,
|
interval: Time.Realtime,
|
||||||
home: "https://weibo.com",
|
home: "https://weibo.com",
|
||||||
},
|
},
|
||||||
@ -73,6 +79,7 @@ export const originSources = {
|
|||||||
"zaobao": {
|
"zaobao": {
|
||||||
name: "联合早报",
|
name: "联合早报",
|
||||||
interval: Time.Common,
|
interval: Time.Common,
|
||||||
|
color: "red",
|
||||||
home: "https://www.zaobao.com",
|
home: "https://www.zaobao.com",
|
||||||
},
|
},
|
||||||
"thepaper": {
|
"thepaper": {
|
||||||
@ -83,10 +90,12 @@ export const originSources = {
|
|||||||
"toutiao": {
|
"toutiao": {
|
||||||
name: "今日头条",
|
name: "今日头条",
|
||||||
type: "hottest",
|
type: "hottest",
|
||||||
|
color: "red",
|
||||||
home: "https://www.toutiao.com",
|
home: "https://www.toutiao.com",
|
||||||
},
|
},
|
||||||
"ithome": {
|
"ithome": {
|
||||||
name: "IT之家",
|
name: "IT之家",
|
||||||
|
color: "red",
|
||||||
home: "https://www.ithome.com",
|
home: "https://www.ithome.com",
|
||||||
},
|
},
|
||||||
} as const satisfies Record<string, OriginSource>
|
} as const satisfies Record<string, OriginSource>
|
||||||
@ -103,14 +112,16 @@ function genSources() {
|
|||||||
redirect: `${id}-${subId}`,
|
redirect: `${id}-${subId}`,
|
||||||
name: source.name,
|
name: source.name,
|
||||||
type: source.type,
|
type: source.type,
|
||||||
interval: source.interval,
|
color: source.color ?? "blue",
|
||||||
|
interval: source.interval ?? Time.Default,
|
||||||
...subSource,
|
...subSource,
|
||||||
}] as [any, Source])
|
}] as [any, Source])
|
||||||
}
|
}
|
||||||
_.push([`${id}-${subId}`, {
|
_.push([`${id}-${subId}`, {
|
||||||
name: source.name,
|
name: source.name,
|
||||||
type: source.type,
|
type: source.type,
|
||||||
interval: source.interval,
|
color: source.color ?? "blue",
|
||||||
|
interval: source.interval ?? Time.Default,
|
||||||
...subSource,
|
...subSource,
|
||||||
}] as [any, Source])
|
}] as [any, Source])
|
||||||
})
|
})
|
||||||
@ -118,7 +129,8 @@ function genSources() {
|
|||||||
_.push([id, {
|
_.push([id, {
|
||||||
name: source.name,
|
name: source.name,
|
||||||
type: source.type,
|
type: source.type,
|
||||||
interval: source.interval,
|
color: source.color ?? "blue",
|
||||||
|
interval: source.interval ?? Time.Default,
|
||||||
title: source.title,
|
title: source.title,
|
||||||
}])
|
}])
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import type { Color } from "./colors"
|
||||||
import type { sectionIds } from "./data"
|
import type { sectionIds } from "./data"
|
||||||
import type { originSources } from "./sources"
|
import type { originSources } from "./sources"
|
||||||
|
|
||||||
@ -26,6 +27,7 @@ export interface OriginSource {
|
|||||||
*/
|
*/
|
||||||
type?: "hottest" | "latest"
|
type?: "hottest" | "latest"
|
||||||
home: string
|
home: string
|
||||||
|
color?: Color
|
||||||
sub?: Record<string, {
|
sub?: Record<string, {
|
||||||
title: string
|
title: string
|
||||||
/**
|
/**
|
||||||
@ -40,7 +42,8 @@ export interface Source {
|
|||||||
name: string
|
name: string
|
||||||
title?: string
|
title?: string
|
||||||
type?: "hottest" | "latest"
|
type?: "hottest" | "latest"
|
||||||
interval?: number
|
color: Color
|
||||||
|
interval: number
|
||||||
redirect?: SourceID
|
redirect?: SourceID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
|||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col h-500px 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",
|
||||||
isDragged && "op-50",
|
isDragged && "op-50",
|
||||||
|
`bg-${sources[id].color}`,
|
||||||
)}
|
)}
|
||||||
style={{
|
style={{
|
||||||
transformOrigin: "50% 50%",
|
transformOrigin: "50% 50%",
|
||||||
@ -59,17 +60,18 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
|||||||
export function CardOverlay({ id }: { id: SourceID }) {
|
export function CardOverlay({ id }: { id: SourceID }) {
|
||||||
return (
|
return (
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
"flex flex-col h-500px rounded-2xl bg-blue bg-op-50 p-4 backdrop-blur-5",
|
"flex flex-col h-500px rounded-2xl bg-op-50 p-4 backdrop-blur-5",
|
||||||
"backdrop-blur-5 bg-op-40",
|
"backdrop-blur-5 bg-op-40",
|
||||||
|
`bg-${sources[id].color}`,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<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")}>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<img
|
<div
|
||||||
src={`/icons/${id.split("-")[0]}.png`}
|
className={clsx("w-8 h-8 rounded-full")}
|
||||||
className={clsx("h-8 rounded-full")}
|
style={{
|
||||||
alt={id}
|
background: `center / contain no-repeat url(/icons/${id.split("-")[0]}.png)`,
|
||||||
onError={e => e.currentTarget.src = "/icons/default.png"}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
@ -131,11 +133,11 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) {
|
|||||||
<>
|
<>
|
||||||
<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")}>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<img
|
<div
|
||||||
src={`/icons/${id.split("-")[0]}.png`}
|
className={clsx("w-8 h-8 rounded-full")}
|
||||||
className={clsx("h-8 rounded-full")}
|
style={{
|
||||||
alt={id}
|
background: `center / contain no-repeat url(/icons/${id.split("-")[0]}.png)`,
|
||||||
onError={e => e.currentTarget.src = "/icons/default.png"}
|
}}
|
||||||
/>
|
/>
|
||||||
<span className="flex flex-col">
|
<span className="flex flex-col">
|
||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
|
18
src/hooks/useSticky.ts
Normal file
18
src/hooks/useSticky.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react"
|
||||||
|
|
||||||
|
export function useSticky() {
|
||||||
|
const ref = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [isSticky, setIsSticky] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([event]) => setIsSticky(event.intersectionRatio < 1),
|
||||||
|
{ threshold: [1], rootMargin: "-1px 0px 0px 0px" },
|
||||||
|
)
|
||||||
|
observer.observe(ref.current!)
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return { ref, isSticky }
|
||||||
|
}
|
@ -5,9 +5,12 @@ import "~/styles/globals.css"
|
|||||||
import "virtual:uno.css"
|
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 { 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
|
||||||
@ -25,10 +28,20 @@ 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="h-full overflow-x-auto relative">
|
<OverlayScrollbar className="h-full overflow-x-auto relative">
|
||||||
<header className="flex justify-between items-center sticky top-0 z-100 bg-base py-4 px-6 md:(pt-8 px-16)">
|
<header
|
||||||
|
ref={ref}
|
||||||
|
className={
|
||||||
|
clsx("flex justify-between items-center py-4 px-8 md:(pt-8 px-20)", "sticky top-0 z-100 backdrop-blur-md")
|
||||||
|
}
|
||||||
|
>
|
||||||
<Header />
|
<Header />
|
||||||
</header>
|
</header>
|
||||||
<main className="min-h-[calc(100vh-12rem)] px-6 md:(px-16)">
|
<main className="min-h-[calc(100vh-12rem)] px-6 md:(px-16)">
|
||||||
|
@ -37,10 +37,9 @@ button:disabled {
|
|||||||
--os-handle-bg-active: rgba(255, 255, 255, 0.66);
|
--os-handle-bg-active: rgba(255, 255, 255, 0.66);
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
|
||||||
box-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 .card {
|
.bg {
|
||||||
box-shadow: rgba(50, 50, 105, 0.15) 0px 2px 5px 0px, rgba(0, 0, 0, 0.05) 0px 1px 1px 0px;
|
background-image: radial-gradient(ellipse 80% 80% at 50% -30%,
|
||||||
|
rgba(254, 94, 83, 0.3),
|
||||||
|
rgba(255, 255, 255, 0));
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import { defineConfig, presetIcons, presetUno, transformerDirectives, transformerVariantGroup } from "unocss"
|
import { defineConfig, presetIcons, presetUno, transformerDirectives, transformerVariantGroup } from "unocss"
|
||||||
|
import { colors } from "./shared/colors"
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
mergeSelectors: false,
|
mergeSelectors: false,
|
||||||
@ -26,26 +27,15 @@ export default defineConfig({
|
|||||||
|
|
||||||
"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": "border border-base rounded flex gap-2 items-center px2 py1 op75 hover:op100 hover:bg-hover",
|
||||||
"btn-action-sm": "btn-action text-sm",
|
|
||||||
"btn-action-active": "color-active border-active! bg-active op100!",
|
"btn-action-active": "color-active border-active! bg-active op100!",
|
||||||
"skeleton": "bg-gray-400/10 rounded-md h-5 w-full animate-pulse",
|
"skeleton": "bg-gray-400/10 rounded-md h-5 w-full animate-pulse",
|
||||||
},
|
},
|
||||||
theme: {
|
safelist: [
|
||||||
colors: {
|
...colors.map(color => `bg-${color}`),
|
||||||
primary: {
|
],
|
||||||
DEFAULT: "#FDB022",
|
extendTheme: (theme) => {
|
||||||
50: "#FFFCF5",
|
// @ts-expect-error >_<
|
||||||
100: "#FFFAEB",
|
theme.colors.primary = theme.colors.red
|
||||||
200: "#FEF0C7",
|
return theme
|
||||||
300: "#FEDF89",
|
|
||||||
400: "#FEC84B",
|
|
||||||
500: "#FDB022",
|
|
||||||
600: "#F79009",
|
|
||||||
700: "#DC6803",
|
|
||||||
800: "#B54708",
|
|
||||||
900: "#93370D",
|
|
||||||
950: "#7A2E0E",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Loading…
x
Reference in New Issue
Block a user