react 版初始化

This commit is contained in:
Celeste6666 2023-01-31 23:15:50 +08:00
parent f53ba1276d
commit 6c215f987b
58 changed files with 5397 additions and 12490 deletions

2
ibms_react/.env Normal file
View File

@ -0,0 +1,2 @@
REACT_APP_FORGE_TOKEN= "eyJhbGciOiJSUzI1NiIsImtpZCI6IlU3c0dGRldUTzlBekNhSzBqZURRM2dQZXBURVdWN2VhIn0.eyJzY29wZSI6WyJ2aWV3YWJsZXM6cmVhZCJdLCJjbGllbnRfaWQiOiJUQTNocXNGZnpRYk5PVVhLcGxkS1VLU2V3NFNKMjF3NSIsImF1ZCI6Imh0dHBzOi8vYXV0b2Rlc2suY29tL2F1ZC9hand0ZXhwNjAiLCJqdGkiOiJxd3huTzBMQ05OYllFYjBQbWY1MTltNXZoTTBVUW5uc3NBZ1JsaG1MTk16aEw4ZjhPeDBVZVlpVHZaVnVyTEc2IiwiZXhwIjoxNjc0MDUwMjUzfQ.Od82JlAwal50fTw3vD-l7kaKYGKiYjUuDRYg4VvPVVgNQ3CNklPSHee8JnAYSCeNV_XWxkYjv1uh8M14gEB5YCmT8R6KMixXWF_dP_EkOTP1-E9PvCKAZIGx1MFE8XG0I9FbPX6Kd3LHdtKViBTS1V88VZckagRhdWJK4kVGtkXGWJRujIvQLq7VrHeQhOIB8UiEqVA91sTtW-g7-5Xz2U11_UZ6WMZMuENNOJHRN7mIyv2WWj_axHQhlTYtBZXuM_3O4bzNb30i47QPDkuotEEG-J-4OF53rzDYxOIysnutkGgnnz4AwR18xoP0xH8dIFAM1XyuAAdHI-NQh2DTDg"
REACT_APP_FORGE_URN="dXJuOmFkc2sub2JqZWN0czpvcy5vYmplY3Q6Zm9yZ2Vfc2FtcGxlX3RhM2hxc2ZmenFibm91eGtwbGRrdWtzZXc0c2oyMXc1LyVFMyU4MCU5MCVFNSU4RiVCMCVFNSU4QyU5NyVFNCVCOCVBRCVFOCU4RiVCMSVFNSVBNCVBNyVFNiVBOCU5MyVFMyU4MCU5MUFSQyVFOSU5QiU5OSVFNiVBOCVBMSVFNSVCQyU4RitNRVAlRTYlOEIlODYlRTclQjMlQkIlRTclQjUlQjEubndk"

View File

@ -0,0 +1,13 @@
const path = require("path");
module.exports = {
webpack: {
alias: {
"@": path.resolve(__dirname, "src/"),
"@COM": "@/components",
"@ASSET": "@/assets",
"@UTIL": "@/utils",
"@STORE": "@/stores",
"@CON": "@/constants",
},
},
};

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,38 @@
{ {
"name": "ibms_react", "name": "react_bims_forge",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"homepage": "./",
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.2.1",
"@fortawesome/free-solid-svg-icons": "^6.2.1",
"@fortawesome/react-fontawesome": "^0.2.0",
"@popperjs/core": "^2.11.6",
"@reduxjs/toolkit": "^1.9.1",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
"axios": "^1.2.3",
"bootstrap": "^5.2.3",
"chart.js": "^4.2.0",
"http-proxy-middleware": "^2.0.6",
"react": "^18.2.0", "react": "^18.2.0",
"react-bootstrap": "^2.7.0",
"react-chartjs-2": "^5.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-redux": "^8.0.5",
"react-router-dom": "^6.7.0",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux-persist": "^6.0.0",
"requirejs": "^2.3.6",
"three": "^0.149.0",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
"scripts": { "scripts": {
"start": "react-scripts start", "start": "craco start",
"build": "react-scripts build", "build": "craco build",
"test": "react-scripts test", "test": "craco test",
"eject": "react-scripts eject" "eject": "craco eject"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -35,8 +52,9 @@
"last 1 safari version" "last 1 safari version"
] ]
}, },
"proxy": "http://localhost:3604",
"devDependencies": { "devDependencies": {
"eslint-config-react-app": "^7.0.1", "@craco/craco": "^7.0.0",
"jest-editor-support": "^31.0.1" "sass": "^1.57.1"
} }
} }

2145
ibms_react/public/Require.js Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,43 +1,39 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<!-- @noSnoop -->
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta name="description" content="Web site created using create-react-app" />
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!-- <title>Mitsubishi</title>
Notice the use of %PUBLIC_URL% in the tags above. <script src="%PUBLIC_URL%/Require.js"></script>
It will be replaced with the URL of the `public` folder during the build. <!-- <script type="text/javascript" src="http://220.132.206.5:8080/requirejs/config.js"></script>
Only files inside the `public` folder can be referenced from the HTML. <script
type="text/javascript"
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will src="http://220.132.206.5:8080/module/js/com/tridium/js/ext/require/require.min.js?"
work correctly both with client-side routing and a non-root public URL. ></script> -->
Learn how to configure a non-root public URL by running `npm run build`. <script>
--> //重新轉址 for Niagara4
<title>React App</title> var temp_cuurent_Url_pathname = window.location.pathname.split("/").slice(0, 3);
var redirectionUrl =
window.location.origin +
"/" +
temp_cuurent_Url_pathname[temp_cuurent_Url_pathname.length - 1].replace(":%5E", "/") +
"/" +
window.location.pathname.split("/").slice(3).join("/");
//判斷url是否包含"ord",如果有重新轉址
if (temp_cuurent_Url_pathname.findIndex((x) => x == "ord") > -1) {
window.location.replace(redirectionUrl.substr(0, redirectionUrl.length - 1));
}
</script>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<!-- <div id="model_root"></div>
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body> </body>
</html> </html>

View File

@ -0,0 +1,18 @@
require.config({
baseUrl: 'js',
"paths": {
"lib": "../lib",
"jquery-ui": "//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui",
"jquery-ui-min": "//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min",
"d3": "https://d3js.org/d3.v4.min",
"datatables.net": "https://cdn.datatables.net/1.10.21/js/jquery.dataTables.min",
"datatables.net.b4": "https://cdn.datatables.net/1.10.21/js/dataTables.bootstrap4.min"
},
urlArgs: "ts=" + new Date().getTime(),
shim: {
"d3": {
exports: "d3"
}
}
});

10
ibms_react/setupProxy.js Normal file
View File

@ -0,0 +1,10 @@
import { createProxyMiddleware } from "http-proxy-middleware";
// module.exports = function (app) {
// app.use(
// "^/api",
// createProxyMiddleware({
// target: "http://localhost:3604",
// changeOrigin: true,
// }),
// );
// };

View File

