nomadweb/frontend/src/components/CompareModal.tsx
eric fd87f765c8 Close discover-decide-plan loop: matcher to trip, shareable /compare hub.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 02:58:40 -05:00

131 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useToast } from "@/lib/toast";
import { COMPARE_METRICS, getWinnerIdx, overallScore, compareUrl } from "@/lib/compareScore";
import { mergeDestinationsIntoTrip } from "@/lib/tripActions";
import type { Destination } from "@/lib/types";
interface Props {
destinations: Destination[];
onClose: () => void;
}
export default function CompareModal({ destinations, onClose }: Props) {
const router = useRouter();
const { toast } = useToast();
const scores = destinations.map(overallScore);
const winnerIdx = scores.indexOf(Math.max(...scores));
const pageHref = compareUrl(destinations.map((d) => d.slug));
const addAllToPlan = () => {
const { added, skipped } = mergeDestinationsIntoTrip(destinations, 1);
onClose();
if (added === 0) {
toast(skipped ? "这些城市已在计划中" : "未能加入", "info");
router.push("/plan");
return;
}
toast(`已加入 ${added} 座城市到计划${skipped ? `(跳过 ${skipped})` : ""}`);
router.push("/plan");
};
const shareCompare = async () => {
const url = `${window.location.origin}${pageHref}`;
await navigator.clipboard.writeText(url);
toast("对比链接已复制");
};
return (
<div className="modal-overlay open" onClick={(e) => e.target === e.currentTarget && onClose()}>
<div className="modal compare-modal">
<button className="modal-close" onClick={onClose} aria-label="关闭">✕</button>
<div className="modal-body compare-modal-body">
<div className="compare-modal-header">
<span className="section-tag">⚖️ COMPARE</span>
<h2>城市对比分析</h2>
<p>可视化对比关键指标,帮你做出最佳选择</p>
</div>
<div className="compare-cards">
{destinations.map((d, i) => (
<div key={d.slug} className={`compare-city-card${i === winnerIdx ? " winner" : ""}`}>
{i === winnerIdx && <span className="compare-winner-badge">👑 综合推荐</span>}
<span className="compare-city-emoji">{d.emoji}</span>
<h3>{d.name}</h3>
<span className="compare-city-country">{d.country}</span>
<div className="compare-score-ring">
<svg viewBox="0 0 80 80">
<circle cx="40" cy="40" r="34" fill="none" stroke="rgba(255,255,255,0.06)" strokeWidth="6" />
<circle
cx="40" cy="40" r="34" fill="none"
stroke="url(#scoreGrad)" strokeWidth="6"
strokeLinecap="round"
strokeDasharray={`${scores[i] * 2.14} 214`}
transform="rotate(-90 40 40)"
/>
</svg>
<span className="compare-score-num">{scores[i]}</span>
</div>
<Link href={`/destinations/${d.slug}`} className="compare-city-link">查看详情 →</Link>
</div>
))}
</div>
<div className="compare-metrics">
{COMPARE_METRICS.map((m) => {
const values = destinations.map(m.get);
const winIdx = getWinnerIdx(values, m.lowerBetter);
const maxVal = Math.max(...values);
return (
<div key={m.label} className="compare-metric-row">
<div className="compare-metric-label">{m.emoji} {m.label}</div>
<div className="compare-metric-bars">
{destinations.map((d, i) => {
const val = m.get(d);
const pct = maxVal > 0 ? (val / maxVal) * 100 : 0;
return (
<div key={d.slug} className="compare-bar-item">
<div className="compare-bar-header">
<span>{d.emoji} {d.name}</span>
<span className={i === winIdx ? "compare-best" : ""}>
{i === winIdx && "🏆 "}{m.format(val)}
</span>
</div>
<div className="compare-bar-track">
<div
className={`compare-bar-fill${i === winIdx ? " best" : ""}`}
style={{ width: `${pct}%`, animationDelay: `${i * 0.1}s` }}
/>
</div>
</div>
);
})}
</div>
</div>
);
})}
</div>
<div className="compare-modal-actions">
<button type="button" className="btn btn-ghost" onClick={shareCompare}>🔗 复制对比链接</button>
<Link href={pageHref} className="btn btn-ghost" onClick={onClose}>打开完整对比页</Link>
<button type="button" className="btn btn-primary" onClick={addAllToPlan}>全部写入计划 →</button>
</div>
<svg width="0" height="0">
<defs>
<linearGradient id="scoreGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#FF6B6B" />
<stop offset="50%" stopColor="#FFE66D" />
<stop offset="100%" stopColor="#4ECDC4" />
</linearGradient>
</defs>
</svg>
</div>
</div>
</div>
);
}