feat: AI 周末行程规划 — DeepSeek 智能排期 + 高德 POI + 多日翻页
- 接入 DeepSeek API,提交想法时自动 AI 打标(品类/时段/时长/搜索策略) - 新增行程规划 API:智能选取想法 → 高德 POI 搜索 → AI 生成最优行程 - 支持多日计划("整个周末"拆分周六/周日,并行 AI 调用) - 行程展示逐日翻页,时间线可滚动,操作按钮固定底部 - 分享卡适配多日格式,支持图片保存与原生分享 - Prisma schema 新增 WeekendPlan 模型及 BlindBoxIdea AI 标签字段 - Jenkinsfile 集成 DEEPSEEK_API_KEY 环境变量
This commit is contained in:
@@ -2,12 +2,64 @@
|
||||
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { Package, Loader2, Pencil, Trash2, Check, X } from "lucide-react";
|
||||
import {
|
||||
Package,
|
||||
Loader2,
|
||||
Pencil,
|
||||
Trash2,
|
||||
Check,
|
||||
X,
|
||||
UtensilsCrossed,
|
||||
TreePine,
|
||||
Film,
|
||||
ShoppingBag,
|
||||
Dumbbell,
|
||||
Landmark,
|
||||
Coffee,
|
||||
} from "lucide-react";
|
||||
import type { IdeaCategory } from "@/types";
|
||||
|
||||
export interface MyIdea {
|
||||
id: string;
|
||||
content: string;
|
||||
createdAt: string;
|
||||
category?: string | null;
|
||||
timeSlot?: string | null;
|
||||
estimatedMinutes?: number | null;
|
||||
outdoor?: boolean | null;
|
||||
searchQuery?: string | null;
|
||||
searchType?: string | null;
|
||||
}
|
||||
|
||||
const CATEGORY_CONFIG: Record<
|
||||
IdeaCategory,
|
||||
{ icon: typeof UtensilsCrossed; color: string; label: string }
|
||||
> = {
|
||||
dining: { icon: UtensilsCrossed, color: "text-orange-400", label: "美食" },
|
||||
outdoor: { icon: TreePine, color: "text-emerald-400", label: "户外" },
|
||||
entertainment: { icon: Film, color: "text-sky-400", label: "娱乐" },
|
||||
shopping: { icon: ShoppingBag, color: "text-pink-400", label: "购物" },
|
||||
sports: { icon: Dumbbell, color: "text-amber-400", label: "运动" },
|
||||
culture: { icon: Landmark, color: "text-violet-400", label: "文化" },
|
||||
relaxation: { icon: Coffee, color: "text-teal-400", label: "休闲" },
|
||||
};
|
||||
|
||||
function CategoryBadge({ category }: { category?: string | null }) {
|
||||
if (!category) return <span className="text-sm">💡</span>;
|
||||
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}`} />;
|
||||
}
|
||||
|
||||
function DurationLabel({ minutes }: { minutes?: number | null }) {
|
||||
if (!minutes) return null;
|
||||
const display = minutes >= 60 ? `${(minutes / 60).toFixed(minutes % 60 === 0 ? 0 : 1)}h` : `${minutes}min`;
|
||||
return (
|
||||
<span className="shrink-0 rounded-md bg-elevated px-1.5 py-0.5 text-[10px] font-medium text-dim">
|
||||
~{display}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function MyIdeaItem({
|
||||
@@ -66,8 +118,9 @@ function MyIdeaItem({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-sm">💡</span>
|
||||
<CategoryBadge category={idea.category} />
|
||||
<p className="min-w-0 flex-1 truncate text-sm text-secondary">{idea.content}</p>
|
||||
<DurationLabel minutes={idea.estimatedMinutes} />
|
||||
<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"
|
||||
@@ -119,3 +172,5 @@ export default function BlindboxMyIdeas({
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
export { CATEGORY_CONFIG, CategoryBadge, DurationLabel };
|
||||
|
||||
Reference in New Issue
Block a user