@ -1,23 +1,116 @@
import logo from './logo.svg'; import { useMemo, useEffect, useState } from "react";
import './App.css'; import { Outlet, NavLink } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { Container, Nav, Navbar, NavDropdown } from "react-bootstrap";
import { fetchUserInfo, fetchUserAuthPages, changeBuilding, fetchSysMainSub } from "@STORE";
import Logo from "@ASSET/img/logo.png";
import { userAllAuthPages } from "@CON";
import AlarmOffcanvas from "@COM/app/AlarmOffcanvas";
import SystemOffcanvas from "@COM/app/SystemOffcanvas";
function App() { function App() {
const dispatch = useDispatch();
// user
const { userAuthPages, userInfo } = useSelector((state) => state.user);
const TopMenu = useMemo(() => {
let menu = [
userAllAuthPages.find((auth) => auth.name === "首頁"),
...userAuthPages?.map((page) => {
if (userAllAuthPages.some((auth) => auth.name === page.subName)) {
return { ...page, ...userAllAuthPages.find((auth) => auth.name === page.subName) };
}
return page;
}),
];
return menu;
}, [userAuthPages]);
// building
const { buildingList, selectedBuiFullName, selectedBuiTag } = useSelector(
(state) => state.buildingInfo,
);
const updateBuilding = (building_tag, urn_3D) => {
dispatch(changeBuilding({ building_tag, urn_3D }));
};
// system
const { mainSub } = useSelector((state) => state.system);
const [systemMenuShow, setSystemMenuShow] = useState(false);
const systemOffCanvasHandler = (e, name) => {
if (name === "系統監控") {
e.preventDefault();
setSystemMenuShow(!systemMenuShow);
}
};
useEffect(() => {
// 取得使用者資料跟建築物資料(地區及棟別)
dispatch(fetchUserAuthPages());
if (selectedBuiTag) {
dispatch(fetchSysMainSub(selectedBuiTag));
}
}, [selectedBuiTag]);
return ( return (
<div className="App"> <div className="App">
<header className="App-header"> {/* 主選單 */}
<img src={logo} className="App-logo" alt="logo" /> <Navbar bg="dark" variant="dark">
<p> <Container fluid>
Edit <code>src/App.js</code> and save to reload. <Navbar.Brand href="#home">
</p> <img alt="" src={Logo} width="150" className="d-inline-block align-top img-fluid" />
<a </Navbar.Brand>
className="App-link" <Nav className="w-100 d-flex justify-content-between">
href="https://reactjs.org" <NavDropdown
target="_blank" title={selectedBuiFullName ? selectedBuiFullName : "選擇大樓"}
rel="noopener noreferrer" className="d-flex align-items-center fs-5"
> >
Learn React <NavDropdown.Item disabled href="#">
</a> 選擇大樓
</header> </NavDropdown.Item>
{buildingList.map(({ full_name, building_tag, urn_3D }) => (
<NavDropdown.Item
data-urn={urn_3D}
key={building_tag}
onClick={() => {
updateBuilding(building_tag, urn_3D);
}}
>
{full_name}
</NavDropdown.Item>
))}
</NavDropdown>
<div className="d-flex">
{TopMenu.map(({ name, path, icon }) => (
<NavLink
key={name}
to={path ?? "none"}
className="nav-link mx-2 d-flex flex-column align-items-center justify-content-between"
onClick={(e) => {
systemOffCanvasHandler(e, name);
}}
>
{icon}
<span className="mt-1">{name}</span>
</NavLink>
))}
</div>
<div className="d-flex align-items-center">
<AlarmOffcanvas />
<div className="mx-3 text-center">
<label className="mb-0 fs-6 position-relative">
Diamond Controls<span className="position-absolute">®</span>
</label>
<br />
<label className="mb-0 fs-6">智慧大樓管理平台</label>
</div>
</div>
</Nav>
</Container>
</Navbar>
<SystemOffcanvas
list={mainSub}
systemMenuShow={systemMenuShow}
setSystemMenuShow={setSystemMenuShow}
/>
<Outlet />
</div> </div>
); );
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@ -0,0 +1,17 @@
<svg width="164" height="164" viewBox="0 0 164 164" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_d)">
<circle cx="81.7363" cy="81.8212" r="70.3389" fill="#535353"/>
<circle cx="81.7363" cy="81.8212" r="62.3112" stroke="white" stroke-width="16.0554"/>
</g>
<defs>
<filter id="filter0_d" x="0.174763" y="0.259602" width="163.123" height="163.123" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset/>
<feGaussianBlur stdDeviation="5.61135"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 916 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,7 @@
import React from "react";
function Loading() {
return <div>Loading</div>;
}
export default Loading;

View File

@ -0,0 +1,46 @@
import { useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Button, Offcanvas } from "react-bootstrap";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCommentDots, faCommentSlash } from "@fortawesome/free-solid-svg-icons";
import { fetchBuiAlarmStateByBaja } from "@STORE";
function AlarmOffcanvas() {
const dispatch = useDispatch();
const { selectedBuiArea, selectedBuiTag } = useSelector((state) => state.buildingInfo);
// alarm
const [showAlarm, setShowAlarm] = useState(false);
const getAlarmList = () => {
dispatch(fetchBuiAlarmStateByBaja(selectedBuiArea, selectedBuiTag));
setShowAlarm(!showAlarm);
};
return (
<>
<Button
variant="link"
className="d-flex flex-column align-items-center justify-content-center mx-3 text-decoration-none text-white"
onClick={getAlarmList}
>
<FontAwesomeIcon icon={showAlarm ? faCommentSlash : faCommentDots} size="2x" />
<span className="mt-1">{showAlarm ? "隱藏警告" : "顯示警告"}</span>
</Button>
<Offcanvas
className="opacity-75"
show={showAlarm}
onHide={() => {
setShowAlarm(false);
}}
placement="end"
>
<Offcanvas.Header closeButton>
<Offcanvas.Title>Offcanvas</Offcanvas.Title>
</Offcanvas.Header>
<Offcanvas.Body>
Some text as placeholder. In real life you can have the elements you have chosen. Like,
text, images, lists, etc.
</Offcanvas.Body>
</Offcanvas>
</>
);
}
export default AlarmOffcanvas;

View File

@ -0,0 +1,38 @@
import { useSelector, useDispatch } from "react-redux";
import { NavLink } from "react-router-dom";
import { Offcanvas, ListGroup } from "react-bootstrap";
function SystemOffcanvas({ list, systemMenuShow, setSystemMenuShow }) {
return (
<Offcanvas
show={systemMenuShow}
onHide={() => {
setSystemMenuShow(false);
}}
placement="start"
>
<Offcanvas.Header
closeButton
closeVariant="white"
className="align-self-end"
></Offcanvas.Header>
<Offcanvas.Body className="px-0">
<ListGroup>
{list.map(({ full_name, main_system_tag, sub_system_tag }) => (
<NavLink
className="text-decoration-none py-2 list-group-item list-group-item-action"
to={`/monitor/${main_system_tag}_${sub_system_tag}`}
key={`${main_system_tag}_${sub_system_tag}`}
onClick={(e) => setSystemMenuShow(false)}
>
<ListGroup.Item action className="border-0 bg-dark text-center">
{full_name}
</ListGroup.Item>
</NavLink>
))}
</ListGroup>
</Offcanvas.Body>
</Offcanvas>
);
}
export default SystemOffcanvas;

View File

@ -0,0 +1,91 @@
import React, { useEffect, useRef, useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import * as THREE from "three";
import { fetchForge, shutdownForge } from "@STORE/forgeSlice";
import hotspot from "@ASSET/img/hotspot.svg";
function ForgeModel({ forgeHeight, sprites = [] }) {
const dispatch = useDispatch();
const viewerEl = useRef(null);
const {
buildingInfo: { urn_3D },
forgeViewer: { viewer, dataVizExtn, DataVizCore },
} = useSelector((store) => store);
// 載入 Forge Viewer
useEffect(() => {
if (urn_3D) {
dispatch(
fetchForge({
viewerEl: viewerEl.current,
urn: urn_3D,
}),
);
return () => {
dispatch(shutdownForge());
};
}
}, [urn_3D]);
// 熱點
const spriteUpdate = (e, targetDbIds= [], style) => {
e.hasStopped = true;
dataVizExtn.invalidateViewables(targetDbIds, (viewable) => {
console.log(style)
return style;
});
};
// 熱點點擊事件
const onSpriteClicked = (event) => {
const targetDbIds = [event.dbId];
spriteUpdate(event, targetDbIds, { scale: 1.3, color: { r: 1, g: 0, b: 0 } });
console.log(`Sprite clicked:${targetDbIds}`);
};
// 熱點取消點擊
const onSpriteClickedOut = (event) => {
const targetDbIds = [event.dbId];
spriteUpdate(event, targetDbIds, { scale: 1, color: { r: 1.0, g: 1.0, b: 1.0 } });
console.log(`Sprite clickedout:${targetDbIds}`);
}
// 進入系統監控頁面,出現熱點
useEffect(() => {
if (sprites.length !== 0 && viewer && dataVizExtn) {
// const filterSprites = sprites.map;
dataVizExtn.removeAllViewables();
sprites.forEach(async ({ device_coordinate_3d, forge_dbid }, index) => {
const viewableType = DataVizCore.ViewableType.SPRITE;
const spriteColor = new THREE.Color(0xffffff);
const spriteIconUrl = hotspot;
const style = new DataVizCore.ViewableStyle(viewableType, spriteColor, spriteIconUrl);
const viewableData = new DataVizCore.ViewableData();
viewableData.spriteSize = 24;
if (forge_dbid && device_coordinate_3d) {
const dbId = 10 + index;
const position = JSON.parse(device_coordinate_3d);
const viewable = new DataVizCore.SpriteViewable(position, style, dbId);
viewable.myContextData = {
manufacturer: "MJM Co. Ltd.",
};
viewableData.addViewable(viewable);
};
await viewableData.finish();
dataVizExtn.addViewables(viewableData);
});
viewer.addEventListener(DataVizCore.MOUSE_CLICK, onSpriteClicked);
viewer.addEventListener(DataVizCore.MOUSE_CLICK_OUT, onSpriteClickedOut);
}
return () => {
if (sprites.length !== 0 && viewer && dataVizExtn) {
viewer.removeEventListener(DataVizCore.MOUSE_CLICK, onSpriteClicked);
viewer.removeEventListener(DataVizCore.MOUSE_CLICK_OUT, onSpriteClickedOut);
}
};
}, [sprites, dataVizExtn, viewer]);
return <div ref={viewerEl} id="forgeViewer" style={{ height: forgeHeight }}></div>;
}
export default ForgeModel;

View File

@ -0,0 +1,63 @@
import React from "react";
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
BarElement,
Title,
Tooltip,
Legend,
} from "chart.js";
import { Bar } from "react-chartjs-2";
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend);
// 傳入的 barData: {current: ["今日/本週"], last: ["昨日/上週"]}
function BarChart({ title, period, barData }) {
const options = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "top",
},
title: {
display: true,
align: "start",
position: "top",
text: title,
},
scales: {
y: {
suggestedMin: 0,
},
},
},
};
const hourLabels = Array.from({ length: 24 }, (v, i) => i);
const dayLabels = ["週一", "週二", "週三", "週四", "週五", "週六", "週日"];
const labels = period === "days" ? dayLabels : hourLabels;
const data = {
labels,
datasets: [
{
label: period === "days" ? "本週用電量" : "今日用電量",
data: barData.current,
backgroundColor: "#10b7b9",
},
{
label: period === "days" ? "上週用電量" : "昨日用電量",
data: barData.last,
backgroundColor: "#7dbffa",
},
],
};
return (
<>
<Bar options={options} data={data} />
</>
);
}
export default BarChart;

