This commit is contained in:
koko 2025-06-17 17:50:29 +08:00
commit 3a98726d1b
231 changed files with 59626 additions and 0 deletions

3
.env.development Normal file
View File

@ -0,0 +1,3 @@
VITE_API_BASEURL = "https://pccv-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = ""
VITE_FORGE_BASEURL = "http://localhost:5173"

3
.env.production Normal file
View File

@ -0,0 +1,3 @@
VITE_API_BASEURL = "https://pccv-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = ".."
VITE_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"

3
.env.staging Normal file
View File

@ -0,0 +1,3 @@
VITE_API_BASEURL = "http://220.132.206.5:8008"
VITE_FILE_API_BASEURL = "http://220.132.206.5:8085/file"
VITE_FORGE_BASEURL = "http://localhost:5173"

26
.gitignore vendored Normal file
View File

@ -0,0 +1,26 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
/public/forge/*

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# ibms_netzero
Node version: 18

26
index.html Normal file
View File

@ -0,0 +1,26 @@
<!DOCTYPE html>
<!-- @noSnoop -->
<html lang="en" data-theme="dracula">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/logo.png" />
<link
rel="stylesheet"
href="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/style.css"
/>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>台灣百家珍釀造食品股份有限公司</title>
<script src="https://code.jquery.com/jquery-3.7.1.js"></script>
<script src="https://code.jquery.com/ui/1.13.3/jquery-ui.js"></script>
<script type="text/javascript" src="/requirejs/config.js"></script>
<script
type="text/javascript"
src="/module/js/com/tridium/js/ext/require/require.min.js?"
></script>
</head>
<body>
<div id="app"></div>
<script src="https://developer.api.autodesk.com/modelderivative/v2/viewers/7.*/viewer3D.js"></script>
<script type="module" src="/src/main.js"></script>
</body>
</html>

5553
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

45
package.json Normal file
View File

@ -0,0 +1,45 @@
{
"name": "ibms_netzero",
"version": "0.0.0",
"private": true,
"homepage": "/netzero",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
"@fortawesome/fontawesome-svg-core": "^1.2.36",
"@fortawesome/free-brands-svg-icons": "^5.15.4",
"@fortawesome/free-regular-svg-icons": "^5.15.4",
"@fortawesome/free-solid-svg-icons": "^5.15.4",
"@fortawesome/vue-fontawesome": "^3.0.5",
"@vuepic/vue-datepicker": "^8.0.0",
"ant-design-vue": "^4.0.7",
"axios": "^1.6.2",
"date-fns": "^3.3.1",
"dayjs": "^1.11.10",
"echarts": "^5.4.3",
"echarts-liquidfill": "^3.1.0",
"pinia": "^2.1.7",
"requirejs": "^2.3.6",
"tailwind-merge": "^2.2.1",
"vue": "^3.3.4",
"vue-router": "^4.2.5",
"yup": "^1.4.0",
"yup-phone-lite": "^2.0.1"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.4.0",
"autoprefixer": "^10.4.16",
"daisyui": "^4.4.17",
"postcss": "^8.4.31",
"sass": "^1.69.5",
"sass-loader": "^13.3.2",
"tailwindcss": "^3.3.5",
"unplugin-vue-components": "^0.26.0",
"vite": "^4.4.11",
"vite-plugin-svg-icons": "^2.0.1"
}
}

7
postcss.config.js Normal file
View File

@ -0,0 +1,7 @@
module.exports = {
plugins: {
"postcss-import": {},
tailwindcss: {},
autoprefixer: {},
},
};

BIN
public/background.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 KiB

21
public/hotspot.svg Normal file
View File

@ -0,0 +1,21 @@
<svg width="164" height="164" viewBox="0 0 164 164" fill="none" xmlns="http://www.w3.org/2000/svg">
<g>
<circle cx="81.7363" cy="81.8212" r="40" stroke-width="60" style="stroke:grey" />
</g>
<g filter="url(#filter0_d)">
<circle cx="81.7363" cy="81.8212" r="20" stroke="white" stroke-width="60"/>
</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: 938 B

BIN
public/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

80
public/logo.svg Normal file
View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
version="1.1"
id="svg1"
width="1122.5195"
height="793.7005"
viewBox="0 0 1122.5195 793.7005"
sodipodi:docname="zhzdn-3va76.ai"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs1" />
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1">
<inkscape:page
x="0"
y="0"
inkscape:label="1"
id="page1"
width="1122.5195"
height="793.7005"
margin="128.01953 145.33723 212.13867 183.48047"
bleed="0" />
</sodipodi:namedview>
<g
id="g1"
inkscape:groupmode="layer"
inkscape:label="1">
<g
id="g2">
<path
id="path2"
d="m 523.1084,503.4834 c 0,-13.61817 -11.02442,-24.66016 -24.65723,-24.66016 H 304.1626 c -13.62158,0 -24.65186,11.04199 -24.65186,24.66016 v 194.28857 c 0,13.60987 11.03028,24.67481 24.65186,24.67481 h 194.28857 c 13.63281,0 24.65723,-11.06494 24.65723,-24.67481 z"
style="fill:#bf2519;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
<path
id="path3"
d="m 423.55273,690.70215 c -19.25244,6.51709 -27.47314,13.63281 -33.33398,9.74121 2.60205,-0.86377 7.40381,-4.11035 7.40381,-4.11035 0,0 53.20654,-65.56153 85.42236,-78.11231 2.91406,-1.24316 23.48926,-5.16894 40.06348,-7.4834 v 14.08106 c -40.67285,4.01855 -71.62305,36.44238 -99.55567,65.88379 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
<path
id="path4"
d="m 313.17773,556.44824 c 10.61622,-0.21875 12.13623,11.66309 17.99659,17.93848 0.40332,6.06836 2.13037,11.69824 4.92822,17.11035 1.73877,4.54785 7.15039,7.7832 8.23242,12.96484 3.46582,5.64161 5.42334,10.59278 6.47119,17.09864 -8.64697,-8.63575 -13.81689,-19.90821 -22.06103,-29.44141 -5.38867,-0.84082 -3.22412,5.6416 -4.95117,8.25488 0.42626,6.05664 7.38037,10.15528 11.46826,13.83985 4.9624,11.04199 -12.12451,15.12988 -9.74121,26.18359 1.9458,5.16992 8.43994,6.06738 12.11279,10.58106 2.39502,4.54785 -0.42578,9.09668 -0.64453,13.83984 l -4.14502,4.12207 c -3.2124,0 -6.45947,3.04004 -9.70654,0.65625 -16.01612,2.16504 -26.19483,-9.30273 -40.01172,-12.13574 2.13037,-3.87989 6.26367,-7.12696 10.60449,-8.20899 4.53662,-5.19336 13.39111,-5.42382 14.70361,-12.96484 -2.39502,-4.75586 1.27832,-11.90625 -3.24707,-15.59082 -6.97754,-5.48047 -18.42236,-1.8877 -25.67627,-5.76856 v -5.28418 c 4.83594,-3.99609 11.68701,-5.80371 15.07178,-12.52734 2.61377,-4.11133 1.73877,-7.77246 0.65674,-12.11328 -6.13721,-9.06152 -12.53906,-18.13477 -15.72852,-28.34766 v -13.80566 c 1.38184,-1.25488 3.24707,-2.00293 5.33106,-2.00293 11.92871,-0.84082 18.82568,10.17871 28.33593,15.60156 m -25.08886,55.40528 c 5.8374,0.18457 13.20654,6.69043 17.72021,0.8291 0.65625,-5.16992 -0.84082,-9.29199 -1.48535,-13.82813 -7.55322,0.86328 -12.56201,6.48242 -16.23486,12.99903 m 3.88037,-46.52832 c 4.97363,4.5371 6.93115,11.00781 12.35449,14.69238 2.34863,-3.22461 -1.3125,-6.25195 0,-9.71778 -0.1958,-6.94335 -8.00244,-9.53417 -12.97656,-11.46875 -2.61377,2.16504 0.85205,4.09961 0.62207,6.49415 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
<path
id="path5"
d="m 399.43018,604.65723 c -4.07618,-2.16407 -6.26368,-6.05567 -5.81495,-10.6045 0.62207,-2.37109 3.02832,-3.46582 4.97413,-4.12207 13.21826,1.75098 21.84228,15.15332 26.82861,26.1836 2.17578,6.71289 0,14.07031 -3.23633,19.49316 -2.61328,-4.3291 -2.83203,-9.74121 -8.0249,-12.99902 -6.68994,0 -10.60449,-6.27539 -15.56738,-10.60449 1.71582,-1.07129 5.40039,-0.86426 4.96289,-4.09961 -1.5083,-1.07032 -2.60254,-3.05078 -4.12207,-3.24707 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
<path
id="path6"
d="m 432.12988,555.75684 c -7.81738,1.29004 -15.16406,-3.02832 -22.08398,0.88671 5.86084,6.88575 13.19531,14.26661 20.35644,20.33399 2.1543,6.26367 14.69239,11.88281 6.45996,18.82519 -4.30566,1.07129 -7.78417,4.29493 -12.97656,3.23633 2.17676,-14.49707 -12.53857,-24.01855 -18.82519,-35.93554 -1.92285,-4.75586 -9.28028,-7.34668 -8.84278,-12.97657 5.38868,-8.44043 12.97608,1.75 20.33399,0.64453 3.24658,1.10547 8.42871,1.95704 11.44433,-0.64453 -5.62988,-19.91992 -28.09375,-28.53222 -40.89746,-43.28125 5.43457,0.667 11.27198,8.66993 15.6128,3.26953 -0.21875,-2.82128 -3.91455,-2.15332 -4.12207,-4.99707 l 2.36035,-1.48535 -1.51953,-2.61328 c 10.41992,3.02832 18.60644,9.93652 27.71435,16.44141 6.04492,9.72949 20.10352,17.31738 14.69141,30.93847 -0.85156,4.76758 -6.26367,5.64258 -9.70606,7.35743 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
<path
id="path7"
d="m 502.12402,513.58105 c -10.71972,-11.08789 -25.46875,-19.42382 -42.47558,-23.5205 -7.62207,-1.8418 -15.65821,-2.83203 -23.96094,-2.83203 -46.74658,0 -84.6626,30.96972 -84.6626,69.19628 0,0 -9.57959,42.37208 42.65967,92.04395 30.17871,28.71582 29.34961,62.93604 28.53223,71.91699 h -0.61035 c -3.40821,-21.6582 -11.67579,-30.62744 -45.82618,-66.8623 -54.27783,-57.52442 -46.94336,-101.27832 -46.94336,-101.27832 0,-38.80176 35.55567,-70.8086 81.48536,-75.47168 h 23.25878 c 29.22266,2.9707 54.20801,16.99219 68.54297,36.80761 z"
style="fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(1.3333333,0,0,-1.3333333,0,1190.0677)" />
</g>
<g
id="g7" />
<g
id="g8" />
<g
id="g9" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

15
public/setting.html Normal file
View File

@ -0,0 +1,15 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>百家珍</title>
</head>
<body>
<script>
window.addEventListener("load", () => {
window.location.href = "/file/dist/index.html";
});
</script>
</body>
</html>

67
src/App.vue Normal file
View File

@ -0,0 +1,67 @@
<script setup>
import { RouterView } from "vue-router";
import Navbar from "./components/navbar/Navbar.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import { ref, provide, onUnmounted, onMounted } from "vue";
const store = useUserInfoStore();
let isToastOpen = ref({
open: false,
content: "",
status: "info",
to: "body",
});
const forgeLock = ref(true);
const toggleForgeLock = () => {
forgeLock.value = !forgeLock.value;
};
const cancelToastOpen = () => {
isToastOpen.value = {
open: false,
content: "",
};
};
const openToast = (status, content, to = "body", confirm = null) => {
isToastOpen.value = {
open: true,
content,
status,
to,
confirm,
};
};
provide("app_toast", { openToast, cancelToastOpen });
provide("app_toggle", { forgeLock, toggleForgeLock });
</script>
<template>
<Toast
:content="isToastOpen.content"
:open="isToastOpen.open"
:status="isToastOpen.status"
:cancel="cancelToastOpen"
:confirm="isToastOpen.confirm"
:to="isToastOpen.to"
/>
<div v-if="store.user.token" class="min-h-screen">
<Navbar />
<div class="px-6 w-full relative app-container">
<RouterView />
</div>
</div>
<div v-else class="h-screen"><RouterView /></div>
</template>
<style scoped>
header {
line-height: 1.5;
}
.logo {
display: block;
margin: 0 auto 1rem;
}
</style>

14
src/apis/account/api.js Normal file
View File

@ -0,0 +1,14 @@
export const GET_ACCOUNT_USERLIST_API = `/User/UserManagerList`;
export const GET_ACCOUNT_ROLELIST_API = `/User/RoleManagerList`;
export const GET_ACCOUNT_ROLEAUTHLIST_API = `/User/RoleAuthList`;
export const GET_ACCOUNT_ROLEAUTHPAGELIST_API = `/User/AuthPageListByVariable`;
// export const POST_ROLEAUTHLIST_API = `/User/SaveRoleAuth`;
export const POST_ACCOUNT_ROLE_API = `/User/SaveRoleAndAuth`;
export const DELETE_ACCOUNT_ROLE_API = `/User/DeleteOneRole`;
export const GET_ACCOUNT_USER_API = `/User/GetOneUser`;
export const POST_ACCOUNT_USER_API = `/User/SaveUser`;
export const DELETE_ACCOUNT_USER_API = `/User/DeleteOneUser`;

158
src/apis/account/index.js Normal file
View File

