{
if (event.button !== 0) return;
event.preventDefault();
const node = asideEl.current;
if (!node) return;
const width = node.offsetWidth;
dragState.current = { active: true, x: event.clientX, width, latest: width };
event.currentTarget.setPointerCapture(event.pointerId);
if (guideEl.current) {
guideEl.current.style.display = "block";
guideEl.current.style.transform = `translateX(${event.clientX}px)`;
}
document.body.style.cursor = "col-resize";
document.body.style.userSelect = "none";
}}
onPointerMove={(event) => {
if (!dragState.current.active) return;
event.preventDefault();
const node = asideEl.current;
if (!node) return;
const { x, width } = dragState.current;
const containerWidth = node.parentElement?.clientWidth ?? window.innerWidth;
// 给左面板(~296px)与压缩后的中间列表至少保留 ~520px,
// 挤压时列表名称/部门列以 truncate 收缩为"XXX…",不会被挤没。
const max = Math.max(MIN_WIDTH, containerWidth - 520);
// 绝对式计算:始终 = 拖动起始宽 + (起点X - 当前X) 总位移,
// 不可把结果累加回 width——该公式已含总位移,累加会越拖越快。
const next = Math.min(max, Math.max(MIN_WIDTH, width + (x - event.clientX)));
dragState.current.latest = next;
// 指示线停在钳制后的新边界,与松手实际生效宽度一致。
if (guideEl.current) {
guideEl.current.style.transform = `translateX(${x - (next - width)}px)`;
}
}}
onPointerUp={handleDragEnd}
onPointerCancel={handleDragEnd}
/>
)}
{!(isModal && windowState === "fullscreen") && (
{
if ((event.target as HTMLElement).closest("input, button")) return;
setWindowState((current) => (current === "maximized" ? "normal" : "maximized"));
} : undefined}
>
{isModal && navigated && (
)}
{mode === "modal" ? `文件查看 · ${current.documentName}` : current.documentName}
{current.fileExtension?.toUpperCase() || "无扩展名"} · {new Date(current.updatedAt).toLocaleString("zh-CN")}
{isModal && (
)}
{isModal && (
)}
{isModal && (
)}
)}
{isModal && windowState === "fullscreen" && (
)}
{isModal ? (
/* 外层 flex 子项容器把内容区限制在文本面板左侧(textPanel 360px),
absolute inset-0 相对本层定位 */
{/* 内容区:倍率经 contentZoom 传入各渲染面板放大真实布局尺寸,
滚动条随内容增长自然出现,标题栏等面板装饰不参与缩放。
Ctrl+滚轮监听不挂本层——modal 模式统一挂在弹窗遮罩根
(见下方 createPortal 分支),标题栏/提取文本面板/遮罩上的
滚轮同样缩放文档并阻止浏览器原生整页缩放。 */}
{previewChildren}
) : (
/* side 模式同款:外部 rightZoom 的倍率同样只传给内容渲染面板,
标题栏等面板装饰不随缩放(监听仍在 aside 上,事件自内容区冒泡) */
{previewChildren}
)}
{isModal && textPanel}
);
if (mode === "modal") {
// Portal 到 body:modal 弹窗必须脱离调用方布局树。共享资料库(F4AttachmentLibrary)
// 渲染在中栏容器内——那里挂着区域缩放(CSS zoom + Ctrl+滚轮监听),弹窗留在其内
// 会导致一次滚轮双命中两个监听器(文档与整个中栏一同缩放),且 CSS zoom 祖先会把
// fixed 遮罩一并缩放。portal 到 body 后 wheel 冒泡链上只有 modalZoom,视觉不变。
// modalZoom.ref 挂遮罩根:弹窗全域(标题栏/提取文本面板/遮罩)Ctrl+滚轮统一缩放
// 文档并 preventDefault,与 side 模式(监听挂 aside 根)语义一致。
return createPortal(
{panel}
{lightboxSrc &&
setLightboxSrc(null)} />},
document.body,
);
}
// 指示线挂在 aside 外层,与面板同级统一渲染
return (
<>
{panel}
{/* 图片浏览遮罩在内容区之外渲染:docx 缩放后内层带 transform,
transform 祖先会成为 fixed 定位的包含块 */}
{lightboxSrc &&
setLightboxSrc(null)} />}
>
);
}