mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-19 11:19:14 +08:00
pref: server logic
This commit is contained in:
parent
f2711fd80f
commit
6a8e3ba1c5
@ -9,6 +9,7 @@ export class Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
const last = performance.now()
|
||||||
await this.db.prepare(`
|
await this.db.prepare(`
|
||||||
CREATE TABLE IF NOT EXISTS cache (
|
CREATE TABLE IF NOT EXISTS cache (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
@ -17,23 +18,29 @@ export class Cache {
|
|||||||
expires INTEGER
|
expires INTEGER
|
||||||
);
|
);
|
||||||
`).run()
|
`).run()
|
||||||
|
console.log(`init: `, performance.now() - last)
|
||||||
}
|
}
|
||||||
|
|
||||||
async set(key: string, value: any) {
|
async set(key: string, value: any) {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
return await this.db.prepare(
|
const last = performance.now()
|
||||||
|
await this.db.prepare(
|
||||||
`INSERT OR REPLACE INTO cache (id, data, updated, expires) VALUES (?, ?, ?, ?)`,
|
`INSERT OR REPLACE INTO cache (id, data, updated, expires) VALUES (?, ?, ?, ?)`,
|
||||||
).run(key, JSON.stringify(value), now, now + TTL)
|
).run(key, JSON.stringify(value), now, now + TTL)
|
||||||
|
console.log(`set ${key}: `, performance.now() - last)
|
||||||
}
|
}
|
||||||
|
|
||||||
async get(key: string): Promise<CacheInfo> {
|
async get(key: string): Promise<CacheInfo> {
|
||||||
|
const last = performance.now()
|
||||||
const row: any = await this.db.prepare(`SELECT id, data, updated, expires FROM cache WHERE id = ?`).get(key)
|
const row: any = await this.db.prepare(`SELECT id, data, updated, expires FROM cache WHERE id = ?`).get(key)
|
||||||
return row
|
const r = row
|
||||||
? {
|
? {
|
||||||
...row,
|
...row,
|
||||||
data: JSON.parse(row.data),
|
data: JSON.parse(row.data),
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
console.log(`get ${key}: `, performance.now() - last)
|
||||||
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(key: string) {
|
async delete(key: string) {
|
||||||
|
@ -11,7 +11,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
const db = useDatabase()
|
const db = useDatabase()
|
||||||
const cacheStore = db ? new Cache(db) : undefined
|
const cacheStore = db ? new Cache(db) : undefined
|
||||||
if (cacheStore) {
|
if (cacheStore) {
|
||||||
await cacheStore.init()
|
// await cacheStore.init()
|
||||||
const cache = await cacheStore.get(id)
|
const cache = await cacheStore.get(id)
|
||||||
if (cache) {
|
if (cache) {
|
||||||
if (!latest && cache.expires > Date.now()) {
|
if (!latest && cache.expires > Date.now()) {
|
||||||
@ -29,14 +29,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!sources[id]) {
|
if (!sources[id]) {
|
||||||
|
const last = performance.now()
|
||||||
const data = await fallback(id)
|
const data = await fallback(id)
|
||||||
|
console.log(`fetch: ${id}`, performance.now() - last)
|
||||||
if (cacheStore) await cacheStore.set(id, data)
|
if (cacheStore) await cacheStore.set(id, data)
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
data,
|
data,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const data = await sources[id]()
|
const last = performance.now()
|
||||||
|
const data = await (await sources[id])()
|
||||||
|
console.log(`fetch: ${id}`, performance.now() - last)
|
||||||
if (cacheStore) await cacheStore.set(id, data)
|
if (cacheStore) await cacheStore.set(id, data)
|
||||||
return {
|
return {
|
||||||
status: "success",
|
status: "success",
|
||||||
|
3
server/sources/36kr-quick.ts
Normal file
3
server/sources/36kr-quick.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { defineRSSSource } from "#/utils"
|
||||||
|
|
||||||
|
export default defineRSSSource("https://rsshub.rssforever.com/36kr/newsflashes")
|
@ -22,9 +22,7 @@ export async function fallback(id: string): Promise<SourceInfo> {
|
|||||||
const res: Res = await $fetch(url)
|
const res: Res = await $fetch(url)
|
||||||
if (res.code !== 200 || !res.data) throw new Error(res.message)
|
if (res.code !== 200 || !res.data) throw new Error(res.message)
|
||||||
return {
|
return {
|
||||||
name: res.title,
|
updatedTime: res.updateTime,
|
||||||
type: res.subtitle,
|
|
||||||
updateTime: res.updateTime,
|
|
||||||
items: res.data.map(item => ({
|
items: res.data.map(item => ({
|
||||||
extra: {
|
extra: {
|
||||||
date: item.time,
|
date: item.time,
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
import { peopledaily } from "./peopledaily"
|
import type { SourceID, SourceInfo } from "@shared/types"
|
||||||
import { weibo } from "./weibo"
|
import peopledaily from "./peopledaily"
|
||||||
import { zaobao } from "./zaobao"
|
import weibo from "./weibo"
|
||||||
|
import zaobao from "./zaobao"
|
||||||
|
import kr from "./36kr-quick"
|
||||||
|
|
||||||
export { fallback } from "./fallback"
|
export { fallback } from "./fallback"
|
||||||
|
|
||||||
export const sources = {
|
export const sources = {
|
||||||
peopledaily,
|
peopledaily,
|
||||||
weibo,
|
weibo,
|
||||||
zaobao: () => zaobao("中国聚焦"),
|
zaobao,
|
||||||
}
|
"36kr-quick": kr,
|
||||||
|
} as Record<SourceID, () => Promise<SourceInfo>>
|
||||||
|
@ -1,17 +1,3 @@
|
|||||||
import type { RSS2JSON, SourceInfo } from "@shared/types"
|
import { defineRSSSource } from "#/utils"
|
||||||
import { rss2json } from "#/utils/rss2json"
|
|
||||||
|
|
||||||
export async function peopledaily(): Promise<SourceInfo> {
|
export default defineRSSSource("https://feedx.net/rss/people.xml")
|
||||||
const source = await rss2json("https://feedx.net/rss/people.xml")
|
|
||||||
if (!source?.items.length) throw new Error("Cannot fetch data")
|
|
||||||
return {
|
|
||||||
name: "人民日报",
|
|
||||||
type: "报纸",
|
|
||||||
updateTime: Date.now(),
|
|
||||||
items: source.items.slice(0, 30).map((item: RSS2JSON) => ({
|
|
||||||
title: item.title,
|
|
||||||
url: item.link,
|
|
||||||
id: item.link,
|
|
||||||
})),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { SourceInfo } from "@shared/types"
|
import { defineSource } from "#/utils"
|
||||||
|
|
||||||
interface Res {
|
interface Res {
|
||||||
ok: number // 1 is ok
|
ok: number // 1 is ok
|
||||||
@ -26,15 +26,14 @@ interface Res {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function weibo(): Promise<SourceInfo> {
|
export default defineSource(async () => {
|
||||||
const url = "https://weibo.com/ajax/side/hotSearch"
|
const url = "https://weibo.com/ajax/side/hotSearch"
|
||||||
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 {
|
return res.data.realtime
|
||||||
name: "微博热搜",
|
.filter(k => !k.icon_desc || k.icon_desc !== "荐")
|
||||||
updateTime: Date.now(),
|
.slice(0, 20)
|
||||||
type: "热搜",
|
.map((k) => {
|
||||||
items: res.data.realtime.filter(k => !k.icon_desc || k.icon_desc !== "荐").map((k) => {
|
|
||||||
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
const keyword = k.word_scheme ? k.word_scheme : `#${k.word}#`
|
||||||
return {
|
return {
|
||||||
id: k.num,
|
id: k.num,
|
||||||
@ -45,6 +44,5 @@ export async function weibo(): Promise<SourceInfo> {
|
|||||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
|
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(keyword)}`,
|
||||||
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
|
mobileUrl: `https://m.weibo.cn/search?containerid=231522type%3D1%26q%3D${encodeURIComponent(keyword)}&_T_WM=16922097837&v_p=42`,
|
||||||
}
|
}
|
||||||
}),
|
})
|
||||||
}
|
})
|
||||||
}
|
|
||||||
|
@ -1,58 +1,43 @@
|
|||||||
import { Buffer } from "node:buffer"
|
import { Buffer } from "node:buffer"
|
||||||
import * as cheerio from "cheerio"
|
import * as cheerio from "cheerio"
|
||||||
import iconv from "iconv-lite"
|
import iconv from "iconv-lite"
|
||||||
import type { NewsItem, OResponse, SourceInfo } from "@shared/types"
|
import type { NewsItem } from "@shared/types"
|
||||||
|
import { $fetch } from "ofetch"
|
||||||
import { tranformToUTC } from "#/utils/date"
|
import { tranformToUTC } from "#/utils/date"
|
||||||
|
import { defineSource } from "#/utils"
|
||||||
|
|
||||||
const columns = [
|
export default defineSource(async () => {
|
||||||
"人物记事",
|
|
||||||
"观点评论",
|
|
||||||
"中国聚焦",
|
|
||||||
"香港澳门",
|
|
||||||
"台湾新闻",
|
|
||||||
"国际时政",
|
|
||||||
"国际军事",
|
|
||||||
"国际视野",
|
|
||||||
] as const
|
|
||||||
export async function zaobao(type: typeof columns[number] = "中国聚焦"): Promise<SourceInfo> {
|
|
||||||
const response: ArrayBuffer = await $fetch("https://www.kzaobao.com/top.html", {
|
const response: ArrayBuffer = await $fetch("https://www.kzaobao.com/top.html", {
|
||||||
responseType: "arrayBuffer",
|
responseType: "arrayBuffer",
|
||||||
})
|
})
|
||||||
const base = "https://www.kzaobao.com"
|
const base = "https://www.kzaobao.com"
|
||||||
const utf8String = iconv.decode(Buffer.from(response), "gb2312")
|
const utf8String = iconv.decode(Buffer.from(response), "gb2312")
|
||||||
const $ = cheerio.load(utf8String)
|
const $ = cheerio.load(utf8String)
|
||||||
// const all = []
|
const $main = $("div[id^='cd0'] tr")
|
||||||
// columns.forEach((column, index) => {
|
|
||||||
const $main = $(`#cd0${columns.indexOf(type) + 1}`)
|
|
||||||
const news: NewsItem[] = []
|
const news: NewsItem[] = []
|
||||||
$main.find("tr").each((_, el) => {
|
$main.each((_, el) => {
|
||||||
const a = $(el).find("h3>a")
|
const a = $(el).find("h3>a")
|
||||||
// https://www.kzaobao.com/shiju/20241002/170659.html
|
// https://www.kzaobao.com/shiju/20241002/170659.html
|
||||||
const url = a.attr("href")
|
const url = a.attr("href")
|
||||||
const title = a.text()
|
const title = a.text()
|
||||||
if (url && title) {
|
const date = $(el).find("td:nth-child(3)").text()
|
||||||
const date = $(el).find("td:nth-child(3)").text()
|
if (url && title && date) {
|
||||||
news.push({
|
news.push({
|
||||||
url: base + url,
|
url: base + url,
|
||||||
title,
|
title,
|
||||||
id: url,
|
id: url,
|
||||||
extra: {
|
extra: {
|
||||||
date: date && tranformToUTC(date),
|
origin: date,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// all.push({
|
return news.sort((m, n) => n.extra!.origin > m.extra!.origin ? 1 : -1)
|
||||||
// type: column,
|
.slice(0, 20)
|
||||||
// items: news,
|
.map(item => ({
|
||||||
// })
|
...item,
|
||||||
// })
|
extra: {
|
||||||
// console.log(all)
|
date: tranformToUTC(item.extra!.origin),
|
||||||
return {
|
},
|
||||||
name: `联合早报`,
|
}))
|
||||||
type,
|
})
|
||||||
updateTime: Date.now(),
|
|
||||||
// items: all[0].items,
|
|
||||||
items: news,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1 +1,26 @@
|
|||||||
import type { SourceInfo } from "@shared/types"
|
import type { NewsItem, SourceInfo } from "@shared/types"
|
||||||
|
|
||||||
|
export function defineSource(source: () => Promise<NewsItem[]>): () => Promise<SourceInfo> {
|
||||||
|
return async () => ({
|
||||||
|
updatedTime: Date.now(),
|
||||||
|
items: await source(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defineRSSSource(url: string): () => Promise<SourceInfo> {
|
||||||
|
return async () => {
|
||||||
|
const source = await rss2json(url)
|
||||||
|
if (!source?.items.length) throw new Error("Cannot fetch data")
|
||||||
|
return {
|
||||||
|
updatedTime: source.updatedTime ?? Date.now(),
|
||||||
|
items: source.items.slice(0, 20).map(item => ({
|
||||||
|
title: item.title,
|
||||||
|
url: item.link,
|
||||||
|
id: item.link,
|
||||||
|
extra: {
|
||||||
|
date: item.created,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
|
import type { RSSInfo } from "@shared/types"
|
||||||
import { XMLParser } from "fast-xml-parser"
|
import { XMLParser } from "fast-xml-parser"
|
||||||
import { $fetch } from "ofetch"
|
import { $fetch } from "ofetch"
|
||||||
|
|
||||||
export async function rss2json(url: string) {
|
export async function rss2json(url: string): Promise<RSSInfo | undefined> {
|
||||||
if (!/^https?:\/\/[^\s$.?#].\S*/i.test(url)) return null
|
if (!/^https?:\/\/[^\s$.?#].\S*/i.test(url)) return
|
||||||
|
|
||||||
const data = await $fetch(url)
|
const data = await $fetch(url)
|
||||||
|
|
||||||
@ -23,6 +24,7 @@ export async function rss2json(url: string) {
|
|||||||
link: channel.link && channel.link.href ? channel.link.href : channel.link,
|
link: channel.link && channel.link.href ? channel.link.href : channel.link,
|
||||||
image: channel.image ? channel.image.url : channel["itunes:image"] ? channel["itunes:image"].href : "",
|
image: channel.image ? channel.image.url : channel["itunes:image"] ? channel["itunes:image"].href : "",
|
||||||
category: channel.category || [],
|
category: channel.category || [],
|
||||||
|
updatedTime: channel.lastBuildDate ?? channel.updated,
|
||||||
items: [],
|
items: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,8 +41,7 @@ export async function rss2json(url: string) {
|
|||||||
description: val.summary && val.summary.$text ? val.summary.$text : val.description,
|
description: val.summary && val.summary.$text ? val.summary.$text : val.description,
|
||||||
link: val.link && val.link.href ? val.link.href : val.link,
|
link: val.link && val.link.href ? val.link.href : val.link,
|
||||||
author: val.author && val.author.name ? val.author.name : val["dc:creator"],
|
author: val.author && val.author.name ? val.author.name : val["dc:creator"],
|
||||||
published: val.created ? Date.parse(val.created) : val.pubDate ? Date.parse(val.pubDate) : Date.now(),
|
created: val.updated ?? val.pubDate ?? val.created,
|
||||||
created: val.updated ? Date.parse(val.updated) : val.pubDate ? Date.parse(val.pubDate) : val.created ? Date.parse(val.created) : Date.now(),
|
|
||||||
category: val.category || [],
|
category: val.category || [],
|
||||||
content: val.content && val.content.$text ? val.content.$text : val["content:encoded"],
|
content: val.content && val.content.$text ? val.content.$text : val["content:encoded"],
|
||||||
enclosures: val.enclosure ? (Array.isArray(val.enclosure) ? val.enclosure : [val.enclosure]) : [],
|
enclosures: val.enclosure ? (Array.isArray(val.enclosure) ? val.enclosure : [val.enclosure]) : [],
|
||||||
|
@ -1 +1,5 @@
|
|||||||
export const TTL = 15 * 60 * 1000
|
export const TTL = 15 * 60 * 1000
|
||||||
|
/**
|
||||||
|
* 默认刷新间隔,否则复用缓存
|
||||||
|
*/
|
||||||
|
export const Interval = 30 * 60 * 1000
|
||||||
|
@ -5,10 +5,19 @@ export const sectionIds = ["focus", "social", "china", "world", "digital"] as co
|
|||||||
export const sources = {
|
export const sources = {
|
||||||
"36kr": {
|
"36kr": {
|
||||||
name: "36氪",
|
name: "36氪",
|
||||||
|
type: "人气榜",
|
||||||
|
interval: 10,
|
||||||
|
home: "https://36kr.com",
|
||||||
|
},
|
||||||
|
"36kr-quick": {
|
||||||
|
name: "36氪",
|
||||||
|
type: "快讯",
|
||||||
|
interval: 3,
|
||||||
home: "https://36kr.com",
|
home: "https://36kr.com",
|
||||||
},
|
},
|
||||||
"douyin": {
|
"douyin": {
|
||||||
name: "抖音",
|
name: "抖音",
|
||||||
|
interval: 1,
|
||||||
home: "https://www.douyin.com",
|
home: "https://www.douyin.com",
|
||||||
},
|
},
|
||||||
"hupu": {
|
"hupu": {
|
||||||
@ -17,18 +26,24 @@ export const sources = {
|
|||||||
},
|
},
|
||||||
"zhihu": {
|
"zhihu": {
|
||||||
name: "知乎",
|
name: "知乎",
|
||||||
|
interval: 10,
|
||||||
home: "https://www.zhihu.com",
|
home: "https://www.zhihu.com",
|
||||||
},
|
},
|
||||||
"weibo": {
|
"weibo": {
|
||||||
name: "微博",
|
name: "微博",
|
||||||
|
type: "实时热搜",
|
||||||
|
interval: 1,
|
||||||
home: "https://weibo.com",
|
home: "https://weibo.com",
|
||||||
},
|
},
|
||||||
"tieba": {
|
"tieba": {
|
||||||
name: "百度贴吧",
|
name: "百度贴吧",
|
||||||
|
interval: 2,
|
||||||
home: "https://tieba.baidu.com",
|
home: "https://tieba.baidu.com",
|
||||||
},
|
},
|
||||||
"zaobao": {
|
"zaobao": {
|
||||||
name: "联合早报",
|
name: "联合早报",
|
||||||
|
type: "实时新闻",
|
||||||
|
interval: 10,
|
||||||
home: "https://www.zaobao.com",
|
home: "https://www.zaobao.com",
|
||||||
},
|
},
|
||||||
"thepaper": {
|
"thepaper": {
|
||||||
@ -37,6 +52,7 @@ export const sources = {
|
|||||||
},
|
},
|
||||||
"toutiao": {
|
"toutiao": {
|
||||||
name: "今日头条",
|
name: "今日头条",
|
||||||
|
interval: 2,
|
||||||
home: "https://www.toutiao.com",
|
home: "https://www.toutiao.com",
|
||||||
},
|
},
|
||||||
"cankaoxiaoxi": {
|
"cankaoxiaoxi": {
|
||||||
@ -49,6 +65,15 @@ export const sources = {
|
|||||||
},
|
},
|
||||||
} as const satisfies Record<string, {
|
} as const satisfies Record<string, {
|
||||||
name: string
|
name: string
|
||||||
|
type?: string
|
||||||
|
/**
|
||||||
|
* 分钟,刷新的间隔时间,复用缓存
|
||||||
|
*/
|
||||||
|
interval?: number
|
||||||
|
/**
|
||||||
|
* 每天刷新一次
|
||||||
|
*/
|
||||||
|
once?: number
|
||||||
home: string
|
home: string
|
||||||
}>
|
}>
|
||||||
|
|
||||||
@ -63,7 +88,7 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
china: {
|
china: {
|
||||||
name: "国内",
|
name: "国内",
|
||||||
sourceList: ["peopledaily", "36kr", "toutiao"],
|
sourceList: ["peopledaily", "36kr", "toutiao", "36kr-quick"],
|
||||||
},
|
},
|
||||||
world: {
|
world: {
|
||||||
name: "国外",
|
name: "国外",
|
||||||
|
@ -19,9 +19,7 @@ export interface NewsItem {
|
|||||||
|
|
||||||
// 路由数据
|
// 路由数据
|
||||||
export interface SourceInfo {
|
export interface SourceInfo {
|
||||||
name: string
|
updatedTime: number | string
|
||||||
type: string
|
|
||||||
updateTime: number | string
|
|
||||||
items: NewsItem[]
|
items: NewsItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,13 +31,19 @@ export type OResponse = {
|
|||||||
message?: string
|
message?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RSS2JSON {
|
export interface RSSInfo {
|
||||||
id?: string
|
|
||||||
title: string
|
title: string
|
||||||
description: string
|
description: string
|
||||||
link: string
|
link: string
|
||||||
published: number
|
image: string
|
||||||
created: number
|
updatedTime: string
|
||||||
|
items: RSSItem[]
|
||||||
|
}
|
||||||
|
export interface RSSItem {
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
link: string
|
||||||
|
created?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CacheInfo {
|
export interface CacheInfo {
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { OResponse, SourceID, SourceInfo } from "@shared/types"
|
import type { NewsItem, SourceID, SourceInfo } from "@shared/types"
|
||||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
import { OverlayScrollbarsComponent } from "overlayscrollbars-react"
|
||||||
import type { UseQueryResult } from "@tanstack/react-query"
|
import type { UseQueryResult } from "@tanstack/react-query"
|
||||||
import { useQuery } from "@tanstack/react-query"
|
import { useQuery } from "@tanstack/react-query"
|
||||||
@ -6,7 +6,7 @@ import { relativeTime } from "@shared/utils"
|
|||||||
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, useEffect, useImperativeHandle, useRef } from "react"
|
import { forwardRef, useCallback, useImperativeHandle, useRef } from "react"
|
||||||
import { sources } from "@shared/data"
|
import { sources } from "@shared/data"
|
||||||
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
import type { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||||
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
import { focusSourcesAtom, refetchSourceAtom } from "~/atoms"
|
||||||
@ -45,7 +45,7 @@ export const CardWrapper = forwardRef<HTMLDivElement, ItemsProps>(({ id, isDragg
|
|||||||
<div
|
<div
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"flex flex-col bg-base border rounded-md h-450px border-gray-500/40",
|
"flex flex-col h-500px aspect-auto border border-gray-100 rounded-xl shadow-2xl shadow-gray-600/10 bg-base dark:( border-gray-700 shadow-none)",
|
||||||
isDragged && "op-50",
|
isDragged && "op-50",
|
||||||
isOverlay ? "bg-glass" : "",
|
isOverlay ? "bg-glass" : "",
|
||||||
)}
|
)}
|
||||||
@ -106,12 +106,13 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
|||||||
])}
|
])}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<img src={`/icons/${id}.png`} className="w-4 h-4 rounded" alt={id} onError={e => e.currentTarget.hidden = true} />
|
<img src={`/icons/${id.split("-")[0]}.png`} className="w-4 h-4 rounded" alt={id} onError={e => e.currentTarget.hidden = true} />
|
||||||
<span className="text-md font-bold">
|
<span className="text-md font-bold">
|
||||||
{sources[id].name}
|
{sources[id].name}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<SubTitle query={query} />
|
{/* @ts-expect-error -_- */}
|
||||||
|
<span className="text-xs">{sources[id]?.type}</span>
|
||||||
</div>
|
</div>
|
||||||
<OverlayScrollbarsComponent
|
<OverlayScrollbarsComponent
|
||||||
defer
|
defer
|
||||||
@ -136,13 +137,8 @@ export function NewsCard({ id, inView, isOverlay, handleListeners }: NewsCardPro
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function SubTitle({ query }: Query) {
|
|
||||||
const subTitle = query.data?.type
|
|
||||||
if (subTitle) return <span className="text-xs">{subTitle}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
function UpdateTime({ query }: Query) {
|
function UpdateTime({ query }: Query) {
|
||||||
const updateTime = query.data?.updateTime
|
const updateTime = query.data?.updatedTime
|
||||||
if (updateTime) return <span>{`${relativeTime(updateTime)}更新`}</span>
|
if (updateTime) return <span>{`${relativeTime(updateTime)}更新`}</span>
|
||||||
if (query.isError) return <span>获取失败</span>
|
if (query.isError) return <span>获取失败</span>
|
||||||
return <span className="skeleton w-20" />
|
return <span className="skeleton w-20" />
|
||||||
@ -157,6 +153,24 @@ function Num({ num }: { num: number }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ExtraInfo({ item }: { item: NewsItem }) {
|
||||||
|
if (item?.extra?.date) {
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-gray-4/80 self-center">
|
||||||
|
{relativeTime(item.extra.date)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.extra?.icon) {
|
||||||
|
return (
|
||||||
|
<span className="text-xs text-gray-4/80 self-start">
|
||||||
|
<img src={item.extra.icon} className="w-2em" />
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function NewsList({ query }: Query) {
|
function NewsList({ query }: Query) {
|
||||||
const items = query.data?.items
|
const items = query.data?.items
|
||||||
if (items?.length) {
|
if (items?.length) {
|
||||||
@ -165,15 +179,11 @@ function NewsList({ query }: Query) {
|
|||||||
{items.slice(0, 20).map((item, i) => (
|
{items.slice(0, 20).map((item, i) => (
|
||||||
<div key={item.title} className="flex gap-2 items-center">
|
<div key={item.title} className="flex gap-2 items-center">
|
||||||
<Num num={i + 1} />
|
<Num num={i + 1} />
|
||||||
<a href={item.url} target="_blank" className="my-1 w-full flex items-center justify-between flex-wrap">
|
<a href={item.url} target="_blank" className="my-1 w-full flex justify-between flex-wrap">
|
||||||
<span className="flex-1 mr-2 hover:(underline underline-offset-4)">
|
<span className="flex-1 mr-2">
|
||||||
{item.title}
|
{item.title}
|
||||||
</span>
|
</span>
|
||||||
{item?.extra?.date && (
|
<ExtraInfo item={item} />
|
||||||
<span className="text-xs text-gray-4/80">
|
|
||||||
{relativeTime(item.extra.date)}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
@ -28,8 +28,12 @@ export function RootComponent() {
|
|||||||
>
|
>
|
||||||
<Header />
|
<Header />
|
||||||
<Outlet />
|
<Outlet />
|
||||||
<ReactQueryDevtools buttonPosition="bottom-left" />
|
{ import.meta.env.DEV && (
|
||||||
<TanStackRouterDevtools position="bottom-right" />
|
<>
|
||||||
|
<ReactQueryDevtools buttonPosition="bottom-left" />
|
||||||
|
<TanStackRouterDevtools position="bottom-right" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</OverlayScrollbarsComponent>
|
</OverlayScrollbarsComponent>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user