@lmy54321/design-system 1.1.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,676 @@
1
+ import React, { useState, useEffect, useCallback, useRef } from "react";
2
+ import { motion, AnimatePresence } from "motion/react";
3
+ import { cn } from "@lmy54321/design-system";
4
+ import { Tag } from "@lmy54321/design-system";
5
+ import { Btn } from "@lmy54321/design-system";
6
+ import { Toast } from "@lmy54321/design-system";
7
+ import { ImageWithFallback } from "@lmy54321/design-system";
8
+ import { IconFont } from "@lmy54321/design-system";
9
+ import { TencentMap } from "@lmy54321/design-system";
10
+
11
+ /* ══════════════════════════════════════════
12
+ 推荐卡片数据
13
+ ══════════════════════════════════════════ */
14
+
15
+ interface POICard {
16
+ id: string;
17
+ images: string[];
18
+ title: string;
19
+ tags: { label: string; icon?: "flag" | "pin"; accent?: boolean }[];
20
+ aiPrompt: string;
21
+ distance: string;
22
+ type: "route" | "single" | "topic" | "explore";
23
+ badge?: string;
24
+ }
25
+
26
+ const poiCards: POICard[] = [
27
+ {
28
+ id: "1",
29
+ images: [
30
+ "/images/scene/relax.png",
31
+ ],
32
+ title: "\u5076\u5c14\u653e\u7a7a\uff0c\u5728\u5306\u5fd9\u7684\u57ce\u5e02\u4eab\u53d7\u9633\u5149",
33
+ tags: [
34
+ { label: "\u6674\u5929\u9002\u5b9c", icon: "pin" },
35
+ { label: "\u65b0\u5f00\u4e1a" },
36
+ { label: "\u6709\u5145\u7535\u6869" },
37
+ ],
38
+ aiPrompt: "\u201c\u6211\u5bb6\u9644\u8fd1\u6709\u6ca1\u6709\u7c7b\u4f3c\u7684\u5e97\u201d",
39
+ distance: "1.1km",
40
+ type: "route",
41
+ },
42
+ {
43
+ id: "2",
44
+ images: [
45
+ "/images/scene/poi-relax.png",
46
+ "/images/scene/cafe.png",
47
+ ],
48
+ title: "\u5076\u5c14\u653e\u7a7a\uff0c\u5728\u5306\u5fd9\u7684\u57ce\u5e02\u4eab\u53d7\u9633\u5149",
49
+ tags: [
50
+ { label: "\u7cbe\u9009\u6f14\u51fa", icon: "flag", accent: true },
51
+ { label: "\u677e\u5f1b\u611f" },
52
+ ],
53
+ aiPrompt: "\u201c\u4e0d\u8981\u6cd5\u9910\u5385\uff0c\u6362\u4e00\u4e2a\u5403\u591c\u5bb5\u7684\u201d",
54
+ distance: "1.1km",
55
+ type: "single",
56
+ },
57
+ {
58
+ id: "3",
59
+ images: [
60
+ "/images/scene/friends-outing.png",
61
+ ],
62
+ title: "\u5076\u5c14\u653e\u7a7a\uff0c\u5728\u5306\u5fd9\u7684\u57ce\u5e02\u4eab\u53d7\u9633\u5149",
63
+ tags: [
64
+ { label: "\u7cbe\u9009\u6f14\u51fa", icon: "flag", accent: true },
65
+ { label: "\u677e\u5f1b\u611f" },
66
+ ],
67
+ aiPrompt: "\u201c\u4e0d\u8981\u6cd5\u9910\u5385\uff0c\u6362\u4e00\u4e2a\u5403\u591c\u5bb5\u7684\u201d",
68
+ distance: "1.1km",
69
+ type: "topic",
70
+ badge: "\u5546\u5708\u5185",
71
+ },
72
+ {
73
+ id: "4",
74
+ images: [
75
+ "/images/scene/poi-retro.png",
76
+ "/images/scene/restaurant.png",
77
+ "/images/scene/cafe.png",
78
+ ],
79
+ title: "\u590d\u53e4\u56de\u6f6e\uff1a\u63a2\u5e97\u5357\u5934\u53e4\u57ce5\u5bb6\u6000\u65e7\u98ce\u5c0f\u5e97",
80
+ tags: [
81
+ { label: "\u5546\u5708\u5185", icon: "flag", accent: true },
82
+ { label: "\u5ba0\u7269\u53cb\u597d" },
83
+ { label: "\u4e3b\u9898\u73a9\u6cd5" },
84
+ ],
85
+ aiPrompt: "\u201c\u6211\u5bb6\u9644\u8fd1\u6709\u6ca1\u6709\u7c7b\u4f3c\u7684\u5e97\u201d",
86
+ distance: "1.1km",
87
+ type: "explore",
88
+ },
89
+ {
90
+ id: "5",
91
+ images: [
92
+ "/images/feed/feed-park.png",
93
+ ],
94
+ title: "\u5076\u5c14\u653e\u7a7a\uff0c\u5728\u5306\u5fd9\u7684\u57ce\u5e02\u4eab\u53d7\u9633\u5149",
95
+ tags: [
96
+ { label: "\u6674\u5929\u9002\u5b9c", icon: "pin" },
97
+ { label: "\u6c1b\u56f4\u611f" },
98
+ { label: "\u597d\u505c\u8f66" },
99
+ ],
100
+ aiPrompt: "\u201c\u6709\u6ca1\u6709\u5b89\u9759\u4e00\u70b9\u7684\u9152\u5427\u201d",
101
+ distance: "2.3km",
102
+ type: "route",
103
+ },
104
+ ];
105
+
106
+ /* ══════════════════════════════════════════
107
+ 图片布局子组件(按卡片类型区分)
108
+ — 带倾斜、压盖、错落的活泼布局
109
+ ══════════════════════════════════════════ */
110
+
111
+ function RouteImageLayout({ card }: { card: POICard }) {
112
+ return (
113
+ <div className="relative w-full h-[200px] overflow-hidden">
114
+ {/* 地图背景 */}
115
+ <TencentMap className="absolute inset-0 w-full h-full" center={{ lat: 39.909, lng: 116.397 }} />
116
+ {/* 倾斜的照片叠在地图上 */}
117
+ <div
118
+ className="absolute bottom-[16px] right-[16px] w-[110px] h-[80px] rounded-[12px] overflow-hidden shadow-lg border-[2.5px] border-white z-10"
119
+ style={{ transform: "rotate(4deg)" }}
120
+ >
121
+ <ImageWithFallback
122
+ src={card.images[0]}
123
+ alt={card.title}
124
+ className="w-full h-full object-cover"
125
+ />
126
+ </div>
127
+ {/* 距离标签 */}
128
+ <div className="absolute top-[var(--spacing-sm)] left-[var(--spacing-sm)] z-10 flex items-center gap-[3px] bg-white/90 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px] shadow-sm">
129
+ <IconFont name="location" size="10px" className="text-accent" />
130
+ <span className="text-[11px] text-foreground/70 leading-[14px]">距你{card.distance}</span>
131
+ </div>
132
+ </div>
133
+ );
134
+ }
135
+
136
+ function SingleImageLayout({ card }: { card: POICard }) {
137
+ return (
138
+ <div className="relative w-full h-[200px] overflow-hidden bg-card-muted">
139
+ {/* 底层大图 — 微微倾斜 */}
140
+ <div
141
+ className="absolute top-[10px] left-[12px] w-[180px] h-[160px] rounded-[16px] overflow-hidden shadow-md"
142
+ style={{ transform: "rotate(-3deg)" }}
143
+ >
144
+ <ImageWithFallback
145
+ src={card.images[0]}
146
+ alt={card.title}
147
+ className="w-full h-full object-cover"
148
+ />
149
+ </div>
150
+ {/* 上层小图 — 反方向倾斜,压盖在大图右下 */}
151
+ {card.images[1] && (
152
+ <div
153
+ className="absolute bottom-[8px] right-[12px] w-[140px] h-[120px] rounded-[14px] overflow-hidden shadow-lg border-[2.5px] border-white"
154
+ style={{ transform: "rotate(3.5deg)", zIndex: 2 }}
155
+ >
156
+ <ImageWithFallback
157
+ src={card.images[1]}
158
+ alt={card.title}
159
+ className="w-full h-full object-cover"
160
+ />
161
+ </div>
162
+ )}
163
+ {/* 距离标签 */}
164
+ <div className="absolute top-[var(--spacing-sm)] left-[var(--spacing-sm)] flex items-center gap-[3px] bg-black/40 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px]" style={{ zIndex: 3 }}>
165
+ <IconFont name="location" size="10px" className="text-white/90" />
166
+ <span className="text-[11px] text-white/90 leading-[14px]">距你{card.distance}</span>
167
+ </div>
168
+ </div>
169
+ );
170
+ }
171
+
172
+ function TopicImageLayout({ card }: { card: POICard }) {
173
+ return (
174
+ <div className="relative w-full h-[200px] overflow-hidden">
175
+ {/* 主图铺满 */}
176
+ <ImageWithFallback
177
+ src={card.images[0]}
178
+ alt={card.title}
179
+ className="w-full h-full object-cover"
180
+ />
181
+ <div className="absolute inset-x-0 bottom-0 h-[80px] bg-gradient-to-t from-black/40 to-transparent" />
182
+ {/* 距离标签 */}
183
+ <div className="absolute top-[var(--spacing-sm)] left-[var(--spacing-sm)] flex items-center gap-[3px] bg-black/40 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px]">
184
+ <IconFont name="location" size="10px" className="text-white/90" />
185
+ <span className="text-[11px] text-white/90 leading-[14px]">距你{card.distance}</span>
186
+ </div>
187
+ {/* 右上角徽章 */}
188
+ {card.badge && (
189
+ <div className="absolute top-[var(--spacing-sm)] right-[var(--spacing-sm)] flex items-center gap-[3px] bg-accent/90 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px]">
190
+ <IconFont name="flag" size="10px" className="text-white" />
191
+ <span className="text-[11px] text-white font-medium leading-[14px]">{card.badge}</span>
192
+ </div>
193
+ )}
194
+ {/* 左下角场景标签 */}
195
+ {card.tags[0] && (
196
+ <div className="absolute bottom-[var(--spacing-sm)] left-[var(--spacing-sm)] flex items-center gap-[3px] bg-white/85 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px] shadow-sm">
197
+ <IconFont name="location" size="10px" className="text-accent" />
198
+ <span className="text-[11px] text-foreground/80 font-medium leading-[14px]">{card.tags[0].label}</span>
199
+ </div>
200
+ )}
201
+ </div>
202
+ );
203
+ }
204
+
205
+ function ExploreImageLayout({ card }: { card: POICard }) {
206
+ return (
207
+ <div className="relative w-full h-[200px] overflow-hidden bg-card-muted">
208
+ {/* 主图(左侧偏大,微倾) */}
209
+ <div
210
+ className="absolute top-[10px] left-[8px] w-[160px] h-[170px] rounded-[14px] overflow-hidden shadow-md"
211
+ style={{ transform: "rotate(-2.5deg)", zIndex: 1 }}
212
+ >
213
+ <ImageWithFallback
214
+ src={card.images[0]}
215
+ alt={card.title}
216
+ className="w-full h-full object-cover"
217
+ />
218
+ </div>
219
+ {/* 右上小图(正向倾斜,压在主图上方) */}
220
+ <div
221
+ className="absolute top-[6px] right-[10px] w-[120px] h-[90px] rounded-[12px] overflow-hidden shadow-lg border-[2px] border-white"
222
+ style={{ transform: "rotate(4deg)", zIndex: 2 }}
223
+ >
224
+ <ImageWithFallback
225
+ src={card.images[1] || card.images[0]}
226
+ alt={card.title}
227
+ className="w-full h-full object-cover"
228
+ />
229
+ </div>
230
+ {/* 右下小图(反向倾斜,与右上图错开) */}
231
+ <div
232
+ className="absolute bottom-[6px] right-[18px] w-[110px] h-[85px] rounded-[12px] overflow-hidden shadow-lg border-[2px] border-white"
233
+ style={{ transform: "rotate(-3deg)", zIndex: 3 }}
234
+ >
235
+ <ImageWithFallback
236
+ src={card.images[2] || card.images[0]}
237
+ alt={card.title}
238
+ className="w-full h-full object-cover"
239
+ />
240
+ </div>
241
+ {/* 距离标签 */}
242
+ <div className="absolute top-[var(--spacing-sm)] left-[var(--spacing-sm)] flex items-center gap-[3px] bg-black/40 backdrop-blur-sm rounded-[8px] px-[8px] py-[4px]" style={{ zIndex: 4 }}>
243
+ <IconFont name="location" size="10px" className="text-white/90" />
244
+ <span className="text-[11px] text-white/90 leading-[14px]">距你{card.distance}</span>
245
+ </div>
246
+ </div>
247
+ );
248
+ }
249
+
250
+ /* ══════════════════════════════════════════
251
+ 推荐卡片组件
252
+ ══════════════════════════════════════════ */
253
+
254
+ function RecommendCard({
255
+ card,
256
+ onTap,
257
+ onAiPrompt,
258
+ }: {
259
+ card: POICard;
260
+ onTap: () => void;
261
+ onAiPrompt: (prompt: string) => void;
262
+ }) {
263
+ return (
264
+ <div
265
+ className={cn(
266
+ "flex flex-col shrink-0 w-[280px] cursor-pointer group",
267
+ "bg-card rounded-[var(--radius-card)]",
268
+ "border border-card-border",
269
+ "shadow-[var(--elevation-sm)]",
270
+ "overflow-hidden"
271
+ )}
272
+ onClick={onTap}
273
+ >
274
+ {/* 图片区域 — 根据类型渲染不同布局 */}
275
+ {card.type === "route" && <RouteImageLayout card={card} />}
276
+ {card.type === "single" && <SingleImageLayout card={card} />}
277
+ {card.type === "topic" && <TopicImageLayout card={card} />}
278
+ {card.type === "explore" && <ExploreImageLayout card={card} />}
279
+
280
+ {/* 卡片内容区域 */}
281
+ <div className="flex flex-col p-[var(--spacing-base)]">
282
+ {/* 标题 */}
283
+ <h3 className="text-[15px] font-semibold text-foreground leading-[22px] line-clamp-2 mb-[var(--spacing-xs)]">
284
+ {card.title}
285
+ </h3>
286
+
287
+ {/* 标签行 */}
288
+ <div className="flex flex-wrap gap-[4px] mb-[var(--spacing-sm)]">
289
+ {card.tags.map((tag, i) => (
290
+ <Tag
291
+ key={i}
292
+ label={tag.label}
293
+ size="xs"
294
+ icon={
295
+ tag.icon === "flag" ? (
296
+ <IconFont name="flag" size="10px" className={tag.accent ? "text-destructive" : "text-muted-foreground"} />
297
+ ) : tag.icon === "pin" ? (
298
+ <IconFont name="location" size="10px" className="text-accent" />
299
+ ) : undefined
300
+ }
301
+ className={cn(
302
+ "border-transparent",
303
+ tag.accent
304
+ ? "bg-accent/8 text-foreground/70"
305
+ : "bg-card-muted text-muted-foreground"
306
+ )}
307
+ />
308
+ ))}
309
+ </div>
310
+
311
+ {/* AI 追问提示 */}
312
+ <button
313
+ onClick={(e) => {
314
+ e.stopPropagation();
315
+ onAiPrompt(card.aiPrompt);
316
+ }}
317
+ className={cn(
318
+ "flex items-center gap-[6px] px-[var(--spacing-sm)] py-[6px]",
319
+ "bg-card-muted rounded-[var(--radius)] w-full",
320
+ "active:bg-muted transition-colors text-left"
321
+ )}
322
+ >
323
+ <IconFont name="star" size="12px" className="text-accent shrink-0" />
324
+ <span className="text-[12px] text-muted-foreground leading-[16px] truncate flex-1">
325
+ {card.aiPrompt}
326
+ </span>
327
+ </button>
328
+ </div>
329
+ </div>
330
+ );
331
+ }
332
+
333
+ /* ══════════════════════════════════════════
334
+ AI 输入栏组件
335
+ ══════════════════════════════════════════ */
336
+
337
+ function AIInputBar({
338
+ value,
339
+ onChange,
340
+ onSend,
341
+ placeholder,
342
+ }: {
343
+ value: string;
344
+ onChange: (v: string) => void;
345
+ onSend: () => void;
346
+ placeholder: string;
347
+ }) {
348
+ return (
349
+ <div
350
+ className={cn(
351
+ "flex items-center gap-[var(--spacing-xs)] px-[var(--spacing-md)] py-[var(--spacing-xs)]",
352
+ "bg-card backdrop-blur-xl rounded-[var(--radius-button)]",
353
+ "shadow-[var(--elevation-sm)]",
354
+ "border border-card-border"
355
+ )}
356
+ >
357
+ {/* 声纹图标 */}
358
+ <div className="shrink-0">
359
+ <IconFont name="sound" size="18px" className="text-accent" />
360
+ </div>
361
+
362
+ {/* 输入框 */}
363
+ <input
364
+ type="text"
365
+ value={value}
366
+ onChange={(e) => onChange(e.target.value)}
367
+ onKeyDown={(e) => {
368
+ if (e.key === "Enter" && value.trim()) {
369
+ onSend();
370
+ }
371
+ }}
372
+ placeholder={placeholder}
373
+ className={cn(
374
+ "flex-1 bg-transparent border-none outline-none",
375
+ "text-[14px] text-foreground placeholder:text-muted-foreground/70",
376
+ "min-w-0"
377
+ )}
378
+ />
379
+
380
+ {/* 发送按钮 */}
381
+ {value.trim() && (
382
+ <motion.button
383
+ initial={{ scale: 0, opacity: 0 }}
384
+ animate={{ scale: 1, opacity: 1 }}
385
+ exit={{ scale: 0, opacity: 0 }}
386
+ onClick={onSend}
387
+ className="size-[32px] rounded-full bg-accent flex items-center justify-center shrink-0 active:opacity-80 transition-opacity"
388
+ >
389
+ <IconFont name="send" size="16px" className="text-white" />
390
+ </motion.button>
391
+ )}
392
+ </div>
393
+ );
394
+ }
395
+
396
+ /* ══════════════════════════════════════════
397
+ AI 生成结果面板
398
+ ══════════════════════════════════════════ */
399
+
400
+ interface AIResult {
401
+ name: string;
402
+ address: string;
403
+ reason: string;
404
+ distance: string;
405
+ rating: string;
406
+ }
407
+
408
+ const mockAIResults: AIResult[] = [
409
+ { name: "胡桃里音乐酒馆", address: "国贸商城B1层", reason: "现场驻唱 + 创意菜,适合4人聚餐", distance: "800m", rating: "4.8" },
410
+ { name: "海底捞(国贸店)", address: "建国门外大街1号", reason: "下班后聚餐首选,不用排队", distance: "1.2km", rating: "4.7" },
411
+ { name: "探鱼(三里屯店)", address: "三里屯路19号", reason: "烤鱼配啤酒,性价比高", distance: "2.1km", rating: "4.6" },
412
+ ];
413
+
414
+ /* ══════════════════════════════════════════
415
+ 主页面
416
+ ══════════════════════════════════════════ */
417
+
418
+ export function POIRecommendPage() {
419
+ const [inputValue, setInputValue] = useState("");
420
+ const [toastText, setToastText] = useState<string | null>(null);
421
+ const [aiGenerating, setAiGenerating] = useState(false);
422
+ const [aiResults, setAiResults] = useState<AIResult[] | null>(null);
423
+ const [currentPrompt, setCurrentPrompt] = useState("");
424
+ const scrollRef = useRef<HTMLDivElement>(null);
425
+
426
+ const showToast = useCallback((text: string) => {
427
+ setToastText(text);
428
+ }, []);
429
+
430
+ useEffect(() => {
431
+ if (!toastText) return;
432
+ const timer = setTimeout(() => setToastText(null), 2200);
433
+ return () => clearTimeout(timer);
434
+ }, [toastText]);
435
+
436
+ const handleSend = useCallback(() => {
437
+ if (!inputValue.trim()) return;
438
+ setCurrentPrompt(inputValue);
439
+ setInputValue("");
440
+ setAiGenerating(true);
441
+ setAiResults(null);
442
+
443
+ // 模拟 AI 生成
444
+ setTimeout(() => {
445
+ setAiGenerating(false);
446
+ setAiResults(mockAIResults);
447
+ }, 2000);
448
+ }, [inputValue]);
449
+
450
+ const handleAiPrompt = useCallback((prompt: string) => {
451
+ setInputValue(prompt.replace(/^"|"$/g, ""));
452
+ }, []);
453
+
454
+ const handleCloseAI = useCallback(() => {
455
+ setAiResults(null);
456
+ setCurrentPrompt("");
457
+ }, []);
458
+
459
+ return (
460
+ <div
461
+ className="relative w-full h-[100dvh] overflow-hidden flex flex-col"
462
+ style={{ fontFamily: "var(--font-family-sans)" }}
463
+ >
464
+ {/* ── 渐变背景 ── */}
465
+ <div className="absolute inset-0 bg-gradient-to-b from-[#E8EEF8] via-[#F0F4FA] to-background" />
466
+ {/* 装饰性模糊光斑 */}
467
+ <div className="absolute top-[-60px] right-[-40px] w-[200px] h-[200px] bg-accent/8 rounded-full blur-[80px]" />
468
+ <div className="absolute top-[100px] left-[-60px] w-[180px] h-[180px] bg-[#F0D6A0]/15 rounded-full blur-[70px]" />
469
+
470
+ {/* ── 主内容区 ── */}
471
+ <div className="relative z-10 flex-1 flex flex-col overflow-hidden">
472
+
473
+ {/* ── 顶部标题区 ── */}
474
+ <div className="px-[var(--spacing-lg)] pt-[56px] pb-[var(--spacing-sm)]">
475
+ {/* 标题 */}
476
+ <div className="flex items-start justify-between">
477
+ <div>
478
+ <div className="flex items-center gap-[6px] mb-[2px]">
479
+ <IconFont name="cherry" size="22px" className="text-[#C57B3C]" />
480
+ <h1 className="text-[22px] font-bold text-foreground leading-[30px]">
481
+ 秋色正好
482
+ </h1>
483
+ </div>
484
+ <h2 className="text-[22px] font-bold text-foreground leading-[30px]">
485
+ 去这些地方兜兜风
486
+ </h2>
487
+ </div>
488
+
489
+ {/* 当前位置 */}
490
+ <div className="flex items-center gap-[4px] mt-[6px] shrink-0">
491
+ <IconFont name="location" size="14px" className="text-accent" />
492
+ <span className="text-[12px] text-muted-foreground leading-[16px]">
493
+ 你正在 <span className="text-accent font-medium">SKP(国贸店)</span> 附近
494
+ </span>
495
+ </div>
496
+ </div>
497
+ </div>
498
+
499
+ {/* ── 卡片横向滚动区 ── */}
500
+ <div className="flex-1 min-h-0 relative">
501
+ <div
502
+ ref={scrollRef}
503
+ className="flex gap-[var(--spacing-base)] overflow-x-auto px-[var(--spacing-lg)] pb-[var(--spacing-base)] pt-[var(--spacing-xs)] snap-x snap-mandatory scrollbar-hide"
504
+ style={{ scrollbarWidth: "none", msOverflowStyle: "none" }}
505
+ >
506
+ {poiCards.map((card) => (
507
+ <div key={card.id} className="snap-start">
508
+ <RecommendCard
509
+ card={card}
510
+ onTap={() => showToast(`查看「${card.title.slice(0, 8)}...」详情`)}
511
+ onAiPrompt={handleAiPrompt}
512
+ />
513
+ </div>
514
+ ))}
515
+ {/* 右侧留白 */}
516
+ <div className="shrink-0 w-[var(--spacing-lg)]" />
517
+ </div>
518
+
519
+ {/* 右侧播放按钮(自动播放指示) */}
520
+ <div className="absolute right-[var(--spacing-base)] top-[var(--spacing-xs)]">
521
+ <button
522
+ className="size-[36px] rounded-full bg-accent flex items-center justify-center shadow-md active:opacity-80 transition-opacity"
523
+ onClick={() => showToast("自动播放推荐中...")}
524
+ >
525
+ <IconFont name="play-circle" size="20px" className="text-white" />
526
+ </button>
527
+ </div>
528
+ </div>
529
+
530
+ {/* ── AI 结果面板 ── */}
531
+ <AnimatePresence>
532
+ {(aiGenerating || aiResults) && (
533
+ <motion.div
534
+ initial={{ opacity: 0, y: 40 }}
535
+ animate={{ opacity: 1, y: 0 }}
536
+ exit={{ opacity: 0, y: 40 }}
537
+ transition={{ type: "spring", damping: 25, stiffness: 200 }}
538
+ className="absolute inset-x-0 bottom-[80px] z-30 mx-[var(--spacing-base)]"
539
+ >
540
+ <div
541
+ className={cn(
542
+ "rounded-[var(--radius-card)] overflow-hidden",
543
+ "bg-card backdrop-blur-xl",
544
+ "shadow-[var(--elevation-sm)]",
545
+ "border border-card-border"
546
+ )}
547
+ >
548
+ {/* 头部 */}
549
+ <div className="flex items-center justify-between px-[var(--spacing-base)] pt-[var(--spacing-base)] pb-[var(--spacing-xs)]">
550
+ <div className="flex items-center gap-[6px]">
551
+ <IconFont name="star" size="14px" className="text-accent" />
552
+ <span className="text-[14px] font-medium text-foreground leading-[20px]">
553
+ AI 推荐
554
+ </span>
555
+ {currentPrompt && (
556
+ <span className="text-[12px] text-muted-foreground ml-[4px] truncate max-w-[180px]">
557
+ 「{currentPrompt}」
558
+ </span>
559
+ )}
560
+ </div>
561
+ <button
562
+ onClick={handleCloseAI}
563
+ className="text-muted-foreground active:opacity-60 text-[12px]"
564
+ >
565
+ 关闭
566
+ </button>
567
+ </div>
568
+
569
+ {/* 内容 */}
570
+ <div className="px-[var(--spacing-base)] pb-[var(--spacing-base)] max-h-[280px] overflow-y-auto">
571
+ {aiGenerating ? (
572
+ <div className="flex flex-col items-center py-[var(--spacing-xl)]">
573
+ <div className="flex items-center gap-[8px] mb-[var(--spacing-sm)]">
574
+ <motion.div
575
+ className="size-[8px] rounded-full bg-accent"
576
+ animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
577
+ transition={{ repeat: Infinity, duration: 1, delay: 0 }}
578
+ />
579
+ <motion.div
580
+ className="size-[8px] rounded-full bg-accent"
581
+ animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
582
+ transition={{ repeat: Infinity, duration: 1, delay: 0.2 }}
583
+ />
584
+ <motion.div
585
+ className="size-[8px] rounded-full bg-accent"
586
+ animate={{ scale: [1, 1.5, 1], opacity: [0.5, 1, 0.5] }}
587
+ transition={{ repeat: Infinity, duration: 1, delay: 0.4 }}
588
+ />
589
+ </div>
590
+ <p className="text-[13px] text-muted-foreground">正在为你生成推荐...</p>
591
+ </div>
592
+ ) : aiResults ? (
593
+ <div className="flex flex-col gap-[var(--spacing-xs)]">
594
+ {aiResults.map((result, index) => (
595
+ <motion.button
596
+ key={result.name}
597
+ initial={{ opacity: 0, x: -12 }}
598
+ animate={{ opacity: 1, x: 0 }}
599
+ transition={{ delay: index * 0.1 }}
600
+ onClick={() => showToast(`正在导航至${result.name}...`)}
601
+ className={cn(
602
+ "flex items-center gap-[var(--spacing-sm)] p-[var(--spacing-sm)] text-left",
603
+ "rounded-[16px] bg-card-muted active:bg-muted transition-colors"
604
+ )}
605
+ >
606
+ {/* 序号 */}
607
+ <div
608
+ className={cn(
609
+ "size-[28px] rounded-full flex items-center justify-center shrink-0 text-[12px] font-bold",
610
+ index === 0
611
+ ? "bg-accent text-white"
612
+ : "bg-muted text-muted-foreground"
613
+ )}
614
+ >
615
+ {index + 1}
616
+ </div>
617
+
618
+ {/* 信息 */}
619
+ <div className="flex-1 min-w-0">
620
+ <div className="flex items-center gap-[6px]">
621
+ <span className="text-[14px] font-medium text-foreground leading-[20px] truncate">
622
+ {result.name}
623
+ </span>
624
+ <span className="text-[12px] text-accent font-medium shrink-0">
625
+ {result.rating}分
626
+ </span>
627
+ </div>
628
+ <p className="text-[12px] text-muted-foreground leading-[16px] truncate mt-[1px]">
629
+ {result.reason}
630
+ </p>
631
+ </div>
632
+
633
+ {/* 距离 + 导航 */}
634
+ <div className="flex flex-col items-end shrink-0 gap-[2px]">
635
+ <span className="text-[12px] text-accent font-medium">{result.distance}</span>
636
+ <Btn size="xsmall" variant="primary" label="去这里" icon={null} />
637
+ </div>
638
+ </motion.button>
639
+ ))}
640
+ </div>
641
+ ) : null}
642
+ </div>
643
+ </div>
644
+ </motion.div>
645
+ )}
646
+ </AnimatePresence>
647
+
648
+ {/* ── 底部 AI 输入栏 ── */}
649
+ <div className="relative z-40 px-[var(--spacing-lg)] pb-[var(--spacing-lg)] pt-[var(--spacing-xs)]">
650
+ <AIInputBar
651
+ value={inputValue}
652
+ onChange={setInputValue}
653
+ onSend={handleSend}
654
+ placeholder="适合下班后和同事聚餐的地方,4个人"
655
+ />
656
+ </div>
657
+ </div>
658
+
659
+ {/* ── Toast ── */}
660
+ <div className="fixed top-[120px] left-0 right-0 z-[200] flex justify-center pointer-events-none">
661
+ <AnimatePresence>
662
+ {toastText && (
663
+ <div className="pointer-events-auto">
664
+ <Toast
665
+ lines={[toastText]}
666
+ showIcon={true}
667
+ showClose={true}
668
+ onClose={() => setToastText(null)}
669
+ />
670
+ </div>
671
+ )}
672
+ </AnimatePresence>
673
+ </div>
674
+ </div>
675
+ );
676
+ }