diff --git a/bun.lock b/bun.lock
index 482c6f9..280b910 100644
--- a/bun.lock
+++ b/bun.lock
@@ -91,7 +91,7 @@
"@elysiajs/server-timing": "^1.4.0",
"@rabbit-company/argon2id": "^2.1.0",
"chalk": "^5.6.2",
- "elysia": "^1.4.15",
+ "elysia": "catalog:",
"elysia-ip": "^1.0.10",
"zod": "^4.1.12",
},
@@ -217,6 +217,7 @@
"sonner": "^2.0.7",
"swr": "^2.3.6",
"tailwind-merge": "^3.3.1",
+ "zod": "^4.1.12",
},
"devDependencies": {
"@react-router/dev": "^7.7.1",
@@ -233,6 +234,9 @@
},
},
},
+ "catalog": {
+ "elysia": "^1.4.15",
+ },
"packages": {
"@alicloud/credentials": ["@alicloud/credentials@2.4.4", "", { "dependencies": { "@alicloud/tea-typescript": "^1.8.0", "httpx": "^2.3.3", "ini": "^1.3.5", "kitx": "^2.0.0" } }, "sha512-/eRAGSKcniLIFQ1UCpDhB/IrHUZisQ1sc65ws/c2avxUMpXwH1rWAohb76SVAUJhiF4mwvLzLJM1Mn1XL4Xe/Q=="],
@@ -2970,6 +2974,8 @@
"@cloudflare/kv-asset-handler/mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
+ "@cvsa/cvsa-temp/zod": ["zod@4.1.12", "", {}, "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ=="],
+
"@deno/shim-deno/which": ["which@4.0.0", "", { "dependencies": { "isexe": "^3.1.1" }, "bin": { "node-which": "bin/which.js" } }, "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg=="],
"@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="],
diff --git a/package.json b/package.json
index f5b6da0..c8ae31c 100644
--- a/package.json
+++ b/package.json
@@ -3,16 +3,21 @@
"version": "3.15.34",
"private": false,
"type": "module",
- "workspaces": [
- "packages/frontend",
- "packages/core",
- "packages/backend",
- "packages/crawler",
- "packages/elysia",
- "packages/temp_frontend",
- "packages/solid",
- "packages/palette"
- ],
+ "workspaces": {
+ "packages": [
+ "packages/frontend",
+ "packages/core",
+ "packages/backend",
+ "packages/crawler",
+ "packages/elysia",
+ "packages/temp_frontend",
+ "packages/solid",
+ "packages/palette"
+ ],
+ "catalog": {
+ "elysia": "^1.4.15"
+ }
+ },
"dependencies": {
"arg": "^5.0.2",
"dotenv": "^17.2.3",
diff --git a/packages/elysia/package.json b/packages/elysia/package.json
index 3acf854..a514664 100644
--- a/packages/elysia/package.json
+++ b/packages/elysia/package.json
@@ -14,7 +14,7 @@
"@elysiajs/server-timing": "^1.4.0",
"@rabbit-company/argon2id": "^2.1.0",
"chalk": "^5.6.2",
- "elysia": "^1.4.15",
+ "elysia": "catalog:",
"elysia-ip": "^1.0.10",
"zod": "^4.1.12"
},
diff --git a/packages/elysia/routes/auth/login.ts b/packages/elysia/routes/auth/login.ts
index 3cd41ea..dedf2c4 100644
--- a/packages/elysia/routes/auth/login.ts
+++ b/packages/elysia/routes/auth/login.ts
@@ -32,7 +32,8 @@ export const loginHandler = new Elysia({ prefix: "/auth" })
username: user.username,
nickname: user.nickname,
role: user.role
- }
+ },
+ sessionID: sessionId
};
},
{
@@ -44,7 +45,8 @@ export const loginHandler = new Elysia({ prefix: "/auth" })
username: t.String(),
nickname: t.Optional(t.String()),
role: t.String()
- })
+ }),
+ sessionID: t.String()
}),
401: t.Object({
message: t.String()
diff --git a/packages/elysia/routes/search/index.ts b/packages/elysia/routes/search/index.ts
index 4cea4c3..8227740 100644
--- a/packages/elysia/routes/search/index.ts
+++ b/packages/elysia/routes/search/index.ts
@@ -105,6 +105,7 @@ const BiliVideoDataSchema = BiliVideoSchema.extend({
export const searchHandler = new Elysia({ prefix: "/search" }).get(
"/result",
async ({ query }) => {
+ const start = performance.now();
const searchQuery = query.query;
const [songResults, videoResults] = await Promise.all([
getSongSearchResult(searchQuery),
@@ -112,24 +113,32 @@ export const searchHandler = new Elysia({ prefix: "/search" }).get(
]);
const combinedResults = [...songResults, ...videoResults];
- return combinedResults.sort((a, b) => b.rank - a.rank);
+ const data = combinedResults.sort((a, b) => b.rank - a.rank);
+ const end = performance.now();
+ return {
+ data,
+ elapsedMs: end - start
+ };
},
{
response: {
- 200: z.array(
- z.union([
- z.object({
- type: z.literal("song"),
- data: SongSchema,
- rank: z.number()
- }),
- z.object({
- type: z.literal("bili-video"),
- data: BiliVideoDataSchema,
- rank: z.number()
- })
- ])
- ),
+ 200: z.object({
+ elapsedMs: z.number(),
+ data: z.array(
+ z.union([
+ z.object({
+ type: z.literal("song"),
+ data: SongSchema,
+ rank: z.number()
+ }),
+ z.object({
+ type: z.literal("bili-video"),
+ data: BiliVideoDataSchema,
+ rank: z.number()
+ })
+ ])
+ )
+ }),
404: z.object({
message: z.string()
})
diff --git a/packages/elysia/routes/song/info.ts b/packages/elysia/routes/song/info.ts
index 8336691..8280edc 100644
--- a/packages/elysia/routes/song/info.ts
+++ b/packages/elysia/routes/song/info.ts
@@ -98,9 +98,13 @@ const songInfoGetHandler = new Elysia({ prefix: "/song" }).get(
cover: t.Optional(t.String())
}),
404: t.Object({
+ code: t.String(),
message: t.String()
})
},
+ headers: t.Object({
+ "Authorization": t.Optional(t.String())
+ }),
detail: {
summary: "Get information of a song",
description:
diff --git a/packages/temp_frontend/app/components/Error.tsx b/packages/temp_frontend/app/components/Error.tsx
index 0855ee8..c06ac6c 100644
--- a/packages/temp_frontend/app/components/Error.tsx
+++ b/packages/temp_frontend/app/components/Error.tsx
@@ -5,7 +5,8 @@ export function Error({ error }: { error: { status: number; value: { message?: s
return (
-
+
diff --git a/packages/temp_frontend/app/components/Layout.tsx b/packages/temp_frontend/app/components/Layout.tsx
index 7ae8a5f..7fe4d52 100644
--- a/packages/temp_frontend/app/components/Layout.tsx
+++ b/packages/temp_frontend/app/components/Layout.tsx
@@ -1,12 +1,18 @@
+import { toast } from "sonner";
import { Search } from "./Search";
export function Layout({ children }: { children?: React.ReactNode }) {
return (
-
-
- 中V档案馆
-
+
+
{children}
@@ -14,13 +20,37 @@ export function Layout({ children }: { children?: React.ReactNode }) {
);
}
-export function LayoutWithouSearch({ children }: { children?: React.ReactNode }) {
+const LoginOrLogout = () => {
+ const session = localStorage.getItem("sessionID");
+ if (session) {
+ return (
+ {
+ localStorage.removeItem("sessionID");
+ toast.success("已退出登录");
+ }}
+ >
+ 退出登录
+
+ );
+ } else {
+ return 登录;
+ }
+};
+
+export function LayoutWithoutSearch({ children }: { children?: React.ReactNode }) {
return (
diff --git a/packages/temp_frontend/app/components/SearchResults.tsx b/packages/temp_frontend/app/components/SearchResults.tsx
index 4aef7ca..1d34f52 100644
--- a/packages/temp_frontend/app/components/SearchResults.tsx
+++ b/packages/temp_frontend/app/components/SearchResults.tsx
@@ -6,7 +6,7 @@ interface SearchResultsProps {
query: string;
}
-const formatDateTime = (date: Date): string => {
+export const formatDateTime = (date: Date): string => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0"); // 月份从0开始,补0
const day = String(date.getDate()).padStart(2, "0");
@@ -20,7 +20,7 @@ const formatDateTime = (date: Date): string => {
const biliIDSchema = z.union([z.string().regex(/BV1[0-9A-Za-z]{9}/), z.string().regex(/av[0-9]+/)]);
export function SearchResults({ results, query }: SearchResultsProps) {
- if (!results || results.length === 0) {
+ if (!results || results.data.length === 0) {
if (!biliIDSchema.safeParse(query).success) {
return (
@@ -43,10 +43,12 @@ export function SearchResults({ results, query }: SearchResultsProps) {
return (
- {results.map((result, index) => (
+
找到 {results.data.length} 个结果({(results.elapsedMs / 1000).toFixed(3)}秒):
+ {results.data.map((result, index) => (
{result.type === "song" ? : }
@@ -55,22 +57,22 @@ export function SearchResults({ results, query }: SearchResultsProps) {
);
}
-function SongResult({ result }: { result: Exclude
[number] }) {
+function SongResult({ result }: { result: Exclude["data"][number] }) {
if (result.type !== "song") return null;
const { data } = result;
return (
-
+
{data.image && (

)}
-
-
{data.name}
+
+
{data.name}
{data.producer &&
{data.producer}
}
{data.duration && (
@@ -95,7 +97,7 @@ function SongResult({ result }: { result: Exclude
[number] })
);
}
-function BiliVideoResult({ result }: { result: Exclude[number] }) {
+function BiliVideoResult({ result }: { result: Exclude["data"][number] }) {
if (result.type !== "bili-video") return null;
const { data } = result;
@@ -116,7 +118,7 @@ function BiliVideoResult({ result }: { result: Exclude[numbe
{data.description}
)}
-
+
{data.duration && (
{Math.floor(data.duration / 60)}:{(data.duration % 60).toString().padStart(2, "0")}
diff --git a/packages/temp_frontend/app/routes.ts b/packages/temp_frontend/app/routes.ts
index c0e4558..557c9ed 100644
--- a/packages/temp_frontend/app/routes.ts
+++ b/packages/temp_frontend/app/routes.ts
@@ -5,4 +5,5 @@ export default [
route("song/:id/info", "routes/song/[id]/info.tsx"),
route("chart-demo", "routes/chartDemo.tsx"),
route("search", "routes/search/index.tsx"),
+ route("login", "routes/login.tsx"),
] satisfies RouteConfig;
diff --git a/packages/temp_frontend/app/routes/login.tsx b/packages/temp_frontend/app/routes/login.tsx
new file mode 100644
index 0000000..bf8f4ea
--- /dev/null
+++ b/packages/temp_frontend/app/routes/login.tsx
@@ -0,0 +1,99 @@
+import { LayoutWithoutSearch } from "@/components/Layout";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { useState } from "react";
+import { useNavigate } from "react-router";
+import { treaty } from "@elysiajs/eden";
+import type { App } from "@elysia/src";
+
+// @ts-expect-error anyway...
+const app = treaty(import.meta.env.VITE_API_URL!);
+
+export default function Login() {
+ const navigate = useNavigate();
+ const [formData, setFormData] = useState({
+ username: "",
+ password: "",
+ });
+ const [isLoading, setIsLoading] = useState(false);
+ const [error, setError] = useState("");
+
+ const handleChange = (e: React.ChangeEvent) => {
+ const { name, value } = e.target;
+ setFormData((prev) => ({
+ ...prev,
+ [name]: value,
+ }));
+ setError("");
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+ setIsLoading(true);
+ setError("");
+
+ try {
+ const { data, error } = await app.auth.session.post(formData);
+
+ if (data) {
+ localStorage.setItem("sessionID", data.sessionID);
+ navigate("/", { replace: true });
+ } else {
+ setError(error?.value?.message || "登录失败");
+ }
+ } catch (err) {
+ setError("网络错误,请稍后重试");
+ console.error("Login error:", err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ 登录
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+ );
+}
diff --git a/packages/temp_frontend/app/routes/search/index.tsx b/packages/temp_frontend/app/routes/search/index.tsx
index ef7e7da..8f33250 100644
--- a/packages/temp_frontend/app/routes/search/index.tsx
+++ b/packages/temp_frontend/app/routes/search/index.tsx
@@ -7,8 +7,9 @@ import { useSearchParams } from "react-router";
import { SearchBox } from "@/components/Search";
import { SearchResults } from "@/components/SearchResults";
import { Title } from "@/components/Title";
-import { LayoutWithouSearch } from "@/components/Layout";
+import { LayoutWithoutSearch } from "@/components/Layout";
+// @ts-expect-error anyway...
const app = treaty(import.meta.env.VITE_API_URL!);
export type SearchResult = Awaited>["data"];
@@ -57,19 +58,19 @@ export default function SearchResult() {
if (!searchParams.get("q")) {
return (
-
+
-
+
);
}
if (!data && !error) {
return (
-
+
-
+
);
}
@@ -78,10 +79,10 @@ export default function SearchResult() {
}
return (
-
+
-
+
);
}
diff --git a/packages/temp_frontend/app/routes/song/[id]/info.tsx b/packages/temp_frontend/app/routes/song/[id]/info.tsx
index b1bd164..4e522b9 100644
--- a/packages/temp_frontend/app/routes/song/[id]/info.tsx
+++ b/packages/temp_frontend/app/routes/song/[id]/info.tsx
@@ -5,13 +5,15 @@ import { useEffect, useState } from "react";
import { Skeleton } from "@/components/ui/skeleton";
import { TriangleAlert } from "lucide-react";
import { Title } from "@/components/Title";
-import { Search } from "@/components/Search";
+import { toast } from "sonner";
import { Error } from "@/components/Error";
import { Layout } from "@/components/Layout";
-import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
+import { formatDateTime } from "@/components/SearchResults";
+// @ts-expect-error anyway...
const app = treaty(import.meta.env.VITE_API_URL!);
type SongInfo = Awaited["info"]["get"]>>["data"];
@@ -27,14 +29,17 @@ const SnapshotsView = ({ snapshots }: { snapshots: Snapshots | null }) => {
if (!snapshots) {
return (
<>
- 历史快照
-
+ {/* 历史快照
*/}
+
>
);
}
return (
-
-
历史快照
+
+
+ 播放: {snapshots[0].views.toLocaleString()},更新于{formatDateTime(new Date(snapshots[0].createdAt))}
+
+ {/*
历史快照
@@ -58,7 +63,7 @@ const SnapshotsView = ({ snapshots }: { snapshots: Snapshots | null }) => {
))}
-
+ */}
);
};
@@ -143,73 +148,75 @@ export default function SongInfo({ loaderData }: Route.ComponentProps) {
}
const formatDuration = (duration: number) => {
- const minutes = Math.floor(duration / 60);
- const seconds = duration % 60;
- return `${minutes}:${seconds}`;
+ return `${Math.floor(duration / 60)}:${(duration % 60).toString().padStart(2, "0")}`;
};
const handleSongNameChange = async () => {
if (songName.trim() === "") return;
- await app.song({ id: loaderData.id }).info.patch({ name: songName });
+ const { data, error } = await app.song({ id: loaderData.id }).info.patch(
+ { name: songName },
+ {
+ headers: {
+ Authorization: `Bearer ${localStorage.getItem("sessionID") || ""}`,
+ },
+ },
+ );
setIsDialogOpen(false);
// Refresh the data to show the updated name
- getInfo();
+ if (error || !data) {
+ toast.error(`无法更新:${error.value.message || "未知错误"}`);
+ }
+ setData(data?.updated);
};
return (
-
+
-
-
- 中V档案馆
-
-
- {data!.cover && (
-
- )}
-
-
-
setIsDialogOpen(true)}>
- {data!.name ? data!.name : "未知歌曲名"}
-
-
+
+
);
}
diff --git a/packages/temp_frontend/package.json b/packages/temp_frontend/package.json
index 3c1f361..73ee2ac 100644
--- a/packages/temp_frontend/package.json
+++ b/packages/temp_frontend/package.json
@@ -29,7 +29,8 @@
"recharts": "^3.2.1",
"sonner": "^2.0.7",
"swr": "^2.3.6",
- "tailwind-merge": "^3.3.1"
+ "tailwind-merge": "^3.3.1",
+ "zod": "^4.1.12"
},
"devDependencies": {
"@react-router/dev": "^7.7.1",