設備卡片處理

This commit is contained in:
JouChun 2024-10-12 09:59:46 -04:00
parent f5c7a27cd6
commit ba16a89ace
20 changed files with 386 additions and 74 deletions

View File

@ -39,7 +39,7 @@ provide("app_toast", { openToast });
/> />
<div v-if="store.user.token" class="min-h-screen"> <div v-if="store.user.token" class="min-h-screen">
<Navbar /> <Navbar />
<div class="mt-6 px-8 w-full relative app-container"> <div class="mt-6 px-10 w-full relative app-container">
<RouterView /> <RouterView />
</div> </div>
</div> </div>

2
src/apis/system/api.js Normal file
View File

@ -0,0 +1,2 @@
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;

32
src/apis/system/index.js Normal file
View File

@ -0,0 +1,32 @@
import { GET_SYSTEM_FLOOR_LIST_API, GET_SYSTEM_DEVICE_LIST_API } from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export const getSystemFloors = async (building_tag, sub_system_tag) => {
const res = await instance.post(GET_SYSTEM_FLOOR_LIST_API, {
building_tag,
sub_system_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getSystemDevices = async ({
sub_system_tag,
building_tag,
floor_tag,
}) => {
const res = await instance.post(GET_SYSTEM_DEVICE_LIST_API, {
sub_system_tag,
building_tag,
floor_tag,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -0,0 +1 @@
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12.66 12.66"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#35eded;}.cls-3{fill:#ffe448;}</style></defs><g class="cls-1"><path class="cls-2" d="M4.89.76a5.87,5.87,0,0,0-1.48.62L2.87.84l-2,2,.54.54A5.87,5.87,0,0,0,.76,4.89H0V6.14H2.25A4.07,4.07,0,0,1,6.14,2.25V0H4.89Z"/><path class="cls-2" d="M10.41,6.51a4.09,4.09,0,0,1-3.9,3.9v2.25H7.76v-.77a5.57,5.57,0,0,0,1.49-.61l.54.54,2-2-.54-.54a5.57,5.57,0,0,0,.61-1.49h.77V6.51Z"/></g><g class="cls-1"><path class="cls-3" d="M2.25,6.51H0V7.76H.76a5.94,5.94,0,0,0,.62,1.49l-.54.54,2,2,.54-.54a5.5,5.5,0,0,0,1.48.61v.77H6.14V10.41A4.08,4.08,0,0,1,2.25,6.51Z"/><path class="cls-3" d="M11.89,4.89a5.5,5.5,0,0,0-.61-1.48l.54-.54-2-2-.54.54A5.94,5.94,0,0,0,7.76.76V0H6.51V2.25a4.08,4.08,0,0,1,3.9,3.89h2.25V4.89Z"/></g></svg>

After

Width:  |  Height:  |  Size: 850 B

View File

@ -0,0 +1 @@
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 13.73 25.44"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#64ed81;}</style></defs><g class="cls-1"><path class="cls-2" d="M2.52,4.79A2.4,2.4,0,0,1,2.52,0,2.42,2.42,0,0,1,4.84,1.79h8.89V3H4.84A2.42,2.42,0,0,1,2.52,4.79Zm0-4.54A2.15,2.15,0,1,0,4.61,2.84l0-.09h8.84V2H4.64l0-.1A2.15,2.15,0,0,0,2.52.25Zm0,3.58A1.44,1.44,0,1,1,4,2.39,1.44,1.44,0,0,1,2.52,3.83Zm0-2.62A1.19,1.19,0,1,0,3.71,2.39,1.18,1.18,0,0,0,2.52,1.21Z"/><path class="cls-2" d="M2.48,11.67a2.4,2.4,0,1,1,2.32-3h8.9V9.88H4.8A2.41,2.41,0,0,1,2.48,11.67Zm0-4.54a2.15,2.15,0,1,0,2.1,2.6l0-.1h8.85V8.92H4.6l0-.09A2.16,2.16,0,0,0,2.48,7.13Zm0,3.59A1.44,1.44,0,1,1,3.92,9.28,1.44,1.44,0,0,1,2.48,10.72Zm0-2.63A1.19,1.19,0,1,0,3.67,9.28,1.18,1.18,0,0,0,2.48,8.09Z"/><path class="cls-2" d="M2.43,18.55a2.39,2.39,0,1,1,2.31-3h8.9v1.2H4.74A2.4,2.4,0,0,1,2.43,18.55Zm0-4.53a2.14,2.14,0,1,0,2.09,2.59l0-.1h8.85v-.7H4.54l0-.1A2.15,2.15,0,0,0,2.43,14Zm0,3.58a1.44,1.44,0,1,1,1.44-1.44A1.45,1.45,0,0,1,2.43,17.6Zm0-2.63a1.19,1.19,0,0,0,0,2.38,1.19,1.19,0,1,0,0-2.38Z"/><path class="cls-2" d="M2.39,25.44a2.4,2.4,0,1,1,2.32-3H13.6v1.21H4.71A2.41,2.41,0,0,1,2.39,25.44Zm0-4.54a2.15,2.15,0,1,0,2.1,2.59l0-.09h8.84v-.71H4.51l0-.1A2.15,2.15,0,0,0,2.39,20.9Zm0,3.58A1.44,1.44,0,1,1,3.83,23,1.43,1.43,0,0,1,2.39,24.48Zm0-2.63A1.19,1.19,0,1,0,3.58,23,1.19,1.19,0,0,0,2.39,21.85Z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg id="圖層_1" data-name="圖層 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44.63 9.62"><defs><style>.cls-1{opacity:0.8;}.cls-2{fill:#35eded;}.cls-3{fill:#64ed81;}</style></defs><g class="cls-1"><rect class="cls-2" y="7.2" width="44.63" height="0.5"/></g><rect class="cls-3" x="34.71" y="4.68" width="9.62" height="0.25"/><rect class="cls-3" x="39.4" width="0.25" height="9.62"/></svg>

After

Width:  |  Height:  |  Size: 397 B

View File

@ -0,0 +1,7 @@
<?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 16.1 11.6" style="enable-background:new 0 0 16.1 11.6;" xml:space="preserve">
<polygon class="st0" fill="currentColor" points="0,11.6 10.3,11.6 16.1,5.8 10.3,0 0,0 5.8,5.8 "/>
</svg>

After

Width:  |  Height:  |  Size: 460 B

View File

@ -7,28 +7,22 @@ const props = defineProps({
withLine: Boolean, withLine: Boolean,
// this is for change active button // this is for change active button
onclick: Function, onclick: Function,
className: String
}); });
</script> </script>
<template> <template>
<div class="flex flex-wrap text-white items-center"> <div class="flex flex-wrap text-white items-center">
<button <button v-for="item in items" :class="twMerge(
v-for="item in items" 'btn my-1',
:class=" item.active ? 'btn-success' : 'btn-outline-success',
twMerge( withLine ? 'line' : 'mx-2 first:ml-0 ',
'btn my-1', className
item.active ? 'btn-success' : 'btn-outline-success', )
withLine ? 'line' : 'mx-2 first:ml-0 ' " :disabled="item.disabled" :key="item.key" @click.stop.prevent="(e) => {
)
"
:disabled="item.disabled"
:key="item.key"
@click.stop.prevent="
(e) => {
item.onClick ? item.onClick(e, item) : onclick(e, item); item.onClick ? item.onClick(e, item) : onclick(e, item);
} }
" ">
>
<slot name="buttonContent" v-bind="{ record: item }"> <slot name="buttonContent" v-bind="{ record: item }">
{{ item.title }} {{ item.title }}
</slot> </slot>

View File

@ -19,7 +19,7 @@ const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
</script> </script>
<template> <template>
<header class="navbar bg-dark text-success py-2 mb-3 w-full relative z-50"> <header class="px-10 navbar bg-dark text-success py-2 mb-3 w-full relative z-50">
<div class="navbar-start"> <div class="navbar-start">
<div class="dropdown"> <div class="dropdown">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden"> <div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
@ -44,7 +44,7 @@ const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
<NavbarItem /> <NavbarItem />
</ul> </ul>
</div> </div>
<router-link to="/dashboard" class="rounded-lg pl-14 text-xl"> <router-link to="/dashboard" class="rounded-lg text-xl">
<img :src="src" alt="logo" class="w-52" /> <img :src="src" alt="logo" class="w-52" />
</router-link> </router-link>
<NavbarBuilding /> <NavbarBuilding />

View File

@ -16,9 +16,9 @@ const iniFroList = async () => {
res.data.map((d) => res.data.map((d) =>
AUTHPAGES.find(({ authCode }) => authCode === d.authCode) AUTHPAGES.find(({ authCode }) => authCode === d.authCode)
? { ? {
...d, ...d,
...AUTHPAGES.find(({ authCode }) => authCode === d.authCode), ...AUTHPAGES.find(({ authCode }) => authCode === d.authCode),
} }
: d : d
) )
); );
@ -42,6 +42,8 @@ const onClose = () => {
}; };
const navigateToSub = (sub) => { const navigateToSub = (sub) => {
// buildingStore.selectedSystem = sub;
onClose()
// console.log("navigateToSub", sub); // console.log("navigateToSub", sub);
// let pageAct = JSON.parse(sessionStorage.getItem("pageAct")); // let pageAct = JSON.parse(sessionStorage.getItem("pageAct"));
// pageAct = { // pageAct = {
@ -71,67 +73,38 @@ onMounted(() => {
<template> <template>
<ul class="px-1 menu-box my-2"> <ul class="px-1 menu-box my-2">
<li class="flex flex-col items-center justify-center"> <li class="flex flex-col items-center justify-center">
<router-link <router-link :to="{ name: 'dashboard' }" class="flex flex-col justify-center items-center btn-group text-white">
:to="{ name: 'dashboard' }" <font-awesome-icon :icon="['fas', 'home']" size="2x" class="menu-icon" />
class="flex flex-col justify-center items-center btn-group text-white"
>
<font-awesome-icon
:icon="['fas', 'home']"
size="2x"
class="menu-icon"
/>
首頁 首頁
</router-link> </router-link>
</li> </li>
<li <li v-for="page in authPages" class="flex flex-col items-center justify-center">
v-for="page in authPages" <a v-if="page.authCode === 'PF1'" @click="showDrawer"
class="flex flex-col items-center justify-center" class="flex flex-col justify-center items-center btn-group text-white cursor-pointer">
> <font-awesome-icon :icon="['fas', page.icon]" size="2x" class="menu-icon" />
<a
v-if="page.authCode === 'PF1'"
@click="showDrawer"
class="flex flex-col justify-center items-center btn-group text-white cursor-pointer"
>
<font-awesome-icon
:icon="['fas', page.icon]"
size="2x"
class="menu-icon"
/>
{{ page.subName }} {{ page.subName }}
</a> </a>
<router-link <router-link v-else :to="page.navigate" type="link"
v-else class="flex flex-col justify-center items-center btn-group text-white">
:to="page.navigate" <font-awesome-icon :icon="['fas', page.icon]" size="2x" class="menu-icon" />
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 }} {{ page.subName }}
</router-link> </router-link>
</li> </li>
</ul> </ul>
<a-drawer <a-drawer :width="200" placement="left" :open="open" :closable="false" @close="onClose" class="sub-drawer"
:width="200" :maskStyle="{ opacity: 0.5 }" :bodyStyle="{ paddingLeft: 0, paddingRight: 0 }">
placement="left"
:open="open"
:closable="false"
@close="onClose"
class="sub-drawer"
:maskStyle="{ opacity: 0.5 }"
:bodyStyle="{ paddingLeft: 0, paddingRight: 0 }"
>
<ul> <ul>
<li <li v-for="sub in buildingStore.subSys" :key="sub.sub_system_tag"
v-for="sub in buildingStore.subSys" class="group text-xl text-center py-3 hover:bg-black" @click="() => navigateToSub(sub)">
:key="sub.sub_system_tag"
@click.prevent="() => navigateToSub(sub)"
class="text-xl text-center py-3 hover:bg-black hover:text-info" <router-link
> :to="{ name: 'sub_system', params: { main_system_id: sub.main_system_tag, sub_system_id: sub.sub_system_tag }, }"
{{ sub.full_name }} type="link" class="group-hover:text-info">
{{ sub.full_name }}
</router-link>
</li> </li>
</ul> </ul>
</a-drawer> </a-drawer>

View File

@ -7,7 +7,7 @@ export const AUTHPAGES = [
{ {
authCode: "PF1", authCode: "PF1",
icon: "tv", icon: "tv",
navigate: "/", navigate: "/system",
}, },
{ {
authCode: "PF2", authCode: "PF2",

View File

@ -31,7 +31,7 @@ export default function useAlarmData() {
timer = null; timer = null;
}, },
each: (record) => { each: (record) => {
const alarmDisplayName = record.get("alarmData").get("sourceName"); let alarmDisplayName = record.get("alarmData").get("sourceName");
alarmDisplayName = alarmDisplayName.replace(/\$2d/g, "_"); // 檢查並替換 $2d 為 _ alarmDisplayName = alarmDisplayName.replace(/\$2d/g, "_"); // 檢查並替換 $2d 為 _
const sourceTmp = alarmDisplayName.split("_"); const sourceTmp = alarmDisplayName.split("_");
const bfName = sourceTmp[1] + "-" + sourceTmp[4]; const bfName = sourceTmp[1] + "-" + sourceTmp[4];

View File

@ -10,8 +10,11 @@ import ProductSetting from "@/views/productSetting/ProductSetting.vue";
import Login from "@/views/login/Login.vue"; import Login from "@/views/login/Login.vue";
import useUserInfoStore from "@/stores/useUserInfoStore"; import useUserInfoStore from "@/stores/useUserInfoStore";
import useGetCookie from "@/hooks/useGetCookie"; import useGetCookie from "@/hooks/useGetCookie";
import System from "@/views/system/System.vue";
import SystemFloor from "@/views/system/SystemFloor.vue";
import Test from "@/views/Test.vue"; import Test from "@/views/Test.vue";
import SystemMain from "@/views/system/SystemMain.vue";
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
@ -28,6 +31,23 @@ const router = createRouter({
name: "dashboard", name: "dashboard",
component: Dashboard, component: Dashboard,
}, },
{
path: "/system/:main_system_id/:sub_system_id",
name: "system",
component: System,
children: [
{
path: "",
name: "sub_system",
component: SystemMain,
},
{
path: "floor/:floor_id",
name: "floor",
component: SystemFloor,
},
],
},
{ {
path: "/historyData", path: "/historyData",
name: "history", name: "history",

View File

@ -1,5 +1,6 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { useRoute } from "vue-router";
const useBuildingStore = defineStore("buildingInfo", () => { const useBuildingStore = defineStore("buildingInfo", () => {
// 所有棟別 // 所有棟別
@ -35,12 +36,21 @@ const useBuildingStore = defineStore("buildingInfo", () => {
return subPages; return subPages;
}); });
const route = useRoute();
const selectedSystem = computed(() => {
if (route.params.sub_system_id && subSys.value.length > 0) {
return subSys.value.find((s) => s.key === route.params.sub_system_id);
}
return null;
});
return { return {
buildings, buildings,
selectedBuilding, selectedBuilding,
mainSubSys, mainSubSys,
mainSys, mainSys,
subSys, subSys,
selectedSystem,
}; };
}); });

View File

@ -0,0 +1,54 @@
<script setup>
import { RouterView, } from 'vue-router';
import { computed } from "vue";
import SystemFloorBar from './components/SystemFloorBar.vue';
import useBuildingStore from "@/stores/useBuildingStore";
const buildingStore = useBuildingStore()
const statusList = computed(() => {
const sub = buildingStore.selectedSystem
if (sub) {
return {
device_normal_color: sub.device_normal_color,
device_normal_text: sub.device_normal_text,
device_close_color: sub.device_close_color,
device_close_text: sub.device_close_text,
device_error_color: sub.device_error_color,
device_error_text: sub.device_error_text
}
}
return null
})
</script>
<template>
<SystemFloorBar />
<div class="grid grid-cols-2 mt-10">
<div class="col-span-1">
<div class="flex mb-4 items-center">
<span class="flex items-center mr-3" v-if="statusList?.device_normal_text">
<span class="w-7 h-7 rounded-full inline-block mr-1"
:style="{ backgroundColor: statusList.device_normal_color }"></span>
<span>{{ statusList.device_normal_text }}</span>
</span>
<span class="flex items-center mr-3" v-if="statusList?.device_close_text">
<span class="w-7 h-7 rounded-full inline-block mr-1"
:style="{ backgroundColor: statusList.device_close_color }"></span>
<span>{{ statusList.device_close_text }}</span>
</span>
<span class="flex items-center mr-3" v-if="statusList?.device_error_text">
<span class="w-7 h-7 rounded-full inline-block mr-1"
:style="{ backgroundColor: statusList.device_error_color }"></span>
<span>{{ statusList.device_error_text }}</span>
</span>
</div>
<RouterView />
</div>
<div class="col-span-1"></div>
</div>
</template>
<style lang='scss' scoped></style>

View File

@ -0,0 +1,10 @@
<script setup>
import { useRoute } from 'vue-router';
const route = useRoute()
</script>
<template>
floor
</template>
<style lang='scss' scoped></style>

View File

@ -0,0 +1,11 @@
<script setup>
import { useRoute } from 'vue-router';
import SystemCard from './components/SystemCard.vue';
const route = useRoute()
</script>
<template>
<SystemCard />
</template>
<style lang='scss' scoped></style>

View File

@ -0,0 +1,140 @@
<script setup>
import { getSystemDevices } from "@/apis/system"
import useBuildingStore from "@/stores/useBuildingStore";
import { useRoute } from "vue-router";
import { ref, watch } from "vue"
const store = useBuildingStore()
const data = ref([])
const route = useRoute()
const getData = async () => {
const res = await getSystemDevices({
sub_system_tag: route.params.sub_system_id,
building_tag: store.selectedBuilding?.building_tag,
})
data.value = res.data.map(d => ({ ...d, key: d.full_name }))
}
watch([() => route, () => store.selectedBuilding], ([newRoute, newBuilding]) => {
if (newBuilding && newRoute.params.sub_system_id) {
getData()
}
}, {
deep: true,
immediate: true,
})
</script>
<template>
<!-- <InfoModal :data="currentInfoModalData" /> -->
<div class="equipment-show" v-for="d in data" :key="d.full_name">
<p class="title">{{ d.full_name }}</p>
<div class="grid grid-cols-3 gap-5" v-if="d.device_list.length > 0">
<div class="col-auto relative" v-for="device in d.device_list" :key="device.device_guid">
<div class="item">
<div class="left w-4/5">
<div class="sec02">
<img v-if="device.device_image_url" :src="device.device_image_url" alt="" class="w-8 h-8">
<span class="w-8 h-8" v-else></span>
<span>{{ device.full_name }}</span>
</div>
<div class="sec03">
<span></span>
<span>狀態</span>
<span></span>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang='css' scoped>
/*設備顯示*/
.title {
@apply text-lg text-white relative inline-block mb-5 after:absolute after:-bottom-2 after:left-1/4 after:w-4/5 after:h-[1px] after:bg-info;
}
.item {
@apply flex items-center border border-success py-4 px-5 relative mb-5 after:absolute after:right-0 after:top-3 after:w-6 after:h-10 after:bg-[url(/src/assets/img/equipment/state-background.svg)] after:z-10;
;
}
.equipment-show .item .sec01 span:nth-child(1) {
font-size: 1rem;
position: relative;
margin-right: 50px;
}
.equipment-show .item .sec01 span:nth-child(1)::after {
content: "";
position: absolute;
bottom: 5px;
right: -47px;
display: block;
transform: translateY(50%);
width: 45px;
height: 1px;
background-color: #35eded;
border-radius: 25px;
z-index: 10;
}
.equipment-show .item .sec01 span:nth-child(2) {
font-size: .6rem;
}
.equipment-show .item .sec02 {
display: flex;
align-items: center;
position: relative;
margin-bottom: 15px;
}
.equipment-show .item .sec02::after {
content: "";
background: url(/src/assets/img/equipment/state-title.svg) center center;
position: absolute;
right: 0;
bottom: -10px;
height: 25px;
width: 105px;
background-repeat: no-repeat;
z-index: 1;
}
.equipment-show .item .sec02 span:nth-child(1) {
background-color: #31afdf;
border: 3px solid #fff;
display: block;
border-radius: 5px;
margin-right: 10px;
}
.equipment-show .item .sec02 span:nth-child(2) {
font-size: 1.5rem;
}
.equipment-show .item .sec03 {
display: flex;
align-items: center;
}
.equipment-show .item .sec03 span:nth-child(1) {
width: 15px;
height: 15px;
border-radius: 100%;
display: block;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,55 @@
<script setup>
import { useRoute } from 'vue-router';
import { getSystemFloors } from "@/apis/system"
import { onMounted, ref, watch } from 'vue';
import useBuildingStore from "@/stores/useBuildingStore";
import useActiveBtn from "@/hooks/useActiveBtn"
const route = useRoute()
const store = useBuildingStore();
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const getFloors = async () => {
const res = await getSystemFloors(store.selectedBuilding?.building_tag, route.params.sub_system_id)
setItems([
{
title: "總覽",
key: "main",
active: true,
},
...res.data.map((d, idx) => ({
title: d.floor_tag,
key: d.floor_guid,
active: false,
}))
]);
}
watch(selectedBtn, (newValue) => {
console.log(newValue);
})
watch(() => store.selectedBuilding, (newValue) => {
console.log(newValue)
newValue && getFloors()
}, {
deep: true,
immediate: true
})
</script>
<template>
<div class="flex items-center border border-info px-4 py-2">
<h5 class="text-md font-extrabold me-4">{{ store.selectedSystem?.full_name }}</h5>
<ButtonGroup :items="items" :withLine="false" className="btn-xs rounded-md"
:onclick="(e, item) => changeActiveBtn(item)" />
</div>
</template>
<style lang='scss' scoped>
</style>