diff --git a/src/hooks/useKeywordCloud.ts b/src/hooks/useKeywordCloud.ts new file mode 100644 index 0000000..0a6b435 --- /dev/null +++ b/src/hooks/useKeywordCloud.ts @@ -0,0 +1,191 @@ +import { prisma } from '@/lib/prisma'; +import type { Quarter, Keyword, VisualStyleRule } from '@prisma/client'; + +// 类型定义 +export type KeywordWithVisual = Keyword & { + visualConfig: { + color: string; + size: string; + border: string; + rotation?: string; + }; +}; + +export type QuarterWithKeywords = Quarter & { + keywords: KeywordWithVisual[]; + _count?: { keywords: number }; +}; + +export type QuarterWithCount = Quarter & { + _count?: { keywords: number }; +}; + +/** + * 获取所有季度列表 + */ +export async function getAllQuarters( + options?: { isActive?: boolean } +): Promise { + const where = options?.isActive !== undefined + ? { isActive: options.isActive } + : {}; + + return prisma.quarter.findMany({ + where, + orderBy: { displayOrder: 'asc' }, + }); +} + +/** + * 获取单个季度的详情(包含关键词计数) + */ +export async function getQuarterByQuarter( + quarter: string +): Promise { + const quarterData = await prisma.quarter.findUnique({ + where: { quarter }, + include: { + _count: { + select: { keywords: true }, + }, + }, + }); + + return quarterData as QuarterWithCount | null; +} + +/** + * 获取指定季度的所有关键词 + */ +export async function getKeywordsByQuarter( + quarter: string +): Promise { + const quarterData = await prisma.quarter.findUnique({ + where: { quarter }, + include: { + keywords: { + orderBy: { trendScore: 'desc' }, + }, + }, + }); + + if (!quarterData) { + return null; + } + + // 转换 visualConfig 从 JSON 到对象 + const keywords: KeywordWithVisual[] = quarterData.keywords.map(kw => ({ + ...kw, + visualConfig: (typeof kw.visualConfig === 'string' + ? JSON.parse(kw.visualConfig) + : kw.visualConfig) as KeywordWithVisual['visualConfig'], + })); + + return { + ...quarterData, + keywords, + } as QuarterWithKeywords; +} + +/** + * 获取所有启用的视觉规则 + */ +export async function getVisualStyleRules( + options?: { enabled?: boolean } +): Promise { + const where = options?.enabled !== undefined + ? { enabled: options.enabled } + : {}; + + return prisma.visualStyleRule.findMany({ + where, + orderBy: [ + { priority: 'asc' }, + { minScore: 'desc' }, + ], + }); +} + +/** + * 创建或更新季度 + */ +export async function upsertQuarter( + quarter: string, + data: { + title: string; + titleEn?: string; + subtitle?: string; + subtitleEn?: string; + displayOrder?: number; + } +): Promise { + return prisma.quarter.upsert({ + where: { quarter }, + update: data, + create: { + quarter, + ...data, + }, + }); +} + +/** + * 批量创建关键词 + */ +export async function createKeywords( + quarterId: number, + keywords: Array<{ + word: string; + trendScore: number; + description: string; + descriptionEn?: string; + detailPoints: string[]; + detailPointsEn?: string[]; + visualConfig: Record; + }> +): Promise<{ created: number; failed: number; errors: Array<{ word: string; error: string }> }> { + const errors: Array<{ word: string; error: string }> = []; + let created = 0; + + for (const kw of keywords) { + try { + await prisma.keyword.create({ + data: { + quarterId, + word: kw.word, + trendScore: kw.trendScore, + description: kw.description, + descriptionEn: kw.descriptionEn, + detailPoints: kw.detailPoints as any, // Prisma Json 类型 + detailPointsEn: kw.detailPointsEn as any, + visualConfig: kw.visualConfig as any, + }, + }); + created++; + } catch (error) { + errors.push({ + word: kw.word, + error: error instanceof Error ? error.message : 'Unknown error', + }); + } + } + + return { created, failed: errors.length, errors }; +} + +/** + * 记录错误日志 + */ +export async function logKeywordCloudError( + data: { + quarter: string; + keyword?: string; + errorType: string; + errorMessage: string; + rawData?: any; + } +): Promise { + await prisma.keywordCloudErrorLog.create({ + data, + }); +}