后台设置和内容编辑改成可导航布局;部署用访客 origin 生成 Canonical,避免落到容器地址。 Co-authored-by: Cursor <cursoragent@cursor.com>
70 lines
2.2 KiB
TypeScript
70 lines
2.2 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useRef, useState } from "react";
|
||
import { usePathname, useRouter } from "next/navigation";
|
||
|
||
/**
|
||
* 有未保存修改时:关闭/刷新走 beforeunload;站内链接先确认再跳转。
|
||
* 未修改时不打扰。hash 分类切换由调用方自行拦截。
|
||
*/
|
||
export default function useUnsavedGuard(dirty: boolean) {
|
||
const router = useRouter();
|
||
const pathname = usePathname();
|
||
const dirtyRef = useRef(dirty);
|
||
dirtyRef.current = dirty;
|
||
const [pendingHref, setPendingHref] = useState<string | null>(null);
|
||
|
||
useEffect(() => {
|
||
const onBeforeUnload = (e: BeforeUnloadEvent) => {
|
||
if (!dirtyRef.current) return;
|
||
e.preventDefault();
|
||
e.returnValue = "";
|
||
};
|
||
window.addEventListener("beforeunload", onBeforeUnload);
|
||
return () => window.removeEventListener("beforeunload", onBeforeUnload);
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
const onClick = (e: MouseEvent) => {
|
||
if (!dirtyRef.current) return;
|
||
if (e.defaultPrevented || e.button !== 0 || e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
|
||
return;
|
||
}
|
||
const target = e.target;
|
||
if (!(target instanceof Element)) return;
|
||
const a = target.closest("a[href]");
|
||
if (!a) return;
|
||
const href = a.getAttribute("href");
|
||
if (!href || href.startsWith("#") || href.startsWith("mailto:") || href.startsWith("javascript:")) return;
|
||
let url: URL;
|
||
try {
|
||
url = new URL(href, window.location.href);
|
||
} catch {
|
||
return;
|
||
}
|
||
if (url.origin !== window.location.origin) return;
|
||
const next = `${url.pathname}${url.search}${url.hash}`;
|
||
const cur = `${pathname}${window.location.search}${window.location.hash}`;
|
||
if (next === cur) return;
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
setPendingHref(next);
|
||
};
|
||
document.addEventListener("click", onClick, true);
|
||
return () => document.removeEventListener("click", onClick, true);
|
||
}, [pathname]);
|
||
|
||
const confirmLeave = () => {
|
||
if (!pendingHref) return;
|
||
const href = pendingHref;
|
||
setPendingHref(null);
|
||
router.push(href);
|
||
};
|
||
|
||
return {
|
||
leaveOpen: pendingHref != null,
|
||
confirmLeave,
|
||
cancelLeave: () => setPendingHref(null),
|
||
};
|
||
}
|