View File

@ -0,0 +1,34 @@
import { Chart as ChartJS, ArcElement, Tooltip, Legend } from "chart.js";
import { Pie } from "react-chartjs-2";
ChartJS.register(ArcElement, Tooltip, Legend);
function PieChart({ labels }) {
const options = {
responsive: false,
plugins: {
legend: {
position: "top",
align: "center",
labels: {
boxWidth: 30,
},
},
},
};
const data = {
labels,
datasets: [
{
label: "數量",
data: [12, 19],
backgroundColor: ["#fd3995", "#51adf6"],
borderColor: ["#fff", "#fff"],
borderWidth: 1,
},
],
};
return <Pie data={data} options={options} className="mb-4" />;
}
export default PieChart;

View File

@ -0,0 +1,22 @@
import Modal from "react-bootstrap/Modal";
function ChartModal({ modalShow, setModalShow, chart }) {
return (
<>
<Modal
size="xl"
show={modalShow}
onHide={() => {
setModalShow(false);
}}
>
<Modal.Header closeButton>
<Modal.Title className="text-dark">{chart.title}</Modal.Title>
</Modal.Header>
<Modal.Body style={{ height: "50vh" }}>{chart.component}</Modal.Body>
</Modal>
</>
);
}
export default ChartModal;

View File

@ -0,0 +1,59 @@
const n4Sup = "Mitsubishi_JACE8000";
// dev
const baseApiUrl = "http://localhost:3604";
const baseImgUrl = "https://localhost:44376/upload/device_icon";
//production
// const baseApiUrl = "http://220.132.206.5";
// const baseImgUrl = "http://220.132.206.5:8848/upload/device_icon/";
// 登入
const loginBaseUrl = `${baseApiUrl}/api/Login/`;
const userAuthBaseUrl = `${baseApiUrl}/api/GetUsrFroList`;
const userInfoBaseUrl = `${baseApiUrl}/api/getUserFull`;
const forgeTokenBaseUrl = `${baseApiUrl}/api/forge/oauth/token`;
// post
const mainSubBaseUrl = `${baseApiUrl}/api/Device/GetMainSub`;
// 設備
const deviceBuiListBaseUrl = `${baseApiUrl}/api/Device/GetBuild`;
const deviceBuiMenuBaseUrl = `${baseApiUrl}/api/Device/GetBuildMenu`;
const deviceForgeInvBaseUrl = `${baseApiUrl}/api/Device/GetForgeInvType`;
const deviceFloorBaseUrl = `${baseApiUrl}/api/Device/GetFloor`;
const deviceNextFloorBaseUrl = `${baseApiUrl}/api/Device/GetNextFloor`;
const deviceListBaseUrl = `${baseApiUrl}/api/Device/GetDeviceList`;
// const deviceMainSubListBaseUrl = `${baseApiUrl}/api/Device/GetMainSub`; // 同 mainSubBaseUrl
const deviceForgeNodeIdBaseUrl = `${baseApiUrl}/api/Device/GetForgeNodeIdFromVar`;
const deviceHotPointBaseUrl = `${baseApiUrl}/api/GetDevForCor`;
// 電錶
const energyBaseUrl = `${baseApiUrl}/api/Energe/GetElecBySubSysTag`;
// 歷史
// post
const historyBaseUrl = `${baseApiUrl}/api/History/GetMainSub`;
const historyDeviceBaseUrl = `${baseApiUrl}/api/History/GetDevPoi`;
// 上傳檔案
const uploadFileBaseUrl = `${baseApiUrl}/api/Upload`;
export {
n4Sup,
baseApiUrl,
baseImgUrl,
loginBaseUrl,
userAuthBaseUrl,
userInfoBaseUrl,
forgeTokenBaseUrl,
mainSubBaseUrl,
deviceBuiListBaseUrl,
deviceBuiMenuBaseUrl,
deviceForgeInvBaseUrl,
deviceFloorBaseUrl,
deviceNextFloorBaseUrl,
deviceListBaseUrl,
deviceForgeNodeIdBaseUrl,
deviceHotPointBaseUrl,
energyBaseUrl,
historyBaseUrl,
historyDeviceBaseUrl,
uploadFileBaseUrl,
};

View File

@ -0,0 +1,52 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faUser, faGem, faLightbulb, faGlobe } from "@fortawesome/free-solid-svg-icons";
export const homeMonitorElectricity = [
{
title: "今日用電量 kWH",
icon: (
<FontAwesomeIcon
size="10x"
className="opacity-75 position-absolute"
style={{ right: "-25px", top: "-5px" }}
icon={faUser}
/>
),
backgroundColor: "bg-primary-300",
},
{
title: "昨日用電量 kWH",
icon: (
<FontAwesomeIcon
size="10x"
className="opacity-75 position-absolute"
style={{ right: "-25px", top: "-5px" }}
icon={faGem}
/>
),
backgroundColor: "bg-warning-400",
},
{
title: "即時功率 kW",
icon: (
<FontAwesomeIcon
size="10x"
className="opacity-75 position-absolute"
style={{ right: "-25px", top: "-5px" }}
icon={faLightbulb}
/>
),
backgroundColor: "bg-success-200",
},
{
title: "即時契約容量占比 %",
icon: (
<FontAwesomeIcon
size="10x"
className="opacity-75 position-absolute"
style={{ right: "-25px", top: "-5px" }}
icon={faGlobe}
/>
),
backgroundColor: "bg-info-200",
},
];

View File

@ -0,0 +1,118 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faGripVertical,
faGripHorizontal,
faBolt,
faCarBattery,
faLightbulb,
faSun,
faIcicles,
faBong,
faSnowflake,
faDoorOpen,
faFireExtinguisher,
faSmog,
faStopwatch,
faTint,
faUserShield,
faWind,
} from "@fortawesome/free-solid-svg-icons";
export const homeMonitorSystem = [
{
text: "高壓配電盤",
mainSys: "EE",
subSys: "E1",
icon: <FontAwesomeIcon size="xl" icon={faGripVertical} />,
},
{
text: "低壓配電盤",
mainSys: "EE",
subSys: "E2",
icon: <FontAwesomeIcon size="xl" icon={faGripHorizontal} />,
},
{
text: "緊急發電機",
mainSys: "EE",
subSys: "E3",
icon: <FontAwesomeIcon size="xl" icon={faBolt} />,
},
{
text: "電錶系統",
mainSys: "EE",
subSys: "E4",
icon: <FontAwesomeIcon size="xl" icon={faCarBattery} />,
},
{
text: "二線式照明系統",
mainSys: "LT",
subSys: "L1",
icon: <FontAwesomeIcon size="xl" icon={faLightbulb} />,
},
{
text: "景觀照明系統",
mainSys: "LT",
subSys: "L2",
icon: <FontAwesomeIcon size="xl" icon={faSun} />,
},
{
text: "儲冰系統",
mainSys: "ME",
subSys: "M1",
icon: <FontAwesomeIcon size="xl" icon={faIcicles} />,
},
{
text: "小型送風機",
mainSys: "ME",
subSys: "M10",
icon: <FontAwesomeIcon size="xl" icon={faWind} />,
},
{
text: "排油煙設備",
mainSys: "ME",
subSys: "M8",
icon: <FontAwesomeIcon size="xl" icon={faBong} />,
},
{
text: "環境感測設備",
mainSys: "ME",
subSys: "M12",
icon: <FontAwesomeIcon size="xl" icon={faSnowflake} />,
},
{
text: "電梯設備",
mainSys: "ELEV",
subSys: "EL",
icon: <FontAwesomeIcon size="xl" icon={faDoorOpen} />,
},
{
text: "消防設備",
mainSys: "FE",
subSys: "F1",
icon: <FontAwesomeIcon size="xl" icon={faFireExtinguisher} />,
},
{
text: "排煙系統",
mainSys: "FE",
subSys: "F2",
icon: <FontAwesomeIcon size="xl" icon={faSmog} />,
},
{
text: "電子水錶",
mainSys: "WP",
subSys: "W1",
icon: <FontAwesomeIcon size="xl" icon={faStopwatch} />,
},
{
text: "熱水系統",
mainSys: "W3",
subSys: "W1",
icon: <FontAwesomeIcon size="xl" icon={faTint} />,
},
{
text: "門禁安全系統",
mainSys: "S",
subSys: "R",
icon: <FontAwesomeIcon size="xl" icon={faUserShield} />,
},
];

View File

