diff --git a/package.json b/package.json index d4d0b3b..2814775 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,8 @@ "react-markdown": "^10.1.0", "react-router-dom": "^7.13.0", "remark-gfm": "^4.0.1", + "sweetalert2": "^11.26.24", + "sweetalert2-react-content": "^5.1.2", "typescript": "^5.9.3", "zustand": "^5.0.11" }, diff --git a/src/components/modals/TripEditor.tsx b/src/components/modals/TripEditor.tsx index 0e84954..10e9ce4 100644 --- a/src/components/modals/TripEditor.tsx +++ b/src/components/modals/TripEditor.tsx @@ -1,5 +1,8 @@ +import { useTranslation } from 'react-i18next'; import React, { useState, useEffect } from 'react'; import { Edit2, Plus, X, ListFilter, AlertTriangle, ArrowRightLeft, ArrowDown, Search, Loader2 } from 'lucide-react'; +import { toast } from 'react-hot-toast'; +import { showConfirm } from '../../utils/confirm'; import { useStore, EditorMode } from '../../store'; import { DropZone } from '../DragContext'; import { LineSelector } from './LineSelector'; @@ -46,8 +49,8 @@ export const TripEditor: React.FC = () => { const onSave = () => { // Validation logic const validSegments = form.segments?.filter(s => s.fromId !== s.toId) || []; - if (validSegments.length === 0) { alert(t("tripEdit.atLeastOne", "至少包含一段有效行程")); return; } - if (validSegments.some(s => !s.lineKey || !s.fromId || !s.toId)) { alert(t("tripEdit.fillInfo", "请完善信息")); return; } + if (validSegments.length === 0) { toast.error(t("tripEdit.atLeastOne", "至少包含一段有效行程")); return; } + if (validSegments.some(s => !s.lineKey || !s.fromId || !s.toId)) { toast.error(t("tripEdit.fillInfo", "请完善信息")); return; } // Resolve 'auto' loops to permanent 'up' or 'down' const resolvedSegments = validSegments.map(seg => { @@ -90,7 +93,7 @@ export const TripEditor: React.FC = () => { setTrips(finalTrips); if (user) { - saveData(user.token, finalTrips, pins, folders, badgeSettings).catch((e: any) => alert('云端保存失败: ' + e.message)); + saveData(user.token, finalTrips, pins, folders, badgeSettings).catch((e: any) => toast.error('云端保存失败: ' + e.message)); } closeEditor(); }; @@ -125,7 +128,7 @@ export const TripEditor: React.FC = () => { } }, [autoRouteEasterEggType, isOpen, autoForm, form.date]); - const onAutoSearch = (retryWithInfiniteSearch = false) => { + const onAutoSearch = async (retryWithInfiniteSearch = false) => { const isInfinite = retryWithInfiniteSearch === true; const { startLine, startStation, endLine, endStation } = autoForm; if (!startLine || !startStation || !endLine || !endStation) return; @@ -152,20 +155,20 @@ export const TripEditor: React.FC = () => { if (result.error) { setIsRouteSearching(false); if (!isInfinite && result.error.includes("超出最大换乘次数")) { - setTimeout(() => { - const wantRetry = window.confirm(t("tripEdit.autoMaxLimit", "自动规划超出6次换乘限制或无解。\n是否继续无限制深度搜索?(这可能需要较长等待时间)")); + setTimeout(async () => { + const wantRetry = await showConfirm(t('tripEdit.autoMaxLimitTitle', '换乘超限'), t("tripEdit.autoMaxLimit", "自动规划超出6次换乘限制或无解。\n是否继续无限制深度搜索?(这可能需要较长等待时间)"), t); if (wantRetry) { onAutoSearch(true); } }, 100); } else { setTimeout(() => { - alert(`${t("tripEdit.autoFail", "无法规划: ")}${result.error}`); + toast.error(`${t("tripEdit.autoFail", "无法规划: ")}${result.error}`); }, 100); } } else { - if (result.segments.length > 20) { setIsRouteSearching(false); alert(t("tripEdit.pathTooLong", "路径过长")); return; } + if (result.segments.length > 20) { setIsRouteSearching(false); toast.error(t("tripEdit.pathTooLong", "路径过长")); return; } setForm({ segments: result.segments }); setEditorMode(EditorMode.Manual); setTimeout(() => setIsRouteSearching(false), 200); @@ -254,7 +257,7 @@ export const TripEditor: React.FC = () => { }; const addSegment = () => { - if ((form.segments?.length || 0) >= 10) { alert(t("tripEdit.maxSegment", "最多 10 段")); return; } + if ((form.segments?.length || 0) >= 10) { toast.error(t("tripEdit.maxSegment", "最多 10 段")); return; } setForm({ segments: [...(form.segments || []), { id: Date.now().toString(), lineKey: '', fromId: '', toId: '', loopVia: 'auto' }] }); }; @@ -365,11 +368,11 @@ export const TripEditor: React.FC = () => { {editorMode === EditorMode.Manual && (
- setForm({ date: e.target.value })} /> + setForm({ date: e.target.value })} />
- setForm({ cost: parseInt(e.target.value) || 0 })} /> + setForm({ cost: parseInt(e.target.value) || 0 })} />
@@ -397,7 +400,7 @@ export const TripEditor: React.FC = () => { return (
{idx + 1}
- {idx > 0 && } + {idx > 0 && }
@@ -434,14 +437,14 @@ export const TripEditor: React.FC = () => { setForm({ segments: newSegs }); } }}> - updateSegment(idx, 'fromId', e.target.value)}> {segment.lineKey && railwayData[segment.lineKey]?.stations.map(s => )}
- +
@@ -539,7 +542,7 @@ export const TripEditor: React.FC = () => {
- +
diff --git a/src/components/modals/WalkTripEditor.tsx b/src/components/modals/WalkTripEditor.tsx index d2a25eb..4334d88 100644 --- a/src/components/modals/WalkTripEditor.tsx +++ b/src/components/modals/WalkTripEditor.tsx @@ -1,3 +1,6 @@ +import { showConfirm } from '../../utils/confirm'; +import { toast } from 'react-hot-toast'; +import { useTranslation } from 'react-i18next'; import React, { useEffect } from 'react'; import { Edit2, X, AlertTriangle, Save, Trash2 } from 'lucide-react'; import { useStore } from '../../store'; @@ -6,6 +9,7 @@ import { useUserData } from '../../hooks/useUserData'; import * as turf from '@turf/turf'; export const WalkTripEditor: React.FC = () => { + const { t } = useTranslation(); const { isOpen, isEditing, form, railwayData, trips, pins, folders, badgeSettings, user } = useStore(useShallow(state => ({ @@ -67,7 +71,7 @@ export const WalkTripEditor: React.FC = () => { const onSave = async () => { if (!form.date) return; if (!form.fromId || !form.toId) { - alert(t('walk.noStartEnd', '缺少起止点')); + toast.error(t('walk.noStartEnd', '缺少起止点')); return; } @@ -109,17 +113,18 @@ export const WalkTripEditor: React.FC = () => { setTrips(finalTrips); if (user) { - saveData(user.token, finalTrips, pins, folders, badgeSettings).catch((e: any) => alert(t('walk.saveFail', '云端保存失败: ') + e.message)); + saveData(user.token, finalTrips, pins, folders, badgeSettings).catch((e: any) => toast.error(t('walk.saveFail', '云端保存失败: ') + e.message)); } closeEditor(); }; const onDelete = async () => { - if (!window.confirm(t('walk.delConfirm', "确定要删除这条步行记录吗?"))) return; + const isConfirmed = await showConfirm(t('walk.deleteTitle', '删除步行记录'), t('walk.delConfirm', '确定要删除这条步行记录吗?'), t); + if (!isConfirmed) return; const nextTrips = trips.filter(t => t.id !== form.id); setTrips(nextTrips); if (user) { - saveData(user.token, nextTrips, pins, folders, badgeSettings).catch((e: any) => alert(t('walk.delFail', '云端删除失败: ') + e.message)); + saveData(user.token, nextTrips, pins, folders, badgeSettings).catch((e: any) => toast.error(t('walk.delFail', '云端删除失败: ') + e.message)); } closeEditor(); }; @@ -157,7 +162,7 @@ export const WalkTripEditor: React.FC = () => { {isEditing ? t('walk.editTitle', '编辑步行路线') : t('walk.newTitle', '新建步行路线')} - + @@ -181,26 +186,26 @@ export const WalkTripEditor: React.FC = () => {
- setForm({ date: e.target.value })} className={`w-full border rounded-lg p-2 focus:ring-2 ${colors.ringFocus} outline-none`} /> + setForm({ date: e.target.value })} className={`w-full border rounded-lg p-2 focus:ring-2 ${colors.ringFocus} focus:border-transparent outline-none transition-shadow`} />
-