feature: frontend for database showing page

basic frontend for submit & edit page
This commit is contained in:
alikia2x 2024-07-12 00:34:57 +08:00
parent a3064385c2
commit a9b1a3f9cd
13 changed files with 352 additions and 32 deletions

1
.gitignore vendored
View File

@ -8,3 +8,4 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
data/pending

View File

@ -1,5 +1,89 @@
{
"id": "BV1Xp421o7hr",
"name": "大哉乾元",
"singer": "洛天依",
"url": "https://www.bilibili.com/video/BV1Xp421o7hr",
"singer": [
"洛天依"
],
"producer": "哈米伦的弄笛者",
"tuning": [
"Creuzer"
],
"lyricist": [
"非桥段"
],
"composer": [
"胡多多"
],
"arranger": [
"胡多多"
],
"mixing": [
"明宣May"
],
"pv": [
"宇言DX",
"顺-其自然",
"Kensei",
"残垣留梦",
"Bung Kon",
"XiroKyo",
"学渣的本愿",
"La pazienza",
"妮可",
"高米迪战士",
"蓉蓉",
"妖孽"
],
"illustrator": [
"Clay菌",
"吃咖喱的poi",
"彩虹咸鱼YA",
"鵺心NUECO",
"知木绕林",
"长耳朵喵舔毛狂魔",
"龙川川川子",
"洛橘",
"脑子",
"依枕云屏",
"鷁-",
"一只勺子spoon",
"腥红",
"叹云洲",
"顾奔波",
"辚古",
"Small小_小小白",
"晓溪",
"羽虫",
"千山白",
"火锅加番茄",
"大写的TY"
],
"harmony": [
"言和",
"乐正绫",
"乐正龙牙",
"墨清弦",
"徵羽摩柯"
],
"instruments": [
"墨韵",
"浑元",
"王佳男",
"李乐",
"李弦月",
"小陈同学"
],
"songURL": [
"https://assets.aquavox.app/public/BV1Xp421o7hr.mp3"
],
"coverURL": [
"https://assets.aquavox.app/public/BV1Xp421o7hr.jpg",
"https://ipfs.a2x.pub/ipfs/QmY76zgNJEerm75tiwoWaFg4Uq2NrJQiTjHiTWK6X2VLyb?filename=BV1Xp421o7hr.jpg"
],
"duration": 259.54,
"views": 2733456,
"publishTime": "2024-02-09 20:31:23",
"updateTime": "2024-07-12 00:23:22",
"lyric": "[ti: 大哉乾元]\n[ar: 洛天依]\n[al: 2024哔哩哔哩拜年纪]\n[tool: 歌词滚动姬 https://lrc-maker.github.io]\n[length: 04:19.536]\n[00:05.630] 经起幽明 悟处通玄\n[00:09.680] 首窥龙堑 见岳见渊\n[00:13.640] 道不善宣 义不善绻\n[00:17.270] 源流万世 大哉乾元!\n[00:21.837]\n[00:36.390] 不曾闻日月争辉\n[00:38.170] 坎离复往 立下恒规\n[00:40.330] 照东南 有坤徇乾 承西北\n[00:43.680] 天道自昆仑巍巍\n[00:45.590] 翻起华夏巽震艮兑\n[00:47.810] 万象予万灵得见 两相盈岁\n[00:51.210] 潜龙长生应紫微\n[00:53.000] 惟向四方五气寻遂\n[00:55.200] 燧火旁八卦百草揆经纬\n[00:58.220] 正位 纪天下一归\n[01:00.370] 不消祈天退水\n[01:02.590] 初难知一念一决生龙髓\n[01:05.930] 百家注龙慧 千军起龙威 砥淬\n[01:10.260] 妙笔生文穗 罡风抚长麾\n[01:13.030] 始见龙形汇 以天田冲腾直向九陲\n[01:20.490] 龙震于疆 万里宁壤 天地皆可往\n[01:24.140] 龙秀于象 引仙来访 诗蜀道河江\n[01:27.940] 龙明于章 执笔成鉴 映五千煌煌\n[01:31.820] 不独九州五岳 帝王将相见苍茫\n[01:35.340] 龙泽于汤 唤水筑乡 单舟见京杭\n[01:39.040] 龙健于常 百音同讲 道一种炎黄\n[01:42.740] 龙景于康 见之庙堂 亦显于曲坊\n[01:46.580] 不劳此间祥云瑞兽频频诰春长!\n[01:57.280] 干支移晷又几回\n[01:59.500] 揽尽天骄襄助一醉\n[02:01.790] 虽万言竟道不尽无字碑\n[02:04.680] 临渊乾乾 君子催\n[02:06.600] 或跃 无咎相随\n[02:09.130] 同为龙 却与往昔不连讳\n[02:12.390] 且待飞龙归 簸却沧溟水 如沸\n[02:16.700] 有龙掸风雷 见首不见尾\n[02:19.540] 苏苏万物蜕 证元亨利贞变易轮回\n[02:27.080] 龙华于旸 红旗漫卷 新水濯旧隍\n[02:30.740] 龙泰于霜 烽烟消长 更赳赳昂昂\n[02:34.430] 龙温于壮 留待潺潺 驰涌成泱泱\n[02:38.260] 好教流光紫极 鹊渡银潢伴流觞\n[02:41.660] 龙韧于刚 龙吟激荡 云止聆佳响\n[02:45.490] 龙德于昌 喜见船马 纵横间丰仓\n[02:49.150] 龙眷于邦 情习众广 仍化为一方\n[02:52.930] 其妙错综复杂 不孤兵车付一匡!\n[02:56.602]\n[03:40.980] 此去向东 瀚海游龙 滔滔几万重\n[03:44.620] 一跃破空 乘风逐虹 猎猎青云中\n[03:48.250] 天音入梦 扶摇上穹 矫矫游星宫\n[03:52.080] 犹念神州谷稻耕耘收藏守时无?\n[03:55.670] 一息一动 似异似同 无之以为用\n[03:59.240] 天地辰龙 龙生九种 但两爻合共\n[04:03.130] 假逢童蒙 欲解懵懂 何处有真龙\n[04:06.830] 只道「大哉乾元」秩秩幽幽必然中\n[04:10.350] 也道「大哉乾元」切切实实一言中!\n[04:14.693]"
}