@ -0,0 +1,51 @@
import {
n4Sup,
baseApiUrl,
baseImgUrl,
loginBaseUrl,
userAuthBaseUrl,
userInfoBaseUrl,
forgeTokenBaseUrl,
mainSubBaseUrl,
deviceBuiListBaseUrl,
deviceBuiMenuBaseUrl,
deviceForgeInvBaseUrl,
deviceFloorBaseUrl,
deviceNextFloorBaseUrl,
deviceListBaseUrl,
deviceForgeNodeIdBaseUrl,
deviceHotPointBaseUrl,
energyBaseUrl,
historyBaseUrl,
historyDeviceBaseUrl,
uploadFileBaseUrl,
} from "./baseSetting";
import { homeMonitorSystem } from "./homeMonitorSystem";
import { homeMonitorElectricity } from "./homeMonitorElectricity";
import { userAllAuthPages } from "./user";
export {
n4Sup,
baseApiUrl,
baseImgUrl,
loginBaseUrl,
userAuthBaseUrl,
userInfoBaseUrl,
forgeTokenBaseUrl,
mainSubBaseUrl,
deviceBuiListBaseUrl,
deviceBuiMenuBaseUrl,
deviceForgeInvBaseUrl,
deviceFloorBaseUrl,
deviceNextFloorBaseUrl,
deviceListBaseUrl,
deviceForgeNodeIdBaseUrl,
deviceHotPointBaseUrl,
energyBaseUrl,
historyBaseUrl,
historyDeviceBaseUrl,
uploadFileBaseUrl,
homeMonitorSystem,
homeMonitorElectricity,
userAllAuthPages,
};

View File

@ -0,0 +1,35 @@
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import {
faHouse,
faDisplay,
faChartPie,
faChartArea,
faChartLine,
faBell,
faServer,
faImage,
faUser,
} from "@fortawesome/free-solid-svg-icons";
export const userAllAuthPages = [
{ name: "首頁", path: "/", icon: <FontAwesomeIcon size="2x" icon={faHouse} /> },
{ name: "系統監控", path: "/monitor", icon: <FontAwesomeIcon size="2x" icon={faDisplay} /> },
{ name: "能源管理", path: "/energy", icon: <FontAwesomeIcon size="2x" icon={faChartPie} /> },
{ name: "歷史資料", path: "/history", icon: <FontAwesomeIcon size="2x" icon={faChartArea} /> },
{ name: "報表管理", path: "/form", icon: <FontAwesomeIcon size="2x" icon={faChartLine} /> },
{ name: "即時告警", path: "/alert", icon: <FontAwesomeIcon size="2x" icon={faBell} /> },
{
name: "運維管理",
path: "/operation",
icon: <FontAwesomeIcon size="2x" icon={faServer} />,
},
{
name: "圖資管理",
path: "/graphManagement",
icon: <FontAwesomeIcon size="2x" icon={faImage} />,
},
{
name: "帳號管理",
path: "/accountManagement",
icon: <FontAwesomeIcon size="2x" icon={faUser} />,
},
];

View File

@ -1,17 +1,48 @@
import React from 'react'; import React from "react";
import ReactDOM from 'react-dom/client'; import ReactDOM from "react-dom/client";
import './index.css'; import { createHashRouter, RouterProvider } from "react-router-dom";
import App from './App'; import store from "./stores/index";
import reportWebVitals from './reportWebVitals'; import { Provider } from "react-redux";
import axios from "axios";
import "./scss/index.scss";
import routes from "./routes/routes";
const root = ReactDOM.createRoot(document.getElementById('root')); // 添加请求拦截器
root.render( axios.interceptors.request.use(
<React.StrictMode> (config) => {
<App /> const myCookie = document.cookie.replace(
</React.StrictMode> /(?:(?:^|.*;\s*)JWT-Authorization\s*\=\s*([^;]*).*$)|^.*$/,
"$1",
);
if (myCookie) {
config.headers["Authorization"] = `Bearer ${myCookie}`;
}
return config;
},
(error) => {
return Promise.reject(error);
},
); );
// If you want to start measuring performance in your app, pass a function // 添加响应拦截器
// to log results (for example: reportWebVitals(console.log)) axios.interceptors.response.use(
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals function (response) {
reportWebVitals(); // 2xx 范围内的状态码都会触发该函数。
// 对响应数据做点什么
return { ...response.data };
},
function (error) {
// 超出 2xx 范围的状态码都会触发该函数。
// 对响应错误做点什么
return Promise.reject(error);
},
);
const router = createHashRouter(routes);
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<RouterProvider router={router} />
</Provider>
</React.StrictMode>,
);

View File

@ -0,0 +1,7 @@
import React from "react";
function AccountManagement() {
return <div>AccountManagement</div>;
}
export default AccountManagement;

View File

@ -0,0 +1,7 @@
import React from "react";
function Alert() {
return <div>Alert</div>;
}
export default Alert;

View File

@ -0,0 +1,7 @@
import React from "react";
function Energy() {
return <div>Energy</div>;
}
export default Energy;

View File

@ -0,0 +1,7 @@
import React from "react";
function GraphManagement() {
return <div>GraphManagement</div>;
}
export default GraphManagement;

View File

@ -0,0 +1,7 @@
import React from "react";
function history() {
return <div>history</div>;
}
export default history;

View File

@ -0,0 +1,179 @@
import { useEffect, useMemo, useState } from "react";
import { useSelector, useDispatch } from "react-redux";
import { Link } from "react-router-dom";
import { Container, Row, Col, Card, ButtonGroup, Button } from "react-bootstrap";
import { homeMonitorSystem, homeMonitorElectricity } from "@CON";
import ForgeModel from "@COM/forges/ForgeModel";
import BarChart from "@COM/general/BarChart";
import PieChart from "@COM/general/PieChart";
import ChartModal from "@COM/home/ChartModal";
import { fetchBuiAlarmStateByBaja, fetchInitElecMeterByBaja } from "@STORE";
function Home() {
const dispatch = useDispatch();
// building
const {
buildingInfo: { selectedBuiArea, selectedBuiTag },
electricity: { electricMeter, hourElectricMeter, weekElectricMeter, curElectricity },
alarm: { sidebarAlarm },
} = useSelector((state) => state);
// electricity
const topElec = useMemo(() => {
const newCard = homeMonitorElectricity.map((elec) => {
if (electricMeter.some((em) => elec.title.includes(em.text))) {
return { ...elec, ...electricMeter.find((em) => elec.title.includes(em.text)) };
} else if (curElectricity.some((cur) => elec.title.includes(cur.text))) {
return { ...elec, ...curElectricity.find((cur) => elec.title.includes(cur.text)) };
}
return elec;
});
return newCard;
}, [electricMeter, curElectricity]);
useEffect(() => {
if (selectedBuiArea && selectedBuiTag) {
// 為了取得首頁小卡片的報警資料
dispatch(fetchBuiAlarmStateByBaja(selectedBuiArea, selectedBuiTag));
dispatch(fetchInitElecMeterByBaja());
}
}, [selectedBuiArea, selectedBuiTag]);
// 圖表放大展示
const [modalShow, setModalShow] = useState(false);
const [chart, setChart] = useState({ title: "", component: <></> });
return (
<>
<Container fluid className="mt-3">
<Row className="g-0" md={3}>
<Col>
<ForgeModel forgeHeight="100%" />
</Col>
<Col xs={1} lg={8} className="h-100">
<Container fluid>
<Row lg={4} className="g-4">
{topElec.map(({ title, data, icon, backgroundColor }) => (
<Col key={title}>
<Card className={`${backgroundColor} overflow-hidden py-1`}>
<div className="w-100 h-100 position-relative">{icon}</div>
<Card.Body>
<Card.Title as="h1" className="fw-bold">
{data ?? 0}
</Card.Title>
<Card.Text>{title}</Card.Text>
</Card.Body>
</Card>
</Col>
))}
</Row>
<Row md={2} className="g-2 mt-2">
<Col>
<Card
bg="dark"
className="p-2 h-100"
onClick={() => {
setModalShow(true);
setChart({
title: "今日/昨日用電量比較",
component: (
<BarChart
title="今日/昨日用電量比較"
period="hours"
barData={hourElectricMeter}
/>
),
});
}}
>
{/* barData 透過 baja 取資料 */}
<BarChart
title="今日/昨日用電量比較"
period="hours"
barData={hourElectricMeter}
/>
</Card>
</Col>
<Col>
<Card
bg="dark"
className="p-2 h-100"
onClick={() => {
setModalShow(true);
setChart({
title: "本週/上週用電量比較",
component: (
<BarChart
title="本週/上週用電量比較"
period="days"
barData={weekElectricMeter}
/>
),
});
}}
>
<BarChart
title="本週/上週用電量比較"
period="days"
barData={weekElectricMeter}
/>
</Card>
</Col>
</Row>
</Container>
</Col>
</Row>
<Row className="my-1 g-2" md={3}>
<Col xs={1} lg={8} className="d-flex justify-content-between flex-wrap">
{homeMonitorSystem.map(({ text, mainSys, subSys, icon }) => (
<ButtonGroup
size="lg"
key={`${selectedBuiArea}/${selectedBuiTag}/${mainSys}/${subSys}`}
className="my-3"
style={{ width: "24%" }}
data-id={`${selectedBuiArea}/${selectedBuiTag}/${mainSys}/${subSys}`}
>
<Button
variant="secondary"
className={`border-0 border-end border-2 p-2 ${
sidebarAlarm.some((alarm) =>
alarm.sourceName.includes(
`${selectedBuiArea}/${selectedBuiTag}/${mainSys}/${subSys}`,
),
)
? "animated"
: ""
}`}
style={{ maxWidth: "30%" }}
>
{icon}
</Button>
<Button variant="secondary" className="p-2 ">
{text}
</Button>
</ButtonGroup>
))}
</Col>
<Col xs={1} lg={2} className="pt-2 my-3">
<Card bg="dark" className="h-100">
<Card.Header>異常狀態</Card.Header>
<Card.Body className="w-100 d-flex flex-column align-items-center justify-content-between">
<PieChart labels={["異常數量", "復歸數量"]} className="mb-4" />
<PieChart labels={["已確認異常", "未確認異常"]} />
</Card.Body>
</Card>
</Col>
<Col xs={1} lg={2} className="pt-2 my-3">
<Card bg="dark" className="h-100">
<Card.Header>工單進度</Card.Header>
<Card.Body className="w-100 d-flex flex-column align-items-center justify-content-between py-0">
<PieChart labels={["異常未派工", "異常已派工"]} />
<PieChart labels={["工單已完成", "工單未完成"]} />
</Card.Body>
</Card>
</Col>
</Row>
</Container>
<ChartModal modalShow={modalShow} setModalShow={setModalShow} chart={chart} />
</>
);
}
export default Home;