@ -0,0 +1,158 @@
import {
GET_ACCOUNT_USERLIST_API,
GET_ACCOUNT_ROLELIST_API,
GET_ACCOUNT_ROLEAUTHLIST_API,
GET_ACCOUNT_ROLEAUTHPAGELIST_API,
DELETE_ACCOUNT_ROLE_API,
POST_ACCOUNT_ROLE_API,
POST_ACCOUNT_USER_API,
GET_ACCOUNT_USER_API,
DELETE_ACCOUNT_USER_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export const getAccountUserList = async (search_condition = {}) => {
const res = await instance.post(GET_ACCOUNT_USERLIST_API, search_condition);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAccountRoleList = async (search_condition = {}) => {
const res = await instance.post(GET_ACCOUNT_ROLELIST_API, {
Layer: 1,
...search_condition,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAccountRoleAuthList = async (SelectedRoleId) => {
const res = await instance.post(GET_ACCOUNT_ROLEAUTHLIST_API, {
SelectedRoleId,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAccountRoleAuthPageList = async () => {
const res = await instance.post(GET_ACCOUNT_ROLEAUTHPAGELIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAccountRole = async ({ Id, Name, SaveCheckAuth }) => {
const res = await instance.post(POST_ACCOUNT_ROLE_API, {
Id,
Name,
SaveCheckAuth,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const delRole = async (Id) => {
const res = await instance.post(DELETE_ACCOUNT_ROLE_API, {
Id,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAccountOneUser = async (Id) => {
const res = await instance.post(GET_ACCOUNT_USER_API, {
Id,
});
res.data = {
Account: res.data?.account,
Name: res.data?.full_name,
Email: res.data?.email,
Phone: res.data?.phone,
RoleId: res.data?.role_guid,
Id,
};
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAccountUser = async ({
Id,
Account,
Name,
Email,
Phone,
RoleId,
Password,
}) => {
const res = await instance.post(
POST_ACCOUNT_USER_API,
Id
? {
Id: Id,
Account,
Name,
Email,
Phone,
RoleId,
}
: {
Id: "0",
Account,
Name,
Email,
Phone,
RoleId,
Password,
}
);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const changePassword = async ({ Id, Password }) => {
const res = await instance.post(POST_ACCOUNT_USER_API, {
Id,
Password,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const delAccount = async (Id) => {
const res = await instance.post(DELETE_ACCOUNT_USER_API, {
Id,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

22
src/apis/alert/api.js Normal file
View File

@ -0,0 +1,22 @@
export const POST_ACK_API = `/obix/alarm`;
export const GET_ALERT_FORMID_API = `/Alert/AlertList`;
export const POST_OPERATION_RECORD_API = `/operation/SavOpeRecord`;
export const GET_ALERT_SUB_LIST_API = `api/Device/GetMainSub`;
export const GET_ALERT_MEMBER_LIST_API = `api/Alarm/GetAlarmMemberList`;
export const GET_ALERT_MEMBER = `api/Alarm/GetAlarmMember`;
export const POST_ALERT_MEMBER = `api/Alarm/SaveAlarmMember`;
export const DELETE_ALERT_MEMBER = `api/Alarm/DeleteAlarmMember`;
export const GET_NOTICE_LIST_API = `api/Alarm/GetNotice`;
export const GET_SHOW_ALERT_API = `api/Alarm/GetShowAlarm`; // 取得告警顯示清單
export const GET_OUTLIERS_LIST_API = `api/Alarm/GetAlarmSetting`;
export const GET_OUTLIERS_DEVLIST_API = `api/Alarm/GetDevList`; // 取得設備
export const GET_OUTLIERS_POINTS_API = `api/Alarm/GetAlarmPoints`; // 取得點位
export const POST_OUTLIERS_SETTING_API = `api/Alarm/SaveAlarmSetting`; // 新增與修改
export const DELETE_OUTLIERS_SETTING_API = `api/Alarm/DeleteAlarmSetting`; // 刪除
export const GET_ALERT_SCHEDULE_LIST_API = `api/Alarm/GetAlarmSchedule`;
export const POST_ALERT_SCHEDULE = `api/Alarm/SaveAlarmSchedule`;
export const DELETE_ALERT_SCHEDULE = `api/Alarm/DeleteAlarmSchedule`;

214
src/apis/alert/index.js Normal file
View File

@ -0,0 +1,214 @@
import {
POST_ACK_API,
GET_ALERT_FORMID_API,
POST_OPERATION_RECORD_API,
GET_ALERT_SUB_LIST_API,
GET_OUTLIERS_LIST_API,
GET_OUTLIERS_DEVLIST_API,
GET_OUTLIERS_POINTS_API,
POST_OUTLIERS_SETTING_API,
DELETE_OUTLIERS_SETTING_API,
GET_ALERT_MEMBER_LIST_API,
GET_ALERT_MEMBER,
POST_ALERT_MEMBER,
DELETE_ALERT_MEMBER,
GET_NOTICE_LIST_API,
GET_SHOW_ALERT_API,
GET_ALERT_SCHEDULE_LIST_API,
POST_ALERT_SCHEDULE,
DELETE_ALERT_SCHEDULE
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import axios from "axios";
export const postChgAck = async (uuid) => {
try {
const data =
'<obj is="obix:AckAlarmIn"><str name="ackUser" val="obix" /></obj>';
const res = await axios.post(`${POST_ACK_API}/${uuid}/ack`, data, {
headers: {
"Content-Type": "text/plain",
},
});
// 解析XML錯誤信息
const parser = new DOMParser();
const xmlDoc = parser.parseFromString(res.data, "text/xml");
const errElement = xmlDoc.querySelector("err");
if (errElement) {
console.error("Error in acknowledging alarm");
return { isSuccess: false, msg: `Error in acknowledging alarm` };
}
return { isSuccess: true };
} catch (error) {
console.error("Error in acknowledging alarm:", error);
return { isSuccess: false, msg: "Error in acknowledging alarm" };
}
};
export const getAlertFormId = async (uuid) => {
const res = await instance.post(GET_ALERT_FORMID_API, uuid);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postOperationRecord = async (formData) => {
const res = await instance.post(POST_OPERATION_RECORD_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAlertSubList = async () => {
const res = await instance.post(GET_ALERT_SUB_LIST_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAlarmMemberList = async () => {
const res = await instance.post(GET_ALERT_MEMBER_LIST_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getNoticeList = async () => {
const res = await instance.post(GET_NOTICE_LIST_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAlertMember = async (data) => {
const res = await instance.post(POST_ALERT_MEMBER, data);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAlarmMember = async (id) => {
try {
const res = await instance.post(DELETE_ALERT_MEMBER, { id });
console.log("Delete Alarm Member Response:", res);
return {
isSuccess: res.code === "0000",
msg: res.msg || "刪除成功",
};
} catch (error) {
console.error("API request failed", error);
return { isSuccess: false, msg: "API request failed" };
}
};
export const getAlarmMember = async (data) => {
try {
const res = await instance.post(GET_ALERT_MEMBER, data);
return res.data;
} catch (error) {
console.error("API request failed", error);
return { isSuccess: false, msg: "API request failed" };
}
};
export const getOutliersList = async (id) => {
const res = await instance.post(GET_OUTLIERS_LIST_API, id);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOutliersDevList = async (id) => {
const res = await instance.post(GET_OUTLIERS_DEVLIST_API, id);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOutliersPoints = async (id) => {
const res = await instance.post(GET_OUTLIERS_POINTS_API, id);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postOutliersSetting = async (data) => {
const res = await instance.post(POST_OUTLIERS_SETTING_API, data);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const delOutliersSetting = async (Id) => {
const res = await instance.post(DELETE_OUTLIERS_SETTING_API, {
Id,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getShowAlarm = async () => {
const res = await instance.post(GET_SHOW_ALERT_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAlarmScheduleList = async () => {
const res = await instance.post(GET_ALERT_SCHEDULE_LIST_API,{});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAlertSchedule = async (data) => {
const res = await instance.post(POST_ALERT_SCHEDULE, data);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAlarmSchedule = async (id) => {
try {
const res = await instance.post(DELETE_ALERT_SCHEDULE, { id });
return {
isSuccess: res.code === "0000",
msg: res.msg || "刪除成功",
};
} catch (error) {
console.error("API request failed", error);
return { isSuccess: false, msg: "API request failed" };
}
};

19
src/apis/asset/api.js Normal file
View File

@ -0,0 +1,19 @@
export const GET_ASSET_SUB_LIST_API = `/AssetManage/GetAssetSubList`;
export const GET_ASSET_MAIN_LIST_API = `/AssetManage/GetAssetMainList`;
export const POST_ASSET_SUB_LIST_API = `/AssetManage/SaveAssetSub`;
export const DELETE_ASSET_SUB_LIST_API = `/AssetManage/DeleteAssetSub`;
export const GET_ASSET_LIST_API = `/AssetManage/GetAssetList`;
export const GET_ASSET_SINGLE_API = `/AssetManage/GetAsset`;
export const POST_ASSET_SINGLE_API = `/AssetManage/SaveAsset`;
export const DELETE_ASSET_ITEM_API = `/AssetManage/DeleteAsset`;
export const GET_ASSET_BUILD_LIST_API = `/AssetManage/GetBuildingList`;
export const POST_ASSET_BUILD_API = `AssetManage/SaveBuilding`;
export const DELETE_ASSET_BUILD_API = `AssetManage/DeleteBuilding`;
export const GET_ASSET_FLOOR_LIST_API = `/AssetManage/GetFloorList`;
export const POST_ASSET_FLOOR_API = `/AssetManage/SaveFloor`;
export const DELETE_ASSET_FLOOR_API = `/AssetManage/DeleteFloor`;
export const GET_ASSET_IOT_LIST_API = `/AssetManage/GetIOTList`;

187
src/apis/asset/index.js Normal file
View File

@ -0,0 +1,187 @@
import {
GET_ASSET_SUB_LIST_API,
GET_ASSET_MAIN_LIST_API,
DELETE_ASSET_SUB_LIST_API,
POST_ASSET_SUB_LIST_API,
GET_ASSET_LIST_API,
GET_ASSET_SINGLE_API,
GET_ASSET_BUILD_LIST_API,
POST_ASSET_BUILD_API,
DELETE_ASSET_BUILD_API,
GET_ASSET_FLOOR_LIST_API,
POST_ASSET_FLOOR_API,
DELETE_ASSET_FLOOR_API,
GET_ASSET_IOT_LIST_API,
DELETE_ASSET_ITEM_API,
POST_ASSET_SINGLE_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import { object } from "yup";
export const getAssetSubList = async () => {
const res = await instance.post(GET_ASSET_SUB_LIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetMainList = async () => {
const res = await instance.post(GET_ASSET_MAIN_LIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAssetSubList = async ({
system_key,
// system_value,
// system_parent_id,
id,
}) => {
const res = await instance.post(POST_ASSET_SUB_LIST_API, {
system_key,
// system_value,
// system_parent_id,
id,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAssetSubItem = async (id) => {
const res = await instance.post(DELETE_ASSET_SUB_LIST_API, { id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetList = async (variable_id) => {
const res = await instance.post(GET_ASSET_LIST_API, {
variable_id: parseInt(variable_id),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetSingle = async (main_id) => {
const res = await instance.post(GET_ASSET_SINGLE_API, { main_id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAssetSingle = async (data) => {
let formData = new FormData();
for (let [key, value] of Object.entries(data)) {
console.log(key, value);
if (Array.isArray(value)) {
if (key === "oriFile") {
value.forEach((element, index) => {
formData.append(`${key}[${index}].file`, element.id ? null : element);
formData.append(`${key}[${index}].orgName`, element.name);
formData.append(
`${key}[${index}].saveName`,
element.id ? element.saveName : ""
);
});
} else {
value.forEach((element, index) => {
formData.append(`${key}[${index}].device_guid`, element);
});
}
} else {
formData.append(key, value);
}
}
const res = await instance.post(POST_ASSET_SINGLE_API, formData);
return apihandler(res.code, res.data, { msg: res.msg, code: res.code });
};
export const deleteAssetItem = async (main_id) => {
const res = await instance.post(DELETE_ASSET_ITEM_API, { main_id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetBuildList = async () => {
const res = await instance.post(GET_ASSET_BUILD_LIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAssetBuild = async (formData) => {
const res = await instance.post(POST_ASSET_BUILD_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAssetBuild = async (building_guid) => {
const res = await instance.post(DELETE_ASSET_BUILD_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetFloorList = async (building_guid) => {
const res = await instance.post(GET_ASSET_FLOOR_LIST_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postAssetFloor = async (formData) => {
const res = await instance.post(POST_ASSET_FLOOR_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteAssetFloor = async (Floor_guid) => {
const res = await instance.post(DELETE_ASSET_FLOOR_API, { Floor_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAssetIOTList = async () => {
const res = await instance.post(GET_ASSET_IOT_LIST_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

4
src/apis/building/api.js Normal file
View File

@ -0,0 +1,4 @@
export const GET_BUILDING_API = `/api/Device/GetBuild`;
export const GET_AUTHPAGE_API = `/api/GetUsrFroList`;
export const GET_SUBAUTHPAGE_API = `/api/Device/GetMainSub`;
export const GET_ALL_DEVICE_API = `/api/Device/GetAllDevice`;

View File

@ -0,0 +1,67 @@
import {
GET_BUILDING_API,
GET_AUTHPAGE_API,
GET_SUBAUTHPAGE_API,
GET_ALL_DEVICE_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export const getBuildings = async () => {
const res = await instance.post(GET_BUILDING_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAuth = async () => {
const res = await instance.post(GET_AUTHPAGE_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAllSysSidebar = async () => {
const res = await instance.post(GET_SUBAUTHPAGE_API, {
building_tag: "",
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getSysSidebar = async (building_tag) => {
const res = await instance.post(GET_SUBAUTHPAGE_API, {
building_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAllDevice = async () => {
const res = await instance.post(GET_ALL_DEVICE_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const ackSingleAlarm = async (uuid) => {
const res = await instance.post(
`/obix/alarm/${uuid}/ack`,
'<obj is="obix:AckAlarmIn"><str name="ackUser" val="obix" /></obj>'
);
console.log("acked", res);
return apihandler(res.code, res, {
msg: res.msg,
code: res.code,
});
};

View File

@ -0,0 +1,9 @@
export const GET_DASHBOARD_INIT_API = `/SituationRoom/Initialize`;
export const GET_DASHBOARD_DEVICE_API = `/SituationRoom/GetDeviceList`;
export const GET_DASHBOARD_PRODUCT_COMPLETE_API = `/SituationRoom/GetProductionStatus`;
export const GET_DASHBOARD_TEMP_API = `/SituationRoom/GetTempratureData`;
export const GET_DASHBOARD_ROOM_TEMP_API = `/SituationRoom/GetFormulaRoomStatusData`;
export const GET_DASHBOARD_ENERGY_API = `/SituationRoom/GetEnergeData`;
export const POST_DASHBOARD_PRODUCT_TARGET_SETTING_API = `/SituationRoom/SetTargetSetting`;
export const GET_DASHBOARD_PRODUCT_TARGET_SETTING_API = `/SituationRoom/GetTargetSetting`
export const GET_DASHBOARD_PRODUCT_HISTORY_API = `/SituationRoom/GetProductionHistory`

137
src/apis/dashboard/index.js Normal file
View File

@ -0,0 +1,137 @@
import {
GET_DASHBOARD_INIT_API,
GET_DASHBOARD_DEVICE_API,
GET_DASHBOARD_PRODUCT_COMPLETE_API,
GET_DASHBOARD_TEMP_API,
GET_DASHBOARD_ROOM_TEMP_API,
GET_DASHBOARD_ENERGY_API,
POST_DASHBOARD_PRODUCT_TARGET_SETTING_API,
GET_DASHBOARD_PRODUCT_TARGET_SETTING_API,
GET_DASHBOARD_PRODUCT_HISTORY_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export const getDashboardInit = async (page_type = "SR") => {
const res = await instance.post(GET_DASHBOARD_INIT_API, {
page_type, // SR:戰情室;PS:生產設定
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardDevice = async ({ option }) => {
const res = await instance.post(GET_DASHBOARD_DEVICE_API, {
option: parseInt(option),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardProductCompletion = async () => {
const res = await instance.post(GET_DASHBOARD_PRODUCT_COMPLETE_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardEnergy = async () => {
const res = await instance.post(GET_DASHBOARD_ENERGY_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardFormulaRoom = async ({ timeInterval, typeOption }) => {
const res = await instance.post(GET_DASHBOARD_ROOM_TEMP_API, {
timeInterval,
typeOption,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardTemp = async ({
timeInterval,
tempOption,
typeOption = "",
}) => {
const res = typeOption
? await instance.post(GET_DASHBOARD_TEMP_API, {
timeInterval,
tempOption,
typeOption,
})
: await instance.post(GET_DASHBOARD_TEMP_API, {
timeInterval,
tempOption,
});
console.log(res);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postDashboardProductTarget = async ({ date, type, data }) => {
let formatData = [];
for (let [key, value] of Object.entries(data)) {
formatData.push({
name: key,
value,
});
}
const res = await instance.post(POST_DASHBOARD_PRODUCT_TARGET_SETTING_API, {
target: {
date,
type,
data: formatData,
},
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardProductTarget = async ({ date, type }) => {
const res = await instance.post(GET_DASHBOARD_PRODUCT_TARGET_SETTING_API, {
target: {
date,
type,
},
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getDashboardProductRecord = async ({ start_time, end_time }) => {
const res = await instance.post(GET_DASHBOARD_PRODUCT_HISTORY_API, {
start_time,
end_time,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

3
src/apis/forge/api.js Normal file
View File

@ -0,0 +1,3 @@
export const GET_FORGETOKEN_API = `/api/forge/oauth/token`;
export const GET_FORGEURN_API = `/api/Device/GetBuild`;

24
src/apis/forge/index.js Normal file
View File

@ -0,0 +1,24 @@
import instance from "@/util/request";
import { GET_FORGETOKEN_API, GET_FORGEURN_API } from "./api";
import apihandler from "@/util/apihandler";
export const getUrn = async () => {
const res = await instance.post(GET_FORGEURN_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getAccessToken = async (callback) => {
try {
const resp = await instance.get(GET_FORGETOKEN_API);
console.log(resp)
const { dictionary } = resp;
callback(dictionary.access_token, dictionary.expires_in);
} catch (err) {
alert("Could not obtain access token. See the console for more details.");
console.error(err);
}
};

16
src/apis/graph/api.js Normal file
View File

@ -0,0 +1,16 @@
// graph
const BASEURL = import.meta.env.VITE_API_BASEURL;
export const GET_GRAPH_SIDEBAR_API = `/GraphManage/GraphManageTreeList`;
export const UPDATE_GRAPH_SIDEBAR_API = `/GraphManage/EditGraphManageTree`;
export const REMOVE_GRAPH_SIDEBAR_API = `/GraphManage/DelGraphManageTree`;
export const POST_GRAPH_SIDEBAR_API = `/GraphManage/SaveGraphManageTree`;
export const GET_GRAPH_PARAM_OPTION_API = `/GraphManage/GraManSpecList`;
export const GET_GRAPH_TABLE_API = `/GraphManage/GraManList`;
export const POST_GRAPH_TABLE_API = `/GraphManage/SaveGraMan`;
export const POST_GRAPH_TABLE_API_2 = `/GraphManage/SaveGraMan`; // 原先沒有小類及規格的
export const UPDATE_GRAPH_TABLE_API = `/GraphManage/EdtOneGraMan`;
export const DELETE_GRAPH_TABLE_API = `/GraphManage/DelOneGraMan`;

122
src/apis/graph/index.js Normal file
View File

@ -0,0 +1,122 @@
import {
GET_GRAPH_SIDEBAR_API,
UPDATE_GRAPH_SIDEBAR_API,
REMOVE_GRAPH_SIDEBAR_API,
POST_GRAPH_SIDEBAR_API,
GET_GRAPH_TABLE_API,
GET_GRAPH_PARAM_OPTION_API,
POST_GRAPH_TABLE_API,
DELETE_GRAPH_TABLE_API,
POST_GRAPH_TABLE_API_2,
UPDATE_GRAPH_TABLE_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apiHandler";
export const getSideBar = async () => {
const res = await instance.post(GET_GRAPH_SIDEBAR_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const addSideBarTreeName = async ({ parent_id, name }) => {
const res = await instance.post(POST_GRAPH_SIDEBAR_API, { parent_id, name });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const updateSideBarTreeName = async ({ id, name }) => {
const res = await instance.post(UPDATE_GRAPH_SIDEBAR_API, { id, name });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const removeSideBarTreeName = async (id) => {
const res = await instance.post(REMOVE_GRAPH_SIDEBAR_API, { id });
return apihandler(
res.code,
{ isSuccess: true },
{
msg: res.msg,
code: res.code,
isSuccess: false,
}
);
};
// 中間 table
export const getGraphData = async (id) => {
const res = await instance.post(GET_GRAPH_TABLE_API, { layer_id: id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const getGraphAddParamOption = async () => {
const res = await instance.post(GET_GRAPH_PARAM_OPTION_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const addGraphTableData = async (formData) => {
const res = await instance.post(POST_GRAPH_TABLE_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const delGraphData = async (id) => {
const res = await instance.post(DELETE_GRAPH_TABLE_API, { id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const addGraphTableDataWithoutSubSys = async (formData) => {
const res = await instance.post(POST_GRAPH_TABLE_API_2, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const editGraphTableDataWithoutSubSys = async (formData) => {
const res = await instance.post(UPDATE_GRAPH_TABLE_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};

11
src/apis/history/api.js Normal file
View File

@ -0,0 +1,11 @@
// history
const BASEURL = import.meta.env.VITE_API_BASEURL;
export const GET_HISTORY_SIDEBAR_API = `/api/History/GetDeviceInfo`;
export const GET_HISTORY_POINT_API = `/api/History/GetAllDevPoi`;
export const GET_HISTORY_DATA_API = `/api/History/GetHistoryData`;
export const GET_HISTORY_EXPORT_API = `/api/ExportHistoryExcel`;
export const GET_HISTORY_FAVORITE_API = `/api/History/GetHistoryFavorite`;
export const POST_HISTORY_FAVORITE_API = `/api/History/SaveHistoryFavorite`;
export const DELETE_HISTORY_FAVORITE_API = `/api/History/DeleteHistoryFavorite`;
export const UPDATE_HISTORY_FAVORITE_API = `/api/History/EditHistoryFavorite`;

173
src/apis/history/index.js Normal file
View File

@ -0,0 +1,173 @@
import {
GET_HISTORY_SIDEBAR_API,
GET_HISTORY_POINT_API,
GET_HISTORY_DATA_API,
GET_HISTORY_FAVORITE_API,
POST_HISTORY_FAVORITE_API,
DELETE_HISTORY_FAVORITE_API,
UPDATE_HISTORY_FAVORITE_API,
GET_HISTORY_EXPORT_API,
} from "./api";
import instance, { fileInstance } from "@/util/request";
import apihandler from "@/util/apiHandler";
import downloadExcel from "@/util/downloadExcel";
export const getHistorySideBar = async (sub_system_tag) => {
const res = await instance.post(GET_HISTORY_SIDEBAR_API, {
sub_system_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getHistoryPoints = async ({ Device_list, Cumulant }) => {
const res = await instance.post(GET_HISTORY_POINT_API, {
Device_list,
value_type: parseInt(Cumulant),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getHistoryData = async ({
Type,
Cumulant,
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
}) => {
/*
{
Type,
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
}
*/
const res = await instance.post(GET_HISTORY_DATA_API, {
Start_date,
End_date,
Start_time,
End_time,
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
Points: Array.isArray(Points) ? Points : [Points],
Type: parseInt(Type),
value_type: parseInt(Cumulant),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getHistoryExportData = async ({
Type,
Cumulant,
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
}) => {
/*
{
Type,
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
}
*/
const res = await fileInstance.post(
GET_HISTORY_EXPORT_API,
{
// ...exportContent,
Start_date: Start_date,
End_date: End_date,
Start_time: Start_time,
End_time: End_time,
Points: Array.isArray(Points) ? Points : [Points],
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
Type: parseInt(Type),
value_type: parseInt(Cumulant),
Building_tag_list: [...new Set(Device_list.map((d) => d.split("_")[1]))],
},
{ responseType: "blob" }
);
return apihandler(
res.code,
res,
{
msg: res.msg,
code: res.code,
},
downloadExcel
);
};
export const getHistoryFavorite = async () => {
const res = await instance.post(GET_HISTORY_FAVORITE_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const addHistoryFavorite = async (value) => {
const res = await instance.post(POST_HISTORY_FAVORITE_API, {
device_name_tag: value.sub_system_tag,
Device_list: Array.isArray(value.Device_list)
? value.Device_list
: [value.Device_list],
Points: Array.isArray(value.Points) ? value.Points : [value.Points],
favorite_name: value.favorite_name,
Type: parseInt(value.Type),
value_type: parseInt(value.Cumulant),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteHistoryFavorite = async (favorite_guid) => {
const res = await instance.post(DELETE_HISTORY_FAVORITE_API, {
favorite_guid,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const editHistoryFavorite = async ({ favorite_guid, favorite_name }) => {
const res = await instance.post(UPDATE_HISTORY_FAVORITE_API, {
favorite_guid,
favorite_name,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

1
src/apis/login/api.js Normal file
View File

@ -0,0 +1 @@
export const POST_LOGIN = `/api/LoginV2/`;

23
src/apis/login/index.js Normal file
View File

@ -0,0 +1,23 @@
import { POST_LOGIN } from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export async function Login({ account, password }) {
const res = await instance.post(POST_LOGIN, {
account,
password,
});
if (res.code === "0000") {
console.log(res.data);
document.cookie = `JWT-Authorization=${res.data.token}; Max-Age=${
24 * 60 * 60 * 1000
}`;
}
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
}

16
src/apis/operation/api.js Normal file
View File

@ -0,0 +1,16 @@
export const GET_OPERATION_RECORD_API = `/operation/OpeRecList`;
export const GET_OPERATION_EXPORT_API = `/operation/OpeExportExcel`;
export const GET_OPERATION_DEVICELIST_API = `/operation/DevList`;
export const GET_OPERATION_COMPANYLIST_API = `/operation/OpeFirSel`;
export const GET_SINGLE_OPERATION_RECORD_API = `/operation/OpeRecRead`;
export const GET_OPERATION_FORMID_API = `/operation/GetFormId`; // 新增單號前置
export const POST_OPERATION_RECORD_API = `/operation/SavOpeRecord`;
export const DELETE_OPERATION_RECORD_API = `/operation/DelOpeRecord`;
// 廠商
export const GET_OPERATION_COMPANY_API = `/operation/OpeFirList`; // 廠商列表
export const GET_OPERATION_SINGLE_CONPANY_API = `/operation/OpeFirRead`; // 單一廠商
export const POST_OPERATION_COMPANY_API = `/operation/SaveOpeFirm`;
export const UPDATE_OPERATION_COMPANY_API = `/operation/EdtOneOpeFirm`;
export const DELETE_OPERATION_COMPANY_API = `/operation/DelOpeFirm`;

190
src/apis/operation/index.js Normal file
View File

@ -0,0 +1,190 @@
import {
GET_OPERATION_RECORD_API,
GET_OPERATION_COMPANY_API,
GET_SINGLE_OPERATION_RECORD_API,
GET_OPERATION_DEVICELIST_API,
POST_OPERATION_RECORD_API,
GET_OPERATION_EXPORT_API,
GET_OPERATION_FORMID_API,
DELETE_OPERATION_RECORD_API,
POST_OPERATION_COMPANY_API,
UPDATE_OPERATION_COMPANY_API,
DELETE_OPERATION_COMPANY_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import dayjs from "dayjs";
export const getOperationRecord = async ({
work_type,
start_created_at,
end_created_at,
serial_number,
sub_system_tag,
}) => {
const res = await instance.post(GET_OPERATION_RECORD_API, {
work_type: parseInt(work_type),
// start_created_at: dayjs(start_created_at).format("YYYY-MM-DDTHH:mm:ss"),
// end_created_at: dayjs(end_created_at)
// .date(dayjs(end_created_at).get("date") + 1)
// .format("YYYY-MM-DDTHH:mm:ss"),
serial_number: serial_number || null,
main_system_tag: null,
sub_system_tag:
typeof sub_system_tag === "string" ? [sub_system_tag] : sub_system_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOperationExportRecord = async ({
work_type,
start_created_at,
end_created_at,
}) => {
const res = await instance.post(GET_OPERATION_EXPORT_API, {
work_type: parseInt(work_type),
startdate: dayjs(start_created_at).format("YYYY-MM-DDTHH:mm:ss"),
enddate: dayjs(end_created_at)
.date(dayjs(end_created_at).get("date") + 1)
.format("YYYY-MM-DDTHH:mm:ss"),
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOperationCompanyList = async () => {
const res = await instance.post(GET_OPERATION_COMPANY_API, {
sub_system_tag: [],
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOperationDeviceList = async ({
list_sub_system_tag,
device_building_tag,
device_area_tag,
}) => {
const res = await instance.post(GET_OPERATION_DEVICELIST_API, {
list_sub_system_tag:
typeof list_sub_system_tag === "string"
? [list_sub_system_tag]
: list_sub_system_tag,
device_building_tag,
device_area_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOperationEditRecord = async (formId) => {
const res = await instance.post(GET_SINGLE_OPERATION_RECORD_API, {
formId,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postOperationRecord = async (formData) => {
const res = await instance.post(POST_OPERATION_RECORD_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getOperationFormId = async () => {
const res = await instance.post(GET_OPERATION_FORMID_API, {});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteOperationRecord = async (id) => {
const res = await instance.post(DELETE_OPERATION_RECORD_API, { id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
// 公司
export const postOperationCompany = async ({
name,
contact_person,
phone,
email,
city,
address,
tax_id_number,
remark,
}) => {
const res = await instance.post(POST_OPERATION_COMPANY_API, {
name,
contact_person,
phone,
email,
city,
address,
tax_id_number,
remark,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const updateOperationCompany = async ({
name,
contact_person,
phone,
email,
city,
address,
tax_id_number,
remark,
id,
}) => {
const res = await instance.post(UPDATE_OPERATION_COMPANY_API, {
id,
name,
contact_person,
phone,
email,
city,
address,
tax_id_number,
remark,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const deleteOperationCompany = async (id) => {
const res = await instance.post(DELETE_OPERATION_COMPANY_API, { id });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -0,0 +1,4 @@
export const POST_SETTING_POINT_API = `/SituationRoom/SetPointSetting`;
export const GET_SETTING_TYPE_API = `/SituationRoom/GetProducts`;
export const POST_SETTING_TYPE_API = `/SituationRoom/SetProduct`;

View File

@ -0,0 +1,45 @@
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
import {
POST_SETTING_POINT_API,
GET_SETTING_TYPE_API,
POST_SETTING_TYPE_API,
} from "./api";
export const postProductSettingPoint = async (type, devices) => {
const res = await instance.post(POST_SETTING_POINT_API, {
devices: devices.map(({ device_number }) => device_number),
values: [
{
point: "Type",
value: type.value,
},
],
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const getProductSettingType = async () => {
const res = await instance.post(GET_SETTING_TYPE_API);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};
export const postProductSettingType = async (data) => {
const res = await instance.post(POST_SETTING_TYPE_API, data);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
isSuccess: false,
});
};

89
src/assets/base.css Normal file
View File

@ -0,0 +1,89 @@
/* color palette from <https://github.com/vuejs/theme> */
:root {
--primary: #6fdda8;
--vt-c-white: #ffffff;
--vt-c-white-soft: #f8f8f8;
--vt-c-white-mute: #f2f2f2;
--vt-c-black: #181818;
--vt-c-black-soft: #222222;
--vt-c-black-mute: #282828;
--vt-c-indigo: #2c3e50;
--vt-c-divider-light-1: rgba(60, 60, 60, 0.29);
--vt-c-divider-light-2: rgba(60, 60, 60, 0.12);
--vt-c-divider-dark-1: rgba(84, 84, 84, 0.65);
--vt-c-divider-dark-2: rgba(84, 84, 84, 0.48);
--vt-c-text-light-1: var(--vt-c-indigo);
--vt-c-text-light-2: rgba(60, 60, 60, 0.66);
--vt-c-text-dark-1: var(--vt-c-white);
--vt-c-text-dark-2: rgba(235, 235, 235, 0.64);
}
/* semantic color variables for this project */
:root {
--color-background: var(--vt-c-white);
--color-background-soft: var(--vt-c-white-soft);
--color-background-mute: var(--vt-c-white-mute);
--color-border: var(--vt-c-divider-light-2);
--color-border-hover: var(--vt-c-divider-light-1);
--color-heading: var(--vt-c-text-light-1);
--color-text: var(--vt-c-text-dark-1);
--section-gap: 160px;
}
@media (prefers-color-scheme: dark) {
:root {
--color-background: var(--vt-c-black);
--color-background-soft: var(--vt-c-black-soft);
--color-background-mute: var(--vt-c-black-mute);
--color-border: var(--vt-c-divider-dark-2);
--color-border-hover: var(--vt-c-divider-dark-1);
--color-heading: var(--vt-c-text-dark-1);
--color-text: var(--vt-c-text-dark-2);
}
}
*,
*::before,
*::after {
box-sizing: border-box;
margin: 0;
font-weight: normal;
}
body {
min-height: 100vh;
color: var(--color-text);
background: var(--color-background);
transition:
color 0.5s,
background-color 0.5s;
line-height: 1.6;
font-family:
Inter,
-apple-system,
BlinkMacSystemFont,
'Segoe UI',
Roboto,
Oxygen,
Ubuntu,
Cantarell,
'Fira Sans',
'Droid Sans',
'Helvetica Neue',
sans-serif;
font-size: 15px;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}

56
src/assets/btn.css Normal file
View File

@ -0,0 +1,56 @@
/**區域框**/
.area-box {
/* width: 100%; */
border-radius: 10px;
display: flex;
align-items: center;
flex-wrap: wrap;
color: #fff;
}
.area-box .item {
display: flex;
align-items: center;
flex-wrap: wrap;
color: #fff;
margin: 0;
}
.area-box .item button:last-child::after {
display: none;
}
.area-box .item button::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
right: -15px;
margin: auto;
display: block;
width: 15px;
height: 1px;
background-color: #a1ffd6;
z-index: -1;
}
.area-box .item button {
position: relative;
z-index: 1;
border-radius: 5px;
margin: 0 7px;
background-color: #021422;
padding: 0.5rem 0;
min-width: 65px;
color: #fff;
border: 1px solid #a1ffd6 !important;
text-align: center;
margin-bottom: 15px;
padding: 0 5px;
}
.area-box .item button.active {
background-color: #6fdda8;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.9);
box-shadow: 0px 0px 5px rgba(255, 255, 255, 0.8);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24.4 19.5" style="enable-background:new 0 0 24.4 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;}
.st1{fill:#83FF97;}
</style>
<g class="st0">
<g>
<path class="st1" d="M17.5,19.5H0l6.8-9.8L0,0h17.5l6.8,9.8l-0.1,0.1L17.5,19.5z M1,19h16.3l6.5-9.3l-6.5-9.3H1l6.5,9.3L1,19z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24.4 19.5" style="enable-background:new 0 0 24.4 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;fill:#83FF97;}
</style>
<polygon class="st0" points="17.5,0 0,0 6.8,9.8 0,19.5 17.5,19.5 24.3,9.9 24.4,9.8 "/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 9.7 11.1" style="enable-background:new 0 0 9.7 11.1;" xml:space="preserve">
<style type="text/css">
.st0{fill:#3BBBC9;}
.st1{fill:#3B69B1;}
</style>
<g>
<g>
<rect y="0" class="st0" width="1" height="0.8"/>
<rect y="2.1" class="st0" width="1" height="0.8"/>
<rect y="4.1" class="st0" width="1" height="0.8"/>
<rect y="6.2" class="st0" width="1" height="0.8"/>
<rect y="8.2" class="st0" width="1" height="0.8"/>
<rect y="10.3" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="1.7" y="0" class="st1" width="1" height="0.8"/>
<rect x="1.7" y="2.1" class="st1" width="1" height="0.8"/>
<rect x="1.7" y="4.1" class="st1" width="1" height="0.8"/>
<rect x="1.7" y="6.2" class="st1" width="1" height="0.8"/>
<rect x="1.7" y="8.2" class="st1" width="1" height="0.8"/>
</g>
<g>
<rect x="3.6" class="st0" width="1" height="0.8"/>
<rect x="3.6" y="2.1" class="st0" width="1" height="0.8"/>
<rect x="3.6" y="4.1" class="st0" width="1" height="0.8"/>
<rect x="3.6" y="6.2" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="5.4" class="st1" width="1" height="0.8"/>
<rect x="5.4" y="2.1" class="st1" width="1" height="0.8"/>
<rect x="5.4" y="4.1" class="st1" width="1" height="0.8"/>
</g>
<g>
<rect x="7.1" y="0" class="st0" width="1" height="0.8"/>
<rect x="7.1" y="2.1" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="8.7" class="st1" width="1" height="0.8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 12 12" style="enable-background:new 0 0 12 12;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000139282756432431693520000014554874042273548179_);fill:#041725;}
.st1{clip-path:url(#SVGID_00000139282756432431693520000014554874042273548179_);}
.st2{clip-path:url(#SVGID_00000180351338875884756140000007490444363737273490_);}
.st3{opacity:0.8;fill:#35EDED;enable-background:new ;}
.st4{fill:#35EDED;}
</style>
<g>
<defs>
<polygon id="SVGID_1_" points="0,6 6.1,12 12,12 12,0 0,0 "/>
</defs>
<clipPath id="SVGID_00000108283119261920149820000010393207167217912208_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<rect style="clip-path:url(#SVGID_00000108283119261920149820000010393207167217912208_);fill:#041725;" width="12" height="12"/>
<g style="clip-path:url(#SVGID_00000108283119261920149820000010393207167217912208_);">
<g>
<defs>
<rect id="SVGID_00000118377240633176785250000005727722924531941272_" y="1.9" width="11" height="10.1"/>
</defs>
<clipPath id="SVGID_00000117676141971077797980000001536752723917697420_">
<use xlink:href="#SVGID_00000118377240633176785250000005727722924531941272_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000117676141971077797980000001536752723917697420_);">
<path class="st3" d="M16.5,256.3c-1.2-0.9-2.4-0.2-2.6,0.8H8v26.3h-792.4l-5.7-5.7v-4.5l0,0v-1.3h-4.1v1.3h3.4v4.7l6.1,6.1H-0.5
l1.7,2.8h10.1v-8.4l-2.7-1.8v-18.9h5.3c0.2,0.7,0.8,1.2,1.5,1.2C16.6,259,17.5,257.6,16.5,256.3z M15.4,258.4
c-0.5,0-0.9-0.4-0.9-0.9s0.4-0.9,0.9-0.9s0.9,0.4,0.9,0.9S15.9,258.4,15.4,258.4z"/>
<polyline class="st3" points="0.8,4.7 8,11.8 8,250.1 8.6,250.1 8.6,11.5 1.1,4 "/>
<polyline class="st3" points="1.1,4 -790.8,4 -790.8,261.5 -794.2,261.5 -794.2,262.7 -790.1,262.7 -790.1,261.5 -790.1,261.5
-790.1,4.7 0.8,4.7 "/>
<rect x="-794.2" y="269.8" class="st4" width="4.1" height="1.2"/>
<rect x="-794.2" y="267.7" class="st4" width="4.1" height="1.2"/>
<rect x="-794.2" y="265.6" class="st4" width="4.1" height="1.2"/>
<rect x="-794.2" y="263.6" class="st4" width="4.1" height="1.2"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 9.8 23.2" style="enable-background:new 0 0 9.8 23.2;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000162336351291435253000000011369610487883135147_);fill:#041725;}
.st1{clip-path:url(#SVGID_00000162336351291435253000000011369610487883135147_);}
.st2{clip-path:url(#SVGID_00000179646246557638455150000007031289474206077343_);}
.st3{opacity:0.8;fill:#35EDED;enable-background:new ;}
.st4{fill:#35EDED;}
</style>
<g>
<defs>
<polygon id="SVGID_1_" points="6,-0.1 6,14.8 9.8,19.3 9.9,23.2 0,23.2 0,-0.1 "/>
</defs>
<clipPath id="SVGID_00000069365692663666114570000004382809312686603691_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<rect x="-0.3" y="-0.1" style="clip-path:url(#SVGID_00000069365692663666114570000004382809312686603691_);fill:#041725;" width="10.2" height="23.3"/>
<g style="clip-path:url(#SVGID_00000069365692663666114570000004382809312686603691_);">
<g>
<defs>
<rect id="SVGID_00000069391242059943023700000011095916327524102832_" x="-0.3" width="10.2" height="23.2"/>
</defs>
<clipPath id="SVGID_00000076592079320817103330000010913419046470824587_">
<use xlink:href="#SVGID_00000069391242059943023700000011095916327524102832_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000076592079320817103330000010913419046470824587_);">
<path class="st3" d="M810.7-5.2c-1.2-0.9-2.4-0.2-2.6,0.8h-5.9v26.3H9.8l-5.7-5.7v-4.5l0,0v-1.3H0v1.3h3.4v4.7l6.1,6.1h784.2
l1.7,2.8h10.1v-8.4l-2.7-1.8V-3.8h5.3c0.2,0.7,0.8,1.2,1.5,1.2C810.8-2.5,811.7-3.9,810.7-5.2z M809.6-3.2
c-0.5,0-0.9-0.4-0.9-0.9s0.4-0.9,0.9-0.9s0.9,0.4,0.9,0.9S810.1-3.2,809.6-3.2z"/>
<polygon class="st3" points="4.1,-0.1 4.1,-0.1 4.1,-256.9 795,-256.9 802.2,-249.8 802.2,-11.5 802.8,-11.5 802.8,-250
795.3,-257.6 3.4,-257.6 3.4,-0.1 0,-0.1 0,1.2 4.1,1.2 "/>
<rect y="8.3" class="st4" width="4.1" height="1.2"/>
<rect y="6.2" class="st4" width="4.1" height="1.2"/>
<rect y="4.1" class="st4" width="4.1" height="1.2"/>
<rect y="2" class="st4" width="4.1" height="1.2"/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 17.7 36.8" style="enable-background:new 0 0 17.7 36.8;" xml:space="preserve">
<style type="text/css">
.st0{clip-path:url(#SVGID_00000014592019429068395720000011308713662110083751_);fill:#041725;}
.st1{clip-path:url(#SVGID_00000014592019429068395720000011308713662110083751_);}
.st2{clip-path:url(#SVGID_00000139268676221631833710000004672253205110111671_);}
.st3{opacity:0.8;fill:#35EDED;enable-background:new ;}
</style>
<g>
<defs>
<polygon id="SVGID_1_" points="0,29.7 4.9,29.7 4.9,0.1 17.7,0.1 17.7,36.9 0,36.9 "/>
</defs>
<clipPath id="SVGID_00000146475346453478477000000018158311794988225670_">
<use xlink:href="#SVGID_1_" style="overflow:visible;"/>
</clipPath>
<rect y="0.1" style="clip-path:url(#SVGID_00000146475346453478477000000018158311794988225670_);fill:#041725;" width="17.7" height="36.8"/>
<g style="clip-path:url(#SVGID_00000146475346453478477000000018158311794988225670_);">
<g>
<defs>
<rect id="SVGID_00000181088581726856941440000011579023093141653907_" y="0.1" width="17.7" height="36.8"/>
</defs>
<clipPath id="SVGID_00000064336413382373671560000002740350498911710124_">
<use xlink:href="#SVGID_00000181088581726856941440000011579023093141653907_" style="overflow:visible;"/>
</clipPath>
<g style="clip-path:url(#SVGID_00000064336413382373671560000002740350498911710124_);">
<path class="st3" d="M17.3,6.3c-1.2-0.9-2.4-0.2-2.6,0.8h-6v26.3h-792.4l-5.7-5.7v-4.5l0,0v-1.3h-4.1v1.3h3.4V28l6.1,6.1H0.3
L2,36.8h10.1v-8.4l-2.7-1.8V7.8h5.3C14.8,8.5,15.4,9,16.2,9C17.4,9,18.3,7.6,17.3,6.3z M16.2,8.4c-0.5,0-0.9-0.4-0.9-0.9
s0.4-0.9,0.9-0.9s0.9,0.4,0.9,0.9S16.7,8.4,16.2,8.4z"/>
<polygon class="st3" points="-789.3,11.5 -789.3,11.5 -789.3,-245.3 1.6,-245.3 8.8,-238.2 8.8,0.1 9.4,0.1 9.4,-238.4
1.9,-246 -790,-246 -790,11.5 -793.4,11.5 -793.4,12.7 -789.3,12.7 "/>
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 9.7 11.1" style="enable-background:new 0 0 9.7 11.1;" xml:space="preserve">
<style type="text/css">
.st0{fill:#3BBBC9;}
.st1{fill:#3B69B1;}
</style>
<g>
<g>
<rect x="8.7" y="10.3" class="st0" width="1" height="0.8"/>
<rect x="8.7" y="8.2" class="st0" width="1" height="0.8"/>
<rect x="8.7" y="6.2" class="st0" width="1" height="0.8"/>
<rect x="8.7" y="4.1" class="st0" width="1" height="0.8"/>
<rect x="8.7" y="2.1" class="st0" width="1" height="0.8"/>
<rect x="8.7" y="0" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="7" y="10.3" class="st1" width="1" height="0.8"/>
<rect x="7" y="8.2" class="st1" width="1" height="0.8"/>
<rect x="7" y="6.2" class="st1" width="1" height="0.8"/>
<rect x="7" y="4.1" class="st1" width="1" height="0.8"/>
<rect x="7" y="2.1" class="st1" width="1" height="0.8"/>
</g>
<g>
<rect x="5.1" y="10.3" class="st0" width="1" height="0.8"/>
<rect x="5.1" y="8.2" class="st0" width="1" height="0.8"/>
<rect x="5.1" y="6.2" class="st0" width="1" height="0.8"/>
<rect x="5.1" y="4.1" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="3.3" y="10.3" class="st1" width="1" height="0.8"/>
<rect x="3.3" y="8.2" class="st1" width="1" height="0.8"/>
<rect x="3.3" y="6.2" class="st1" width="1" height="0.8"/>
</g>
<g>
<rect x="1.6" y="10.3" class="st0" width="1" height="0.8"/>
<rect x="1.6" y="8.2" class="st0" width="1" height="0.8"/>
</g>
<g>
<rect x="0" y="10.3" class="st1" width="1" height="0.8"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 36.7 19.5" style="enable-background:new 0 0 36.7 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;}
.st1{fill:#FFFFFF;}
.st2{fill:#83FF97;}
</style>
<g class="st0">
<g>
<polygon class="st1" points="0.5,19.3 29.7,19.3 36.4,9.8 29.7,0.2 0.5,0.2 7.1,9.8 "/>
</g>
<g>
<path class="st2" d="M29.8,19.5H0l6.8-9.8L0,0h29.8l6.8,9.8L29.8,19.5z M1,19h28.6l6.5-9.3l-6.5-9.3H1l6.5,9.3L1,19z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 708 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 36.7 19.5" style="enable-background:new 0 0 36.7 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;}
.st1{fill:#83FF97;}
</style>
<g class="st0">
<g>
<path class="st1" d="M29.8,19.5H0l6.8-9.8L0,0h29.8l6.8,9.8l-0.1,0.1L29.8,19.5z M1,19h28.6l6.5-9.3l-6.5-9.3H1l6.5,9.3L1,19z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24.4 19.5" style="enable-background:new 0 0 24.4 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;}
.st1{fill:#83FF97;}
</style>
<g class="st0">
<g>
<path class="st1" d="M17.5,19.5H0l6.8-9.8L0,0h17.5l6.8,9.8l-0.1,0.1L17.5,19.5z M1,19h16.3l6.5-9.3l-6.5-9.3H1l6.5,9.3L1,19z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 595 B

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="圖層_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24.4 19.5" style="enable-background:new 0 0 24.4 19.5;" xml:space="preserve">
<style type="text/css">
.st0{opacity:0.6;fill:#83FF97;}
</style>
<polygon class="st0" points="17.5,0 0,0 6.8,9.8 0,19.5 17.5,19.5 24.3,9.9 24.4,9.8 "/>
</svg>

After

Width:  |  Height:  |  Size: 514 B

68
src/assets/index.css Normal file
View File

@ -0,0 +1,68 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer components {
.arrow {
@apply relative triangle flex justify-center items-center text-lg;
box-shadow: inset 0px 6px 10px -10px rgba(255, 255, 255, 0.8),
inset 0px -6px 10px -10px rgba(255, 255, 255, 0.8);
}
.triangle {
@apply after:block after:absolute after:bottom-0 after:left-[100%]
after:border-t-[1rem] after:border-b-[1rem] after:border-l-[2rem]
after:border-t-transparent after:border-b-transparent after:z-50;
}
.triangle-dark {
@apply after:border-l-info;
}
.triangle-light {
@apply after:border-l-success;
}
.item button {
@apply hover:bg-active !important;
}
/* table */
.content-box-background {
background: linear-gradient(
180deg,
rgba(127, 237, 193, 0.1),
rgba(0, 0, 0, 0),
rgba(127, 237, 193, 0.1)
);
}
}
@layer utilities {
.btn{
@apply px-4 py-1;
text-shadow: 0px 0px 5px rgba(0, 0, 0, 0.9);
box-shadow: 0px 0px 5px rgba(255, 255, 255, 0.8);
}
.btn-success {
@apply text-white border border-active bg-active hover:bg-[theme("colors.green.500")]
}
.btn-info {
@apply text-white border border-info bg-info hover:bg-[theme("colors.sky.400")]
}
.btn-outline-success {
@apply text-white border border-active hover:bg-active bg-transparent
}
.btn-outline-info {
@apply text-white border border-info hover:bg-info bg-transparent
}
.custom-border {
@apply border border-info rounded-md;
}
.btn-text-without-border {
@apply active:border-0 focus:border-0 focus-visible:border-0 active:outline-none focus:outline-none focus-visible:outline-none;
}
}

1
src/assets/logo.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

After

Width:  |  Height:  |  Size: 276 B

29
src/assets/main.css Normal file
View File

@ -0,0 +1,29 @@
@import "./base.css";
#app {
overflow: hidden;
font-weight: normal;
background-color: theme("colors.body");
background-image: url("./img/background.jpg");
background-size: cover;
color: #fff;
min-height: 100dvh;
}
::-webkit-scrollbar {
width: 5px !important;
height: 8px !important;
}
/* Track */
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px grey;
border-radius: 10px;
}
/* Handle */
::-webkit-scrollbar-thumb {
background: theme("colors.info");
border-radius: 10px;
box-shadow: theme("boxShadow.custom");
}

40
src/assets/pagination.css Normal file
View File

@ -0,0 +1,40 @@
.page-box ul {
display: flex;
justify-content: flex-end;
flex-wrap: wrap;
font-size: 1rem;
color: #fff;
margin-bottom: 10px;
}
.page-box ul li {
padding: 10px;
white-space: nowrap;
cursor: pointer;
}
.page-box ul .ant-pagination-item {
background-image: url(./img/pagination/small-btn.svg);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
padding: 10px 15px 10px 25px;
background-color: transparent;
display: flex;
justify-content: center;
align-items: center;
border: none;
}
.page-box ul .ant-pagination-item.ant-pagination-item-active {
background-image: url(./img/pagination/small-btn02.svg);
background-repeat: no-repeat;
background-position: center;
background-size: contain;
padding: 10px 15px 10px 25px;
}
.page-box .ant-pagination-item a {
color: #fff;
}

122
src/assets/table.css Normal file
View File

@ -0,0 +1,122 @@
/**資料框**/
.ant-table {
width: 100%;
margin-bottom: 1rem;
background-color: transparent !important;
color: white;
}
.ant-table::-webkit-scrollbar-thumb {
background-color: theme("colors.info") !important;
}
.ant-table th.ant-table-cell::before{
height: 100% !important;
}
.content-box {
border: 1px solid #35eded;
padding: 5px;
position: relative;
margin-bottom: 15px;
background-color: theme("colors.body");
}
.content-box table,
.content-box table th {
border-radius: 0 !important;
}
.content-box .ant-table th.ant-table-cell,
.content-box .ant-table td.ant-table-cell {
border-color: #fff !important;
color: #fff !important;
font-size: 1rem !important;
font-weight: 300 !important;
border-right: 1px solid #fff !important;
text-align: center !important;
padding: 0.5rem 0.75rem !important;
background-color: transparent !important;
}
.content-box .ant-table th.ant-table-cell.ant-table-cell-fix-left,
.content-box .ant-table th.ant-table-cell.ant-table-cell-fix-right {
background-color: theme("colors.body") !important;
color: white;
}
.content-box .ant-table th.ant-table-cell {
border-bottom: 1px solid #e9e9e9 !important;
}
.content-box .ant-table tr td:last-child,
.content-box .ant-table tr:first-child th:last-child {
border-right: 0 !important;
}
.content-box .ant-table tr:last-child td {
border-bottom: 0 !important;
}
/**資料框裝飾**/
.content-box::before {
content: "" !important;
background: url(./img/table/content-box-background01.svg) center center !important;
position: absolute !important;
left: 4px !important;
top: 4px !important;
height: 20px !important;
width: 20px !important;
background-repeat: no-repeat !important;
z-index: 1 !important;
}
.content-box::after {
content: "" !important;
background: url(./img/table/content-box-background05.svg) center center !important;
position: absolute !important;
right: 4px !important;
bottom: 4px !important;
height: 20px !important;
width: 20px !important;
background-repeat: no-repeat !important;
z-index: 3 !important;
}
.content-box .page-box::before {
content: "" !important;
background: url(./img/table/content-box-background03.svg) center center !important;
position: absolute !important;
left: -1.2% !important;
bottom: -2px !important;
height: 56px !important;
width: 30px !important;
background-repeat: no-repeat !important;
z-index: 2 !important;
}
.content-box .page-box::after {
content: "" !important;
background: url(./img/table/content-box-background04.svg) center center !important;
position: absolute !important;
right: -27px !important;
bottom: -7px !important;
height: 65px !important;
width: 50px !important;
background-repeat: no-repeat !important;
z-index: 2 !important;
}
.content-box .content-decoration {
@apply px-2;
}
.content-box .content-decoration::before {
content: "" !important;
background: url(./img/table/content-box-background02.svg) center center !important;
position: absolute !important;
right: -10px !important;
top: -10px !important;
height: 30px !important;
width: 29px !important;
background-repeat: no-repeat !important;
z-index: 1 !important;
}

View File

@ -0,0 +1,32 @@
<script setup></script>
<template>
<div class="w-full h-full flex items-center justify-center">
<div class="loader"></div>
</div>
</template>
<style lang="css" scoped>
.loader {
width: 20%;
height: 22px;
border-radius: 20px;
color: theme(colors.success);
border: 2px solid theme(colors.success);
position: relative;
}
.loader::before {
content: "";
position: absolute;
margin: 2px;
inset: 0 100% 0 0;
border-radius: inherit;
background: currentColor;
animation: l6 2s infinite;
}
@keyframes l6 {
100% {
inset: 0;
}
}
</style>

View File

@ -0,0 +1,44 @@
<template>
<i :style="getStyle">
<svg :class="class" aria-hidden="true">
<use :xlink:href="symbolId" :stroke="color" :fill="color" />
</svg>
</i>
</template>
<script setup name="SvgIcon">
import { computed } from 'vue'
const props = defineProps({
prefix: {
type: String,
default: 'icon',
},
name: {
type: String,
required: true,
},
color: {
type: String,
default: 'transparent',
},
size: {
type: [Number, String],
default: 10,
},
class: {
type: String,
default: "w-10 h-10",
},
})
const symbolId = computed(() => `#${props.prefix}-${props.name}`)
const getStyle = computed(() => {
const { size } = props
let s = `${size}`
s = `${s.replace('px', '')}px`
return {
fontSize: s,
}
})
</script>

View File

@ -0,0 +1,75 @@
<script setup>
import { defineProps } from "vue";
import { ackSingleAlarm } from "@/apis/building";
const props = defineProps({
alarms: {
type: Array,
required: true,
},
});
const ackedAlarm = async (uuid) => {
const res = await ackSingleAlarm(uuid);
console.log("ackedAlarm", res);
};
</script>
<template>
<div>
<ul class="py-6 pr-4 min-h-full text-base-content">
<!-- Sidebar content here -->
<li class="my-3" v-for="alarm in alarms" :key="alarm.uuid">
<div
class="w-full shadow-xl border border-success bg-body bg-opacity-80"
>
<div class="p-5">
<p class="text-base flex justify-between">
<span>
<font-awesome-icon
:icon="['fas', 'exclamation-triangle']"
class="text-warning mr-2"
/>
<span>異常通知</span></span
>
<small>
<span class="mr-4"
>{{ alarm.timestamp_date }} {{ alarm.timestamp_time }}</span
>
</small>
</p>
<div class="divider my-2"></div>
<div>
<p>異常編號{{ alarm.uuid }}</p>
<!-- <p>異常等級255</p> -->
<p>異常類別{{ alarm.alarmClass }}</p>
<p>設備名稱{{ alarm.full_name }}</p>
<p>異常訊息{{ alarm.msg }}</p>
</div>
<div class="card-actions mt-1 justify-end">
<button
class="btn btn-sm btn-success"
@click="() => ackedAlarm(alarm.uuid)"
>
確認
</button>
</div>
</div>
</div>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.card::before {
@apply absolute h-5 w-5 top-1 left-1 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background01.svg')] bg-center;
content: "";
}
.card::after {
@apply absolute bottom-1 right-1 h-5 w-5 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background05.svg')] bg-center;
content: "";
}
</style>

View File

@ -0,0 +1,74 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import AlarmCards from "./AlarmCards.vue";
import useAlarmStore from "@/stores/useAlarmStore";
import { getShowAlarm } from "@/apis/alert";
const showAlarmData = ref([]);
const showErr = ref(false);
const alarms = ref([]);
const store = useAlarmStore();
const fetchAlarmData = async () => {
const res = await getShowAlarm();
showAlarmData.value = res.data;
const showAlarmSet = new Set(showAlarmData.value);
alarms.value = store.alarmData.filter((alarm) =>
showAlarmSet.has(alarm.sourceName)
);
};
const toggleErrIcon = async () => {
console.log("Toggle");
showErr.value = !showErr.value;
if (showErr.value) {
await fetchAlarmData();
}
};
onMounted(() => {
store.getAlarmDataFromBaja();
});
</script>
<template>
<div class="drawer drawer-end">
<input id="alarm" type="checkbox" class="drawer-toggle" />
<div class="drawer-content">
<!-- Page content here -->
<label
for="alarm"
class="drawer-button flex flex-col justify-center items-center btn-group"
@click="toggleErrIcon"
>
<font-awesome-icon
v-if="!showErr"
:icon="['fas', 'comment-dots']"
size="2x"
class="text-white menu-icon"
/>
<font-awesome-icon
v-else
:icon="['fas', 'comment-slash']"
size="2x"
class="text-white menu-icon"
/>
<span class="text-white"> 顯示警告</span>
</label>
</div>
<div class="drawer-side translate-y-16 max-h-[95vh] overflow-y-scroll overflow-x-hidden">
<AlarmCards :alarms="alarms" />
</div>
</div>
</template>
<style lang="scss" scoped>
.drawer-toggle,
.drawer-button {
outline: none !important;
outline-offset: 0 !important;
border: 0 !important;
}
</style>

View File

@ -0,0 +1,31 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
const props = defineProps({
option: Object,
class: String,
id: String,
});
let chart = ref(null);
function init() {
let echart = echarts;
chart.value = markRaw(echart.init(document.getElementById(props.id)));
chart.value.setOption(props.option);
}
onMounted(() => {
init();
});
defineExpose({
chart,
});
</script>
<template>
<div :id="id" :class="class" class="border p-3"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,75 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
import axios from "axios";
const props = defineProps({
option: Object,
className: String,
id: String,
svg: Object,
getCoordinate: Function,
});
let chart = ref(null);
let dom = ref(null);
let currentClickPosition = ref([]);
async function updateSvg(svg, option) {
if (!chart.value && dom.value && svg) {
init();
}
axios.get(svg.path).then(({ data }) => {
echarts.registerMap(svg.full_name, { svg: data });
chart.value.setOption(option);
chart.value.getZr().on("click", function (params) {
var pixelPoint = [params.offsetX, params.offsetY];
var dataPoint = chart.value.convertFromPixel({ geoIndex: 0 }, pixelPoint);
currentClickPosition.value = dataPoint;
props.getCoordinate(dataPoint);
chart.value.setOption({
series: {
data: [dataPoint],
},
});
console.log(chart.value.getOption());
});
});
console.log("updateSvg", svg.path);
// fetch(svg.path)
// .then((res) => console.log(res))
// .then(function (svg) {
// console.log(svg);
// // echarts.registerMap(svg.full_name, { svg });
// // chart.setOption(option);
// // chart.getZr().on("click", function (params) {
// // var pixelPoint = [params.offsetX, params.offsetY];
// // var dataPoint = curChart.convertFromPixel({ geoIndex: 0 }, pixelPoint);
// // console.log(dataPoint);
// // currentClickPosition.value = dataPoint;
// // });
// });
}
function init() {
const curChart = echarts.init(dom.value);
chart.value = markRaw(curChart);
}
onMounted(() => {
if (!chart.value && dom.value && props.svg) {
init();
}
});
defineExpose({
chart,
currentClickPosition,
updateSvg,
});
</script>
<template>
<div :id="id" class="min-h-[500px] max-h-fit w-full" ref="dom"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,34 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
const props = defineProps({
option: Object,
class: String,
id: String,
});
let chart = ref(null);
let dom = ref(null);
function init() {
let echart = echarts;
chart.value = markRaw(echart.init(dom.value));
chart.value.setOption(props.option);
}
onMounted(() => {
if (!chart.value && dom.value) {
init();
}
});
defineExpose({
chart,
});
</script>
<template>
<div :id="id" :class="class" ref="dom"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,34 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
const props = defineProps({
option: Object,
class: String,
id: String,
});
let chart = ref(null);
let dom = ref(null);
function init() {
let echart = echarts;
chart.value = markRaw(echart.init(dom.value));
chart.value.setOption(props.option);
}
onMounted(() => {
if (!chart.value && dom.value) {
init();
}
});
defineExpose({
chart,
});
</script>
<template>
<div :id="id" :class="class" ref="dom"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,119 @@
<script setup>
import * as echarts from "echarts";
import "echarts-liquidfill";
import { onMounted, ref, watch, markRaw } from "vue";
const props = defineProps({
id: {
type: String,
required: true,
},
value: {
type: Number,
default: 0,
},
title: {
type: String,
default: "",
},
color: {
type: String,
},
});
const dom = ref(null);
const chart = ref(null);
const chartOption = ref({
title: {
text: props.title,
textStyle: {
color: "#fff",
fontSize: 16,
align: "center",
},
left: "center",
top: "5%",
},
series: [
{
type: "liquidFill",
radius: "85%",
waveAnimation: true,
data: [
{
value: props.value / 100, // value props
direction: "right",
},
],
color:[props.color],
outline: {
show: true,
borderDistance: 5,
itemStyle: {
opacity: 0.9,
borderWidth: 2,
shadowBlur: 14,
shadowColor: "#fff",
borderColor: props.color,
},
},
backgroundStyle: {
color: "rgba(0, 0, 0, 0.1)",
},
animationDuration: 1000,
label: {
show: true,
color: "#888",
insideColor: "#fff",
fontSize: 24,
fontWeight: 400,
},
},
],
});
const initChart = () => {
chart.value = markRaw(echarts.init(dom.value));
chart.value.setOption(chartOption.value);
};
onMounted(() => {
initChart();
});
watch(
() => props.value,
(newValue) => {
if (chart.value) {
chartOption.value.series[0].data = [newValue / 100];
chart.value.setOption(chartOption.value);
}
},
{ immediate: true
}
);
watch(
() => props.color,
(newColor) => {
if (chart.value) {
chartOption.value.series[0].data.itemStyle.normal.color = newColor;
chart.value.setOption(chartOption.value);
}
},
{ immediate: true }
);
defineExpose({
chart,
});
</script>
<template>
<div>
<div :id="id" ref="dom" style="width: 100%; height: 200px"></div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,31 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
const props = defineProps({
option: Object,
class: String,
id: String,
});
let chart = ref(null);
function init() {
let echart = echarts;
chart.value = markRaw(echart.init(document.getElementById(props.id)));
chart.value.setOption(props.option);
}
onMounted(() => {
init();
});
defineExpose({
chart,
});
</script>
<template>
<div :id="id" :class="class" class="p-3"></div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,52 @@
<script setup>
import { computed, defineProps } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
items: Array,
// this is for change active button
onclick: Function,
className: String,
size: {
type: String,
default: "xs",
},
color: {
type: String,
default: "success",
},
});
const btnSize = computed(() => `btn-${props.size}`);
const btnColor = computed(() => `btn-${props.color}`);
const btnOutlineColor = computed(() => `btn-outline-${props.color}`);
</script>
<template>
<div :class="className">
<button
type="button"
v-for="item in items"
:key="item.key"
:class="
twMerge(
'btn shadow-none rounded-none first-of-type:rounded-l-lg last-of-type:rounded-r-lg',
item.active ? btnColor : btnOutlineColor,
btnSize
)
"
:disabled="item.disabled"
@click.stop.prevent="
(e) => {
item.onClick ? item.onClick(e, item) : onclick(e, item);
}
"
>
<slot name="buttonContent" v-bind="{ item }">
{{ item.title }}
</slot>
</button>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,43 @@
<script setup>
import { defineProps } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
items: Array,
withLine: Boolean,
// this is for change active button
onclick: Function,
});
</script>
<template>
<div class="flex flex-wrap text-white items-center">
<button
v-for="item in items"
:class="
twMerge(
'btn my-1',
item.active ? 'btn-success' : 'btn-outline-success',
withLine ? 'line' : 'mx-2 first:ml-0 '
)
"
:disabled="item.disabled"
:key="item.key"
@click.stop.prevent="
(e) => {
item.onClick ? item.onClick(e, item) : onclick(e, item);
}
"
>
<slot name="buttonContent" v-bind="{ record: item }">
{{ item.title }}
</slot>
</button>
</div>
</template>
<style lang="css" scoped>
.line {
@apply mr-3 relative z-20 after:absolute after:top-1/2 after:-right-3 after:w-3 after:h-[1px] after:bg-info after:z-10 last:after:h-0;
}
</style>

View File

@ -0,0 +1,65 @@
<script setup>
import { twMerge } from "tailwind-merge";
import { defineProps } from "vue";
const props = defineProps({
checked: Boolean,
disabled: Boolean,
title: String,
onClick: Function,
onChange: Function,
name: String,
value: String,
className: {
type: String,
default: "",
},
});
</script>
<template>
<label
:class="
twMerge('cursor-pointer flex justify-center items-center my-0', className)
"
>
<input
:name="name || 'checkbox'"
type="checkbox"
class="checkbox checkbox-sm rounded-full checkbox-success"
:checked="checked"
:disabled="disabled"
:value="value"
@click.stop="
(e) => {
onClick && onClick();
}
"
@change.stop.prevent="
(e) => {
console.log('@', e.target.value, e.target.checked);
onChange && onChange(e.target.value, e.target.checked);
}
"
/>
<!-- <input
v-else
type="checkbox"
:name="name||'checkbox'"
:disabled="disabled"
class="checkbox checkbox-sm rounded-full checkbox-success mr-3"
@click.stop.prevent="
(e) => {
onClick && onClick();
}
"
/> -->
<span v-if="title" class="ml-3"> {{ title }}</span>
</label>
</template>
<style lang="css" scoped>
.checkbox:disabled {
@apply bg-opacity-50;
}
</style>

View File

@ -0,0 +1,72 @@
<script setup>
import { defineProps } from "vue";
import { twMerge } from "tailwind-merge";
/*
title: title
data: [{
title: subtitle,
key,
}]
*/
const props = defineProps({
open: Boolean,
title: String,
data: Array,
toggle: Function,
borderIsActive: {
type: Boolean,
default: true,
},
borderClass: {
type: String,
default: "",
},
});
</script>
<template>
<div
tabindex="0"
:class="
twMerge(
'collapse collapse-arrow bg-transparent rounded-none accordion',
open ? 'collapse-open ' : 'collapse-close'
)
"
>
<div
:class="
twMerge(
'collapse-title text-2xl font-bold w-full flex justify-between items-center text-white px-2 border-b cursor-pointer',
borderIsActive ? 'border-info' : borderClass
)
"
@click="toggle"
>
<slot name="collapseTitle" v-bind="{ data: d }">
{{ title }}
</slot>
</div>
<div
v-show="open"
:class="
twMerge(
'collapse-content border-b bg-normal m-0 p-4',
borderIsActive ? 'border-info' : borderClass
)
"
>
<ul>
<li v-for="d in data" class="cursor-pointer text-base text-white py-2">
<slot name="collapseContent" v-bind="{ data: d }">
{{ d.title }}
</slot>
</li>
</ul>
</div>
</div>
</template>
<style lang="css" scoped></style>

View File

@ -0,0 +1,124 @@
<script setup>
import { defineProps, computed } from "vue";
import VueDatePicker from "@vuepic/vue-datepicker";
import "@vuepic/vue-datepicker/dist/main.css";
import { zhTW } from "date-fns/locale";
import { twMerge } from "tailwind-merge";
const props = defineProps({
items: Array,
withLine: Boolean,
label: String,
inputClass: String,
borderColor: String,
isTopLabelExist: {
type: Boolean,
default: true,
},
isBottomLabelExist: {
type: Boolean,
default: true,
},
className: String,
width: String,
});
const curWidth = computed(() => {
if (props.width) {
return {
style: {
width: `${props.width}px`,
},
class: "",
};
} else {
return {
class: "w-80 max-w-sm",
};
}
});
</script>
<template>
<div
:class="twMerge('form-control', className, curWidth.class)"
:style="curWidth.style"
>
<div :class="twMerge(isTopLabelExist ? 'label' : '')">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<div class="flex text-white items-center">
<template v-for="item in items" :key="item.key">
<VueDatePicker
:name="item.name || item.key"
:month-picker="Boolean(item.monthPicker)"
:year-picker="Boolean(item.yearPicker)"
dark
:action-row="{
showNow: false,
showCancel: false,
showPreview: false,
}"
v-model="item.value"
locale="zh-TW"
:day-names="['一', '二', '三', '四', '五', '六', '日']"
:format="item.dateFormat"
:enable-time-picker="false"
:time-picker="Boolean(item.timePicker)"
:placeholder="item.placeholder"
selectText="確定"
:input-class-name="
twMerge('dp-custom-input', 'btn border', inputClass)
"
calendar-cell-class-name="dp-custom-cell"
:class="twMerge(withLine ? 'line my-1' : '')"
:max-date="item.maxDate"
:max-time="item.maxTime"
:start-date="item.startDate"
:start-time="item.startTime"
:min-date="item.minDate"
></VueDatePicker>
</template>
</div>
<div :class="twMerge(isBottomLabelExist ? 'label' : '')">
<span class="label-text-alt"> <slot name="bottomLeft"></slot></span>
<span class="label-text-alt"> <slot name="bottomRight"></slot></span>
</div>
</div>
</template>
<style lang="css" coped>
.dp__theme_dark {
--dp-primary-color: var(--primary);
}
.dp__input.dp-custom-input {
@apply bg-transparent rounded-md min-w-[155px] border-info hover:border-info text-white hover:bg-transparent;
}
.dp__input.dp-custom-input.shadow-none {
box-shadow: none !important;
}
.dp-custom-cell {
@apply rounded-full;
}
.dp__today {
@apply border-active;
}
.dp__active_date {
@apply border-active bg-active;
}
.dp__action_buttons .dp__action_button.dp__action_select {
@apply bg-white text-dark text-lg;
}
</style>
<style scoped>
.line {
@apply mr-3 relative z-20 after:absolute after:top-1/2 after:-right-3 after:w-3 after:h-[1px] after:bg-info after:z-10 last:after:h-0;
}
</style>

View File

@ -0,0 +1,29 @@
<script setup>
import { twMerge } from "tailwind-merge";
import { defineProps } from "vue";
const props = defineProps({
open: Boolean,
items: Array,
title: String,
onClick: Function,
});
</script>
<template>
<div :class="twMerge('dropdown', open ? 'dropdown-open' : 'dropdown-close')">
<slot name="dropdownButton"></slot>
<ul
tabindex="0"
class="dropdown-content flex flex-col items-center justify-between px-3 text-base translate-y-2 z-50 py-3 shadow rounded min-w-max bg-[#4c625e] border text-center"
>
<li v-for="(item, index) in items" :key="item.key" class="w-full py-0">
<slot name="dropdownItem" v-bind="{ record: item, index }"></slot>
</li>
<slot name="dropdownAction"></slot>
</ul>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,189 @@
<script setup>
import { computed, defineProps, onMounted, ref, watch } from "vue";
import { twMerge } from "tailwind-merge";
/*
title: title
data: [{
title: subtitle,
key, // required
}]
*/
const props = defineProps({
open: Boolean,
data: Array,
cls: {
type: String,
default: "",
},
onClick: Function, //
onRightClick: Function, //
filenameInputIsOpen: Boolean,
selected: Object,
edit: Function,
openItem: Array,
});
const openChildren = ref([]);
const toggleOpenChildren = (dataParentKey) => {
if (openChildren.value.includes(dataParentKey)) {
openChildren.value = [];
} else {
openChildren.value = [dataParentKey];
}
};
watch(
() => props.openItem,
() => {
openChildren.value = props.openItem;
}
);
//
// const getInitFileKey=(d)=>{
// let initKey = [];
// d.forEach(element => {
// if(element.children?.length > 0){
// let childrenKey = getInitFileKey(element.children);
// initKey.push(element.key, ...childrenKey);
// }
// });
// return initKey;
// }
// onMounted(()=>{
// const keys = getInitFileKey(props.data);
// openChildren.value.push(...keys);
// })
</script>
<template>
<ul
:class="
twMerge(
'flex-col text-xl',
cls,
openChildren.includes(dataParentKey) || open ? 'flex' : 'hidden'
)
"
v-for="d in data"
:data-parent="d.key"
:open="open"
>
<li
v-if="d.children?.length > 0"
class="py-1 cursor-pointer"
:data-value="d.key"
>
<font-awesome-icon
:icon="['fas', 'caret-right']"
:class="
twMerge(
openChildren.includes(d.key) ? 'rotate-90' : '',
'group-hover/item:text-success'
)
"
@click.stop.prevent="
(e) => {
toggleOpenChildren(d.key);
onClick && onClick(d.key, d);
}
"
/>
<font-awesome-icon
:icon="['fas', 'folder']"
class="ml-5 text-amber-400"
/>
<input
v-if="filenameInputIsOpen && selected?.key === d.key"
type="text"
class="ml-2 px-2 w-3/4"
:value="d.title"
@click.stop.prevent
v-focus
@change.stop.prevent="
(e) => {
console.log('edit', e);
edit(e);
}
"
/>
<span
v-else
class="ml-2 hover:text-success"
@click.stop.prevent="
(e) => {
toggleOpenChildren(d.key);
onClick && onClick(d.key, d);
}
"
@contextmenu.prevent.stop="
(e) => {
console.log(e);
onRightClick(e, d);
}
"
>
{{ d.title }}</span
>
</li>
<li v-else class="pl-7 py-1 cursor-pointer" :data-value="d.key">
<font-awesome-icon
:icon="['fas', 'folder']"
class="mr-2 text-amber-400"
@click.stop.prevent="
(e) => {
toggleOpenChildren(d.key);
onClick && onClick(d.key, d);
}
"
/>
<input
v-if="filenameInputIsOpen && selected?.key === d.key"
class="ml-2 px-2 w-3/4"
type="text"
:value="d.title"
v-focus
@click.stop.prevent
@change.stop.prevent="
(e) => {
console.log('edit', e);
edit(e);
}
"
/>
<span
v-else
class="ml-2 hover:text-success"
@click="
(e) => {
onClick && onClick(d.key, d);
}
"
@contextmenu.stop.prevent="
(e) => {
onRightClick(e, d);
}
"
>
{{ d.title }}</span
>
</li>
<FileSystemCollapse
v-if="d.children?.length > 0"
:data="d.children"
cls="pl-4"
:open="openChildren.includes(d.key)"
:onClick="onClick"
:key="d.key"
:openItem="openItem"
:selected="selected"
:filenameInputIsOpen="filenameInputIsOpen"
:edit="edit"
:onRightClick="onRightClick"
/>
</ul>
</template>
<style lang="css" scoped></style>

View File

@ -0,0 +1,96 @@
<script setup>
import { computed, defineProps } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
label: String,
name: String,
placeholder: {
type: String,
default: "",
},
value: String,
isTopLabelExist: {
type: Boolean,
default: true,
},
isBottomLabelExist: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
required: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
className: String,
width: String,
type: {
default: "text",
type: String,
},
});
const curWidth = computed(() => {
if (props.width) {
return {
style: {
width: `${props.width}px`,
},
class: "",
};
} else {
return {
class: "w-80 max-w-sm",
};
}
});
</script>
<template>
<label
:class="twMerge('form-control', className, curWidth.class)"
:style="curWidth.style"
>
<div :class="twMerge(isTopLabelExist ? 'label' : '')">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<slot name="icon"> </slot>
<input
v-if="name"
:type="type"
:name="name || 'input'"
:placeholder="placeholder"
v-model="value[name]"
:disabled="disabled"
:readonly="readonly"
:required="required"
class="text-lg text-white bg-transparent w-full input input-bordered rounded-md px-3 border-info focus-within:border-info read-only:bg-base-300 read-only:text-zinc-500 read-only:border-0 read-only:focus-within:outline-0 read-only:focus:outline-0"
/>
<input
v-else
type="text"
:name="name || 'input'"
:placeholder="placeholder"
:value="value"
:disabled="disabled"
:readonly="readonly"
:required="required"
class="text-lg text-white bg-transparent w-full input input-bordered rounded-md px-3 border-info focus-within:border-info read-only:bg-base-300 read-only:text-zinc-500 read-only:border-0 read-only:focus-within:outline-0 read-only:focus:outline-0"
/>
<div :class="twMerge(isBottomLabelExist ? 'label' : '')">
<span class="label-text-alt"><slot name="bottomLeft"></slot></span>
<span class="label-text-alt"><slot name="bottomRight"></slot></span>
</div>
</label>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,92 @@
<script setup>
import { computed, defineProps } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
label: String,
name: String,
placeholder: {
type: String,
default: "",
},
value: String,
isTopLabelExist: {
type: Boolean,
default: true,
},
isBottomLabelExist: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
required: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
className: String,
width: String,
});
const curWidth = computed(() => {
if (props.width) {
return {
style: {
width: `${props.width}px`,
},
class: "",
};
} else {
return {
class: "w-80 max-w-sm",
};
}
});
</script>
<template>
<label
:class="twMerge('form-control', className, curWidth.class)"
:style="curWidth.style"
>
<div :class="twMerge(isTopLabelExist ? 'label' : '')">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<slot name="icon"> </slot>
<input
v-if="name"
type="text"
:name="name || 'input'"
:placeholder="placeholder"
v-model.number="value[name]"
:disabled="disabled"
:readonly="readonly"
:required="required"
class="text-lg text-white bg-transparent w-full input input-bordered rounded-md px-3 border-info focus-within:border-info read-only:bg-base-300 read-only:text-zinc-500 read-only:border-0 read-only:focus-within:outline-0 read-only:focus:outline-0"
/>
<input
v-else
type="text"
:name="name || 'input'"
:placeholder="placeholder"
:value="value"
:disabled="disabled"
:readonly="readonly"
:required="required"
class="text-lg text-white bg-transparent w-full input input-bordered rounded-md px-3 border-info focus-within:border-info read-only:bg-base-300 read-only:text-zinc-500 read-only:border-0 read-only:focus-within:outline-0 read-only:focus:outline-0"
/>
<div :class="twMerge(isBottomLabelExist ? 'label' : '')">
<span class="label-text-alt"><slot name="bottomLeft"></slot></span>
<span class="label-text-alt"><slot name="bottomRight"></slot></span>
</div>
</label>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,94 @@
<script setup>
import { twMerge } from "tailwind-merge";
import { defineProps, ref, watch } from "vue";
/* ----------------------------------------------------------------
id名.showModal(): 開啟 modal
id名.close(): 關閉 modal
詳細請參考 daisyUI
------------------------------------------------------------------- */
const props = defineProps({
id: String,
title: String,
onCancel: Function,
modalClass: String,
width: Number || String,
draggable: {
type: Boolean,
default: false,
},
backdrop: {
type: Boolean,
default: true,
},
modalStyle: {
type: Object,
default: {},
},
});
</script>
<template>
<!-- Open the modal using ID.showModal() method -->
<!-- :class="twMerge('modal', open && innerOpen ? 'modal-open' : 'modal-close')" -->
<dialog
:id="id"
:class="
twMerge(
'modal',
backdrop
? ''
: 'h-fit w-fit max-h-fit max-w-fit focus-visible:outline-none backdrop:bg-transparent'
)
"
:style="modalStyle"
v-draggable="draggable"
>
<div
:class="
twMerge(
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-scroll overflow-x-hidden bg-normal',
modalClass
)
"
:style="{ minWidth: isNaN(width) ? width : `${width}px` }"
>
<div class="text-2xl font-bold">
<slot name="modalTitle">
{{ title }}
</slot>
</div>
<div class="min-h-[150px]">
<slot name="modalContent"></slot>
</div>
<div class="modal-action relative">
<slot name="modalAction"></slot>
</div>
</div>
<form v-if="backdrop" method="dialog" class="modal-backdrop">
<button
@click="
() => {
onCancel ? onCancel() : cancel();
}
"
>
close
</button>
</form>
</dialog>
</template>
<style lang="css" scoped>
.modal-box::before {
@apply fixed top-1 right-1 h-5 w-5 rotate-90 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background01.svg')] bg-center;
content: "";
}
.modal-action::after {
@apply absolute -bottom-3 -left-4 h-5 w-5 rotate-90 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background05.svg')] bg-center;
content: "";
}
</style>

View File

@ -0,0 +1,192 @@
<script setup>
import { defineProps, ref, computed, inject, watch } from "vue";
import { twMerge } from "tailwind-merge";
import { data } from "autoprefixer";
/* -------------------------------------------------------------
> 6 => 會有 input 跳頁且前三後三顯示
---------------------------------------------------------------- */
const props = defineProps({
pageSize: {
type: Number,
default: 10,
}, //
totalPages: {
type: Number,
default: 0,
},
onPageChange: Function, //
dataSource: Array,
totalItems: Number,
sort: Object,
});
const current_table_data = inject("current_table_data");
const currentPage = ref(0);
const totalPage = ref(0);
const beforeInputPage = computed(() => {
if (totalPage.value > 6) {
return 3;
} else {
return totalPage.value;
}
});
const choosePage = (page) => {
currentPage.value = parseInt(page);
props.onPageChange
? props.onPageChange(parseInt(page))
: changePageData(parseInt(page));
};
//
const changePageData = (currentPage) => {
const start = (currentPage - 1) * props.pageSize;
const end = currentPage * props.pageSize;
const data = props.dataSource.slice(start, end);
current_table_data.updateDataSource(data);
};
watch(
() => [props.dataSource, props.sort],
([newVal, newVal2]) => {
// console.log(props.dataSource, newVal);
currentPage.value = 1;
totalPage.value =
props.totalPages || Math.ceil(props.dataSource.length / props.pageSize);
props.onPageChange
? current_table_data.updateDataSource(props.dataSource)
: changePageData(1);
},
{
deep: true,
}
);
const pageInput = computed(() => {
if (currentPage.value > 3 && currentPage.value < totalPage.value - 2) {
return currentPage.value;
} else {
return "";
}
});
</script>
<template>
<div
class="relative flex justify-end items-end my-5"
v-if="dataSource.length > 0"
>
<span class="mx-1">
<button
type="button"
class="prev focus:border-0 disabled:text-gray-500 hover:text-warning"
:disabled="currentPage === 1"
@click="
() => {
choosePage(currentPage - 1 > 0 ? currentPage - 1 : 1);
}
"
>
<font-awesome-icon :icon="['fas', 'chevron-left']" class="text-3xl" />
</button>
</span>
<ul class="flex items-center list-none">
<li
v-for="page in beforeInputPage"
:key="`page${page}`"
:class="
twMerge(
'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center',
currentPage === page ? 'bg-sub-success' : 'bg-transparent'
)
"
@click="
() => {
choosePage(page);
}
"
>
<span class="text-white font-extrabold italic">
{{ page }}
</span>
</li>
</ul>
<span
v-if="totalPage < 6"
class="absolute -bottom-8 -translate-x-1/2 text-base text-center"
>
{{ dataSource.length }} </span
>
<label
v-if="totalPage > 6"
class="mx-2 relative input border-2 border-sub-success flex items-center gap-2"
>
<input
type="text"
maxlength="6"
class="bg-transparent h-full w-20 font-extrabold italic text-lg"
placeholder="跳至"
:value="pageInput"
@change="
(e) => {
choosePage(e.target.value);
}
"
/>
<span
><font-awesome-icon
:icon="['fas', 'search']"
class="text-xl text-sub-success"
/></span>
<span
class="w-full text-center absolute -bottom-8 left-1/2 -translate-x-1/2 text-base"
>
{{ totalItems || dataSource.length }} </span
>
</label>
<ul
v-if="totalPage > 6"
class="flex flex-row-reverse items-center list-none"
>
<li
v-for="(page, index) in 3"
:key="`page${totalPage - index}`"
:class="
twMerge(
'w-10 h-10 mx-1 border-2 border-sub-success rounded-full flex items-center justify-center',
currentPage === totalPage - index
? 'bg-sub-success'
: 'bg-transparent'
)
"
@click.stop.prevent="
() => {
choosePage(totalPage - index);
}
"
>
<span class="text-white font-extrabold italic">
{{ totalPage - index }}
</span>
</li>
</ul>
<span class="mx-1">
<button
type="button"
class="next focus:border-0 disabled:text-gray-500 hover:text-warning"
:disabled="currentPage === totalPage"
@click="
() => {
choosePage(
currentPage + 1 <= totalPage ? currentPage + 1 : totalPage
);
}
"
>
<font-awesome-icon :icon="['fas', 'chevron-right']" class="text-3xl" />
</button>
</span>
</div>
</template>

View File

@ -0,0 +1,56 @@
<script setup>
import { defineProps } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
name: String,
value: String,
items: Array,
isLabelExist: {
type: Boolean,
default: true,
},
required: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
});
</script>
<template>
<div class="form-control w-80 max-w-sm">
<div :class="twMerge(isLabelExist ? 'label' : '')">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<div class="flex flex-wrap">
<label
v-for="item in items"
:key="item.key"
class="label cursor-pointer mr-5"
>
<input
type="radio"
:name="name || 'radio'"
v-model="value[name]"
class="radio radio-info border-info rounded-full mr-3"
:value="item.value"
:required="required"
:readonly="readonly"
/>
<span class="label-text text-xl">{{ item.title }}</span>
</label>
</div>
<div :class="twMerge(isLabelExist ? 'label' : '')">
<span class="label-text-alt"> <slot name="bottomLeft"></slot></span>
<span class="label-text-alt"> <slot name="bottomRight"></slot></span>
</div>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,85 @@
<script setup>
import { defineProps } from "vue";
import { twMerge } from "tailwind-merge";
/* --------------------------------------------------------------
options: [
{
key: unique,
content: "",
selected: true / false,
disabled: true / false
}
]
---------------------------------------------------------------- */
const props = defineProps({
options: Array,
name: String,
Attribute: String,
onChange: Function,
selectClass: String,
value: String || Number,
isTopLabelExist: {
type: Boolean,
default: true,
},
isBottomLabelExist: {
type: Boolean,
default: true,
},
required: {
type: Boolean,
default: false,
},
readonly: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
});
</script>
<template>
<label class="form-control w-80 max-w-sm">
<div :class="twMerge(isTopLabelExist ? 'label' : '')">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<select
:disabled="disabled"
:name="name"
:class="twMerge('select bg-transparent rounded-md text-lg', selectClass)"
@change="
(e) => {
console.log(e.target.value);
props.onChange && props.onChange(e.target.value);
}
"
v-model="value[name]"
:required="required"
:readonly="readonly"
>
<option
v-for="option in options"
:key="option.key || option"
:selected="option.selected"
:disabled="option.disabled"
:class="twMerge(disabled ? `text-white` : 'text-dark')"
:value="option.value || option.key || option"
>
<span>
{{ option[Attribute] || option }}
</span>
</option>
</select>
<div :class="twMerge(isBottomLabelExist ? 'label' : '')">
<span class="label-text-alt"> <slot name="bottomLeft"></slot></span>
<span class="label-text-alt"> <slot name="bottomRight"></slot></span>
</div>
</label>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,381 @@
<script setup>
import { twMerge } from "tailwind-merge";
import { computed, defineProps, provide, ref, watch } from "vue";
import Pagination from "@/components/customUI/Pagination.vue";
import Checkbox from "@/components/customUI/Checkbox.vue";
import dayjs from "dayjs";
/*
column={
title,key,class, width, filter:Boolean, sort:Boolean
}
*/
const props = defineProps({
columns: Array,
dataSource: Array,
rowKey: String,
withStyle: {
type: Boolean,
default: true,
},
pagination: { type: Boolean, default: true } || {
pageSize: Number,
totalPages: Number,
totalItems: Number,
},
loading: Boolean,
});
const currentDataSource = ref([]);
const dataSourceStorage = ref([]);
watch(
() => props.dataSource,
(newValue) => {
dataSourceStorage.value = newValue;
filterItems.value = Object.fromEntries(
props.columns.map((c, i) => [
c.key,
[...new Set(newValue.map((d) => d[c.key]))].map((name) => ({
name,
selected: false,
})),
])
);
}
);
const updateDataSource = (data) => {
console.log("update", data);
currentDataSource.value = data;
};
provide("current_table_data", {
updateDataSource,
});
const sortRule = ref({});
const filterColumn = ref({});
const filterItems = ref({});
const selectedFilterItem = ref({});
const toggleFilterModal = (key) => {
let newFilter = Object.assign(filterColumn.value);
for (let oKey in newFilter) {
newFilter[oKey] = key === oKey && !newFilter[key];
}
filterColumn.value = newFilter;
};
watch(
() => props.columns,
(newValue) => {
sortRule.value = Object.fromEntries(newValue.map((c) => [c.key, 0]));
filterColumn.value = Object.fromEntries(
newValue.map((c, i) => [c.key, false])
);
selectedFilterItem.value = Object.fromEntries(
newValue.map((c, i) => [c.key, []])
);
},
{
immediate: true,
}
);
/*
0:取消
1:ascending
2:descending
*/
const toggleSortRule = (key) => {
let newSort = Object.assign(sortRule.value);
for (let oKey in newSort) {
newSort[oKey] = key === oKey ? newSort[key] : 0;
}
sortRule.value = newSort;
};
const sort = (column) => {
toggleSortRule(column);
const cantSort = ["object", "boolean"];
console.log(props.dataSource?.[0][column]);
if (cantSort.includes(typeof props.dataSource?.[0][column])) return;
// ->
const newArray = Object.assign(props.dataSource, []).sort((a, b) => {
// if (column === "timestamp") {
// return dayjs(a[column]).valueOf() - dayjs(b[column]).valueOf();
// }
if (typeof a[column] === "number") return a[column] - b[column];
else if (typeof a[column] === "string") {
console.log(a[column], b[column], a[column].localeCompare(b[column]));
return a[column].localeCompare(b[column]);
}
// return parseInt(a[column]) - parseInt(b[column]);
});
if (sortRule.value[column] === 0) {
sortRule.value[column] = 1;
dataSourceStorage.value = newArray;
} else if (sortRule.value[column] === 1) {
sortRule.value[column] = 2;
dataSourceStorage.value = newArray.reverse();
} else if (sortRule.value[column] === 2) {
sortRule.value[column] = 0;
dataSourceStorage.value = props.dataSource;
}
};
const form = ref(null);
const onFilter = (key, reset = false) => {
const formData = new FormData(form.value);
reset && formData.delete(key);
for (let [name, value] of formData) {
console.log(name, value);
}
selectedFilterItem.value[key] = formData.getAll(key);
toggleFilterModal(key);
};
watch(
selectedFilterItem,
(newVal) => {
let newData = Object.assign(props.dataSource);
for (let key in newVal) {
if (newVal[key].length > 0) {
newData = newData.filter((d) => newVal[key].includes(d[key]));
}
}
dataSourceStorage.value = newData;
},
{
deep: true,
}
);
</script>
<template>
<div :class="withStyle ? 'content-box' : 'py-5'">
<div class="content-decoration">
<slot name="beforeTable"></slot>
<form ref="form">
<table
:class="
twMerge(
withStyle ? 'table' : 'table border',
currentDataSource.length === 0 ? 'h-96' : ''
)
"
>
<!-- head -->
<thead>
<tr>
<th
v-for="column in columns"
:key="column.key"
:class="`${column.class ? column.class : ''}`"
:style="{
width: `${
column.width
? typeof column.width === 'string'
? column.width
: column.width + 'px'
: 'auto'
}`,
}"
>
<span class="flex justify-center">
{{ column.title }}
<div
v-if="column.sort"
class="flex flex-col justify-center w-3 mx-2 relative"
@click="() => sort(column.key)"
>
<font-awesome-icon
:icon="['fas', 'sort-up']"
:class="
twMerge(
'absolute top-0',
sortRule[column.key] === 1 ? 'text-success' : ''
)
"
size="lg"
/>
<font-awesome-icon
:icon="['fas', 'sort-down']"
:class="
twMerge(
'absolute bottom-1',
sortRule[column.key] === 2 ? 'text-success' : ''
)
"
size="lg"
/>
</div>
<div class="ml-2 relative" v-if="column.filter">
<font-awesome-icon
:icon="['fas', 'filter']"
:class="
twMerge(
filterColumn[column.key] ||
selectedFilterItem[column.key].length > 0
? 'text-success'
: ''
)
"
@click="() => toggleFilterModal(column.key)"
/>
<div
class="absolute top-full -left-1/2 z-50"
v-if="filterColumn[column.key]"
>
<div class="card min-w-max bg-body shadow-xl px-10 py-5">
<Checkbox
v-for="item in filterItems[column.key]"
:title="item.name"
:value="item.name"
:key="item.name"
:name="column.key"
:checked="
selectedFilterItem[column.key].includes(item.name)
"
className="justify-start"
/>
<div class="card-actions mt-4 justify-end">
<input
type="reset"
class="btn btn-sm text-white btn-error"
value="重置"
@click="() => onFilter(column.key, true)"
/>
<button
class="btn btn-sm btn-success"
@click.stop.prevent="() => onFilter(column.key)"
>
確定
</button>
</div>
</div>
</div>
</div>
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-if="loading">
<td :colspan="columns.length">
<Loading />
</td>
</tr>
<tr v-else-if="currentDataSource.length == 0">
<td :colspan="columns.length">表中數據為空</td>
</tr>
<template v-else :sort="sortRule">
<tr
v-for="(data, index) in currentDataSource"
:key="data.key || data[rowKey]"
:class="{ 'bg-slate-800 bg-opacity-80': data.enable == 0 }"
>
<template
v-for="column in columns"
:key="`${data.key || data[rowKey]}_${column.key}`"
>
<td
:class="column.class"
:style="{
width: `${
column.width
? typeof column.width === 'string'
? column.width
: column.width + 'px'
: 'auto'
}`,
}"
>
<slot
name="bodyCell"
v-bind="{ record: data, column, index }"
>
{{ data[column.key] }}</slot
>
</td>
</template>
</tr>
</template>
</tbody>
</table>
</form>
<slot name="afterTable"></slot>
<Pagination
:pagination="pagination"
:dataSource="dataSourceStorage"
:sort="sortRule"
/>
</div>
<div class="content-decoration2"></div>
</div>
</template>
<style lang="css" scoped>
/**資料框**/
.content-box {
@apply border border-info p-1 relative mb-4 bg-transparent;
}
.content-box .table {
@apply rounded-none;
}
.table th,
.table td {
@apply border-r border-b border-white text-lg font-semibold text-white text-center px-2 py-3;
}
.table tr td:last-child,
.table tr:first-child th:last-child {
border-right: 0;
}
/* .table tr:last-child td {
border-bottom: v-bind("withStyle ? '0px': '1px'");
} */
/**資料框裝飾**/
.content-box::before {
@apply absolute top-1 left-1 h-5 w-5 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background01.svg')] bg-center;
content: "";
}
.content-box::after {
@apply absolute bottom-1 right-1 h-5 w-5 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background05.svg')] bg-center;
content: "";
}
.content-box .content-decoration {
@apply bg-normal px-8 py-4;
}
.content-box .content-decoration::before {
@apply absolute -top-3 -right-[10px] h-8 w-8 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background02.svg')] bg-center;
content: "";
}
.content-box .content-decoration2::before {
@apply absolute -bottom-1 -left-8 h-14 w-14 bg-no-repeat z-10 bg-[url('../../assets/img/table/content-box-background03.svg')] bg-center;
content: "";
}
.content-box .content-decoration2::after {
content: "";
background: url(../../assets/img/table/content-box-background04.svg) center
center;
position: absolute;
right: -27px;
bottom: -7px;
height: 65px;
width: 50px;
background-repeat: no-repeat;
z-index: 2;
}
</style>

View File

@ -0,0 +1,30 @@
<script setup>
import { defineProps } from "vue";
const props = defineProps({
name: String,
value: String,
placeholder: String,
});
</script>
<template>
<label class="form-control">
<div class="label">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<textarea
class="textarea text-lg rounded-md border-info focus-within:border-info h-24"
:placeholder="placeholder"
:name="name"
v-model="value[name]"
></textarea>
<div class="">
<span class="label-text-alt"> <slot name="bottomLeft"></slot></span>
<span class="label-text-alt"> <slot name="bottomRight"></slot></span>
</div>
</label>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,73 @@
<script setup>
import { defineProps, ref, watch } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
content: String,
status: String,
open: Boolean,
cancel: Function,
confirm: Function,
to: {
default: "body",
type: String,
},
});
const isOpen = ref(false);
// TODO: Toast App.js
watch(
() => props.open,
(newVal) => {
isOpen.value = newVal;
if (newVal && props.status !== "warning") {
setTimeout(() => {
props.cancel && props.cancel();
}, 3000);
}
}
);
</script>
<template>
<Teleport :to="to">
<template v-if="isOpen">
<div
role="alert"
:class="
twMerge(
`alert text-xl rounded-md fixed left-1/2 -translate-x-1/2 top-24 z-[1000] max-w-fit`,
status === 'info'
? 'alert-info'
: status === 'error'
? 'alert-error text-white'
: status === 'warning'
? 'alert-warning bg-yellow-400'
: 'alert-success'
)
"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="stroke-current shrink-0 w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<span>{{ content }}</span>
<div v-if="status === 'warning'">
<button @click="props.cancel && props.cancel();" className="btn btn-sm btn-outline me-2">取消</button>
<button @click="props.confirm && props.confirm()" className="btn btn-sm">確認</button>
</div>
</div>
</template>
</Teleport>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,287 @@
<script setup>
import { ref, defineProps, watch, onMounted } from "vue";
import { twMerge } from "tailwind-merge";
const props = defineProps({
name: String,
fileList: Array,
getFileList: Function,
multiple: Boolean,
baseUrl: String,
});
const acceptFileType = [
".csv",
// https://stackoverflow.com/questions/11832930/html-input-file-accept-attribute-file-type-csv
// Excel Files 97-2003
"application/vnd.ms-excel",
// Excel Files 2007+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"text/plain",
"image/*",
"application/pdf",
// Microsoft Word -> https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/accept
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
];
const loading = ref(false);
const fileInput = ref(null);
const fileCheck = (fileList) => {
for (let { type, name } of fileList) {
const ext = name.split(".")[name.split(".").length - 1];
if (
!acceptFileType.includes(type) &&
!acceptFileType.includes(`.${ext}`) &&
!type.match("image/*")
) {
return false;
}
}
return true;
};
const createPreviewImgURL = (file) => {
loading.value = true;
const src = URL.createObjectURL(file);
return src;
};
const createPreviewFileURL = async (file) => {
console.log(file);
if (file) {
let objectURL = "";
if (file.lastModified) {
objectURL = URL.createObjectURL(file);
} else {
const response = await fetch(`${props.baseUrl}/${file.src}`);
const blob = await response.blob();
objectURL = URL.createObjectURL(blob);
}
const link = document.createElement("a");
link.href = objectURL;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
};
const handleMultipleFiles = (files) => {
for (let file of files) {
if (file.type?.match("image/*")) {
file.src = createPreviewImgURL(file);
console.log("File", file);
}
}
if (props.multiple) {
props.getFileList([...props.fileList, ...files]);
} else {
props.getFileList([files[0]]);
}
};
const uploadFile = (e) => {
console.log("Uploading file", e.target.files);
const fileCheckIsSuccess = fileCheck(e.target.files);
if (fileCheckIsSuccess) {
handleMultipleFiles(e.target.files);
}
};
const dragFile = (e) => {
console.log(e.dataTransfer.files);
const fileCheckIsSuccess = fileCheck(e.dataTransfer.files);
if (fileCheckIsSuccess) {
handleMultipleFiles(e.dataTransfer.files);
}
};
const removeFile = (key) => {
props.getFileList(
props.fileList.filter(
(f) => f.key !== key && f.lastModified !== key && f.id !== key
)
);
};
const focusInput = (e) => {
if (e.constructor.name === "PointerEvent") {
console.log(e, e.constructor.name);
fileInput.value.focus();
//
var event = new MouseEvent("click", {
view: window,
bubbles: false,
cancelable: true,
});
fileInput.value.dispatchEvent(event);
}
};
const revokeURL = (src) => {
console.log(props.fileList);
loading.value = false;
URL.revokeObjectURL(src);
};
</script>
<template>
<label class="form-control w-full">
<div class="label" @click.stop.prevent="() => {}">
<span class="label-text text-lg"><slot name="topLeft"></slot></span>
<span class="label-text-alt"> <slot name="topRight"></slot></span>
</div>
<div
class="dropzone p-3 flex flex-col justify-center text-base min-h-[200px] border border-stone-900 shadow-lg rounded-md bg-sub-success bg-opacity-25"
@click.stop.prevent="focusInput"
@dragover.stop.prevent
>
<div @drop.stop.prevent="dragFile">
<template v-if="fileList.length">
<ul class="flex flex-wrap justify-start items-center">
<li
:class="
twMerge(
'relative my-2 w-1/5 flex justify-center shadow rounded-lg mx-3 h-40',
file.lastModified ? 'bg-dark bg-opacity-50' : 'bg-sub-warning'
)
"
v-for="file in fileList"
:key="file.key || file.id || file.lastModified"
>
<button
class="absolute z-30 text-2xl -top-3 right-0 w-10 h-10 rounded-full bg-error flex justify-center items-center"
@click.stop.prevent="
() => {
removeFile(file.key || file.id || file.lastModified);
}
"
>
<font-awesome-icon :icon="['fas', 'trash-alt']" />
</button>
<div
v-if="
file.src &&
(file.type?.match('image/*') ||
file.ext?.match(/png|jpg|jpeg|gif|bmp/g))
"
class="w-full h-full z-10 absolute top-0 left-0 rounded-lg opacity-30"
@click.stop.prevent="() => createPreviewFileURL(file)"
>
<img
v-if="file.id"
:src="`${baseUrl}/${file.src}`"
class="w-full h-full"
/>
<img
v-else-if="file.lastModified"
class="w-full h-full"
:src="file.src"
@load="() => revokeURL(file.src)"
alt=""
/>
</div>
<div
v-if="!loading"
:class="
twMerge(
'relative cursor-pointer px-5 z-20 text-center text-white flex flex-col justify-center w-full h-full'
)
"
@click.stop.prevent="() => createPreviewFileURL(file)"
>
<template
v-if="file.type?.match(/pdf/g) || file.ext?.match(/pdf/g)"
>
<font-awesome-icon
class="mx-auto mb-2 text-4xl text-white"
:icon="['fas', 'file-pdf']"
/>
</template>
<template
v-else-if="
file.type?.match(/excel|sheet/g) ||
file.ext?.match(/csv|xls/g)
"
>
<font-awesome-icon
class="mx-auto mb-2 text-4xl text-white"
:icon="['fas', 'file-excel']"
/></template>
<template
v-else-if="
file.type?.match(/plain|word/g) ||
file.ext?.match(/doc|docx/g)
"
>
<font-awesome-icon
class="mx-auto mb-2 text-4xl text-white"
:icon="['fas', 'file-word']"
/></template>
<template
v-else-if="
file.type?.match(/powerpoint/g) || file.ext?.match(/ppt/g)
"
>
<font-awesome-icon
class="mx-auto mb-2 text-4xl text-white"
:icon="['fas', 'file-powerpoint']"
/>
</template>
<template
v-else-if="
!file.type?.match('image/*') &&
!file.ext?.match(/png|jpg|jpeg|gif|bmp/g)
"
>
<font-awesome-icon
class="mx-auto mb-2 text-4xl text-white"
:icon="['fas', 'file-alt']"
/>
</template>
<p class="font-bold mb-3" v-if="file.size">
{{ (file.size / 1000).toFixed(1) }} KB
</p>
<p class="truncate font-extrabold">{{ file.name }}</p>
</div>
<span
v-else
class="loading loading-dots loading-lg text-slate-800"
></span>
</li>
</ul>
</template>
<div class="w-full text-center" v-else>
<div>
<font-awesome-icon size="2x" :icon="['fas', 'cloud-upload-alt']" />
</div>
<p class="text-2xl my-2">選擇一個文件或拖放到這裡</p>
<p class="mb-0 col-grey">檔案不超過 10MB</p>
</div>
</div>
<!-- :accept="acceptFileType.join(',')" -->
<input
type="file"
:name="name || 'file'"
ref="fileInput"
:multiple="multiple"
class="h-0"
@change="uploadFile"
/>
</div>
<div class="">
<span class="label-text-alt"> <slot name="bottomLeft"></slot></span>
<span class="label-text-alt"> <slot name="bottomRight"></slot></span>
</div>
</label>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,282 @@
<script setup>
import {
ref,
onMounted,
defineProps,
computed,
onUnmounted,
watch,
watchEffect,
provide,
inject,
} from "vue";
import { getUrn, getAccessToken } from "@/apis/forge";
import { twMerge } from "tailwind-merge";
import useSystemStatusByBaja from "@/hooks/baja/useSystemStatusByBaja";
import ForgeInfoModal from "./ForgeInfoModal.vue";
import useAlarmStore from "@/stores/useAlarmStore";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const { forgeLock } = inject("app_toggle");
const props = defineProps({
fullScreen: Boolean,
initialData: Object,
cubeStyle: {
type: Object,
default: {
right: 25,
top: 2,
},
},
});
const heat_bar_isShow = ref(false);
const updateHeatBarIsShow = (isShow) => {
heat_bar_isShow.value = isShow;
};
const {
subscribeData,
visibleDbid,
updateDbidPosition,
hideAllObjects,
updateForgeViewer,
forgeViewer,
urn,
loadModel,
updateInitialData,
subComponents,
} = useSystemStatusByBaja(updateHeatBarIsShow);
watch(
() => props.initialData,
(newValue) => {
newValue && updateInitialData(newValue);
},
{
deep: true,
}
);
const store = useAlarmStore();
const subscribeDataWithErrorMsg = computed(() => {
let data = { ...subscribeData.value };
for (let [key, value] of Object.entries(subscribeData.value)) {
const alarm = store.alarmData.find(
({ device_number }) => device_number === key
);
data[key].alarmMsg = alarm ? alarm.msg : "";
}
console.log("baja update data: ", data);
return data;
});
const forgeDom = ref(null);
const initViewer = (container) => {
return new Promise(function (resolve, reject) {
Autodesk.Viewing.Initializer(
{
env: "Local",
language: "en",
},
function () {
const config = {
extensions: [
"Autodesk.DataVisualization",
"Autodesk.DocumentBrowser",
],
};
let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
viewer.start();
resolve(viewer);
}
);
});
};
const initForge = () => {
initViewer(forgeDom.value).then((viewer) => {
const localFilePath =
import.meta.env.MODE === "production"
? `${FILE_BASEURL}/upload/forge/0.svf`
: "/forge/0.svf";
loadModel(viewer, localFilePath).then(() => {
viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
async function () {
console.log(
"Autodesk.Viewing.GEOMETRY_LOADED_EVENT",
viewer.isLoadDone()
);
updateForgeViewer(viewer);
const tree = viewer.model.getData().instanceTree;
hideAllObjects(tree, visibleDbid.value);
// dbid
// viewer.addEventListener(
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
// function (event) {
// console.log(" forge_dbid", event.dbIdArray);
// }
// );
}
);
viewer.addEventListener(
Autodesk.Viewing.CAMERA_CHANGE_EVENT,
function (e) {
viewer.isLoadDone() && updateDbidPosition(this, subscribeData.value);
console.log(
"camera position changed: ",
NOP_VIEWER.navigation.getTarget(),
e.camera.position
);
}
);
});
});
};
onMounted(() => {
console.log("Forge 加載");
initForge();
});
//
const currentInfoModalData = ref(null);
const isMobile = (pointerType) => {
return pointerType !== "mouse"; // is desktop
};
const getCurrentInfoModalData = (e, position, value) => {
const mobile = isMobile(e.pointerType);
currentInfoModalData.value = {
initPos: mobile
? { left: `50%`, top: `50%` }
: { left: `${position.left}px`, top: `${position.top}px` },
value,
isMobile: mobile,
};
forge_info_modal.showModal();
};
watch([forgeViewer, forgeLock], ([newViewer, newLock]) => {
if (newViewer && newLock !== undefined) {
newViewer.setNavigationLock(newLock); //
newViewer.navigation.setZoomTowardsPivot(!newLock); //
newViewer.navigation.setReverseZoomDirection(newLock); //
}
});
onUnmounted(() => {
console.log("Forge 銷毀");
console.log("sub", subComponents);
subComponents.value?.unsubscribeAll();
subComponents.value?.detach();
updateForgeViewer(null);
NOP_VIEWER.tearDown();
});
</script>
<template>
<ForgeInfoModal :data="currentInfoModalData" />
<div
:class="
twMerge(
fullScreen
? 'absolute top-0 left-0 w-screen h-full z-0'
: 'w-full relative'
)
"
>
<div
id="forge-preview"
ref="forgeDom"
:class="
twMerge(
'relative w-full h-full',
fullScreen ? 'min-h-screen ' : 'min-h-[600px]'
)
"
>
<div v-show="heat_bar_isShow" class="absolute z-10 heatbar">
<div class="w-40 flex justify-between text-[10px] mb-1">
<span class="text-gradient-1">-20°C</span>
<span class="text-gradient-2">0°C</span>
<span class="text-gradient-3">20°C</span>
<span class="text-gradient-4">40°C</span>
</div>
<div
class="w-40 h-3"
style="
background: linear-gradient(
to right,
#0000ff 0%,
#00ff00 33%,
#ffff00 66%,
#ff0000 100%
);
"
></div>
</div>
<label
v-for="(value, key) in subscribeDataWithErrorMsg"
:key="key"
:data-dbid="value.forge_dbid"
:class="
twMerge(
`after:border-t-[${value.currentColor}]`,
'flex items-center justify-center h-12 -translate-x-1/2 -translate-y-1/5 absolute z-50 px-5 py-4 text-center rounded-md text-lg border-2 border-white',
'after:absolute after:border-t-[10px] after:border-x-[12px] after:border-x-transparent after:-bottom-[8px] after:left-1/2 after:-translate-x-1/2 ',
'before:absolute before:border-t-[12px] before:border-x-[14px] before:border-x-transparent before:-bottom-[12px] before:left-1/2 before:-translate-x-1/2 before:border-white'
)
"
:style="{
left: `${Math.floor(value.device_coordinate_3d.x)}px`,
top: `${Math.floor(value.device_coordinate_3d.y) - 100}px`,
display: value.is_show,
backgroundColor: value.currentColor,
}"
@click.prevent="
(e) =>
getCurrentInfoModalData(
e,
{ left: e.clientX, top: e.clientY },
value
)
"
>
<span class="mr-2">{{ value.full_name }}</span>
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
<span v-else>{{ value.show_value }}</span>
</label>
</div>
</div>
</template>
<style lang="css">
.adsk-viewing-viewer {
background-color: transparent !important;
}
#guiviewer3d-toolbar {
display: none;
bottom: 200px;
}
.viewcubeWrapper {
right: v-bind("`${props.cubeStyle.right}%`") !important;
top: v-bind("`${props.cubeStyle.top}%`") !important;
}
.homeViewWrapper {
transform: scale(1.5) !important;
}
.heatbar {
right: v-bind("`${props.cubeStyle.right + 2}%`") !important;
top: 0% !important;
}
</style>

View File

@ -0,0 +1,106 @@
<script setup>
import { defineProps, onMounted, ref, watch } from "vue";
import ForgeInfoModalDesktop from "./ForgeInfoModalDesktop.vue";
import ForgeInfoModalCog from "./ForgeInfoModalCog.vue";
import ForgeInfoModalChart from "./ForgeInfoModalChart.vue";
const props = defineProps({
data: Object,
});
const currentTab = ref("desktop");
const tabs = {
desktop: ForgeInfoModalDesktop,
cog: ForgeInfoModalCog,
chart: ForgeInfoModalChart,
};
const changeOpenKey = (key) => {
currentTab.value = key;
};
const onCancel = () => {
forge_info_modal.close();
currentTab.value="desktop";
};
const position = ref({
left: "0px",
top: "0px",
});
watch(
() => props.data,
(newValue) => {
position.value = newValue.initPos;
}
);
</script>
<template>
<Modal
id="forge_info_modal"
:onCancel="onCancel"
width="550"
:draggable="!data?.isMobile"
>
<template #modalContent>
<div class="card bg-transparent text-white">
<div class="card-title py-2 border-b border-zinc-700 justify-between">
<h3>{{ data?.value.full_name }}</h3>
<div>
<Button
type="link"
class="btn-link btn-text-without-border px-2"
@click="() => changeOpenKey('desktop')"
>
<font-awesome-icon
:icon="['fas', 'desktop']"
size="lg"
class="text-[#a5abb1]"
/>
</Button>
<Button
type="link"
class="btn-link btn-text-without-border px-2"
@click="() => changeOpenKey('cog')"
>
<font-awesome-icon
:icon="['fas', 'cog']"
size="lg"
class="text-[#a5abb1]"
/>
</Button>
<Button
type="link"
class="btn-link btn-text-without-border px-2"
@click="() => changeOpenKey('chart')"
>
<font-awesome-icon
:icon="['fas', 'chart-line']"
size="lg"
class="text-[#a5abb1]"
/>
</Button>
<Button
type="link"
class="btn-link btn-text-without-border px-2"
@click="onCancel"
>
<font-awesome-icon
:icon="['fas', 'times']"
size="lg"
class="text-[#a5abb1]"
/>
</Button>
</div>
</div>
<div class="card-body px-0 py-3">
<component :is="tabs[currentTab]" :key="currentTab" :data="data"></component>
</div>
</div>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,219 @@
<script setup>
import { defineProps, onMounted, onUnmounted, ref, nextTick, watch } from "vue";
import { getHistoryData } from "@/apis/history";
import LineChart from "@/components/chart/LineChart.vue";
import { SECOND_CHART_COLOR } from "@/constant";
import dayjs from "dayjs";
const props = defineProps({
data: Object,
});
const pointsList = ref([]);
const timeList = ref([
{ value: 1, name: "1小時" },
{ value: 4, name: "4小時" },
{ value: 8, name: "8小時" },
]);
const chartData = ref([]);
const forge_chart = ref(null);
const loading = ref(false);
//
const defaultChartOption = {
tooltip: {
trigger: "axis",
},
legend: {
data: [],
textStyle: {
color: "#ffffff",
fontSize: 16,
},
},
grid: {
top: "25%",
left: "0%",
right: "0%",
bottom: "0%",
containLabel: true,
},
xAxis: {
type: "category",
splitLine: { show: false },
axisLabel: {
color: "#ffffff",
formatter: (value) => dayjs(value).format("HH:mm"), //
},
data: [],
},
yAxis: {
type: "value",
splitLine: { show: false },
axisLabel: { color: "#ffffff" },
},
series: [],
};
const formState = ref({
Cumulant: 1,
Type: 2,
Points: [],
Start_date: dayjs().format("YYYY-MM-DD"),
Start_time: dayjs().format("HH:00"),
End_date: dayjs().format("YYYY-MM-DD"),
End_time: dayjs().format("HH:00"),
Device_list: [],
});
const updateTimeRange = (hours) => {
const now = dayjs();
const startTime = now.subtract(hours, "hour");
formState.value.Start_date = startTime.format("YYYY-MM-DD");
formState.value.Start_time = startTime.format("HH:00");
formState.value.End_date = now.format("YYYY-MM-DD");
formState.value.End_time = now.format("HH:00");
};
const onSearch = async () => {
loading.value = true;
const res = await getHistoryData(formState.value);
if (res.isSuccess) {
if (res.data.items.length > 0) {
chartData.value = res.data.items
.map((d) => ({
timestamp: d.timestamp,
value: parseFloat(d.value),
}))
.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
//
await nextTick();
if (forge_chart.value?.chart) {
forge_chart.value.chart.setOption({
xAxis: {
data: chartData.value.map((d) => d.timestamp),
},
series: [
{
name: props.data?.value.full_name,
type: "line",
data: chartData.value.map((d) => d.value),
showSymbol: false,
itemStyle: {
color: SECOND_CHART_COLOR[0], // 使
},
},
],
});
}
} else {
chartData.value = [];
if (forge_chart.value?.chart) {
forge_chart.value.chart.clear(); //
}
}
} else {
console.error("API Error:", res.msg);
}
loading.value = false;
};
onMounted(() => {
console.log("Initial data:", props.data.value);
if (props.data?.value.device_number) {
formState.value.Device_list = [props.data.value.device_number];
}
if (props.data?.value.points) {
const filteredKeys = Object.keys(props.data.value.points).filter(
(key) => !["ST", "Type", "Light", "Size"].includes(key)
);
if (props.data?.value.subSys === "Wtr") {
pointsList.value = [
{ name: "Total", value: "Total" },
...filteredKeys.map((key) => ({
name: key,
value: key,
})),
];
formState.value.Cumulant = 2;
} else {
pointsList.value = filteredKeys.map((key) => ({
name: key,
value: key,
}));
formState.value.Cumulant = 1;
}
}
if (pointsList.value.length > 0) {
formState.value.Points = pointsList.value[0].value;
}
if (timeList.value.length > 0) {
formState.value.time = timeList.value[0].value;
updateTimeRange(timeList.value[0].value);
}
onSearch();
});
onUnmounted(() => {
formState.value = {};
chartData.value = [];
});
watch(
() => formState.value.Points,
(newPoints) => {
if (newPoints.includes("Total")) {
formState.value.Cumulant = 2;
} else {
formState.value.Cumulant = 1;
}
},
{ immediate: true }
);
</script>
<template>
<div class="flex items-center gap-4">
<Select
:value="formState"
class=""
selectClass="border-info focus-within:border-info"
name="Points"
Attribute="name"
:options="pointsList"
></Select>
<Select
:value="formState"
class=""
selectClass="border-info focus-within:border-info"
name="time"
Attribute="name"
:options="timeList"
@change="(value) => updateTimeRange(value)"
></Select>
<button class="btn btn-success" @click.stop.prevent="onSearch">
<font-awesome-icon :icon="['fas', 'search']" class="" />搜尋
</button>
</div>
<div class="min-h-[300px] relative">
<span
v-if="loading"
className="loading loading-spinner loading-lg text-info absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-20"
></span>
<LineChart
v-if="chartData.length > 0"
id="forge_chart"
class="min-h-[300px] max-h-fit"
:option="defaultChartOption"
ref="forge_chart"
/>
<p class="text-center text-xl" v-if="!loading && chartData.length === 0">
沒有資料
</p>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,118 @@
<script setup>
import { defineProps, onMounted, onUnmounted, ref } from "vue";
import { getAssetSingle, getAssetFloorList } from "@/apis/asset";
import { getOperationCompanyList } from "@/apis/operation";
import dayjs from "dayjs";
const tableData = ref({});
const floors = ref([]);
const companyOptions = ref([]);
const props = defineProps({
data: Object,
});
const getFloors = async () => {
const res = await getAssetFloorList();
floors.value = res.data[0]?.floors.map((d) => ({ ...d, key: d.floor_guid }));
};
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getCurrentData = async (value) => {
if (value.main_id) {
const res = await getAssetSingle(value.main_id);
if (res.isSuccess) {
tableData.value = {
...res.data,
key: res.data.id,
floor: floors.value.find(
({ floor_guid }) => res.data.floor_guid === floor_guid
)?.full_name,
company: companyOptions.value.find(
({ id }) => res.data.operation_id === id
)?.name,
contact_person: companyOptions.value.find(
({ id }) => res.data.operation_id === id
)?.contact_person,
buying_date: res.data?.buying_date
? dayjs(res.data.buying_date).format("YYYY-MM-DD")
: "",
created_at: res.data?.created_at
? dayjs(res.data.created_at).format("YYYY-MM-DD")
: "",
};
}
}
};
onMounted(async () => {
console.log("Initial data:", props.data.value);
await getFloors();
await getCompany();
getCurrentData(props.data.value);
});
onUnmounted(() => {
tableData.value = {};
});
</script>
<template>
<table class="table">
<tbody>
<tr>
<td>設備編號</td>
<td>{{ tableData.device_number }}</td>
</tr>
<tr>
<td>設備名稱</td>
<td>{{ tableData.full_name }}</td>
</tr>
<tr>
<td>資產編號</td>
<td>{{ tableData.asset_number }}</td>
</tr>
<tr>
<td>設備位置</td>
<td>{{ tableData.floor }}</td>
</tr>
<tr>
<td>圖面標識</td>
<td>{{ tableData.device_coordinate }}</td>
</tr>
<tr>
<td>品牌 / 型號</td>
<td>{{ tableData.brand }} / {{ tableData.device_model }}</td>
</tr>
<tr>
<td>廠商 / 聯絡人</td>
<td>
{{ tableData.company }} /
{{ tableData.contact_person }}
</td>
</tr>
<tr>
<td>購買日期</td>
<td>{{ tableData.buying_date }}</td>
</tr>
<tr>
<td>建立時間</td>
<td>{{ tableData.created_at }}</td>
</tr>
</tbody>
</table>
</template>
<style lang="scss" scoped>
td {
border: 1px solid #ddd
}
td:first-child {
font-weight: bold;
}
</style>

View File

@ -0,0 +1,34 @@
<script setup>
import { defineProps, watch, computed } from "vue";
import { useRoute } from "vue-router";
const route = useRoute();
const props = defineProps({
data: Object,
});
const pxRoute = computed(() =>
route.path === "/dashboard" ? "GraphicM" : "GraphicU"
);
watch(
() => props.data,
(newValue) => {
console.log(newValue, newValue.value.device_number);
}
);
const device_px_route = computed(() =>
props.data?.value.device_number.replaceAll("_", "/")
);
</script>
<template>
<iframe
v-if="data"
:src="`/ord?station:%7Cslot:/${device_px_route}|view:${pxRoute}?fullScreen=true`"
style="width: 500px; height: 350px"
></iframe>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,160 @@
<script setup>
import { defineProps, inject, ref, watch } from "vue";
const props = defineProps({
open: Boolean,
toggleModal: Function,
});
const modalContent = inject("modalContent");
console.log("Modal context", modalContent);
const openkey = ref("leaf");
const changeOpenKey = (val) => {
openkey.value = val;
};
</script>
<template>
<a-modal
:open="open"
:footer="null"
:mask="false"
wrapClassName="forge_modal"
:onCancel="toggleModal"
:destroyOnClose="true"
>
<div class="card bg-transparent text-white">
<h2 class="card-title pb-2 border-b border-zinc-700 justify-between">
<span>{{ modalContent?.full_name }}</span>
<div>
<a-button
type="link"
class="px-1"
@click="() => changeOpenKey('leaf')"
>
<font-awesome-icon
:icon="['fas', 'leaf']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
<a-button
type="link"
class="px-1"
@click="() => changeOpenKey('desktop')"
>
<font-awesome-icon
:icon="['fas', 'desktop']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
<a-button
type="link"
class="px-1"
@click="() => changeOpenKey('cog')"
>
<font-awesome-icon
:icon="['fas', 'cog']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
<a-button
type="link"
class="px-1"
@click="() => changeOpenKey('triangle')"
>
<font-awesome-icon
:icon="['fas', 'exclamation-triangle']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
<a-button
type="link"
class="px-1"
@click="() => changeOpenKey('bars')"
>
<font-awesome-icon
:icon="['fas', 'bars']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
<a-button type="link" class="px-1" @click="props.toggleModal">
<font-awesome-icon
:icon="['fas', 'times']"
size="2x"
class="text-[#a5abb1]"
/>
</a-button>
</div>
</h2>
<div class="card-body px-0">
<ForgeModalContent :openkey="openkey" />
</div>
</div>
</a-modal>
</template>
<style lang="css">
.forge_modal {
position: relative;
}
.forge_modal .ant-modal-close {
display: none;
}
.forge_modal .ant-modal {
border: 1px solid #35eded;
padding: 10px;
margin-bottom: 15px;
background-color: theme("colors.body");
position: absolute;
top: 30%;
left: 40%;
}
.ant-modal-content {
background: linear-gradient(
180deg,
rgba(127, 237, 193, 0.1),
rgba(0, 0, 0, 0),
rgba(127, 237, 193, 0.1)
) !important;
}
.ant-modal-header {
background-color: transparent !important;
}
/**資料框裝飾**/
.forge_modal .ant-modal .ant-modal-content::before {
content: "" !important;
background: url(../../assets/img/table/content-box-background01.svg) center
center !important;
position: absolute !important;
left: 4px !important;
top: 4px !important;
height: 20px !important;
width: 20px !important;
background-repeat: no-repeat !important;
z-index: 1 !important;
}
.forge_modal .ant-modal .ant-modal-content::after {
content: "" !important;
background: url(../../assets/img/table/content-box-background05.svg) center
center !important;
position: absolute !important;
right: 4px !important;
bottom: 4px !important;
height: 20px !important;
width: 20px !important;
background-repeat: no-repeat !important;
z-index: 3 !important;
}
</style>

View File

@ -0,0 +1,223 @@
<script setup>
import { defineProps, inject, ref, watch } from "vue";
const props = defineProps({
openkey: String,
});
const modalContent = inject("modalContent");
const leafColumns = [
{
title: "冷媒 / 製冷劑原始填充量(Kg)",
dataIndex: "oriAmount",
key: "oriAmount",
},
{ title: "使用年度", dataIndex: "usedMonth", key: "usedMonth" },
{ title: "kgCO2e", dataIndex: "kgCO2e", key: "kgCO2e" },
{ title: "處理人員", dataIndex: "operator", key: "operator" },
{ title: "記錄時間", dataIndex: "time", key: "time" },
];
const leafSource = [
{
key: "1",
oriAmount: 100,
usedMonth: "一月",
kgCO2e: 150,
operator: "John",
time: "2023-01-01",
},
{
key: "2",
oriAmount: 200,
usedMonth: "二月",
kgCO2e: 320,
operator: "Mike",
time: "2023-02-01",
},
];
const warningColumns = [
{
title: "異常ID",
dataIndex: "errorCode",
key: "errorCode",
},
{
title: "異常原因",
dataIndex: "errorMsg",
key: "errorMsg",
},
{
title: "ACK 確認",
dataIndex: "ack",
key: "ack",
},
{
title: "發生/ 復歸時間",
dataIndex: "ackTime",
key: "ackTime",
},
];
const warningSource = [
{
key: "1",
errorCode: "7db7187f",
errorMsg: "異常",
ack: "未確認",
ackTime: "2023/12/26 23:39:44",
},
{
key: "2",
errorCode: "943d4f1e",
errorMsg: "異常",
ack: "未確認",
ackTime: "2023/12/26 23:39:44",
},
];
const operationColumns = [
{
title: "類型",
dataIndex: "type",
key: "type",
},
{
title: "項目",
dataIndex: "item",
key: "item",
},
{
title: "處理人員",
dataIndex: "operator",
key: "operator",
},
{
title: "發生/ 完成時間",
dataIndex: "occurTime",
key: "occurTime",
},
];
const operationSource = [
{
key: "1",
type: "保養",
item: "ke_test_保養紀錄",
operator: "webUser",
occurTime: "2023/07/31 11:42:25",
},
{
key: "2",
type: "維修",
item: "空調01",
operator: "郭純胤",
occurTime: "2022/11/17 10:48:12",
},
];
const allCol = {
leaf: leafColumns,
triangle: warningColumns,
bars: operationColumns,
};
const allSource = {
leaf: leafSource,
triangle: warningSource,
bars: operationSource,
};
const columns = ref(allCol.leaf);
const dataSource = ref(allSource.leaf);
watch(
() => props.openkey,
(newVal) => {
if (allCol[newVal]) {
columns.value = allCol[newVal];
dataSource.value = allSource[newVal];
}
}
);
</script>
<template>
<!-- 碳排放 -->
<template v-if="openkey === 'desktop'">
<div>
<iframe
:src="
'/ord?station:%7Cslot:/' +
modalContent?.device_number.replace(/_/g, '/') +
'|view:?fullScreen=true'
"
style="width: 100%; height: 100%"
></iframe>
</div>
</template>
<template v-else-if="openkey === 'cog'">
<table class="border-collapse border border-slate-500">
<tbody>
<tr>
<td class="border border-slate-600 px-3 py-3">設備編號</td>
<td class="border border-slate-600 px-3 py-3">
{{ modalContent?.device_number }}
</td>
</tr>
<tr>
<td class="border border-slate-600 px-3 py-3">設備名稱</td>
<td class="border border-slate-600 px-3 py-3">
{{ modalContent?.full_name }}
</td>
</tr>
</tbody>
</table>
</template>
<template v-else>
<template v-if="openkey === 'leaf'">
<div class="flex flex-wrap flex-row items-center justify-between">
<p>排放量輸入</p>
<a-button
type="link"
class="btn-info text-white btn btn-sm"
><font-awesome-icon :icon="['fas', 'plus']" size="1x" class="text-white me-1"/>Add
</a-button>
</div>
<div class="flex flex-wrap flex-row items-center">
<p><span class="opacity-50">冷媒:</span> R-22</p>
<p><span class="opacity-50">設備逸散率:</span> 0.16</p>
<p><span class="opacity-50">GWP:</span> 1530</p>
</div>
</template>
<div class="content-box forge-modal-table">
<a-table
:columns="columns"
:data-source="dataSource"
:bordered="false"
>
</a-table>
</div>
</template>
</template>
<style lang="css">
/**資料框**/
.content-box.forge-modal-table {
border: none;
background-color: transparent;
}
.content-box.forge-modal-table .ant-table {
border: 1px solid #fff !important;
border-radius: 0;
}
.content-box.forge-modal-table::before,
.content-box.forge-modal-table::after {
background-image: none !important;
}
</style>

View File

@ -0,0 +1,270 @@
<script setup>
import {
ref,
onMounted,
defineProps,
markRaw,
watch,
nextTick,
provide,
} from "vue";
import { getUrn, getAccessToken } from "@/apis/forge";
import useForgeDbIdStore from "@/stores/useForgeDbIdStore";
import ForgeModal from "./ForgeModal.vue";
import useRefrigerantTemp from "@/hooks/baja/useRefrigerantHeatMap";
import { twMerge } from "tailwind-merge";
import hexToRgb from "@/util/hexToRgb"
const props = defineProps({
data: Array,
fullScreen: Boolean,
});
const deviceList = ref([]);
const forgeDom = ref(null);
const forgeViewer = ref(null);
const forgeViewerModel = ref(null);
const urn = ref("");
let allDbIdsStr = [];
const open = ref(false);
const modalContent = ref(null);
provide("modalContent", modalContent);
const moveModal = (elmnt) => {
console.log(elmnt);
var pos1 = 0,
pos2 = 0,
pos3 = 0,
pos4 = 0;
document.querySelector(
".forge_modal .ant-modal-body .card-title"
).onmousedown = dragMouseDown;
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX;
pos4 = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
pos1 = pos3 - e.clientX;
pos2 = pos4 - e.clientY;
pos3 = e.clientX;
pos4 = e.clientY;
// set the element's new position:
elmnt.style.top = elmnt.offsetTop - pos2 + "px";
elmnt.style.left = elmnt.offsetLeft - pos1 + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
};
const toggleModal = async (e) => {
open.value = !open.value;
await nextTick();
if (open.value) {
const elmnt = document.querySelector(".forge_modal .ant-modal");
elmnt.style.top = e.clientY - 20 + "px";
elmnt.style.left = e.clientX + 20 + "px";
moveModal(elmnt);
}
};
const onSpriteClicked = (event) => {
event.hasStopped = true;
const data = deviceList.value.find((d) => d.spriteDbId === event.dbId);
modalContent.value = data;
store.getDbIdStore(data.forge_dbid);
toggleModal(event.originalEvent);
};
// sprites
const createSprites = async (dataVizExtn) => {
if (!dataVizExtn || !props.data || props.data.length === 0) return;
const DataVizCore = Autodesk.DataVisualization.Core;
const viewableType = DataVizCore.ViewableType.SPRITE;
let spriteColor = new THREE.Color(0xffffff);
const BASEURL = import.meta.env.VITE_FORGE_BASEURL;
const spriteIconUrl = `${BASEURL}/hotspot.svg`;
const style = new DataVizCore.ViewableStyle(
viewableType,
spriteColor,
spriteIconUrl
);
const viewableData = new DataVizCore.ViewableData();
viewableData.spriteSize = 24; // Sprites as points of size 24 x 24 pixels
deviceList.value = props.data?.map((myData, index) => {
const dbId = 10 + index;
const position = JSON.parse(myData.device_coordinate_3d);
style.color = new THREE.Color(hexToRgb(myData.device_normal_color));
const viewable = new DataVizCore.SpriteViewable(position, style, dbId);
viewableData.addViewable(viewable);
return {
...myData,
spriteDbId: dbId,
};
});
await viewableData.finish();
dataVizExtn.addViewables(viewableData);
forgeViewer.value.addEventListener(DataVizCore.MOUSE_CLICK, onSpriteClicked);
forgeViewer.value.addEventListener(
Autodesk.Viewing.SELECTION_CHANGED_EVENT,
onSpriteClicked
);
};
const initViewer = (container) => {
return new Promise(function (resolve, reject) {
Autodesk.Viewing.Initializer({ getAccessToken }, function () {
const config = {
extensions: ["Autodesk.DataVisualization", "Autodesk.DocumentBrowser"],
};
let viewer = new Autodesk.Viewing.GuiViewer3D(container, config);
Autodesk.Viewing.Private.InitParametersSetting.alpha = true;
viewer.start();
viewer.setTheme("light-theme");
resolve(viewer);
});
});
};
const loadModel = (viewer, urn) => {
return new Promise(function (resolve, reject) {
async function onDocumentLoadSuccess(doc) {
viewer.setGroundShadow(false);
viewer.impl.renderer().setClearAlpha(0); //clear alpha channel
viewer.impl.glrenderer().setClearColor(0xffffff, 0); //set transparent background, color code does not matter
viewer.impl.invalidate(true); //trigger rendering
const documentNode = await viewer.loadDocumentNode(
doc,
doc.getRoot().getDefaultGeometry()
);
resolve(documentNode);
}
function onDocumentLoadFailure(code, message, errors) {
reject({ code, message, errors });
}
viewer.setLightPreset(0);
Autodesk.Viewing.Document.load(
"urn:" + urn,
onDocumentLoadSuccess,
onDocumentLoadFailure
);
});
};
const hideAllObjects = (viewer, filDbids = []) => {
for (var i = 0; i < allDbIdsStr.length; i++) {
viewer.hide(parseInt(allDbIdsStr[i]));
}
for (var i = 0; i < filDbids.length; i++) {
viewer.show(parseInt(filDbids[i]));
}
viewer.impl.invalidate(true);
};
const initForge = () => {
getUrn().then((res) => {
if (!res.isSuccess) return;
urn.value = res.data[0].urn_3D;
initViewer(forgeDom.value).then((viewer) => {
loadModel(viewer, res.data[0].urn_3D).then(() => {
viewer.addEventListener(
Autodesk.Viewing.GEOMETRY_LOADED_EVENT,
async function () {
forgeViewer.value = markRaw(viewer);
forgeViewerModel.value = viewer.model;
const dataVizExtn = await viewer.loadExtension(
"Autodesk.DataVisualization"
);
createSprites(dataVizExtn);
refrigerantHeatMap.getBasicData(
viewer,
dataVizExtn,
deviceList.value
);
viewer.fitToView([0], viewer.model);
window.setTimeout(() => {
let instanceTree = viewer.model?.getData().instanceTree;
allDbIdsStr = Object.keys(instanceTree.nodeAccess.dbIdToIndex);
const filDbids = deviceList.value.map((x) => x.forge_dbid);
hideAllObjects(forgeViewer.value, filDbids);
}, 500);
}
);
});
});
});
};
onMounted(() => {
initForge();
});
//
const store = useForgeDbIdStore();
const fitToView = () => {
forgeViewer.value?.fitToView(store.dbId);
};
const refrigerantHeatMap = useRefrigerantTemp();
watch(
() => store.dbId,
(newVal) => {
if (forgeViewer.value) {
fitToView();
}
}
);
</script>
<template>
<div
:class="
twMerge(
fullScreen
? 'absolute top-0 left-0 w-screen h-screen z-0'
: 'w-full relative'
)
"
>
<slot name="heat_bar"></slot>
<div
id="forge-preview"
ref="forgeDom"
:class="
twMerge(
'relative w-full max-h-full ',
fullScreen ? 'h-screen' : 'min-h-[600px]'
)
"
></div>
</div>
<ForgeModal :open="open" :toggleModal="toggleModal" />
</template>
<style lang="css">
.adsk-viewing-viewer {
background-color: transparent !important;
}
#guiviewer3d-toolbar {
/* display: none; */
bottom: 200px;
}
</style>

View File

@ -0,0 +1,156 @@
<script setup>
import { onMounted, ref, inject } from "vue";
import NavbarItem from "./NavbarItem.vue";
import NavbarBuilding from "./NavbarBuilding.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import AlarmDrawer from "@/components/alarm/AlarmDrawer.vue";
const user = ref("");
const { forgeLock, toggleForgeLock } = inject("app_toggle");
const store = useUserInfoStore();
onMounted(() => {
const name = store.user.user_name;
if (name) {
user.value = name;
}
});
</script>
<template>
<header class="navbar bg-dark text-success py-2 mb-3 w-full relative z-50">
<div class="navbar-start">
<div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h8m-8 6h16"
/>
</svg>
</div>
<ul
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"
>
<NavbarItem />
</ul>
</div>
<router-link to="/dashboard" class="rounded-lg pl-14 text-2xl flex items-center text-stone-100">
<img src="/logo.png" alt="logo" class="w-12 me-2" />百家珍
</router-link>
<NavbarBuilding class="hidden" />
</div>
<div class="navbar-center hidden lg:flex">
<NavbarItem />
</div>
<div class="navbar-end mr-4">
<ul class="menu-box">
<li>
<button
class="drawer-button flex flex-col justify-center items-center btn-group"
@click="toggleForgeLock"
>
<font-awesome-icon
v-if="forgeLock"
:icon="['fas', 'lock']"
size="2x"
class="text-white menu-icon"
/>
<font-awesome-icon
v-else
:icon="['fas', 'lock-open']"
size="2x"
class="text-white menu-icon"
/>
<span class="text-white">
模型{{ forgeLock ? "鎖定" : "解鎖" }}</span
>
</button>
</li>
<li>
<AlarmDrawer />
</li>
<li>
<div class="dropdown dropdown-bottom dropdown-end">
<button
tabindex="0"
type="link"
class="flex flex-col justify-center items-center btn-group"
>
<font-awesome-icon
:icon="['fas', 'user-circle']"
size="2x"
class="text-white menu-icon"
/>
<span class="text-white"> {{ user || "webUser" }}</span>
</button>
<ul
tabindex="0"
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
>
<li class="text-white">
<a
href="/logout"
class="flex flex-col justify-center items-center"
>登出</a>
</li>
</ul>
</div>
</li>
</ul>
</div>
</header>
</template>
<style lang="css">
.sub-drawer {
@apply bg-dark bg-opacity-80 shadow-xl !important;
}
/**menu**/
.menu-box {
@apply flex justify-center;
position: relative;
z-index: 0;
}
.menu-box::after {
content: "";
position: absolute;
top: -12px;
bottom: 0;
left: 25px;
right: 25px;
margin: auto;
display: block;
width: calc(100% - 50px);
height: 2px;
background-color: #7cedc1;
z-index: -10;
}
.menu-box .btn-group {
background: #111;
padding: 0 5px;
margin: 0 15px;
}
.menu-icon {
width: 40px;
margin: auto;
}
.menu-box .btn-group span {
color: #fff;
display: block;
margin-top: 0px;
}
</style>

View File

@ -0,0 +1,48 @@
<script setup>
import { getBuildings } from "@/apis/building";
import { onMounted, ref } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
// const buildings = ref(null);
// const selectedBuilding = ref(null);
const store = useBuildingStore();
const getBui = async () => {
console.log(store.buildings);
const res = await getBuildings();
store.buildings = res.data;
store.selectedBuilding = res?.data[0];
};
onMounted(() => {
getBui();
});
</script>
<template>
<div class="dropdown dropdown-bottom">
<div
tabindex="0"
role="button"
class="text-white ml-8 text-2xl font-semiLight"
>
{{ store.selectedBuilding?.full_name }}
<font-awesome-icon :icon="['fas', 'angle-down']" />
</div>
<ul
tabindex="0"
class="dropdown-content left-8 translate-y-2 z-[1] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
>
<li
class="text-white my-1 text-base"
v-for="bui in store.buildings"
:key="bui.building_tag"
>
{{ bui.full_name }}
</li>
</ul>
</div>
</template>
<style lang="scss" scoped></style>

View File

@ -0,0 +1,129 @@
<script setup>
import { onMounted, ref, watch, computed } from "vue";
import { AUTHPAGES } from "@/constant";
import { getAuth, getAllSysSidebar } from "@/apis/building";
import useBuildingStore from "@/stores/useBuildingStore";
import useUserInfoStore from "@/stores/useUserInfoStore";
const store = useUserInfoStore();
const buildingStore = useBuildingStore();
const iniFroList = async () => {
const res = await getAuth();
store.updateAuthPage(
res.data.map((d) =>
AUTHPAGES.find(({ authCode }) => authCode === d.authCode)
? {
...d,
...AUTHPAGES.find(({ authCode }) => authCode === d.authCode),
}
: d
)
);
};
const authPages = computed(() =>
store.auth_page.filter(({ showView }) => showView)
);
const open = ref(false);
const getSubMonitorPage = async (building) => {
const res = await getAllSysSidebar();
buildingStore.mainSubSys = res.data.history_Main_Systems;
};
const showDrawer = () => {
getSubMonitorPage();
open.value = true;
};
const onClose = () => {
open.value = false;
};
const navigateToSub = (sub) => {
// console.log("navigateToSub", sub);
// let pageAct = JSON.parse(sessionStorage.getItem("pageAct"));
// pageAct = {
// ...pageAct,
// sysMainTag: sub.main_system_tag,
// sysSubTag: sub.sub_system_tag,
// sysSubName: sub.full_name,
// };
// sessionStorage.setItem("lastPage", "systemMonitor");
// sessionStorage.setItem("pageAct", JSON.stringify(pageAct));
// window.location.href = "/file/index.html";
};
watch(
() => buildingStore.selectedBuilding,
(newVal) => {
if (newVal !== null) {
getSubMonitorPage(newVal.building_tag);
}
}
);
onMounted(() => {
iniFroList();
});
</script>
<template>
<ul class="px-1 menu-box my-2">
<li
v-for="page in authPages"
class="flex flex-col items-center justify-center"
>
<!-- <a
v-if="page.authCode === 'PF1'"
@click="showDrawer"
class="flex flex-col justify-center items-center btn-group text-white"
>
<font-awesome-icon
:icon="['fas', page.icon]"
size="2x"
class="menu-icon"
/>
{{ page.subName }}
</a> -->
<router-link
:to="page.navigate"
type="link"
class="flex flex-col justify-center items-center btn-group text-white"
>
<font-awesome-icon
:icon="['fas', page.icon]"
size="2x"
class="menu-icon"
/>
{{ page.subName }}
</router-link>
</li>
</ul>
<a-drawer
:width="200"
placement="left"
:open="open"
:closable="false"
@close="onClose"
class="sub-drawer"
:maskStyle="{ opacity: 0.5 }"
:bodyStyle="{ paddingLeft: 0, paddingRight: 0 }"
>
<ul>
<li
v-for="sub in buildingStore.subSys"
:key="sub.sub_system_tag"
@click.prevent="() => navigateToSub(sub)"
class="text-xl text-center py-3 hover:bg-black hover:text-info"
>
{{ sub.full_name }}
</li>
</ul>
</a-drawer>
</template>
<style lang="css" scoped>
.router-link-active.router-link-exact-active {
color: #7cedc1;
}
</style>

View File

@ -0,0 +1,127 @@
const transformColumns = (columns) =>
columns.map((col) => ({
...col,
dataIndex: col.key,
width: col.width ?? 120,
align: "center",
}));
const MONTHCOLUMNS = transformColumns([
{ title: "項目", key: "item", width: 190, fixed: true },
{ title: "1月", key: "January" },
{ title: "2月", key: "February" },
{ title: "3月", key: "March" },
{ title: "4月", key: "April" },
{ title: "5月", key: "May" },
{ title: "6月", key: "June" },
{ title: "7月", key: "July" },
{ title: "8月", key: "August" },
{ title: "9月", key: "September" },
{ title: "10月", key: "October" },
{ title: "11月", key: "November" },
{ title: "12月", key: "December" },
]);
const WORKHOURSROW = [
{
key: "Index",
item: "月份",
},
{
key: "WorkerNumber",
item: "員工數",
},
{
key: "WorkDay",
item: "每日每人平均工作時數",
},
{
key: "Scalar",
item: "總工時",
},
{
key: "OverTimeWorkerNumber",
item: "加班員工數",
},
{
key: "OverTimeAverageHourPerDay",
item: "每日每人平均加班時數",
},
{
key: "OverTimeWorkDay",
item: "月加班工作天數",
},
{
key: "OverTimeScalar",
item: "月合計加班時數",
},
{ key: "TotalHours", item: "總工時", readonly: true },
{ key: "KgCO2e", item: "KgCO2e", readonly: true },
{ key: "Description", item: "描述/說明" },
{
item: "使用量佐證文件",
key: "ReferenceFileLink",
},
];
const ELECTRICROW = transformColumns([
{
key: "Index",
title: "月份",
},
{
key: "Peak",
title: "尖峰 / 峰",
},
{
key: "HalfPeak",
title: "半尖峰 / 平",
},
{
key: "SaturdayHalfPeak",
title: "週六半尖峰",
},
{
key: "OffPeak",
title: "離峰 / 谷",
},
{
key: "KgCO2e",
title: "碳排放 KgCO2e",
},
// {
// key: "Elecdeduct1",
// item: "電力扣除額1",
// },
// {
// key: "Elecdeduct2",
// item: "電力扣除額2",
// },
// {
// key: "Scalar",
// item: "總用電量",
// },
// { key: "Description", item: "描述/說明" },
// { key: "KgCO2e", item: "KgCO2e", readonly: true },
// {
// item: "使用量佐證文件",
// key: "ReferenceFileLink",
// },
]);
const REFRIGERANTCOLUMNS = transformColumns([
{ title: "廠區 / 製程別", key: "ProcessName" },
{ title: "負責單位", key: "ResponsibleUnit" },
{ title: "設備名稱", key: "Name" },
{ title: "型號", key: "ModelNumber" },
{ title: "使用冷媒 / 製冷劑種類 ", key: "ParameterIDTitle" },
{ title: "全廠台數", key: "TotalNumber" },
{ title: "冷媒 / 製冷劑原始填充量(Kg) ", key: "Scalar" },
{ title: "使用月數", key: "UsedMonth" },
{ title: "設備類型(排放因子) ", key: "ParameterID2Title" },
{ title: "GWP", key: "GWP" },
{ title: "設備逸散率", key: "factor" },
{ title: "KgCO2e", key: "KgCO2e" },
]);
export { MONTHCOLUMNS, WORKHOURSROW, ELECTRICROW, REFRIGERANTCOLUMNS };

8
src/constant/api_app.js Normal file
View File

@ -0,0 +1,8 @@
const BASEURL = import.meta.env.VITE_API_BASEURL;
export const POST_LOGIN = `${BASEURL}/api/Login/`;
export const GET_AUTHPAGE_API = `${BASEURL}/api/GetUsrFroList`;
export const GET_SUBAUTHPAGE_API = `${BASEURL}/api/Device/GetMainSub`;
export const GET_DEVICELIST_API = `${BASEURL}/api/Device/GetDeviceList`;
export const GET_DEVICEIMME_API = `${BASEURL}/api/Energe/GetElecBySubSysTag`;

View File

@ -0,0 +1,4 @@
const BASEURL = import.meta.env.VITE_API_BASEURL;
export const GET_FORGETOKEN_API = `${BASEURL}/api/forge/oauth/token`;
export const GET_FORGEURN_API = `${BASEURL}/api/Device/GetBuild`;

61
src/constant/authPage.js Normal file
View File

@ -0,0 +1,61 @@
export const AUTHPAGES = [
{
authCode: "PF1",
icon: "tv",
// pageName: "systemMonitor",
navigate: "/dashboard",
},
{
authCode: "PF2",
icon: "chart-pie",
pageName: "energyManagement",
navigate: "/energyManagement",
},
{
authCode: "PF3",
icon: "chart-area",
// pageName: "historyData",
navigate: "/historyData",
},
{
authCode: "PF4",
icon: "chart-line",
navigate: "/historyData",
},
{
authCode: "PF5",
icon: "bell",
pageName: "alert",
navigate: "/alert",
},
{
authCode: "PF6",
icon: "server",
pageName: "operation",
navigate: "/operation",
},
{
authCode: "PF7",
icon: "image",
pageName: "graphManagement",
navigate: "/graphManagement",
},
{
authCode: "PF8",
icon: "user",
pageName: "accountManagement",
navigate: "/accountManagement",
},
{
authCode: "PF9",
icon: "database",
pageName: "AssetManagement",
navigate: "/assetManagement",
},
{
authCode: "PF10",
icon: "leaf",
pageName: "ProductSetting",
navigate: "/productSetting",
},
];

View File

@ -0,0 +1,465 @@
// import CapitalGood from "@ASSET/icon/CapitalGood.svg"; // 上游購買資本物品
// import CapitalGoodHover from "@ASSET/icon/CapitalGood-h.svg";
// import upstreamEnergyEmission from "@ASSET/icon/UpstreamEnergyEmission.svg"; // 與能源相關活動
// import upstreamEnergyEmissionHover from "@ASSET/icon/UpstreamEnergyEmission-h.svg";
// import Energy from "@ASSET/icon/Energy.svg"; // 外購能源排放
// import EnergyHover from "@ASSET/icon/Energy-h.svg";
// import Disposal from "@ASSET/icon/Disposal.svg"; // 上游廢棄
// import DisposalHover from "@ASSET/icon/Disposal-h.svg";
// import LeasedAsset from "@ASSET/icon/LeasedAsset.svg"; // 上游租賃資產
// import LeasedAssetHover from "@ASSET/icon/LeasedAsset-h.svg";
// import PurchasedGood from "@ASSET/icon/PurchasedGood.svg"; // 購買商品
// import PurchasedGoodHover from "@ASSET/icon/PurchasedGood-h.svg";
// import Consultant from "@ASSET/icon/Consultant.svg"; // 購買服務
// import ConsultantHover from "@ASSET/icon/Consultant-h.svg";
// import UpstreamTransport from "@ASSET/icon/UpstreamTransport.svg"; // 上游運輸及配送
// import UpstreamTransportHover from "@ASSET/icon/UpstreamTransport-h.svg";
// import BusinessTravel from "@ASSET/icon/BusinessTravel.svg"; // 商務旅行
// import BusinessTravelHover from "@ASSET/icon/BusinessTravel-h.svg";
// import Visitor from "@ASSET/icon/Visitor.svg"; // 訪客
// import VisitorHover from "@ASSET/icon/Visitor-h.svg";
// import Commuting from "@ASSET/icon/Commuting.svg"; // 員工通勤
// import CommutingHover from "@ASSET/icon/Commuting-h.svg";
// import StationaryCombustion from "@ASSET/icon/StationaryCombustion.svg"; // 固定源
// import StationaryCombustionHover from "@ASSET/icon/StationaryCombustion-h.svg";
// import MobileCombustion from "@ASSET/icon/MobileCombustion.svg"; // 移動源
// import MobileCombustionHover from "@ASSET/icon/MobileCombustion-h.svg";
// import DirectFugitiveEmission from "@ASSET/icon/DirectFugitiveEmission.svg"; // 逸散源
// import DirectFugitiveEmissionHover from "@ASSET/icon/DirectFugitiveEmission-h.svg";
// import DirectProcessEmission from "@ASSET/icon/DirectProcessEmission.svg"; // 製程
// import DirectProcessEmissionHover from "@ASSET/icon/DirectProcessEmission-h.svg";
// import Electricity from "@ASSET/icon/Electricity.svg"; // 用電量
// import ElectricityHover from "@ASSET/icon/Electricity-h.svg";
// import Steam from "@ASSET/icon/Steam.svg"; // 蒸氣
// import SteamHover from "@ASSET/icon/Steam-h.svg";
// import Refrigerant from "@ASSET/icon/Refrigerant.svg"; // 冷媒
// import RefrigerantHover from "@ASSET/icon/Refrigerant-h.svg";
// import OtherCompound from "@ASSET/icon/OtherCompound.svg"; // 其他關注類
// import OtherCompoundHover from "@ASSET/icon/OtherCompound-h.svg";
// import UseEmission from "@ASSET/icon/UseEmission.svg"; // 下游使用銷售產品
// import UseEmissionHover from "@ASSET/icon/UseEmission-h.svg";
// import DownstreamDisposal from "@ASSET/icon/DownstreamDisposal.svg"; // 下銷售產品廢棄處理
// import DownstreamDisposalHover from "@ASSET/icon/DownstreamDisposal-h.svg";
// import Investment from "@ASSET/icon/Investment.svg"; // 投資
// import InvestmentHover from "@ASSET/icon/Investment-h.svg";
// import Other from "@ASSET/icon/Other.svg"; // 投資
// import OtherHover from "@ASSET/icon/Other-h.svg";
// import WaterUsages from "@ASSET/icon/WaterUsages.png"; // 水
// import WaterUsagesHover from "@ASSET/icon/WaterUsagesHover.png";
import {
MONTHCOLUMNS,
WORKHOURSROW,
ELECTRICROW,
REFRIGERANTCOLUMNS,
} from "./CalculateTableColumn";
const alterSourceIcon = (sourceList) =>
sourceList.map(({ link, icon, hoverIcon, title, children = null }) => ({
icon,
hoverIcon,
title,
key: link,
link,
children,
}));
const sourceIconListForUpstreamS3 = alterSourceIcon([
{
icon: "CapitalGood",
hoverIcon: "CapitalGood-h",
link: "capitalGood",
title: "購買資本物品 C4",
children: [
{
title: "購買資本物品 C4",
editType: "lifecycle",
link: "capitalGood",
},
],
},
{
icon: "UpstreamEnergyEmission",
hoverIcon: "UpstreamEnergyEmission-h",
link: "upstreamEmissions",
title: "輸入能源上游排放 C4",
children: [
{
title: "輸入能源上游排放 C4",
editType: "lifecycle",
link: "upstreamEmissions",
},
],
},
{
icon: "WaterUsages",
hoverIcon: "WaterUsages-h",
link: "waterUsage",
title: "用水(水資源管理) C4",
children: [
{
title: "用水(水資源管理) C4",
editType: "lifecycle",
link: "waterUsage",
},
],
},
{
icon: "PurchasedGood",
hoverIcon: "PurchasedGood-h",
link: "purchasedGood",
title: "購買商品 C4",
children: [
{
title: "購買商品 C4",
editType: "lifecycle",
link: "purchasedGood",
},
],
},
{
icon: "LeasedAsset",
hoverIcon: "LeasedAsset-h",
link: "leasedAsset",
title: "上游租賃資產 C4",
children: [
{
title: "上游租賃資產 C4",
editType: "lifecycle",
link: "leasedAsset",
},
],
},
{
icon: "Consultant",
hoverIcon: "Consultant-h",
link: "consultant",
title: "顧問諮詢、清潔、維護等 C4",
children: [
{
title: "顧問諮詢、清潔、維護等 C4",
editType: "lifecycle",
link: "consultant",
},
],
},
]);
const sourceIconListForBusinessS2UP = alterSourceIcon([
{
icon: "UpstreamTransport",
hoverIcon: "UpstreamTransport-h",
link: "upstreamTransport",
title: "上游運輸及配送 C3",
children: [
{
title: "上游運輸及配送 C3",
editType: "lifecycle",
link: "upstreamTransport",
},
],
},
]);
const sourceIconListForBusinessS1 = alterSourceIcon([
{
icon: "MobileCombustion",
hoverIcon: "MobileCombustion-h",
link: "mobileCombustion",
title: "移動源 C1",
children: [
{
title: "移動源 C1",
editType: "yearlyDevice",
link: "mobileCombustion",
},
],
},
{
icon: "StationaryCombustion",
hoverIcon: "StationaryCombustion-h",
link: "stationaryCombustion",
title: "固定燃燒源 C1",
children: [
{
title: "固定燃燒源 C1",
editType: "yearlyDevice",
link: "stationaryCombustion",
},
],
},
{
icon: "DirectProcessEmission",
hoverIcon: "DirectProcessEmission-h",
link: "directProcessEmission",
title: "工業製程 C1",
children: [
{
title: "工業製程 C1",
editType: "yearlyDevice",
link: "directProcessEmission",
},
],
},
{
icon: "DirectFugitiveEmission",
hoverIcon: "DirectFugitiveEmission-h",
link: "directFugitiveEmission",
title: "人為逸散 C1",
children: [
{
title: "冷媒設備 B.2.2.d",
editType: "nonYearlyDevice",
link: "refrigerant",
cols: REFRIGERANTCOLUMNS,
main_system_tag: "ME",
sub_system_tag: "M10",
},
{
title: "工時計算 B.2.2.d",
editType: "lifecycle",
link: "workHour",
rows: WORKHOURSROW,
cols: MONTHCOLUMNS,
data_api: "./mock/workhour.json",
},
{
title: "消防設備 B.2.2.d",
editType: "nonYearlyDevice",
link: "fireEquipment",
},
],
},
{
icon: "OtherCompound",
hoverIcon: "OtherCompound-h",
link: "otherCompound",
title: "其他關注類物質 C1",
children: [
{
title: "其他關注類物質 C1",
editType: "nonYearlyDevice",
link: "otherCompound",
},
],
},
{
icon: "Electricity",
hoverIcon: "Electricity-h",
link: "electricity",
title: "輸入電力 C2",
children: [
{
title: "一般用電 B.3.2.a",
editType: "month",
link: "plus",
rows: ELECTRICROW,
cols: MONTHCOLUMNS,
main_system_tag: "EE",
sub_system_tag: "E4",
data_api: "./mock/electricity.json",
},
// {
// title: "綠電 B.3.2.a",
// editType: "month",
// link: "minus",
// rows: ELECTRICROW,
// cols: MONTHCOLUMNS,
// },
],
},
{
icon: "Steam",
hoverIcon: "Steam-h",
link: "steam",
title: "輸入蒸汽 C2",
children: [
{
title: "蒸氣加項 B.3.2.b",
editType: "month",
link: "plus",
},
{
title: "蒸氣減項 B.3.2.b",
editType: "month",
link: "minus",
},
],
},
{
icon: "Visitor",
hoverIcon: "Visitor-h",
link: "visitor",
title: "客戶和訪客運輸 C3",
children: [
{
title: "客戶和訪客運輸 C3",
editType: "lifecycle",
link: "visitor",
},
],
},
{
icon: "BusinessTravel",
hoverIcon: "BusinessTravel-h",
link: "businessTravel",
title: "員工差旅 C3",
children: [
{
title: "員工差旅 C3",
editType: "lifecycle",
link: "businessTravel",
},
],
},
{
icon: "Commuting",
hoverIcon: "Commuting-h",
link: "commuting",
title: "員工通勤 C3",
children: [
{
title: "員工通勤 C3",
editType: "lifecycle",
link: "commuting",
},
],
},
{
icon: "Disposal",
hoverIcon: "Disposal-h",
link: "disposal",
title: "營運產生之廢棄物 C4",
children: [
{
title: "營運產生之廢棄物 C4",
editType: "lifecycle",
link: "disposal",
},
],
},
]);
const sourceIconListForBusinessS2Down = alterSourceIcon([
{
icon: "UpstreamTransport",
hoverIcon: "UpstreamTransport-h",
link: "downstreamTransport",
title: "下游運輸及配送 C3",
children: [
{
title: "下游運輸及配送 C3",
editType: "lifecycle",
link: "downstreamTransport",
},
],
},
{
icon: "UpstreamTransport",
hoverIcon: "UpstreamTransport-h",
link: "disposalDownTransport",
title: "廢棄物運輸 C3",
children: [
{
title: "廢棄物運輸 C3",
editType: "lifecycle",
link: "disposalDownTransport",
},
],
},
]);
const sourceIconListForDownstreamS3 = alterSourceIcon([
{
icon: "LeasedAsset",
hoverIcon: "LeasedAsset-h",
link: "downLeasedAsset",
title: "下游租賃資產 C5",
children: [
{
title: "下游租賃資產 C5",
editType: "lifecycle",
link: "downLeasedAsset",
},
],
},
{
icon: "UseEmission",
hoverIcon: "UseEmission-h",
link: "useEmission",
title: "產品使用階段 C5",
children: [
{
title: "產品使用階段 C5",
editType: "lifecycle",
link: "useEmission",
},
],
},
{
icon: "Investment",
hoverIcon: "Investment-h",
link: "investment",
title: "投資 C5",
children: [
{
title: "投資 C5",
editType: "lifecycle",
link: "investment",
},
],
},
{
icon: "DownstreamDisposal",
hoverIcon: "DownstreamDisposal-h",
link: "downstreamDisposal",
title: "產品壽命終止階段 C5",
children: [
{
title: "產品壽命終止階段 C5",
editType: "lifecycle",
link: "downstreamDisposal",
},
],
},
]);
const sourceIconListForOtherS3 = alterSourceIcon([
{
icon: "Other",
hoverIcon: "Other-h",
link: "other",
title: "其它間接排放 C6",
children: [
{
title: "其它間接排放 C6",
editType: "lifecycle",
link: "other",
},
],
},
]);
const allSourceIconList = [
...sourceIconListForUpstreamS3,
...sourceIconListForBusinessS1,
...sourceIconListForBusinessS2UP,
...sourceIconListForBusinessS2Down,
...sourceIconListForDownstreamS3,
...sourceIconListForOtherS3,
];
export {
sourceIconListForUpstreamS3,
sourceIconListForBusinessS1,
sourceIconListForBusinessS2UP,
sourceIconListForBusinessS2Down,
sourceIconListForDownstreamS3,
sourceIconListForOtherS3,
allSourceIconList,
};

32
src/constant/colors.js Normal file
View File

@ -0,0 +1,32 @@
export const COLOR = [
"#4cf3e9", // 亮藍
"#f5d54e", // 黃
"#63ed84", // 亮綠
"#9afed8", // 淺綠
"#e266fe", // 紫
"#e0e2e2", // 灰
"#500080", // 紫
"#115852", // 深綠
"#4B4E6C", // 灰
"#eb4c42", // 紅
"#007ba7", // 藍
"#da3287", // 粉紅
"#ccff00", // 綠
"#fff44f", // 黃
];
// 淺色
export const CHART_COLOR = [
"#cb4154", // 紅
"#fad6a5", // 黃
"#ace1af", // 綠
"#9bddff", // 亮藍
];
// 深色
export const SECOND_CHART_COLOR = [
"#cc0000", // 紅
"#ffb300", // 黃
"#00cc99", // 綠
"#4997d0", // 藍
];

Some files were not shown because too many files have changed in this diff Show More