包含前端源码、预构建产物(dist/)、零依赖 Node.js 服务器(server.mjs)、 Cloudflare Worker 版本(worker/)、部署文档(DEPLOY.md)和交接文档。 技术总监可直接 clone 后运行 node server.mjs 启动,无需安装 React 或 build。 DEEPSEEK_API_KEY 通过环境变量注入,未纳入仓库。
70 lines
2.1 KiB
TypeScript
70 lines
2.1 KiB
TypeScript
import { useState, type PointerEvent as ReactPointerEvent } from "react";
|
|
|
|
type CursorState = {
|
|
visible: boolean;
|
|
active: boolean;
|
|
x: number;
|
|
y: number;
|
|
};
|
|
|
|
export function useMobileCursor() {
|
|
const debug =
|
|
typeof window !== "undefined" && new URLSearchParams(window.location.search).has("cursorDebug");
|
|
const [cursor, setCursor] = useState<CursorState>({
|
|
visible: false,
|
|
active: false,
|
|
x: 0,
|
|
y: 0,
|
|
});
|
|
|
|
const updatePosition = (event: ReactPointerEvent<HTMLElement>) => {
|
|
const bounds = event.currentTarget.getBoundingClientRect();
|
|
const localX = bounds.width === 0 ? 0 : ((event.clientX - bounds.left) / bounds.width) * event.currentTarget.offsetWidth;
|
|
const localY =
|
|
bounds.height === 0 ? 0 : ((event.clientY - bounds.top) / bounds.height) * event.currentTarget.offsetHeight;
|
|
|
|
setCursor((current) => ({
|
|
...current,
|
|
visible: true,
|
|
x: localX,
|
|
y: localY,
|
|
}));
|
|
};
|
|
|
|
return {
|
|
cursorHandlers: {
|
|
onPointerEnter: updatePosition,
|
|
onPointerMove: updatePosition,
|
|
onPointerDown: (event: ReactPointerEvent<HTMLElement>) => {
|
|
updatePosition(event);
|
|
setCursor((current) => ({ ...current, active: true }));
|
|
},
|
|
onPointerUp: (event: ReactPointerEvent<HTMLElement>) => {
|
|
updatePosition(event);
|
|
setCursor((current) => ({ ...current, active: false }));
|
|
},
|
|
onPointerCancel: () => {
|
|
setCursor((current) => ({ ...current, active: false, visible: false }));
|
|
},
|
|
onPointerLeave: () => {
|
|
setCursor((current) => ({ ...current, active: false, visible: false }));
|
|
},
|
|
},
|
|
cursorDebug: debug,
|
|
cursorElement: (
|
|
<div
|
|
className="mobile-cursor"
|
|
data-active={cursor.active ? "true" : "false"}
|
|
data-debug={debug ? "true" : "false"}
|
|
data-visible={cursor.visible ? "true" : "false"}
|
|
data-testid="mobile-cursor"
|
|
style={{
|
|
transform: `translate3d(${cursor.x}px, ${cursor.y}px, 0) translate(-50%, -50%)`,
|
|
}}
|
|
>
|
|
{debug ? <span className="mobile-cursor-hotspot" aria-hidden="true" /> : null}
|
|
</div>
|
|
),
|
|
};
|
|
}
|