feat: 改进标签系统 — 新增品类/费用/强度/预约标签,timeSlot 参与选活动
- IdeaCategory 扩展 7→9,新增 experience(体验)和 nature(自然) - 替换 outdoor boolean 为 costLevel/intensity/needsBooking 三个高价值字段 - AI 标注 prompt 同步更新,行程规划新增强度交替、费用平衡、预约提醒原则 - selectIdeasForSlots 重写为四优先级:timeSlot+category > category > timeSlot > 任意 - 前端想法卡片展示费用/强度/预约标签
This commit is contained in:
@@ -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 <span className="text-sm">💡</span>;
|
||||
const Icon = cfg.icon;
|
||||
return <Icon size={14} className={`shrink-0 ${cfg.color}`} />;
|
||||
return <Icon size={14} className={`mt-0.5 shrink-0 ${cfg.color}`} />;
|
||||
}
|
||||
|
||||
function DurationLabel({ minutes }: { minutes?: number | null }) {
|
||||
@@ -62,6 +68,39 @@ function DurationLabel({ minutes }: { minutes?: number | null }) {
|
||||
);
|
||||
}
|
||||
|
||||
const COST_LABELS: Record<string, string> = {
|
||||
free: "免费",
|
||||
budget: "实惠",
|
||||
moderate: "适中",
|
||||
premium: "高端",
|
||||
};
|
||||
|
||||
const INTENSITY_LABELS: Record<string, string> = {
|
||||
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 (
|
||||
<div className="mt-1 flex flex-wrap items-center gap-1">
|
||||
{pills.map((label) => (
|
||||
<span key={label} className="rounded-md bg-elevated px-1.5 py-0.5 text-[10px] font-medium text-dim">
|
||||
{label}
|
||||
</span>
|
||||
))}
|
||||
<DurationLabel minutes={idea.estimatedMinutes} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MyIdeaItem({
|
||||
idea,
|
||||
onEdit,
|
||||
@@ -86,7 +125,7 @@ function MyIdeaItem({
|
||||
return (
|
||||
<motion.div
|
||||
layout
|
||||
className="flex items-center gap-2 rounded-xl bg-surface/60 px-3 py-2.5 ring-1 ring-border/80"
|
||||
className="flex items-start gap-2 rounded-xl bg-surface/60 px-3 py-2.5 ring-1 ring-border/80"
|
||||
initial={{ opacity: 0, y: -8 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, x: -20 }}
|
||||
@@ -119,8 +158,10 @@ function MyIdeaItem({
|
||||
) : (
|
||||
<>
|
||||
<CategoryBadge category={idea.category} />
|
||||
<p className="min-w-0 flex-1 truncate text-sm text-secondary">{idea.content}</p>
|
||||
<DurationLabel minutes={idea.estimatedMinutes} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="truncate text-sm text-secondary">{idea.content}</p>
|
||||
<TagPills idea={idea} />
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setEditing(true)}
|
||||
className="flex h-7 w-7 shrink-0 items-center justify-center rounded-lg text-muted transition-colors active:bg-elevated active:text-purple-400"
|
||||
|
||||
@@ -30,7 +30,9 @@ interface BlindboxPlanProps {
|
||||
function guessCategory(activity: string): string | null {
|
||||
const lower = activity.toLowerCase();
|
||||
if (/吃|餐|饭|火锅|烧烤|面|菜|厨|食/.test(lower)) return "dining";
|
||||
if (/公园|山|湖|海|户外|骑|徒步|露营/.test(lower)) return "outdoor";
|
||||
if (/手作|工坊|烘焙|插花|陶艺|DIY|体验/.test(lower)) return "experience";
|
||||
if (/露营|徒步|赶海|农场|自然|野|营地/.test(lower)) return "nature";
|
||||
if (/公园|山|湖|海|户外|骑/.test(lower)) return "outdoor";
|
||||
if (/电影|KTV|密室|游戏|桌游|剧/.test(lower)) return "entertainment";
|
||||
if (/逛街|购物|商场|买/.test(lower)) return "shopping";
|
||||
if (/运动|健身|球|跑|游泳|瑜伽/.test(lower)) return "sports";
|
||||
|
||||
Reference in New Issue
Block a user