View File

@ -0,0 +1,72 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { Form, Button, Container, Card } from "react-bootstrap";
import { userLogin } from "@UTIL/index";
import clouds from "@ASSET/img/clouds.png";
import video1 from "@ASSET/video/cc.mp4";
import video2 from "@ASSET/video/cc.webm";
function Login() {
const [user, setUser] = useState({ account: "webUser", password: "rJ2T5Kkj" });
const navigate = useNavigate();
return (
<div className="vh-100 d-flex align-items-center justify-content-center position-relative overflow-hidden">
<video poster={clouds} playsInline autoPlay muted className="position-absolute">
<source src={video2} type="video/webm" />
<source src={video1} type="video/mp4" />
</video>
<Card className="w-25 position-absolute" bg="dark">
<Card.Body>
<Form>
<Form.Group className="mb-3" controlId="account">
<Form.Label>帳號</Form.Label>
<Form.Control
value="webUser"
type="text"
name="account"
placeholder="請輸入帳號"
onChange={(e) => {
setUser({ ...user, account: e.target.value });
}}
/>
<Form.Text className="text-muted">您的帳號</Form.Text>
</Form.Group>
<Form.Group className="mb-3" controlId="password">
<Form.Label>Password</Form.Label>
<Form.Control
type="password"
name="password"
placeholder="請輸入密碼"
autoComplete="off"
value="rJ2T5Kkj"
onChange={(e) => {
setUser({ ...user, password: e.target.value });
}}
/>
<Form.Text className="text-muted">您的密碼</Form.Text>
</Form.Group>
<Form.Group className="mb-3" controlId="formBasicCheckbox">
<Form.Check type="checkbox" label="記住我" />
</Form.Group>
<Button
variant="primary"
type="submit"
onClick={(e) => {
userLogin(e, user, navigate);
}}
>
登入
</Button>
<div className="blankpage-footer text-center">
<Button variant="link" asp-controller="Login" asp-action="ForgotPassword">
<strong>忘記密碼</strong>
</Button>
</div>
</Form>
</Card.Body>
</Card>
</div>
);
}
export default Login;

View File

@ -0,0 +1,102 @@
import React, { useEffect } from "react";
import { useParams } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { Container, Row, Col, Card, ButtonGroup, Button } from "react-bootstrap";
import ForgeModel from "@COM/forges/ForgeModel";
import { fetchSelectedDeviceMenu, fetchSelectedDeviceList, fetchSelectedDevFlTags } from "@STORE";
import defdev from "@ASSET/img/defdev.png";
function Monitor() {
const params = useParams().systemId; // main_sub
const [main_system_tag, sub_system_tag] = params.split("_");
const dispatch = useDispatch();
const {
buildingInfo: { selectedBuiTag },
device: { selectedDeviceMenu, selectedDeviceList, selectedDeviceFloorTags },
system: { mainSub },
} = useSelector((state) => state);
useEffect(() => {
if (selectedBuiTag && main_system_tag && sub_system_tag) {
dispatch(fetchSelectedDeviceMenu({ main_system_tag, sub_system_tag }));
dispatch(fetchSelectedDeviceList({ sub_system_tag }));
dispatch(fetchSelectedDevFlTags({ sub_system_tag }));
}
// 載入資料
}, [main_system_tag, sub_system_tag, selectedBuiTag]);
return (
<>
<Container
fluid
className="mb-3 py-1 d-flex align-items-center"
style={{ background: "#505050" }}
>
<span className="fs-4 text-white-50 mx-4">
{mainSub.length !== 0 &&
mainSub.find((s) => params.includes(s.sub_system_tag))?.full_name}
</span>
<ButtonGroup size="sm">
<Button variant="secondary">總覽</Button>
{selectedDeviceFloorTags.length !== 0 &&
selectedDeviceFloorTags.map(({ floor_tag, floor_guid }) => (
<Button variant="secondary" key={floor_guid}>
{floor_tag}
</Button>
))}
</ButtonGroup>
</Container>
<Container fluid className="pe-2 ps-5">
<Row>
<Col md={6}>
{selectedDeviceMenu?.left_system_url &&
window.location.href.includes("http://220.132.206.5") ? (
<iframe
src={selectedDeviceMenu.left_system_url}
className="w-100 h-100"
title="設計圖稿"
></iframe>
) : (
selectedDeviceFloorTags.map(({ floor_tag, floor_guid }) => (
<Row className="g-3" key={floor_guid}>
<Col md={1}>
<Button className="px-4 py-2 text-white">{floor_tag}</Button>
</Col>
<Col className="d-flex justify-content-evenly align-items-center flex-wrap">
{selectedDeviceList.map(({ device_guid, full_name, forge_dbid }) => (
<Card
bg="dark"
key={device_guid}
className="mb-3"
style={{ maxWidth: "45%" }}
>
<Card.Body>
<Card.Title>
<img src={defdev} className="w-25 h-25 me-3" />
{full_name}
</Card.Title>
<Card.Text>
<span className="text-secondary me-4">功率 kW</span>
<Card.Link
className="text-white text-decoration-none"
data-forgeId={forge_dbid}
>
詳細內容
</Card.Link>
</Card.Text>
</Card.Body>
</Card>
))}
</Col>
</Row>
))
)}
</Col>
<Col md={6}>
<ForgeModel forgeHeight="90vh" sprites={selectedDeviceList} />
</Col>
</Row>
</Container>
</>
);
}
export default Monitor;

View File

@ -0,0 +1,7 @@
import React from "react";
function Operation() {
return <div>Operation</div>;
}
export default Operation;

View File

@ -0,0 +1,82 @@
import Login from "@/pages/Login";
import App from "@/App";
import Home from "@/pages/Home";
import Monitor from "@/pages/Monitor";
import Energy from "@/pages/Energy";
import History from "@/pages/History";
import Alert from "@/pages/Alert";
import Operation from "@/pages/Operation";
import GraphManagement from "@/pages/GraphManagement";
import AccountManagement from "@/pages/AccountManagement";
import { redirect } from "react-router-dom";
import { setUserAccountByBaja, userLogin } from "@UTIL/index";
const monitor = [{}, {}];
const routes = [
{
path: "/",
element: <App />,
loader: async () => {
let JWT_Authorization = document.cookie.replace(
/(?:(?:^|.*;\s*)JWT-Authorization\s*\=\s*([^;]*).*$)|^.*$/,
"$1",
);
if (window.location.href.includes("localhost:3000") && !JWT_Authorization) {
console.log(JWT_Authorization);
return redirect("/login");
} else if (window.location.href.includes("http://220.132.206.5") && !JWT_Authorization) {
const account = await setUserAccountByBaja();
console.log("account", account);
await userLogin(
null,
{
account,
password: "rJ2T5Kkj",
},
null,
);
return null;
}
return null;
},
children: [
{
index: true,
element: <Home />,
},
{
path: "monitor/:systemId",
element: <Monitor />,
children: monitor,
},
{
path: "energy",
element: <Energy />,
},
{
path: "history",
element: <History />,
},
{
path: "alert",
element: <Alert />,
},
{
path: "operation",
element: <Operation />,
},
{
path: "graphManagement",
element: <GraphManagement />,
},
{
path: "accountManagement",
element: <AccountManagement />,
},
],
},
{
path: "/login",
element: <Login />,
},
];
export default routes;

View File

