mirror of
https://github.com/imsyy/DailyHotApi.git
synced 2026-01-12 13:14:55 +08:00
🦄 refactor: Refactoring using hono
This commit is contained in:
54
src/app.tsx
Normal file
54
src/app.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Hono } from "hono";
|
||||
import { cors } from "hono/cors";
|
||||
import { config } from "./config.js";
|
||||
import { serveStatic } from "@hono/node-server/serve-static";
|
||||
import { compress } from "hono/compress";
|
||||
import logger from "./utils/logger.js";
|
||||
import registry from "./registry.js";
|
||||
import robotstxt from "./robots.txt.js";
|
||||
import NotFound from "./views/NotFound.js";
|
||||
import Home from "./views/Home.js";
|
||||
import Error from "./views/Error.js";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// 压缩响应
|
||||
app.use(compress());
|
||||
|
||||
// CORS
|
||||
app.use(
|
||||
"*",
|
||||
cors({
|
||||
// 可写为数组
|
||||
origin: config.ALLOWED_DOMAIN,
|
||||
allowMethods: ["POST", "GET", "OPTIONS"],
|
||||
allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"],
|
||||
credentials: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// 静态资源
|
||||
app.use(
|
||||
"/*",
|
||||
serveStatic({
|
||||
root: "./public",
|
||||
rewriteRequestPath: (path) => (path === "/favicon.ico" ? "/favicon.png" : path),
|
||||
}),
|
||||
);
|
||||
|
||||
// 主路由
|
||||
app.route("/", registry);
|
||||
|
||||
// robots
|
||||
app.get("/robots.txt", robotstxt);
|
||||
// 首页
|
||||
app.get("/", (c) => c.html(<Home />));
|
||||
// 404
|
||||
app.notFound((c) => c.html(<NotFound />, 404));
|
||||
// error
|
||||
app.onError((err, c) => {
|
||||
logger.error(`出现致命错误:${err}`);
|
||||
return c.html(<Error error={err?.message} />, 500);
|
||||
});
|
||||
|
||||
export default app;
|
||||
50
src/config.ts
Normal file
50
src/config.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import dotenv from "dotenv";
|
||||
|
||||
// 环境变量
|
||||
dotenv.config();
|
||||
|
||||
export type Config = {
|
||||
PORT: number;
|
||||
DISALLOW_ROBOT: boolean;
|
||||
CACHE_TTL: number;
|
||||
REQUEST_TIMEOUT: number;
|
||||
ALLOWED_DOMAIN: string;
|
||||
USE_LOG_FILE: boolean;
|
||||
RSS_MODE: boolean;
|
||||
};
|
||||
|
||||
// 验证并提取环境变量
|
||||
const getEnvVariable = (key: string): string | undefined => {
|
||||
const value = process.env[key];
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
// 将环境变量转换为数值
|
||||
const getNumericEnvVariable = (key: string, defaultValue: number): number => {
|
||||
const value = getEnvVariable(key) ?? String(defaultValue);
|
||||
const parsedValue = parseInt(value, 10);
|
||||
if (isNaN(parsedValue)) {
|
||||
return defaultValue
|
||||
}
|
||||
return parsedValue;
|
||||
};
|
||||
|
||||
// 将环境变量转换为布尔值
|
||||
const getBooleanEnvVariable = (key: string, defaultValue: boolean): boolean => {
|
||||
const value = getEnvVariable(key) ?? String(defaultValue);
|
||||
return value.toLowerCase() === "true";
|
||||
};
|
||||
|
||||
// 创建配置对象
|
||||
export const config: Config = {
|
||||
PORT: getNumericEnvVariable("PORT", 6688),
|
||||
DISALLOW_ROBOT: getBooleanEnvVariable("DISALLOW_ROBOT", true),
|
||||
CACHE_TTL: getNumericEnvVariable("CACHE_TTL", 3600),
|
||||
REQUEST_TIMEOUT: getNumericEnvVariable("CACHE_TTL", 6000),
|
||||
ALLOWED_DOMAIN: getEnvVariable("ALLOWED_DOMAIN") || "*",
|
||||
USE_LOG_FILE: getBooleanEnvVariable("USE_LOG_FILE", true),
|
||||
RSS_MODE: getBooleanEnvVariable("RSS_MODE", false),
|
||||
};
|
||||
15
src/index.ts
Normal file
15
src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { serve } from "@hono/node-server";
|
||||
import { config } from "./config.js";
|
||||
import logger from "./utils/logger.js";
|
||||
import app from "./app.js";
|
||||
|
||||
logger.info(`🔥 DailyHot API 成功在端口 ${config.PORT} 上运行`);
|
||||
logger.info(`🔗 Local: 👉 http://localhost:${config.PORT}`);
|
||||
|
||||
// 启动服务器
|
||||
const server = serve({
|
||||
fetch: app.fetch,
|
||||
port: config.PORT,
|
||||
});
|
||||
|
||||
export default server;
|
||||
105
src/registry.ts
Normal file
105
src/registry.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { fileURLToPath } from "url";
|
||||
import { config } from "./config.js";
|
||||
import { Hono } from "hono";
|
||||
import getRSS from "./utils/getRSS.js";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
|
||||
const app = new Hono();
|
||||
|
||||
// 模拟 __dirname
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
// 路由数据
|
||||
let allRoutePath: Array<string> = [];
|
||||
const routersDirName: string = "routes";
|
||||
|
||||
// 建立完整目录路径
|
||||
const routersDirPath = path.join(__dirname, routersDirName);
|
||||
|
||||
// 递归查找函数
|
||||
const findTsFiles = (dirPath: string, allFiles: string[] = [], basePath: string = ""): string[] => {
|
||||
// 读取目录下的所有文件和文件夹
|
||||
const items: Array<string> = fs.readdirSync(dirPath);
|
||||
// 遍历每个文件或文件夹
|
||||
items.forEach((item) => {
|
||||
const fullPath: string = path.join(dirPath, item);
|
||||
const relativePath: string = basePath ? path.posix.join(basePath, item) : item;
|
||||
const stat: fs.Stats = fs.statSync(fullPath);
|
||||
if (stat.isDirectory()) {
|
||||
// 如果是文件夹,递归查找
|
||||
findTsFiles(fullPath, allFiles, relativePath);
|
||||
} else if (stat.isFile() && (item.endsWith(".ts") || item.endsWith(".js"))) {
|
||||
// 符合条件
|
||||
allFiles.push(relativePath.replace(/\.(ts|js)$/, ""));
|
||||
}
|
||||
});
|
||||
return allFiles;
|
||||
};
|
||||
|
||||
// 获取全部路由
|
||||
if (fs.existsSync(routersDirPath) && fs.statSync(routersDirPath).isDirectory()) {
|
||||
allRoutePath = findTsFiles(routersDirPath);
|
||||
} else {
|
||||
console.error(`目录 ${routersDirPath} 不存在或不是目录`);
|
||||
}
|
||||
|
||||
// 注册全部路由
|
||||
for (let index = 0; index < allRoutePath.length; index++) {
|
||||
const router = allRoutePath[index];
|
||||
const listApp = app.basePath(`/${router}`);
|
||||
// 返回榜单
|
||||
listApp.get("/", async (c) => {
|
||||
// 是否采用缓存
|
||||
const noCache = c.req.query("cache") === "false";
|
||||
// 限制显示条目
|
||||
const limit = c.req.query("limit");
|
||||
// 是否输出 RSS
|
||||
const rssEnabled = c.req.query("rss") === "true";
|
||||
// 获取路由路径
|
||||
const { handleRoute } = await import(`./routes/${router}.js`);
|
||||
const listData = await handleRoute(c, noCache);
|
||||
// 是否限制条目
|
||||
if (limit && listData?.data?.length > parseInt(limit)) {
|
||||
listData.total = parseInt(limit);
|
||||
listData.data = listData.data.slice(0, parseInt(limit));
|
||||
}
|
||||
// 是否输出 RSS
|
||||
if (rssEnabled || config.RSS_MODE) {
|
||||
const rss = getRSS(listData);
|
||||
if (typeof rss === "string") {
|
||||
c.header("Content-Type", "application/xml; charset=utf-8");
|
||||
return c.body(rss);
|
||||
} else {
|
||||
return c.json(
|
||||
{
|
||||
code: 500,
|
||||
message: "RSS 生成失败",
|
||||
},
|
||||
500,
|
||||
);
|
||||
}
|
||||
}
|
||||
return c.json({
|
||||
code: 200,
|
||||
...listData,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 获取全部路由
|
||||
app.get("/all", (c) =>
|
||||
c.json(
|
||||
{
|
||||
code: 200,
|
||||
count: allRoutePath.length,
|
||||
routes: allRoutePath.map((path) => ({
|
||||
name: path,
|
||||
path: `/${path}`,
|
||||
})),
|
||||
},
|
||||
200,
|
||||
),
|
||||
);
|
||||
|
||||
export default app;
|
||||
13
src/robots.txt.ts
Normal file
13
src/robots.txt.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { Handler } from "hono";
|
||||
import { config } from "./config.js";
|
||||
|
||||
const handler: Handler = (c) => {
|
||||
if (config.DISALLOW_ROBOT) {
|
||||
return c.text("User-agent: *\nDisallow: /");
|
||||
} else {
|
||||
c.status(404);
|
||||
return c.text("");
|
||||
}
|
||||
};
|
||||
|
||||
export default handler;
|
||||
188
src/router.types.ts
Normal file
188
src/router.types.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
export type RouterType = {
|
||||
"36kr": {
|
||||
itemId: number;
|
||||
templateMaterial: {
|
||||
widgetTitle: string;
|
||||
authorName: string;
|
||||
statCollect: number;
|
||||
widgetImage: string;
|
||||
};
|
||||
};
|
||||
"qq-news": {
|
||||
id: string;
|
||||
title: string;
|
||||
abstract: string;
|
||||
source: string;
|
||||
hotEvent: {
|
||||
hotScore: number;
|
||||
};
|
||||
miniProShareImage: string;
|
||||
};
|
||||
"netease-news": {
|
||||
title: string;
|
||||
imgsrc: string;
|
||||
source: string;
|
||||
docid: string;
|
||||
};
|
||||
"zhihu-daily": {
|
||||
id: number;
|
||||
images: [string];
|
||||
title: string;
|
||||
hint: string;
|
||||
url: string;
|
||||
type: number;
|
||||
};
|
||||
bilibili: {
|
||||
bvid: string;
|
||||
title: string;
|
||||
desc: string;
|
||||
pic: string;
|
||||
owner: {
|
||||
name: string;
|
||||
};
|
||||
stat: {
|
||||
view: number;
|
||||
};
|
||||
short_link_v2?: string;
|
||||
};
|
||||
juejin: {
|
||||
content: {
|
||||
content_id: string;
|
||||
title: string;
|
||||
name: string;
|
||||
};
|
||||
author: {
|
||||
name: string;
|
||||
};
|
||||
content_counter: {
|
||||
hot_rank: string;
|
||||
};
|
||||
};
|
||||
weibo: {
|
||||
mid: string;
|
||||
word: string;
|
||||
word_scheme: string;
|
||||
note: string;
|
||||
category: string;
|
||||
raw_hot: number;
|
||||
};
|
||||
zhihu: {
|
||||
target: {
|
||||
id: number;
|
||||
title: string;
|
||||
excerpt: string;
|
||||
};
|
||||
children: [
|
||||
{
|
||||
thumbnail: string;
|
||||
},
|
||||
];
|
||||
detail_text: string;
|
||||
};
|
||||
douyin: {
|
||||
sentence_id: string;
|
||||
word: string;
|
||||
hot_value: number;
|
||||
};
|
||||
baidu: {
|
||||
index: number;
|
||||
word: string;
|
||||
desc: string;
|
||||
img: string;
|
||||
hotScore: string;
|
||||
show: string;
|
||||
rawUrl: string;
|
||||
query: string;
|
||||
};
|
||||
miyoushe: {
|
||||
post: {
|
||||
post_id: string;
|
||||
subject: string;
|
||||
content: string;
|
||||
cover: string;
|
||||
};
|
||||
stat: {
|
||||
view_num: number;
|
||||
};
|
||||
user: {
|
||||
nickname: string;
|
||||
};
|
||||
image_list: [
|
||||
{
|
||||
url: string;
|
||||
},
|
||||
];
|
||||
};
|
||||
weread: {
|
||||
readingCount: number;
|
||||
bookInfo: {
|
||||
bookId: string;
|
||||
title: string;
|
||||
intro: string;
|
||||
cover: string;
|
||||
author: string;
|
||||
};
|
||||
};
|
||||
toutiao: {
|
||||
ClusterIdStr: string;
|
||||
Title: string;
|
||||
HotValue: string;
|
||||
Image: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
thepaper: {
|
||||
contId: string;
|
||||
name: string;
|
||||
pic: string;
|
||||
praiseTimes: string;
|
||||
};
|
||||
sspai: {
|
||||
id: number;
|
||||
title: string;
|
||||
summary: string;
|
||||
banner: string;
|
||||
like_count: number;
|
||||
author: {
|
||||
nickname: string;
|
||||
};
|
||||
};
|
||||
lol: {
|
||||
sAuthor: string;
|
||||
sIMG: string;
|
||||
sTitle: string;
|
||||
iTotalPlay: string;
|
||||
iDocID: string;
|
||||
};
|
||||
ngabbs: {
|
||||
tid: number;
|
||||
subject: string;
|
||||
author: string;
|
||||
tpcurl: string;
|
||||
replies: number;
|
||||
};
|
||||
tieba: {
|
||||
topic_id: number;
|
||||
topic_name: string;
|
||||
topic_desc: string;
|
||||
topic_pic: string;
|
||||
topic_url: string;
|
||||
discuss_num: number;
|
||||
};
|
||||
acfun: {
|
||||
dougaId: string;
|
||||
contentTitle: string;
|
||||
userName: string;
|
||||
contentDesc: string;
|
||||
likeCount: number;
|
||||
coverUrl: string;
|
||||
};
|
||||
hellogithub: {
|
||||
item_id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
summary: string;
|
||||
clicks_total: number;
|
||||
};
|
||||
};
|
||||
74
src/routes/36kr.ts
Normal file
74
src/routes/36kr.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { post } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "hot";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "36kr",
|
||||
title: "36氪",
|
||||
type: "热榜",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "热榜分类",
|
||||
type: {
|
||||
hot: "人气榜",
|
||||
video: "视频榜",
|
||||
comment: "热议榜",
|
||||
collect: "收藏榜",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://m.36kr.com/hot-list-m",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://gateway.36kr.com/api/mis/nav/home/nav/rank/${type}`;
|
||||
const result = await post({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
body: {
|
||||
partner_id: "wap",
|
||||
param: {
|
||||
siteId: 1,
|
||||
platformId: 2,
|
||||
},
|
||||
timestamp: new Date().getTime(),
|
||||
},
|
||||
});
|
||||
const listType = {
|
||||
hot: "hotRankList",
|
||||
video: "videoList",
|
||||
comment: "remarkList",
|
||||
collect: "collectList",
|
||||
};
|
||||
const list =
|
||||
result.data.data[(listType as Record<string, keyof typeof result.data.data>)[type || "hot"]];
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["36kr"]) => {
|
||||
const item = v.templateMaterial;
|
||||
return {
|
||||
id: v.itemId,
|
||||
title: item.widgetTitle,
|
||||
cover: item.widgetImage,
|
||||
author: item.authorName,
|
||||
hot: item.statCollect,
|
||||
url: `https://www.36kr.com/p/${v.itemId}`,
|
||||
mobileUrl: `https://m.36kr.com/p/${v.itemId}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
75
src/routes/acfun.ts
Normal file
75
src/routes/acfun.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "-1";
|
||||
const range = c.req.query("range") || "DAY";
|
||||
const { fromCache, data, updateTime } = await getList({ type, range }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "acfun",
|
||||
title: "AcFun",
|
||||
type: "排行榜",
|
||||
description: "AcFun是一家弹幕视频网站,致力于为每一个人带来欢乐。",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "频道",
|
||||
type: {
|
||||
"-1": "全站综合",
|
||||
"155": "番剧",
|
||||
"1": "动画",
|
||||
"60": "娱乐",
|
||||
"201": "生活",
|
||||
"58": "音乐",
|
||||
"123": "舞蹈·偶像",
|
||||
"59": "游戏",
|
||||
"70": "科技",
|
||||
"68": "影视",
|
||||
"69": "体育",
|
||||
"125": "鱼塘",
|
||||
},
|
||||
},
|
||||
range: {
|
||||
name: "时间",
|
||||
type: {
|
||||
DAY: "今日",
|
||||
THREE_DAYS: "三日",
|
||||
WEEK: "本周",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://www.acfun.cn/rank/list/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type, range } = options;
|
||||
const url = `https://www.acfun.cn/rest/pc-direct/rank/channel?channelId=${type === "-1" ? "" : type}&rankLimit=30&rankPeriod=${range}`;
|
||||
const result = await get({
|
||||
url,
|
||||
headers: {
|
||||
Referer: `https://www.acfun.cn/rank/list/?cid=-1&pcid=${type}&range=${range}`,
|
||||
},
|
||||
noCache,
|
||||
});
|
||||
const list = result.data.rankList;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["acfun"]) => ({
|
||||
id: v.dougaId,
|
||||
title: v.contentTitle,
|
||||
desc: v.contentDesc,
|
||||
cover: v.coverUrl,
|
||||
author: v.userName,
|
||||
hot: v.likeCount,
|
||||
url: `https://www.acfun.cn/v/ac${v.dougaId}`,
|
||||
mobileUrl: `https://m.acfun.cn/v/?ac=${v.dougaId}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
63
src/routes/baidu.ts
Normal file
63
src/routes/baidu.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "realtime";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "baidu",
|
||||
title: "百度",
|
||||
type: "热搜榜",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "热搜类别",
|
||||
type: {
|
||||
realtime: "热搜",
|
||||
novel: "小说",
|
||||
movie: "电影",
|
||||
teleplay: "电视剧",
|
||||
car: "汽车",
|
||||
game: "游戏",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://top.baidu.com/board",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://top.baidu.com/board?tab=${type}`;
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 14_2_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) FxiOS/1.0 Mobile/12F69 Safari/605.1.15",
|
||||
},
|
||||
});
|
||||
// 正则查找
|
||||
const pattern = /<!--s-data:(.*?)-->/s;
|
||||
const matchResult = result.data.match(pattern);
|
||||
const jsonObject = JSON.parse(matchResult[1]).cards[0].content;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: jsonObject.map((v: RouterType["baidu"]) => ({
|
||||
id: v.index,
|
||||
title: v.word,
|
||||
desc: v.desc,
|
||||
cover: v.img,
|
||||
author: v.show?.length ? v.show : "",
|
||||
hot: Number(v.hotScore),
|
||||
url: `https://www.baidu.com/s?wd=${encodeURIComponent(v.query)}`,
|
||||
mobileUrl: v.rawUrl,
|
||||
})),
|
||||
};
|
||||
};
|
||||
67
src/routes/bilibili.ts
Normal file
67
src/routes/bilibili.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "0";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "bilibili",
|
||||
title: "哔哩哔哩",
|
||||
type: "热门榜",
|
||||
description: "你所热爱的,就是你的生活",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "排行榜分区",
|
||||
type: {
|
||||
0: "全站",
|
||||
1: "动画",
|
||||
3: "音乐",
|
||||
4: "游戏",
|
||||
5: "娱乐",
|
||||
36: "科技",
|
||||
119: "鬼畜",
|
||||
129: "舞蹈",
|
||||
155: "时尚",
|
||||
160: "生活",
|
||||
168: "国创相关",
|
||||
188: "数码",
|
||||
181: "影视",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://www.bilibili.com/v/popular/rank/all",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://api.bilibili.com/x/web-interface/ranking/v2?rid=${type}`;
|
||||
const result = await get({
|
||||
url,
|
||||
headers: {
|
||||
Referer: `https://www.bilibili.com/ranking/all`,
|
||||
},
|
||||
noCache,
|
||||
});
|
||||
const list = result.data.data.list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["bilibili"]) => ({
|
||||
id: v.bvid,
|
||||
title: v.title,
|
||||
desc: v.desc,
|
||||
cover: v.pic.replace(/http:/, "https:"),
|
||||
author: v.owner.name,
|
||||
hot: v.stat.view,
|
||||
url: v.short_link_v2 || `https://www.bilibili.com/video/${v.bvid}`,
|
||||
mobileUrl: `https://m.bilibili.com/video/${v.bvid}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
54
src/routes/douban-group.ts
Normal file
54
src/routes/douban-group.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import { load } from "cheerio";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "douban-group",
|
||||
title: "豆瓣讨论",
|
||||
type: "讨论精选",
|
||||
link: "https://www.douban.com/group/explore",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
// 数据处理
|
||||
const getNumbers = (text: string | undefined) => {
|
||||
if (!text) return 100000000;
|
||||
const regex = /\d+/;
|
||||
const match = text.match(regex);
|
||||
if (match) {
|
||||
return Number(match[0]);
|
||||
} else {
|
||||
return 100000000;
|
||||
}
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://www.douban.com/group/explore`;
|
||||
const result = await get({ url, noCache });
|
||||
const $ = load(result.data);
|
||||
const listDom = $(".article .channel-item");
|
||||
const listData = listDom.toArray().map((item) => {
|
||||
const dom = $(item);
|
||||
const url = dom.find("h3 a").attr("href") || undefined;
|
||||
return {
|
||||
id: getNumbers(url),
|
||||
title: dom.find("h3 a").text().trim(),
|
||||
cover: dom.find(".pic-wrap img").attr("src"),
|
||||
desc: dom.find(".block p").text().trim(),
|
||||
url,
|
||||
mobileUrl: `https://m.douban.com/group/topic/${getNumbers(url)}/`,
|
||||
};
|
||||
});
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: listData,
|
||||
};
|
||||
};
|
||||
63
src/routes/douban-movie.ts
Normal file
63
src/routes/douban-movie.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import { load } from "cheerio";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "douban-movie",
|
||||
title: "豆瓣电影",
|
||||
type: "新片排行榜",
|
||||
link: "https://movie.douban.com/chart",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
// 数据处理
|
||||
const getNumbers = (text: string | undefined) => {
|
||||
if (!text) return 10000000;
|
||||
const regex = /\d+/;
|
||||
const match = text.match(regex);
|
||||
if (match) {
|
||||
return Number(match[0]);
|
||||
} else {
|
||||
return 10000000;
|
||||
}
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://movie.douban.com/chart/`;
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1",
|
||||
},
|
||||
});
|
||||
const $ = load(result.data);
|
||||
const listDom = $(".article tr.item");
|
||||
const listData = listDom.toArray().map((item) => {
|
||||
const dom = $(item);
|
||||
const url = dom.find("a").attr("href") || undefined;
|
||||
const score = dom.find(".rating_nums").text() ?? "0.0";
|
||||
return {
|
||||
id: getNumbers(url),
|
||||
title: `【${score}】${dom.find("a").attr("title")}`,
|
||||
cover: dom.find("img").attr("src"),
|
||||
desc: dom.find("p.pl").text(),
|
||||
hot: getNumbers(dom.find("span.pl").text()),
|
||||
url,
|
||||
mobileUrl:`https://m.douban.com/movie/subject/${getNumbers(url)}/`,
|
||||
};
|
||||
});
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: listData,
|
||||
};
|
||||
};
|
||||
59
src/routes/douyin.ts
Normal file
59
src/routes/douyin.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "douyin",
|
||||
title: "抖音",
|
||||
type: "热榜",
|
||||
description: "实时上升热点",
|
||||
link: "https://www.douyin.com",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
// 获取抖音临时 Cookis
|
||||
const getDyCookies = async () => {
|
||||
try {
|
||||
const cookisUrl = "https://www.douyin.com/passport/general/login_guiding_strategy/?aid=6383";
|
||||
const { data } = await get({ url: cookisUrl, originaInfo: true });
|
||||
const pattern = /passport_csrf_token=(.*); Path/s;
|
||||
const matchResult = data.headers["set-cookie"][0].match(pattern);
|
||||
const cookieData = matchResult[1];
|
||||
return cookieData;
|
||||
} catch (error) {
|
||||
console.error("获取抖音 Cookie 出错" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url =
|
||||
"https://www.douyin.com/aweme/v1/web/hot/search/list/?device_platform=webapp&aid=6383&channel=channel_pc_web&detail_list=1";
|
||||
const cookie = await getDyCookies();
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
Cookie: `passport_csrf_token=${cookie}`,
|
||||
},
|
||||
});
|
||||
const list = result.data.data.word_list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["douyin"]) => ({
|
||||
id: v.sentence_id,
|
||||
title: v.word,
|
||||
hot: v.hot_value,
|
||||
url: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||
mobileUrl: `https://www.douyin.com/hot/${encodeURIComponent(v.sentence_id)}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
53
src/routes/genshin.ts
Normal file
53
src/routes/genshin.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "1";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "genshin",
|
||||
title: "原神",
|
||||
type: "最新动态",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "榜单分类",
|
||||
type: {
|
||||
1: "公告",
|
||||
2: "活动",
|
||||
3: "资讯",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://www.miyoushe.com/ys/home/28",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=2&page_size=20&type=${type}`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["miyoushe"]) => {
|
||||
const data = v.post;
|
||||
return {
|
||||
id: data.post_id,
|
||||
title: data.subject,
|
||||
desc: data.content,
|
||||
cover: data.cover,
|
||||
author: v.user.nickname,
|
||||
hot: v.stat.view_num,
|
||||
url: `https://www.miyoushe.com/ys/article/${data.post_id}`,
|
||||
mobileUrl: `https://m.miyoushe.com/ys/#/article/${data.post_id}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
49
src/routes/hellogithub.ts
Normal file
49
src/routes/hellogithub.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const sort = c.req.query("sort") || "hot";
|
||||
const { fromCache, data, updateTime } = await getList({ sort }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "hellogithub",
|
||||
title: "HelloGitHub",
|
||||
type: "热门仓库",
|
||||
description: "分享 GitHub 上有趣、入门级的开源项目",
|
||||
parameData: {
|
||||
sort: {
|
||||
name: "排行榜分区",
|
||||
type: {
|
||||
hot: "热门",
|
||||
last: "最新",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://hellogithub.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { sort } = options;
|
||||
const url = `https://abroad.hellogithub.com/v1/?sort_by=${sort}&tid=&page=1`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["hellogithub"]) => ({
|
||||
id: v.item_id,
|
||||
title: v.title,
|
||||
desc: v.summary,
|
||||
author: v.author,
|
||||
hot: v.clicks_total,
|
||||
url: `https://hellogithub.com/repository/${v.item_id}`,
|
||||
mobileUrl: `https://hellogithub.com/repository/${v.item_id}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
53
src/routes/honkai.ts
Normal file
53
src/routes/honkai.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "1";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "honkai",
|
||||
title: "崩坏3",
|
||||
type: "最新动态",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "榜单分类",
|
||||
type: {
|
||||
1: "公告",
|
||||
2: "活动",
|
||||
3: "资讯",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://www.miyoushe.com/bh3/home/6",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=1&page_size=20&type=${type}`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["miyoushe"]) => {
|
||||
const data = v.post;
|
||||
return {
|
||||
id: data.post_id,
|
||||
title: data.subject,
|
||||
desc: data.content,
|
||||
cover: data.cover || v.image_list[0].url,
|
||||
author: v.user.nickname,
|
||||
hot: v.stat.view_num,
|
||||
url: `https://www.miyoushe.com/bh3/article/${data.post_id}`,
|
||||
mobileUrl: `https://m.miyoushe.com/bh3/#/article/${data.post_id}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
56
src/routes/ithome.ts
Normal file
56
src/routes/ithome.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import { load } from "cheerio";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "ithome",
|
||||
title: "IT之家",
|
||||
type: "热榜",
|
||||
description: "爱科技,爱这里 - 前沿科技新闻网站",
|
||||
link: "https://m.ithome.com/rankm/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
// 链接处理
|
||||
const replaceLink = (url: string, getId: boolean = false) => {
|
||||
const match = url.match(/[html|live]\/(\d+)\.htm/);
|
||||
// 是否匹配成功
|
||||
if (match && match[1]) {
|
||||
return getId
|
||||
? match[1]
|
||||
: `https://www.ithome.com/0/${match[1].slice(0, 3)}/${match[1].slice(3)}.htm`;
|
||||
}
|
||||
// 返回原始 URL
|
||||
return url;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://m.ithome.com/rankm/`;
|
||||
const result = await get({ url, noCache });
|
||||
const $ = load(result.data);
|
||||
const listDom = $(".rank-box .placeholder");
|
||||
const listData = listDom.toArray().map((item) => {
|
||||
const dom = $(item);
|
||||
const href = dom.find("a").attr("href");
|
||||
return {
|
||||
id: href ? Number(replaceLink(href, true)) : 100000,
|
||||
title: dom.find(".plc-title").text().trim(),
|
||||
cover: dom.find("img").attr("data-original"),
|
||||
hot: Number(dom.find(".review-num").text().replace(/\D/g, "")),
|
||||
url: href ? replaceLink(href) : undefined,
|
||||
mobileUrl: href || undefined,
|
||||
};
|
||||
});
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: listData,
|
||||
};
|
||||
};
|
||||
57
src/routes/jianshu.ts
Normal file
57
src/routes/jianshu.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import { load } from "cheerio";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "jianshu",
|
||||
title: "简书",
|
||||
type: "热门推荐",
|
||||
description: "一个优质的创作社区",
|
||||
link: "https://www.jianshu.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
// 获取 ID
|
||||
const getID = (url: string) => {
|
||||
if (!url) return "undefined";
|
||||
const match = url.match(/([^/]+)$/);
|
||||
return match ? match[1] : "undefined";
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://www.jianshu.com/`;
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
Referer: "https://www.jianshu.com",
|
||||
},
|
||||
});
|
||||
const $ = load(result.data);
|
||||
const listDom = $("ul.note-list li");
|
||||
const listData = listDom.toArray().map((item) => {
|
||||
const dom = $(item);
|
||||
const href = dom.find("a").attr("href") || "";
|
||||
return {
|
||||
id: getID(href),
|
||||
title: dom.find("a.title").text()?.trim(),
|
||||
cover: dom.find("img").attr("src"),
|
||||
desc: dom.find("p.abstract").text()?.trim(),
|
||||
author: dom.find("a.nickname").text()?.trim(),
|
||||
url: `https://www.jianshu.com${href}`,
|
||||
mobileUrl: `https://www.jianshu.com${href}`,
|
||||
};
|
||||
});
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: listData,
|
||||
};
|
||||
};
|
||||
36
src/routes/juejin.ts
Normal file
36
src/routes/juejin.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "juejin",
|
||||
title: "稀土掘金",
|
||||
type: "文章榜",
|
||||
link: "https://juejin.cn/hot/articles",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://api.juejin.cn/content_api/v1/content/article_rank?category_id=1&type=hot`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["juejin"]) => ({
|
||||
id: v.content.content_id,
|
||||
title: v.content.title,
|
||||
author: v.author.name,
|
||||
hot: v.content_counter.hot_rank,
|
||||
url: `https://juejin.cn/post/${v.content.content_id}`,
|
||||
mobileUrl: `https://juejin.cn/post/${v.content.content_id}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
38
src/routes/lol.ts
Normal file
38
src/routes/lol.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "lol",
|
||||
title: "英雄联盟",
|
||||
type: "更新公告",
|
||||
link: "https://lol.qq.com/gicp/news/423/2/1334/1.html",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url =
|
||||
"https://apps.game.qq.com/cmc/zmMcnTargetContentList?r0=json&page=1&num=30&target=24&source=web_pc";
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.result;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["lol"]) => ({
|
||||
id: v.iDocID,
|
||||
title: v.sTitle,
|
||||
cover: `https:${v.sIMG}`,
|
||||
author: v.sAuthor,
|
||||
hot: Number(v.iTotalPlay),
|
||||
url: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||
mobileUrl: `https://lol.qq.com/news/detail.shtml?docid=${encodeURIComponent(v.iDocID)}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
36
src/routes/netease-news.ts
Normal file
36
src/routes/netease-news.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "netease-news",
|
||||
title: "网易新闻",
|
||||
type: "热点榜",
|
||||
link: "https://m.163.com/hot",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://m.163.com/fe/api/hot/news/flow`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["netease-news"]) => ({
|
||||
id: v.docid,
|
||||
title: v.title,
|
||||
cover: v.imgsrc,
|
||||
author: v.source,
|
||||
url: `https://www.163.com/dy/article/${v.docid}.html`,
|
||||
mobileUrl: `https://m.163.com/dy/article/${v.docid}.html`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
55
src/routes/ngabbs.ts
Normal file
55
src/routes/ngabbs.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { post } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "ngabbs",
|
||||
title: "NGA",
|
||||
type: "论坛热帖",
|
||||
description: "精英玩家俱乐部",
|
||||
link: "https://ngabbs.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://ngabbs.com/nuke.php?__lib=load_topic&__act=load_topic_reply_ladder2&opt=1&all=1`;
|
||||
const result = await post({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
Accept: "*/*",
|
||||
Host: "ngabbs.com",
|
||||
Referer: "https://ngabbs.com/",
|
||||
Connection: "keep-alive",
|
||||
"Content-Length": "11",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
"Accept-Language": "zh-Hans-CN;q=1",
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"User-Agent": "Apifox/1.0.0 (https://apifox.com)",
|
||||
"X-User-Agent": "NGA_skull/7.3.1(iPhone13,2;iOS 17.2.1)",
|
||||
},
|
||||
body: {
|
||||
__output: "14",
|
||||
},
|
||||
});
|
||||
const list = result.data.result[0];
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["ngabbs"]) => ({
|
||||
id: v.tid,
|
||||
title: v.subject,
|
||||
author: v.author,
|
||||
hot: v.replies,
|
||||
url: `https://bbs.nga.cn${v.tpcurl}`,
|
||||
mobileUrl: `https://bbs.nga.cn${v.tpcurl}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
37
src/routes/qq-news.ts
Normal file
37
src/routes/qq-news.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "qq-news",
|
||||
title: "腾讯新闻",
|
||||
type: "热点榜",
|
||||
link: "https://news.qq.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://r.inews.qq.com/gw/event/hot_ranking_list?page_size=50`;
|
||||
const result = await get({ url, noCache });const list = result.data.idlist[0].newslist.slice(1);
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["qq-news"]) => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
desc: v.abstract,
|
||||
cover: v.miniProShareImage,
|
||||
author: v.source,
|
||||
hot: v.hotEvent.hotScore,
|
||||
url: `https://new.qq.com/rain/a/${v.id}`,
|
||||
mobileUrl: `https://view.inews.qq.com/k/${v.id}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
46
src/routes/sspai.ts
Normal file
46
src/routes/sspai.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "热门文章";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "sspai",
|
||||
title: "少数派",
|
||||
type: "热榜",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "分类",
|
||||
type: ["热门文章", "应用推荐", "生活方式", "效率技巧", "少数派播客"],
|
||||
},
|
||||
},
|
||||
link: "https://sspai.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://sspai.com/api/v1/article/tag/page/get?limit=40&tag=${type}`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["sspai"]) => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
desc: v.summary,
|
||||
cover: v.banner,
|
||||
author: v.author.nickname,
|
||||
hot: v.like_count,
|
||||
url: `https://sspai.com/post/${v.id}`,
|
||||
mobileUrl: `https://sspai.com/post/${v.id}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
53
src/routes/starrail.ts
Normal file
53
src/routes/starrail.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { RouterData, ListContext, Options } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (c: ListContext, noCache: boolean) => {
|
||||
const type = c.req.query("type") || "1";
|
||||
const { fromCache, data, updateTime } = await getList({ type }, noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "starrail",
|
||||
title: "崩坏:星穹铁道",
|
||||
type: "最新动态",
|
||||
parameData: {
|
||||
type: {
|
||||
name: "榜单分类",
|
||||
type: {
|
||||
1: "公告",
|
||||
2: "活动",
|
||||
3: "资讯",
|
||||
},
|
||||
},
|
||||
},
|
||||
link: "https://www.miyoushe.com/sr/home/53",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (options: Options, noCache: boolean) => {
|
||||
const { type } = options;
|
||||
const url = `https://bbs-api.miyoushe.com/post/wapi/getNewsList?gids=6&page_size=20&type=${type}`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["miyoushe"]) => {
|
||||
const data = v.post;
|
||||
return {
|
||||
id: data.post_id,
|
||||
title: data.subject,
|
||||
desc: data.content,
|
||||
cover: data.cover,
|
||||
author: v.user.nickname,
|
||||
hot: v.stat.view_num,
|
||||
url: `https://www.miyoushe.com/sr/article/${data.post_id}`,
|
||||
mobileUrl: `https://m.miyoushe.com/sr/#/article/${data.post_id}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
36
src/routes/thepaper.ts
Normal file
36
src/routes/thepaper.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "thepaper",
|
||||
title: "澎湃新闻",
|
||||
type: "热榜",
|
||||
link: "https://www.thepaper.cn/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://cache.thepaper.cn/contentapi/wwwIndex/rightSidebar`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.hotNews;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["thepaper"]) => ({
|
||||
id: v.contId,
|
||||
title: v.name,
|
||||
cover: v.pic,
|
||||
hot: Number(v.praiseTimes),
|
||||
url: `https://www.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||
mobileUrl: `https://m.thepaper.cn/newsDetail_forward_${v.contId}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
38
src/routes/tieba.ts
Normal file
38
src/routes/tieba.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "tieba",
|
||||
title: "百度贴吧",
|
||||
type: "热议榜",
|
||||
description: "全球领先的中文社区",
|
||||
link: "https://tieba.baidu.com/hottopic/browse/topicList",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://tieba.baidu.com/hottopic/browse/topicList`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data.bang_topic.topic_list;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["tieba"]) => ({
|
||||
id: v.topic_id,
|
||||
title: v.topic_name,
|
||||
desc: v.topic_desc,
|
||||
cover: v.topic_pic,
|
||||
hot: v.discuss_num,
|
||||
url: v.topic_url,
|
||||
mobileUrl: v.topic_url,
|
||||
})),
|
||||
};
|
||||
};
|
||||
36
src/routes/toutiao.ts
Normal file
36
src/routes/toutiao.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "toutiao",
|
||||
title: "今日头条",
|
||||
type: "热榜",
|
||||
link: "https://www.toutiao.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://www.toutiao.com/hot-event/hot-board/?origin=toutiao_pc`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["toutiao"]) => ({
|
||||
id: v.ClusterIdStr,
|
||||
title: v.Title,
|
||||
cover: v.Image.url,
|
||||
hot: Number(v.HotValue),
|
||||
url: `https://www.toutiao.com/trending/${v.ClusterIdStr}/`,
|
||||
mobileUrl: `https://api.toutiaoapi.com/feoffline/amos_land/new/html/main/index.html?topic_id=${v.ClusterIdStr}`,
|
||||
})),
|
||||
};
|
||||
};
|
||||
43
src/routes/weibo.ts
Normal file
43
src/routes/weibo.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "weibo",
|
||||
title: "微博",
|
||||
type: "热搜榜",
|
||||
description: "实时热点,每分钟更新一次",
|
||||
link: "https://s.weibo.com/top/summary/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://weibo.com/ajax/side/hotSearch`;
|
||||
const result = await get({ url, noCache, ttl: 60 });
|
||||
const list = result.data.data.realtime;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["weibo"]) => {
|
||||
const key = v.word_scheme ? v.word_scheme : `#${v.word}`;
|
||||
return {
|
||||
id: v.mid,
|
||||
title: v.word,
|
||||
desc: v.note || key,
|
||||
author: v.category,
|
||||
hot: v.raw_hot,
|
||||
url: `https://s.weibo.com/weibo?q=${encodeURIComponent(key)}&t=31&band_rank=1&Refer=top`,
|
||||
mobileUrl: `https://s.weibo.com/weibo?q=${encodeURIComponent(
|
||||
key,
|
||||
)}&t=31&band_rank=1&Refer=top`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
49
src/routes/weread.ts
Normal file
49
src/routes/weread.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
import getWereadID from "../utils/getWereadID.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "weread",
|
||||
title: "微信读书",
|
||||
type: "飙升榜",
|
||||
link: "https://weread.qq.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://weread.qq.com/web/bookListInCategory/rising?rank=1`;
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67",
|
||||
},
|
||||
});
|
||||
const list = result.data.books;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["weread"]) => {
|
||||
const data = v.bookInfo;
|
||||
return {
|
||||
id: data.bookId,
|
||||
title: data.title,
|
||||
author: data.author,
|
||||
desc: data.intro,
|
||||
cover: data.cover.replace("s_", "t9_"),
|
||||
hot: v.readingCount,
|
||||
url: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,
|
||||
mobileUrl: `https://weread.qq.com/web/bookDetail/${getWereadID(data.bookId)}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
44
src/routes/zhihu-daily.ts
Normal file
44
src/routes/zhihu-daily.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "zhihu-daily",
|
||||
title: "知乎日报",
|
||||
type: "推荐榜",
|
||||
description: "每天三次,每次七分钟",
|
||||
link: "https://daily.zhihu.com/",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://news-at.zhihu.com/api/4/news/latest`;
|
||||
const result = await get({
|
||||
url,
|
||||
noCache,
|
||||
headers: {
|
||||
Referer: "https://news-at.zhihu.com/api/4/news/latest",
|
||||
Host: "news-at.zhihu.com",
|
||||
},
|
||||
});
|
||||
const list = result.data.stories.filter((el: RouterType["zhihu-daily"]) => el.type === 0);
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["zhihu-daily"]) => ({
|
||||
id: v.id,
|
||||
title: v.title,
|
||||
cover: v.images[0],
|
||||
author: v.hint,
|
||||
url: v.url,
|
||||
mobileUrl: v.url,
|
||||
})),
|
||||
};
|
||||
};
|
||||
40
src/routes/zhihu.ts
Normal file
40
src/routes/zhihu.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { RouterData } from "../types.js";
|
||||
import type { RouterType } from "../router.types.js";
|
||||
import { get } from "../utils/getData.js";
|
||||
|
||||
export const handleRoute = async (_: undefined, noCache: boolean) => {
|
||||
const { fromCache, data, updateTime } = await getList(noCache);
|
||||
const routeData: RouterData = {
|
||||
name: "zhihu",
|
||||
title: "知乎",
|
||||
type: "热榜",
|
||||
link: "https://www.zhihu.com/hot",
|
||||
total: data?.length || 0,
|
||||
updateTime,
|
||||
fromCache,
|
||||
data,
|
||||
};
|
||||
return routeData;
|
||||
};
|
||||
|
||||
const getList = async (noCache: boolean) => {
|
||||
const url = `https://www.zhihu.com/api/v3/feed/topstory/hot-lists/total?limit=50&desktop=true`;
|
||||
const result = await get({ url, noCache });
|
||||
const list = result.data.data;
|
||||
return {
|
||||
fromCache: result.fromCache,
|
||||
updateTime: result.updateTime,
|
||||
data: list.map((v: RouterType["zhihu"]) => {
|
||||
const data = v.target;
|
||||
return {
|
||||
id: data.id,
|
||||
title: data.title,
|
||||
desc: data.excerpt,
|
||||
cover: v.children[0].thumbnail,
|
||||
hot: parseInt(v.detail_text.replace(/[^\d]/g, "")) * 10000,
|
||||
url: `https://www.zhihu.com/question/${data.id}`,
|
||||
mobileUrl: `https://www.zhihu.com/question/${data.id}`,
|
||||
};
|
||||
}),
|
||||
};
|
||||
};
|
||||
56
src/types.ts
Normal file
56
src/types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Context } from "hono";
|
||||
|
||||
// Context
|
||||
export type ListContext = Context;
|
||||
|
||||
// 榜单数据
|
||||
export type ListItem = {
|
||||
id: number | string;
|
||||
title: string;
|
||||
cover?: string;
|
||||
author?: string;
|
||||
desc?: string;
|
||||
hot?: number;
|
||||
url: string | undefined;
|
||||
mobileUrl: string | undefined;
|
||||
};
|
||||
|
||||
// 路由数据
|
||||
export type RouterData = {
|
||||
name: string;
|
||||
title: string;
|
||||
type: string;
|
||||
description?: string;
|
||||
parameData?: Record<string, string | object>;
|
||||
total: number;
|
||||
link?: string;
|
||||
updateTime: string;
|
||||
fromCache: boolean;
|
||||
data: ListItem[];
|
||||
};
|
||||
|
||||
// 请求类型
|
||||
export type Get = {
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
params?: Record<string, string | number>;
|
||||
timeout?: number;
|
||||
noCache?: boolean;
|
||||
ttl?: number;
|
||||
originaInfo?: boolean;
|
||||
};
|
||||
|
||||
export type Post = {
|
||||
url: string;
|
||||
headers?: Record<string, string | string[]>;
|
||||
body?: string | object | Buffer | undefined;
|
||||
timeout?: number;
|
||||
noCache?: boolean;
|
||||
ttl?: number;
|
||||
originaInfo?: boolean;
|
||||
};
|
||||
|
||||
// 参数类型
|
||||
export type Options = {
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
37
src/utils/cache.ts
Normal file
37
src/utils/cache.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { config } from "../config.js";
|
||||
import NodeCache from "node-cache";
|
||||
import logger from "./logger.js";
|
||||
|
||||
// init
|
||||
const cache = new NodeCache({
|
||||
// 缓存过期时间( 秒 )
|
||||
stdTTL: config.CACHE_TTL,
|
||||
// 定期检查过期缓存( 秒 )
|
||||
checkperiod: 600,
|
||||
// 克隆变量
|
||||
useClones: false,
|
||||
// 最大键值对
|
||||
maxKeys: 100,
|
||||
});
|
||||
|
||||
interface GetCache<T> {
|
||||
updateTime: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
// 从缓存中获取数据
|
||||
export const getCache = <T>(key: string): GetCache<T> | undefined => {
|
||||
return cache.get(key);
|
||||
};
|
||||
|
||||
// 将数据写入缓存
|
||||
export const setCache = <T>(key: string, value: T, ttl: number = config.CACHE_TTL) => {
|
||||
const success = cache.set(key, value, ttl);
|
||||
if (logger) logger.info("数据缓存成功", { url: key });
|
||||
return success;
|
||||
};
|
||||
|
||||
// 从缓存中删除数据
|
||||
export const delCache = (key: string) => {
|
||||
return cache.del(key);
|
||||
};
|
||||
100
src/utils/getData.ts
Normal file
100
src/utils/getData.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import type { Get, Post } from "../types.ts";
|
||||
import { config } from "../config.js";
|
||||
import { getCache, setCache, delCache } from "./cache.js";
|
||||
import logger from "./logger.js";
|
||||
import axios from "axios";
|
||||
|
||||
// 基础配置
|
||||
const request = axios.create({
|
||||
// 请求超时设置
|
||||
timeout: config.REQUEST_TIMEOUT,
|
||||
withCredentials: true,
|
||||
});
|
||||
|
||||
// 请求拦截
|
||||
request.interceptors.request.use(
|
||||
(request) => {
|
||||
if (!request.params) request.params = {};
|
||||
// 发送请求
|
||||
return request;
|
||||
},
|
||||
(error) => {
|
||||
logger.error("请求失败,请稍后重试");
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// 响应拦截
|
||||
request.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
(error) => {
|
||||
// 继续传递错误
|
||||
return Promise.reject(error);
|
||||
},
|
||||
);
|
||||
|
||||
// GET
|
||||
export const get = async (options: Get) => {
|
||||
const { url, headers, noCache, ttl = config.CACHE_TTL, originaInfo = false } = options;
|
||||
logger.info("发起 GET 请求", options);
|
||||
try {
|
||||
// 检查缓存
|
||||
if (noCache) delCache(url);
|
||||
else {
|
||||
const cachedData = getCache(url);
|
||||
if (cachedData) {
|
||||
logger.info("采用缓存", { url });
|
||||
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
|
||||
}
|
||||
}
|
||||
// 缓存不存在时请求接口
|
||||
logger.info("请求接口", { url });
|
||||
const response = await request.get(url, { headers });
|
||||
const responseData = response?.data || response;
|
||||
// 存储新获取的数据到缓存
|
||||
const updateTime = new Date().toISOString();
|
||||
const data = originaInfo ? response : responseData;
|
||||
setCache(url, { data, updateTime }, ttl);
|
||||
// 返回数据
|
||||
logger.info("接口调用成功", { status: response?.statusText });
|
||||
return { fromCache: false, data, updateTime };
|
||||
} catch (error) {
|
||||
logger.error("GET 请求出错", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// POST
|
||||
export const post = async (options: Post) => {
|
||||
const { url, headers, body, noCache, ttl = config.CACHE_TTL, originaInfo = false } = options;
|
||||
logger.info("发起 POST 请求", options);
|
||||
try {
|
||||
// 检查缓存
|
||||
if (noCache) delCache(url);
|
||||
else {
|
||||
const cachedData = getCache(url);
|
||||
if (cachedData) {
|
||||
logger.info("采用缓存", { url });
|
||||
return { fromCache: true, data: cachedData.data, updateTime: cachedData.updateTime };
|
||||
}
|
||||
}
|
||||
// 缓存不存在时请求接口
|
||||
logger.info("请求接口", { url });
|
||||
const response = await request.post(url, body, { headers });
|
||||
const responseData = response?.data || response;
|
||||
// 存储新获取的数据到缓存
|
||||
const updateTime = new Date().toISOString();
|
||||
const data = originaInfo ? response : responseData;
|
||||
if (!noCache) {
|
||||
setCache(url, { data, updateTime }, ttl);
|
||||
}
|
||||
// 返回数据
|
||||
logger.info("接口调用成功", { status: response?.statusText });
|
||||
return { fromCache: false, data, updateTime };
|
||||
} catch (error) {
|
||||
logger.error("POST 请求出错", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
42
src/utils/getRSS.ts
Normal file
42
src/utils/getRSS.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { RouterData, ListItem } from "../types.ts";
|
||||
import { Feed } from "feed";
|
||||
import logger from "./logger.js";
|
||||
|
||||
// 生成 RSS
|
||||
const getRSS = (data: RouterData) => {
|
||||
try {
|
||||
// 基本信息
|
||||
const feed = new Feed({
|
||||
title: data.title,
|
||||
description: data.title + data.type + (data?.description ? " - " + data?.description : ""),
|
||||
id: data.name,
|
||||
link: data.link,
|
||||
language: "zh",
|
||||
generator: "DailyHotApi",
|
||||
copyright: "Copyright © 2020-present imsyy",
|
||||
updated: new Date(data.updateTime),
|
||||
});
|
||||
// 获取数据
|
||||
const listData = data.data;
|
||||
listData.forEach((item: ListItem) => {
|
||||
feed.addItem({
|
||||
id: item.id?.toString(),
|
||||
title: item.title,
|
||||
date: new Date(data.updateTime),
|
||||
link: item.url || "获取失败",
|
||||
description: item?.desc,
|
||||
author: [
|
||||
{
|
||||
name: item.author,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
const rssData = feed.rss2();
|
||||
return rssData;
|
||||
} catch (error) {
|
||||
logger.error("RSS 生成失败:", error);
|
||||
}
|
||||
};
|
||||
|
||||
export default getRSS;
|
||||
67
src/utils/getWereadID.ts
Normal file
67
src/utils/getWereadID.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import crypto from "crypto";
|
||||
|
||||
/**
|
||||
* 获取微信读书的书籍 ID
|
||||
* 感谢 @MCBBC 及 ChatGPT
|
||||
*/
|
||||
const getWereadID = (bookId: string) => {
|
||||
try {
|
||||
// 使用 MD5 哈希算法创建哈希对象
|
||||
const hash = crypto.createHash("md5");
|
||||
hash.update(bookId);
|
||||
const str = hash.digest("hex");
|
||||
// 取哈希结果的前三个字符作为初始值
|
||||
let strSub = str.substring(0, 3);
|
||||
// 判断书籍 ID 的类型并进行转换
|
||||
let fa;
|
||||
if (/^\d*$/.test(bookId)) {
|
||||
// 如果书籍 ID 只包含数字,则将其拆分成长度为 9 的子字符串,并转换为十六进制表示
|
||||
const chunks = [];
|
||||
for (let i = 0; i < bookId.length; i += 9) {
|
||||
const chunk = bookId.substring(i, i + 9);
|
||||
chunks.push(parseInt(chunk).toString(16));
|
||||
}
|
||||
fa = ["3", chunks];
|
||||
} else {
|
||||
// 如果书籍 ID 包含其他字符,则将每个字符的 Unicode 编码转换为十六进制表示
|
||||
let hexStr = "";
|
||||
for (let i = 0; i < bookId.length; i++) {
|
||||
hexStr += bookId.charCodeAt(i).toString(16);
|
||||
}
|
||||
fa = ["4", [hexStr]];
|
||||
}
|
||||
// 将类型添加到初始值中
|
||||
strSub += fa[0];
|
||||
// 将数字 2 和哈希结果的后两个字符添加到初始值中
|
||||
strSub += "2" + str.substring(str.length - 2);
|
||||
// 处理转换后的子字符串数组
|
||||
for (let i = 0; i < fa[1].length; i++) {
|
||||
const sub = fa[1][i];
|
||||
const subLength = sub.length.toString(16);
|
||||
// 如果长度只有一位数,则在前面添加 0
|
||||
const subLengthPadded = subLength.length === 1 ? "0" + subLength : subLength;
|
||||
// 将长度和子字符串添加到初始值中
|
||||
strSub += subLengthPadded + sub;
|
||||
// 如果不是最后一个子字符串,则添加分隔符 'g'
|
||||
if (i < fa[1].length - 1) {
|
||||
strSub += "g";
|
||||
}
|
||||
}
|
||||
// 如果初始值长度不足 20,从哈希结果中取足够的字符补齐
|
||||
if (strSub.length < 20) {
|
||||
strSub += str.substring(0, 20 - strSub.length);
|
||||
}
|
||||
// 使用 MD5 哈希算法创建新的哈希对象
|
||||
const finalHash = crypto.createHash("md5");
|
||||
finalHash.update(strSub);
|
||||
const finalStr = finalHash.digest("hex");
|
||||
// 取最终哈希结果的前三个字符并添加到初始值的末尾
|
||||
strSub += finalStr.substring(0, 3);
|
||||
return strSub;
|
||||
} catch (error) {
|
||||
console.error("处理微信读书 ID 时出现错误:" + error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export default getWereadID;
|
||||
49
src/utils/logger.ts
Normal file
49
src/utils/logger.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { config } from "../config.js";
|
||||
import { createLogger, format, transports } from "winston";
|
||||
import path from "path";
|
||||
|
||||
let pathOption: (typeof transports.File)[] = [];
|
||||
|
||||
// 日志输出目录
|
||||
if (config.USE_LOG_FILE) {
|
||||
pathOption = [
|
||||
new transports.File({
|
||||
filename: path.resolve("logs/error.log"),
|
||||
level: "error",
|
||||
maxsize: 1024 * 1024,
|
||||
maxFiles: 1,
|
||||
}),
|
||||
new transports.File({
|
||||
filename: path.resolve("logs/logger.log"),
|
||||
maxsize: 1024 * 1024,
|
||||
maxFiles: 1,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
// logger
|
||||
const logger = createLogger({
|
||||
// 最低的日志级别
|
||||
level: "info",
|
||||
// 定义日志的格式
|
||||
format: format.combine(
|
||||
format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss",
|
||||
}),
|
||||
format.errors({ stack: true }),
|
||||
format.splat(),
|
||||
format.json(),
|
||||
),
|
||||
transports: pathOption,
|
||||
});
|
||||
|
||||
// 控制台输出
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
logger.add(
|
||||
new transports.Console({
|
||||
format: format.combine(format.colorize(), format.simple()),
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export default logger;
|
||||
60
src/views/Error.tsx
Normal file
60
src/views/Error.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { FC } from "hono/jsx";
|
||||
import { html } from "hono/html";
|
||||
import Layout from "./Layout.js";
|
||||
|
||||
const Error: FC = (props) => {
|
||||
return (
|
||||
<Layout title="Error | DailyHot API">
|
||||
<main className="error">
|
||||
<div className="img">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 36 36">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M30 13.5a7.49 7.49 0 0 1-6.78-4.3H4V7h18.57a7.52 7.52 0 0 1-.07-1a7.52 7.52 0 0 1 .07-1H4a2 2 0 0 0-2 2v22a2 2 0 0 0 2 2h28a2 2 0 0 0 2-2V12.34a7.46 7.46 0 0 1-4 1.16m-13.2 6.33l-10 4.59v-2.64l6.51-3l-6.51-3v-2.61l10 4.59Zm6.6 5.57H17V23h6.4Z"
|
||||
class="clr-i-solid--badged clr-i-solid-path-1--badged"
|
||||
/>
|
||||
<circle
|
||||
cx="30"
|
||||
cy="6"
|
||||
r="5"
|
||||
fill="currentColor"
|
||||
class="clr-i-solid--badged clr-i-solid-path-2--badged clr-i-badge"
|
||||
/>
|
||||
<path fill="none" d="M0 0h36v36H0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="title">
|
||||
<h1 className="title-text">Looks like something went wrong</h1>
|
||||
<span className="title-tip">程序执行出错</span>
|
||||
{props?.error ? <p className="content">{props.error}</p> : null}
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="reload-button">
|
||||
<svg
|
||||
className="btn-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17.65 6.35a7.95 7.95 0 0 0-6.48-2.31c-3.67.37-6.69 3.35-7.1 7.02C3.52 15.91 7.27 20 12 20a7.98 7.98 0 0 0 7.21-4.56c.32-.67-.16-1.44-.9-1.44c-.37 0-.72.2-.88.53a5.994 5.994 0 0 1-6.8 3.31c-2.22-.49-4.01-2.3-4.48-4.52A6.002 6.002 0 0 1 12 6c1.66 0 3.14.69 4.22 1.78l-1.51 1.51c-.63.63-.19 1.71.7 1.71H19c.55 0 1-.45 1-1V6.41c0-.89-1.08-1.34-1.71-.71z"
|
||||
/>
|
||||
</svg>
|
||||
<span className="btn-text">刷新重试</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
{html`
|
||||
<script>
|
||||
document.getElementById("reload-button").addEventListener("click", () => {
|
||||
window.location.reload();
|
||||
});
|
||||
</script>
|
||||
`}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Error;
|
||||
60
src/views/Home.tsx
Normal file
60
src/views/Home.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { FC } from "hono/jsx";
|
||||
import { html } from "hono/html";
|
||||
import Layout from "./Layout.js";
|
||||
|
||||
const Home: FC = () => {
|
||||
return (
|
||||
<Layout title="DailyHot API">
|
||||
<main className="home">
|
||||
<div className="img">
|
||||
<img src="/ico/favicon.png" alt="logo" />
|
||||
</div>
|
||||
<div className="title">
|
||||
<h1 className="title-text">DailyHot API</h1>
|
||||
<span className="title-tip">服务已正常运行</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="test-button">
|
||||
<svg
|
||||
className="btn-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M7.71 6.71a.996.996 0 0 0-1.41 0L1.71 11.3a.996.996 0 0 0 0 1.41L6.3 17.3a.996.996 0 1 0 1.41-1.41L3.83 12l3.88-3.88c.38-.39.38-1.03 0-1.41m8.58 0a.996.996 0 0 0 0 1.41L20.17 12l-3.88 3.88a.996.996 0 1 0 1.41 1.41l4.59-4.59a.996.996 0 0 0 0-1.41L17.7 6.7c-.38-.38-1.02-.38-1.41.01M8 13c.55 0 1-.45 1-1s-.45-1-1-1s-1 .45-1 1s.45 1 1 1m4 0c.55 0 1-.45 1-1s-.45-1-1-1s-1 .45-1 1s.45 1 1 1m4-2c-.55 0-1 .45-1 1s.45 1 1 1s1-.45 1-1s-.45-1-1-1"
|
||||
/>
|
||||
</svg>
|
||||
<span className="btn-text">测试接口</span>
|
||||
</button>
|
||||
<button id="docs-button">
|
||||
<svg
|
||||
className="btn-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M3 6c-.55 0-1 .45-1 1v13c0 1.1.9 2 2 2h13c.55 0 1-.45 1-1s-.45-1-1-1H5c-.55 0-1-.45-1-1V7c0-.55-.45-1-1-1m17-4H8c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2m-2 9h-8c-.55 0-1-.45-1-1s.45-1 1-1h8c.55 0 1 .45 1 1s-.45 1-1 1m-4 4h-4c-.55 0-1-.45-1-1s.45-1 1-1h4c.55 0 1 .45 1 1s-.45 1-1 1m4-8h-8c-.55 0-1-.45-1-1s.45-1 1-1h8c.55 0 1 .45 1 1s-.45 1-1 1"
|
||||
/>
|
||||
</svg>
|
||||
<span className="btn-text">项目文档</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
{html`
|
||||
<script>
|
||||
document.getElementById("test-button").addEventListener("click", () => {
|
||||
window.location.href = "/all";
|
||||
});
|
||||
</script>
|
||||
`}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default Home;
|
||||
243
src/views/Layout.tsx
Normal file
243
src/views/Layout.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
import type { FC } from "hono/jsx";
|
||||
import { css, Style } from "hono/css";
|
||||
|
||||
type LayoutProps = {
|
||||
title: string;
|
||||
children: JSX.Element | JSX.Element[];
|
||||
};
|
||||
|
||||
const Layout: FC<LayoutProps> = (props) => {
|
||||
const globalClass = css`
|
||||
:-hono-global {
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
user-select: none;
|
||||
box-sizing: border-box;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
:root {
|
||||
--text-color: #000;
|
||||
--text-color-gray: #cbcbcb;
|
||||
--text-color-hover: #fff;
|
||||
--icon-color: #444;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text-color: #fff;
|
||||
--text-color-gray: #cbcbcb;
|
||||
--text-color-hover: #3c3c3c;
|
||||
--icon-color: #cbcbcb;
|
||||
}
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--text-color);
|
||||
}
|
||||
body {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
background-color: var(--text-color-hover);
|
||||
font-family: "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei";
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s;
|
||||
}
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
height: 100%;
|
||||
}
|
||||
.img {
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.img img,
|
||||
.img svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
.title .title-text {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.title .title-tip {
|
||||
font-size: 20px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.title .content {
|
||||
margin-top: 30px;
|
||||
display: flex;
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
border: 1px dashed var(--text-color);
|
||||
}
|
||||
.control {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.control button {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
color: var(--text-color);
|
||||
border: var(--text-color) solid;
|
||||
background-color: var(--text-color-hover);
|
||||
border-radius: 8px;
|
||||
padding: 8px 12px;
|
||||
margin: 0 8px;
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
.control button .btn-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.control button .btn-text {
|
||||
font-size: 14px;
|
||||
}
|
||||
.control button:hover {
|
||||
border: var(--text-color) solid;
|
||||
background: var(--text-color);
|
||||
color: var(--text-color-hover);
|
||||
}
|
||||
.control button i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
footer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 30px;
|
||||
padding: 20px;
|
||||
}
|
||||
.social {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.social .link {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
margin: 0 4px;
|
||||
}
|
||||
.social .link::after {
|
||||
content: "";
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--text-color);
|
||||
opacity: 0.4;
|
||||
margin-left: 8px;
|
||||
}
|
||||
.social .link:last-child::after {
|
||||
display: none;
|
||||
}
|
||||
.social .link svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
footer .power,
|
||||
footer .icp {
|
||||
font-size: 14px;
|
||||
}
|
||||
footer a {
|
||||
color: var(--text-color-gray);
|
||||
transition: color 0.3s;
|
||||
}
|
||||
footer a:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<meta charset="utf-8" />
|
||||
<title>{props.title}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<meta name="description" content="今日热榜 API,一个聚合热门数据的 API 接口" />
|
||||
<Style>{globalClass}</Style>
|
||||
</head>
|
||||
<body>
|
||||
{props.children}
|
||||
<footer>
|
||||
<div class="social">
|
||||
<a href="https://github.com/imsyy/DailyHotApi" className="link" target="_blank">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2A10 10 0 0 0 2 12c0 4.42 2.87 8.17 6.84 9.5c.5.08.66-.23.66-.5v-1.69c-2.77.6-3.36-1.34-3.36-1.34c-.46-1.16-1.11-1.47-1.11-1.47c-.91-.62.07-.6.07-.6c1 .07 1.53 1.03 1.53 1.03c.87 1.52 2.34 1.07 2.91.83c.09-.65.35-1.09.63-1.34c-2.22-.25-4.55-1.11-4.55-4.92c0-1.11.38-2 1.03-2.71c-.1-.25-.45-1.29.1-2.64c0 0 .84-.27 2.75 1.02c.79-.22 1.65-.33 2.5-.33c.85 0 1.71.11 2.5.33c1.91-1.29 2.75-1.02 2.75-1.02c.55 1.35.2 2.39.1 2.64c.65.71 1.03 1.6 1.03 2.71c0 3.82-2.34 4.66-4.57 4.91c.36.31.69.92.69 1.85V21c0 .27.16.59.67.5C19.14 20.16 22 16.42 22 12A10 10 0 0 0 12 2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="https://www.imsyy.top" className="link" target="_blank">
|
||||
<svg
|
||||
className="btn-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="mailto:one@imsyy.top" className="link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="m20 8l-8 5l-8-5V6l8 5l8-5m0-2H4c-1.11 0-2 .89-2 2v12a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="power">
|
||||
Copyright ©
|
||||
<a href="https://www.imsyy.top/" target="_blank">
|
||||
無名
|
||||
</a>
|
||||
| Power by
|
||||
<a href="https://github.com/honojs/hono/" target="_blank">
|
||||
Hono
|
||||
</a>
|
||||
</div>
|
||||
<div class="icp">
|
||||
<a href="https://beian.miit.gov.cn/" target="_blank">
|
||||
豫ICP备2022018134号-1
|
||||
</a>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
44
src/views/NotFound.tsx
Normal file
44
src/views/NotFound.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import type { FC } from "hono/jsx";
|
||||
import { html } from "hono/html";
|
||||
import Layout from "./Layout.js";
|
||||
|
||||
const NotFound: FC = () => {
|
||||
return (
|
||||
<Layout title="404 Not Found | DailyHot API">
|
||||
<main className="not-found">
|
||||
<div className="img">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 17q.425 0 .713-.288Q13 16.425 13 16t-.287-.713Q12.425 15 12 15t-.712.287Q11 15.575 11 16t.288.712Q11.575 17 12 17Zm0 5q-2.075 0-3.9-.788q-1.825-.787-3.175-2.137q-1.35-1.35-2.137-3.175Q2 14.075 2 12t.788-3.9q.787-1.825 2.137-3.175q1.35-1.35 3.175-2.138Q9.925 2 12 2t3.9.787q1.825.788 3.175 2.138q1.35 1.35 2.137 3.175Q22 9.925 22 12t-.788 3.9q-.787 1.825-2.137 3.175q-1.35 1.35-3.175 2.137Q14.075 22 12 22Zm0-9q.425 0 .713-.288Q13 12.425 13 12V8q0-.425-.287-.713Q12.425 7 12 7t-.712.287Q11 7.575 11 8v4q0 .425.288.712q.287.288.712.288Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div className="title">
|
||||
<h1 className="title-text">404 Not Found</h1>
|
||||
<span className="title-tip">请检查您的路径</span>
|
||||
</div>
|
||||
<div class="control">
|
||||
<button id="home-button">
|
||||
<svg className="btn-icon" xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1"
|
||||
/>
|
||||
</svg>
|
||||
<span className="btn-text">回到首页</span>
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
{html`
|
||||
<script>
|
||||
document.getElementById("home-button").addEventListener("click", () => {
|
||||
window.location.href = "/";
|
||||
});
|
||||
</script>
|
||||
`}
|
||||
</Layout>
|
||||
);
|
||||
};
|
||||
|
||||
export default NotFound;
|
||||
Reference in New Issue
Block a user