View File

@ -168,16 +168,6 @@
</div>
<div class="relative top-52 h-6 flex">
<img class="scale-75" src="/volumeDown.svg" alt="最小音量" />
<!-- <input
class="mx-2 progress-bar shadow-md !translate-y-[-50%] !top-1/2"
bind:this={volumeBar}
on:input={volumeBarOnChange}
type="range"
min="0"
max="1"
step="0.01"
value={$userAdjustingProgress ? volumeBar.value : volume}
/> -->
<div
class="progress-bar shadow-md !top-1/2 !translate-y-[-50%]"
on:click={(e) => volumeBarOnChange(e)}

8
src/lib/formatViews.ts Normal file
View File

@ -0,0 +1,8 @@
export function formatViews(num: number): string {
if (num >= 10000) {
const formattedNum = Math.floor(num / 1000) / 10; // 向下保留1位小数
return `${formattedNum}`;
} else {
return num.toString();
}
}

View File

@ -1,18 +1,23 @@
interface MusicMetadata {
id: string;
name: string;
signer?: string | string[];
producer?: string;
lyric?: string;
tuning?: string | string[];
lyricist?: string | string[];
composer?: string | string[];
arranger?: string | string[];
mixing?: string | string[];
video?: string | string[];
illustrator?: string | string[];
songURL?: string;
duration?: number;
publishTime?: string;
views?: number;
updateTime?: string;
url: string;
singer: string[];
producer: string | null;
tuning: string[];
lyricist: string[];
composer: string[];
arranger: string[];
mixing: string[];
pv: string[];
illustrator: string[];
harmony: string[];
instruments: string[];
songURL: string[];
coverURL: string[];
duration: number | null;
views: number | null;
publishTime: string | null;
updateTime: string | null;
lyric: string | null;
}

13
src/lib/songUpdateTime.ts Normal file
View File

@ -0,0 +1,13 @@
export function getCurrentFormattedDateTime() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0'); // getMonth() is zero-based
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}

View File

@ -1 +0,0 @@
export const ssr = false;

View File

@ -1,3 +1,4 @@
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
import { json, error } from '@sveltejs/kit';
import fs from 'fs';
@ -19,7 +20,11 @@ export async function POST({ params, request }) {
}
const filePath = `./data/pending/${params.id}-${timeStamp}.json`;
const data: MusicMetadata = await request.json();
data.updateTime = new Date().getTime().toString();
fs.writeFileSync(filePath, JSON.stringify(data));
return json({});
data.updateTime = getCurrentFormattedDateTime();
fs.writeFileSync(filePath, JSON.stringify(data, null, 4));
return json({
"message": "successfully created"
}, {
status: 201
});
}

