255 lines
8.7 KiB
TypeScript
255 lines
8.7 KiB
TypeScript
"use client";
|
|
|
|
import { UndoAction } from "@/lib/useEditorState";
|
|
|
|
type Mode = "draw" | "select" | "idle" | "add-point" | "add-path" | "add-circle";
|
|
type EntityOption = {
|
|
id: string;
|
|
name: string;
|
|
geometry_count?: number;
|
|
};
|
|
|
|
type Props = {
|
|
mode: Mode;
|
|
setMode: (mode: Mode) => void;
|
|
entities: EntityOption[];
|
|
selectedEntityId: string | null;
|
|
onSelectEntityId: (entityId: string | null) => void;
|
|
entityStatus?: string | null;
|
|
onUndo: () => void;
|
|
onSave: () => void;
|
|
isSaving: boolean;
|
|
changesCount: number;
|
|
undoStack: UndoAction[];
|
|
};
|
|
|
|
export default function Editor({
|
|
mode,
|
|
setMode,
|
|
entities,
|
|
selectedEntityId,
|
|
onSelectEntityId,
|
|
entityStatus,
|
|
onUndo,
|
|
onSave,
|
|
isSaving,
|
|
changesCount,
|
|
undoStack,
|
|
}: Props) {
|
|
const toggleMode = (newMode: Mode) => {
|
|
if (mode === newMode) {
|
|
setMode("idle"); // bấm lại → tắt
|
|
} else {
|
|
setMode(newMode); // chuyển mode
|
|
}
|
|
};
|
|
|
|
// Lấy tối đa 8 tác vụ mới nhất, bỏ trùng nhãn (cùng loại/cùng id)
|
|
const recentUndoLabels = (() => {
|
|
const seen = new Set<string>();
|
|
const labels: string[] = [];
|
|
for (let i = undoStack.length - 1; i >= 0 && labels.length < 8; i -= 1) {
|
|
const label = formatUndoLabel(undoStack[i]);
|
|
if (seen.has(label)) continue;
|
|
seen.add(label);
|
|
labels.push(label);
|
|
}
|
|
return labels.reverse();
|
|
})();
|
|
|
|
const getButtonStyle = (btnMode: Mode) => ({
|
|
width: "100%",
|
|
padding: "8px",
|
|
marginBottom: "6px",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
background: mode === btnMode ? "#4caf50" : "#222",
|
|
color: "white",
|
|
borderRadius: "4px",
|
|
});
|
|
|
|
return (
|
|
<div
|
|
style={{
|
|
width: "220px",
|
|
background: "#111",
|
|
color: "white",
|
|
padding: "12px",
|
|
borderRight: "1px solid #333",
|
|
}}
|
|
>
|
|
<h3 style={{ marginBottom: "10px" }}>Editor</h3>
|
|
|
|
<button
|
|
style={getButtonStyle("draw")}
|
|
onClick={() => toggleMode("draw")}
|
|
>
|
|
Draw
|
|
</button>
|
|
|
|
<button
|
|
style={getButtonStyle("select")}
|
|
onClick={() => toggleMode("select")}
|
|
>
|
|
Select
|
|
</button>
|
|
|
|
<button
|
|
style={getButtonStyle("idle")}
|
|
onClick={() => setMode("idle")}
|
|
>
|
|
Idle
|
|
</button>
|
|
|
|
<button
|
|
style={getButtonStyle("add-point")}
|
|
onClick={() => setMode("add-point")}
|
|
>
|
|
Add point
|
|
</button>
|
|
|
|
<button
|
|
style={getButtonStyle("add-path")}
|
|
onClick={() => setMode("add-path")}
|
|
>
|
|
Add path
|
|
</button>
|
|
|
|
<button
|
|
style={getButtonStyle("add-circle")}
|
|
onClick={() => setMode("add-circle")}
|
|
>
|
|
Add circle
|
|
</button>
|
|
|
|
<div style={{ marginTop: "12px", fontSize: "14px" }}>
|
|
Mode: <b>{mode}</b>
|
|
</div>
|
|
{mode === "add-path" ? (
|
|
<div style={{ marginTop: "6px", fontSize: "12px", color: "#93c5fd" }}>
|
|
Click để thêm điểm, Enter để hoàn tất, Esc để hủy.
|
|
</div>
|
|
) : null}
|
|
{mode === "add-circle" ? (
|
|
<div style={{ marginTop: "6px", fontSize: "12px", color: "#93c5fd" }}>
|
|
Giữ chuột trái kéo để mở bán kính, thả chuột để hoàn tất.
|
|
</div>
|
|
) : null}
|
|
|
|
<div
|
|
style={{
|
|
marginTop: "12px",
|
|
padding: "10px",
|
|
background: "#0b1220",
|
|
borderRadius: "6px",
|
|
border: "1px solid #1f2937",
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: "6px", fontWeight: 600, fontSize: "13px", color: "#e2e8f0" }}>
|
|
Entity mặc định cho geometry mới
|
|
</div>
|
|
<select
|
|
value={selectedEntityId || ""}
|
|
onChange={(event) => onSelectEntityId(event.target.value || null)}
|
|
style={{
|
|
width: "100%",
|
|
padding: "6px 8px",
|
|
borderRadius: "4px",
|
|
border: "1px solid #334155",
|
|
background: "#111827",
|
|
color: "#f8fafc",
|
|
fontSize: "13px",
|
|
}}
|
|
>
|
|
<option value="">Không gắn entity</option>
|
|
{entities.map((entity) => (
|
|
<option key={entity.id} value={entity.id}>
|
|
{entity.name}
|
|
{typeof entity.geometry_count === "number" ? ` (${entity.geometry_count})` : ""}
|
|
</option>
|
|
))}
|
|
</select>
|
|
<div style={{ marginTop: "6px", color: "#94a3b8", fontSize: "12px" }}>
|
|
Geometry mới tạo sẽ gắn sẵn entity này, bạn có thể thêm nhiều entity ở panel bên phải.
|
|
</div>
|
|
{entityStatus ? (
|
|
<div style={{ marginTop: "6px", color: "#fca5a5", fontSize: "12px" }}>
|
|
{entityStatus}
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div style={{ marginTop: "12px", display: "flex", gap: "8px" }}>
|
|
<button
|
|
style={{
|
|
flex: 1,
|
|
padding: "8px",
|
|
borderRadius: "4px",
|
|
border: "none",
|
|
cursor: "pointer",
|
|
background: "#334155",
|
|
color: "white",
|
|
}}
|
|
onClick={onUndo}
|
|
>
|
|
Undo
|
|
</button>
|
|
<button
|
|
style={{
|
|
flex: 1,
|
|
padding: "8px",
|
|
borderRadius: "4px",
|
|
border: "none",
|
|
cursor: isSaving ? "not-allowed" : "pointer",
|
|
background: isSaving ? "#555" : "#3b82f6",
|
|
color: "white",
|
|
opacity: changesCount === 0 ? 0.6 : 1,
|
|
}}
|
|
onClick={onSave}
|
|
disabled={isSaving || changesCount === 0}
|
|
>
|
|
Save ({changesCount})
|
|
</button>
|
|
</div>
|
|
|
|
<div
|
|
style={{
|
|
marginTop: "16px",
|
|
padding: "10px",
|
|
background: "#0b1220",
|
|
borderRadius: "6px",
|
|
border: "1px solid #1f2937",
|
|
}}
|
|
>
|
|
<div style={{ marginBottom: "6px", fontWeight: 600, fontSize: "14px" }}>
|
|
Tác vụ có thể undo ({recentUndoLabels.length})
|
|
</div>
|
|
{recentUndoLabels.length === 0 ? (
|
|
<div style={{ color: "#94a3b8", fontSize: "13px" }}>Chưa có thao tác</div>
|
|
) : (
|
|
<ul style={{ listStyle: "none", margin: 0, padding: 0, fontSize: "13px", color: "#e2e8f0" }}>
|
|
{recentUndoLabels.map((label, idx) => (
|
|
<li key={`${label}-${idx}`} style={{ padding: "4px 0", borderBottom: "1px solid #1f2937" }}>
|
|
{label}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function formatUndoLabel(action: UndoAction) {
|
|
switch (action.type) {
|
|
case "create":
|
|
return `Thêm mới #${action.id}`;
|
|
case "delete":
|
|
return `Xóa #${action.feature.properties.id}`;
|
|
case "update":
|
|
return `Chỉnh sửa #${action.id}`;
|
|
default:
|
|
return "Tác vụ";
|
|
}
|
|
}
|