diff --git a/app/page.tsx b/app/page.tsx index 0b0dabe..031be03 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,36 +1,72 @@ "use client"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import DraggablePanel from "@/components/DraggablePanel"; import Logo from "@/components/Logo"; import Preview from "@/components/Draggable/Preview"; import Draggable from "@/components/Draggable/Draggable"; -import { useComponentsStore } from "@/hooks/useComponentsStore"; +import PreviewStore from "@/stores/previewStore"; +import { useObserver } from "mobx-react-lite"; +import ComponentsStore from "@/stores/componentStore"; export default function Home() { const [isChangeSize, setIsSizeChangeSize] = useState(false); const [isDraggable, setIsDraggable] = useState(false); - const [units, setUnit] = useComponentsStore([ - { - id: "1", - x: 0, - y: 0, - width: 320, - height: 160, - component: ()=> - }, - { - id: "2", - x: 336, - y: 0, - width: 160, - height: 320, - component: ()=> - }, - ]); + const { components } = ComponentsStore; + useEffect(() => { + ComponentsStore.initComponent([ + { + id: "1", + x: 0, + y: 0, + width: 320, + height: 160, + component: () => + }, + { + id: "2", + x: 336, + y: 0, + width: 160, + height: 320, + component: () => + }, + { + id: "3", + x: 336, + y: 0, + width: 160, + height: 320, + component: () => + }, + { + id: "4", + x: 336, + y: 0, + width: 160, + height: 320, + component: () => + }, + { + id: "5", + x: 336, + y: 0, + width: 160, + height: 320, + component: () => + }, + { + id: "6", + x: 336, + y: 0, + width: 160, + height: 320, + component: () => + }, + ]); + }, []) - return ( + return useObserver(() => (
-
- {units.map((item) => ( + {ComponentsStore.components.map((item) => ( - ); + )); } diff --git a/components/Draggable/Draggable.tsx b/components/Draggable/Draggable.tsx index e6b2062..6f8f45f 100644 --- a/components/Draggable/Draggable.tsx +++ b/components/Draggable/Draggable.tsx @@ -1,38 +1,35 @@ "use client"; import "@/app/globals.css"; +import _ from "lodash"; + import { useDraggable } from "@dnd-kit/core"; import { ReactElement, useEffect, useRef, useState } from "react"; import { nearestMultiple } from "./utils"; +import PreviewStore from "@/stores/previewStore"; export default function Draggable(props: DraggablePropsType) { const targetRef = useRef(null); + const timerRef = useRef(null); const [size, setSize] = useState({ width: 16, height: 16 }); - const { id, component, data, x, y, width, height } = props; + const { id, component, data, x, y, width:_width, height:_height } = props; + const [width, setWidth] = useState(_width); + const [height, setHeight] = useState(_height); + const { attributes, listeners, setNodeRef, transform } = useDraggable({ id, + data: props }); const style = transform ? { - top: y, - left: x, - width: width, - height: height, - transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, - opacity: 0.6, - } - : { - top: y, - left: x, - width: width, - height: height, - }; + transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`, + opacity: 0.3, + } + : {}; useEffect(() => { const element = targetRef.current; if (!element) return; - // 创建 ResizeObserver 实例 - let timer: any; const observer = new ResizeObserver( _.throttle((entries: any) => { for (const entry of entries) { @@ -41,33 +38,43 @@ export default function Draggable(props: DraggablePropsType) { width: nearestMultiple(width), height: nearestMultiple(height), }); - if (timer) { - clearTimeout(timer); + const x = nearestMultiple(element.offsetLeft); + const y = nearestMultiple(element.offsetTop); + PreviewStore.changePreviewX(x); + PreviewStore.changePreviewY(y); + PreviewStore.changePreviewWidth(nearestMultiple(width)); + PreviewStore.changePreviewHeight(nearestMultiple(height)); + if (timerRef.current) { + clearTimeout(timerRef.current); } - // setIsResize(true); - timer = setTimeout(() => { - entry.target.style.width = nearestMultiple(width) + "px"; - entry.target.style.height = nearestMultiple(height) + "px"; - // syncSize(entry.contentRect, containerRef.current); - // setIsResize(false); - }, 150); + timerRef.current = setTimeout(() => { + console.log(PreviewStore.width, PreviewStore.height); + setWidth(PreviewStore.width); + setHeight(PreviewStore.height); + }, 2000); } }, 30), ); - // 开始观察元素 observer.observe(element); - - // 组件卸载时断开连接 return () => { observer.disconnect(); }; - }, []); // 空依赖数组确保只运行一次 + }, []); const className = transform ? "shadow-xl absolute w-min min-w-[128px] min-h-[128px]" : "absolute w-min resize overflow-hidden min-w-[128px] min-h-[128px]"; return ( -
+
{ + setNodeRef(el); // DnD 的 ref + (targetRef as React.MutableRefObject).current = el; + }} style={{ + top: y, + left: x, + width: width, + height: height, + ...style, + }} {...attributes} title={`${width}x${height}`}> - {component && component(data??{})} + {component && component(data ?? {})}
); } diff --git a/components/Draggable/Preview.tsx b/components/Draggable/Preview.tsx index 1c5bfdf..2d194ab 100644 --- a/components/Draggable/Preview.tsx +++ b/components/Draggable/Preview.tsx @@ -13,17 +13,21 @@ export default function Preview(props: PreviewPropsType) { useEffect(() => { setStyle({ - top: x, - left: y, + top: y, + left: x, width: width, height: height, + // visibility: width * height === 0 ? 'hidden' : 'none' }); - }, [height, props, width, x, y]) + }, [props.height, props.width, props.x, props.y]) return (
+ > + {width * height === 0 || `${width} * ${height}`} + {x * y === 0 || `${x}, ${y}`} +
); } diff --git a/components/DraggablePanel.tsx b/components/DraggablePanel.tsx index 7e92fba..1e6f2cc 100644 --- a/components/DraggablePanel.tsx +++ b/components/DraggablePanel.tsx @@ -2,6 +2,12 @@ import classnames from "classnames"; import { DndContext } from "@dnd-kit/core"; import { Droppable } from "./Droppable"; +import PreviewStore from "@/stores/previewStore"; +import ComponentStore from '@/stores/componentStore'; +import Preview from "./Draggable/Preview"; +import { useObserver } from "mobx-react-lite"; +import { nearestMultiple } from "./Draggable/utils"; +import { ReactElement } from "react"; export default function DraggablePanel(props: DraggablePanelType) { const { draggable, wdith = "full", height = "full", children } = props; @@ -15,13 +21,46 @@ export default function DraggablePanel(props: DraggablePanelType) { "base-100": !draggable, // 当 disabled 为 true 时添加 }, ); - return ( - + return useObserver(() => ( + { + const node = event?.activatorEvent?.target?.parentNode; + const rect = node.getBoundingClientRect(); + const { width, height } = rect; + const x = nearestMultiple(node.offsetLeft + event.delta.x); + const y = nearestMultiple(node.offsetTop + event.delta.y); + PreviewStore.changePreviewX(x); + PreviewStore.changePreviewY(y); + PreviewStore.changePreviewWidth(nearestMultiple(width)); + PreviewStore.changePreviewHeight(nearestMultiple(height)); + }} + onDragEnd={(event) => { + const node = event?.activatorEvent?.target?.parentNode; + const rect = node.getBoundingClientRect(); + const { width, height } = rect; + const x = nearestMultiple(node.offsetLeft + event.delta.x); + const y = nearestMultiple(node.offsetTop + event.delta.y); + PreviewStore.changePreviewX(0); + PreviewStore.changePreviewY(0); + PreviewStore.changePreviewWidth(0); + PreviewStore.changePreviewHeight(0); + ComponentStore.changeComponent({ + id: event?.active?.data?.current?.id, + component: event?.active?.data?.current?.component, + x, + y, + width: nearestMultiple(width), + height: nearestMultiple(height), + }); + }}> -
{children}
+
+ + {children} +
- ); + )); } export type DraggablePanelType = { diff --git a/package.json b/package.json index 331f027..fd6a0b3 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,8 @@ "@heroicons/react": "^2.2.0", "classnames": "^2.5.1", "lodash": "^4.17.21", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", "next": "15.2.3", "next-auth": "5.0.0-beta.25", "react": "^19.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5cc295..e631e62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -210,79 +210,67 @@ packages: resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-arm@1.0.5': resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-s390x@1.0.4': resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-libvips-linux-x64@1.0.4': resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.0.4': resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.0.4': resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-linux-arm64@0.33.5': resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [glibc] '@img/sharp-linux-arm@0.33.5': resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] - libc: [glibc] '@img/sharp-linux-s390x@0.33.5': resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] - libc: [glibc] '@img/sharp-linux-x64@0.33.5': resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [glibc] '@img/sharp-linuxmusl-arm64@0.33.5': resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] - libc: [musl] '@img/sharp-linuxmusl-x64@0.33.5': resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] - libc: [musl] '@img/sharp-wasm32@0.33.5': resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==} @@ -327,28 +315,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@next/swc-linux-arm64-musl@15.2.3': resolution: {integrity: sha512-2gAPA7P652D3HzR4cLyAuVYwYqjG0mt/3pHSWTCyKZq/N/dJcUAEoNQMyUmwTZWCJRKofB+JPuDVP2aD8w2J6Q==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@next/swc-linux-x64-gnu@15.2.3': resolution: {integrity: sha512-ODSKvrdMgAJOVU4qElflYy1KSZRM3M45JVbeZu42TINCMG3anp7YCBn80RkISV6bhzKwcUqLBAmOiWkaGtBA9w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@next/swc-linux-x64-musl@15.2.3': resolution: {integrity: sha512-ZR9kLwCWrlYxwEoytqPi1jhPd1TlsSJWAc+H/CJHmHkf2nD92MQpSRIURR1iNgA/kuFSdxB8xIPt4p/T78kwsg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@next/swc-win32-arm64-msvc@15.2.3': resolution: {integrity: sha512-+G2FrDcfm2YDbhDiObDU/qPriWeiz/9cRR0yMWJeTLGGX6/x8oryO3tt7HhodA1vZ8r2ddJPCjtLcpaVl7TE2Q==} @@ -431,28 +415,24 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.0.14': resolution: {integrity: sha512-gVkJdnR/L6iIcGYXx64HGJRmlme2FGr/aZH0W6u4A3RgPMAb+6ELRLi+UBiH83RXBm9vwCfkIC/q8T51h8vUJQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.0.14': resolution: {integrity: sha512-EE+EQ+c6tTpzsg+LGO1uuusjXxYx0Q00JE5ubcIGfsogSKth8n8i2BcS2wYTQe4jXGs+BQs35l78BIPzgwLddw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.0.14': resolution: {integrity: sha512-KCCOzo+L6XPT0oUp2Jwh233ETRQ/F6cwUnMnR0FvMUCbkDAzHbcyOgpfuAtRa5HD0WbTbH4pVD+S0pn1EhNfbw==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [musl] '@tailwindcss/oxide-win32-arm64-msvc@4.0.14': resolution: {integrity: sha512-AHObFiFL9lNYcm3tZSPqa/cHGpM5wOrNmM2uOMoKppp+0Hom5uuyRh0QkOp7jftsHZdrZUpmoz0Mp6vhh2XtUg==} @@ -570,25 +550,21 @@ packages: resolution: {integrity: sha512-RfYtlCtJrv5i6TO4dSlpbyOJX9Zbhmkqrr9hjDfr6YyE5KD0ywLRzw8UjXsohxG1XWgRpb2tvPuRYtURJwbqWg==} cpu: [arm64] os: [linux] - libc: [glibc] '@unrs/rspack-resolver-binding-linux-arm64-musl@1.1.2': resolution: {integrity: sha512-MaITzkoqsn1Rm3+YnplubgAQEfOt+2jHfFvuFhXseUfcfbxe8Zyc3TM7LKwgv7mRVjIl+/yYN5JqL0cjbnhAnQ==} cpu: [arm64] os: [linux] - libc: [musl] '@unrs/rspack-resolver-binding-linux-x64-gnu@1.1.2': resolution: {integrity: sha512-Nu981XmzQqis/uB3j4Gi3p5BYCd/zReU5zbJmjMrEH7IIRH0dxZpdOmS/+KwEk6ao7Xd8P2D2gDHpHD/QTp0aQ==} cpu: [x64] os: [linux] - libc: [glibc] '@unrs/rspack-resolver-binding-linux-x64-musl@1.1.2': resolution: {integrity: sha512-xJupeDvaRpV0ADMuG1dY9jkOjhUzTqtykvchiU2NldSD+nafSUcMWnoqzNUx7HGiqbTMOw9d9xT8ZiFs+6ZFyQ==} cpu: [x64] os: [linux] - libc: [musl] '@unrs/rspack-resolver-binding-wasm32-wasi@1.1.2': resolution: {integrity: sha512-un6X/xInks+KEgGpIHFV8BdoODHRohaDRvOwtjq+FXuoI4Ga0P6sLRvf4rPSZDvoMnqUhZtVNG0jG9oxOnrrLQ==} @@ -1302,28 +1278,24 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] - libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] - libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} diff --git a/stores/componentStore.ts b/stores/componentStore.ts new file mode 100644 index 0000000..04ac5f3 --- /dev/null +++ b/stores/componentStore.ts @@ -0,0 +1,29 @@ +import { DraggablePropsType } from "@/components/Draggable/Draggable"; +import { makeAutoObservable } from "mobx"; + +class ComponentsStore { + components:DraggablePropsType[] = []; + + constructor() { + makeAutoObservable(this); + } + + initComponent(componentsList:DraggablePropsType[]) { + this.components = componentsList || []; + } + + changeComponent(componentsList:DraggablePropsType) { + this.components = this.components.map(item => { + if (item.id !== componentsList.id) { + return item; + } + return componentsList; + }) + } + + delectComponent(componentsList:DraggablePropsType) { + this.components = this.components.filter(item => item.id !== componentsList.id); + } +} + +export default new ComponentsStore(); \ No newline at end of file diff --git a/stores/counterStore.ts b/stores/counterStore.ts index c7b12e3..1e3a333 100644 --- a/stores/counterStore.ts +++ b/stores/counterStore.ts @@ -1,4 +1,3 @@ -// stores/counterStore.ts import { makeAutoObservable } from "mobx"; export class CounterStore { diff --git a/stores/previewStore.ts b/stores/previewStore.ts new file mode 100644 index 0000000..9f7c946 --- /dev/null +++ b/stores/previewStore.ts @@ -0,0 +1,43 @@ +import { DraggablePropsType } from "@/components/Draggable/Draggable"; +import { makeAutoObservable } from "mobx"; + +class PreviewStore { + width: number = 0; + height: number = 0; + x: number = 0; + y: number = 0; + + constructor() { + makeAutoObservable(this); + } + + changePreview(x: number, y: number, width: number, height: number) { + this.x = x; + this.y = y; + this.width = width; + this.height = height; + } + + changePreviewX(x: number) { + this.x = x; + } + changePreviewY(y: number) { + this.y = y; + } + changePreviewWidth(width: number) { + this.width = width; + } + changePreviewHeight(height: number) { + this.height = height; + } + + clearPreview() { + this.width = 0; + this.height = 0; + this.x = 0; + this.y = 0; + } +} + + +export default new PreviewStore(); \ No newline at end of file diff --git a/stores/storeContext.tsx b/stores/storeContext.tsx index c6cfdeb..d3288cc 100644 --- a/stores/storeContext.tsx +++ b/stores/storeContext.tsx @@ -1,3 +1,4 @@ +"use client"; // stores/storeContext.ts import { createContext, useContext } from "react"; import { CounterStore } from "./counterStore"; @@ -12,7 +13,7 @@ export const initializeStores = (initialData = {}) => { const counterStore = new CounterStore(); // 服务端预取数据注入 - if (initialData.counterStore) { + if (initialData?.counterStore) { counterStore.count = initialData.counterStore.count; }