add: complete docker config

This commit is contained in:
alikia2x (寒寒) 2025-04-29 06:12:44 +08:00
parent 784939074a
commit 43b52dee0b
Signed by: alikia2x
GPG Key ID: 56209E0CCD8420C6
22 changed files with 277 additions and 169 deletions

3
.gitignore vendored
View File

@ -91,5 +91,6 @@ model/
*.db
*.sqlite
*.sqlite3
db/
data/
docker-compose.yml

30
Dockerfile.frontend Normal file
View File

@ -0,0 +1,30 @@
FROM oven/bun AS bun-builder
WORKDIR /app
COPY ./packages/core ./core
COPY ./packages/frontend/package.json ./packages/frontend/bun.lock ./packages/frontend/tsconfig.json ./packages/frontend/astro.config.mjs ./frontend/
WORKDIR frontend
RUN bun install
COPY ./packages/frontend/ .
RUN bun run build
FROM node:lts-alpine
WORKDIR /app/frontend
COPY --from=bun-builder /app/frontend/dist ./dist
COPY --from=bun-builder /app/frontend/node_modules ./node_modules
COPY --from=bun-builder /app/frontend/package.json ./package.json
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]

10
package.json Normal file
View File

@ -0,0 +1,10 @@
{
"name": "cvsa",
"version": "2.13.22",
"private": false,
"type": "module",
"workspaces": [
"packages/frontend",
"packages/core"
]
}

View File

@ -1,6 +1,4 @@
import postgres from "postgres";
import { postgresConfigNpm } from "./config";
const sql = postgres(postgresConfigNpm);
export default sql;
export const sql = postgres(postgresConfigNpm);

View File

