화면관리 컴포넌트 드래그앤 드롭기능
This commit is contained in:
parent
ca56cff114
commit
6c45686157
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useCallback, useEffect } from "react";
|
import { useState, useCallback, useEffect, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { Separator } from "@/components/ui/separator";
|
import { Separator } from "@/components/ui/separator";
|
||||||
|
|
@ -26,6 +26,8 @@ import {
|
||||||
Settings,
|
Settings,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
List,
|
||||||
|
AlignLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
ScreenDefinition,
|
ScreenDefinition,
|
||||||
|
|
@ -70,13 +72,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
gridSettings: { columns: 12, gap: 16, padding: 16 },
|
gridSettings: { columns: 12, gap: 16, padding: 16 },
|
||||||
});
|
});
|
||||||
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
const [selectedComponent, setSelectedComponent] = useState<ComponentData | null>(null);
|
||||||
const [dragState, setDragState] = useState<DragState>({
|
const [dragState, setDragState] = useState({
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
draggedItem: null,
|
draggedComponent: null as ComponentData | null,
|
||||||
draggedComponent: null,
|
originalPosition: { x: 0, y: 0 },
|
||||||
dragSource: "toolbox",
|
currentPosition: { x: 0, y: 0 },
|
||||||
dropTarget: null,
|
|
||||||
dragOffset: { x: 0, y: 0 },
|
|
||||||
});
|
});
|
||||||
const [groupState, setGroupState] = useState<GroupState>({
|
const [groupState, setGroupState] = useState<GroupState>({
|
||||||
isGrouping: false,
|
isGrouping: false,
|
||||||
|
|
@ -94,6 +94,11 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
const [tables, setTables] = useState<TableInfo[]>([]);
|
const [tables, setTables] = useState<TableInfo[]>([]);
|
||||||
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
const [expandedTables, setExpandedTables] = useState<Set<string>>(new Set());
|
||||||
|
|
||||||
|
// 테이블 검색 및 페이징 상태 추가
|
||||||
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [itemsPerPage] = useState(10);
|
||||||
|
|
||||||
// 테이블 데이터 로드 (실제로는 API에서 가져와야 함)
|
// 테이블 데이터 로드 (실제로는 API에서 가져와야 함)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTables = async () => {
|
const fetchTables = async () => {
|
||||||
|
|
@ -128,6 +133,45 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
fetchTables();
|
fetchTables();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// 검색된 테이블 필터링
|
||||||
|
const filteredTables = useMemo(() => {
|
||||||
|
if (!searchTerm.trim()) return tables;
|
||||||
|
|
||||||
|
return tables.filter(
|
||||||
|
(table) =>
|
||||||
|
table.tableName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
table.tableLabel.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
table.columns.some(
|
||||||
|
(column) =>
|
||||||
|
column.columnName.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
(column.columnLabel || column.columnName).toLowerCase().includes(searchTerm.toLowerCase()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}, [tables, searchTerm]);
|
||||||
|
|
||||||
|
// 페이징된 테이블
|
||||||
|
const paginatedTables = useMemo(() => {
|
||||||
|
const startIndex = (currentPage - 1) * itemsPerPage;
|
||||||
|
const endIndex = startIndex + itemsPerPage;
|
||||||
|
return filteredTables.slice(startIndex, endIndex);
|
||||||
|
}, [filteredTables, currentPage, itemsPerPage]);
|
||||||
|
|
||||||
|
// 총 페이지 수 계산
|
||||||
|
const totalPages = Math.ceil(filteredTables.length / itemsPerPage);
|
||||||
|
|
||||||
|
// 페이지 변경 핸들러
|
||||||
|
const handlePageChange = (page: number) => {
|
||||||
|
setCurrentPage(page);
|
||||||
|
setExpandedTables(new Set()); // 페이지 변경 시 확장 상태 초기화
|
||||||
|
};
|
||||||
|
|
||||||
|
// 검색어 변경 핸들러
|
||||||
|
const handleSearchChange = (value: string) => {
|
||||||
|
setSearchTerm(value);
|
||||||
|
setCurrentPage(1); // 검색 시 첫 페이지로 이동
|
||||||
|
setExpandedTables(new Set()); // 검색 시 확장 상태 초기화
|
||||||
|
};
|
||||||
|
|
||||||
// 임시 테이블 데이터 (API 실패 시 사용)
|
// 임시 테이블 데이터 (API 실패 시 사용)
|
||||||
const getMockTables = (): TableInfo[] => [
|
const getMockTables = (): TableInfo[] => [
|
||||||
{
|
{
|
||||||
|
|
@ -405,47 +449,121 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
}
|
}
|
||||||
}, [layout, selectedScreen]);
|
}, [layout, selectedScreen]);
|
||||||
|
|
||||||
// 드래그 시작
|
// 드래그 시작 (새 컴포넌트 추가)
|
||||||
const startDrag = useCallback((componentData: Partial<ComponentData>, e: React.DragEvent) => {
|
const startDrag = useCallback((component: Partial<ComponentData>, e: React.DragEvent) => {
|
||||||
e.dataTransfer.setData("application/json", JSON.stringify(componentData));
|
setDragState({
|
||||||
setDragState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
draggedComponent: componentData as ComponentData,
|
draggedComponent: component as ComponentData,
|
||||||
}));
|
originalPosition: { x: 0, y: 0 },
|
||||||
|
currentPosition: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
e.dataTransfer.setData("application/json", JSON.stringify(component));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 기존 컴포넌트 드래그 시작 (재배치)
|
||||||
|
const startComponentDrag = useCallback((component: ComponentData, e: React.DragEvent) => {
|
||||||
|
setDragState({
|
||||||
|
isDragging: true,
|
||||||
|
draggedComponent: component,
|
||||||
|
originalPosition: component.position,
|
||||||
|
currentPosition: component.position,
|
||||||
|
});
|
||||||
|
e.dataTransfer.setData("application/json", JSON.stringify({ ...component, isMoving: true }));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 드래그 중
|
||||||
|
const onDragOver = useCallback(
|
||||||
|
(e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (dragState.isDragging) {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / 80) * 80;
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / 60) * 60;
|
||||||
|
|
||||||
|
setDragState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
currentPosition: { x, y },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dragState.isDragging],
|
||||||
|
);
|
||||||
|
|
||||||
|
// 드롭 처리
|
||||||
|
const onDrop = useCallback((e: React.DragEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(e.dataTransfer.getData("application/json"));
|
||||||
|
|
||||||
|
if (data.isMoving) {
|
||||||
|
// 기존 컴포넌트 재배치
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / 80) * 80;
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / 60) * 60;
|
||||||
|
|
||||||
|
setLayout((prev) => ({
|
||||||
|
...prev,
|
||||||
|
components: prev.components.map((comp) => (comp.id === data.id ? { ...comp, position: { x, y } } : comp)),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// 새 컴포넌트 추가
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
const x = Math.floor((e.clientX - rect.left) / 80) * 80;
|
||||||
|
const y = Math.floor((e.clientY - rect.top) / 60) * 60;
|
||||||
|
|
||||||
|
const newComponent: ComponentData = {
|
||||||
|
...data,
|
||||||
|
id: generateComponentId(),
|
||||||
|
position: { x, y },
|
||||||
|
} as ComponentData;
|
||||||
|
|
||||||
|
setLayout((prev) => ({
|
||||||
|
...prev,
|
||||||
|
components: [...prev.components, newComponent],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("드롭 처리 중 오류:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
setDragState({
|
||||||
|
isDragging: false,
|
||||||
|
draggedComponent: null,
|
||||||
|
originalPosition: { x: 0, y: 0 },
|
||||||
|
currentPosition: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 드래그 종료
|
// 드래그 종료
|
||||||
const endDrag = useCallback(() => {
|
const endDrag = useCallback(() => {
|
||||||
setDragState((prev) => ({
|
setDragState({
|
||||||
...prev,
|
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
draggedComponent: null,
|
draggedComponent: null,
|
||||||
}));
|
originalPosition: { x: 0, y: 0 },
|
||||||
|
currentPosition: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 드롭 처리
|
// 컴포넌트 클릭 (선택)
|
||||||
const handleDrop = useCallback(
|
const handleComponentClick = useCallback((component: ComponentData) => {
|
||||||
(e: React.DragEvent) => {
|
setSelectedComponent(component);
|
||||||
e.preventDefault();
|
}, []);
|
||||||
const componentData = JSON.parse(e.dataTransfer.getData("application/json"));
|
|
||||||
|
|
||||||
// 드롭 위치 계산 (그리드 기반)
|
// 컴포넌트 삭제
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const deleteComponent = useCallback(
|
||||||
const x = Math.floor((e.clientX - rect.left) / 80); // 80px = 1 그리드 컬럼
|
(componentId: string) => {
|
||||||
const y = Math.floor((e.clientY - rect.top) / 60); // 60px = 1 그리드 행
|
setLayout((prev) => ({
|
||||||
|
...prev,
|
||||||
addComponent(componentData, { x, y });
|
components: prev.components.filter((comp) => comp.id !== componentId),
|
||||||
endDrag();
|
}));
|
||||||
|
if (selectedComponent?.id === componentId) {
|
||||||
|
setSelectedComponent(null);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[addComponent, endDrag],
|
[selectedComponent],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 드래그 오버 처리
|
|
||||||
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 화면이 선택되지 않았을 때 처리
|
// 화면이 선택되지 않았을 때 처리
|
||||||
if (!selectedScreen) {
|
if (!selectedScreen) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -490,21 +608,57 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
|
|
||||||
{/* 메인 컨텐츠 영역 */}
|
{/* 메인 컨텐츠 영역 */}
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* 좌측: 테이블 타입 관리 */}
|
{/* 좌측 사이드바 - 테이블 타입 */}
|
||||||
<div className="w-80 border-r bg-gray-50">
|
<div className="flex w-80 flex-col border-r bg-gray-50">
|
||||||
<div className="p-4">
|
<div className="border-b bg-white p-4">
|
||||||
<h3 className="mb-4 text-lg font-medium">테이블 타입</h3>
|
<h3 className="mb-4 text-lg font-medium">테이블 타입</h3>
|
||||||
|
|
||||||
|
{/* 검색 입력창 */}
|
||||||
|
<div className="mb-4">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="테이블명, 컬럼명으로 검색..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => handleSearchChange(e.target.value)}
|
||||||
|
className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-transparent focus:ring-2 focus:ring-blue-500 focus:outline-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 검색 결과 정보 */}
|
||||||
|
<div className="mb-2 text-sm text-gray-600">
|
||||||
|
총 {filteredTables.length}개 테이블 중 {(currentPage - 1) * itemsPerPage + 1}-
|
||||||
|
{Math.min(currentPage * itemsPerPage, filteredTables.length)}번째
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="mb-4 text-sm text-gray-600">테이블과 컬럼을 드래그하여 캔버스에 배치하세요.</p>
|
<p className="mb-4 text-sm text-gray-600">테이블과 컬럼을 드래그하여 캔버스에 배치하세요.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2 px-4">
|
{/* 테이블 목록 */}
|
||||||
{tables.map((table) => (
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div key={table.tableName} className="rounded-lg border bg-white">
|
{paginatedTables.map((table) => (
|
||||||
|
<div key={table.tableName} className="border-b bg-white">
|
||||||
{/* 테이블 헤더 */}
|
{/* 테이블 헤더 */}
|
||||||
<div className="flex items-center justify-between p-3">
|
<div
|
||||||
|
className="flex cursor-pointer items-center justify-between p-3 hover:bg-gray-100"
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) =>
|
||||||
|
startDrag(
|
||||||
|
{
|
||||||
|
type: "container",
|
||||||
|
tableName: table.tableName,
|
||||||
|
label: table.tableLabel,
|
||||||
|
size: { width: 12, height: 120 },
|
||||||
|
},
|
||||||
|
e,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Database className="h-4 w-4 text-blue-600" />
|
<Database className="h-4 w-4 text-blue-600" />
|
||||||
<span className="font-medium text-gray-900">{table.tableLabel}</span>
|
<div>
|
||||||
|
<div className="text-sm font-medium">{table.tableLabel}</div>
|
||||||
|
<div className="text-xs text-gray-500">{table.tableName}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
@ -520,39 +674,13 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 테이블 드래그 가능 */}
|
|
||||||
<div
|
|
||||||
className="cursor-grab border-t bg-gray-50 p-3 hover:bg-gray-100 active:cursor-grabbing"
|
|
||||||
draggable
|
|
||||||
onDragStart={(e) =>
|
|
||||||
startDrag(
|
|
||||||
{
|
|
||||||
type: "container",
|
|
||||||
tableName: table.tableName,
|
|
||||||
label: table.tableLabel,
|
|
||||||
size: { width: 12, height: 80 },
|
|
||||||
},
|
|
||||||
e,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
onDragEnd={endDrag}
|
|
||||||
>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Table className="h-4 w-4 text-gray-600" />
|
|
||||||
<span className="text-sm text-gray-700">테이블 전체</span>
|
|
||||||
<Badge variant="outline" className="ml-auto text-xs">
|
|
||||||
{table.columns.length} 컬럼
|
|
||||||
</Badge>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 컬럼 목록 */}
|
{/* 컬럼 목록 */}
|
||||||
{expandedTables.has(table.tableName) && (
|
{expandedTables.has(table.tableName) && (
|
||||||
<div className="bg-gray-25 border-t">
|
<div className="bg-gray-25 border-t">
|
||||||
{table.columns.map((column) => (
|
{table.columns.map((column) => (
|
||||||
<div
|
<div
|
||||||
key={column.columnName}
|
key={column.columnName}
|
||||||
className="cursor-grab border-b border-gray-100 p-3 last:border-b-0 hover:bg-gray-50 active:cursor-grabbing"
|
className="flex cursor-pointer items-center space-x-2 p-2 pl-6 hover:bg-gray-100"
|
||||||
draggable
|
draggable
|
||||||
onDragStart={(e) =>
|
onDragStart={(e) =>
|
||||||
startDrag(
|
startDrag(
|
||||||
|
|
@ -567,28 +695,22 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
e,
|
e,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
onDragEnd={endDrag}
|
|
||||||
>
|
>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex-shrink-0">
|
||||||
<div className="flex h-6 w-6 items-center justify-center rounded bg-blue-100">
|
{column.webType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
||||||
{column.webType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
{column.webType === "number" && <Hash className="h-3 w-3 text-green-600" />}
|
||||||
{column.webType === "number" && <Hash className="h-3 w-3 text-blue-600" />}
|
{column.webType === "date" && <Calendar className="h-3 w-3 text-purple-600" />}
|
||||||
{column.webType === "date" && <Calendar className="h-3 w-3 text-blue-600" />}
|
{column.webType === "select" && <List className="h-3 w-3 text-orange-600" />}
|
||||||
{column.webType === "select" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
{column.webType === "textarea" && <AlignLeft className="h-3 w-3 text-indigo-600" />}
|
||||||
{column.webType === "textarea" && <FileText className="h-3 w-3 text-blue-600" />}
|
{column.webType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
||||||
{column.webType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
{column.webType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
||||||
{column.webType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
||||||
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
column.webType,
|
||||||
column.webType,
|
) && <Type className="h-3 w-3 text-blue-600" />}
|
||||||
) && <Type className="h-3 w-3 text-blue-600" />}
|
</div>
|
||||||
</div>
|
<div className="flex-1">
|
||||||
<div className="flex-1">
|
<div className="text-sm font-medium">{column.columnLabel || column.columnName}</div>
|
||||||
<div className="text-sm font-medium text-gray-900">{column.columnLabel}</div>
|
<div className="text-xs text-gray-500">{column.columnName}</div>
|
||||||
<div className="text-xs text-gray-500">{column.columnName}</div>
|
|
||||||
</div>
|
|
||||||
<Badge variant="secondary" className="text-xs">
|
|
||||||
{column.webType}
|
|
||||||
</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -597,6 +719,35 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 페이징 컨트롤 */}
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="border-t bg-white p-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage - 1)}
|
||||||
|
disabled={currentPage === 1}
|
||||||
|
>
|
||||||
|
이전
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="text-sm text-gray-600">
|
||||||
|
{currentPage} / {totalPages}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => handlePageChange(currentPage + 1)}
|
||||||
|
disabled={currentPage === totalPages}
|
||||||
|
>
|
||||||
|
다음
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 중앙: 캔버스 영역 */}
|
{/* 중앙: 캔버스 영역 */}
|
||||||
|
|
@ -604,8 +755,8 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
<div className="h-full w-full overflow-auto p-6">
|
<div className="h-full w-full overflow-auto p-6">
|
||||||
<div
|
<div
|
||||||
className="min-h-full rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-4"
|
className="min-h-full rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-4"
|
||||||
onDrop={handleDrop}
|
onDrop={onDrop}
|
||||||
onDragOver={handleDragOver}
|
onDragOver={onDragOver}
|
||||||
>
|
>
|
||||||
{layout.components.length === 0 ? (
|
{layout.components.length === 0 ? (
|
||||||
<div className="flex h-full items-center justify-center">
|
<div className="flex h-full items-center justify-center">
|
||||||
|
|
@ -630,43 +781,49 @@ export default function ScreenDesigner({ selectedScreen, onBackToList }: ScreenD
|
||||||
{layout.components.map((component) => (
|
{layout.components.map((component) => (
|
||||||
<div
|
<div
|
||||||
key={component.id}
|
key={component.id}
|
||||||
className={`absolute cursor-pointer rounded border-2 border-transparent p-2 transition-all hover:border-blue-300 ${
|
className={`absolute cursor-move rounded border-2 ${
|
||||||
selectedComponent?.id === component.id ? "border-blue-500 bg-blue-50" : ""
|
selectedComponent?.id === component.id
|
||||||
|
? "border-blue-500 bg-blue-50"
|
||||||
|
: "border-gray-300 bg-white hover:border-gray-400"
|
||||||
}`}
|
}`}
|
||||||
style={{
|
style={{
|
||||||
left: `${component.position.x * 80}px`,
|
left: `${component.position.x}px`,
|
||||||
top: `${component.position.y * 60}px`,
|
top: `${component.position.y}px`,
|
||||||
width: `${component.size.width * 80 - 16}px`,
|
width: `${component.size.width * 80 - 16}px`,
|
||||||
height: `${component.size.height}px`,
|
height: `${component.size.height}px`,
|
||||||
}}
|
}}
|
||||||
onClick={() => setSelectedComponent(component)}
|
onClick={() => handleComponentClick(component)}
|
||||||
|
draggable
|
||||||
|
onDragStart={(e) => startComponentDrag(component, e)}
|
||||||
|
onDragEnd={endDrag}
|
||||||
>
|
>
|
||||||
<div className="flex h-full flex-col items-center justify-center rounded bg-white p-2 text-sm text-gray-600 shadow-sm">
|
<div className="flex h-full items-center justify-center p-2">
|
||||||
{component.type === "container" && (
|
{component.type === "container" && (
|
||||||
<>
|
<div className="flex flex-col items-center space-y-1">
|
||||||
<Database className="mb-1 h-4 w-4 text-blue-600" />
|
<Database className="h-6 w-6 text-blue-600" />
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium">{component.label}</div>
|
<div className="text-sm font-medium">{component.label}</div>
|
||||||
<div className="text-xs text-gray-500">{component.tableName}</div>
|
<div className="text-xs text-gray-500">{component.tableName}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
{component.type === "widget" && (
|
{component.type === "widget" && (
|
||||||
<>
|
<div className="flex flex-col items-center space-y-1">
|
||||||
<div className="mb-1 flex h-6 w-6 items-center justify-center rounded bg-blue-100">
|
{component.widgetType === "text" && <Type className="h-6 w-6 text-blue-600" />}
|
||||||
{component.widgetType === "text" && <Type className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "number" && <Hash className="h-6 w-6 text-green-600" />}
|
||||||
{component.widgetType === "number" && <Hash className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "date" && <Calendar className="h-6 w-6 text-purple-600" />}
|
||||||
{component.widgetType === "date" && <Calendar className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "select" && <List className="h-6 w-6 text-orange-600" />}
|
||||||
{component.widgetType === "select" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "textarea" && <AlignLeft className="h-6 w-6 text-indigo-600" />}
|
||||||
{component.widgetType === "textarea" && <FileText className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "checkbox" && <CheckSquare className="h-6 w-6 text-blue-600" />}
|
||||||
{component.widgetType === "checkbox" && <CheckSquare className="h-3 w-3 text-blue-600" />}
|
{component.widgetType === "radio" && <Radio className="h-6 w-6 text-blue-600" />}
|
||||||
{component.widgetType === "radio" && <Radio className="h-3 w-3 text-blue-600" />}
|
{!["text", "number", "date", "select", "textarea", "checkbox", "radio"].includes(
|
||||||
</div>
|
component.widgetType || "text",
|
||||||
|
) && <Type className="h-6 w-6 text-blue-600" />}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium">{component.label}</div>
|
<div className="text-sm font-medium">{component.label}</div>
|
||||||
<div className="text-xs text-gray-500">{component.columnName}</div>
|
<div className="text-xs text-gray-500">{component.columnName}</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue