From 93499867d57ae818480d5c44b6cbc6e03f069ae3 Mon Sep 17 00:00:00 2001 From: kurihada Date: Mon, 2 Mar 2026 00:03:35 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=20=E2=80=94=20=E6=96=B0=E5=A2=9E=E5=93=81?= =?UTF-8?q?=E7=B1=BB/=E8=B4=B9=E7=94=A8/=E5=BC=BA=E5=BA=A6/=E9=A2=84?= =?UTF-8?q?=E7=BA=A6=E6=A0=87=E7=AD=BE=EF=BC=8CtimeSlot=20=E5=8F=82?= =?UTF-8?q?=E4=B8=8E=E9=80=89=E6=B4=BB=E5=8A=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - IdeaCategory 扩展 7→9,新增 experience(体验)和 nature(自然) - 替换 outdoor boolean 为 costLevel/intensity/needsBooking 三个高价值字段 - AI 标注 prompt 同步更新,行程规划新增强度交替、费用平衡、预约提醒原则 - selectIdeasForSlots 重写为四优先级:timeSlot+category > category > timeSlot > 任意 - 前端想法卡片展示费用/强度/预约标签 --- prisma/schema.prisma | 4 +- src/__tests__/helpers/fixtures.ts | 4 +- src/app/api/blindbox/route.test.ts | 4 +- src/app/api/blindbox/route.ts | 12 +++-- src/app/blindbox/[code]/page.tsx | 8 +++- src/components/BlindboxMyIdeas.tsx | 51 ++++++++++++++++++-- src/components/BlindboxPlan.tsx | 4 +- src/lib/ai.ts | 28 ++++++++--- src/lib/blindboxPlanGen.ts | 77 ++++++++++++++++++++++-------- src/types/index.ts | 11 ++++- 10 files changed, 160 insertions(+), 43 deletions(-) diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 3d294e0..5a46dd9 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -101,7 +101,9 @@ model BlindBoxIdea { category String? timeSlot String? estimatedMinutes Int? - outdoor Boolean? + costLevel String? + intensity String? + needsBooking Boolean? searchQuery String? searchType String? diff --git a/src/__tests__/helpers/fixtures.ts b/src/__tests__/helpers/fixtures.ts index 667cb59..25c53d1 100644 --- a/src/__tests__/helpers/fixtures.ts +++ b/src/__tests__/helpers/fixtures.ts @@ -97,7 +97,9 @@ export const TEST_BLINDBOX_IDEA = { category: "outdoor", timeSlot: "morning", estimatedMinutes: 120, - outdoor: true, + costLevel: "free", + intensity: "active", + needsBooking: false, searchQuery: "公园", searchType: "category", drawnById: null, diff --git a/src/app/api/blindbox/route.test.ts b/src/app/api/blindbox/route.test.ts index 6a9e39d..ceb9503 100644 --- a/src/app/api/blindbox/route.test.ts +++ b/src/app/api/blindbox/route.test.ts @@ -12,7 +12,9 @@ vi.mock("@/lib/ai", () => ({ category: "outdoor", timeSlot: "morning", estimatedMinutes: 120, - outdoor: true, + costLevel: "free", + intensity: "active", + needsBooking: false, searchQuery: "公园", searchType: "category", }), diff --git a/src/app/api/blindbox/route.ts b/src/app/api/blindbox/route.ts index 91d4a42..ad406b4 100644 --- a/src/app/api/blindbox/route.ts +++ b/src/app/api/blindbox/route.ts @@ -32,7 +32,9 @@ export const POST = apiHandler(async (req) => { category: tags.category, timeSlot: tags.timeSlot, estimatedMinutes: tags.estimatedMinutes, - outdoor: tags.outdoor, + costLevel: tags.costLevel, + intensity: tags.intensity, + needsBooking: tags.needsBooking, searchQuery: tags.searchQuery, searchType: tags.searchType, }, @@ -65,7 +67,9 @@ export const GET = apiHandler(async (req) => { category: true, timeSlot: true, estimatedMinutes: true, - outdoor: true, + costLevel: true, + intensity: true, + needsBooking: true, searchQuery: true, searchType: true, }, @@ -109,7 +113,9 @@ export const PUT = apiHandler(async (req) => { category: tags.category, timeSlot: tags.timeSlot, estimatedMinutes: tags.estimatedMinutes, - outdoor: tags.outdoor, + costLevel: tags.costLevel, + intensity: tags.intensity, + needsBooking: tags.needsBooking, searchQuery: tags.searchQuery, searchType: tags.searchType, }, diff --git a/src/app/blindbox/[code]/page.tsx b/src/app/blindbox/[code]/page.tsx index 4650768..1001535 100644 --- a/src/app/blindbox/[code]/page.tsx +++ b/src/app/blindbox/[code]/page.tsx @@ -448,7 +448,9 @@ export default function BlindboxRoomPage() { category: data.tags.category, timeSlot: data.tags.timeSlot, estimatedMinutes: data.tags.estimatedMinutes, - outdoor: data.tags.outdoor, + costLevel: data.tags.costLevel, + intensity: data.tags.intensity, + needsBooking: data.tags.needsBooking, searchQuery: data.tags.searchQuery, searchType: data.tags.searchType, }, @@ -490,7 +492,9 @@ export default function BlindboxRoomPage() { category: data.tags.category, timeSlot: data.tags.timeSlot, estimatedMinutes: data.tags.estimatedMinutes, - outdoor: data.tags.outdoor, + costLevel: data.tags.costLevel, + intensity: data.tags.intensity, + needsBooking: data.tags.needsBooking, searchQuery: data.tags.searchQuery, searchType: data.tags.searchType, }, diff --git a/src/components/BlindboxMyIdeas.tsx b/src/components/BlindboxMyIdeas.tsx index 0b85877..e2724b9 100644 --- a/src/components/BlindboxMyIdeas.tsx +++ b/src/components/BlindboxMyIdeas.tsx @@ -16,6 +16,8 @@ import { Dumbbell, Landmark, Coffee, + Palette, + Mountain, } from "lucide-react"; import type { IdeaCategory } from "@/types"; @@ -26,7 +28,9 @@ export interface MyIdea { category?: string | null; timeSlot?: string | null; estimatedMinutes?: number | null; - outdoor?: boolean | null; + costLevel?: string | null; + intensity?: string | null; + needsBooking?: boolean | null; searchQuery?: string | null; searchType?: string | null; } @@ -42,6 +46,8 @@ const CATEGORY_CONFIG: Record< sports: { icon: Dumbbell, color: "text-amber-400", label: "运动" }, culture: { icon: Landmark, color: "text-violet-400", label: "文化" }, relaxation: { icon: Coffee, color: "text-teal-400", label: "休闲" }, + experience: { icon: Palette, color: "text-rose-400", label: "体验" }, + nature: { icon: Mountain, color: "text-lime-400", label: "自然" }, }; function CategoryBadge({ category }: { category?: string | null }) { @@ -49,7 +55,7 @@ function CategoryBadge({ category }: { category?: string | null }) { const cfg = CATEGORY_CONFIG[category as IdeaCategory]; if (!cfg) return 💡; const Icon = cfg.icon; - return ; + return ; } function DurationLabel({ minutes }: { minutes?: number | null }) { @@ -62,6 +68,39 @@ function DurationLabel({ minutes }: { minutes?: number | null }) { ); } +const COST_LABELS: Record = { + free: "免费", + budget: "实惠", + moderate: "适中", + premium: "高端", +}; + +const INTENSITY_LABELS: Record = { + chill: "轻松", + moderate: "适度", + active: "活跃", +}; + +function TagPills({ idea }: { idea: MyIdea }) { + const pills: string[] = []; + if (idea.costLevel && COST_LABELS[idea.costLevel]) pills.push(COST_LABELS[idea.costLevel]); + if (idea.intensity && INTENSITY_LABELS[idea.intensity]) pills.push(INTENSITY_LABELS[idea.intensity]); + if (idea.needsBooking) pills.push("需预约"); + + if (pills.length === 0 && !idea.estimatedMinutes) return null; + + return ( +
+ {pills.map((label) => ( + + {label} + + ))} + +
+ ); +} + function MyIdeaItem({ idea, onEdit, @@ -86,7 +125,7 @@ function MyIdeaItem({ return ( -

{idea.content}

- +
+

{idea.content}

+ +