feat: 行程活动拖拽排序 + 编辑表单(含高德 POI 搜索)

- 安装 @dnd-kit/core/sortable/utilities/modifiers
- BlindboxPlan: 同天内拖拽排序(PointerSensor + TouchSensor,限垂直轴)
- BlindboxPlan: 点击编辑弹出 sheet modal,支持改时间(type=time)、时长、活动名
- BlindboxPlan: POI 字段改为高德 inputtips 搜索下拉,选中自动填入名称/地址/坐标
- BlindboxPlan: 搜索传入房间坐标 location 参数,结果按距离排序
- BlindboxPlan: 跨天移动通过 select 立即生效
- plan route: 新增 update_plan action,支持 PATCH 保存修改后的 days
- page.tsx: 新增 handlePlanDaysChange,乐观更新本地 state + 失败时回滚
This commit is contained in:
2026-03-02 12:06:50 +08:00
parent df2e373beb
commit 4e6a3e007c
5 changed files with 502 additions and 55 deletions
+72
View File
@@ -8,6 +8,10 @@
"name": "no-whatever", "name": "no-whatever",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
@@ -568,6 +572,74 @@
"node": ">=20.19.0" "node": ">=20.19.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/core": { "node_modules/@emnapi/core": {
"version": "1.8.1", "version": "1.8.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz",
+4
View File
@@ -12,6 +12,10 @@
"test:coverage": "vitest run --coverage" "test:coverage": "vitest run --coverage"
}, },
"dependencies": { "dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@prisma/client": "^6.19.2", "@prisma/client": "^6.19.2",
"bcryptjs": "^3.0.3", "bcryptjs": "^3.0.3",
"canvas-confetti": "^1.9.4", "canvas-confetti": "^1.9.4",
+21 -1
View File
@@ -75,7 +75,7 @@ function computeEndTime(planData: string, now: Date): Date | null {
} }
export const PATCH = apiHandler(async (req) => { export const PATCH = apiHandler(async (req) => {
const { planId, userId, action } = await req.json(); const { planId, userId, action, days } = await req.json();
requireUserId(userId); requireUserId(userId);
if (!planId) throw new ApiError("planId 不能为空"); if (!planId) throw new ApiError("planId 不能为空");
@@ -105,6 +105,26 @@ export const PATCH = apiHandler(async (req) => {
return NextResponse.json({ ok: true }); return NextResponse.json({ ok: true });
} }
if (act === "update_plan") {
if (plan.status !== "active" && plan.status !== "accepted") {
throw new ApiError("只能编辑进行中的计划", 400);
}
if (!Array.isArray(days) || days.length === 0) {
throw new ApiError("days 数据无效", 400);
}
const newPlanData = JSON.stringify({ days });
await prisma.weekendPlan.update({
where: { id: planId },
data: {
planData: newPlanData,
...(plan.status === "accepted"
? { endTime: computeEndTime(newPlanData, new Date()) }
: {}),
},
});
return NextResponse.json({ ok: true });
}
throw new ApiError("无效的操作", 400); throw new ApiError("无效的操作", 400);
}); });
+23
View File
@@ -606,6 +606,27 @@ export default function BlindboxRoomPage() {
router.refresh(); router.refresh();
}, [router]); }, [router]);
const handlePlanDaysChange = useCallback(async (newDays: WeekendPlanData[]) => {
if (!planId || !profile) return;
const prevDays = planDays;
setPlanDays(newDays);
if (planAccepted) {
setActiveContract((prev) => prev ? { ...prev, days: newDays } : prev);
}
try {
const res = await fetch("/api/blindbox/plan", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ planId, userId: profile.id, action: "update_plan", days: newDays }),
});
if (!res.ok) throw new Error((await res.json().catch(() => ({}))).error || "保存失败");
} catch (e) {
setPlanDays(prevDays);
if (planAccepted) setActiveContract((prev) => prev ? { ...prev, days: prevDays } : prev);
toast.show(e instanceof Error ? e.message : "保存失败");
}
}, [planId, profile, planDays, planAccepted, toast]);
/** Non-creator: leave room (remove membership). Creator: delete room (after confirm). */ /** Non-creator: leave room (remove membership). Creator: delete room (after confirm). */
const handleLeaveOrDelete = async () => { const handleLeaveOrDelete = async () => {
if (!confirmLeave) { if (!confirmLeave) {
@@ -1090,6 +1111,8 @@ export default function BlindboxRoomPage() {
days={planDays} days={planDays}
accepted={planAccepted} accepted={planAccepted}
regenerating={generating} regenerating={generating}
onDaysChange={handlePlanDaysChange}
location={room.lng != null && room.lat != null ? `${room.lng},${room.lat}` : undefined}
onAccept={async () => { onAccept={async () => {
setPlanAccepted(true); setPlanAccepted(true);
fireConfetti(); fireConfetti();
+375 -47
View File
@@ -12,10 +12,31 @@ import {
ChevronRight, ChevronRight,
ChevronLeft, ChevronLeft,
CornerDownLeft, CornerDownLeft,
GripVertical,
Pencil,
X,
Loader2,
} from "lucide-react"; } from "lucide-react";
import {
DndContext,
DragEndEvent,
PointerSensor,
TouchSensor,
useSensor,
useSensors,
} from "@dnd-kit/core";
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
arrayMove,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { CategoryBadge } from "@/components/BlindboxMyIdeas"; import { CategoryBadge } from "@/components/BlindboxMyIdeas";
import Button from "@/components/Button"; import Button from "@/components/Button";
import type { WeekendPlanData } from "@/types"; import Modal from "@/components/Modal";
import type { WeekendPlanData, PlanItem } from "@/types";
interface BlindboxPlanProps { interface BlindboxPlanProps {
days: WeekendPlanData[]; days: WeekendPlanData[];
@@ -25,6 +46,9 @@ interface BlindboxPlanProps {
onBack: () => void; onBack: () => void;
accepted?: boolean; accepted?: boolean;
regenerating?: boolean; regenerating?: boolean;
onDaysChange?: (newDays: WeekendPlanData[]) => void;
/** "lng,lat" 格式,用于 POI 搜索附近优先 */
location?: string;
} }
function guessCategory(activity: string): string | null { function guessCategory(activity: string): string | null {
@@ -50,6 +74,187 @@ function formatDuration(minutes: number): string {
return `${minutes}min`; return `${minutes}min`;
} }
interface PoiSuggestion {
id: string;
name: string;
district: string;
address: string;
lat: number;
lng: number;
}
interface PoiSearchFieldProps {
poi: string;
address: string;
onSelect: (s: PoiSuggestion) => void;
location?: string;
}
function PoiSearchField({ poi, address, onSelect, location }: PoiSearchFieldProps) {
const [query, setQuery] = useState(poi);
const [suggestions, setSuggestions] = useState<PoiSuggestion[]>([]);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const lastSelectedRef = useRef(poi);
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
if (timerRef.current) clearTimeout(timerRef.current);
if (!query.trim() || query === lastSelectedRef.current) {
setSuggestions([]);
setOpen(false);
return;
}
timerRef.current = setTimeout(async () => {
setLoading(true);
try {
const params = new URLSearchParams({ keywords: query });
if (location) params.set("location", location);
const res = await fetch(`/api/location/suggest?${params}`);
if (res.ok) {
const data: PoiSuggestion[] = await res.json();
setSuggestions(data);
setOpen(data.length > 0);
}
} catch { /* ignore */ }
finally { setLoading(false); }
}, 400);
return () => { if (timerRef.current) clearTimeout(timerRef.current); };
}, [query]);
return (
<div className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-muted"></span>
<div className="relative">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="搜索地点名称..."
className="h-9 w-full rounded-lg bg-elevated px-3 pr-8 text-sm text-foreground outline-none ring-1 ring-border focus:ring-purple-600"
/>
{loading && (
<Loader2 size={14} className="absolute right-2.5 top-2.5 animate-spin text-muted" />
)}
</div>
{open && suggestions.length > 0 && (
<div className="overflow-hidden rounded-lg bg-elevated ring-1 ring-border">
{suggestions.map((s) => (
<button
key={s.id}
type="button"
onClick={() => {
lastSelectedRef.current = s.name;
setQuery(s.name);
setOpen(false);
setSuggestions([]);
onSelect(s);
}}
className="flex w-full flex-col border-b border-border/40 px-3 py-2 text-left last:border-0 active:bg-purple-600/10"
>
<span className="text-xs font-medium text-foreground">{s.name}</span>
<span className="truncate text-[10px] text-muted">{s.district} {s.address}</span>
</button>
))}
</div>
)}
{address && !open && (
<p className="flex items-center gap-1 text-[10px] text-dim">
<MapPin size={9} className="shrink-0" />
{address}
</p>
)}
</div>
);
}
interface SortablePlanItemProps {
id: string;
item: PlanItem;
index: number;
canEdit: boolean;
onEdit: () => void;
}
function SortablePlanItem({ id, item, canEdit, onEdit }: SortablePlanItemProps) {
const { attributes, listeners, setNodeRef, transform, transition, isDragging } =
useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.5 : 1,
};
return (
<div ref={setNodeRef} style={style} className="relative mb-4 last:mb-0">
<div className="absolute -left-6 top-3 flex h-[18px] w-[18px] items-center justify-center rounded-full bg-purple-600/20 ring-2 ring-background">
<div className="h-2 w-2 rounded-full bg-purple-400" />
</div>
<div className="rounded-xl bg-surface/80 p-3.5 ring-1 ring-border/80">
<div className="flex items-start gap-2">
{canEdit && (
<button
{...attributes}
{...listeners}
className="mt-1 shrink-0 touch-none cursor-grab text-muted/40 active:cursor-grabbing active:text-muted"
aria-label="拖拽排序"
>
<GripVertical size={14} />
</button>
)}
<span className="mt-0.5 text-sm font-black text-purple-400">{item.time}</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<CategoryBadge category={guessCategory(item.activity)} />
<p className="truncate text-sm font-bold text-heading">{item.activity}</p>
</div>
<div className="mt-1.5 flex items-center gap-1 text-[11px] text-muted">
<MapPin size={10} className="shrink-0" />
<span className="truncate">{item.poi}</span>
</div>
{item.address && (
<p className="mt-0.5 truncate text-[10px] text-dim">{item.address}</p>
)}
<div className="mt-2 flex items-center gap-3">
<span className="flex items-center gap-1 text-[10px] text-dim">
<Clock size={9} />
{formatDuration(item.duration)}
</span>
{item.lat !== 0 && item.lng !== 0 && (
<a
href={`https://uri.amap.com/marker?position=${item.lng},${item.lat}&name=${encodeURIComponent(item.poi)}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-[10px] font-medium text-purple-400/70 active:text-purple-400"
>
<Navigation size={9} />
</a>
)}
</div>
{item.reason && (
<p className="mt-1.5 text-[10px] leading-relaxed text-dim italic">
{item.reason}
</p>
)}
</div>
{canEdit && (
<button
onClick={onEdit}
className="mt-0.5 shrink-0 text-muted/40 active:text-purple-400"
aria-label="编辑"
>
<Pencil size={13} />
</button>
)}
</div>
</div>
</div>
);
}
export default function BlindboxPlan({ export default function BlindboxPlan({
days, days,
onAccept, onAccept,
@@ -58,17 +263,81 @@ export default function BlindboxPlan({
onBack, onBack,
accepted, accepted,
regenerating, regenerating,
onDaysChange,
location,
}: BlindboxPlanProps) { }: BlindboxPlanProps) {
const [dayIndex, setDayIndex] = useState(0); const [dayIndex, setDayIndex] = useState(0);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
const currentDay = days[dayIndex]; const currentDay = days[dayIndex];
const hasNext = dayIndex < days.length - 1; const hasNext = dayIndex < days.length - 1;
const hasPrev = dayIndex > 0; const hasPrev = dayIndex > 0;
const canEdit = !!onDaysChange;
const [editingItem, setEditingItem] = useState<{ dayIndex: number; itemIndex: number } | null>(null);
const [draft, setDraft] = useState<PlanItem | null>(null);
useEffect(() => { useEffect(() => {
scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" }); scrollRef.current?.scrollTo({ top: 0, behavior: "smooth" });
}, [dayIndex]); }, [dayIndex]);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
useSensor(TouchSensor, { activationConstraint: { delay: 200, tolerance: 5 } }),
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id || !onDaysChange) return;
const items = days[dayIndex].items;
const ids = items.map((_, i) => `${dayIndex}-${i}`);
const oldIndex = ids.indexOf(String(active.id));
const newIndex = ids.indexOf(String(over.id));
if (oldIndex === -1 || newIndex === -1) return;
const newDays = days.map((day, di) =>
di === dayIndex ? { ...day, items: arrayMove(day.items, oldIndex, newIndex) } : day,
);
onDaysChange(newDays);
}
function handleSaveDraft() {
if (!draft || !editingItem || !onDaysChange) return;
const newDays = days.map((day, di) =>
di !== editingItem.dayIndex
? day
: {
...day,
items: day.items.map((item, ii) =>
ii === editingItem.itemIndex ? draft : item,
),
},
);
onDaysChange(newDays);
setEditingItem(null);
setDraft(null);
}
// Cross-day move: immediately call onDaysChange
function handleMoveToDayIndex(targetDayIndex: number) {
if (!draft || !editingItem || !onDaysChange) return;
if (targetDayIndex === editingItem.dayIndex) return;
const newDays = days.map((day, di) => {
if (di === editingItem.dayIndex) {
return {
...day,
items: day.items.filter((_, ii) => ii !== editingItem.itemIndex),
};
}
if (di === targetDayIndex) {
return { ...day, items: [...day.items, draft] };
}
return day;
});
onDaysChange(newDays);
setEditingItem(null);
setDraft(null);
}
if (!currentDay) return null; if (!currentDay) return null;
return ( return (
@@ -129,60 +398,37 @@ export default function BlindboxPlan({
> >
<div className="absolute left-[9px] top-2 bottom-2 w-px bg-purple-500/20" /> <div className="absolute left-[9px] top-2 bottom-2 w-px bg-purple-500/20" />
<DndContext
key={dayIndex}
sensors={sensors}
modifiers={[restrictToVerticalAxis]}
onDragEnd={handleDragEnd}
>
<SortableContext
items={currentDay.items.map((_, i) => `${dayIndex}-${i}`)}
strategy={verticalListSortingStrategy}
>
{currentDay.items.map((item, i) => ( {currentDay.items.map((item, i) => (
<motion.div <motion.div
key={i} key={`${dayIndex}-${i}`}
className="relative mb-4 last:mb-0"
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.1 + i * 0.08 }} transition={{ delay: 0.1 + i * 0.08 }}
> >
<div className="absolute -left-6 top-3 flex h-[18px] w-[18px] items-center justify-center rounded-full bg-purple-600/20 ring-2 ring-background"> <SortablePlanItem
<div className="h-2 w-2 rounded-full bg-purple-400" /> id={`${dayIndex}-${i}`}
</div> item={item}
index={i}
<div className="rounded-xl bg-surface/80 p-3.5 ring-1 ring-border/80"> canEdit={canEdit}
<div className="flex items-start gap-2"> onEdit={() => {
<span className="mt-0.5 text-sm font-black text-purple-400">{item.time}</span> setEditingItem({ dayIndex, itemIndex: i });
<div className="min-w-0 flex-1"> setDraft({ ...item });
<div className="flex items-center gap-1.5"> }}
<CategoryBadge category={guessCategory(item.activity)} /> />
<p className="truncate text-sm font-bold text-heading">{item.activity}</p>
</div>
<div className="mt-1.5 flex items-center gap-1 text-[11px] text-muted">
<MapPin size={10} className="shrink-0" />
<span className="truncate">{item.poi}</span>
</div>
{item.address && (
<p className="mt-0.5 truncate text-[10px] text-dim">{item.address}</p>
)}
<div className="mt-2 flex items-center gap-3">
<span className="flex items-center gap-1 text-[10px] text-dim">
<Clock size={9} />
{formatDuration(item.duration)}
</span>
{item.lat !== 0 && item.lng !== 0 && (
<a
href={`https://uri.amap.com/marker?position=${item.lng},${item.lat}&name=${encodeURIComponent(item.poi)}`}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-[10px] font-medium text-purple-400/70 active:text-purple-400"
>
<Navigation size={9} />
</a>
)}
</div>
{item.reason && (
<p className="mt-1.5 text-[10px] leading-relaxed text-dim italic">
{item.reason}
</p>
)}
</div>
</div>
</div>
</motion.div> </motion.div>
))} ))}
</SortableContext>
</DndContext>
</motion.div> </motion.div>
</AnimatePresence> </AnimatePresence>
@@ -265,6 +511,88 @@ export default function BlindboxPlan({
)} )}
</div> </div>
</div> </div>
{/* Edit item modal */}
<Modal open={!!editingItem && !!draft} onClose={() => { setEditingItem(null); setDraft(null); }} variant="sheet">
{draft && editingItem && (
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold text-heading"></h3>
<button
onClick={() => { setEditingItem(null); setDraft(null); }}
className="text-muted active:text-foreground"
>
<X size={16} />
</button>
</div>
<div className="flex flex-col gap-3">
<label className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-muted"></span>
<input
type="text"
value={draft.activity}
onChange={(e) => setDraft({ ...draft, activity: e.target.value })}
className="h-9 rounded-lg bg-elevated px-3 text-sm text-foreground outline-none ring-1 ring-border focus:ring-purple-600"
/>
</label>
<div className="flex gap-2">
<label className="flex flex-1 flex-col gap-1">
<span className="text-[11px] font-medium text-muted"></span>
<input
type="time"
value={draft.time}
onChange={(e) => setDraft({ ...draft, time: e.target.value })}
className="h-9 rounded-lg bg-elevated px-3 text-sm text-foreground outline-none ring-1 ring-border focus:ring-purple-600"
/>
</label>
<label className="flex flex-1 flex-col gap-1">
<span className="text-[11px] font-medium text-muted"></span>
<input
type="number"
step={15}
min={15}
value={draft.duration}
onChange={(e) => setDraft({ ...draft, duration: Number(e.target.value) })}
className="h-9 rounded-lg bg-elevated px-3 text-sm text-foreground outline-none ring-1 ring-border focus:ring-purple-600"
/>
</label>
</div>
<PoiSearchField
key={`${editingItem.dayIndex}-${editingItem.itemIndex}`}
poi={draft.poi}
address={draft.address}
location={location}
onSelect={(s) => setDraft({ ...draft, poi: s.name, address: s.address, lat: s.lat, lng: s.lng })}
/>
{days.length > 1 && (
<label className="flex flex-col gap-1">
<span className="text-[11px] font-medium text-muted"></span>
<select
value={editingItem.dayIndex}
onChange={(e) => handleMoveToDayIndex(Number(e.target.value))}
className="h-9 rounded-lg bg-elevated px-3 text-sm text-foreground outline-none ring-1 ring-border focus:ring-purple-600"
>
{days.map((day, i) => (
<option key={day.date} value={i}>
{day.date}
{i === editingItem.dayIndex ? "(当前)" : ""}
</option>
))}
</select>
</label>
)}
</div>
<Button onClick={handleSaveDraft} variant="purple" shape="pill">
</Button>
</div>
)}
</Modal>
</div> </div>
); );
} }