View File

@ -1,3 +1,85 @@
<script lang="ts">
import formatDuration from '$lib/formatDuration';
import { formatViews } from '$lib/formatViews';
import { onMount } from 'svelte';
let songList: MusicMetadata[] = [];
onMount(async () => {
fetch('/api/database/songs')
.then((response) => response.json())
.then((data) => {
songList = data;
})
.catch((error) => {
console.log(error);
return [];
});
});
</script>
<svelte:head>
<title>AquaVox 音乐数据库</title>
</svelte:head>
<div>
<h1>AquaVox 音乐数据库</h1>
</div>
<div class="flex justify-between items-center h-20 mb-8">
<h1>AquaVox 音乐数据库</h1>
<a
href="/database/submit"
class="h-12 w-24 border-black dark:border-white border-2 flex items-center justify-center rounded-lg"
>提交新曲</a
>
</div>
<div class="grid">
{#each songList as song}
<a
class="relative w-56 h-56 bg-zinc-300 dark:bg-zinc-600 rounded-lg overflow-hidden
shadow-lg cursor-pointer"
href={song.url}
>
<div
class="absolute top-0 left-0 w-full h-full duration-100
z-10 opacity-0 hover:opacity-100 bg-[rgba(0,0,0,0.15)]"
>
<a
class="brightness-125 absolute top-2 right-2 w-8 h-8 rounded-full
bg-[rgba(49,49,49,0.7)] backdrop-blur-lg z-10 hover:bg-red-500"
href={`/database/edit/${song.id}`}
>
<img class="relative w-4 h-4 top-2 left-2 scale-90" src="/edit.svg" alt="编辑" />
</a>
</div>
<img src={song.coverURL[0]} class="w-56 h-56" alt="" />
<div
class="absolute bottom-0 w-full h-28 backdrop-blur-xl"
style="mask-image: linear-gradient(to top, black 50%, transparent);"
>
<div class="absolute bottom-0 w-full h-16 pl-2">
<span
class="font-semibold text-2xl text-white"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);">{song.name}</span
>
<br />
<span
class="relative inline-block whitespace-nowrap text-white w-40
overflow-hidden text-ellipsis"
style="text-shadow: 0px 0px 4px rgba(65, 65, 65, .6);"
>
{song.singer.join(', ')}
</span>
<div class="absolute right-2 bottom-2 text-right">
{#if song.duration}
<span>{formatDuration(song.duration)}</span>
{/if}
<br />
{#if song.views}
<span>{formatViews(song.views)}</span>
{/if}
</div>
</div>
</div>
</a>
{/each}
</div>
</div>

View File

@ -0,0 +1,24 @@
/** @type {import('./$types').PageLoad} */
import fs from 'fs';
export function load({ params }) {
const filePath = `./data/song/${params.id}.json`;
if (!fs.existsSync(filePath)) {
return {
songData: null
}
}
const dataBuffer = fs.readFileSync(filePath);
try {
const data = JSON.parse(dataBuffer.toString());
return {
songData: data
};
}
catch {
return {
songData: null
}
}
}

View File

@ -0,0 +1,37 @@
<script lang="ts">
/** @type {import('./$types').PageData} */
export let data;
import { page } from '$app/stores';
const songID = $page.params.id;
let editingData: string = JSON.stringify(data.songData, null, 8);
async function submit() {
fetch(`/api/database/song/${songID}`, {
method: "POST",
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json'
},
body: editingData
})
.catch((error) => {
console.log(error);
return [];
});
}
</script>
<svelte:head>
<title>建议编辑: {data.songData.name} ({songID})</title>
</svelte:head>
<h1>建议编辑: {data.songData.name} ({songID})</h1>
<textarea bind:value={editingData} class="dark:bg-zinc-600 w-full min-h-[30rem] mt-6" />
<button
class="mt-4 h-12 w-24 border-black dark:border-white border-2 flex items-center justify-center rounded-lg"
on:click={() => {
submit();
}}>提交</button
>

View File

@ -0,0 +1,61 @@
<script lang="ts">
import { page } from '$app/stores';
import { getCurrentFormattedDateTime } from '$lib/songUpdateTime';
let templateSongData: MusicMetadata = {
id: '',
name: '',
url: '',
singer: [],
producer: '',
tuning: [],
lyricist: [],
composer: [],
arranger: [],
mixing: [],
pv: [],
illustrator: [],
harmony: [],
instruments: [],
songURL: [],
coverURL: [],
duration: null,
views: null,
publishTime: null,
updateTime: getCurrentFormattedDateTime(),
lyric: null
};
let editingData: string = JSON.stringify(templateSongData, null, 8);
async function submit() {
fetch(`/api/database/song/${templateSongData.id}`, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: editingData
}).catch((error) => {
console.log(error);
return [];
});
}
</script>
<svelte:head>
<title>提交新曲</title>
</svelte:head>
<h1>提交新曲</h1>
<textarea bind:value={editingData} class="dark:bg-zinc-600 w-full min-h-[36rem] mt-6" />
<button
class="mt-4 h-12 w-24 border-black dark:border-white border-2 flex items-center justify-center rounded-lg"
on:click={() => {
submit();
}}>提交</button
>
<div class="relative h-[50vh]">
</div>

