feat-update
This commit is contained in:
@@ -3,29 +3,27 @@ import { z } from "zod";
|
|||||||
export const SuperTBeamSchema = z.object({
|
export const SuperTBeamSchema = z.object({
|
||||||
id: z.string().uuid(),
|
id: z.string().uuid(),
|
||||||
code: z.string().min(1, "Mã dầm không được để trống"),
|
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
|
length: z
|
||||||
.number()
|
.number()
|
||||||
.min(12000, "Chiều dài dầm tối thiểu 12,000mm")
|
.min(12, "Chiều dài dầm tối thiểu 12m")
|
||||||
.max(40000, "Chiều dài dầm tối đa 40,000mm"),
|
.max(40, "Chiều dài dầm tối đa 40m"),
|
||||||
height: z
|
height: z
|
||||||
.number()
|
.number()
|
||||||
.min(1000, "Chiều cao tối thiểu 1,000mm")
|
.min(1.0, "Chiều cao tối thiểu 1m")
|
||||||
.max(2500, "Chiều cao tối đa 2,500mm"),
|
.max(2.5, "Chiều cao tối đa 2.5m"),
|
||||||
topFlangeWidth: z.number().min(1500).max(2500),
|
topFlangeWidth: z.number().min(1.5).max(2.5),
|
||||||
topFlangeThickness: z.number().min(150).max(400),
|
topFlangeThickness: z.number().min(0.15).max(0.4),
|
||||||
webThickness: z.number().min(100).max(300),
|
webThickness: z.number().min(0.1).max(0.3),
|
||||||
bottomFlangeWidth: z.number().min(500).max(1200),
|
bottomFlangeWidth: z.number().min(0.5).max(1.2),
|
||||||
bottomFlangeThickness: z.number().min(200).max(500),
|
bottomFlangeThickness: z.number().min(0.2).max(0.5),
|
||||||
|
|
||||||
// Cấu kiện kèm theo
|
diaphragmPositions: z.array(z.number()),
|
||||||
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),
|
||||||
strandCount: z.number().int().min(2).max(60), // Số lượng tao cáp DƯL
|
|
||||||
|
|
||||||
// Vị trí lắp đặt kết cấu nhịp
|
|
||||||
placement: z.object({
|
placement: z.object({
|
||||||
station: z.number(), // Lý trình (m)
|
station: z.number(),
|
||||||
skewAngle: z.number().min(-45).max(45), // Góc chéo (độ)
|
skewAngle: z.number().min(-45).max(45),
|
||||||
offsetY: z.number(),
|
offsetY: z.number(),
|
||||||
offsetZ: z.number(),
|
offsetZ: z.number(),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -13,44 +13,44 @@ const BaseLayout = ({ children, title, breadcrumb }: PropsBaseLayout) => {
|
|||||||
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
const [openMenuMobile, setOpenMenuMobile] = React.useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RequireAuth>
|
// <RequireAuth>
|
||||||
<ContextBaseLayout
|
<ContextBaseLayout
|
||||||
value={{
|
value={{
|
||||||
showFull,
|
showFull,
|
||||||
setShowFull,
|
setShowFull,
|
||||||
openMenuMobile,
|
openMenuMobile,
|
||||||
setOpenMenuMobile,
|
setOpenMenuMobile,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="min-h-screen w-full overflow-x-hidden bg-[#f8f8f8]">
|
<div className="min-h-screen w-full overflow-x-hidden bg-[#f8f8f8]">
|
||||||
{/* HEADER */}
|
{/* HEADER */}
|
||||||
<header
|
<header
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"top-0 right-0 z-[11] h-[68px]",
|
"top-0 right-0 z-[11] h-[68px]",
|
||||||
"w-full",
|
"w-full",
|
||||||
"border-b border-[#f4f7fa]",
|
"border-b border-[#f4f7fa]",
|
||||||
"bg-white",
|
"bg-white",
|
||||||
"transition-all duration-300",
|
"transition-all duration-300",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Header title={title} breadcrumb={breadcrumb} />
|
<Header title={title} breadcrumb={breadcrumb} />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* MAIN CONTENT */}
|
{/* MAIN CONTENT */}
|
||||||
<main
|
<main
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"w-full",
|
"w-full",
|
||||||
"min-w-0",
|
"min-w-0",
|
||||||
"overflow-x-hidden",
|
"overflow-x-hidden",
|
||||||
"transition-all duration-300 ease-in-out",
|
"transition-all duration-300 ease-in-out",
|
||||||
"ml-0",
|
"ml-0",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</ContextBaseLayout>
|
</ContextBaseLayout>
|
||||||
</RequireAuth>
|
// </RequireAuth>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { useRef } from "react";
|
||||||
import { Download, AlertTriangle, CheckCircle2, Layers } from "lucide-react";
|
import {
|
||||||
|
Download,
|
||||||
|
Upload,
|
||||||
|
AlertTriangle,
|
||||||
|
CheckCircle2,
|
||||||
|
Layers,
|
||||||
|
} from "lucide-react";
|
||||||
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
import { useSuperTStore } from "@/hooks/useSuperTStore";
|
||||||
|
|
||||||
export function BeamForm() {
|
export function BeamForm() {
|
||||||
const {
|
const {
|
||||||
activeTab,
|
activeTab,
|
||||||
|
globalConfig,
|
||||||
|
setGlobalConfig,
|
||||||
beamData,
|
beamData,
|
||||||
setBeamData,
|
setBeamData,
|
||||||
bridgeData,
|
bridgeData,
|
||||||
@@ -15,24 +23,46 @@ export function BeamForm() {
|
|||||||
setIsAssemblyMode,
|
setIsAssemblyMode,
|
||||||
validationIssues,
|
validationIssues,
|
||||||
exportJSONContract,
|
exportJSONContract,
|
||||||
|
importJSONContract,
|
||||||
} = useSuperTStore();
|
} = useSuperTStore();
|
||||||
|
|
||||||
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleExportContract = () => {
|
const handleExportContract = () => {
|
||||||
const jsonString = exportJSONContract();
|
const jsonString = exportJSONContract();
|
||||||
const blob = new Blob([jsonString], { type: "application/json" });
|
const blob = new Blob([jsonString], { type: "application/json" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
link.download = `TEDI_DDP_${activeTab}_${Date.now()}.json`;
|
link.download = `TEDI_SuperT_Contract_${Date.now()}.json`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
URL.revokeObjectURL(url);
|
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 (
|
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">
|
<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
|
<button
|
||||||
onClick={() => setIsAssemblyMode(!isAssemblyMode)}
|
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 ${
|
className={`w-full py-2 px-3 rounded text-xs font-semibold flex items-center justify-center gap-2 transition-all border ${
|
||||||
@@ -44,51 +74,100 @@ export function BeamForm() {
|
|||||||
<Layers className="w-4 h-4 shrink-0" />
|
<Layers className="w-4 h-4 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{isAssemblyMode
|
{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"}
|
: "Xem riêng lẻ cấu kiện"}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{/* Dynamic Form Content */}
|
|
||||||
<div className="flex-1 space-y-4">
|
<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" && (
|
{activeTab === "superTBeam" && (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
<h2 className="text-xs font-bold text-sky-400 uppercase tracking-wider">
|
||||||
1.1 Thông số Dầm Super-T
|
1.1 Thông số Dầm Super-T
|
||||||
</h2>
|
</h2>
|
||||||
|
<div>
|
||||||
<div className="space-y-2">
|
<label className="text-xs text-slate-400 block mb-1">
|
||||||
<div>
|
Chiều dài L (mm):
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
</label>
|
||||||
Chiều dài L (mm):
|
<input
|
||||||
</label>
|
type="number"
|
||||||
<input
|
value={beamData.length}
|
||||||
type="number"
|
onChange={(e) =>
|
||||||
value={beamData.length}
|
setBeamData({ length: Number(e.target.value) })
|
||||||
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"
|
||||||
}
|
/>
|
||||||
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"
|
</div>
|
||||||
/>
|
<div>
|
||||||
</div>
|
<label className="text-xs text-slate-400 block mb-1">
|
||||||
|
Chiều cao H (mm):
|
||||||
<div>
|
</label>
|
||||||
<label className="text-xs text-slate-400 block mb-1">
|
<input
|
||||||
Chiều cao H (mm):
|
type="number"
|
||||||
</label>
|
value={beamData.height}
|
||||||
<input
|
onChange={(e) =>
|
||||||
type="number"
|
setBeamData({ height: Number(e.target.value) })
|
||||||
value={beamData.height}
|
}
|
||||||
onChange={(e) =>
|
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 transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Validation Panel */}
|
{/* Panel Cảnh báo Validation */}
|
||||||
<div className="pt-2">
|
<div className="pt-2">
|
||||||
{validationIssues && validationIssues.length > 0 ? (
|
{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">
|
<div className="p-2.5 bg-rose-500/10 border border-rose-500/30 rounded text-rose-400 text-xs space-y-1">
|
||||||
@@ -112,184 +191,34 @@ export function BeamForm() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 1.2 BẢN MẶT CẦU */}
|
{/* BỔ SUNG THÊM CÁC TAB BẢN MẶT CẦU, TRỤ, MỐ, CỌC TƯƠNG TỰ... */}
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Button Xuất dữ liệu Contract */}
|
{/* Cụm Nút Xuất/Nhập Data Contract & Regenerate */}
|
||||||
<button
|
<div className="mt-auto space-y-2">
|
||||||
onClick={handleExportContract}
|
<input
|
||||||
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"
|
type="file"
|
||||||
>
|
ref={fileInputRef}
|
||||||
<Download className="w-4 h-4 shrink-0" />
|
onChange={handleImportFile}
|
||||||
<span className="truncate">Xuất Data Contract ({activeTab})</span>
|
accept=".json"
|
||||||
</button>
|
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="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>Xuất Data Contract (JSON)</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default function MainLogin() {
|
|||||||
http: authServices.login({
|
http: authServices.login({
|
||||||
username: form.username,
|
username: form.username,
|
||||||
password: form.password,
|
password: form.password,
|
||||||
deviceId: "Browser-Chrome-Windowns",
|
// deviceId: "Browser-Chrome-Windowns",
|
||||||
}),
|
}),
|
||||||
})) as LoginResponse;
|
})) as LoginResponse;
|
||||||
},
|
},
|
||||||
@@ -194,14 +194,14 @@ export default function MainLogin() {
|
|||||||
</ContextFormCustom.Consumer>
|
</ContextFormCustom.Consumer>
|
||||||
|
|
||||||
{/* REGISTER BUTTON */}
|
{/* REGISTER BUTTON */}
|
||||||
<CustomButton
|
{/* <CustomButton
|
||||||
variant="default"
|
variant="default"
|
||||||
rounded="full"
|
rounded="full"
|
||||||
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
className="font-bold text-2xl disabled:cursor-not-allowed disabled:opacity-50 h-[52px]"
|
||||||
onClick={() => router.push(PATH.REGISTER)}
|
onClick={() => router.push(PATH.REGISTER)}
|
||||||
>
|
>
|
||||||
Đăng ký
|
Đăng ký
|
||||||
</CustomButton>
|
</CustomButton> */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,121 +18,224 @@ const SelectFocus: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||||||
return <group>{children}</group>;
|
return <group>{children}</group>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Canvas3D: React.FC = () => {
|
// Component Render nhóm cọc móng
|
||||||
const { activeTab, beamData, bridgeData, isAssemblyMode } = useSuperTStore();
|
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
|
// Tính toán vị trí các cọc trong bệ
|
||||||
const renderSuperTBeam = () => <SuperTBeamMesh data={beamData} />;
|
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 = () => (
|
return (
|
||||||
<mesh position={[0, beamData.height + bridgeData.deck.thickness / 2, 0]}>
|
|
||||||
<boxGeometry
|
|
||||||
args={[
|
|
||||||
bridgeData.deck.width,
|
|
||||||
bridgeData.deck.thickness,
|
|
||||||
beamData.length,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<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]}>
|
|
||||||
<cylinderGeometry
|
|
||||||
args={[
|
|
||||||
bridgeData.pier.diameter / 2,
|
|
||||||
bridgeData.pier.diameter / 2,
|
|
||||||
bridgeData.pier.height,
|
|
||||||
32,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<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,
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
<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>
|
<group>
|
||||||
<mesh
|
{/* Bệ cọc */}
|
||||||
position={[
|
<mesh position={[0, -1000, 0]}>
|
||||||
-bridgeData.deck.width / 2 + 200,
|
<boxGeometry args={[diameter * 6, 2000, diameter * 4]} />
|
||||||
beamData.height +
|
<meshStandardMaterial color="#475569" />
|
||||||
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>
|
</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>
|
</group>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const renderDiaphragm = () => (
|
export const Canvas3D: React.FC = () => {
|
||||||
<mesh position={[0, beamData.height / 2, 0]}>
|
const { activeTab, globalConfig, beamData, bridgeData, isAssemblyMode } =
|
||||||
<boxGeometry
|
useSuperTStore();
|
||||||
args={[
|
const { spanCount, spanLength, beamCountPerSpan, beamSpacing } = globalConfig;
|
||||||
bridgeData.deck.width - 1000,
|
|
||||||
beamData.height * 0.8,
|
// Render toàn bộ tổ hợp cầu N nhịp (Assembly Mode)
|
||||||
bridgeData.diaphragm.thickness,
|
const renderFullBridge = () => {
|
||||||
]}
|
const totalLength = spanCount * spanLength;
|
||||||
/>
|
|
||||||
<meshStandardMaterial color="#e2e8f0" />
|
return (
|
||||||
</mesh>
|
<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 (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="w-full h-full">
|
||||||
<Canvas
|
<Canvas
|
||||||
camera={{ position: [15000, 10000, 25000], fov: 45, far: 500000 }}
|
camera={{ position: [30000, 20000, 40000], fov: 45, far: 500000 }}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={1.2} />
|
<ambientLight intensity={1.2} />
|
||||||
<directionalLight position={[20000, 30000, 20000]} intensity={1.5} />
|
<directionalLight position={[20000, 30000, 20000]} intensity={1.5} />
|
||||||
@@ -140,29 +243,46 @@ export const Canvas3D: React.FC = () => {
|
|||||||
<Bounds fit clip observe margin={1.2}>
|
<Bounds fit clip observe margin={1.2}>
|
||||||
<SelectFocus>
|
<SelectFocus>
|
||||||
{isAssemblyMode ? (
|
{isAssemblyMode ? (
|
||||||
/* Ghép toàn bộ các cấu kiện lại thành cầu */
|
renderFullBridge()
|
||||||
<group>
|
|
||||||
{renderSuperTBeam()}
|
|
||||||
{renderDeck()}
|
|
||||||
{renderPile()}
|
|
||||||
{renderPier()}
|
|
||||||
{renderAbutment()}
|
|
||||||
{renderBearing()}
|
|
||||||
{renderRailing()}
|
|
||||||
{renderDiaphragm()}
|
|
||||||
</group>
|
|
||||||
) : (
|
) : (
|
||||||
/* Hiển thị đơn lẻ theo Tab đang chọn */
|
/* Chế độ xem riêng lẻ cấu kiện */
|
||||||
<>
|
<group>
|
||||||
{activeTab === "superTBeam" && renderSuperTBeam()}
|
{activeTab === "superTBeam" && (
|
||||||
{activeTab === "deck" && renderDeck()}
|
<SuperTBeamMesh data={beamData} />
|
||||||
{activeTab === "pile" && renderPile()}
|
)}
|
||||||
{activeTab === "pier" && renderPier()}
|
{activeTab === "deck" && (
|
||||||
{activeTab === "abutment" && renderAbutment()}
|
<mesh position={[0, bridgeData.deck.thickness / 2, 0]}>
|
||||||
{activeTab === "bearing" && renderBearing()}
|
<boxGeometry
|
||||||
{activeTab === "railing" && renderRailing()}
|
args={[
|
||||||
{activeTab === "diaphragm" && renderDiaphragm()}
|
bridgeData.deck.width,
|
||||||
</>
|
bridgeData.deck.thickness,
|
||||||
|
beamData.length,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#64748b" />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
{activeTab === "pier" && (
|
||||||
|
<mesh position={[0, 0, 0]}>
|
||||||
|
<cylinderGeometry
|
||||||
|
args={[
|
||||||
|
bridgeData.pier.diameter / 2,
|
||||||
|
bridgeData.pier.diameter / 2,
|
||||||
|
bridgeData.pier.height,
|
||||||
|
32,
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
<meshStandardMaterial color="#94a3b8" />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
{activeTab === "pile" && (
|
||||||
|
<PileGroup
|
||||||
|
diameter={bridgeData.pile.diameter}
|
||||||
|
length={bridgeData.pile.length}
|
||||||
|
count={bridgeData.pile.count}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</group>
|
||||||
)}
|
)}
|
||||||
</SelectFocus>
|
</SelectFocus>
|
||||||
</Bounds>
|
</Bounds>
|
||||||
|
|||||||
+56
-13
@@ -1,5 +1,8 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
import { SuperTBeamData } from "@/components/core/schema/superT.schema";
|
import {
|
||||||
|
SuperTBeamData,
|
||||||
|
SuperTBeamSchema,
|
||||||
|
} from "@/components/core/schema/superT.schema";
|
||||||
import {
|
import {
|
||||||
validateSuperTEngineering,
|
validateSuperTEngineering,
|
||||||
ValidationIssue,
|
ValidationIssue,
|
||||||
@@ -9,20 +12,22 @@ export type BridgeComponentType =
|
|||||||
| "superTBeam"
|
| "superTBeam"
|
||||||
| "deck"
|
| "deck"
|
||||||
| "diaphragm"
|
| "diaphragm"
|
||||||
| "asphalt"
|
|
||||||
| "railing"
|
| "railing"
|
||||||
| "expansionJoint"
|
|
||||||
| "subComponents"
|
|
||||||
| "bearing"
|
| "bearing"
|
||||||
| "beamDeckJoint"
|
| "beamDeckJoint"
|
||||||
| "diaphragmJoint"
|
|
||||||
| "substructureJoint"
|
|
||||||
| "abutment"
|
| "abutment"
|
||||||
| "pier"
|
| "pier"
|
||||||
| "footing"
|
|
||||||
| "pile"
|
| "pile"
|
||||||
| "alignment";
|
| "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 {
|
interface BridgeFullData {
|
||||||
deck: { width: number; thickness: number; slope: number };
|
deck: { width: number; thickness: number; slope: number };
|
||||||
diaphragm: { thickness: number; count: number };
|
diaphragm: { thickness: number; count: number };
|
||||||
@@ -40,6 +45,10 @@ interface SuperTStore {
|
|||||||
isAssemblyMode: boolean;
|
isAssemblyMode: boolean;
|
||||||
setIsAssemblyMode: (val: boolean) => void;
|
setIsAssemblyMode: (val: boolean) => void;
|
||||||
|
|
||||||
|
// Cấu hình chung cầu
|
||||||
|
globalConfig: GlobalBridgeConfig;
|
||||||
|
setGlobalConfig: (config: Partial<GlobalBridgeConfig>) => void;
|
||||||
|
|
||||||
beamData: SuperTBeamData;
|
beamData: SuperTBeamData;
|
||||||
validationIssues: ValidationIssue[];
|
validationIssues: ValidationIssue[];
|
||||||
setBeamData: (data: Partial<SuperTBeamData>) => void;
|
setBeamData: (data: Partial<SuperTBeamData>) => void;
|
||||||
@@ -50,9 +59,18 @@ interface SuperTStore {
|
|||||||
data: Partial<BridgeFullData[K]>,
|
data: Partial<BridgeFullData[K]>,
|
||||||
) => void;
|
) => void;
|
||||||
|
|
||||||
|
// Quản lý Import/Export & Regenerate Data Contract
|
||||||
exportJSONContract: () => string;
|
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 = {
|
const initialData: SuperTBeamData = {
|
||||||
id: "550e8400-e29b-41d4-a716-446655440000",
|
id: "550e8400-e29b-41d4-a716-446655440000",
|
||||||
code: "SUPER-T-33M-01",
|
code: "SUPER-T-33M-01",
|
||||||
@@ -66,7 +84,7 @@ const initialData: SuperTBeamData = {
|
|||||||
diaphragmPositions: [0, 16500, 33000],
|
diaphragmPositions: [0, 16500, 33000],
|
||||||
strandCount: 24,
|
strandCount: 24,
|
||||||
placement: {
|
placement: {
|
||||||
station: 1025.5,
|
station: 0,
|
||||||
skewAngle: 0,
|
skewAngle: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
offsetZ: 0,
|
offsetZ: 0,
|
||||||
@@ -81,15 +99,22 @@ const initialBridgeData: BridgeFullData = {
|
|||||||
beamDeckJoint: { rebarDiameter: 16, spacing: 200 },
|
beamDeckJoint: { rebarDiameter: 16, spacing: 200 },
|
||||||
abutment: { height: 7500, topWidth: 1800 },
|
abutment: { height: 7500, topWidth: 1800 },
|
||||||
pier: { height: 12000, diameter: 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) => ({
|
export const useSuperTStore = create<SuperTStore>((set, get) => ({
|
||||||
activeTab: "superTBeam",
|
activeTab: "superTBeam",
|
||||||
setActiveTab: (tab) => set({ activeTab: tab }),
|
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 }),
|
setIsAssemblyMode: (val) => set({ isAssemblyMode: val }),
|
||||||
|
|
||||||
|
globalConfig: initialGlobalConfig,
|
||||||
|
setGlobalConfig: (updated) => {
|
||||||
|
set((state) => ({
|
||||||
|
globalConfig: { ...state.globalConfig, ...updated },
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
beamData: initialData,
|
beamData: initialData,
|
||||||
bridgeData: initialBridgeData,
|
bridgeData: initialBridgeData,
|
||||||
validationIssues: validateSuperTEngineering(initialData),
|
validationIssues: validateSuperTEngineering(initialData),
|
||||||
@@ -122,18 +147,36 @@ export const useSuperTStore = create<SuperTStore>((set, get) => ({
|
|||||||
},
|
},
|
||||||
|
|
||||||
exportJSONContract: () => {
|
exportJSONContract: () => {
|
||||||
const { beamData, bridgeData } = get();
|
const { globalConfig, beamData, bridgeData } = get();
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
{
|
{
|
||||||
header: {
|
header: {
|
||||||
schemaVersion: "1.0",
|
schemaVersion: "1.0",
|
||||||
generator: "TEDI DDP Web Module",
|
generator: "TEDI BIM Super-T Generator - Trial Phase",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
payload: { beamData, bridgeData },
|
payload: { globalConfig, beamData, bridgeData },
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
2,
|
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;
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -11,40 +11,27 @@ const authServices = {
|
|||||||
data: {
|
data: {
|
||||||
username: string;
|
username: string;
|
||||||
password: string;
|
password: string;
|
||||||
deviceId: string;
|
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/api/v1/Auth/login`, data, {
|
return axiosClient.post(`/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, {
|
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: (
|
refresh: (
|
||||||
data: {
|
data: {
|
||||||
identifier: string;
|
refreshToken: string;
|
||||||
},
|
},
|
||||||
tokenAxios?: any,
|
tokenAxios?: any,
|
||||||
) => {
|
) => {
|
||||||
return axiosClient.post(`/api/v1/Auth/refresh`, data, {
|
return axiosClient.post(`/auth/refresh`, data, {
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
logout: (tokenAxios?: any) => {
|
logout: (tokenAxios?: any) => {
|
||||||
return axiosClient.post(
|
return axiosClient.post(
|
||||||
`/api/v1/Auth/logout`,
|
`/auth/logout`,
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
cancelToken: tokenAxios,
|
cancelToken: tokenAxios,
|
||||||
|
|||||||
@@ -124,16 +124,17 @@ const refreshAccessToken = async (): Promise<string | null> => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await refreshClient.post<{
|
const response = await refreshClient.post<{
|
||||||
token?: string;
|
accessToken: string;
|
||||||
accessToken?: string;
|
refreshToken: string;
|
||||||
refreshToken?: string;
|
accessTokenExpiresAt: string;
|
||||||
}>("/Auth/refresh", {
|
sessionId: string;
|
||||||
identifier: refreshToken,
|
}>("/api/auth/refresh", {
|
||||||
|
refreshToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
const responseData = response.data;
|
const responseData = response.data;
|
||||||
|
|
||||||
const newToken = responseData?.token || responseData?.accessToken;
|
const newToken = responseData.accessToken;
|
||||||
|
|
||||||
if (!newToken) {
|
if (!newToken) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user