add: some real data fetched from database for the song info page to replace placeholders
This commit is contained in:
parent
29b924de5d
commit
2358229ca8
4
.gitignore
vendored
4
.gitignore
vendored
@ -41,4 +41,6 @@ build/
|
||||
|
||||
docker-compose.yml
|
||||
|
||||
ucaptcha-config.yaml
|
||||
ucaptcha-config.yaml
|
||||
|
||||
temp/
|
||||
@ -25,6 +25,7 @@
|
||||
</option>
|
||||
<option name="myCustomValuesEnabled" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="HtmlUnknownTarget" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
|
||||
<inspection_tool class="SpellCheckingInspection" enabled="false" level="TYPO" enabled_by_default="false">
|
||||
<option name="processCode" value="true" />
|
||||
|
||||
@ -6,5 +6,6 @@ export default defineConfig({
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL_MAIN!
|
||||
}
|
||||
},
|
||||
tablesFilter: ["*"],
|
||||
});
|
||||
|
||||
148
packages/solid/src/components/song/Content.tsx
Normal file
148
packages/solid/src/components/song/Content.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import { Component } from "solid-js";
|
||||
import { Card, CardContent, CardMedia, Typography } from "@m3-components/solid";
|
||||
import { TabSwitcher } from "~/components/song/TabSwitcher";
|
||||
import { Staff } from "~/components/song/Staff";
|
||||
import { SongType } from "~db/outerSchema";
|
||||
|
||||
export const Content: Component<{data: SongType | null}> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<Card variant="outlined" class="w-full max-lg:rounded-none max-lg:border-none">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="relative w-full z-[2] max-lg:hidden"
|
||||
/>
|
||||
<div class="relative w-full overflow-hidden lg:hidden">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="relative w-full z-[2]"
|
||||
/>
|
||||
<div class="h-10 lg:h-0" />
|
||||
<CardMedia
|
||||
round={false}
|
||||
src={props.data?.image || ""}
|
||||
referrerpolicy="no-referrer"
|
||||
class="w-full absolute lg:hidden top-10 z-[1]"
|
||||
/>
|
||||
<span
|
||||
class="left-3 absolute bottom-14 z-10 text-sm text-white/95"
|
||||
style="text-shadow:0px 1px 1px rgb(0 0 0 / 0.2) "
|
||||
>
|
||||
赤羽 & 洛凛
|
||||
</span>
|
||||
<span
|
||||
class="left-3 absolute bottom-3 z-10 font-medium text-4xl text-white/90 "
|
||||
style="text-shadow: 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075)"
|
||||
>
|
||||
尘海绘仙缘
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-10 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
4:54
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-3 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
12,422
|
||||
<span class="ml-1 text-sm">再生</span>
|
||||
</span>
|
||||
<div class="lg:hidden w-full gradient-blur !absolute !h-32">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<CardContent class="max-lg:hidden px-7 py-6 flex flex-col gap-4">
|
||||
<Typography.Display class="leading-[2.75rem]" variant="small">
|
||||
{props.data?.name}
|
||||
</Typography.Display>
|
||||
<div class="grid grid-cols-2 grid-rows-3 gap-2">
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
演唱
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">赤羽</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
时长
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">4:28</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
投稿
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">洛凛</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
链接
|
||||
</Typography.Label>
|
||||
<Typography.Body class="flex gap-2" variant="large">
|
||||
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/">哔哩哔哩</a>
|
||||
<a href="https://vocadb.net/S/742394">VocaDB</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
发布时间
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
再生
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">1.24 万 (12,422)</Typography.Body>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="mx-1 my-6 lg:hidden">
|
||||
<TabSwitcher />
|
||||
</div>
|
||||
<article class="mt-6">
|
||||
<Typography.Headline class="mx-4" variant="medium">
|
||||
简介
|
||||
</Typography.Headline>
|
||||
<Typography.Body class="mx-4 mt-2" variant="large">
|
||||
<span class="font-medium">《尘海绘仙缘》</span>是<a href="#">洛凛</a>于
|
||||
<span>
|
||||
 2024 年 12 月 15 日