11
static/edit.svg Normal file
View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generator: Apple Native CoreSVG 232.5-->
<!DOCTYPE svg
PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="20.5" height="17.0234">
<g>
<rect height="17.0234" opacity="0" width="20.5" x="0" y="0"/>
<path d="M13.7656 17.0234C15.0469 17.0234 16.0938 15.9766 16.0938 14.6953C16.0938 13.4219 15.0469 12.375 13.7656 12.375C12.4922 12.375 11.4453 13.4219 11.4453 14.6953C11.4453 15.9766 12.4922 17.0234 13.7656 17.0234ZM13.7656 15.9141C13.0859 15.9141 12.5547 15.375 12.5547 14.6953C12.5547 14.0078 13.0859 13.4766 13.7656 13.4766C14.4531 13.4766 14.9844 14.0078 14.9844 14.6953C14.9844 15.375 14.4531 15.9141 13.7656 15.9141ZM12.1562 14L0.695312 14C0.304688 14 0 14.3047 0 14.6953C0 15.0781 0.304688 15.3828 0.695312 15.3828L12.1562 15.3828ZM19.4297 14L15.4922 14L15.4922 15.3828L19.4297 15.3828C19.7891 15.3828 20.0938 15.0781 20.0938 14.6953C20.0938 14.3047 19.7891 14 19.4297 14ZM6.52344 10.8594C7.80469 10.8594 8.85156 9.82031 8.85156 8.53906C8.85156 7.25781 7.80469 6.21094 6.52344 6.21094C5.25 6.21094 4.20312 7.25781 4.20312 8.53906C4.20312 9.82031 5.25 10.8594 6.52344 10.8594ZM6.52344 9.75781C5.84375 9.75781 5.3125 9.21094 5.3125 8.53125C5.3125 7.84375 5.84375 7.3125 6.52344 7.3125C7.21094 7.3125 7.74219 7.84375 7.74219 8.53125C7.74219 9.21094 7.21094 9.75781 6.52344 9.75781ZM0.664062 7.84375C0.304688 7.84375 0 8.14844 0 8.53125C0 8.92188 0.304688 9.22656 0.664062 9.22656L4.82031 9.22656L4.82031 7.84375ZM19.3984 7.84375L8.14062 7.84375L8.14062 9.22656L19.3984 9.22656C19.7891 9.22656 20.0938 8.92188 20.0938 8.53125C20.0938 8.14844 19.7891 7.84375 19.3984 7.84375ZM13.7656 4.6875C15.0469 4.6875 16.0938 3.64062 16.0938 2.35938C16.0938 1.08594 15.0469 0.0390625 13.7656 0.0390625C12.4922 0.0390625 11.4453 1.08594 11.4453 2.35938C11.4453 3.64062 12.4922 4.6875 13.7656 4.6875ZM13.7656 3.57812C13.0859 3.57812 12.5547 3.03906 12.5547 2.35938C12.5547 1.67188 13.0859 1.14062 13.7656 1.14062C14.4531 1.14062 14.9844 1.67188 14.9844 2.35938C14.9844 3.03906 14.4531 3.57812 13.7656 3.57812ZM12.1797 1.67969L0.695312 1.67969C0.304688 1.67969 0 1.98438 0 2.375C0 2.75781 0.304688 3.0625 0.695312 3.0625L12.1797 3.0625ZM19.4297 1.67969L15.4062 1.67969L15.4062 3.0625L19.4297 3.0625C19.7891 3.0625 20.0938 2.75781 20.0938 2.375C20.0938 1.98438 19.7891 1.67969 19.4297 1.67969Z" fill="#ffffff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB