add: complete docker config
This commit is contained in:
parent
784939074a
commit
43b52dee0b
5
.gitignore
vendored
5
.gitignore
vendored
@ -91,5 +91,6 @@ model/
|
|||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
*.sqlite3
|
*.sqlite3
|
||||||
db/
|
data/
|
||||||
data/
|
|
||||||
|
docker-compose.yml
|
30
Dockerfile.frontend
Normal file
30
Dockerfile.frontend
Normal 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
10
package.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"name": "cvsa",
|
||||||
|
"version": "2.13.22",
|
||||||
|
"private": false,
|
||||||
|
"type": "module",
|
||||||
|
"workspaces": [
|
||||||
|
"packages/frontend",
|
||||||
|
"packages/core"
|
||||||
|
]
|
||||||
|
}
|
@ -1,6 +1,4 @@
|
|||||||
import postgres from "postgres";
|
import postgres from "postgres";
|
||||||
import { postgresConfigNpm } from "./config";
|
import { postgresConfigNpm } from "./config";
|
||||||
|
|
||||||
const sql = postgres(postgresConfigNpm);
|
export const sql = postgres(postgresConfigNpm);
|
||||||
|
|
||||||
export default sql;
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import sql from "./db";
|
import { sql } from "./db";
|
||||||
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
|
import type { VideoSnapshotType } from "@core/db/schema.d.ts";
|
||||||
|
|
||||||
export async function getVideoSnapshots(
|
export async function getVideoSnapshots(
|
||||||
|
@ -3,7 +3,7 @@ import Argon2id from "@rabbit-company/argon2id";
|
|||||||
import { object, string, ValidationError } from "yup";
|
import { object, string, ValidationError } from "yup";
|
||||||
import type { Context } from "hono";
|
import type { Context } from "hono";
|
||||||
import type { Bindings, BlankEnv, BlankInput } from "hono/types";
|
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";
|
import { ErrorResponse, StatusResponse } from "./schema";
|
||||||
|
|
||||||
const RegistrationBodySchema = object({
|
const RegistrationBodySchema = object({
|
||||||
|
@ -34,7 +34,6 @@ async function insertVideoSnapshot(data: VideoInfoData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const videoInfoHandler = createHandlers(async (c: ContextType) => {
|
export const videoInfoHandler = createHandlers(async (c: ContextType) => {
|
||||||
const client = c.get("db");
|
|
||||||
try {
|
try {
|
||||||
const id = await idSchema.validate(c.req.param("id"));
|
const id = await idSchema.validate(c.req.param("id"));
|
||||||
let videoId: string | number = id as string;
|
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 redis.setex(cacheKey, CACHE_EXPIRATION_SECONDS, JSON.stringify(result));
|
||||||
|
|
||||||
await insertVideoSnapshot(client, result);
|
await insertVideoSnapshot(result);
|
||||||
|
|
||||||
return c.json(result);
|
return c.json(result);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
"ioredis": "^5.6.1",
|
||||||
"logform": "^2.7.0",
|
"logform": "^2.7.0",
|
||||||
"winston": "^3.17.0",
|
"winston": "^3.17.0",
|
||||||
},
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ioredis": "^5.0.0",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"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=="],
|
"@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=="],
|
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
|
||||||
|
|
||||||
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
|
||||||
|
|
||||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
"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": ["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=="],
|
"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=="],
|
"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=="],
|
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
|
||||||
|
|
||||||
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
|
"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=="],
|
"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-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
|
||||||
|
|
||||||
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
|
||||||
|
|
||||||
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"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-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
|
||||||
|
|
||||||
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
|
"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=="],
|
"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=="],
|
"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=="],
|
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
|
||||||
|
4
packages/core/db/dbNew.ts
Normal file
4
packages/core/db/dbNew.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import postgres from "postgres";
|
||||||
|
import { postgresConfigNpm } from "./pgConfigNew";
|
||||||
|
|
||||||
|
export const sql = postgres(postgresConfigNpm);
|
38
packages/core/db/pgConfigNew.ts
Normal file
38
packages/core/db/pgConfigNew.ts
Normal 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
|
||||||
|
};
|
@ -1,15 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "@cvsa/core",
|
"name": "@cvsa/core",
|
||||||
"exports": "./main.ts",
|
"exports": "./main.ts",
|
||||||
"imports": {
|
"imports": {
|
||||||
"ioredis": "npm:ioredis",
|
"ioredis": "npm:ioredis",
|
||||||
"log/": "./log/",
|
"log/": "./log/",
|
||||||
"db/": "./db/",
|
"db/": "./db/",
|
||||||
"$std/": "https://deno.land/std@0.216.0/",
|
"$std/": "https://deno.land/std@0.216.0/",
|
||||||
"mq/": "./mq/",
|
"mq/": "./mq/",
|
||||||
"chalk": "npm:chalk",
|
"chalk": "npm:chalk",
|
||||||
"winston": "npm:winston",
|
"winston": "npm:winston",
|
||||||
"logform": "npm:logform",
|
"logform": "npm:logform",
|
||||||
"@core/": "./"
|
"@core/": "./",
|
||||||
}
|
"child_process": "node:child_process",
|
||||||
|
"util": "node:util"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,8 +2,12 @@ import logger from "log/logger.ts";
|
|||||||
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
|
import { RateLimiter, type RateLimiterConfig } from "mq/rateLimiter.ts";
|
||||||
import { SlidingWindow } from "mq/slidingWindow.ts";
|
import { SlidingWindow } from "mq/slidingWindow.ts";
|
||||||
import { redis } from "db/redis.ts";
|
import { redis } from "db/redis.ts";
|
||||||
import Redis from "ioredis";
|
import { ReplyError } from "ioredis";
|
||||||
import { SECOND } from "$std/datetime/constants.ts";
|
import { SECOND } from "../const/time.ts";
|
||||||
|
import { execFile } from 'child_process';
|
||||||
|
import { promisify } from 'util';
|
||||||
|
|
||||||
|
const execAsync = promisify(execFile);
|
||||||
|
|
||||||
interface Proxy {
|
interface Proxy {
|
||||||
type: string;
|
type: string;
|
||||||
@ -99,7 +103,7 @@ class NetworkDelegate {
|
|||||||
await this.providerLimiters[providerLimiterId]?.trigger();
|
await this.providerLimiters[providerLimiterId]?.trigger();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
if (e instanceof Redis.ReplyError) {
|
if (e instanceof ReplyError) {
|
||||||
logger.error(error, "redis");
|
logger.error(error, "redis");
|
||||||
}
|
}
|
||||||
logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest");
|
logger.warn(`Unhandled error: ${error.message}`, "mq", "proxyRequest");
|
||||||
@ -209,7 +213,7 @@ class NetworkDelegate {
|
|||||||
return providerAvailable && proxyAvailable;
|
return providerAvailable && proxyAvailable;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e as Error;
|
const error = e as Error;
|
||||||
if (e instanceof Redis.ReplyError) {
|
if (e instanceof ReplyError) {
|
||||||
logger.error(error, "redis");
|
logger.error(error, "redis");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -239,8 +243,7 @@ class NetworkDelegate {
|
|||||||
private async alicloudFcRequest<R>(url: string, region: string): Promise<R> {
|
private async alicloudFcRequest<R>(url: string, region: string): Promise<R> {
|
||||||
try {
|
try {
|
||||||
const decoder = new TextDecoder();
|
const decoder = new TextDecoder();
|
||||||
const output = await new Deno.Command("aliyun", {
|
const output = await execAsync("aliyun", [
|
||||||
args: [
|
|
||||||
"fc",
|
"fc",
|
||||||
"POST",
|
"POST",
|
||||||
`/2023-03-30/functions/proxy-${region}/invocations`,
|
`/2023-03-30/functions/proxy-${region}/invocations`,
|
||||||
@ -258,9 +261,8 @@ class NetworkDelegate {
|
|||||||
"10",
|
"10",
|
||||||
"--profile",
|
"--profile",
|
||||||
`CVSA-${region}`,
|
`CVSA-${region}`,
|
||||||
],
|
])
|
||||||
}).output();
|
const out = output.stdout;
|
||||||
const out = decoder.decode(output.stdout);
|
|
||||||
const rawData = JSON.parse(out);
|
const rawData = JSON.parse(out);
|
||||||
if (rawData.statusCode !== 200) {
|
if (rawData.statusCode !== 200) {
|
||||||
// noinspection ExceptionCaughtLocallyJS
|
// noinspection ExceptionCaughtLocallyJS
|
||||||
|
@ -1,7 +1,11 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
|
"ioredis": "^5.6.1",
|
||||||
"logform": "^2.7.0",
|
"logform": "^2.7.0",
|
||||||
"winston": "^3.17.0"
|
"winston": "^3.17.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ioredis": "^5.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -9,7 +9,7 @@
|
|||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@core/*": ["../core/*"]
|
"@core/*": ["./*"]
|
||||||
},
|
},
|
||||||
"allowSyntheticDefaultImports": true,
|
"allowSyntheticDefaultImports": true,
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
|
@ -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"]
|
|
8
packages/frontend/src/db/bilibili_metadata/aidExists.ts
Normal file
8
packages/frontend/src/db/bilibili_metadata/aidExists.ts
Normal 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;
|
||||||
|
}
|
15
packages/frontend/src/db/bilibili_metadata/getAidFromBV.ts
Normal file
15
packages/frontend/src/db/bilibili_metadata/getAidFromBV.ts
Normal 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;
|
||||||
|
}
|
@ -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 {};
|
||||||
|
}
|
11
packages/frontend/src/db/snapshots/getAllSnapshots.ts
Normal file
11
packages/frontend/src/db/snapshots/getAllSnapshots.ts
Normal 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;
|
||||||
|
}
|
@ -1,73 +1,31 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@layouts/Layout.astro";
|
import Layout from "@layouts/Layout.astro";
|
||||||
import TitleBar from "@components/TitleBar.astro";
|
import TitleBar from "@components/TitleBar.astro";
|
||||||
import pg from "pg";
|
import { format } from "date-fns";
|
||||||
import { format } from 'date-fns';
|
import { zhCN } from "date-fns/locale";
|
||||||
import { zhCN } from 'date-fns/locale';
|
|
||||||
import MetadataRow from "@components/InfoPage/MetadataRow.astro";
|
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 databaseHost = import.meta.env.DB_HOST;
|
||||||
const databaseName = import.meta.env.DB_NAME
|
const databaseName = import.meta.env.DB_NAME;
|
||||||
const databaseUser = import.meta.env.DB_USER
|
const databaseUser = import.meta.env.DB_USER;
|
||||||
const databasePassword = import.meta.env.DB_PASSWORD
|
const databasePassword = import.meta.env.DB_PASSWORD;
|
||||||
const databasePort = import.meta.env.DB_PORT
|
const databasePort = import.meta.env.DB_PORT;
|
||||||
|
|
||||||
const postgresConfig = {
|
const postgresConfig = {
|
||||||
hostname: databaseHost,
|
hostname: databaseHost,
|
||||||
port: parseInt(databasePort!),
|
port: parseInt(databasePort!),
|
||||||
database: databaseName,
|
database: databaseName,
|
||||||
user: databaseUser,
|
user: databaseUser,
|
||||||
password: databasePassword,
|
password: databasePassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(postgresConfig)
|
console.log(postgresConfig);
|
||||||
|
|
||||||
// 路由参数
|
|
||||||
const { id } = Astro.params;
|
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) {
|
async function getVideoAid(id: string) {
|
||||||
if (id.startsWith("av")) {
|
if (id.startsWith("av")) {
|
||||||
@ -78,27 +36,22 @@ async function getVideoAid(id: string) {
|
|||||||
return parseInt(id);
|
return parseInt(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取数据
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
Astro.response.status = 404;
|
Astro.response.status = 404;
|
||||||
client.end();
|
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
const aid = await getVideoAid(id);
|
const aid = await getVideoAid(id);
|
||||||
if (!aid || isNaN(aid)) {
|
if (!aid || isNaN(aid)) {
|
||||||
Astro.response.status = 404;
|
Astro.response.status = 404;
|
||||||
client.end();
|
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
const aidExists = await idExists(aid);
|
const aidExists = await idExists(aid);
|
||||||
if (!aidExists) {
|
if (!aidExists) {
|
||||||
Astro.response.status = 404;
|
Astro.response.status = 404;
|
||||||
client.end();
|
|
||||||
return new Response(null, { status: 404 });
|
return new Response(null, { status: 404 });
|
||||||
}
|
}
|
||||||
const videoInfo = await getVideoMetadata(aid);
|
const videoInfo = await getVideoMetadata(aid);
|
||||||
const snapshots = await getVideoSnapshots(aid);
|
const snapshots = await getAllSnapshots(aid);
|
||||||
client.end();
|
|
||||||
|
|
||||||
interface Snapshot {
|
interface Snapshot {
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -117,24 +70,36 @@ interface Snapshot {
|
|||||||
<TitleBar />
|
<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">
|
<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">
|
<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">
|
<div class="mb-6">
|
||||||
<h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2>
|
<h2 class="px-2 mb-2 text-xl font-medium">基本信息</h2>
|
||||||
<div class="overflow-x-auto max-w-full px-2">
|
<div class="overflow-x-auto max-w-full px-2">
|
||||||
<table class="table-fixed">
|
<table class="table-fixed">
|
||||||
<tbody>
|
<tbody>
|
||||||
<MetadataRow title={id} description={videoInfo?.id}/>
|
<MetadataRow title={id} description={videoInfo?.id} />
|
||||||
<MetadataRow title={videoInfo?.aid} description={videoInfo?.aid}/>
|
<MetadataRow title={videoInfo?.aid} description={videoInfo?.aid} />
|
||||||
<MetadataRow title={videoInfo?.bvid} description={videoInfo?.bvid}/>
|
<MetadataRow title={videoInfo?.bvid} description={videoInfo?.bvid} />
|
||||||
<MetadataRow title="标题" description={videoInfo?.title}/>
|
<MetadataRow title="标题" description={videoInfo?.title} />
|
||||||
<MetadataRow title="描述" description={videoInfo?.description}/>
|
<MetadataRow title="描述" description={videoInfo?.description} />
|
||||||
<MetadataRow title="UID" description={videoInfo?.uid}/>
|
<MetadataRow title="UID" description={videoInfo?.uid} />
|
||||||
<MetadataRow title="标签" description={videoInfo?.tags}/>
|
<MetadataRow title="标签" description={videoInfo?.tags} />
|
||||||
<MetadataRow title="发布时间" description={format(new Date(videoInfo?.published_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/>
|
<MetadataRow
|
||||||
<MetadataRow title="时长 (秒)" description={videoInfo?.duration}/>
|
title="发布时间"
|
||||||
<MetadataRow title="创建时间" description={format(new Date(videoInfo?.created_at), 'yyyy-MM-dd HH:mm:ss', { locale: zhCN })}/>
|
description={format(new Date(videoInfo?.published_at), "yyyy-MM-dd HH:mm:ss", {
|
||||||
<MetadataRow title="封面" description={videoInfo?.cover_url}/>
|
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={videoInfo?.cover_url} />
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
@ -142,40 +107,46 @@ interface Snapshot {
|
|||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2>
|
<h2 class="px-2 mb-2 text-xl font-medium">播放量历史数据</h2>
|
||||||
{snapshots && snapshots.length > 0 ? (
|
{
|
||||||
<div class="overflow-x-auto px-2">
|
snapshots && snapshots.length > 0 ? (
|
||||||
<table class="table-auto w-full">
|
<div class="overflow-x-auto px-2">
|
||||||
<thead>
|
<table class="table-auto w-full">
|
||||||
<tr>
|
<thead>
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">创建时间</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">观看</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">硬币</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">点赞</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">收藏</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">分享</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">弹幕</th>
|
|
||||||
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">评论</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{snapshots.map((snapshot: Snapshot) => (
|
|
||||||
<tr>
|
<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>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">创建时间</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.views}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">观看</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.coins}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">硬币</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.likes}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">点赞</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.favorites}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">收藏</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.shares}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">分享</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.danmakus}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">弹幕</th>
|
||||||
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.replies}</td>
|
<th class="border dark:border-zinc-500 px-4 py-2 font-medium">评论</th>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
</thead>
|
||||||
</tbody>
|
<tbody>
|
||||||
</table>
|
{snapshots.map((snapshot: Snapshot) => (
|
||||||
</div>
|
<tr>
|
||||||
) : (
|
<td class="border dark:border-zinc-500 px-4 py-2">
|
||||||
<p>暂无历史数据。</p>
|
{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>
|
||||||
|
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.favorites}</td>
|
||||||
|
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.shares}</td>
|
||||||
|
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.danmakus}</td>
|
||||||
|
<td class="border dark:border-zinc-500 px-4 py-2">{snapshot.replies}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p>暂无历史数据。</p>
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"@assets/*": ["src/assets/*"],
|
"@assets/*": ["src/assets/*"],
|
||||||
"@styles": ["src/styles/*"],
|
"@styles": ["src/styles/*"],
|
||||||
"@core/*": ["../core/*"]
|
"@core/*": ["../core/*"]
|
||||||
}
|
},
|
||||||
|
"verbatimModuleSyntax": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user