@ -0,0 +1,79 @@
// Include any default variable overrides here (though functions won't be available)
$font-size-base: 0.8125rem;
$font-weight-bold: 900;
$body-bg: #37393e;
$body-color: #fff;
$primary: #886ab5;
$primary-hover: #ccbfdf;
$success: #10b7b9;
// $danger:
$info:#0c7cd5;
$dark: #212225;
$navbar-dark-color: white;
$navbar-dark-hover-color: $primary-hover;
$navbar-dark-active-color: $primary;
$offcanvas-horizontal-width: 250px;
$offcanvas-bg-color: rgba(33, 34, 37,0.95);
$list-group-color: $body-color;
$list-group-bg: $dark;
$list-group-action-color: $primary;
$list-group-hover-bg: white;
$list-group-action-active-bg: white;
$list-group-border-radius: 0;
$list-group-border-width: 0;
@import "~/node_modules/bootstrap/scss/bootstrap";
// Then add additional custom code here
body {
margin: 0;
padding: 0;
min-height: 100vh;
overflow-x: hidden;
color: white
}
#forgeViewer {
width: 95%;
height: 100%;
margin: 0;
background-color: #f0f8ff;
position: relative;
}
.list-group-item-action:hover, .list-group-item-action:hover>.list-group-item-action{
background-color: $list-group-hover-bg !important;
color: $list-group-action-color !important;
}
.list-group-item-action.active,.list-group-item-action.active> .list-group-item-action{
background-color: $list-group-action-active-bg !important;
color: $list-group-action-color !important;
}
.bg-primary-300 {
background-color: rgba(163, 140, 198,0.4);}
.bg-success-200 {
background-color: rgba(29,201,183,0.3); }
.bg-info-200 {
background-color: rgba(106, 184, 247,.3);}
.bg-warning-400 {
background-color: rgba(255, 202, 91,.5);}
.animated{
animation-name: text_twinkle;
animation-duration: .5s;
animation-iteration-count: infinite;
animation-timing-function: linear;
animation-fill-mode: none;
}
@keyframes text_twinkle {
0% {
color : $danger
}
75% {
color : $danger
}
100% {
color : $white
}
}

View File

@ -0,0 +1,37 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { getBuiAlarmStateByBaja } from "@UTIL";
export const fetchBuiAlarmStateByBaja = createAsyncThunk(
"alarm/fetchBuiAlarmStateByBaja",
async ({ area_tag, building_tag }, { dispatch }) => {
const res = await getBuiAlarmStateByBaja(area_tag, building_tag);
console.log(res);
dispatch(getSidebarAlarm(res));
const timer = window.setInterval(async () => {
const res = await getBuiAlarmStateByBaja(area_tag, building_tag);
dispatch(getSidebarAlarm(res));
}, 300000);
},
);
const alarmSlice = createSlice({
name: "alarm",
initialState: {
sidebarAlarm: [],
},
reducers: {
getSidebarAlarm: (state, { payload }) => {
console.log(payload);
state.sidebarAlarm = payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchBuiAlarmStateByBaja.fulfilled, (state) => {
return;
});
},
});
const { reducer, actions } = alarmSlice;
export const { getSidebarAlarm } = actions;
export default reducer;

View File

@ -0,0 +1,47 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { deviceBuiListBaseUrl } from "@CON";
import { ajaxRes } from "@UTIL";
// 異步取得區域及棟別
export const fetchBuiList = createAsyncThunk("building/fetchBuiList", async (token, thunkAPI) => {
const res = await axios.post(deviceBuiListBaseUrl);
const { building_tag, urn_3D, full_name } = res.data[0];
thunkAPI.dispatch(changeBuilding({ building_tag, urn_3D, full_name }));
return ajaxRes(res, thunkAPI);
});
const buildingSlice = createSlice({
name: "building",
initialState: {
buildingList: ["中凌大樓"],
urn_3D: "",
selectedBuiArea: "TPE",
selectedBuiTag: "",
selectedBuiFullName: "",
errMsg: "",
},
reducers: {
// 改變區域
changeArea: () => {},
// 改變棟別
changeBuilding: (state, { payload: { building_tag, urn_3D, full_name } }) => {
state.urn_3D = urn_3D;
state.selectedBuiTag = building_tag;
state.selectedBuiFullName = full_name;
},
},
extraReducers: (builder) => {
// pending, fulfilled, rejected
builder
.addCase(fetchBuiList.fulfilled, (state, { payload }) => {
state.buildingList = payload;
})
.addCase(fetchBuiList.rejected, (state, { payload }) => {
state.errMsg = payload;
});
},
});
const { actions, reducer } = buildingSlice;
export const { changeBuilding } = actions;
export default reducer;

View File

@ -0,0 +1,92 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { deviceBuiMenuBaseUrl, deviceListBaseUrl, deviceFloorBaseUrl } from "@CON";
import { ajaxRes } from "@UTIL";
export const fetchSelectedDeviceMenu = createAsyncThunk(
"deviceList/fetchSelectedDeviceMenu",
async ({ main_system_tag, sub_system_tag }, thunkAPI) => {
const { selectedBuiTag } = thunkAPI.getState().buildingInfo;
const res = await axios.post(deviceBuiMenuBaseUrl, {
building_tag: selectedBuiTag,
main_system_tag,
sub_system_tag,
});
return ajaxRes(res, thunkAPI);
},
);
export const fetchSelectedDeviceList = createAsyncThunk(
"deviceList/fetchSelectedDeviceList",
async ({ sub_system_tag }, { getState, fulfillWithValue, rejectWithValue }) => {
const { selectedBuiTag } = getState().buildingInfo;
const res = await axios.post(deviceListBaseUrl, {
building_tag: selectedBuiTag,
sub_system_tag,
});
const { code = 9999, msg, data = null } = res;
if (code !== "0000" || !data) {
return rejectWithValue(msg);
} else {
console.log(data)
let results=[];
data.forEach((p) => {
p.device_list.forEach((d) => {
results.push({ ...d, device_floor: p.full_name });
});
});
console.log("v", [...results]);
return fulfillWithValue([...results]);
}
},
);
export const fetchSelectedDevFlTags = createAsyncThunk(
"deviceList/fetchSelectedDevFlTags",
async ({ sub_system_tag }, thunkAPI) => {
const { selectedBuiTag } = thunkAPI.getState().buildingInfo;
const res = await axios.post(deviceFloorBaseUrl, {
building_tag: selectedBuiTag,
sub_system_tag,
});
return ajaxRes(res, thunkAPI);
},
);
const deviceListSlice = createSlice({
name: "deviceList",
initialState: {
allDeviceList: [],
selectedDeviceMenu: {}, //總覽
selectedDeviceList: [],
selectedDeviceSysTag: "", // ELEV
selectedDeviceNameTag: "", // EL
selectedDeviceFloorTags: [], // R2F
selectedDeviceMaster: "", // BANK1
selectedDeviceLastName: "", // ELEV
selectedDeviceSerialTag: "", // N1
},
reducers: {
getAllDevice: (state, action) => {},
},
extraReducers: (builder) => {
builder.addCase(fetchSelectedDeviceMenu.fulfilled, (state, { payload }) => {
state.selectedDeviceMenu = payload;
});
builder.addCase(fetchSelectedDeviceList.fulfilled, (state, { payload }) => {
console.log("de", payload);
state.selectedDeviceList = payload;
});
builder.addCase(fetchSelectedDevFlTags.fulfilled, (state, { payload }) => {
state.selectedDeviceFloorTags = payload.map((p) => {
return { floor_tag: p.floor_tag, floor_guid: p.floor_guid };
});
});
},
});
const { reducer, actions } = deviceListSlice;
export const { getAllDevice } = actions;
export default reducer;

View File

@ -0,0 +1,73 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import axios from "axios";
import { energyBaseUrl } from "@CON";
import { getTotalElectricByBaja, getCurPower } from "@UTIL";
const getElecMeter = (ordPath, dispatch) => {
const date = new Date();
// 取得昨日及今日電錶總量
getTotalElectricByBaja(ordPath, date, "daily", dispatch);
// 取得昨日及今日電錶總量比較(長條圖用)
getTotalElectricByBaja(ordPath, date, "hourly", dispatch);
// 取得本週/上週用電量比較(長條圖用)
getTotalElectricByBaja(ordPath, date, "weekly", dispatch);
};
export const fetchInitElecMeterByBaja = createAsyncThunk(
"electricity/fetchInitElecMeterByBaja",
async (token = "", { fulfillWithValue, rejectWithValue, dispatch }) => {
const res = await axios.post(energyBaseUrl);
const { code = 9999, msg, data = null } = res;
if (code !== "0000" || !data) {
return rejectWithValue(msg);
} else {
// 取得總電錶
const total = res.data.find((d) => d.mainSubTag === "total");
// 訂閱 "TPE_B1_EE_E4_R2F_NA_WHT_N1" ==> 總電錶
getElecMeter(`${total.system_device_tag}_KWH`, dispatch); //"TPE_B1_EE_E4_R2F_NA_WHT_N1_KWH"
const ordPath = total?.system_device_tag.replaceAll("_", "/");
getCurPower(ordPath, dispatch); // "TPE/B1/EE/E4/R2F/NA/WHT/N1"
const timer = window.setInterval(async () => {
await getElecMeter(ordPath, dispatch);
}, 3600000);
return fulfillWithValue(timer);
}
},
);
const electricitySlice = createSlice({
name: "electricity",
initialState: {
timer: [],
electricMeter: [{ text: "", data: 0 }],
hourElectricMeter: { current: [], last: [] },
weekElectricMeter: { current: [], last: [] },
curElectricity: [
{ data: "", text: "即時功率 " },
{ data: "", text: "即時契約容量占比 " },
],
},
reducers: {
getElectricMeter: (state, { payload }) => {
state.electricMeter = payload;
},
getHourElectricMeter: (state, { payload }) => {
state.hourElectricMeter = payload;
},
getWeekElectricMeter: (state, { payload }) => {
state.weekElectricMeter = payload;
},
getCurElectricity: (state, { payload }) => {
state.curElectricity = payload;
},
},
extraReducers: (builder) => {
builder.addCase(fetchInitElecMeterByBaja.fulfilled, (state, { payload }) => {
state.timer = [...state.timer, payload];
});
},
});
const { reducer, actions } = electricitySlice;
export const { getElectricMeter, getHourElectricMeter, getWeekElectricMeter, getCurElectricity } =
actions;
export default reducer;

