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
+21 -1
View File
@@ -75,7 +75,7 @@ function computeEndTime(planData: string, now: Date): Date | null {
}
export const PATCH = apiHandler(async (req) => {
const { planId, userId, action } = await req.json();
const { planId, userId, action, days } = await req.json();
requireUserId(userId);
if (!planId) throw new ApiError("planId 不能为空");
@@ -105,6 +105,26 @@ export const PATCH = apiHandler(async (req) => {
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);
});
+23
View File
@@ -606,6 +606,27 @@ export default function BlindboxRoomPage() {
router.refresh();
}, [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). */
const handleLeaveOrDelete = async () => {
if (!confirmLeave) {
@@ -1090,6 +1111,8 @@ export default function BlindboxRoomPage() {
days={planDays}
accepted={planAccepted}
regenerating={generating}
onDaysChange={handlePlanDaysChange}
location={room.lng != null && room.lat != null ? `${room.lng},${room.lat}` : undefined}
onAccept={async () => {
setPlanAccepted(true);
fireConfetti();