@ -1,4 +1,4 @@
import sql from "./db";
import { sql } from "./db";
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
export async function getVideoSnapshots(

View File

@ -3,7 +3,7 @@ import Argon2id from "@rabbit-company/argon2id";
import { object, string, ValidationError } from "yup";
import type { Context } from "hono";
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
import sql from "./db/db.ts";
import { sql } from "./db/db.ts";
import { ErrorResponse, StatusResponse } from "./schema";
const RegistrationBodySchema = object({

View File

@ -34,7 +34,6 @@ async function insertVideoSnapshot(data: VideoInfoData) {
}
export const videoInfoHandler = createHandlers(async (c: ContextType) => {
const client = c.get("db");
try {
const id = await idSchema.validate(c.req.param("id"));
let videoId: string | number = id as string;
@ -64,7 +63,7 @@ export const videoInfoHandler = createHandlers(async (c: ContextType) => {
await redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result));
await insertVideoSnapshot(client, result);
await insertVideoSnapshot(result);
return c.json(result);
} catch (e) {

View File

@ -4,9 +4,13 @@
"": {
"dependencies": {
"chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0",
"winston": "^3.17.0",
},
"devDependencies": {
"@types/ioredis": "^5.0.0",
},
},
},
"packages": {
@ -14,12 +18,18 @@
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@ioredis/commands": ["@ioredis/commands@1.2.0", "", {}, "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg=="],
"@types/ioredis": ["@types/ioredis@5.0.0", "", { "dependencies": { "ioredis": "*" } }, "sha512-zJbJ3FVE17CNl5KXzdeSPtdltc4tMT3TzC6fxQS0sQngkbFZ6h+0uTafsRqu+eSLIugf6Yb0Ea0SUuRr42Nk9g=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
@ -30,6 +40,10 @@
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
@ -38,12 +52,18 @@
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ioredis": ["ioredis@5.6.1", "", { "dependencies": { "@ioredis/commands": "^1.1.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", "lodash.defaults": "^4.2.0", "lodash.isarguments": "^3.1.0", "redis-errors": "^1.2.0", "redis-parser": "^3.0.0", "standard-as-callback": "^2.1.0" } }, "sha512-UxC0Yv1Y4WRJiGQxQkP0hfdL0/5/6YvdfOOClRgJ0qppSarkhneSa6UvkMkms0AkdGimSH3Ikqm+6mkMmX7vGA=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"lodash.defaults": ["lodash.defaults@4.2.0", "", {}, "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ=="],
"lodash.isarguments": ["lodash.isarguments@3.1.0", "", {}, "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
@ -52,6 +72,10 @@
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
"redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
@ -60,6 +84,8 @@
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],

View File

@ -0,0 +1,4 @@
import postgres from "postgres";
import { postgresConfigNpm } from "./pgConfigNew";
export const sql = postgres(postgresConfigNpm);

View File

@ -0,0 +1,38 @@
const requiredEnvVars = ["DB_HOST", "DB_NAME", "DB_USER", "DB_PASSWORD", "DB_PORT", "DB_NAME_CRED"];
const unsetVars = requiredEnvVars.filter((key) => process.env[key] === undefined);
if (unsetVars.length > 0) {
throw new Error(`Missing required environment variables: ${unsetVars.join(", ")}`);
}
const databaseHost = process.env["DB_HOST"]!;
const databaseName = process.env["DB_NAME"];
const databaseNameCred = process.env["DB_NAME_CRED"]!;
const databaseUser = process.env["DB_USER"]!;
const databasePassword = process.env["DB_PASSWORD"]!;
const databasePort = process.env["DB_PORT"]!;
export const postgresConfig = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseName,
user: databaseUser,
password: databasePassword
};
export const postgresConfigNpm = {
host: databaseHost,
port: parseInt(databasePort),
database: databaseName,
username: databaseUser,
password: databasePassword
};
export const postgresConfigCred = {
hostname: databaseHost,
port: parseInt(databasePort),
database: databaseNameCred,
user: databaseUser,
password: databasePassword
};

View File

@ -10,6 +10,8 @@
"chalk": "npm:chalk",
"winston": "npm:winston",
"logform": "npm:logform",
"@core/": "./"
"@core/": "./",
"child_process": "node:child_process",
"util": "node:util"
}
}

View File

@ -2,8 +2,12 @@ import logger from "log/logger.ts";
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
import { SlidingWindow } from "mq/slidingWindow.ts";
import { redis } from "db/redis.ts";
import Redis from "ioredis";
import { SECOND } from "$std/datetime/constants.ts";
import { ReplyError } from "ioredis";
import { SECOND } from "../const/time.ts";
import { execFile } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(execFile);
interface Proxy {
type: string;
@ -99,7 +103,7 @@ class NetworkDelegate {
await this.providerLimiters[providerLimiterId]?.trigger();
} catch (e) {
const error = e as Error;
if (e instanceof Redis.ReplyError) {
if (e instanceof ReplyError) {
logger.error(error, "redis");
}
logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest");
@ -209,7 +213,7 @@ class NetworkDelegate {
return providerAvailable && proxyAvailable;
} catch (e) {
const error = e as Error;
if (e instanceof Redis.ReplyError) {
if (e instanceof ReplyError) {
logger.error(error, "redis");
return false;
}
@ -239,8 +243,7 @@ class NetworkDelegate {
private async alicloudFcRequest<R>(url: string, region: string): Promise<R> {
try {
const decoder = new TextDecoder();
const output = await new Deno.Command("aliyun", {
args: [
const output = await execAsync("aliyun", [
"fc",
"POST",
`/2023-03-30/functions/proxy-${region}/invocations`,
@ -258,9 +261,8 @@ class NetworkDelegate {
"10",
"--profile",
`CVSA-${region}`,
],
}).output();
const out = decoder.decode(output.stdout);
])
const out = output.stdout;
const rawData = JSON.parse(out);
if (rawData.statusCode !== 200) {
// noinspection ExceptionCaughtLocallyJS

View File

@ -1,7 +1,11 @@
{
"dependencies": {
"chalk": "^5.4.1",
"ioredis": "^5.6.1",
"logform": "^2.7.0",
"winston": "^3.17.0"
},
"devDependencies": {
"@types/ioredis": "^5.0.0"
}
}

View File

@ -9,7 +9,7 @@
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"paths": {
"@core/*": ["../core/*"]
"@core/*": ["./*"]
},
"allowSyntheticDefaultImports": true,
"allowImportingTsExtensions": true,

View File

@ -1,27 +0,0 @@
FROM oven/bun AS bun-builder
WORKDIR /app
COPY package.json bun.lock tsconfig.json astro.config.mjs ./
RUN bun install
COPY . .
RUN bun run build
FROM node:lts-alpine
WORKDIR /app
COPY --from=bun-builder /app/dist ./dist
COPY --from=bun-builder /app/node_modules ./node_modules
COPY --from=bun-builder /app/package.json ./package.json
ENV HOST=0.0.0.0
ENV PORT=4321
EXPOSE 4321
CMD ["node", "dist/server/entry.mjs"]

View File

@ -0,0 +1,8 @@
import { sql } from "@core/db/dbNew";
export async function aidExists(aid: number) {
const res = await sql`
SELECT 1 FROM bilibili_metadata WHERE aid = ${aid}
`;
return res.length > 0;
}

View File

@ -0,0 +1,15 @@
import { sql } from "@core/db/dbNew";
export async function getAidFromBV(bv: string) {
const res = await sql`
SELECT aid FROM bilibili_metadata WHERE bvid = ${bv}
`
if (res.length <= 0) {
return null;
}
const row = res[0];
if (row && row.aid) {
return Number(row.aid);
}
return null;
}

View File

@ -0,0 +1,15 @@
import { sql } from "@core/db/dbNew";
export async function getVideoMetadata(aid: number) {
const res = await sql`
SELECT * FROM bilibili_metadata WHERE aid = ${aid}
`;
if (res.length <= 0) {
return null;
}
const row = res[0];
if (row) {
return row;
}
return {};
}

View File

@ -0,0 +1,11 @@
import { sql } from "@core/db/dbNew";
export async function getAllSnapshots(aid: number) {
const res = await sql`
SELECT * FROM video_snapshot WHERE aid = ${aid} ORDER BY created_at DESC
`;
if (res.length <= 0) {
return null;
}
return res;
}

View File

@ -1,16 +1,19 @@
---
import Layout from "@layouts/Layout.astro";
import TitleBar from "@components/TitleBar.astro";
import pg from "pg";
import { format } from 'date-fns';
import { zhCN } from 'date-fns/locale';
import { format } from "date-fns";
import { zhCN } from "date-fns/locale";
import MetadataRow from "@components/InfoPage/MetadataRow.astro";
import { getAllSnapshots } from "src/db/snapshots/getAllSnapshots";
import { getAidFromBV } from "src/db/bilibili_metadata/getAidFromBV";
import { getVideoMetadata } from "src/db/bilibili_metadata/getVideoMetadata";
import { aidExists as idExists } from "src/db/bilibili_metadata/aidExists";
const databaseHost = import.meta.env.DB_HOST
const databaseName = import.meta.env.DB_NAME
const databaseUser = import.meta.env.DB_USER
const databasePassword = import.meta.env.DB_PASSWORD
const databasePort = import.meta.env.DB_PORT
const databaseHost = import.meta.env.DB_HOST;
const databaseName = import.meta.env.DB_NAME;
const databaseUser = import.meta.env.DB_USER;
const databasePassword = import.meta.env.DB_PASSWORD;
const databasePort = import.meta.env.DB_PORT;
const postgresConfig = {
hostname: databaseHost,
@ -20,54 +23,9 @@ const postgresConfig = {
password: databasePassword,
};
console.log(postgresConfig)
console.log(postgresConfig);
// 路由参数
const { id } = Astro.params;
const { Client } = pg;
const client = new Client(postgresConfig);
await client.connect();
// 数据库查询函数
async function getVideoMetadata(aid: number) {
const res = await client.query("SELECT * FROM bilibili_metadata WHERE aid = $1", [aid]);
if (res.rows.length <= 0) {
return null;
}
const row = res.rows[0];
if (row) {
return row;
}
return {};
}
async function getVideoSnapshots(aid: number) {
const res = await client.query("SELECT * FROM video_snapshot WHERE aid = $1 ORDER BY created_at DESC", [
aid,
]);
if (res.rows.length <= 0) {
return null;
}
return res.rows;
}
async function getAidFromBV(bv: string) {
const res = await client.query("SELECT aid FROM bilibili_metadata WHERE bvid = $1" +
"", [bv]);
if (res.rows.length <= 0) {
return null;
}
const row = res.rows[0];
if (row && row.aid) {
return Number(row.aid);
}
return null;
}
async function idExists(aid: number) {
const res = await client.query("SELECT COUNT(*) FROM bilibili_metadata WHERE aid = $1", [aid]);
return res.rows[0].count > 0;
}
async function getVideoAid(id: string) {
if (id.startsWith("av")) {
@ -78,27 +36,22 @@ async function getVideoAid(id: string) {
return parseInt(id);
}
// 获取数据
if (!id) {
Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 });
}
const aid = await getVideoAid(id);
if (!aid || isNaN(aid)) {
Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 });
}
const aidExists = await idExists(aid);
if (!aidExists) {
Astro.response.status = 404;
client.end();
return new Response(null, { status: 404 });
}
const videoInfo = await getVideoMetadata(aid);
const snapshots = await getVideoSnapshots(aid);
client.end();
const snapshots = await getAllSnapshots(aid);
interface Snapshot {
created_at: Date;
@ -117,7 +70,9 @@ interface Snapshot {
<TitleBar />
<main class="flex flex-col items-center min-h-screen gap-8 mt-10 md:mt-6 relative z-0 overflow-x-auto pb-8">
<div class="w-full lg:max-w-4xl lg:mx-auto lg:p-6">
<h1 class="text-2xl font-medium ml-2 mb-4">视频信息: <a href={`https://www.bilibili.com/video/av${aid}`} class="underline ">av{aid}</a></h1>
<h1 class="text-2xl font-medium ml-2 mb-4">
视频信息: <a href={`https://www.bilibili.com/video/av${aid}`} class="underline">av{aid}</a>
</h1>
<div class="mb-6">
<h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2>
@ -131,9 +86,19 @@ interface Snapshot {
<MetadataRow title="描述" description={videoInfo?.description} />
<MetadataRow title="UID" description={videoInfo?.uid} />
<MetadataRow title="标签" description={videoInfo?.tags} />
<MetadataRow title="发布时间" description={format(new Date(videoInfo?.published_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/>
<MetadataRow
title="发布时间"
description={format(new Date(videoInfo?.published_at), "yyyy-MM-dd HH:mm:ss", {
locale: zhCN,
})}
/>
<MetadataRow title="时长 (秒)" description={videoInfo?.duration} />
<MetadataRow title="创建时间" description={format(new Date(videoInfo?.created_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/>
<MetadataRow
title="创建时间"
description={format(new Date(videoInfo?.created_at), "yyyy-MM-dd HH:mm:ss", {
locale: zhCN,
})}
/>
<MetadataRow title="封面" description={videoInfo?.cover_url} />
</tbody>
</table>
@ -142,7 +107,8 @@ interface Snapshot {
<div>
<h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2>
{snapshots && snapshots.length > 0 ? (
{
snapshots && snapshots.length > 0 ? (
<div class="overflow-x-auto px-2">
<table class="table-auto w-full">
<thead>
@ -160,7 +126,11 @@ interface Snapshot {
<tbody>
{snapshots.map((snapshot: Snapshot) => (
<tr>
<td class="border dark:border-zinc-500 px-4 py-2">{format(new Date(snapshot.created_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}</td>
<td class="border dark:border-zinc-500 px-4 py-2">
{format(new Date(snapshot.created_at), "yyyy-MM-dd HH:mm:ss", {
locale: zhCN,
})}
</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.likes}</td>
@ -175,7 +145,8 @@ interface Snapshot {
</div>
) : (
<p>暂无历史数据。</p>
)}
)
}
</div>
</div>
</main>

View File

@ -11,6 +11,7 @@
"@assets/*": ["src/assets/*"],
"@styles": ["src/styles/*"],
"@core/*": ["../core/*"]
}
},
"verbatimModuleSyntax": true
}
}