|
||||
</span>
|
||||
投稿至
|
||||
<a href="#">哔哩哔哩</a>的 <a href="#">Synthesizer V</a> 
|
||||
<span>中文</span>
|
||||
<span>原创歌曲</span>, 由<a href="#">赤羽</a>演唱。
|
||||
</Typography.Body>
|
||||
<div class="h-7" />
|
||||
<Typography.Headline class="mx-4" variant="medium">
|
||||
制作人员
|
||||
</Typography.Headline>
|
||||
<div class="mt-3 mx-1">
|
||||
<Staff num={1} name="洛凛" role="策划、作词" />
|
||||
<Staff num={2} name="鱼柳" role="作曲、编曲" />
|
||||
<Staff num={3} name="月华" role="混音" />
|
||||
<Staff num={4} name="城西阿灵" role="视频" />
|
||||
<Staff num={5} name="与嬴酌棠" role="题字" />
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
};
|
||||
51
packages/solid/src/components/song/LeftSideBar.tsx
Normal file
51
packages/solid/src/components/song/LeftSideBar.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import { Component } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { Button } from "@m3-components/solid";
|
||||
import { HomeIcon, MusicIcon } from "~/components/icons";
|
||||
import { StarBadge4, StarBadge6, StarBadge8 } from "~/components/icons/StarBadges";
|
||||
import { HistoryIcon } from "../icons/History";
|
||||
|
||||
export const LeftSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="inline-flex flex-col gap-4">
|
||||
<A href="/">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HomeIcon class="w-5 h-5 text-xl -translate-y-0.25" />
|
||||
<span>主页</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<MusicIcon class="w-5 h-5 text-xl" />
|
||||
<span>歌曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/denndou/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge4 class="w-5 h-5 text-xl" />
|
||||
<span>殿堂曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/densetsu/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge6 class="w-5 h-5 text-xl" />
|
||||
<span>传说曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/shinwa/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge8 class="w-5 h-5 text-xl" />
|
||||
<span>神话曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="../history">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HistoryIcon class="w-5 h-5 text-xl" />
|
||||
<span>页面历史</span>
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
17
packages/solid/src/components/song/RightSideBar.tsx
Normal file
17
packages/solid/src/components/song/RightSideBar.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Component } from "solid-js";
|
||||
import { ExtendedFAB } from "@m3-components/solid";
|
||||
import { EditIcon } from "~/components/icons";
|
||||
import { TabSwitcher } from "~/components/song/TabSwitcher";
|
||||
|
||||
export const RightSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="w-48 self-center 2xl:self-end flex justify-end mb-6">
|
||||
<ExtendedFAB position="unset" size="small" elevation={false} text="编辑" color="primary">
|
||||
<EditIcon />
|
||||
</ExtendedFAB>
|
||||
</div>
|
||||
<TabSwitcher />
|
||||
</>
|
||||
);
|
||||
};
|
||||
37
packages/solid/src/components/song/Staff.tsx
Normal file
37
packages/solid/src/components/song/Staff.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { Component } from "solid-js";
|
||||
import { A } from "@solidjs/router";
|
||||
import { IconButton, Typography } from "@m3-components/solid";
|
||||
import { RightArrow } from "~/components/icons/Arrow";
|
||||
|
||||
export const Staff: Component<{ name: string; role: string; num: number }> = (props) => {
|
||||
return (
|
||||
<A
|
||||
href={`/author/${props.name}/info`}
|
||||
class="group rounded-[1.25rem] hover:bg-surface-container h-16 flex items-center
|
||||
px-4 justify-between"
|
||||
>
|
||||
<div class="ml-2 flex gap-5 lg:gap-4 grow w-full">
|
||||
<span
|
||||
class="font-[IPSD] font-medium text-[2rem] text-on-surface-variant"
|
||||
style="
|
||||
-webkit-text-stroke: var(--md-sys-color-on-surface-variant);
|
||||
-webkit-text-stroke-width: 1.2px;
|
||||
-webkit-text-fill-color: transparent;"
|
||||
>
|
||||
{props.num}
|
||||
</span>
|
||||
<div class="flex flex-col gap-[3px]">
|
||||
<Typography.Body variant="large" class="text-on-surface font-medium">
|
||||
{props.name}
|
||||
</Typography.Body>
|
||||
<Typography.Label variant="large" class="text-on-surface-variant">
|
||||
{props.role}
|
||||
</Typography.Label>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton class="text-on-surface-variant opacity-0 group-hover:opacity-80 duration-200">
|
||||
<RightArrow />
|
||||
</IconButton>
|
||||
</A>
|
||||
);
|
||||
};
|
||||
@ -1,2 +1,3 @@
|
||||
import { relations } from "drizzle-orm/relations";
|
||||
import {} from "./schema";
|
||||
import { } from "./schema";
|
||||
|
||||
|
||||
@ -1,316 +1,174 @@
|
||||
import {
|
||||
pgTable,
|
||||
index,
|
||||
text,
|
||||
timestamp,
|
||||
unique,
|
||||
serial,
|
||||
bigint,
|
||||
integer,
|
||||
uniqueIndex,
|
||||
varchar,
|
||||
uuid,
|
||||
smallint,
|
||||
bigserial,
|
||||
boolean,
|
||||
interval,
|
||||
real,
|
||||
pgSequence
|
||||
} from "drizzle-orm/pg-core";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { pgTable, uniqueIndex, index, integer, bigint, varchar, text, timestamp, unique, serial, smallint, boolean, bigserial, uuid, pgSequence } from "drizzle-orm/pg-core"
|
||||
import { sql } from "drizzle-orm"
|
||||
|
||||
export const allDataIdSeq = pgSequence("all_data_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const labelingResultIdSeq = pgSequence("labeling_result_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const songsIdSeq = pgSequence("songs_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const videoSnapshotIdSeq = pgSequence("video_snapshot_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "2147483647",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
});
|
||||
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", {
|
||||
startWith: "1",
|
||||
increment: "1",
|
||||
minValue: "1",
|
||||
maxValue: "9223372036854775807",
|
||||
cache: "1",
|
||||
cycle: false
|
||||
|
||||
export const allDataIdSeq = pgSequence("all_data_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const labelingResultIdSeq = pgSequence("labeling_result_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const songsIdSeq = pgSequence("songs_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const videoSnapshotIdSeq = pgSequence("video_snapshot_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "2147483647", cache: "1", cycle: false })
|
||||
export const viewsIncrementRateIdSeq = pgSequence("views_increment_rate_id_seq", { startWith: "1", increment: "1", minValue: "1", maxValue: "9223372036854775807", cache: "1", cycle: false })
|
||||
|
||||
export const bilibiliMetadata = pgTable("bilibili_metadata", {
|
||||
id: integer().default(sql`nextval('all_data_id_seq'::regclass)`).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
bvid: varchar({ length: 12 }),
|
||||
description: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }),
|
||||
tags: text(),
|
||||
title: text(),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
|
||||
duration: integer(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`),
|
||||
status: integer().default(0).notNull(),
|
||||
coverUrl: text("cover_url"),
|
||||
}, (table) => [
|
||||
uniqueIndex("all_data_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
index("idx_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_all-data_bvid").using("btree", table.bvid.asc().nullsLast().op("text_ops")),
|
||||
index("idx_all-data_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_bili-meta_status").using("btree", table.status.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const bilibiliUser = pgTable("bilibili_user", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }).notNull(),
|
||||
username: text().notNull(),
|
||||
desc: text().notNull(),
|
||||
fans: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
unique("unq_bili-user_uid").on(table.uid),
|
||||
]);
|
||||
|
||||
export const labellingResult = pgTable("labelling_result", {
|
||||
id: integer().default(sql`nextval('labeling_result_id_seq'::regclass)`).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
label: smallint().notNull(),
|
||||
modelVersion: text("model_version").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
logits: smallint().array(),
|
||||
}, (table) => [
|
||||
index("idx_labeling_label_model-version").using("btree", table.label.asc().nullsLast().op("int2_ops"), table.modelVersion.asc().nullsLast().op("int2_ops")),
|
||||
index("idx_labeling_model-version").using("btree", table.modelVersion.asc().nullsLast().op("text_ops")),
|
||||
index("idx_labelling_aid-label").using("btree", table.aid.asc().nullsLast().op("int2_ops"), table.label.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("labeling_result_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_labelling-result_aid_model-version").using("btree", table.aid.asc().nullsLast().op("int8_ops"), table.modelVersion.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const latestVideoSnapshot = pgTable("latest_video_snapshot", {
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).primaryKey().notNull(),
|
||||
time: timestamp({ withTimezone: true, mode: 'string' }).notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
replies: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_latest-video-snapshot_time").using("btree", table.time.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_latest-video-snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
]);
|
||||
|
||||
export const videoSnapshot = pgTable("video_snapshot", {
|
||||
id: integer().default(sql`nextval('video_snapshot_id_seq'::regclass)`).notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
replies: integer().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_vid_snapshot_aid_created_at").using("btree", table.aid.asc().nullsLast().op("timestamptz_ops"), table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
]);
|
||||
|
||||
export const songs = pgTable("songs", {
|
||||
id: integer().default(sql`nextval('songs_id_seq'::regclass)`).notNull(),
|
||||
name: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: 'string' }),
|
||||
duration: integer(),
|
||||
type: smallint(),
|
||||
romanizedName: text("romanized_name"),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
neteaseId: bigint("netease_id", { mode: "number" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
deleted: boolean().default(false).notNull(),
|
||||
image: text(),
|
||||
}, (table) => [
|
||||
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const singer = pgTable("singer", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
name: text().notNull(),
|
||||
});
|
||||
|
||||
export const content = pgTable(
|
||||
"content",
|
||||
{
|
||||
pageId: text("page_id").primaryKey().notNull(),
|
||||
pageContent: text("page_content").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" }),
|
||||
deletedAt: timestamp("deleted_at", { withTimezone: true, mode: "string" })
|
||||
},
|
||||
(table) => [index("idx_content_created-at").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops"))]
|
||||
);
|
||||
export const relations = pgTable("relations", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
sourceId: bigint("source_id", { mode: "number" }).notNull(),
|
||||
sourceType: text("source_type").notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
targetId: bigint("target_id", { mode: "number" }).notNull(),
|
||||
targetType: text("target_type").notNull(),
|
||||
relation: text().notNull(),
|
||||
}, (table) => [
|
||||
index("idx_relations_source_id_source_type_relation").using("btree", table.sourceId.asc().nullsLast().op("int8_ops"), table.sourceType.asc().nullsLast().op("int8_ops"), table.relation.asc().nullsLast().op("text_ops")),
|
||||
index("idx_relations_target_id_target_type_relation").using("btree", table.targetId.asc().nullsLast().op("text_ops"), table.targetType.asc().nullsLast().op("text_ops"), table.relation.asc().nullsLast().op("text_ops")),
|
||||
unique("unq_relations").on(table.sourceId, table.sourceType, table.targetId, table.targetType, table.relation),
|
||||
]);
|
||||
|
||||
export const bilibiliUser = pgTable(
|
||||
"bilibili_user",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }).notNull(),
|
||||
username: text().notNull(),
|
||||
desc: text().notNull(),
|
||||
fans: integer().notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_bili-user_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
unique("unq_bili-user_uid").on(table.uid)
|
||||
]
|
||||
);
|
||||
export const snapshotSchedule = pgTable("snapshot_schedule", {
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
type: text(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: 'string' }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: 'string' }),
|
||||
status: text().default('pending').notNull(),
|
||||
}, (table) => [
|
||||
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_snapshot_schedule_started_at").using("btree", table.startedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
|
||||
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops")),
|
||||
]);
|
||||
|
||||
export const bilibiliMetadata = pgTable(
|
||||
"bilibili_metadata",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('all_data_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
bvid: varchar({ length: 12 }),
|
||||
description: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
uid: bigint({ mode: "number" }),
|
||||
tags: text(),
|
||||
title: text(),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: "string" }),
|
||||
duration: integer(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" }).default(sql`CURRENT_TIMESTAMP`),
|
||||
status: integer().default(0).notNull(),
|
||||
coverUrl: text("cover_url")
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("all_data_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
index("idx_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_all-data_bvid").using("btree", table.bvid.asc().nullsLast().op("text_ops")),
|
||||
index("idx_all-data_uid").using("btree", table.uid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_bili-meta_status").using("btree", table.status.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_all-data_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const classifiedLabelsHuman = pgTable(
|
||||
"classified_labels_human",
|
||||
{
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
author: uuid().notNull(),
|
||||
label: smallint().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_classified-labels-human_author").using("btree", table.author.asc().nullsLast().op("uuid_ops")),
|
||||
index("idx_classified-labels-human_created-at").using(
|
||||
"btree",
|
||||
table.createdAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const labellingResult = pgTable(
|
||||
"labelling_result",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('labeling_result_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
label: smallint().notNull(),
|
||||
modelVersion: text("model_version").notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
logits: smallint().array()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_labeling_label_model-version").using(
|
||||
"btree",
|
||||
table.label.asc().nullsLast().op("int2_ops"),
|
||||
table.modelVersion.asc().nullsLast().op("int2_ops")
|
||||
),
|
||||
index("idx_labeling_model-version").using("btree", table.modelVersion.asc().nullsLast().op("text_ops")),
|
||||
index("idx_labelling_aid-label").using(
|
||||
"btree",
|
||||
table.aid.asc().nullsLast().op("int2_ops"),
|
||||
table.label.asc().nullsLast().op("int2_ops")
|
||||
),
|
||||
uniqueIndex("labeling_result_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_labelling-result_aid_model-version").using(
|
||||
"btree",
|
||||
table.aid.asc().nullsLast().op("int8_ops"),
|
||||
table.modelVersion.asc().nullsLast().op("int8_ops")
|
||||
)
|
||||
]
|
||||
);
|
||||
|
||||
export const latestVideoSnapshot = pgTable(
|
||||
"latest_video_snapshot",
|
||||
{
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).primaryKey().notNull(),
|
||||
time: timestamp({ withTimezone: true, mode: "string" }).notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
replies: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
shares: integer().notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_latest-video-snapshot_time").using("btree", table.time.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_latest-video-snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const videoSnapshot = pgTable(
|
||||
"video_snapshot",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('video_snapshot_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
views: integer().notNull(),
|
||||
coins: integer().notNull(),
|
||||
likes: integer().notNull(),
|
||||
favorites: integer().notNull(),
|
||||
shares: integer().notNull(),
|
||||
danmakus: integer().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
replies: integer().notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_vid_snapshot_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_vid_snapshot_time").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_vid_snapshot_views").using("btree", table.views.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("video_snapshot_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const snapshotSchedule = pgTable(
|
||||
"snapshot_schedule",
|
||||
{
|
||||
id: bigserial({ mode: "bigint" }).notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
type: text(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
startedAt: timestamp("started_at", { withTimezone: true, mode: "string" }),
|
||||
finishedAt: timestamp("finished_at", { withTimezone: true, mode: "string" }),
|
||||
status: text().default("pending").notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_snapshot_schedule_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_snapshot_schedule_started_at").using(
|
||||
"btree",
|
||||
table.startedAt.asc().nullsLast().op("timestamptz_ops")
|
||||
),
|
||||
index("idx_snapshot_schedule_status").using("btree", table.status.asc().nullsLast().op("text_ops")),
|
||||
index("idx_snapshot_schedule_type").using("btree", table.type.asc().nullsLast().op("text_ops")),
|
||||
uniqueIndex("snapshot_schedule_pkey").using("btree", table.id.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const songs = pgTable(
|
||||
"songs",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('songs_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
name: text(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }),
|
||||
publishedAt: timestamp("published_at", { withTimezone: true, mode: "string" }),
|
||||
duration: integer(),
|
||||
type: smallint(),
|
||||
romanizedName: text("romanized_name"),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
neteaseId: bigint("netease_id", { mode: "number" }),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
deleted: boolean().default(false).notNull()
|
||||
},
|
||||
(table) => [
|
||||
index("idx_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_hash_songs_aid").using("hash", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_published_at").using("btree", table.publishedAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_type").using("btree", table.type.asc().nullsLast().op("int2_ops")),
|
||||
uniqueIndex("songs_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops")),
|
||||
uniqueIndex("unq_songs_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
uniqueIndex("unq_songs_netease_id").using("btree", table.neteaseId.asc().nullsLast().op("int8_ops"))
|
||||
]
|
||||
);
|
||||
|
||||
export const viewsIncrementRate = pgTable(
|
||||
"views_increment_rate",
|
||||
{
|
||||
id: integer()
|
||||
.default(sql`nextval('views_increment_rate_id_seq'::regclass)`)
|
||||
.notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
oldTime: timestamp("old_time", { withTimezone: true, mode: "string" }).notNull(),
|
||||
newTime: timestamp("new_time", { withTimezone: true, mode: "string" }).notNull(),
|
||||
oldViews: integer("old_views").notNull(),
|
||||
newViews: integer("new_views").notNull(),
|
||||
interval: interval().notNull(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true, mode: "string" })
|
||||
.default(sql`CURRENT_TIMESTAMP`)
|
||||
.notNull(),
|
||||
speed: real()
|
||||
},
|
||||
(table) => [
|
||||
uniqueIndex("unq_views-increment-rate_aid_interval").using(
|
||||
"btree",
|
||||
table.aid.asc().nullsLast().op("int8_ops"),
|
||||
table.interval.asc().nullsLast().op("int8_ops")
|
||||
),
|
||||
uniqueIndex("views_increment_rate_pkey").using("btree", table.id.asc().nullsLast().op("int4_ops"))
|
||||
]
|
||||
);
|
||||
export const classifiedLabelsHuman = pgTable("classified_labels_human", {
|
||||
id: serial().primaryKey().notNull(),
|
||||
// You can use { mode: "bigint" } if numbers are exceeding js number limitations
|
||||
aid: bigint({ mode: "number" }).notNull(),
|
||||
author: uuid().notNull(),
|
||||
label: smallint().notNull(),
|
||||
createdAt: timestamp("created_at", { withTimezone: true, mode: 'string' }).default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
}, (table) => [
|
||||
index("idx_classified-labels-human_aid").using("btree", table.aid.asc().nullsLast().op("int8_ops")),
|
||||
index("idx_classified-labels-human_author").using("btree", table.author.asc().nullsLast().op("uuid_ops")),
|
||||
index("idx_classified-labels-human_created-at").using("btree", table.createdAt.asc().nullsLast().op("timestamptz_ops")),
|
||||
index("idx_classified-labels-human_label").using("btree", table.label.asc().nullsLast().op("int2_ops")),
|
||||
]);
|
||||
|
||||
3
packages/solid/src/drizzle/outerSchema.d.ts
vendored
3
packages/solid/src/drizzle/outerSchema.d.ts
vendored
@ -1,9 +1,10 @@
|
||||
import type { InferSelectModel } from "drizzle-orm";
|
||||
import { users } from "~db/cred/schema";
|
||||
import { bilibiliMetadata, latestVideoSnapshot, videoSnapshot } from "~db/main/schema";
|
||||
import { bilibiliMetadata, latestVideoSnapshot, songs, videoSnapshot } from "~db/main/schema";
|
||||
|
||||
export type UserType = InferSelectModel<typeof users>;
|
||||
export type SensitiveUserFields = "password" | "unqId";
|
||||
export type BilibiliMetadataType = InferSelectModel<typeof bilibiliMetadata>;
|
||||
export type VideoSnapshotType = InferSelectModel<typeof videoSnapshot>;
|
||||
export type LatestVideoSnapshotType = InferSelectModel<typeof latestVideoSnapshot>;
|
||||
export type SongType = InferSelectModel<typeof songs>;
|
||||
@ -1,246 +1,63 @@
|
||||
import { Layout } from "~/components/layout";
|
||||
import { Button, Card, CardContent, CardMedia, ExtendedFAB, IconButton, Typography } from "@m3-components/solid";
|
||||
import { LeftSideBar } from "~/components/song/LeftSideBar";
|
||||
import { RightSideBar } from "~/components/song/RightSideBar";
|
||||
import { Content } from "~/components/song/Content";
|
||||
import { createAsync, query, RouteDefinition, useParams } from "@solidjs/router";
|
||||
import { dbMain } from "~/drizzle";
|
||||
import { bilibiliMetadata, songs } from "~db/main/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
import { TabSwitcher } from "~/components/song/TabSwitcher";
|
||||
import { EditIcon, HomeIcon, MusicIcon } from "~/components/icons";
|
||||
import { A } from "@solidjs/router";
|
||||
import { RightArrow } from "~/components/icons/Arrow";
|
||||
import { Component } from "solid-js";
|
||||
import { LinkIcon } from "~/components/icons/Link";
|
||||
import { StarBadge4, StarBadge6, StarBadge8 } from "~/components/icons/StarBadges";
|
||||
import { HistoryIcon } from "~/components/icons/History";
|
||||
|
||||
const Staff: Component<{ name: string; role: string; num: number }> = (props) => {
|
||||
return (
|
||||
<A
|
||||
href={`/author/${props.name}/info`}
|
||||
class="group rounded-[1.25rem] hover:bg-surface-container h-16 flex items-center
|
||||
px-4 justify-between"
|
||||
>
|
||||
<div class="ml-2 flex gap-5 lg:gap-4 grow w-full">
|
||||
<span
|
||||
class="font-[IPSD] font-medium text-[2rem] text-on-surface-variant"
|
||||
style="
|
||||
-webkit-text-stroke: var(--md-sys-color-on-surface-variant);
|
||||
-webkit-text-stroke-width: 1.2px;
|
||||
-webkit-text-fill-color: transparent;"
|
||||
>
|
||||
{props.num}
|
||||
</span>
|
||||
<div class="flex flex-col gap-[3px]">
|
||||
<Typography.Body variant="large" class="text-on-surface font-medium">
|
||||
{props.name}
|
||||
</Typography.Body>
|
||||
<Typography.Label variant="large" class="text-on-surface-variant">
|
||||
{props.role}
|
||||
</Typography.Label>
|
||||
</div>
|
||||
</div>
|
||||
<IconButton class="text-on-surface-variant opacity-0 group-hover:opacity-80 duration-200">
|
||||
<RightArrow />
|
||||
</IconButton>
|
||||
</A>
|
||||
);
|
||||
const getVideoAID = async (id: string) => {
|
||||
"use server";
|
||||
if (id.startsWith("av")) {
|
||||
return parseInt(id.slice(2));
|
||||
} else if (id.startsWith("BV")) {
|
||||
const data = await dbMain
|
||||
.select()
|
||||
.from(bilibiliMetadata)
|
||||
.where(eq(bilibiliMetadata.bvid, id));
|
||||
return data[0].aid;
|
||||
}
|
||||
else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
const Content: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<Card variant="outlined" class="w-full max-lg:rounded-none max-lg:border-none">
|
||||
<div class="relative w-full overflow-hidden ">
|
||||
<CardMedia
|
||||
round={false}
|
||||
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
|
||||
referrerpolicy="no-referrer"
|
||||
class="relative w-full z-[2]"
|
||||
/>
|
||||
<div class="h-10 lg:h-0" />
|
||||
<CardMedia
|
||||
round={false}
|
||||
src="https://i0.hdslb.com/bfs/archive/8ad220336f96e4d2ea05baada3bc04592d56b2a5.jpg"
|
||||
referrerpolicy="no-referrer"
|
||||
class="w-full absolute lg:hidden top-10 z-[1]"
|
||||
/>
|
||||
<span
|
||||
class="left-3 absolute bottom-14 z-10 text-sm text-white/95"
|
||||
style="text-shadow:0px 1px 1px rgb(0 0 0 / 0.2) "
|
||||
>
|
||||
赤羽 & 洛凛
|
||||
</span>
|
||||
<span
|
||||
class="left-3 absolute bottom-3 z-10 font-medium text-4xl text-white/90 "
|
||||
style="text-shadow: 0px 1px 0px rgb(0 0 0 / 0.075), 0px 1px 1px rgb(0 0 0 / 0.075), 0px 2px 2px rgb(0 0 0 / 0.075)"
|
||||
>
|
||||
尘海绘仙缘
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-10 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
4:54
|
||||
</span>
|
||||
<span
|
||||
class="font-[Inter] right-3 absolute bottom-3 z-10 text-xl text-white/95"
|
||||
style="text-shadow: 0px 1px 2px rgb(0 0 0 / 0.1), 0px 3px 2px rgb(0 0 0 / 0.1), 0px 4px 8px rgb(0 0 0 / 0.1)"
|
||||
>
|
||||
12,422
|
||||
<span class="ml-1 text-sm">再生</span>
|
||||
</span>
|
||||
<div class="lg:hidden w-full gradient-blur !absolute !h-32">
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
const findSongIDFromAID = async (aid: number) => {
|
||||
"use server";
|
||||
const data = await dbMain.select({
|
||||
id: songs.id,
|
||||
}).from(songs).where(eq(songs.aid, aid)).limit(1);
|
||||
return data[0].id;
|
||||
}
|
||||
|
||||
<CardContent class="max-lg:hidden px-7 py-6 flex flex-col gap-4">
|
||||
<Typography.Display class="leading-[2.75rem]" variant="small">
|
||||
尘海绘仙缘
|
||||
</Typography.Display>
|
||||
<div class="grid grid-cols-2 grid-rows-3 gap-2">
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
演唱
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">赤羽</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
时长
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">4:28</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
投稿
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">
|
||||
<a href="#">洛凛</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
链接
|
||||
</Typography.Label>
|
||||
<Typography.Body class="flex gap-2" variant="large">
|
||||
<a href="https://www.bilibili.com/video/BV1eaq9Y3EVV/">哔哩哔哩</a>
|
||||
<a href="https://vocadb.net/S/742394">VocaDB</a>
|
||||
</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
发布时间
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">2024-12-15 12:15:00</Typography.Body>
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<Typography.Label class="text-on-surface-variant" variant="large">
|
||||
再生
|
||||
</Typography.Label>
|
||||
<Typography.Body variant="large">1.24 万 (12,422)</Typography.Body>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="mx-1 my-6 lg:hidden">
|
||||
<TabSwitcher />
|
||||
</div>
|
||||
<article class="mt-6">
|
||||
<Typography.Headline class="mx-4" variant="medium">简介</Typography.Headline>
|
||||
<Typography.Body class="mx-4 mt-2" variant="large">
|
||||
<span class="font-medium">《尘海绘仙缘》</span>是<a href="#">洛凛</a>于
|
||||
<span>
|
||||
 2024 年 12 月 15 日
|
||||
</span>
|
||||
投稿至
|
||||
<a href="#">哔哩哔哩</a>的 <a href="#">Synthesizer V</a> 
|
||||
<span>中文</span>
|
||||
<span>原创歌曲</span>, 由<a href="#">赤羽</a>演唱。
|
||||
</Typography.Body>
|
||||
<div class="h-7" />
|
||||
<Typography.Headline class="mx-4" variant="medium">制作人员</Typography.Headline>
|
||||
<div class="mt-3 mx-1">
|
||||
<Staff num={1} name="洛凛" role="策划、作词" />
|
||||
<Staff num={2} name="鱼柳" role="作曲、编曲" />
|
||||
<Staff num={3} name="月华" role="混音" />
|
||||
<Staff num={4} name="城西阿灵" role="视频" />
|
||||
<Staff num={5} name="与嬴酌棠" role="题字" />
|
||||
</div>
|
||||
</article>
|
||||
</>
|
||||
);
|
||||
};
|
||||
const getSongInfo = query(async (songID: number) => {
|
||||
"use server";
|
||||
const data = await dbMain.select().from(songs).where(eq(songs.id, songID));
|
||||
return data[0] || null;
|
||||
}, "songs");
|
||||
|
||||
const RightSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="w-48 self-center 2xl:self-end flex justify-end mb-6">
|
||||
<ExtendedFAB position="unset" size="small" elevation={false} text="编辑" color="primary">
|
||||
<EditIcon />
|
||||
</ExtendedFAB>
|
||||
</div>
|
||||
<TabSwitcher />
|
||||
</>
|
||||
);
|
||||
};
|
||||
const getSongInfoFromID = query(async (id: string) => {
|
||||
"use server";
|
||||
const aid = await getVideoAID(id);
|
||||
if (!aid && parseInt(id)) {
|
||||
return getSongInfo(parseInt(id));
|
||||
}
|
||||
else if (!aid) {
|
||||
return null;
|
||||
}
|
||||
const songID = await findSongIDFromAID(aid);
|
||||
return getSongInfo(songID);
|
||||
}, "songsRaw")
|
||||
|
||||
export const route = {
|
||||
preload: ({ params }) => getSongInfoFromID(params.id)
|
||||
} satisfies RouteDefinition;
|
||||
|
||||
const LeftSideBar: Component = () => {
|
||||
return (
|
||||
<>
|
||||
<div class="inline-flex flex-col gap-4">
|
||||
<A href="/">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HomeIcon class="w-5 h-5 text-xl -translate-y-0.25" />
|
||||
<span>主页</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<MusicIcon class="w-5 h-5 text-xl" />
|
||||
<span>歌曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/denndou/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge4 class="w-5 h-5 text-xl" />
|
||||
<span>殿堂曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/densetsu/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge6 class="w-5 h-5 text-xl" />
|
||||
<span>传说曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/milestone/shinwa/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<StarBadge8 class="w-5 h-5 text-xl" />
|
||||
<span>神话曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/singer/赤羽/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<LinkIcon class="w-5 h-5 text-xl" />
|
||||
<span>赤羽的其它歌曲</span>
|
||||
</Button>
|
||||
</A>
|
||||
<A href="/songs">
|
||||
<Button variant="outlined" class="gap-1 items-center" size="extra-small">
|
||||
<HistoryIcon class="w-5 h-5 text-xl" />
|
||||
<span>页面历史</span>
|
||||
</Button>
|
||||
</A>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Info() {
|
||||
const params = useParams();
|
||||
const info = createAsync(() => getSongInfoFromID(params.id));
|
||||
return (
|
||||
<Layout>
|
||||
<title>尘海绘仙缘 - 歌曲信息 - 中 V 档案馆</title>
|
||||
@ -252,7 +69,7 @@ export default function Info() {
|
||||
<LeftSideBar />
|
||||
</nav>
|
||||
<main class="mb-24">
|
||||
<Content />
|
||||
<Content data={info() || null}/>
|
||||
</main>
|
||||
<div class="top-32 hidden lg:flex self-start sticky flex-col pb-12 px-6">
|
||||
<RightSideBar />
|
||||
|
||||
Loading…
Reference in New Issue
Block a user