View File

@ -0,0 +1,50 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { forgeInit, modelLoad } from "@UTIL";
export const fetchForge = createAsyncThunk(
"forge/fetchForge",
async ({ viewerEl, urn }, { dispatch, fulfillWithValue, rejectWithValue }) => {
const res = await forgeInit(viewerEl);
if (res.ok) {
modelLoad(res.viewer, urn, dispatch);
return fulfillWithValue(res.viewer);
} else {
return rejectWithValue(res.errCode);
}
},
);
const forgeSlice = createSlice({
name: "forge",
initialState: {
viewer: null,
dataVizExtn: null,
DataVizCore: null,
errCode: 0,
urn_3D: "",
},
reducers: {
loadDataVizExtn: (state, { payload }) => {
state.dataVizExtn = payload;
state.DataVizCore = window.Autodesk.DataVisualization.Core;
},
shutdownForge: (state, action) => {
state.viewer?.finish();
state.viewer = null;
window.Autodesk?.Viewing.shutdown();
},
},
extraReducers: (builder) => {
builder
.addCase(fetchForge.fulfilled, (state, action) => {
state.viewer = action.payload;
})
.addCase(fetchForge.rejected, (state, action) => {
state.errCode = action.payload;
});
},
});
const { reducer, actions } = forgeSlice;
export const { shutdownForge, loadDataVizExtn } = actions;
export default reducer;

View File

@ -0,0 +1,13 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
const generalSlice = createSlice({
name: "general",
initialState: {
timer: [],
},
reducers: {
addTimer: (state, { payload }) => {
state.timer = [...state.timer, payload];
},
},
});

View File

@ -0,0 +1,58 @@
import { configureStore } from "@reduxjs/toolkit";
import buildingInfo, { fetchBuiList, changeBuilding } from "./buildingSlice";
import device, {
fetchSelectedDeviceList,
fetchSelectedDeviceMenu,
fetchSelectedDevFlTags,
} from "./deviceListSlice";
import forgeViewer, { fetchForge, loadDataVizExtn, shutdownForge } from "./forgeSlice";
import system, { fetchSysMainSub } from "./systemSlice";
import user, { fetchUserInfo, fetchUserAuthPages } from "./userSlice";
import alarm, { fetchBuiAlarmStateByBaja } from "./alarmSlice";
import electricity, {
fetchInitElecMeterByBaja,
getCurElectricity,
getElectricMeter,
getHourElectricMeter,
getWeekElectricMeter,
} from "./electricitySlice";
const reducers = {
buildingInfo,
device,
forgeViewer,
system,
user,
alarm,
electricity,
};
// 向外暴露管理對象 store
const store = configureStore({
reducer: reducers,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: false,
}),
devTools: process.env.NODE_ENV !== "production",
});
export default store;
export {
fetchBuiList,
changeBuilding,
fetchSelectedDeviceList,
fetchSelectedDeviceMenu,
fetchSelectedDevFlTags,
fetchForge,
loadDataVizExtn,
shutdownForge,
fetchSysMainSub,
fetchUserInfo,
fetchUserAuthPages,
fetchBuiAlarmStateByBaja,
fetchInitElecMeterByBaja,
getElectricMeter,
getHourElectricMeter,
getWeekElectricMeter,
getCurElectricity,
};

View File

@ -0,0 +1,37 @@
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
import { mainSubBaseUrl } from "@CON";
import axios from "axios";
export const fetchSysMainSub = createAsyncThunk(
"systemList/fetchSysMainSub",
async (building_tag, { fulfillWithValue }) => {
const res = await axios({
method: "post",
url: mainSubBaseUrl,
data: { building_tag },
});
let result = [];
res.data.history_Main_Systems.forEach((mainSys) => {
mainSys.history_Sub_systems.forEach((subSys) => {
result.push({ ...subSys, main_system_tag: mainSys.main_system_tag });
});
});
return fulfillWithValue(result);
},
);
const systemSlice = createSlice({
name: "systemList",
initialState: {
mainSub: [],
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchSysMainSub.fulfilled, (state, { payload }) => {
state.mainSub = payload;
});
},
});
const { reducer, actions } = systemSlice;
export default reducer;

View File

@ -0,0 +1,40 @@
import { createSlice, createAsyncThunk } from "@reduxjs/toolkit";
import axios from "axios";
import { userAuthBaseUrl, userInfoBaseUrl } from "@CON";
import { ajaxRes, deviceBuiListBaseUrl } from "@UTIL";
import { fetchBuiList } from "./buildingSlice";
export const fetchUserInfo = createAsyncThunk("user/fetchUserInfo", async (token, thunkAPI) => {
const res = await axios.post(userInfoBaseUrl);
console.log("user/fetchUserInfo", res);
return ajaxRes(res, thunkAPI);
});
export const fetchUserAuthPages = createAsyncThunk(
"user/fetchUserAuthPages",
async (token, thunkAPI) => {
const res = await axios.post(userAuthBaseUrl);
thunkAPI.dispatch(fetchBuiList());
return ajaxRes(res, thunkAPI);
},
);
const userSlice = createSlice({
name: "user",
initialState: {
userAuthPages: [],
userInfo: {},
},
reducers: {},
extraReducers: (builder) => {
builder.addCase(fetchUserInfo.fulfilled, (state, { payload }) => {
state.userInfo = payload;
});
builder.addCase(fetchUserAuthPages.fulfilled, (state, { payload }) => {
state.userAuthPages = payload;
});
},
});
const { reducer } = userSlice;
export default reducer;

View File

@ -0,0 +1,9 @@
export function ajaxRes(res, thunkAPI) {
const { code = 9999, msg, data = null } = res;
const { fulfillWithValue, rejectWithValue } = thunkAPI;
if (code !== "0000" || !data) {
return rejectWithValue(msg);
} else {
return fulfillWithValue(data);
}
}

View File

@ -0,0 +1,49 @@
// 首頁指定區域報警訊息(Model)
export async function getBuiAlarmStateByBaja(area_tag, building_tag) {
return new Promise((resolve) => {
let result = [];
window.require(["baja!"], function (baja) {
baja.Ord.make(
`local:|foxs:|alarm:|bql:select alarmData, alarmData.sourceName, sourceState, uuid, ackState where alarmData.sourceName like '%${area_tag}_${building_tag}%' order by timestamp desc`,
).get({
cursor: {
before: function () {
console.log("alarm before");
},
each: function () {
const { msgText, sourceName } = this.get("alarmData").$map.$map;
const { $enc: ackState } = this.get("ackState").getType();
const { $enc: sourceState } = this.get("sourceState").getType();
if (ackState !== "acked" && sourceState !== "normal") {
result.push({
msgText,
sourceName,
ackState,
sourceState,
uuid: this.get("uuid").$val,
});
}
},
after: function () {
console.log("alarm after");
resolve(result);
},
limit: -1,
},
});
});
});
}
// 選定期間報警
export function getDeviceAlarmByBaja(
ord,
{ alarmClass, startDate_millisecond, endDate_millisecond, recoverState, ackState },
) {
var _recoverState = recoverState ? "!= null" : "= null";
var _ackState = ackState ? "= 'acked'" : "= 'unacked'";
window.requirejs(["baja!"], (baja) => {
baja.Ord.make(
`local:|foxs:|alarm:|bql:select timestamp, ackState, alarmClass, alarmClassDisplayName, alarmValue, alarmData, alarmData.sourceName, uuid, alarmData.msgText, alarmData.numericValue, alarmData.presentValue, alarmData.status, alarmData.toState, normalTime from openAlarms where alarmClass='${alarmClass}_AlarmClass' and timestamp.millis > '${startDate_millisecond}' and timestamp.millis < '${endDate_millisecond}' and normalTime ${_recoverState} and ackState ${_ackState} order by timestamp asc`,
);
});
}

View File

@ -0,0 +1,26 @@
// 訂閱設備
// 針對數值的點位
export function SubscribeDeviceByBaja(ord) {
window.require &&
window.requirejs(["baja!"], (baja) => {
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
const sub = new baja.subscriber();
sub.attach("changed", (prop, cx) => {
console.log(prop, cx);
});
// ord 為要訂閱的點位
baja.Ord.make(ord)
.get({
subscriber: sub,
// cursor 對返回的數據做處理
cursor: {
before: function () {},
each: function () {},
after: function () {},
},
})
.then((point) => {
console.log(point);
});
});
}

