init
3
.env.development
Normal 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
@ -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
@ -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
@ -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/*
|
26
index.html
Normal 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
45
package.json
Normal 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
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: {
|
||||
"postcss-import": {},
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
};
|
BIN
public/background.jpg
Normal file
After Width: | Height: | Size: 337 KiB |
21
public/hotspot.svg
Normal 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
After Width: | Height: | Size: 24 KiB |
80
public/logo.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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`;
|
67
src/apis/building/index.js
Normal 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,
|
||||
});
|
||||
};
|
9
src/apis/dashboard/api.js
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
export const POST_LOGIN = `/api/LoginV2/`;
|
23
src/apis/login/index.js
Normal 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
@ -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
@ -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,
|
||||
});
|
||||
};
|
4
src/apis/productSetting/api.js
Normal 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`;
|
45
src/apis/productSetting/index.js
Normal 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
@ -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
@ -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);
|
||||
}
|
BIN
src/assets/img/background.jpg
Normal file
After Width: | Height: | Size: 6.3 KiB |
14
src/assets/img/pagination/small-btn.svg
Normal 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 |
9
src/assets/img/pagination/small-btn02.svg
Normal 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 |
44
src/assets/img/table/content-box-background01.svg
Normal 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 |
43
src/assets/img/table/content-box-background02.svg
Normal 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 |
43
src/assets/img/table/content-box-background03.svg
Normal 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 |
38
src/assets/img/table/content-box-background04.svg
Normal 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 |
44
src/assets/img/table/content-box-background05.svg
Normal 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 |
18
src/assets/img/table/large-btn.svg
Normal 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 |
14
src/assets/img/table/large-btn02.svg
Normal 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 |
14
src/assets/img/table/small-btn.svg
Normal 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 |
9
src/assets/img/table/small-btn02.svg
Normal 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
@ -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
@ -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
@ -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
@ -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
@ -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;
|
||||
}
|
32
src/components/Loading.vue
Normal 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>
|
44
src/components/SvgIcon.vue
Normal 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>
|
||||
|
75
src/components/alarm/AlarmCards.vue
Normal 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>
|
74
src/components/alarm/AlarmDrawer.vue
Normal 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>
|
31
src/components/chart/BarChart.vue
Normal 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>
|
75
src/components/chart/EffectScatter.vue
Normal 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>
|
34
src/components/chart/GaugeChart.vue
Normal 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>
|
34
src/components/chart/LineChart.vue
Normal 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>
|
119
src/components/chart/LiquidfillChart.vue
Normal 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>
|
31
src/components/chart/SankeyChart.vue
Normal 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>
|
52
src/components/customUI/ButtonConnectedGroup.vue
Normal 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>
|
43
src/components/customUI/ButtonGroup.vue
Normal 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>
|
65
src/components/customUI/Checkbox.vue
Normal 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>
|
72
src/components/customUI/Collapse.vue
Normal 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>
|
124
src/components/customUI/DateGroup.vue
Normal 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>
|
29
src/components/customUI/Dropdown.vue
Normal 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>
|
189
src/components/customUI/FileSystemCollapse.vue
Normal 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>
|
96
src/components/customUI/Input.vue
Normal 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>
|
92
src/components/customUI/InputNumber.vue
Normal 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>
|
94
src/components/customUI/Modal.vue
Normal 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>
|
192
src/components/customUI/Pagination.vue
Normal 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>
|
56
src/components/customUI/RadioGroup.vue
Normal 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>
|
85
src/components/customUI/Select.vue
Normal 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>
|
381
src/components/customUI/Table.vue
Normal 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>
|
30
src/components/customUI/Textarea.vue
Normal 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>
|
73
src/components/customUI/Toast.vue
Normal 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>
|
287
src/components/customUI/Upload.vue
Normal 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>
|
282
src/components/forge/Forge.vue
Normal 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>
|
106
src/components/forge/ForgeInfoModal.vue
Normal 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>
|
219
src/components/forge/ForgeInfoModalChart.vue
Normal 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>
|
118
src/components/forge/ForgeInfoModalCog.vue
Normal 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>
|
34
src/components/forge/ForgeInfoModalDesktop.vue
Normal 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>
|
160
src/components/forge/ForgeModal.vue
Normal 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>
|
223
src/components/forge/ForgeModalContent.vue
Normal 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>
|
270
src/components/forge/index.vue
Normal 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>
|
156
src/components/navbar/Navbar.vue
Normal 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>
|
48
src/components/navbar/NavbarBuilding.vue
Normal 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>
|
129
src/components/navbar/NavbarItem.vue
Normal 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>
|
127
src/constant/CalculateTableColumn.js
Normal 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
@ -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`;
|
||||
|
||||
|
4
src/constant/api_forge.js
Normal 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
@ -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",
|
||||
},
|
||||
];
|
465
src/constant/calculateIcon.js
Normal 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
@ -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", // 藍
|
||||
];
|