131 lines
5.5 KiB
TypeScript
131 lines
5.5 KiB
TypeScript
"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>
|
||
);
|
||
}
|