View File

@ -0,0 +1,118 @@
import { n4Sup } from "@CON";
import {
getCurElectricity,
getElectricMeter,
getHourElectricMeter,
getWeekElectricMeter,
} from "@STORE";
import { roundDecimal } from "@UTIL";
// 首頁指定區域總電錶歷史信息
/*
@param {Date} start yyyy-mm-dd
@param {Date} end yyyy-mm-dd
@param {string} range hourly|daily
*/
export async function getTotalElectricByBaja(ordPath, date, range, dispatch) {
const year = date.getFullYear();
const month = date.getMonth() + 1 >= 10 ? date.getMonth() + 1 : `0${date.getMonth() + 1}`;
const curDate = date.getDate();
let startDate;
const endDate = `${year}-${month}-${curDate}`;
const rangePara = range === "weekly" ? "daily" : range;
const weekday = date.getDay();
if (range === "weekly") {
startDate = `${year}-${month}-${curDate - weekday - 7}`;
} else {
startDate = `${year}-${month}-${curDate - 1}`;
}
let result = [];
window.require &&
window.require(["baja!"], function (baja) {
baja.Ord.make(
`local:|foxs:|history:/${n4Sup}/${ordPath}?peroid=timerange;start=${startDate}T00:00:00.000+08:00;end=${endDate}T24:00:00.000+08:00;delta=true|bql:history:HistoryRollup.rollup(history:RollupInterval '${rangePara}')`,
).get({
cursor: {
before: function () {},
each: function () {
result.push({
date: this.get("timestamp").$date,
// time: this.get("timestamp").$time,
timestamp: this.get("timestamp").$cEncStr,
min: this.get("min"),
max: this.get("max"),
sum: this.get("sum"),
avg: this.get("avg"),
});
},
after: function () {
const yesterday = result.filter((r) => r.date.$day === date.getDate() - 1);
const today = result.filter((r) => r.date.$day === date.getDate());
if (range === "daily") {
// 日
dispatch(
getElectricMeter([
{ text: "今日用電量", data: roundDecimal(today[0].sum) },
{ text: "昨日用電量", data: roundDecimal(yesterday[0].sum) },
]),
);
} else if (range === "hourly") {
dispatch(
getHourElectricMeter({
current: today.map((t) => t.sum),
last: yesterday.map((y) => y.sum),
}),
);
} else if (range === "weekly") {
// 周
const weekParam = new Date(
`${year}-${month}-${curDate - weekday}T00:00:00`,
).getTime();
const currentData = result.filter(
(r) => new Date(`${r.timestamp}`).getTime() >= weekParam,
);
const lastData = result.filter(
(r) => new Date(`${r.timestamp}`).getTime() < weekParam,
);
getWeekElectricMeter({
current: currentData.map((c) => c.sum),
last: lastData.map((l) => l.sum),
});
}
},
limit: -1,
},
});
});
}
export async function getCurPower(ordPath, dispatch) {
window.require &&
window.require(["baja!"], function (baja) {
const sub = new baja.Subscriber();
sub.attach("changed", function (prop) {
const value = roundDecimal(this.get("out").getValue());
dispatch(
getCurElectricity([
{ data: value, text: "即時功率 " },
{ data: roundDecimal(value / 4), text: "即時契約容量占比 " },
]),
);
});
baja.Ord.make(`local:|foxs:|station:|slot:/${ordPath}`)
.get()
.then((folder) => {
folder
.getSlots()
.is("control:NumericWritable")
.eachValue((point) => {
if (point.getDisplayName() === "P") {
baja.Ord.make(
`local:|foxs:|station:|slot:/TPE/B1/EE/E4/R2F/NA/WHT/N1/${point.getDisplayName()}`,
).get({
subscriber: sub,
});
}
});
});
});
}

View File

@ -0,0 +1,34 @@
import { getBuiAlarmStateByBaja, getDeviceAlarmByBaja } from "./alarmBaja";
import { SubscribeDeviceByBaja } from "./deviceBaja";
import { getTotalElectricByBaja, getCurPower } from "./electricityBaja";
// 下載 N4 內置requireJS
function loadRequireJS() {
const config = document.createElement("script");
config.type = "text/javascript";
config.src = "/requirejs/config.js";
document.head.appendChild(config);
const script = document.createElement("script");
script.type = "text/javascript";
script.src = "/module/js/com/tridium/js/ext/require/require.min.js?";
document.head.appendChild(script);
}
function setUserAccountByBaja(callBackFunc = null) {
return new Promise((resolve, reject) => {
window.require &&
window.require(["baja!"], function (baja) {
const user_name = baja.getUserName();
user_name ? resolve(user_name) : window.location.replace("http://220.132.206.5:8080/login");
});
});
}
export {
loadRequireJS,
setUserAccountByBaja,
getBuiAlarmStateByBaja,
getDeviceAlarmByBaja,
SubscribeDeviceByBaja,
getTotalElectricByBaja,
getCurPower,
};

View File

@ -0,0 +1,101 @@
import { forgeTokenBaseUrl } from "@CON";
import { loadDataVizExtn } from "@STORE";
// 載入 forgeCss 樣式
function loadForgeCss(src) {
const link = document.createElement("link");
link.rel = "stylesheet";
link.href = src;
link.type = "text/css";
document.head.appendChild(link);
return link;
}
// 載入 forgeScript 樣式,並讓 Autodesk 變成全域
function loadForgeScript(src) {
const script = document.createElement("script");
script.type = "text/javascript";
script.src = src;
script.async = true;
script.defer = true;
document.body.appendChild(script);
return script;
}
function getForgeToken(onTokenReady) {
fetch(forgeTokenBaseUrl)
.then((res) => res.json())
.then((res) => {
// ACCESS_TOKEN;
const { access_token, expires_in } = res.dictionary;
onTokenReady(access_token, expires_in);
});
}
const options = {
env: "AutodeskProduction2",
api: "streamingV2",
getAccessToken: getForgeToken,
};
function initializeForge(container, resolve, reject) {
window.Autodesk?.Viewing.Initializer(options, function () {
// Create Viewer instance
let viewer = new window.Autodesk.Viewing.GuiViewer3D(container);
var startedCode = viewer.start();
if (startedCode > 0) {
console.error("錯誤: 無法載入 WebGL not supported");
reject({ ok: false, errCode: startedCode });
return;
}
resolve({ ok: true, viewer });
});
}
// forge init
export function forgeInit(container) {
return new Promise(function (resolve, reject) {
if (!window.Autodesk) {
loadForgeCss(
"https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.min.css",
);
loadForgeScript(
"https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.min.js",
).onload = () => {
initializeForge(container, resolve, reject);
};
} else {
initializeForge(container, resolve, reject);
}
});
}
export function modelLoad(viewer, urn, dispatch) {
const documentId = `urn:${urn}`;
window.Autodesk.Viewing.Document.load(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
function onDocumentLoadSuccess(viewerDocument) {
var defaultModel = viewerDocument.getRoot().getDefaultGeometry();
viewer.loadDocumentNode(viewerDocument, defaultModel);
viewer.addEventListener(window.Autodesk.Viewing.GEOMETRY_LOADED_EVENT, async function () {
// 取得所有 forge dbid
const allDbIds = getAllDbIds(this);
const dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
dispatch(loadDataVizExtn(dataVizExtn));
});
}
function onDocumentLoadFailure() {
console.error("錯誤: Model 導入失敗");
}
}
// 取得 forge dbid
function getAllDbIds(viewer) {
var instanceTree = viewer.model?.getData().instanceTree;
var allDbIdsStr = Object.keys(instanceTree.nodeAccess.dbIdToIndex);
return allDbIdsStr.map(function (id) {
return parseInt(id);
});
}

View File

@ -0,0 +1,28 @@
import {
loadRequireJS,
setUserAccountByBaja,
getBuiAlarmStateByBaja,
getDeviceAlarmByBaja,
SubscribeDeviceByBaja,
getTotalElectricByBaja,
getCurPower,
} from "./baja/index";
import { forgeInit, modelLoad } from "./forge";
import { ajaxRes } from "./ajaxRes";
import { userLogin } from "./login";
import { roundDecimal } from "./math";
export {
loadRequireJS,
setUserAccountByBaja,
getBuiAlarmStateByBaja,
getDeviceAlarmByBaja,
SubscribeDeviceByBaja,
getTotalElectricByBaja,
getCurPower,
forgeInit,
modelLoad,
ajaxRes,
userLogin,
roundDecimal,
};

View File

@ -0,0 +1,17 @@
import axios from "axios";
import { loginBaseUrl } from "@CON/index";
export const userLogin = async (e, user, callBackFunc = null) => {
e?.preventDefault();
const {
code,
msg,
data: { token, expires },
} = await axios.post(loginBaseUrl, user);
console.log(code, msg);
if (code !== "0000") {
console.log(msg || "系統內部發生錯誤,請聯絡系統管理員");
} else {
document.cookie = `JWT-Authorization=${token}; max-age=${expires}`;
callBackFunc && callBackFunc("/");
}
};

View File

@ -0,0 +1,3 @@
export const roundDecimal = (value) => {
return parseFloat(value).toFixed(2);
};