feat-update

This commit is contained in:
TuanVT
2026-08-19 10:07:45 +07:00
parent 719851effa
commit a5ef1901da
8 changed files with 510 additions and 432 deletions
+14 -16
View File
@@ -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(),
}),
@@ -13,7 +13,7 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
return (
<RequireAuth>
// <RequireAuth>
<ContextBaseLayout
value={{
showFull,
@@ -50,7 +50,7 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
</main>
</div>
</ContextBaseLayout>
</RequireAuth>
// </RequireAuth>
);
};
@@ -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<HTMLInputElement>(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<HTMLInputElement>) => {
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 (
<div className="w-[360px] h-full bg-slate-900 border-r border-slate-800 text-slate-100 p-4 overflow-y-auto flex flex-col gap-5 shrink-0 select-none">
{/* Nút bật/tắt Chế độ Ghép Cầu */}
{/* Nút bật/tắt Chế độ Ghép Toàn Cầu */}
<button
onClick={() => setIsAssemblyMode(!isAssemblyMode)}
className={`w-full py-2 px-3 rounded text-xs font-semibold flex items-center justify-center gap-2 transition-all border ${
@@ -44,21 +74,72 @@ export function BeamForm() {
<Layers className="w-4 h-4 shrink-0" />
<span>
{isAssemblyMode
? "Chế độ: Ghép toàn bộ cầu"
? "Chế độ: Mô hình tổng thể Cầu"
: "Xem riêng lẻ cấu kiện"}
</span>
</button>
{/* Dynamic Form Content */}
<div className="flex-1 space-y-4">
{/* 1.1 DẦM SUPER-T */}
{/* TAB CẤU HÌNH TỔNG THỂ CẦU (ALIGNMENT) */}
{activeTab === "alignment" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
Cấu hình Nhịp & Tuyến Cầu
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Số lượng nhịp cầu (N 2):
</label>
<input
type="number"
min={2}
max={10}
value={globalConfig.spanCount}
onChange={(e) =>
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"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">
Số dầm Super-T / nhịp:
</label>
<input
type="number"
min={2}
max={8}
value={globalConfig.beamCountPerSpan}
onChange={(e) =>
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"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">
Khoảng cách giữa các dầm (mm):
</label>
<input
type="number"
value={globalConfig.beamSpacing}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* TAB DẦM SUPER-T */}
{activeTab === "superTBeam" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
1.1 Thông số Dầm Super-T
</h2>
<div className="space-y-2">
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều dài L (mm):
@@ -69,10 +150,9 @@ export function BeamForm() {
onChange={(e) =>
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"
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"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều cao H (mm):
@@ -83,12 +163,11 @@ export function BeamForm() {
onChange={(e) =>
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"
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"
/>
</div>
</div>
{/* Validation Panel */}
{/* Panel Cảnh báo Validation */}
<div className="pt-2">
{validationIssues && validationIssues.length > 0 ? (
<div className="p-2.5 bg-rose-500/10 border border-rose-500/30 rounded text-rose-400 text-xs space-y-1">
@@ -112,185 +191,35 @@ export function BeamForm() {
</div>
)}
{/* 1.2 BẢN MẶT CẦU */}
{activeTab === "deck" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
1.2 Bản Mặt Cầu
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Bề rộng bản mặt cầu (mm):
</label>
<input
type="number"
value={bridgeData.deck.width}
onChange={(e) =>
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"
/>
</div>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều dày bản (mm):
</label>
<input
type="number"
value={bridgeData.deck.thickness}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 1.3 DẦM NGANG */}
{activeTab === "diaphragm" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
1.3 Dầm Ngang / Diaphragms
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều dày dầm ngang (mm):
</label>
<input
type="number"
value={bridgeData.diaphragm.thickness}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 1.4 LAN CAN */}
{activeTab === "railing" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
1.4 Lan Can & Gờ Chắn
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều cao lan can (mm):
</label>
<input
type="number"
value={bridgeData.railing.height}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 2.1 GỐI CẦU */}
{activeTab === "bearing" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
2.1 Gối Cầu
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều cao gối (mm):
</label>
<input
type="number"
value={bridgeData.bearing.height}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 3.1 MỐ CẦU */}
{activeTab === "abutment" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
3.1 Mố Cầu
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều cao mố H (mm):
</label>
<input
type="number"
value={bridgeData.abutment.height}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 3.2 TRỤ CẦU */}
{activeTab === "pier" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
3.2 Trụ Cầu
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Chiều cao thân trụ (mm):
</label>
<input
type="number"
value={bridgeData.pier.height}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* 3.3 CỌC / MÓNG CỌC */}
{activeTab === "pile" && (
<div className="space-y-3">
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
3.3 Cọc / Móng Cọc
</h2>
<div>
<label className="text-xs text-slate-400 block mb-1">
Đưng kính cọc D (mm):
</label>
<input
type="number"
value={bridgeData.pile.diameter}
onChange={(e) =>
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"
/>
</div>
</div>
)}
{/* BỔ SUNG THÊM CÁC TAB BẢN MẶT CẦU, TRỤ, MỐ, CỌC TƯƠNG TỰ... */}
</div>
{/* Button Xuất dữ liệu Contract */}
{/* Cụm Nút Xuất/Nhập Data Contract & Regenerate */}
<div className="mt-auto space-y-2">
<input
type="file"
ref={fileInputRef}
onChange={handleImportFile}
accept=".json"
className="hidden"
/>
<button
onClick={() => fileInputRef.current?.click()}
className="w-full bg-slate-800 hover:bg-slate-700 text-slate-200 font-medium py-2 px-3 rounded text-xs flex items-center justify-center gap-2 border border-slate-700 transition-all"
>
<Upload className="w-3.5 h-3.5" />
<span>Import Config (Regenerate)</span>
</button>
<button
onClick={handleExportContract}
className="mt-auto w-full bg-sky-600 hover:bg-sky-500 active:bg-sky-700 text-white font-medium py-2.5 px-3 rounded text-xs flex items-center justify-center gap-2 transition-all duration-150 shadow-lg shadow-sky-600/20"
className="w-full bg-sky-600 hover:bg-sky-500 active:bg-sky-700 text-white font-medium py-2.5 px-3 rounded text-xs flex items-center justify-center gap-2 transition-all shadow-lg shadow-sky-600/20"
>
<Download className="w-4 h-4 shrink-0" />
<span className="truncate">Xuất Data Contract ({activeTab})</span>
<span>Xuất Data Contract (JSON)</span>
</button>
</div>
</div>
);
}
@@ -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() {
</ContextFormCustom.Consumer>
{/* REGISTER BUTTON */}
<CustomButton
{/* <CustomButton
variant="default"
rounded="full"
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
onClick={() => router.push(PATH.REGISTER)}
>
Đăng ký
</CustomButton>
</CustomButton> */}
</div>
</div>
</div>
+241 -121
View File
@@ -18,14 +18,240 @@ const SelectFocus: React.FC<{ children: React.ReactNode }> = ({ children }) => {
return <group>{children}</group>;
};
// 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;
// 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]);
}
}
return (
<group>
{/* Bệ cọc */}
<mesh position={[0, -1000, 0]}>
<boxGeometry args={[diameter * 6, 2000, diameter * 4]} />
<meshStandardMaterial color="#475569" />
</mesh>
{/* Các cọc */}
{positions.map(([x, z], idx) => (
<mesh key={idx} position={[x, -pileLengthMM / 2 - 2000, z]}>
<cylinderGeometry args={[radius, radius, pileLengthMM, 32]} />
<meshStandardMaterial color="#38bdf8" />
</mesh>
))}
</group>
);
};
export const Canvas3D: React.FC = () => {
const { activeTab, beamData, bridgeData, isAssemblyMode } = useSuperTStore();
const { activeTab, globalConfig, beamData, bridgeData, isAssemblyMode } =
useSuperTStore();
const { spanCount, spanLength, beamCountPerSpan, beamSpacing } = globalConfig;
// Khai báo các mesh bộ phận
const renderSuperTBeam = () => <SuperTBeamMesh data={beamData} />;
// Render toàn bộ tổ hợp cầu N nhịp (Assembly Mode)
const renderFullBridge = () => {
const totalLength = spanCount * spanLength;
const renderDeck = () => (
<mesh position={[0, beamData.height + bridgeData.deck.thickness / 2, 0]}>
return (
<group>
{/* Render theo từng nhịp */}
{Array.from({ length: spanCount }).map((_, spanIdx) => {
const spanStartZ = spanIdx * spanLength;
const spanCenterZ = spanStartZ + spanLength / 2;
return (
<group key={`span-${spanIdx}`}>
{/* 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 (
<group
key={`beam-${beamIdx}`}
position={[offsetX, 0, spanCenterZ]}
>
<SuperTBeamMesh
data={{ ...beamData, length: spanLength }}
/>
</group>
);
})}
{/* 2. Bản mặt cầu cho nhịp */}
<mesh
position={[
0,
beamData.height + bridgeData.deck.thickness / 2,
spanCenterZ,
]}
>
<boxGeometry
args={[
bridgeData.deck.width,
bridgeData.deck.thickness,
spanLength,
]}
/>
<meshStandardMaterial color="#64748b" />
</mesh>
{/* 3. Lan can 2 bên */}
<mesh
position={[
-bridgeData.deck.width / 2 + 200,
beamData.height +
bridgeData.deck.thickness +
bridgeData.railing.height / 2,
spanCenterZ,
]}
>
<boxGeometry
args={[100, bridgeData.railing.height, spanLength]}
/>
<meshStandardMaterial color="#cbd5e1" />
</mesh>
<mesh
position={[
bridgeData.deck.width / 2 - 200,
beamData.height +
bridgeData.deck.thickness +
bridgeData.railing.height / 2,
spanCenterZ,
]}
>
<boxGeometry
args={[100, bridgeData.railing.height, spanLength]}
/>
<meshStandardMaterial color="#cbd5e1" />
</mesh>
</group>
);
})}
{/* 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 (
<group key={`node-${nodeIdx}`} position={[0, 0, nodeZ]}>
{/* Gối cầu dưới các dầm */}
{Array.from({ length: beamCountPerSpan }).map((_, beamIdx) => {
const offsetX =
(beamIdx - (beamCountPerSpan - 1) / 2) * beamSpacing;
return (
<mesh
key={`bearing-${beamIdx}`}
position={[offsetX, -bridgeData.bearing.height / 2, 0]}
>
<boxGeometry args={[500, bridgeData.bearing.height, 500]} />
<meshStandardMaterial color="#0f172a" />
</mesh>
);
})}
{/* Mố hoặc Trụ */}
{isAbutment ? (
<group
position={[
0,
-bridgeData.abutment.height / 2 - bridgeData.bearing.height,
0,
]}
>
<mesh>
<boxGeometry
args={[
bridgeData.deck.width,
bridgeData.abutment.height,
bridgeData.abutment.topWidth,
]}
/>
<meshStandardMaterial color="#334155" />
</mesh>
{/* Móng cọc mố */}
<group position={[0, -bridgeData.abutment.height / 2, 0]}>
<PileGroup
diameter={bridgeData.pile.diameter}
length={bridgeData.pile.length}
count={bridgeData.pile.count}
/>
</group>
</group>
) : (
<group
position={[
0,
-bridgeData.pier.height / 2 - bridgeData.bearing.height,
0,
]}
>
<mesh>
<cylinderGeometry
args={[
bridgeData.pier.diameter / 2,
bridgeData.pier.diameter / 2,
bridgeData.pier.height,
32,
]}
/>
<meshStandardMaterial color="#475569" />
</mesh>
{/* Móng cọc trụ */}
<group position={[0, -bridgeData.pier.height / 2, 0]}>
<PileGroup
diameter={bridgeData.pile.diameter}
length={bridgeData.pile.length}
count={bridgeData.pile.count}
/>
</group>
</group>
)}
</group>
);
})}
</group>
);
};
return (
<div className="w-full h-full">
<Canvas
camera={{ position: [30000, 20000, 40000], fov: 45, far: 500000 }}
>
<ambientLight intensity={1.2} />
<directionalLight position={[20000, 30000, 20000]} intensity={1.5} />
<Bounds fit clip observe margin={1.2}>
<SelectFocus>
{isAssemblyMode ? (
renderFullBridge()
) : (
/* Chế độ xem riêng lẻ cấu kiện */
<group>
{activeTab === "superTBeam" && (
<SuperTBeamMesh data={beamData} />
)}
{activeTab === "deck" && (
<mesh position={[0, bridgeData.deck.thickness / 2, 0]}>
<boxGeometry
args={[
bridgeData.deck.width,
@@ -35,24 +261,9 @@ export const Canvas3D: React.FC = () => {
/>
<meshStandardMaterial color="#64748b" />
</mesh>
);
const renderPile = () => (
<mesh position={[0, (-bridgeData.pile.length * 1000) / 2, 0]}>
<cylinderGeometry
args={[
bridgeData.pile.diameter / 2,
bridgeData.pile.diameter / 2,
bridgeData.pile.length * 1000,
32,
]}
/>
<meshStandardMaterial color="#38bdf8" />
</mesh>
);
const renderPier = () => (
<mesh position={[0, -bridgeData.pier.height / 2, 0]}>
)}
{activeTab === "pier" && (
<mesh position={[0, 0, 0]}>
<cylinderGeometry
args={[
bridgeData.pier.diameter / 2,
@@ -63,106 +274,15 @@ export const Canvas3D: React.FC = () => {
/>
<meshStandardMaterial color="#94a3b8" />
</mesh>
);
const renderAbutment = () => (
<mesh position={[0, -bridgeData.abutment.height / 2, beamData.length / 2]}>
<boxGeometry
args={[
bridgeData.deck.width,
bridgeData.abutment.height,
bridgeData.abutment.topWidth,
]}
)}
{activeTab === "pile" && (
<PileGroup
diameter={bridgeData.pile.diameter}
length={bridgeData.pile.length}
count={bridgeData.pile.count}
/>
<meshStandardMaterial color="#475569" />
</mesh>
);
const renderBearing = () => (
<mesh
position={[0, -bridgeData.bearing.height / 2, beamData.length / 2 - 500]}
>
<boxGeometry args={[600, bridgeData.bearing.height, 600]} />
<meshStandardMaterial color="#0f172a" />
</mesh>
);
const renderRailing = () => (
<group>
<mesh
position={[
-bridgeData.deck.width / 2 + 200,
beamData.height +
bridgeData.deck.thickness +
bridgeData.railing.height / 2,
0,
]}
>
<boxGeometry args={[100, bridgeData.railing.height, beamData.length]} />
<meshStandardMaterial color="#cbd5e1" />
</mesh>
<mesh
position={[
bridgeData.deck.width / 2 - 200,
beamData.height +
bridgeData.deck.thickness +
bridgeData.railing.height / 2,
0,
]}
>
<boxGeometry args={[100, bridgeData.railing.height, beamData.length]} />
<meshStandardMaterial color="#cbd5e1" />
</mesh>
)}
</group>
);
const renderDiaphragm = () => (
<mesh position={[0, beamData.height / 2, 0]}>
<boxGeometry
args={[
bridgeData.deck.width - 1000,
beamData.height * 0.8,
bridgeData.diaphragm.thickness,
]}
/>
<meshStandardMaterial color="#e2e8f0" />
</mesh>
);
return (
<div className="w-full h-full">
<Canvas
camera={{ position: [15000, 10000, 25000], fov: 45, far: 500000 }}
>
<ambientLight intensity={1.2} />
<directionalLight position={[20000, 30000, 20000]} intensity={1.5} />
<Bounds fit clip observe margin={1.2}>
<SelectFocus>
{isAssemblyMode ? (
/* Ghép toàn bộ các cấu kiện lại thành cầu */
<group>
{renderSuperTBeam()}
{renderDeck()}
{renderPile()}
{renderPier()}
{renderAbutment()}
{renderBearing()}
{renderRailing()}
{renderDiaphragm()}
</group>
) : (
/* 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()}
</>
)}
</SelectFocus>
</Bounds>
+56 -13
View File
@@ -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<GlobalBridgeConfig>) => void;
beamData: SuperTBeamData;
validationIssues: ValidationIssue[];
setBeamData: (data: Partial<SuperTBeamData>) => void;
@@ -50,9 +59,18 @@ interface SuperTStore {
data: Partial<BridgeFullData[K]>,
) => 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<SuperTStore>((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<SuperTStore>((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;
}
},
}));
+5 -18
View File
@@ -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,
+7 -6
View File
@@ -124,16 +124,17 @@ const refreshAccessToken = async (): Promise<string | null> => {
}
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;