import { rangeDates } from "@/lib/planMeta"; import type { TripItem } from "@/lib/types"; function pad(n: number) { return String(n).padStart(2, "0"); } function formatDate(d: Date): string { return `${d.getFullYear()}${pad(d.getMonth() + 1)}${pad(d.getDate())}`; } function formatStamp(d = new Date()): string { return `${formatDate(d)}T${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}Z`; } function escapeText(s: string): string { return s.replace(/\\/g, "\\\\").replace(/;/g, "\\;").replace(/,/g, "\\,").replace(/\n/g, "\\n"); } export interface IcsStop extends TripItem { offset: number; } /** Build an ICS calendar from plan stops (requires startMonth YYYY-MM). */ export function buildPlanIcs(opts: { title: string; startMonth: string; stops: IcsStop[]; }): string | null { if (!opts.startMonth || opts.stops.length === 0) return null; const now = formatStamp(); const lines = [ "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//nomadro//Move Plan//CN", "CALSCALE:GREGORIAN", "METHOD:PUBLISH", `X-WR-CALNAME:${escapeText(opts.title || "nomadro 旅居计划")}`, ]; opts.stops.forEach((stop, i) => { const range = rangeDates(opts.startMonth, stop.offset, stop.months); if (!range) return; const summary = `${stop.emoji} ${stop.name}, ${stop.country}`; const descParts = [ `停留 ${stop.months} 个月`, `约 ¥${(stop.cost * stop.months).toLocaleString()}`, stop.note ? `备注:${stop.note}` : "", "由 nomadro 旅居计划中心生成", ].filter(Boolean); lines.push( "BEGIN:VEVENT", `UID:nomadro-${stop.slug}-${i}@nomadro.com`, `DTSTAMP:${now}`, `DTSTART;VALUE=DATE:${formatDate(range.start)}`, `DTEND;VALUE=DATE:${formatDate(range.endExclusive)}`, `SUMMARY:${escapeText(summary)}`, `DESCRIPTION:${escapeText(descParts.join(" · "))}`, `LOCATION:${escapeText(`${stop.name}, ${stop.country}`)}`, "END:VEVENT" ); }); lines.push("END:VCALENDAR"); return lines.join("\r\n"); } export function downloadPlanIcs(filename: string, ics: string) { const blob = new Blob([ics], { type: "text/calendar;charset=utf-8" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = filename; a.click(); URL.revokeObjectURL(url); }