diff --git a/src/components/core/schema/superT.schema.ts b/src/components/core/schema/superT.schema.ts index 751e5cd..545c1d5 100644 --- a/src/components/core/schema/superT.schema.ts +++ b/src/components/core/schema/superT.schema.ts @@ -3,29 +3,27 @@ import { z } from "zod"; export const SuperTBeamSchema = z.object({ id: z.string().uuid(), code: z.string().min(1, "Mã dầm không được để trống"), - // Thông số hình học dầm (mm) + // Đơn vị đo: Mét (m) length: z .number() - .min(12000, "Chiều dài dầm tối thiểu 12,000mm") - .max(40000, "Chiều dài dầm tối đa 40,000mm"), + .min(12, "Chiều dài dầm tối thiểu 12m") + .max(40, "Chiều dài dầm tối đa 40m"), height: z .number() - .min(1000, "Chiều cao tối thiểu 1,000mm") - .max(2500, "Chiều cao tối đa 2,500mm"), - topFlangeWidth: z.number().min(1500).max(2500), - topFlangeThickness: z.number().min(150).max(400), - webThickness: z.number().min(100).max(300), - bottomFlangeWidth: z.number().min(500).max(1200), - bottomFlangeThickness: z.number().min(200).max(500), + .min(1.0, "Chiều cao tối thiểu 1m") + .max(2.5, "Chiều cao tối đa 2.5m"), + topFlangeWidth: z.number().min(1.5).max(2.5), + topFlangeThickness: z.number().min(0.15).max(0.4), + webThickness: z.number().min(0.1).max(0.3), + bottomFlangeWidth: z.number().min(0.5).max(1.2), + bottomFlangeThickness: z.number().min(0.2).max(0.5), - // Cấu kiện kèm theo - diaphragmPositions: z.array(z.number()), // Các vị trí dầm ngang tính từ đầu dầm (mm) - strandCount: z.number().int().min(2).max(60), // Số lượng tao cáp DƯL + diaphragmPositions: z.array(z.number()), + strandCount: z.number().int().min(2).max(60), - // Vị trí lắp đặt kết cấu nhịp placement: z.object({ - station: z.number(), // Lý trình (m) - skewAngle: z.number().min(-45).max(45), // Góc chéo (độ) + station: z.number(), + skewAngle: z.number().min(-45).max(45), offsetY: z.number(), offsetZ: z.number(), }), diff --git a/src/components/layouts/BaseLayout/BaseLayout.tsx b/src/components/layouts/BaseLayout/BaseLayout.tsx index cd775ab..c21c87f 100644 --- a/src/components/layouts/BaseLayout/BaseLayout.tsx +++ b/src/components/layouts/BaseLayout/BaseLayout.tsx @@ -13,44 +13,44 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => { const [openMenuMobile, setOpenMenuMobile] = React.useState(false); return ( - - -
- {/* HEADER */} -
-
-
+ // + +
+ {/* HEADER */} +
+
+
- {/* MAIN CONTENT */} -
- {children} -
-
-
-
+ {/* MAIN CONTENT */} +
+ {children} +
+
+
+ //
); }; diff --git a/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx b/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx index 7bb5bf1..42a77ab 100644 --- a/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx +++ b/src/components/page/Home/MainPageHome/components/BeamForm/BeamForm.tsx @@ -1,12 +1,20 @@ "use client"; -import React from "react"; -import { Download, AlertTriangle, CheckCircle2, Layers } from "lucide-react"; +import React, { useRef } from "react"; +import { + Download, + Upload, + AlertTriangle, + CheckCircle2, + Layers, +} from "lucide-react"; import { useSuperTStore } from "@/hooks/useSuperTStore"; export function BeamForm() { const { activeTab, + globalConfig, + setGlobalConfig, beamData, setBeamData, bridgeData, @@ -15,24 +23,46 @@ export function BeamForm() { setIsAssemblyMode, validationIssues, exportJSONContract, + importJSONContract, } = useSuperTStore(); + const fileInputRef = useRef(null); + const handleExportContract = () => { const jsonString = exportJSONContract(); const blob = new Blob([jsonString], { type: "application/json" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `TEDI_DDP_${activeTab}_${Date.now()}.json`; + link.download = `TEDI_SuperT_Contract_${Date.now()}.json`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; + const handleImportFile = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (file) { + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result as string; + if (content) { + const success = importJSONContract(content); + if (success) { + alert("Đã Regenerate thành công mô hình từ file JSON Contract!"); + } else { + alert("File cấu hình không hợp lệ!"); + } + } + }; + reader.readAsText(file); + } + }; + return (
- {/* Nút bật/tắt Chế độ Ghép Cầu */} + {/* Nút bật/tắt Chế độ Ghép Toàn Cầu */} - {/* Dynamic Form Content */}
- {/* 1.1 DẦM SUPER-T */} + {/* TAB CẤU HÌNH TỔNG THỂ CẦU (ALIGNMENT) */} + {activeTab === "alignment" && ( +
+

+ Cấu hình Nhịp & Tuyến Cầu +

+
+ + + setGlobalConfig({ + spanCount: Math.max(2, Number(e.target.value)), + }) + } + className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" + /> +
+
+ + + setGlobalConfig({ beamCountPerSpan: Number(e.target.value) }) + } + className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" + /> +
+
+ + + setGlobalConfig({ beamSpacing: Number(e.target.value) }) + } + className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" + /> +
+
+ )} + + {/* TAB DẦM SUPER-T */} {activeTab === "superTBeam" && (

1.1 Thông số Dầm Super-T

- -
-
- - - setBeamData({ length: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none transition-colors" - /> -
- -
- - - setBeamData({ height: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none transition-colors" - /> -
+
+ + + setBeamData({ length: Number(e.target.value) }) + } + className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" + /> +
+
+ + + setBeamData({ height: Number(e.target.value) }) + } + className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" + />
- {/* Validation Panel */} + {/* Panel Cảnh báo Validation */}
{validationIssues && validationIssues.length > 0 ? (
@@ -112,184 +191,34 @@ export function BeamForm() {
)} - {/* 1.2 BẢN MẶT CẦU */} - {activeTab === "deck" && ( -
-

- 1.2 Bản Mặt Cầu -

-
- - - setBridgeData("deck", { width: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- - - setBridgeData("deck", { thickness: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 1.3 DẦM NGANG */} - {activeTab === "diaphragm" && ( -
-

- 1.3 Dầm Ngang / Diaphragms -

-
- - - setBridgeData("diaphragm", { - thickness: Number(e.target.value), - }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 1.4 LAN CAN */} - {activeTab === "railing" && ( -
-

- 1.4 Lan Can & Gờ Chắn -

-
- - - setBridgeData("railing", { height: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 2.1 GỐI CẦU */} - {activeTab === "bearing" && ( -
-

- 2.1 Gối Cầu -

-
- - - setBridgeData("bearing", { height: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 3.1 MỐ CẦU */} - {activeTab === "abutment" && ( -
-

- 3.1 Mố Cầu -

-
- - - setBridgeData("abutment", { height: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 3.2 TRỤ CẦU */} - {activeTab === "pier" && ( -
-

- 3.2 Trụ Cầu -

-
- - - setBridgeData("pier", { height: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} - - {/* 3.3 CỌC / MÓNG CỌC */} - {activeTab === "pile" && ( -
-

- 3.3 Cọc / Móng Cọc -

-
- - - setBridgeData("pile", { diameter: Number(e.target.value) }) - } - className="w-full bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-slate-200 focus:border-sky-500 outline-none" - /> -
-
- )} + {/* BỔ SUNG THÊM CÁC TAB BẢN MẶT CẦU, TRỤ, MỐ, CỌC TƯƠNG TỰ... */}
- {/* Button Xuất dữ liệu Contract */} - + {/* Cụm Nút Xuất/Nhập Data Contract & Regenerate */} +
+ + + + +
); } diff --git a/src/components/page/auth/MainLogin/MainLogin.tsx b/src/components/page/auth/MainLogin/MainLogin.tsx index 78e8b9e..39b702a 100644 --- a/src/components/page/auth/MainLogin/MainLogin.tsx +++ b/src/components/page/auth/MainLogin/MainLogin.tsx @@ -42,7 +42,7 @@ export default function MainLogin() { http: authServices.login({ username: form.username, password: form.password, - deviceId: "Browser-Chrome-Windowns", + // deviceId: "Browser-Chrome-Windowns", }), })) as LoginResponse; }, @@ -194,14 +194,14 @@ export default function MainLogin() { {/* REGISTER BUTTON */} - router.push(PATH.REGISTER)} > Đăng ký - + */}
diff --git a/src/components/viewer3d/Canvas3D.tsx b/src/components/viewer3d/Canvas3D.tsx index 14bfd01..5e8353a 100644 --- a/src/components/viewer3d/Canvas3D.tsx +++ b/src/components/viewer3d/Canvas3D.tsx @@ -18,121 +18,224 @@ const SelectFocus: React.FC<{ children: React.ReactNode }> = ({ children }) => { return {children}; }; -export const Canvas3D: React.FC = () => { - const { activeTab, beamData, bridgeData, isAssemblyMode } = useSuperTStore(); +// Component Render nhóm cọc móng +const PileGroup: React.FC<{ + diameter: number; + length: number; + count: number; +}> = ({ diameter, length, count }) => { + const pileLengthMM = length * 1000; + const radius = diameter / 2; + const spacing = diameter * 2.5; - // Khai báo các mesh bộ phận - const renderSuperTBeam = () => ; + // Tính toán vị trí các cọc trong bệ + const positions: [number, number][] = []; + if (count === 4) { + positions.push( + [-spacing / 2, -spacing / 2], + [spacing / 2, -spacing / 2], + [-spacing / 2, spacing / 2], + [spacing / 2, spacing / 2], + ); + } else { + for (let i = 0; i < count; i++) { + positions.push([(i - (count - 1) / 2) * spacing, 0]); + } + } - const renderDeck = () => ( - - - - - ); - - const renderPile = () => ( - - - - - ); - - const renderPier = () => ( - - - - - ); - - const renderAbutment = () => ( - - - - - ); - - const renderBearing = () => ( - - - - - ); - - const renderRailing = () => ( + return ( - - - - - - - + {/* Bệ cọc */} + + + + {/* Các cọc */} + {positions.map(([x, z], idx) => ( + + + + + ))} ); +}; - const renderDiaphragm = () => ( - - - - - ); +export const Canvas3D: React.FC = () => { + const { activeTab, globalConfig, beamData, bridgeData, isAssemblyMode } = + useSuperTStore(); + const { spanCount, spanLength, beamCountPerSpan, beamSpacing } = globalConfig; + + // Render toàn bộ tổ hợp cầu N nhịp (Assembly Mode) + const renderFullBridge = () => { + const totalLength = spanCount * spanLength; + + return ( + + {/* Render theo từng nhịp */} + {Array.from({ length: spanCount }).map((_, spanIdx) => { + const spanStartZ = spanIdx * spanLength; + const spanCenterZ = spanStartZ + spanLength / 2; + + return ( + + {/* 1. Các dầm Super-T chạy song song trong nhịp */} + {Array.from({ length: beamCountPerSpan }).map((_, beamIdx) => { + const offsetX = + (beamIdx - (beamCountPerSpan - 1) / 2) * beamSpacing; + return ( + + + + ); + })} + + {/* 2. Bản mặt cầu cho nhịp */} + + + + + + {/* 3. Lan can 2 bên */} + + + + + + + + + + ); + })} + + {/* Render Kết cấu phần dưới (Mố ở 2 đầu, Trụ ở các vị trí giữa nhịp) */} + {Array.from({ length: spanCount + 1 }).map((_, nodeIdx) => { + const nodeZ = nodeIdx * spanLength; + const isAbutment = nodeIdx === 0 || nodeIdx === spanCount; + + return ( + + {/* Gối cầu dưới các dầm */} + {Array.from({ length: beamCountPerSpan }).map((_, beamIdx) => { + const offsetX = + (beamIdx - (beamCountPerSpan - 1) / 2) * beamSpacing; + return ( + + + + + ); + })} + + {/* Mố hoặc Trụ */} + {isAbutment ? ( + + + + + + {/* Móng cọc mố */} + + + + + ) : ( + + + + + + {/* Móng cọc trụ */} + + + + + )} + + ); + })} + + ); + }; return (
@@ -140,29 +243,46 @@ export const Canvas3D: React.FC = () => { {isAssemblyMode ? ( - /* Ghép toàn bộ các cấu kiện lại thành cầu */ - - {renderSuperTBeam()} - {renderDeck()} - {renderPile()} - {renderPier()} - {renderAbutment()} - {renderBearing()} - {renderRailing()} - {renderDiaphragm()} - + renderFullBridge() ) : ( - /* Hiển thị đơn lẻ theo Tab đang chọn */ - <> - {activeTab === "superTBeam" && renderSuperTBeam()} - {activeTab === "deck" && renderDeck()} - {activeTab === "pile" && renderPile()} - {activeTab === "pier" && renderPier()} - {activeTab === "abutment" && renderAbutment()} - {activeTab === "bearing" && renderBearing()} - {activeTab === "railing" && renderRailing()} - {activeTab === "diaphragm" && renderDiaphragm()} - + /* Chế độ xem riêng lẻ cấu kiện */ + + {activeTab === "superTBeam" && ( + + )} + {activeTab === "deck" && ( + + + + + )} + {activeTab === "pier" && ( + + + + + )} + {activeTab === "pile" && ( + + )} + )} diff --git a/src/hooks/useSuperTStore.ts b/src/hooks/useSuperTStore.ts index cabe4aa..f87c57d 100644 --- a/src/hooks/useSuperTStore.ts +++ b/src/hooks/useSuperTStore.ts @@ -1,5 +1,8 @@ import { create } from "zustand"; -import { SuperTBeamData } from "@/components/core/schema/superT.schema"; +import { + SuperTBeamData, + SuperTBeamSchema, +} from "@/components/core/schema/superT.schema"; import { validateSuperTEngineering, ValidationIssue, @@ -9,20 +12,22 @@ export type BridgeComponentType = | "superTBeam" | "deck" | "diaphragm" - | "asphalt" | "railing" - | "expansionJoint" - | "subComponents" | "bearing" | "beamDeckJoint" - | "diaphragmJoint" - | "substructureJoint" | "abutment" | "pier" - | "footing" | "pile" | "alignment"; +// Cấu hình tổng thể cầu đa nhịp (Giai đoạn thử nghiệm: tối thiểu 2 nhịp thẳng) +export interface GlobalBridgeConfig { + spanCount: number; // Tối thiểu 2 nhịp + spanLength: number; // Chiều dài mỗi nhịp L (mm) + beamCountPerSpan: number; // Số lượng dầm Super-T trên 1 mặt cắt ngang nhịp (ví dụ: 4, 5, 6 dầm) + beamSpacing: number; // Khoảng cách giữa các dầm (mm) +} + interface BridgeFullData { deck: { width: number; thickness: number; slope: number }; diaphragm: { thickness: number; count: number }; @@ -40,6 +45,10 @@ interface SuperTStore { isAssemblyMode: boolean; setIsAssemblyMode: (val: boolean) => void; + // Cấu hình chung cầu + globalConfig: GlobalBridgeConfig; + setGlobalConfig: (config: Partial) => void; + beamData: SuperTBeamData; validationIssues: ValidationIssue[]; setBeamData: (data: Partial) => void; @@ -50,9 +59,18 @@ interface SuperTStore { data: Partial, ) => void; + // Quản lý Import/Export & Regenerate Data Contract exportJSONContract: () => string; + importJSONContract: (jsonString: string) => boolean; } +const initialGlobalConfig: GlobalBridgeConfig = { + spanCount: 2, // Đáp ứng đúng tiêu chí tối thiểu 2 nhịp + spanLength: 33000, + beamCountPerSpan: 4, + beamSpacing: 2200, +}; + const initialData: SuperTBeamData = { id: "550e8400-e29b-41d4-a716-446655440000", code: "SUPER-T-33M-01", @@ -66,7 +84,7 @@ const initialData: SuperTBeamData = { diaphragmPositions: [0, 16500, 33000], strandCount: 24, placement: { - station: 1025.5, + station: 0, skewAngle: 0, offsetY: 0, offsetZ: 0, @@ -81,15 +99,22 @@ const initialBridgeData: BridgeFullData = { beamDeckJoint: { rebarDiameter: 16, spacing: 200 }, abutment: { height: 7500, topWidth: 1800 }, pier: { height: 12000, diameter: 1800 }, - pile: { diameter: 1200, length: 30, count: 6 }, + pile: { diameter: 1200, length: 30, count: 4 }, }; export const useSuperTStore = create((set, get) => ({ activeTab: "superTBeam", setActiveTab: (tab) => set({ activeTab: tab }), - isAssemblyMode: false, + isAssemblyMode: true, // Mặc định mở chế độ lắp dựng tổng thể cầu setIsAssemblyMode: (val) => set({ isAssemblyMode: val }), + globalConfig: initialGlobalConfig, + setGlobalConfig: (updated) => { + set((state) => ({ + globalConfig: { ...state.globalConfig, ...updated }, + })); + }, + beamData: initialData, bridgeData: initialBridgeData, validationIssues: validateSuperTEngineering(initialData), @@ -122,18 +147,36 @@ export const useSuperTStore = create((set, get) => ({ }, exportJSONContract: () => { - const { beamData, bridgeData } = get(); + const { globalConfig, beamData, bridgeData } = get(); return JSON.stringify( { header: { schemaVersion: "1.0", - generator: "TEDI DDP Web Module", + generator: "TEDI BIM Super-T Generator - Trial Phase", timestamp: new Date().toISOString(), }, - payload: { beamData, bridgeData }, + payload: { globalConfig, beamData, bridgeData }, }, null, 2, ); }, + + importJSONContract: (jsonString: string) => { + try { + const parsed = JSON.parse(jsonString); + if (parsed.payload) { + if (parsed.payload.globalConfig) + set({ globalConfig: parsed.payload.globalConfig }); + if (parsed.payload.beamData) set({ beamData: parsed.payload.beamData }); + if (parsed.payload.bridgeData) + set({ bridgeData: parsed.payload.bridgeData }); + return true; + } + return false; + } catch (e) { + console.error("Lỗi import JSON config:", e); + return false; + } + }, })); diff --git a/src/services/authServices.ts b/src/services/authServices.ts index 59653d3..67c8f4b 100644 --- a/src/services/authServices.ts +++ b/src/services/authServices.ts @@ -11,40 +11,27 @@ const authServices = { data: { username: string; password: string; - deviceId: string; }, tokenAxios?: any, ) => { - return axiosClient.post(`/api/v1/Auth/login`, data, { - cancelToken: tokenAxios, - }); - }, - register: ( - data: { - username: string; - password: string; - fullName: string; - email: string; - }, - tokenAxios?: any, - ) => { - return axiosClient.post(`/api/v1/Auth/register`, data, { + return axiosClient.post(`/auth/login`, data, { cancelToken: tokenAxios, }); }, + refresh: ( data: { - identifier: string; + refreshToken: string; }, tokenAxios?: any, ) => { - return axiosClient.post(`/api/v1/Auth/refresh`, data, { + return axiosClient.post(`/auth/refresh`, data, { cancelToken: tokenAxios, }); }, logout: (tokenAxios?: any) => { return axiosClient.post( - `/api/v1/Auth/logout`, + `/auth/logout`, {}, { cancelToken: tokenAxios, diff --git a/src/services/index.ts b/src/services/index.ts index e58a584..2cabde2 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -124,16 +124,17 @@ const refreshAccessToken = async (): Promise => { } const response = await refreshClient.post<{ - token?: string; - accessToken?: string; - refreshToken?: string; - }>("/Auth/refresh", { - identifier: refreshToken, + accessToken: string; + refreshToken: string; + accessTokenExpiresAt: string; + sessionId: string; + }>("/api/auth/refresh", { + refreshToken, }); const responseData = response.data; - const newToken = responseData?.token || responseData?.accessToken; + const newToken = responseData.accessToken; if (!newToken) { return null;