refactor: Next.js 15兼容性改进和数据库模型标准化
This commit is contained in:
+37
-28
@@ -8,22 +8,23 @@ datasource db {
|
|||||||
url = env("DATABASE_URL")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
model external_links {
|
model ExternalLink {
|
||||||
id String @id
|
id String @id @default(cuid())
|
||||||
type LinkType
|
type LinkType
|
||||||
url String
|
url String
|
||||||
title String?
|
title String?
|
||||||
projectId String
|
projectId String
|
||||||
projects projects @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@unique([projectId, url])
|
@@unique([projectId, url])
|
||||||
@@index([projectId], map: "idx_link_projectId")
|
@@index([projectId], map: "idx_link_projectId")
|
||||||
@@index([type], map: "idx_link_type")
|
@@index([type], map: "idx_link_type")
|
||||||
@@index([type, url], map: "idx_link_type_url")
|
@@index([type, url], map: "idx_link_type_url")
|
||||||
@@index([url], map: "idx_link_url")
|
@@index([url], map: "idx_link_url")
|
||||||
|
@@map("external_links")
|
||||||
}
|
}
|
||||||
|
|
||||||
model keyword_cloud_error_logs {
|
model KeywordCloudErrorLog {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
quarter String
|
quarter String
|
||||||
keyword String?
|
keyword String?
|
||||||
@@ -34,9 +35,10 @@ model keyword_cloud_error_logs {
|
|||||||
|
|
||||||
@@index([errorType], map: "idx_keywordCloudErrorLog_errorType")
|
@@index([errorType], map: "idx_keywordCloudErrorLog_errorType")
|
||||||
@@index([quarter], map: "idx_keywordCloudErrorLog_quarter")
|
@@index([quarter], map: "idx_keywordCloudErrorLog_quarter")
|
||||||
|
@@map("keyword_cloud_error_logs")
|
||||||
}
|
}
|
||||||
|
|
||||||
model keywords {
|
model Keyword {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
word String
|
word String
|
||||||
trendScore Int
|
trendScore Int
|
||||||
@@ -47,16 +49,17 @@ model keywords {
|
|||||||
detailPointsEn Json?
|
detailPointsEn Json?
|
||||||
visualConfig Json
|
visualConfig Json
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime
|
updatedAt DateTime @updatedAt
|
||||||
quarters quarters @relation(fields: [quarterId], references: [id], onDelete: Cascade)
|
quarter Quarter @relation(fields: [quarterId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@index([quarterId], map: "idx_keyword_quarterId")
|
@@index([quarterId], map: "idx_keyword_quarterId")
|
||||||
@@index([trendScore], map: "idx_keyword_trendScore")
|
@@index([trendScore], map: "idx_keyword_trendScore")
|
||||||
@@index([word], map: "idx_keyword_word")
|
@@index([word], map: "idx_keyword_word")
|
||||||
|
@@map("keywords")
|
||||||
}
|
}
|
||||||
|
|
||||||
model project_discovery_tasks {
|
model ProjectDiscoveryTask {
|
||||||
id String @id
|
id String @id @default(cuid())
|
||||||
status TaskStatus @default(PENDING)
|
status TaskStatus @default(PENDING)
|
||||||
sourceUrl String
|
sourceUrl String
|
||||||
sourceType String @default("manual")
|
sourceType String @default("manual")
|
||||||
@@ -69,26 +72,28 @@ model project_discovery_tasks {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
startedAt DateTime?
|
startedAt DateTime?
|
||||||
completedAt DateTime?
|
completedAt DateTime?
|
||||||
updatedAt DateTime
|
updatedAt DateTime @updatedAt
|
||||||
projects projects? @relation(fields: [projectId], references: [id])
|
project Project? @relation(fields: [projectId], references: [id])
|
||||||
|
|
||||||
@@index([projectId], map: "idx_task_project_id")
|
@@index([projectId], map: "idx_task_project_id")
|
||||||
@@index([sourceUrl], map: "idx_task_source_url")
|
@@index([sourceUrl], map: "idx_task_source_url")
|
||||||
@@index([status, createdAt], map: "idx_task_status_created")
|
@@index([status, createdAt], map: "idx_task_status_created")
|
||||||
|
@@map("project_discovery_tasks")
|
||||||
}
|
}
|
||||||
|
|
||||||
model project_tags {
|
model ProjectTag {
|
||||||
projectId String
|
projectId String
|
||||||
tagId String
|
tagId String
|
||||||
projects projects @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||||
tags tags @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
@@id([projectId, tagId])
|
@@id([projectId, tagId])
|
||||||
@@index([tagId])
|
@@index([tagId])
|
||||||
|
@@map("project_tags")
|
||||||
}
|
}
|
||||||
|
|
||||||
model projects {
|
model Project {
|
||||||
id String @id
|
id String @id @default(cuid())
|
||||||
name String
|
name String
|
||||||
nameEn String?
|
nameEn String?
|
||||||
slug String @unique
|
slug String @unique
|
||||||
@@ -99,19 +104,20 @@ model projects {
|
|||||||
status ProjectStatus @default(ACTIVE)
|
status ProjectStatus @default(ACTIVE)
|
||||||
source String?
|
source String?
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime
|
updatedAt DateTime @updatedAt
|
||||||
embedding Unsupported("vector")?
|
embedding Unsupported("vector")?
|
||||||
embeddingUpdatedAt DateTime?
|
embeddingUpdatedAt DateTime?
|
||||||
external_links external_links[]
|
links ExternalLink[]
|
||||||
project_discovery_tasks project_discovery_tasks[]
|
projectDiscoveryTasks ProjectDiscoveryTask[]
|
||||||
project_tags project_tags[]
|
tags ProjectTag[]
|
||||||
|
|
||||||
@@index([embedding], map: "idx_project_embedding_cosine")
|
@@index([embedding], map: "idx_project_embedding_cosine")
|
||||||
@@index([slug], map: "idx_project_slug")
|
@@index([slug], map: "idx_project_slug")
|
||||||
@@index([status, createdAt], map: "idx_project_status_createdAt")
|
@@index([status, createdAt], map: "idx_project_status_createdAt")
|
||||||
|
@@map("projects")
|
||||||
}
|
}
|
||||||
|
|
||||||
model quarters {
|
model Quarter {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
quarter String @unique
|
quarter String @unique
|
||||||
title String
|
title String
|
||||||
@@ -121,25 +127,27 @@ model quarters {
|
|||||||
displayOrder Int @default(0)
|
displayOrder Int @default(0)
|
||||||
isActive Boolean @default(true)
|
isActive Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime
|
updatedAt DateTime @updatedAt
|
||||||
keywords keywords[]
|
keywords Keyword[]
|
||||||
|
|
||||||
@@index([displayOrder], map: "idx_quarter_displayOrder")
|
@@index([displayOrder], map: "idx_quarter_displayOrder")
|
||||||
@@index([quarter], map: "idx_quarter_quarter")
|
@@index([quarter], map: "idx_quarter_quarter")
|
||||||
|
@@map("quarters")
|
||||||
}
|
}
|
||||||
|
|
||||||
model tags {
|
model Tag {
|
||||||
id String @id
|
id String @id @default(cuid())
|
||||||
name String @unique
|
name String @unique
|
||||||
nameEn String?
|
nameEn String?
|
||||||
slug String @unique
|
slug String @unique
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
project_tags project_tags[]
|
projects ProjectTag[]
|
||||||
|
|
||||||
@@index([slug], map: "idx_tag_slug")
|
@@index([slug], map: "idx_tag_slug")
|
||||||
|
@@map("tags")
|
||||||
}
|
}
|
||||||
|
|
||||||
model visual_style_rules {
|
model VisualStyleRule {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String @unique
|
name String @unique
|
||||||
minScore Int
|
minScore Int
|
||||||
@@ -151,10 +159,11 @@ model visual_style_rules {
|
|||||||
priority Int @default(0)
|
priority Int @default(0)
|
||||||
enabled Boolean @default(true)
|
enabled Boolean @default(true)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
@@index([enabled], map: "idx_visualStyleRule_enabled")
|
@@index([enabled], map: "idx_visualStyleRule_enabled")
|
||||||
@@index([minScore, maxScore], map: "idx_visualStyleRule_scoreRange")
|
@@index([minScore, maxScore], map: "idx_visualStyleRule_scoreRange")
|
||||||
|
@@map("visual_style_rules")
|
||||||
}
|
}
|
||||||
|
|
||||||
enum LinkType {
|
enum LinkType {
|
||||||
|
|||||||
@@ -1,41 +1,44 @@
|
|||||||
import { KeywordCloud } from './components/KeywordCloud';
|
import { KeywordCloud } from "./components/KeywordCloud";
|
||||||
import { getTranslations } from 'next-intl/server';
|
import { getTranslations } from "next-intl/server";
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params?: Promise<{
|
||||||
locale: string;
|
locale?: string | string[];
|
||||||
};
|
}>;
|
||||||
searchParams: {
|
searchParams?: Promise<{
|
||||||
quarter?: string;
|
quarter?: string | string[];
|
||||||
};
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({ params }: PageProps) {
|
||||||
params
|
const resolvedParams = (await params) ?? {};
|
||||||
}: {
|
const rawLocale = resolvedParams.locale;
|
||||||
params: Promise<{ locale: string }>
|
const locale = (Array.isArray(rawLocale) ? rawLocale[0] : rawLocale) ?? "zh";
|
||||||
}) {
|
const t = await getTranslations("keywordCloud");
|
||||||
const { locale } = await params;
|
|
||||||
const t = await getTranslations('keywordCloud');
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: t('metaTitle'),
|
title: t("metaTitle"),
|
||||||
description: t('metaDescription'),
|
description: t("metaDescription"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function KeywordCloudPage({ searchParams, params }: PageProps) {
|
export default async function KeywordCloudPage({ searchParams, params }: PageProps) {
|
||||||
const { locale } = await params;
|
const resolvedParams = (await params) ?? {};
|
||||||
const t = await getTranslations('keywordCloud');
|
const rawLocale = resolvedParams.locale;
|
||||||
|
const locale = (Array.isArray(rawLocale) ? rawLocale[0] : rawLocale) ?? "zh";
|
||||||
|
|
||||||
|
const resolvedSearchParams = (await searchParams) ?? {};
|
||||||
|
const t = await getTranslations("keywordCloud");
|
||||||
|
|
||||||
// 如果 URL 中有 quarter 参数,使用它;否则使用默认季度
|
// 如果 URL 中有 quarter 参数,使用它;否则使用默认季度
|
||||||
const quarter = searchParams.quarter || '2024-Q1';
|
const rawQuarter = resolvedSearchParams.quarter;
|
||||||
|
const quarter = (Array.isArray(rawQuarter) ? rawQuarter[0] : rawQuarter) || "2024-Q1";
|
||||||
|
|
||||||
const texts = {
|
const texts = {
|
||||||
loading: t('loading'),
|
loading: t("loading"),
|
||||||
loadFailed: t('loadFailed'),
|
loadFailed: t("loadFailed"),
|
||||||
retry: t('retry'),
|
retry: t("retry"),
|
||||||
hotKeyword: t('hotKeyword'),
|
hotKeyword: t("hotKeyword"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -43,13 +46,19 @@ export default async function KeywordCloudPage({ searchParams, params }: PagePro
|
|||||||
{/* 页面标题 */}
|
{/* 页面标题 */}
|
||||||
<header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4">
|
<header className="relative z-10 pt-16 pb-8 text-center max-w-4xl mx-auto px-4">
|
||||||
<div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]">
|
<div className="inline-block bg-accent dark:bg-purple-700 border-2 border-black px-3 py-1 font-display font-bold text-xs mb-4 shadow-hard-sm rotate-[-2deg]">
|
||||||
{t('badge')}
|
{t("badge")}
|
||||||
</div>
|
</div>
|
||||||
<h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight">
|
<h1 className="font-display text-5xl md:text-7xl font-bold mb-6 tracking-tighter leading-tight">
|
||||||
{t('title')} <span className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500" style={{ WebkitTextStroke: '1.5px black' }}>{t('titleHighlight')}</span>
|
{t("title")}{" "}
|
||||||
|
<span
|
||||||
|
className="text-transparent bg-clip-text bg-gradient-to-r from-blue-500 to-purple-500"
|
||||||
|
style={{ WebkitTextStroke: "1.5px black" }}
|
||||||
|
>
|
||||||
|
{t("titleHighlight")}
|
||||||
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8">
|
<p className="text-lg md:text-xl max-w-2xl mx-auto font-medium text-gray-700 dark:text-gray-300 mb-8">
|
||||||
{t('subtitle')}
|
{t("subtitle")}
|
||||||
</p>
|
</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from "next/server";
|
||||||
import { getKeywordsByQuarter } from '@/hooks/useKeywordCloud';
|
import { getKeywordsByQuarter } from "@/hooks/useKeywordCloud";
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/keyword-cloud/keywords/[quarter]
|
* GET /api/keyword-cloud/keywords/[quarter]
|
||||||
* 获取指定季度的关键词
|
* 获取指定季度的关键词
|
||||||
*/
|
*/
|
||||||
export async function GET(
|
export async function GET(request: Request, { params }: { params: Promise<{ quarter: string }> }) {
|
||||||
request: Request,
|
|
||||||
{ params }: { params: { quarter: string } }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const { quarter } = params;
|
const { quarter } = await params;
|
||||||
|
|
||||||
// 验证 quarter 格式
|
// 验证 quarter 格式
|
||||||
if (!/^\d{4}-Q[1-4]$/.test(quarter)) {
|
if (!/^\d{4}-Q[1-4]$/.test(quarter)) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Invalid quarter format. Expected: YYYY-QN',
|
error: "Invalid quarter format. Expected: YYYY-QN",
|
||||||
},
|
},
|
||||||
{ status: 400 }
|
{ status: 400 }
|
||||||
);
|
);
|
||||||
@@ -47,11 +44,11 @@ export async function GET(
|
|||||||
keywords: data.keywords,
|
keywords: data.keywords,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching keywords:', error);
|
console.error("Error fetching keywords:", error);
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{
|
{
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to fetch keywords',
|
error: "Failed to fetch keywords",
|
||||||
},
|
},
|
||||||
{ status: 500 }
|
{ status: 500 }
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user