Merge branch 'feature/headquartersSetting' into feature/dockerSetting

This commit is contained in:
koko 2025-08-18 16:35:15 +08:00
commit 42742d1063
104 changed files with 2425 additions and 458 deletions

29
.github/prompts/exportCSV.prompt.md vendored Normal file
View File

@ -0,0 +1,29 @@
---
mode: agent
---
# API 路徑整理與引用檢查規則
## 目標
- 針對 apis 目錄下所有子資料夾(如 account、alert、asset、building、dashboard、energy、forge、graph、history、login、operation、productSetting、system的 api.js 與 index.js 檔案,完整追蹤 API 路徑的實際引用情形。
- 追蹤流程:
1. 先在 api.js 找出所有 API 路徑常數(如 `export const GET_XXX_API = '/path'`)。
2. 在 index.js 檔案確認這些常數有被 import 並包裝成 API function`getXXX`)。
3. 再全專案搜尋這些 API function 是否有被其他檔案 import 並呼叫。
- 產生一份 csv 報表,格式如下:
| API 路徑 | 定義常數 | API function | 是否有被引用 |
|----------|----------|--------------|-------------|
| /user | GET_USER_API | getUser | Y |
| /admin | GET_ADMIN_API | getAdmin | N |
## 詳細規則
- 處理 apis 目錄下所有子資料夾的 api.js 與 index.js 檔案。
- API 路徑的定義需涵蓋 get/post/put/delete 等(如 `export const API = '/path'`)。
- 只統計有被 index.js import 並包裝成 function 的 API 路徑。
- 檢查 function 是否有被其他檔案 import 並呼叫(排除 apis 目錄本身)。
- 匹配到的檔案需記錄完整路徑,可多個檔案以分號分隔。
- 統計結果輸出為 csv 檔案。
## 輸出
- 檔名api_usage_report.csv
- 欄位API 路徑, 定義常數, API function, 是否有被引用,

7
package-lock.json generated
View File

@ -22,6 +22,7 @@
"echarts": "^5.4.3",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"leaflet": "^1.9.4",
"mqtt": "^5.10.3",
"pinia": "^2.1.7",
"requirejs": "^2.3.6",
@ -3350,6 +3351,12 @@
"node": ">=0.10.0"
}
},
"node_modules/leaflet": {
"version": "1.9.4",
"resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz",
"integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==",
"license": "BSD-2-Clause"
},
"node_modules/libphonenumber-js": {
"version": "1.10.60",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.60.tgz",

View File

@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
"preview": "vite preview",
"build:staging": "vite build --mode staging"
},
"dependencies": {
"@ant-design/icons-vue": "^7.0.1",
@ -23,6 +24,7 @@
"echarts": "^5.4.3",
"jquery-ui": "^1.14.1",
"json-schema-generator": "^2.0.6",
"leaflet": "^1.9.4",
"mqtt": "^5.10.3",
"pinia": "^2.1.7",
"requirejs": "^2.3.6",

BIN
public/CviLux_globalmap.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
public/CviLux_globalmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 552 KiB

View File

@ -34,4 +34,6 @@ export const GET_ASSET_ELECTYPE_API = `/AssetManage/GetElecType`;
export const POST_ASSET_ELECTYPE_API = `/AssetManage/SaveElecType`;
export const DELETE_ASSET_ELECTYPE_API = `/AssetManage/DeleteElecType`;
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;

View File

@ -26,13 +26,14 @@ import {
POST_ASSET_ELECTYPE_API,
DELETE_ASSET_ELECTYPE_API,
POST_ASSET_ELEC_SETTING_API,
POST_ASSET_MQTT_PUBLISH_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apiHandler";
import { object } from "yup";
export const getAssetMainList = async (building_guid) => {
const res = await instance.post(GET_ASSET_MAIN_LIST_API,{building_guid});
const res = await instance.post(GET_ASSET_MAIN_LIST_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
@ -49,12 +50,17 @@ export const deleteAssetMainItem = async (id) => {
});
};
export const postAssetMainList = async ({ id, system_key, system_value, building_guid }) => {
export const postAssetMainList = async ({
id,
system_key,
system_value,
building_guid,
}) => {
const res = await instance.post(POST_ASSET_MAIN_LIST_API, {
id,
system_key,
system_value,
building_guid
building_guid,
});
return apihandler(res.code, res.data, {
@ -241,6 +247,9 @@ export const postDeviceItem = async ({
decimals,
is_bool,
is_link,
show_event_switch_btn,
event_switch_on_message,
event_switch_off_message,
}) => {
const res = await instance.post(POST_ASSET_DEVICE_ITEM_API, {
id,
@ -250,6 +259,9 @@ export const postDeviceItem = async ({
decimals,
is_bool,
is_link,
show_event_switch_btn,
event_switch_on_message,
event_switch_off_message,
});
return apihandler(res.code, res.data, {
@ -335,3 +347,15 @@ export const postAssetElecSetting = async (formData) => {
code: res.code,
});
};
export const postMQTTpublish = async ({ Topic, Payload }) => {
const res = await instance.post(POST_ASSET_MQTT_PUBLISH_API, {
Topic,
Payload,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -4,3 +4,4 @@ export const DELETE_BUILDING_API = `/AssetManage/DeleteBuilding`;
export const GET_AUTHPAGE_API = `/api/GetUsrFroList`;
export const GET_SUBAUTHPAGE_API = `/api/Device/GetMainSub`;
export const GET_ALL_DEVICE_API = `/api/Device/GetAllDevice`;
export const GET_FUNCTION_LIST_API = `/api/function/get-function-list`;

View File

@ -5,6 +5,7 @@ import {
GET_AUTHPAGE_API,
GET_SUBAUTHPAGE_API,
GET_ALL_DEVICE_API,
GET_FUNCTION_LIST_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apiHandler";
@ -39,9 +40,9 @@ export const deleteBuildings = async (building_guid) => {
});
};
export const getAuth = async (lang) => {
const res = await instance.post(GET_AUTHPAGE_API, {
lang,
export const getAuth = async (building_id) => {
const res = await instance.get(GET_FUNCTION_LIST_API, {
params: { building_id },
});
return apihandler(res.code, res.data, {
msg: res.msg,
@ -50,7 +51,7 @@ export const getAuth = async (lang) => {
};
export const getAllSysSidebar = async (building_guid) => {
const res = await instance.post(GET_SUBAUTHPAGE_API, {building_guid});
const res = await instance.post(GET_SUBAUTHPAGE_API, { building_guid });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,

View File

@ -10,4 +10,8 @@ export const GET_DASHBOARD_PRODUCT_HISTORY_API = `/SituationRoom/GetProductionHi
export const GET_DASHBOARD_ENERGY_INFO_API = `api/dashboard/GetEnergyInfo`
export const GET_DASHBOARD_ENERGY_COST_API = `api/dashboard/GetEnergyCost`
export const GET_DASHBOARD_ALARMOPERATION_INFO_API = `api/dashboard/GetAlarmOperationInfo`
export const GET_DASHBOARD_ALARMOPERATION_INFO_API = `api/dashboard/GetAlarmOperationInfo`
export const GET_DASHBOARD_2D3DINFO_API = `api/setting/visual/query`
export const POST_DASHBOARD_2D3DINFO_API = `api/setting/visual/update`

View File

@ -11,6 +11,8 @@ import {
GET_DASHBOARD_ENERGY_INFO_API,
GET_DASHBOARD_ENERGY_COST_API,
GET_DASHBOARD_ALARMOPERATION_INFO_API,
GET_DASHBOARD_2D3DINFO_API,
POST_DASHBOARD_2D3DINFO_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apiHandler";
@ -176,3 +178,22 @@ export const getAlarmOperationInfo = async (building_guid) => {
code: res.code,
});
};
export const getDashboard2D3D = async (BuildingId) => {
const res = await instance.post(GET_DASHBOARD_2D3DINFO_API, {
BuildingId});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const posttDashboard2D3D = async (formData) => {
const res = await instance.post(POST_DASHBOARD_2D3DINFO_API, formData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -1,6 +1,6 @@
export const GET_REALTIME_DIST_API = `/api/Energe/GetRealTimeDistribution`;
export const GET_ELECUSE_DAY_API = `/api/Energe/GetElecUseDay`;
export const GET_TAI_POWER_API = `/api/Energe/GetTaipower`;
export const GET_TAI_POWER_API = `/api/energy-manager/power-usage`;
export const GET_SIDEBAR_API = `/api/GetSideBar`;
export const GET_SEARCH_API = `/api/Energe/GetFilter`;

View File

@ -0,0 +1,6 @@
export const GET_SITES_SYSTEM_STATUS_API = `/api/monitoring/sites-system-status`;
export const GET_SITES_SYSTEM_ENERGY_COST_RANK_API = `/api/energy-manager/all-site/energy-cost-rank`;
export const GET_SITES_SYSTEM_ENERGY_COST_TREND_API = `/api/energy-manager/all-site/energy-cost-trend`;
export const GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API = `/api/energy-manager/all-site/energy-cost-growth-rate`;

View File

@ -0,0 +1,44 @@
import {
GET_SITES_SYSTEM_STATUS_API,
GET_SITES_SYSTEM_ENERGY_COST_RANK_API,
GET_SITES_SYSTEM_ENERGY_COST_TREND_API,
GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
export const getSystemStatus = async (building_ids) => {
const res = await instance.post(GET_SITES_SYSTEM_STATUS_API, building_ids);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getSystemEnergyCostRank = async (building_ids) => {
const res = await instance.post(GET_SITES_SYSTEM_ENERGY_COST_RANK_API, building_ids);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const getSystemEnergyCostTrend = async (building_ids) => {
const res = await instance.post(GET_SITES_SYSTEM_ENERGY_COST_TREND_API, building_ids);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
}
export const getSystemEnergyCostGrowth = async (building_ids) => {
const res = await instance.get(GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API, building_ids);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
}

View File

@ -4,6 +4,10 @@ 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_EXPORT_REPORT_API = `/api/History/GetHistoryExcelReport`;
export const GET_HISTORY_EXPORT_CURVE_API = `/api/History/GetHistoricalCurveExcelReport`;
export const GET_HISTORY_EXPORT_QUICK_API = `/api/History/GetQuickMeteringExcelReport`;
export const GET_HISTORY_EXPORT_CLASS_API = `/api/History/GetElectricityClassificationExcelReport`;
export const GET_HISTORY_FAVORITE_API = `/api/History/GetHistoryFavorite`;
export const POST_HISTORY_FAVORITE_API = `/api/History/SaveHistoryFavorite`;

View File

@ -7,6 +7,10 @@ import {
DELETE_HISTORY_FAVORITE_API,
UPDATE_HISTORY_FAVORITE_API,
GET_HISTORY_EXPORT_API,
GET_HISTORY_EXPORT_REPORT_API,
GET_HISTORY_EXPORT_CURVE_API,
GET_HISTORY_EXPORT_QUICK_API,
GET_HISTORY_EXPORT_CLASS_API,
} from "./api";
import instance, { fileInstance } from "@/util/request";
import apihandler from "@/util/apiHandler";
@ -81,7 +85,52 @@ export const getHistoryData = async ({
};
export const getHistoryExportData = async ({
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
Type,
table_type,
}) => {
const api =
parseInt(table_type) === 1
? GET_HISTORY_EXPORT_CURVE_API
: parseInt(table_type) === 2
? GET_HISTORY_EXPORT_QUICK_API
: parseInt(table_type) === 3
? GET_HISTORY_EXPORT_CLASS_API
: GET_HISTORY_EXPORT_API;
const res = await fileInstance.post(
api,
{
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),
Building_tag_list: [...new Set(Device_list.map((d) => d.split("_")[1]))],
table_type: parseInt(table_type),
},
{ responseType: "blob" }
);
return apihandler(
res.code,
res,
{
msg: res.msg,
code: res.code,
},
downloadExcel
);
};
export const getHistoryExportReport = async ({
Start_date,
End_date,
Start_time,
@ -89,19 +138,8 @@ export const getHistoryExportData = async ({
Device_list,
Points,
}) => {
/*
{
Type,
Start_date,
End_date,
Start_time,
End_time,
Device_list,
Points,
}
*/
const res = await fileInstance.post(
GET_HISTORY_EXPORT_API,
GET_HISTORY_EXPORT_REPORT_API,
{
// ...exportContent,
Start_date: Start_date,
@ -110,7 +148,6 @@ export const getHistoryExportData = async ({
End_time: End_time,
Points: Array.isArray(Points) ? Points : [Points],
Device_list: Array.isArray(Device_list) ? Device_list : [Device_list],
Type: parseInt(Type),
Building_tag_list: [...new Set(Device_list.map((d) => d.split("_")[1]))],
},
{ responseType: "blob" }

View File

@ -1,3 +1,4 @@
export const GET_SYSTEM_FLOOR_LIST_API = `/api/Device/GetFloor`;
export const GET_SYSTEM_DEVICE_LIST_API = `/api/Device/GetDeviceList`;
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
export const GET_SYSTEM_REALTIME_API = `/api/Device/GetRealTimeData`;
export const GET_SYSTEM_DEVICE_POWER_TOGGLE_API = `/api/device-events/power-toggle`;

View File

@ -2,6 +2,7 @@ import {
GET_SYSTEM_FLOOR_LIST_API,
GET_SYSTEM_DEVICE_LIST_API,
GET_SYSTEM_REALTIME_API,
GET_SYSTEM_DEVICE_POWER_TOGGLE_API
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apiHandler";
@ -42,3 +43,16 @@ export const getSystemRealTime = async (device_list) => {
code: res.code,
});
};
export const toggleDevicePower = async ({topic_publish, device_item_id,new_value}) => {
const res = await instance.post(GET_SYSTEM_DEVICE_POWER_TOGGLE_API, {
topic_publish,
device_item_id,
new_value,
});
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
}

View File

@ -58,6 +58,7 @@
box-sizing: border-box;
margin: 0;
font-weight: normal;
scrollbar-color: auto !important;
}
body {

View File

@ -36,7 +36,7 @@ const toggleErrIcon = () => {
</div>
<div
v-if="showErr"
class="drawer-side translate-y-20 max-h-[90vh] overflow-x-hidden overflow-y-scroll"
class="drawer-side translate-y-20 max-h-[90vh] overflow-x-hidden overflow-y-scroll w-[300px] left-auto right-0"
>
<AlarmCards />
</div>

View File

@ -1,6 +1,6 @@
<script setup>
import * as echarts from "echarts";
import { onMounted, ref, markRaw } from "vue";
import { onMounted, ref, markRaw, nextTick } from "vue";
import axios from "axios";
const props = defineProps({
@ -24,9 +24,15 @@ async function updateSvg(svg, option) {
} else {
clear();
}
axios.get(svg.path).then(({ data }) => {
axios.get(svg.path).then(async ({ data }) => {
echarts.registerMap(svg.full_name, { svg: data });
chart.value.setOption(option);
await nextTick();
// 調 setOption
setTimeout(() => {
if (chart.value && !chart.value.isDisposed()) {
chart.value.setOption(option);
}
}, 0);
if (props.getCoordinate) {
chart.value.getZr().on("click", function (params) {
var pixelPoint = [params.offsetX, params.offsetY];
@ -44,11 +50,15 @@ async function updateSvg(svg, option) {
value: dataPoint, //
itemStyle: { color: "#0000FF" }, //
});
chart.value.setOption({
series: {
data: updatedData,
},
});
setTimeout(() => {
if (chart.value && !chart.value.isDisposed()) {
chart.value.setOption({
series: {
data: updatedData,
},
});
}
}, 0);
});
}
});

View File

@ -64,10 +64,11 @@ watch(
twMerge(
'flex-col text-xl',
cls,
openChildren.includes(dataParentKey) || open ? 'flex' : 'hidden'
openChildren.includes(d.key) || open ? 'flex' : 'hidden'
)
"
v-for="d in data"
:key="d.key"
:data-parent="d.key"
:open="open"
>

View File

@ -9,7 +9,7 @@ const props = defineProps({
type: String,
default: "",
},
value: String,
value: Object,
isTopLabelExist: {
type: Boolean,
default: true,

View File

@ -9,7 +9,7 @@ const props = defineProps({
type: String,
default: "",
},
value: String,
value: Object,
isTopLabelExist: {
type: Boolean,
default: true,

View File

@ -51,7 +51,7 @@ onMounted(() => {
: '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 bg-normal',
'modal-box static rounded-md border border-info py-5 px-6 overflow-y-auto bg-normal',
modalClass
)
" :style="{ minWidth: isNaN(width) ? width : `${width}px` }">

View File

@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
const props = defineProps({
name: String,
value: String,
value: Object,
items: Array,
isLabelExist: {
type: Boolean,

View File

@ -18,7 +18,7 @@ const props = defineProps({
Attribute: String,
onChange: Function,
selectClass: String,
value: String || Number,
value: Object,
isTopLabelExist: {
type: Boolean,
default: true,

View File

@ -18,7 +18,7 @@ const props = defineProps({
Attribute: String,
onChange: Function,
selectClass: String,
value: String || Number,
value: Object,
isTopLabelExist: {
type: Boolean,
default: true,
@ -70,9 +70,7 @@ const props = defineProps({
: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' : '')">

View File

@ -3,7 +3,7 @@ import { defineProps } from "vue";
const props = defineProps({
name: String,
value: String,
value: Object,
placeholder: String,
});
</script>
@ -15,7 +15,7 @@ const props = defineProps({
<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"
class="textarea text-lg rounded-md border-info focus-within:border-info h-40"
:placeholder="placeholder"
:name="name"
v-model="value[name]"

View File

@ -1,17 +1,22 @@
<script setup>
import { onMounted, ref } from "vue";
import { useRouter } from "vue-router";
import NavbarItem from "./NavbarItem.vue";
import NavbarBuilding from "./NavbarBuilding.vue";
import Logo from "@/assets/img/logo.svg";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
import AlarmDrawer from "@/components/alarm/AlarmDrawer.vue";
import NavbarLang from "./NavbarLang.vue";
import { twMerge } from "tailwind-merge";
const user = ref("");
const menuShow = ref(true);
const router = useRouter();
const store = useUserInfoStore();
const storeBuilding = useBuildingStore();
onMounted(() => {
const name = store.user.user_name;
if (name) {
@ -24,12 +29,26 @@ const toggleMenu = () => {
};
const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
const logout = () => {
document.cookie = "JWT-Authorization=; Max-Age=0";
document.cookie = "user_name=; Max-Age=0";
store.user.token = "";
store.user.user_name = "";
storeBuilding.deleteBuilding();
router.push({ path: "/login" });
};
</script>
<template>
<header class="navbar bg-dark text-light-info w-full relative z-50">
<div class="navbar-start min-w-[480px] lg:min-w-[440px]">
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden" @click="toggleMenu">
<div
tabindex="0"
role="button"
class="btn btn-ghost lg:hidden"
@click="toggleMenu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
@ -93,12 +112,12 @@ const src = import.meta.env.MODE === "production" ? "./logo.svg" : Logo;
class="dropdown-content translate-y-2 z-[100] menu py-3 shadow rounded w-32 bg-[#4c625e] border text-center"
>
<li class="text-white">
<router-link
to="logout"
type="link"
<a
href="#"
@click.prevent="logout"
class="flex flex-col justify-center items-center"
>{{ $t("sign_out") }}
</router-link>
</a>
</li>
</ul>
</div>

View File

@ -1,12 +1,18 @@
<script setup>
import { onMounted } from "vue";
import { onMounted,watch } from "vue";
import useBuildingStore from "@/stores/useBuildingStore";
import { useRouter } from "vue-router";
const store = useBuildingStore();
const router = useRouter();
const selectBuilding = (bui) => {
store.selectedBuilding = bui; // selectedBuildingwatch
localStorage.setItem("CviBuilding", JSON.stringify(bui));
if (bui.is_headquarter == true) {
router.replace({ path: "/headquarters" });
} else {
router.replace({ path: "/dashboard" });
}
};
onMounted(() => {
@ -15,29 +21,35 @@ onMounted(() => {
</script>
<template>
<div class="dropdown dropdown-bottom">
<div
tabindex="0"
role="button"
class="text-white ml-8 text-lg font-semiLight"
>
{{ store.selectedBuilding?.full_name }}
<font-awesome-icon :icon="['fas', 'angle-down']" class="ml-1" />
</div>
<ul
tabindex="0"
class="dropdown-content w-48 left-8 translate-y-2 z-[1] menu py-3 shadow rounded bg-[#4c625e] border text-center"
>
<li
class="text-white my-1 text-base cursor-pointer"
v-for="bui in store.buildings"
:key="bui.building_tag"
@click="selectBuilding(bui)"
<template v-if="store.buildings.length > 1">
<div class="dropdown dropdown-bottom">
<div
tabindex="0"
role="button"
class="text-white ml-8 text-lg font-semiLight"
>
{{ bui.full_name }}
</li>
</ul>
</div>
{{ store.selectedBuilding?.full_name }}
<font-awesome-icon :icon="['fas', 'angle-down']" class="ml-1" />
</div>
<ul
tabindex="0"
class="dropdown-content w-48 left-8 translate-y-2 z-[1] menu py-3 shadow rounded bg-[#4c625e] border text-center"
>
<li
class="text-white my-1 text-base cursor-pointer"
v-for="bui in store.buildings"
:key="bui.building_tag"
@click="selectBuilding(bui)"
>
{{ bui.full_name }}
</li>
</ul>
</div>
</template>
<template v-else>
<div class="text-white ml-8 text-lg font-semiLight">
{{ store.selectedBuilding?.full_name }}
</div>
</template>
</template>
<style lang="scss" scoped></style>
<style lang="scss" scoped></style>

View File

@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
import { useRoute } from "vue-router";
import { twMerge } from "tailwind-merge";
const { locale } = useI18n();
const { locale, t } = useI18n();
const store = useUserInfoStore();
const buildingStore = useBuildingStore();
const route = useRoute();
@ -17,8 +17,8 @@ const openKeys = ref([]); // 追蹤當前打開的子菜單
const menu_array = ref([]);
const currentAuthCode = ref("");
const iniFroList = async () => {
const res = await getAuth(locale.value);
const iniFroList = async (building_id) => {
const res = await getAuth(building_id);
store.updateAuthPage(
res.data.map((d) =>
@ -72,23 +72,21 @@ watch(
(newVal) => {
if (newVal !== null) {
getSubMonitorPage(newVal.building_guid);
iniFroList(newVal.building_guid);
}
}
);
watch(locale, () => {
iniFroList();
});
onMounted(() => {
iniFroList();
});
</script>
<template>
<ul class="px-1 menu-box my-2">
<li class="flex flex-col items-center justify-center">
<router-link
:to="{ name: 'dashboard' }"
:to="{
name:
buildingStore.selectedBuilding?.is_headquarter === true
? 'headquarters'
: 'dashboard',
}"
class="flex lg:flex-col justify-center items-center btn-group text-white"
>
<font-awesome-icon
@ -96,7 +94,7 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ $t("home") }}</span>
<span>{{ $t("navbar.home") }}</span>
</router-link>
</li>
<li
@ -132,11 +130,11 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ page.subName }}</span>
<span>{{ $t(`navbar.${page.showView}`) }}</span>
</a>
<router-link
v-else
:to="page.navigate"
:to="`/` + page.showView"
type="link"
class="flex lg:flex-col justify-center items-center btn-group text-white"
>
@ -145,7 +143,7 @@ onMounted(() => {
size="2x"
class="w-10 m-auto"
/>
<span>{{ page.subName }}</span>
<span>{{ $t(`navbar.${page.showView}`) }}</span>
</router-link>
</li>
</ul>
@ -169,7 +167,7 @@ onMounted(() => {
<a-sub-menu
v-for="main in menu_array"
:key="main.main_system_tag"
:title="main.full_name"
:title="main.resource ? t(`navbar.${main.resource}`) : main.full_name"
v-if="menu_array.length > 0 && open"
>
<a-menu-item
@ -192,13 +190,13 @@ onMounted(() => {
params: {
main_system_id: main.main_system_tag,
sub_system_id: sub.sub_system_tag,
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
...(currentAuthCode === 'PF2' || currentAuthCode === 'PF11'
? { type: sub.type }
: { floor_id: 'main' }),
},
}"
>
{{ sub.full_name }}
{{ sub.resource ? $t(`navbar.${sub.resource}`): sub.full_name }}
</router-link>
</a-menu-item>
</a-sub-menu>

View File

@ -13,6 +13,38 @@
"name": "名称",
"time": "时间"
},
"navbar": {
"home": "首页",
"sysMonBtnList": "系统监控",
"historyData": "历史资料",
"energyManagement": "能源管理",
"alert": "告警",
"operation": "运维管理",
"graphManagement": "图资管理",
"AssetManagement": "资产管理",
"accountManagement": "帐号管理",
"Setting": "系统设定",
"energy_analysis": "能源分析",
"consumption_report": "用电报表",
"chart_analysis": "图表分析",
"historical_curve": "历史曲线",
"quick_metering": "快速抄表",
"electricity_classification": "用电分类",
"daily_report": "日报表",
"weekly_report": "周报表",
"monthly_report": "月报表",
"annual_report": "年报表",
"setting":"系统设置",
"device":"设置设置",
"department": "部门",
"settings_2d_3d": "2D/3D 显示设置",
"maintenance_vendor": "维修厂商",
"building": "厂区",
"floor": "楼层",
"demand": "电表",
"time_based_pricing": "时段电价",
"mqtt_result": "MQTT 结果"
},
"upload": {
"title": "选择一个文件或拖放到这里",
"description": "档案不超过 10MB",
@ -89,11 +121,11 @@
"reset_value": "复归值",
"edit_automatic_demand": "编辑自动需量",
"elec_bills": "今年电费累计(元)",
"interval_elec_charges": "区间电费(元)",
"interval_elec_charges": "本月电费(元)",
"year_carbon_emission": "今年碳排当量累计(公斤)",
"interval_carbon_emission": "区间碳排当量",
"interval_carbon_emission": "本月碳排当量",
"year_elec_consumption": "今年用电度数(kWh)",
"interval_elec_consumption": "区间用电度数(kWh)",
"interval_elec_consumption": "本月用电度数(kWh)",
"monthly_elec_consumption": "每月用电分析",
"monthly_carbon_emission_and_reduction": "每月碳排当量 (kgCO2e)",
"monthly_bill_power": "每月计费度数 (kWh)",
@ -155,7 +187,9 @@
"off_peak_contract": "离峰契约",
"variable_electricity_charge": "流动电费",
"saturday": "周六",
"sunday_and_off_peak_days": "周日及离峰日"
"sunday_and_off_peak_days": "周日及离峰日",
"past_elec_data": "去年±数值(百分比)",
"past_month_elec_data": "上月±数值(百分比)"
},
"alarm": {
"title": "显示警告",
@ -399,9 +433,14 @@
"sure_to_delete_permanent": "是否确认永久删除该项目?",
"delete_success": "删除成功",
"delete_failed": "删除失败",
"mqtt_refresh": "重新设定成功"
"mqtt_refresh": "重新设定成功",
"schema_name_required": "架构名称栏位必填",
"incorrect_format":"格式不正确",
"send_successfully":"送出成功",
"edit_successfully":"修改成功"
},
"setting": {
"electricity_meter": "电表",
"MQTT_parse": "MQTT 解析",
"schema": "架构",
"point": "点位",
@ -411,6 +450,9 @@
"number_of_decimal_places": "小数位数",
"boolean_value": "布林值",
"hide_point": "点位显示",
"hide_switch": "switch 功能",
"switch_on_message": "switch 开启时传送的讯息",
"switch_off_message": "switch 关闭时传送的讯息",
"schema_name": "架构名称",
"IoT_point_structure": "IoT点位结构",
"system_point_name": "系统点位名称",

View File

@ -10,9 +10,41 @@
"in_otal": "筆資料",
"skip_to": "跳至",
"serial_number": "序號",
"name": "名",
"name": "",
"time": "時間"
},
"navbar": {
"home": "首頁",
"sysMonBtnList": "系統監控",
"historyData": "歷史資料",
"energyManagement": "能源管理",
"alert": "告警",
"operation": "運維管理",
"graphManagement": "圖資管理",
"AssetManagement": "資產管理",
"accountManagement": "帳號管理",
"Setting": "系統設定",
"energy_analysis": "能耗分析",
"consumption_report": "用電報表",
"chart_analysis": "圖表分析",
"historical_curve": "歷史曲線",
"quick_metering": "快速抄表",
"electricity_classification": "用電分類",
"daily_report": "日報表",
"weekly_report": "周報表",
"monthly_report": "月報表",
"annual_report": "年報表",
"setting":"系統設定",
"device":"設備設定",
"department": "部門",
"settings_2d_3d": "2D/3D 顯示設定",
"maintenance_vendor": "維運廠商",
"building": "廠區",
"floor": "樓層",
"demand": "電表",
"time_based_pricing": "時段電價",
"mqtt_result": "MQTT 結果"
},
"upload": {
"title": "選擇一個文件或拖放到這裡",
"description": "檔案不超過 10MB",
@ -89,11 +121,11 @@
"reset_value": "復歸值",
"edit_automatic_demand": "編輯自動需量",
"elec_bills": "今年電費累計(元)",
"interval_elec_charges": "區間電費(元)",
"interval_elec_charges": "本月電費(元)",
"year_carbon_emission": "今年碳排當量累計(公斤)",
"interval_carbon_emission": "區間碳排當量",
"interval_carbon_emission": "本月碳排當量",
"year_elec_consumption": "今年用電度數(kWh)",
"interval_elec_consumption": "區間用電度數(kWh)",
"interval_elec_consumption": "本月用電度數(kWh)",
"monthly_elec_consumption": "每月用電分析",
"monthly_carbon_emission_and_reduction": "每月碳排當量 (kgCO2e)",
"monthly_bill_power": "每月計費度數 (kWh)",
@ -155,7 +187,9 @@
"off_peak_contract": "離峰契約",
"variable_electricity_charge": "流動電費",
"saturday": "週六",
"sunday_and_off_peak_days": "週日及離峰日"
"sunday_and_off_peak_days": "週日及離峰日",
"past_elec_data": "去年±數值(百分比)",
"past_month_elec_data": "上月±數值(百分比)"
},
"alarm": {
"title": "顯示警告",
@ -399,9 +433,14 @@
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
"delete_success": "刪除成功",
"delete_failed": "刪除失敗",
"mqtt_refresh": "重新設定成功"
"mqtt_refresh": "重新設定成功",
"schema_name_required": "架構名稱欄位必填",
"incorrect_format": "格式不正確",
"send_successfully": "送出成功",
"edit_successfully": "修改成功"
},
"setting": {
"electricity_meter": "電表",
"MQTT_parse": "MQTT 解析",
"schema": "架構",
"point": "點位",
@ -411,6 +450,9 @@
"number_of_decimal_places": "小數位數",
"boolean_value": "布林值",
"hide_point": "點位顯示",
"hide_switch": "switch 功能",
"switch_on_message": "switch 開啟時傳送的訊息",
"switch_off_message": "switch 關閉時傳送的訊息",
"schema_name": "架構名稱",
"IoT_point_structure": "IoT點位結構",
"system_point_name": "系統點位名稱",

View File

@ -13,6 +13,38 @@
"name": "Name",
"time": "Time"
},
"navbar": {
"home": "Home",
"sysMonBtnList": "Monitoring",
"historyData": "History Data",
"energyManagement": "Energy",
"alert": "Alert",
"operation": "Maintenance",
"graphManagement": "Graph",
"AssetManagement": "Devices",
"accountManagement": "Account",
"Setting": "Setting",
"energy_analysis": "Energy Analysis",
"consumption_report": "Consumption Report",
"chart_analysis": "Chart Analysis",
"historical_curve": "Historical Curve",
"quick_metering": "Quick Metering",
"electricity_classification": "Electricity Classification",
"daily_report": "Daily Report",
"weekly_report": "Weekly Report",
"monthly_report": "Monthly Report",
"annual_report": "Annual Report",
"setting":"Settings",
"device":"Device",
"department": "Department",
"settings_2d_3d": "2D/3D Display Settings",
"maintenance_vendor": "Vendor",
"building": "Building",
"floor": "Floor",
"demand": "Demand",
"time_based_pricing": "Time-based Pricing",
"mqtt_result": "MQTT Result"
},
"upload": {
"title": "Select a file or drag and drop here",
"description": "File size cannot exceed 10MB",
@ -89,11 +121,11 @@
"reset_value": "Reset Value",
"edit_automatic_demand": "Edit automatic demand",
"elec_bills": "Total electricity bills this year (yuan)",
"interval_elec_charges": "Interval electricity charges (yuan)",
"interval_elec_charges": "Electricity charges for this month (yuan)",
"year_carbon_emission": "Cumulative carbon emission equivalent this year (kg)",
"interval_carbon_emission": "Interval carbon emission equivalent",
"interval_carbon_emission": "This month's carbon emission equivalent",
"year_elec_consumption": "This year's electricity consumption (kWh)",
"interval_elec_consumption": "Interval electricity consumption (kWh)",
"interval_elec_consumption": "This month's electricity consumption (kWh)",
"monthly_elec_consumption": "Monthly electricity consumption analysis",
"monthly_carbon_emission_and_reduction": "Monthly carbon emission equivalent (kgCO2e)",
"monthly_bill_power": "Monthly billing power (kWh)",
@ -155,7 +187,9 @@
"off_peak_contract": "Off-Peak Demand Charge",
"variable_electricity_charge": "Variable Electricity Charge",
"saturday": "Saturday",
"sunday_and_off_peak_days": "Sunday and Off-Peak Days"
"sunday_and_off_peak_days": "Sunday and Off-Peak Days",
"past_elec_data": "Last year ± value (percentage)",
"past_month_elec_data": "Last month ± value (percentage)"
},
"alarm": {
"title": "Warning",
@ -399,9 +433,14 @@
"sure_to_delete_permanent": "Are you sure you want to permanently delete this item?",
"delete_success": "Delete successfully",
"delete_failed": "Delete failed",
"mqtt_refresh": "MQTT reset successful"
"mqtt_refresh": "MQTT reset successful",
"schema_name_required": "The schema name field is required",
"incorrect_format":"Incorrect format",
"send_successfully":"Sent successfully",
"edit_successfully":"Edited successfully"
},
"setting": {
"electricity_meter": "Electricity Meter",
"MQTT_parse": "MQTT Parse",
"schema": "Schema",
"point": "Point",
@ -411,6 +450,9 @@
"number_of_decimal_places": "Number of Decimal Places",
"boolean_value": "Boolean Value",
"hide_point": "Point Display",
"hide_switch": "Switch Function",
"switch_on_message": "Switch On Message",
"switch_off_message": "Switch Off Message",
"schema_name": "Schema name",
"IoT_point_structure": "IoT Point Structure",
"system_point_name": "System Point Name",

View File

@ -2,69 +2,49 @@ export const AUTHPAGES = [
{
authCode: "PF0",
icon: "home",
navigate: "/dashboard",
},
{
authCode: "PF1",
icon: "tv",
navigate: "/system",
},
{
authCode: "PF2",
icon: "chart-pie",
pageName: "energyManagement",
navigate: "/energyManagement",
},
{
authCode: "PF3",
icon: "chart-area",
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",
},
{
authCode: "PF11",
icon: "cog",
pageName: "Setting",
navigate: "/Setting",
},
];

View File

@ -65,7 +65,7 @@ import {
faClock,
faCheckCircle
} from "@fortawesome/free-solid-svg-icons";
import { faCircle } from "@fortawesome/free-regular-svg-icons";
import { faCircle,faPaperPlane } from "@fortawesome/free-regular-svg-icons";
/* add icons to the library */
library.add(
@ -130,7 +130,8 @@ library.add(
faCrown,
faClock,
faCheckCircle,
faCircle
faCircle,
faPaperPlane
);
export default library;

View File

@ -6,7 +6,7 @@ import useForgeHeatmap from "./useForgeHeatmap";
import useForgeFloor from "./useForgeFloor";
export default function useForgeSprite() {
const { subscribeData } = inject("system_deviceList");
const { subscribeData, realtimeData } = inject("system_deviceList");
const { getCurrentInfoModalData, clearSelectedDeviceInfo, selected_dbid } =
inject("system_selectedDevice");
const forgeViewer = ref(null);
@ -25,7 +25,7 @@ export default function useForgeSprite() {
forgeViewer.value.navigation.setView(newPosition, newTarget);
// 確保 Home 視角
forgeViewer.value.autocam.setCurrentViewAsHome(true);
forgeViewer.value.autocam.setCurrentViewAsHome(true);
};
const updateDataVisualization = async (viewer) => {
@ -65,6 +65,23 @@ export default function useForgeSprite() {
const { flatSubData } = useSystemShowData();
// 根據設備取得即時狀態顏色
const getDeviceRealtimeColor = (d) => {
if (d.full_name === "SmartSocket-AA001") return "#ff0000";
if (
d.full_name === "SmartSocket-AA003" ||
d.full_name === "SmartSocket-AA004"
)
return "#888888";
const realtimeDevice = realtimeData?.value?.find(
(item) => item.device_number === d.device_number
);
const state = realtimeDevice?.state || "";
if (state === "offnormal" || state === "")
return d.device_close_color || "#999999";
return d.device_normal_color || "#009100";
};
// 創建 sprites
const createSprites = async () => {
if (dataVizExtn.value) {
@ -74,28 +91,25 @@ export default function useForgeSprite() {
let spriteColor = new THREE.Color(0xffffff);
const BASEURL = window.env?.VITE_FILE_API_BASEURL;
const spriteIconUrl = `${BASEURL}/dist/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
flatSubData.value?.forEach((d, index) => {
if (d.device_coordinate_3d) {
const position = d.device_coordinate_3d;
style.color = new THREE.Color(hexToRgb(d.device_normal_color));
// 每個都 new 一個 style
const pointStyle = new DataVizCore.ViewableStyle(
viewableType,
new THREE.Color(hexToRgb(getDeviceRealtimeColor(d))),
spriteIconUrl
);
const viewable = new DataVizCore.SpriteViewable(
position,
style,
pointStyle,
d.spriteDbId
);
viewableData.addViewable(viewable);
}
});
// await viewableData.finish();
// dataVizExtn.value.addViewables(viewableData);
// console.log(dataVizExtn.value);
viewableData.finish().then(
() => {
dataVizExtn.value.addViewables(viewableData);
@ -107,6 +121,16 @@ export default function useForgeSprite() {
);
}
};
// 監聽 realtimeData 變化,重建 sprites
watch(
() => realtimeData?.value,
() => {
if (forgeViewer.value?.isLoadDone()) {
createSprites();
}
},
{ deep: true }
);
watch(
() => flatSubData,

View File

@ -9,12 +9,14 @@ import AlertManagement from "@/views/alert/AlertManagement.vue";
import ProductSetting from "@/views/productSetting/ProductSetting.vue";
import EnergyManagement from "@/views/energyManagement/EnergyManagement.vue";
import SettingManagement from "@/views/setting/SettingManagement.vue";
import HeadquartersManagement from "@/views/headquarters/HeadquartersManagement.vue";
import Login from "@/views/login/Login.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
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 SystemMain from "@/views/system/SystemMain.vue";
@ -90,6 +92,11 @@ const router = createRouter({
name: "setting",
component: SettingManagement,
},
{
path: "/headquarters",
name: "headquarters",
component: HeadquartersManagement,
},
{
path: "/mytestfile/mjm",
name: "mytestfile",
@ -106,18 +113,11 @@ router.beforeEach(async (to, from, next) => {
const auth = useUserInfoStore();
const token = useGetCookie("JWT-Authorization");
const user_name = useGetCookie("user_name");
if (to.path === "/logout") {
document.cookie = "JWT-Authorization=; Max-Age=0";
document.cookie = "user_name=; Max-Age=0";
auth.user.token = "";
auth.user.user_name = "";
window.location.reload();
next({ path: "/login" });
}
const buildingStore = useBuildingStore();
if ((authRequired && !token) || to.path === "/") {
auth.user.token = "";
buildingStore.deleteBuilding();
next({ path: "/login" });
} else if (!authRequired) {
document.cookie = "JWT-Authorization=; Max-Age=0";

View File

@ -2,6 +2,7 @@ import { defineStore } from "pinia";
import { ref, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { getBuildings } from "@/apis/building";
import { getDashboard2D3D } from "@/apis/dashboard";
import { getAssetFloorList, getDepartmentList } from "@/apis/asset";
const useBuildingStore = defineStore("buildingInfo", () => {
@ -11,6 +12,9 @@ const useBuildingStore = defineStore("buildingInfo", () => {
const floorList = ref([]);
const deptList = ref([]);
const mainSubSys = ref([]);
// 控制顯示2D/3D切換與內容
const showForgeArea = ref(true);
const previewImageExt = ref("");
// 計算屬性
const mainSys = computed(() =>
@ -45,38 +49,62 @@ const useBuildingStore = defineStore("buildingInfo", () => {
// 獲取所有建築物
const fetchBuildings = async () => {
const res = await getBuildings();
buildings.value = res.data;
if (res.data.length > 0 && !selectedBuilding.value) {
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
selectedBuilding.value = storedBuilding || res.data[0]; // 預設選第一個建築
// const res = await getBuildings();
buildings.value = JSON.parse(localStorage.getItem("CviBuildingList")) || [];
const storedBuilding = JSON.parse(localStorage.getItem("CviBuilding"));
if (buildings.value.length > 0) {
selectedBuilding.value = storedBuilding || buildings.value[0]; // 預設選第一個建築
} else {
selectedBuilding.value = null; // 如果沒有建築物,清空選擇
}
};
// 獲取樓層資料
const fetchFloorList = async (building_guid) => {
const res = await getAssetFloorList(building_guid);
floorList.value = res.data[0]?.floors.map((d) => ({
...d,
title: d.full_name,
key: d.floor_guid,
})) || [];
floorList.value =
res.data[0]?.floors.map((d) => ({
...d,
title: d.full_name,
key: d.floor_guid,
})) || [];
};
// 獲取部門資料
const fetchDepartmentList = async () => {
const res = await getDepartmentList();
deptList.value = res.data.map((d) => ({
...d,
title: d.name,
key: d.id,
})) || [];
deptList.value =
res.data.map((d) => ({
...d,
title: d.name,
key: d.id,
})) || [];
};
// 獲取2D、3D顯示與否
const fetchDashboard2D3D = async (BuildingId) => {
const res = await getDashboard2D3D(BuildingId);
showForgeArea.value = res.data.is3DEnabled || false;
previewImageExt.value = res.data.previewImageExt || "";
};
// 清除localStorage建築物
const deleteBuilding = () => {
localStorage.removeItem("CviBuildingList");
localStorage.removeItem("CviBuilding");
buildings.value = [];
selectedBuilding.value = null;
};
// 當 selectedBuilding 改變時,更新 floorList 和 deptList
watch(selectedBuilding, async (newBuilding) => {
if (newBuilding) {
await Promise.all([fetchFloorList(newBuilding.building_guid), fetchDepartmentList()]);
localStorage.setItem("CviBuilding", JSON.stringify(newBuilding));
await Promise.all([
fetchFloorList(newBuilding.building_guid),
fetchDepartmentList(),
fetchDashboard2D3D(newBuilding.building_guid),
]);
}
});
@ -94,9 +122,13 @@ const useBuildingStore = defineStore("buildingInfo", () => {
mainSys,
subSys,
selectedSystem,
showForgeArea,
previewImageExt,
deleteBuilding,
fetchBuildings,
fetchFloorList,
fetchDepartmentList,
fetchDashboard2D3D,
initialize,
};
});

View File

@ -4,7 +4,6 @@ import { ref } from "vue";
const useUserInfoStore = defineStore("userInfo", () => {
const user = ref({
token: "",
expires: 0,
user_name:"",
});

View File

@ -2,38 +2,59 @@ import useGetCookie from "@/hooks/useGetCookie";
import axios from "axios";
const BASEURL = window.env?.VITE_API_BASEURL;
// --- 請求攔截器的共用邏輯 ---
const requestInterceptor = (config) => {
// 確保 headers 物件存在
if (!config.headers) {
config.headers = {};
}
// 1. 取得並附加最新的 Token
const token = useGetCookie("JWT-Authorization");
if (token) {
// 正確做法:修改屬性,而不是覆蓋整個物件
config.headers.Authorization = `Bearer ${token}`;
}
// 2. 取得並附加選定的建築物 GUID
const storedBuilding = localStorage.getItem("CviBuilding");
if (storedBuilding) {
try {
const buildingObject = JSON.parse(storedBuilding);
if (buildingObject && buildingObject.building_guid) {
// 與後端約定好要用哪個標頭,這裡使用 'X-Building-GUID' 作為範例
config.headers["X-Building-GUID"] = buildingObject.building_guid;
}
} catch (e) {
console.error("解析 localStorage 中的 CviBuilding 失敗:", e);
}
}
return config;
};
const requestErrorInterceptor = (error) => {
return Promise.reject(error);
};
// --- 一般 API 實例 ---
const instance = axios.create({
baseURL: BASEURL,
timeout: -1,
headers: { Authorization: `Bearer ${useGetCookie("JWT-Authorization")}` },
timeout: 10000, // 建議設定超時
// 移除靜態 headers
});
// Add a request interceptor
instance.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = useGetCookie("JWT-Authorization");
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// 使用共用的攔截器
instance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
// Add a response interceptor
instance.interceptors.response.use(
function (response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// Do something with response data
const { status, data, headers } = response;
const { data } = response;
return {
...data,
};
return { ...data };
},
function (error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
@ -45,27 +66,16 @@ instance.interceptors.response.use(
}
);
// --- 檔案處理 API 實例 ---
export const fileInstance = axios.create({
baseURL: BASEURL,
timeout: -1,
headers: { Authorization: `Bearer ${useGetCookie("JWT-Authorization")}` },
// 移除靜態 headers
});
// Add a request interceptor
fileInstance.interceptors.request.use(
function (config) {
// Do something before request is sent
const token = useGetCookie("JWT-Authorization");
config.headers = {
Authorization: `Bearer ${token}`,
};
return config;
},
function (error) {
// Do something with request error
return Promise.reject(error);
}
);
// 使用共用的攔截器
fileInstance.interceptors.request.use(requestInterceptor, requestErrorInterceptor);
// Add a response interceptor
fileInstance.interceptors.response.use(

View File

@ -61,9 +61,8 @@ const onReset = () => {
? t('assetManagement.edit_system_category')
: t('assetManagement.add_system_category')
"
:open="open"
:onCancel="onReset"
width="300"
:width="300"
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">
@ -75,7 +74,7 @@ const onReset = () => {
</span></template
>
</Input>
<Input name="system_value" :value="formState" :readonly="props.formState?.id">
<Input name="system_value" :value="formState" :readonly="Boolean(props.formState?.id)">
<template #topLeft>{{ $t("assetManagement.system_value") }}</template>
<template #bottomLeft
><span class="text-error text-base">

View File

@ -95,9 +95,8 @@ const onOk = async () => {
? t('assetManagement.edit_device_category')
: t('assetManagement.add_device_category')
"
:open="open"
:onCancel="onCancel"
width="300"
:width="300"
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">
@ -112,7 +111,7 @@ const onOk = async () => {
<Input
name="system_value"
:value="formState"
:readonly="props.formState?.id"
:readonly="Boolean(props.formState?.id)"
>
<template #topLeft>{{ $t("assetManagement.system_value") }}</template>
<template #bottomLeft
@ -128,7 +127,7 @@ const onOk = async () => {
name="system_parent_id"
:value="formState"
selectClass="border-info focus-within:border-info"
:disabled="props.formState?.id"
:disabled="Boolean(props.formState?.id)"
>
<template #topLeft>{{
$t("assetManagement.system_parent")

View File

@ -4,7 +4,7 @@ import { onMounted, ref, watch, inject, provide, computed } from "vue";
import useSearchParam from "@/hooks/useSearchParam";
import AssetTableAddModal from "./AssetTableAddModal.vue";
import { getOperationCompanyList } from "@/apis/operation";
import { getAssetFloorList } from "@/apis/asset";
import { postMQTTRefresh } from "@/apis/alert";
import dayjs from "dayjs";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
@ -33,7 +33,8 @@ const getAssetData = async () => {
floor: floors.value.find(({ floor_guid }) => d.floor_guid === floor_guid)
?.full_name,
company: companyOptions.value.find(({ id }) => d.operation_id === id),
department: departmentList.value.find(({ id }) => d.department_id === id)?.name,
department: departmentList.value.find(({ id }) => d.department_id === id)
?.name,
buying_date: d?.buying_date
? dayjs(d?.buying_date).format("YYYY-MM-DD")
: "",
@ -44,6 +45,15 @@ const getAssetData = async () => {
}
};
const refreshMQTT = async () => {
const res = await postMQTTRefresh();
if (res.isSuccess) {
openToast("success", t("msg.mqtt_refresh"));
} else {
openToast("error", res.msg, "#outliers_add_table_item");
}
};
onMounted(async () => {
getAssetData();
});
@ -113,8 +123,8 @@ watch(
(newValue) => {
if (newValue.value?.subSys_id) {
getAssetData();
}else{
tableData.value=[];
} else {
tableData.value = [];
}
},
{
@ -162,7 +172,7 @@ const remove = async (id) => {
const res = await deleteAssetItem(id);
if (res.isSuccess) {
getAssetData();
openToast("success", t("msg.delete_success"));
openToast("success", t("msg.delete_success"));
} else {
openToast("error", res.msg);
}
@ -175,14 +185,21 @@ provide("asset_table_data", {
</script>
<template>
<div class="flex justify-start items-center mt-10">
<h3 class="text-xl mr-5">{{ $t("assetManagement.device_list") }}</h3>
<AssetTableAddModal
:openModal="openModal"
:onCancel="onCancel"
:editRecord="editRecord"
:getData="getAssetData"
/>
<div class="flex justify-between items-center mt-10">
<div class="flex">
<h3 class="text-xl mr-5">{{ $t("assetManagement.device_list") }}</h3>
<AssetTableAddModal
:openModal="openModal"
:onCancel="onCancel"
:editRecord="editRecord"
:getData="getAssetData"
/>
</div>
<button class="btn btn-sm btn-add" @click.prevent="refreshMQTT">
<font-awesome-icon :icon="['fas', 'cog']" />{{
$t("alert.reorganization")
}}
</button>
</div>
<Table :columns="columns" :dataSource="tableData" class="mt-3">
<template #bodyCell="{ record, column, index }">

View File

@ -6,7 +6,9 @@ import AssetTableModalLeft from "./AssetTableModalLeft.vue";
import AssetTableModalRight from "./AssetTableModalRight.vue";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import * as yup from "yup";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const { searchParams, changeParams } = useSearchParam();
@ -75,8 +77,11 @@ const onOk = async () => {
main_id: props.editRecord ? props.editRecord.main_id : 0,
});
if (res.isSuccess) {
openToast("success", t("msg.send_successfully"), "#asset_add_table_item");
props.getData();
closeModal();
setTimeout(() => {
closeModal();
}, 1000);
} else {
openToast("error", res.msg, "#asset_add_table_item");
}
@ -102,15 +107,22 @@ const closeModal = () => {
</script>
<template>
<button class="btn btn-sm btn-add mr-3" @click.stop.prevent="openModal" :disabled="!searchParams.subSys_id">
<button
class="btn btn-sm btn-add mr-3"
@click.stop.prevent="openModal"
:disabled="!searchParams.subSys_id"
>
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal
id="asset_add_table_item"
:title="editRecord?.main_id ? $t('assetManagement.edit_device') : $t('assetManagement.add_device')"
:open="open"
:title="
editRecord?.main_id
? $t('assetManagement.edit_device')
: $t('assetManagement.add_device')
"
:onCancel="closeModal"
width="1600"
:width="1600"
>
<template #modalContent>
<form ref="form" class="grid grid-cols-5 gap-5">

View File

@ -3,8 +3,6 @@ import { ref, inject, onBeforeMount, onMounted, watch } from "vue";
import * as yup from "yup";
import "yup-phone-lite";
import useSearchParam from "@/hooks/useSearchParam";
import AssetTableModalLeftInfoIoT from "./AssetTableModalLeftInfoIoT.vue";
import AssetTableModalLeftInfoGraph from "./AssetTableModalLeftInfoGraph.vue";
import AssetTableModalLeftInfoMQTT from "./AssetTableModalLeftInfoMQTT.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import dayjs from "dayjs";
@ -209,8 +207,6 @@ watch(
</Select>
</div>
<AssetTableModalLeftInfoMQTT />
<AssetTableModalLeftInfoGraph />
<AssetTableModalLeftInfoIoT />
</template>
<style lang="scss" scoped></style>

View File

@ -127,7 +127,7 @@ const onCancel = () => {
id="asset_add_dept"
:title="t('assetManagement.department')"
:onCancel="onCancel"
width="400"
:width="400"
>
<template #modalContent>
<form ref="form">

View File

@ -138,7 +138,7 @@ onMounted(async () => {
id="asset_add_graph_item"
:title="t('graphManagement.title')"
:onCancel="onCancel"
width="500"
:width="500"
>
<template #modalContent>
<ul class="menu bg-base-200 rounded-box text-lg w-full mt-3">

View File

@ -225,7 +225,7 @@ const deleteItem = (value) => {
id="asset_add_IoT_item"
:title="t('assetManagement.associated_device')"
:onCancel="onCancel"
width="900"
:width="900"
>
<template #modalContent>
<ButtonGroup

View File

@ -1,6 +1,7 @@
<script setup>
import { onMounted, ref, inject, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import { postMQTTpublish } from "@/apis/asset";
import mqtt from "mqtt";
import dayjs from "dayjs";
@ -93,12 +94,34 @@ const onCancel = () => {
}
countdown.value = 60;
};
const onSubmit = async () => {
const Topic = formState.value.topic_publish;
let Payload = "";
try {
Payload = JSON.stringify(JSON.parse(formState.value.publish_message));
} catch (e) {
openToast("error", t("msg.incorrect_format"), "#asset_add_table_item");
return;
}
try {
const res = await postMQTTpublish({ Topic, Payload });
if (res.isSuccess) {
openToast("success", t("msg.send_successfully"), "#asset_add_table_item");
} else {
openToast("error", res.msg, "#asset_add_table_item");
}
} catch (error) {
openToast("error", t("setting.mqtt_send_error"), "#asset_add_table_item");
}
};
</script>
<template>
<div class="flex w-72">
<div class="flex col-span-2 pb-5">
<Input :value="formState" name="topic">
<template #topLeft>MQTT Topic</template>
<template #topLeft>MQTT subscribe topic</template>
</Input>
<button type="button" class="btn btn-add mt-11 ms-1" @click="openModal">
<font-awesome-icon :icon="['fas', 'cog']" />
@ -106,7 +129,20 @@ const onCancel = () => {
</button>
</div>
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" width="400">
<div class="flex flex-col col-span-2 border-t-gray-400 border-t py-5">
<Input :value="formState" name="topic_publish">
<template #topLeft>MQTT publish topic</template>
</Input>
<Textarea :value="formState" name="publish_message">
<template #topLeft>MQTT messages</template>
</Textarea>
<button type="button" class="btn btn-add mt-6 w-24" @click="onSubmit">
<font-awesome-icon :icon="['far', 'paper-plane']" />
Send
</button>
</div>
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" :width="400">
<template #modalContent>
<!-- 顯示接收到的訊息 -->
<div v-if="receivedMessages.length > 0" class="overflow-y-auto h-96">

View File

@ -2,6 +2,8 @@
import { onMounted, ref, inject, onBeforeMount, watch, computed } from "vue";
import EffectScatter from "@/components/chart/EffectScatter.vue";
import useBuildingStore from "@/stores/useBuildingStore";
import AssetTableModalLeftInfoIoT from "./AssetTableModalLeftInfoIoT.vue";
import AssetTableModalLeftInfoGraph from "./AssetTableModalLeftInfoGraph.vue";
import * as yup from "yup";
import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
@ -115,7 +117,7 @@ const getCoordinate = (position) => {
<template>
<!-- 平面圖 -->
<div class="flex gap-4 mb-5">
<Select
:value="formState"
@ -143,7 +145,7 @@ const getCoordinate = (position) => {
></Input
>
</div>
<div class="relative">
<div class="relative min-h-[70vh]">
<EffectScatter
id="asset_floor_chart"
ref="asset_floor_chart"
@ -157,11 +159,13 @@ const getCoordinate = (position) => {
/>
<div
v-if="!currentFloor?.floor_map_url"
class="absolute top-0 left-0 flex justify-center items-center min-h-[500px] w-full border border-stone-900 shadow-lg bg-sub-success bg-opacity-25 rounded-md"
class="absolute top-0 left-0 flex justify-center items-center min-h-[70vh] w-full border border-stone-900 shadow-lg bg-sub-success bg-opacity-25 rounded-md"
>
<p class="text-2xl">{{ $t("assetManagement.add_floor_text") }}</p>
</div>
</div>
<AssetTableModalLeftInfoGraph />
<AssetTableModalLeftInfoIoT />
</template>
<style lang="scss" scoped></style>

View File

@ -2,7 +2,7 @@
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
import Account from "./components/Account.vue";
import Role from "./components/Role.vue";
import { computed, watch, onBeforeMount } from "vue";
import { computed, watch, onBeforeMount, markRaw } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
@ -18,13 +18,13 @@ const initializeItems = () => {
title: t("accountManagement.account_title"),
key: "account",
active: true,
component: Account,
component: markRaw(Account),
},
{
title: t("accountManagement.role_title"),
key: "role",
active: false,
component: Role,
component: markRaw(Role),
},
]);
};

View File

@ -127,7 +127,7 @@ const onOk = async () => {
id="account_user_modal"
:title="formState?.Id ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="710"
:width="710"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -9,7 +9,7 @@ const { t } = useI18n();
const { openToast } = inject("app_toast");
const props = defineProps({
account: String,
account: Object,
});
const formState = ref({
@ -48,7 +48,7 @@ const onOk = async () => {
id="account_user_password_modal"
:title="t('accountManagement.change_password')"
:onCancel="onCancel"
width="710"
:width="710"
>
<template #modalContent>
<p class="mt-10 text-3xl">{{ account.Name }}</p>

View File

@ -13,7 +13,7 @@ import useActiveBtn from "@/hooks/useActiveBtn";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
selectedRole: String,
selectedRole: Object,
cancelModal: Function,
disabled: Boolean,
update: Function,

View File

@ -2,7 +2,7 @@
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
import AlertQuery from "./components/AlertQuery/AlertQuery.vue";
import AlertSetting from "./components/AlertSetting/AlertSetting.vue";
import { computed, watch, onBeforeMount } from "vue";
import { computed, watch, onBeforeMount, markRaw } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
@ -18,13 +18,13 @@ const initializeItems = () => {
title: t("alert.query_title"),
key: "Query",
active: true,
component: AlertQuery,
component: markRaw(AlertQuery),
},
{
title: t("alert.setting_title"),
key: "Setting",
active: false,
component: AlertSetting,
component: markRaw(AlertSetting),
},
]);
};

View File

@ -160,7 +160,7 @@ watch(
id="alert_action_item"
:title="t('alert.repair_order')"
:onCancel="onCancel"
width="710"
:width="710"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
@ -199,7 +199,7 @@ watch(
{
key: 1,
value: 1,
title: $t('alert.maintenance'),
title: $t('operation.maintenance'),
},
{
key: 2,

View File

@ -94,9 +94,8 @@ const closeModal = () => {
<Modal
id="notify_add_table_item"
:title="t('alert.notify_list')"
:open="open"
:onCancel="closeModal"
width="300"
:width="300"
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">
@ -128,7 +127,7 @@ const closeModal = () => {
<p class="text-light text-base">{{ $t("alert.notify_items") }}</p>
<AlertNoticesTable
:SaveCheckAuth="SaveCheckAuth"
:NoticeData="[noticeList[2]]"
:NoticeData="[noticeList[1]]"
:onChange="onChange"
/>
<span class="text-error text-base">

View File

@ -119,9 +119,8 @@ const closeModal = () => {
<Modal
id="outliers_add_table_item"
:title="t('alert.alarm_settings')"
:open="open"
:onCancel="closeModal"
width="710"
:width="710"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -10,17 +10,17 @@ const { locale } = useI18n();
const timesList = ref([]);
const noticeList = ref([]);
const timesListData = async () => {
const res = await getAlarmScheduleList();
timesList.value = res.data.map((items) => ({
...items,
key:items.id,
schedule_array: JSON.parse(items.schedule_json).map((time, index) => ({
day: index + 1,
time,
})),
}));
};
// const timesListData = async () => {
// const res = await getAlarmScheduleList();
// timesList.value = res.data.map((items) => ({
// ...items,
// key:items.id,
// schedule_array: JSON.parse(items.schedule_json).map((time, index) => ({
// day: index + 1,
// time,
// })),
// }));
// };
const NoticeListData = async () => {
const res = await getNoticeList(locale.value);
@ -32,11 +32,15 @@ watch(locale, () => {
});
onMounted(() => {
timesListData();
// timesListData();
NoticeListData();
});
provide("notify_table", { timesList, noticeList, timesListData });
provide("notify_table", {
timesList,
noticeList,
// timesListData
});
</script>
<template>

View File

@ -16,14 +16,14 @@ import { twMerge } from "tailwind-merge";
const store = useBuildingStore();
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
let intervalId = null;
const energyCostData = ref(null);
const energyCostData = ref({});
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const imgBaseUrl = ref("");
const formState = ref({
building_guid: null,
floor_guid: "all",
department_id: "all",
});
// 2D/3D
const showForgeArea = ref(true);
const getEnergyCostData = async (params) => {
const res = await getEnergyCost(params);
@ -40,6 +40,18 @@ watch(
{ immediate: true, deep: true }
);
watch(
() => store.previewImageExt,
(newExt) => {
if (formState.value.building_guid) {
imgBaseUrl.value = newExt
? `${FILE_BASEURL}/upload/setting/previewImage/${formState.value.building_guid}${newExt}`
: "/build_img.jpg";
}
},
{ immediate: true }
);
watch(
() => formState.value,
(newVal) => {
@ -68,22 +80,26 @@ watch(
{ immediate: true, deep: true }
);
onMounted(() => {
if (showForgeArea.value) {
setItems([
{
title: "2D",
key: "2D",
active: false,
},
{
title: "3D",
key: "3D",
active: true,
},
]);
}
});
watch(
() => store.showForgeArea,
(newVal) => {
if (newVal == true) {
setItems([
{
title: "2D",
key: "2D",
active: false,
},
{
title: "3D",
key: "3D",
active: true,
},
]);
}
},
{ immediate: true }
);
onUnmounted(() => {
clearInterval(intervalId);
@ -94,7 +110,7 @@ onUnmounted(() => {
<div class="flex flex-wrap items-center">
<!-- 建築圖 -->
<div class="w-full xl:w-1/3 relative">
<template v-if="showForgeArea">
<template v-if="store.showForgeArea">
<ButtonConnectedGroup
:items="items"
className="btn-xs absolute right-3 top-6 z-20 bg-slate-800 p-0 rounded-lg "
@ -104,7 +120,7 @@ onUnmounted(() => {
<!-- setting頁面要新增讓他能上傳圖片 -->
<img
alt="build"
src="/build_img.jpg"
:src="imgBaseUrl"
:class="
twMerge(
'absolute w-full h-full transition-opacity duration-300',
@ -114,6 +130,7 @@ onUnmounted(() => {
)
"
/>
<Forge
:class="
twMerge(
@ -129,7 +146,7 @@ onUnmounted(() => {
<template v-else>
<img
alt="build"
src="/build_img.jpg"
:src="imgBaseUrl"
class="area-img-box w-full h-[460px] block relative rounded-sm mt-3"
/>
</template>

View File

@ -26,7 +26,7 @@ const currentEnergyType = ref({
//
const getCurrentEnergyData = () => {
if (!props.energyCostData) {
if (!props.energyCostData || !props.energyCostData.rank) {
return []; //
}

View File

@ -35,6 +35,16 @@ watch(selectedBtn, (newVal, oldVal) => {
detailData.value = props.modalData[newVal.key];
}
});
watch(
() => props.modalData,
(newVal, oldVal) => {
if (newVal) {
changeActiveBtn(items.value[0]);
}
},
{ immediate: true }
);
</script>
<template>
@ -51,13 +61,13 @@ watch(selectedBtn, (newVal, oldVal) => {
}
"
/>
<Button
<button
type="link"
class="btn-link btn-text-without-border px-2"
@click="onCancel"
>
<font-awesome-icon :icon="['fas', 'times']" class="text-[#a5abb1]" />
</Button>
</button>
</div>
</template>
<template #modalContent>
@ -78,7 +88,7 @@ watch(selectedBtn, (newVal, oldVal) => {
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("table.time") }}
{{ $t("operation.updated_time") }}
</th>
</tr>
</thead>

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, markRaw } from "vue";
import { useRoute } from "vue-router";
import EnergyChart from "./components/EnergyChart/EnergyChart.vue";
@ -14,12 +14,12 @@ const updateComponent = () => {
if (main_system_id === "energy_chart") {
if (sub_system_id === "chart") {
currentComponent.value = EnergyChart;
currentComponent.value = markRaw(EnergyChart);
} else {
currentComponent.value = EnergyHistoryTable;
currentComponent.value = markRaw(EnergyHistoryTable);
}
} else if (main_system_id === "energy_report") {
currentComponent.value = EnergyReport;
currentComponent.value = markRaw(EnergyReport);
} else {
currentComponent.value = null;
}

View File

@ -42,7 +42,7 @@ const onOk = async () => {
const res = await postEditCarbonValue({
...values,
"building_guid":store.selectedBuilding.building_guid,
building_guid: store.selectedBuilding.building_guid,
});
if (res.isSuccess) {
props.getData();
@ -67,19 +67,24 @@ const closeModal = () => {
</script>
<template>
<button class="btn btn-sm btn-success ms-auto me-3" @click.stop.prevent="openModal">
<button
class="btn btn-sm btn-success ms-auto me-3"
@click.stop.prevent="openModal"
>
{{ $t("button.edit") }}
</button>
<Modal
id="carbon_emission_item"
:title="t('energy.edit_carbon_emission')"
:onCancel="closeModal"
width="300"
:width="300"
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">
<Input :value="formState" class="w-full" name="coefficient">
<template #topLeft>{{$t('energy.carbon_emission_coefficient')}}</template>
<template #topLeft>{{
$t("energy.carbon_emission_coefficient")
}}</template>
<template #bottomLeft>
<span class="text-error text-base">
{{ formErrorMsg.coefficient }}

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, nextTick, watch, inject } from "vue";
import { ref, onMounted, watch, inject } from "vue";
import * as echarts from "echarts";
import { getRealTimeDist } from "@/apis/energy";
import { useI18n } from "vue-i18n";
@ -7,6 +7,7 @@ const { search_data } = inject("energy_data");
const { t } = useI18n();
const chartDiv = ref(null);
let myChart = null; // myChart
const chartOption = {
tooltip: {
@ -91,12 +92,18 @@ const loadData = async (value) => {
chartOption.series[0].links = links;
//
const myChart = echarts.init(chartDiv.value);
if (myChart) {
myChart.dispose(); //
}
myChart = echarts.init(chartDiv.value);
myChart.setOption(chartOption);
}
} else {
//
echarts.init(chartDiv.value).clear();
//
if (myChart) {
myChart.dispose(); //
myChart = null;
}
}
};
@ -115,6 +122,12 @@ watch(
deep: true,
}
);
onMounted(() => {
//
myChart = echarts.init(chartDiv.value);
myChart.setOption(chartOption);
});
</script>
<template>
@ -138,4 +151,4 @@ ul li:last-child:after {
@apply absolute top-0 bottom-0 left-full block w-full h-[1px] bg-slate-600 m-auto z-10;
content: "";
}
</style>
</style>

View File

@ -28,6 +28,7 @@ const {
} = useActiveBtn("multiple");
const taipower_data = ref([]);
const yearly_taipower_data = ref([]);
const carbonValue = ref(null);
const search_data = computed(() => {
return {
@ -41,9 +42,10 @@ const search_data = computed(() => {
const getData = async (value) => {
const res = await getTaipower(value);
if (res.isSuccess) {
taipower_data.value = res.data
? res.data.sort((a, b) => a.month.localeCompare(b.month))
taipower_data.value = res.data?.monthlyUsage
? res.data?.monthlyUsage.sort((a, b) => a.month.localeCompare(b.month))
: [];
yearly_taipower_data.value = res.data?.yearlyUsage;
}
};
@ -96,7 +98,12 @@ watch(
}
);
provide("energy_data", { taipower_data, search_data, carbonValue });
provide("energy_data", {
taipower_data,
yearly_taipower_data,
search_data,
carbonValue,
});
</script>
<template>

View File

@ -81,7 +81,7 @@ const onCancel = () => {
id="immediate_demand_add_item"
:title="t('energy.edit_automatic_demand')"
:onCancel="closeModal"
width="300"
:width="300"
>
<template #modalContent>
<form ref="form" class="mt-5 flex flex-col items-center">

View File

@ -4,7 +4,7 @@ import { twMerge } from "tailwind-merge";
import { useI18n } from "vue-i18n";
const { t, locale } = useI18n();
const { taipower_data } = inject("energy_data");
const { taipower_data, yearly_taipower_data } = inject("energy_data");
const daysInMonth = (month) => {
const [year, monthNumber] = month.split("-");
@ -19,7 +19,10 @@ const calculateData = () => {
item.month.startsWith(currentYear)
);
const totalElecBills = filteredData.reduce((sum, item) => sum + item.costTotal, 0);
const totalElecBills = filteredData.reduce(
(sum, item) => sum + item.costTotal,
0
);
const latestMonthData = filteredData[filteredData.length - 1];
const latestMonth = latestMonthData ? latestMonthData.month : "";
const monthDays = latestMonth ? daysInMonth(latestMonth) : 0;
@ -37,29 +40,53 @@ const calculateData = () => {
);
return [
{ title: t("energy.elec_bills"), data: totalElecBills.toLocaleString() },
{
title: t("energy.elec_bills"),
data: totalElecBills.toLocaleString(),
past: yearly_taipower_data.value.costTotalGrowthPercent
? yearly_taipower_data.value.costTotalGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_elec_charges"),
time: monthTxt,
data: latestMonthData ? latestMonthData.costTotal.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.costTotalGrowthPercent
? latestMonthData.costTotalGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.year_carbon_emission"),
data: totalCarbonEmission.toLocaleString(),
past: yearly_taipower_data.value.carbonGrowthPercent
? yearly_taipower_data.value.carbonGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_carbon_emission"),
time: monthTxt,
data: latestMonthData ? latestMonthData.carbon.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.carbonGrowthPercent
? latestMonthData.carbonGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.year_elec_consumption"),
data: totalElecConsumption.toLocaleString(),
past: yearly_taipower_data.value.kWhGrowthPercent
? yearly_taipower_data.value.kWhGrowthPercent.toLocaleString()
: 0,
},
{
title: t("energy.interval_elec_consumption"),
time: monthTxt,
data: latestMonthData ? latestMonthData.kWh.toLocaleString() : 0,
past:
latestMonthData && latestMonthData.kWhGrowthPercent
? latestMonthData.kWhGrowthPercent.toLocaleString()
: 0,
},
];
};
@ -95,7 +122,9 @@ watch(
>
<div class="title">{{ item.title }}</div>
<div v-if="item.time" class="time">{{ item.time }}</div>
<div class="data">{{ item.data }}</div>
<div class="data">
<span>{{ item.data }}</span> / <span>{{ item.past }}</span>
</div>
</div>
</div>
</template>

View File

@ -40,9 +40,15 @@ const submit = async (e, type = "") => {
if (type === "export") {
const res = await getHistoryExportData({
type: searchParams.value.selectedType,
...params,
...searchParams.value,
Type:
route.params.type != 1
? 2
: searchParams.value.Type
? searchParams.value.Type
: 1,
table_type: route.params.type,
}).catch((err) => {
isToastOpen.value = {
open: true,

View File

@ -132,7 +132,7 @@ const columns = computed(() => {
key: "endTime",
},
{
title: t("energy.ectricity_classification"),
title: t("energy.electricity_classification"),
key: "elecType",
},
{

View File

@ -35,9 +35,23 @@ const columns = computed(() => {
if (tableData.value && tableData.value.length > 0) {
const firstDataItem = tableData.value[0];
if (firstDataItem && firstDataItem.data) {
firstDataItem.data.forEach((item, index) => {
const sortedData = [...firstDataItem.data].sort((a, b) =>
dayjs(a.time).valueOf() - dayjs(b.time).valueOf()
);
sortedData.forEach((item, index) => {
let formatString = "MM/DD"; //
switch (route.params.type) {
case "1":
formatString = "MM/DD";
break;
case "2":
//
const startOfWeek = dayjs(item.time).startOf('week');
const endOfWeek = dayjs(item.time).endOf('week');
formatString = `${startOfWeek.format("MM/DD")}-${endOfWeek.format("MM/DD")}`;
break;
case "3":
formatString = "YYYY/MM";
break;
@ -45,7 +59,7 @@ const columns = computed(() => {
formatString = "YYYY";
break;
default:
formatString = "MM/DD"; // case 1 case 2 "MM-DD"
formatString = "MM/DD ";
break;
}
const formattedTime = dayjs(item.time).format(formatString);
@ -73,8 +87,13 @@ const dataSource = computed(() => {
return tableData.value.map((item) => {
let subtotalValue = 0;
const newData = {}; // data 便 Table
if (item.data && item.data.length > 0) {
item.data.forEach((dataItem, index) => {
const sortedData = [...item.data].sort((a, b) =>
dayjs(a.time).valueOf() - dayjs(b.time).valueOf()
);
if (sortedData && sortedData.length > 0) {
sortedData.forEach((dataItem, index) => {
const value = parseFloat(dataItem.value || 0);
subtotalValue += value;
// newData key columns key

View File

@ -126,7 +126,7 @@ watch(
<button class="btn btn-add mr-3" @click.stop.prevent="openModal">
<font-awesome-icon :icon="['fas', 'plus']" />{{ $t("button.add") }}
</button>
<Modal id="graph_add_item" :title="t('graphManagement.upload')" :onCancel="onCancel" width="800">
<Modal id="graph_add_item" :title="t('graphManagement.upload')" :onCancel="onCancel" :width="800">
<template #modalContent>
<form ref="form" class="mt-5">
<div class="mb-2">

View File

@ -0,0 +1,50 @@
<script setup>
import { ref, computed, watch, onUnmounted } from "vue";
import SysMap from "./components/SysMap.vue";
import SysProgress from "./components/SysProgress.vue";
import ElecRank from "./components/ElecRank.vue";
import ElecTrends from "./components/ElecTrends.vue";
import ElecCompare from "./components/ElecCompare.vue";
</script>
<template>
<div class="grid grid-cols-1 xl:grid-cols-4 gap-4 my-2">
<div class="col-span-1 grid grid-cols-1 md:grid-cols-2 md:grid-rows-1 xl:grid-cols-1 xl:grid-rows-2 gap-4">
<div class="area-img-box">
<img
alt="build"
src="/build_img.jpg"
class="w-full object-cover border-cyan-400 shadow-cyan-500/40"
/>
<p
class="p-4 h-full text-gray-100 text-base font-light bg-gray-800/60 backdrop-blur-md border-t border-cyan-400/30 shadow-inner"
>
深耕電子精密連接器光通信元件軟性排線線纜組件PCBA電子機板電子成品專業製造廠並代理電子零組件做為整合行銷公司創立於1990年產品行銷全球以穩定快速以及高品質知名;
未來瀚荃會持續精進提供更快更好以及高附加價值的產品與服務來滿足您的需求
</p>
</div>
<!--在線狀態-->
<SysProgress />
</div>
<div class="col-span-2 h-full border border-cyan-400 shadow-md shadow-cyan-500/40">
<img
src="/CviLux_globalmap.png"
alt=""
class="w-full h-full"
/>
</div>
<div class="col-span-1 grid grid-cols-1 xl:grid-rows-3 gap-4">
<ElecRank />
<ElecTrends
/>
<ElecCompare />
</div>
</div>
</template>
<style lang="scss" scoped>
.area-img-box {
@apply border border-light-info bg-gray-900/80 backdrop-blur-lg relative overflow-hidden shadow-md shadow-blue-300;
}
</style>

View File

@ -0,0 +1,316 @@
<script setup>
import { ref, onMounted, watch, computed, onUnmounted } from "vue";
import * as echarts from "echarts";
import { getSystemEnergyCostGrowth } from "@/apis/headquarters";
import BarChart from "@/components/chart/BarChart.vue";
import { useI18n } from "vue-i18n";
const { locale, t } = useI18n();
const energyCostGrowthData = ref({ day: [], week: [], month: [], year: [] });
const chartData = ref([]);
const currentType = ref({
name: "day",
});
const energyTypeList = ref([
{
title: t("dashboard.daily_relative_change"),
key: "day",
},
{
title: t("dashboard.weekly_relative_change"),
key: "week",
},
{
title: t("dashboard.monthly_relative_change"),
key: "month",
},
{
title: t("dashboard.yearly_relative_change"),
key: "year",
},
]);
let intervalId = null;
const labels = computed(() => {
switch (currentType.value.name) {
case "day":
return [t("dashboard.today"), t("dashboard.yesterday")];
case "week":
return [t("dashboard.this_week"), t("dashboard.last_week")];
case "month":
return [t("dashboard.this_month"), t("dashboard.last_month")];
case "year":
return [t("dashboard.this_year"), t("dashboard.last_year")];
default:
return [t("dashboard.today"), t("dashboard.yesterday")];
}
});
const barWidth = 30; // Set barWidth
const barChartOptions = computed(() => ({
xAxis: {
type: "category",
data: chartData.value.map((item) => item.name),
axisLine: { lineStyle: { color: "#fff" } },
},
yAxis: { type: "value", show: false },
grid: {
left: "-10%",
right: "1%",
bottom: "3%",
top: "10%",
containLabel: true,
},
series: [
{
name: "當前",
data: chartData.value.map((item) => item.current),
type: "bar",
barWidth: barWidth,
barGap: "-10%",
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#186B80" },
{ offset: 1, color: "#50C3E3" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.3)",
shadowOffsetY: 2,
shadowOffsetX: 5,
},
z: 3,
},
{
name: "對比",
data: chartData.value.map((item) => item.last),
type: "bar",
barWidth: barWidth,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#988F2C" },
{ offset: 1, color: "#FFF26D" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.3)",
shadowOffsetY: 2,
shadowOffsetX: 5,
},
},
{
// this top
z: 6,
type: "pictorialBar",
symbolPosition: "end",
data: chartData.value.map((item) => item.current),
symbol: "diamond",
symbolOffset: ["-45%", "-50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#50C3E3",
},
},
{
// this bot
z: 6,
type: "pictorialBar",
symbolPosition: "start",
data: chartData.value.map((item) => item.current),
symbol: "diamond",
symbolOffset: ["-45%", "50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#50C3E3",
},
},
{
// last top
z: 3,
type: "pictorialBar",
symbolPosition: "end",
data: chartData.value.map((item) => item.last),
symbol: "diamond",
symbolOffset: ["45%", "-50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#FFF26D",
},
},
{
// last bot
z: 3,
type: "pictorialBar",
symbolPosition: "start",
data: chartData.value.map((item) => item.last),
symbol: "diamond",
symbolOffset: ["45%", "50%"],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
borderWidth: 0,
color: "#FFF26D",
},
},
],
tooltip: {
trigger: "axis",
axisPointer: { type: "shadow" },
formatter: function (params) {
let tooltipText = `<div>${params[0].axisValueLabel}</div>`;
const filteredParams = params.filter((item) => item.seriesType === "bar");
filteredParams.forEach((item) => {
tooltipText += `<div>${item.marker} ${item.value}</div>`;
});
return tooltipText;
},
},
}));
async function fetchEnergyCostGrowth() {
try {
const res = await getSystemEnergyCostGrowth();
energyCostGrowthData.value = res.data || {
day: [],
week: [],
month: [],
year: [],
};
updateChartData();
} catch (error) {
console.error("Error fetching energy cost growth:", error);
energyCostGrowthData.value = { day: [], week: [], month: [], year: [] };
chartData.value = [];
}
}
function updateChartData() {
const list = energyCostGrowthData.value[currentType.value.name] || [];
chartData.value = list.map((item) => ({
name: item.name,
current: item.current,
last: item.last,
difference: ((item.current ?? 0) - (item.last ?? 0)).toFixed(2),
percentage: item.percentage,
}));
}
watch(
() => currentType.value.name,
(newValue) => {
if (newValue) {
updateChartData();
}
},
{
immediate: true,
}
);
onMounted(() => {
fetchEnergyCostGrowth();
if (intervalId) {
clearInterval(intervalId);
}
intervalId = setInterval(() => {
fetchEnergyCostGrowth();
}, 60 * 60 * 1000);
});
onUnmounted(() => {
clearInterval(intervalId);
});
</script>
<template>
<div class="flex flex-wrap">
<div class="w-full chart-data relative px-3">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light pt-1 px-1">
{{ $t("dashboard.relative_energy_consumption") }}
</h2>
<Select
:value="currentType"
class="!w-24"
selectClass="border-info focus-within:border-info btn-xs text-xs"
name="name"
Attribute="title"
:options="energyTypeList"
:isTopLabelExist="false"
:isBottomLabelExist="false"
>
</Select>
</div>
<div class="h-[100px]">
<BarChart
id="dashboard_chart_compare"
class="h-full"
:option="barChartOptions"
/>
</div>
<!-- 表格數據展示 -->
<div class="flex justify-between">
<div
v-for="(data, index) in chartData"
:key="index"
class="text-center mx-1"
:style="{ width: 100 / chartData.length + '%' }"
>
<div
class="text-xs bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ labels[0] }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ data.current ?? "-" }}
</div>
<div
class="text-xs bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ labels[1] }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
{{ data.last ?? "-" }}
</div>
<div
class="text-sm bg-cyan-900 p-1 border border-cyan-100 border-opacity-20"
>
<span
:class="{
'text-red-500': data.difference > 0,
'text-green-500': data.difference < 0,
}"
>
{{
data.difference
? (data.difference > 0 ? "+" : "") + data.difference
: "-"
}}
</span>
</div>
</div>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.chart-data:before {
@apply absolute -left-0 -top-2 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background01.svg) center center;
}
.chart-data::after {
@apply absolute -right-1 -bottom-3 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background02.svg) center center;
}
</style>

View File

@ -0,0 +1,140 @@
<script setup>
import { ref, watch, computed, onUnmounted } from "vue";
import { getSystemEnergyCostRank } from "@/apis/headquarters";
import { useI18n } from "vue-i18n";
import useBuildingStore from "@/stores/useBuildingStore";
const store = useBuildingStore();
const { t } = useI18n();
const energyCostData = ref({});
const energyTypeList = ref([
{
title: t("dashboard.today_energy_consumption"),
key: "day",
},
{
title: t("dashboard.this_month_energy_consumption"),
key: "month",
},
]);
const currentEnergyType = ref({
name: "month",
});
let intervalId = null;
const currentEnergyData = computed(() => {
if (!energyCostData.value) {
return [];
}
return currentEnergyType.value.name === "month"
? energyCostData.value?.month || []
: energyCostData.value?.day || [];
});
const getEnergyRank = async () => {
try {
const res = await getSystemEnergyCostRank({
building_ids: store.buildings.map((building) => building.building_guid),
});
energyCostData.value = res.data;
} catch (error) {
console.error("Error fetching energy cost rank:", error);
}
};
watch(
() => store.buildings,
(newBuilding) => {
if (newBuilding) {
getEnergyRank();
if (intervalId) {
clearInterval(intervalId);
}
intervalId = setInterval(() => {
getEnergyRank();
}, 60 * 60 * 1000);
}
},
{ immediate: true }
);
onUnmounted(() => {
clearInterval(intervalId);
});
</script>
<template>
<div class="state-box-col relative">
<!-- 標題和切換按鈕 -->
<div class="flex justify-between items-center mb-2">
<h2 class="font-light relative">
{{ $t("dashboard.energy_ranking") }}
</h2>
<Select
:value="currentEnergyType"
class="!w-24"
selectClass="border-info focus-within:border-info btn-xs text-xs"
name="name"
Attribute="title"
:options="energyTypeList"
:isTopLabelExist="false"
:isBottomLabelExist="false"
>
</Select>
</div>
<!-- 能耗排名列表 -->
<div class="overflow-y-auto" style="height: 200px;">
<table class="table table-sm text-center">
<tbody>
<tr
v-for="(item, index) in currentEnergyData"
:key="index"
:class="[
{ 'text-red-300': index + 1 === 1 },
{ 'text-orange-300': index + 1 === 2 },
{ 'text-yellow-300': index + 1 === 3 },
{ 'text-teal-300': index + 1 > 3 },
]"
>
<td class="px-0 align-top">
<p class="flex items-center">
<font-awesome-icon :icon="['fas', 'crown']" class="me-1" />
{{ index + 1 }}
</p>
</td>
<td class="align-top whitespace-nowrap px-0">
{{ item.site_name }}
</td>
<td class="align-top">{{ item.name }}</td>
<td class="align-top ps-0">{{ item.value }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
.state-box-col {
@apply border-2 border-light-info rounded-sm p-2 text-white relative;
}
.state-box-col:before {
@apply absolute left-0 right-0 -top-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-top.png);
}
.state-box-col:after {
@apply absolute left-0 right-0 -bottom-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-bottom.png);
}
tr td {
@apply text-[13px] text-start;
}
</style>

View File

@ -0,0 +1,208 @@
<script setup>
import { ref, onMounted, watch, onUnmounted } from "vue";
import * as echarts from "echarts";
import { getSystemEnergyCostTrend } from "@/apis/headquarters";
import BarChart from "@/components/chart/BarChart.vue";
import { useI18n } from "vue-i18n";
import dayjs from "dayjs";
import useBuildingStore from "@/stores/useBuildingStore";
const storeBuild = useBuildingStore();
const { t } = useI18n();
const chartData = ref([]);
const buildingList = ref([]);
const energyCostData = ref([]);
const weekComparisonOption = ref({});
const currentType = ref({});
let intervalId = null;
// option
const generateCylinderChartOption = (data) => {
const barWidth = 15;
return {
xAxis: {
type: "category",
data: data.map((item) => item.date),
axisLine: {
lineStyle: {
color: "#fff",
},
},
},
yAxis: {
type: "value",
name: "kWh",
axisLine: {
lineStyle: {
color: "#fff",
},
},
splitLine: {
show: false,
},
},
series: [
{
data: data.map((item) => item.energy),
type: "bar",
barWidth: barWidth,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 1, 1, [
{ offset: 0, color: "#1F7B47" },
{ offset: 1, color: "#247E95" },
]),
shadowBlur: 5,
shadowColor: "rgba(0, 0, 0, 0.5)",
shadowOffsetY: 5,
},
},
{
z: 15,
type: "pictorialBar",
symbolPosition: "end",
data: data.map((item) => item.energy),
symbol: "diamond",
symbolOffset: [0, -5],
symbolSize: [barWidth, barWidth * 0.5],
itemStyle: {
color: "#62E39A",
},
},
{
z: 10,
type: "pictorialBar",
data: data.map((item) => item.energy),
symbol: "diamond",
symbolSize: [barWidth, barWidth * 0.5],
symbolOffset: [0, 6],
itemStyle: {
color: "#247E95",
},
},
],
grid: {
left: "0%",
right: "0%",
bottom: "0%",
top: "16%",
containLabel: true,
},
tooltip: {
trigger: "axis",
formatter: function (params) {
const item = params[0];
return `<p>${item.name}</p> <p>${item.marker}Energy consumption : ${item.value}</p>`;
},
},
};
};
const processEnergyData = async () => {
try {
const res = await getSystemEnergyCostTrend({
building_id: currentType.value.name,
});
energyCostData.value = res.data.trend || [];
if (!energyCostData.value || energyCostData.value.length === 0) {
chartData.value = [];
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
return;
}
const dailyData = [...energyCostData.value].sort(
(a, b) => new Date(a.time) - new Date(b.time)
);
chartData.value = dailyData.map((item) => ({
date: dayjs(item.time).format("MM/DD"),
energy: item.value,
}));
weekComparisonOption.value = generateCylinderChartOption(chartData.value);
} catch (error) {
console.error("Error fetching energy cost trend:", error);
}
};
watch(
() => storeBuild.buildings,
(newValue) => {
if (newValue) {
currentType.value = {
name: newValue[0]?.building_guid || "all",
};
buildingList.value = [
...newValue.map((building) => ({
title: building.full_name,
key: building.building_guid,
})),
];
}
},
{
immediate: true,
}
);
// currentType
watch(
() => currentType.value.name,
(newValue) => {
if (newValue) {
processEnergyData();
if (intervalId) {
clearInterval(intervalId);
}
intervalId = setInterval(() => {
processEnergyData();
}, 60 * 60 * 1000);
}
},
{
immediate: true,
}
);
onUnmounted(() => {
clearInterval(intervalId);
});
</script>
<template>
<div class="w-full chart-data relative px-3">
<div class="flex flex-wrap items-center justify-between">
<h2 class="font-light pt-1 px-1">
{{ $t("dashboard.last_30_days_energy_trend") }}
</h2>
<Select
:value="currentType"
class="w-[8.5rem] my-2"
selectClass="border-info focus-within:border-info btn-xs text-xs"
name="name"
Attribute="title"
:options="buildingList"
:isTopLabelExist="false"
:isBottomLabelExist="false"
>
</Select>
</div>
<div class="h-[200px]">
<BarChart
id="dashboard_chart_week_comparison"
class="h-full"
:option="weekComparisonOption"
/>
</div>
</div>
</template>
<style lang="scss" scoped>
.chart-data:before {
@apply absolute -left-0 -top-1 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background01.svg) center center;
}
.chart-data::after {
@apply absolute -right-1 bottom-1 h-10 w-10 bg-no-repeat z-10;
content: "";
background: url(@ASSET/img/chart-data-background02.svg) center center;
}
</style>

View File

@ -0,0 +1,139 @@
<script setup>
import L from "leaflet";
import "leaflet/dist/leaflet.css";
import { onMounted, ref } from "vue";
import { nextTick } from "vue";
const leafletmapContainer = ref(null);
const selectedFactory = ref("");
let map = null;
let markerRefs = [];
const customOptions = {
minWidth: 250,
};
const markers = [
{
position: [31.29834, 120.58319],
popup: {
title: "CCT瀚荃蘇州廠",
img: "https://picsum.photos/id/700/600/400",
deviceNumber: 10,
online: 8,
offline: 2,
},
},
{
position: [29.56301, 106.55156],
popup: {
title: "CCT瀚荃重慶廠",
img: "https://picsum.photos/id/701/600/400",
deviceNumber: 20,
online: 15,
offline: 5,
},
},
{
position: [23.02067, 113.75179],
popup: {
title: "CCT瀚荃東莞廠",
img: "https://picsum.photos/id/702/600/400",
deviceNumber: 30,
online: 25,
offline: 5,
},
},
{
position: [25.16742, 121.44587],
popup: {
title: "CCT瀚荃淡水廠",
img: "https://picsum.photos/id/703/600/400",
deviceNumber: 46,
online: 25,
offline: 21,
},
},
];
onMounted(() => {
map = L.map(leafletmapContainer.value, {
center: [31.35, 113.4],
zoom: 5,
});
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution:
'&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
}).addTo(map);
markerRefs = markers.map(({ position, popup }) => {
const marker = L.marker(position)
.bindPopup(
`
<div class="font-bold text-lg mb-2">${popup.title}</div>
<img src="${popup.img}" class="w-full rounded mb-2" />
<div class="flex justify-between text-base mt-2">
<div class="text-center">
設備總數<br><span class="text-white text-2xl">${popup.deviceNumber}</span>
</div>
<div class="text-center">
在線設備<br><span class="text-green-500 text-2xl">${popup.online}</span>
</div>
<div class="text-center">
離線設備<br><span class="text-red-600 text-2xl">${popup.offline}</span>
</div>
</div>
`,
customOptions
)
.addTo(map);
return marker;
});
});
function focusFactory(idx) {
if (!map || !markerRefs[idx]) return;
const pos = markers[idx].position;
map.flyTo(pos, 6, { animate: true });
markerRefs[idx].openPopup();
}
</script>
<template>
<div class="relative w-full h-full">
<div class="absolute top-4 right-4 z-20 flex items-center gap-2">
<select
id="factory-select"
v-model="selectedFactory"
class="select select-sm bg-cyan-950 rounded-md border-info focus-within:border-info"
@change="focusFactory(selectedFactory)"
>
<option value="" disabled>下屬共計 5 家子企業</option>
<option v-for="(m, idx) in markers" :key="idx" :value="idx">
{{ m.popup.title }}
</option>
</select>
</div>
<div class="leafletmapContainer z-10" ref="leafletmapContainer"></div>
</div>
</template>
<style lang="scss">
.leafletmapContainer {
width: 100%;
height: 100%;
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: #164e63 !important; //
color: #ffffff;
box-shadow: 0 3px 14px rgba(0, 0, 0, 0.4);
}
.leaflet-container a.leaflet-popup-close-button{
color: #fff;
font-size: 1rem;
padding-top: 0.4rem;
}
</style>

View File

@ -0,0 +1,151 @@
<script setup>
import { ref, computed, watch, onUnmounted } from "vue";
import { useRouter } from "vue-router";
import { getSystemStatus } from "@/apis/headquarters";
import useBuildingStore from "@/stores/useBuildingStore";
import SysProgressModal from "./SysProgressModal.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const router = useRouter();
const store = useBuildingStore();
const equipmentData = ref([]);
const modalData = ref({});
let intervalId = null;
const openModal = (item) => {
modalData.value = item;
system_status_modal.showModal();
};
const onCancel = () => {
modalData.value = {};
system_status_modal.close();
};
const getAlarmsInfos = async () => {
try {
const res = await getSystemStatus({
building_ids: store.buildings.map((building) => building.building_guid),
});
const apiData = res.data;
// equipmentData
if (apiData && apiData.alarm) {
equipmentData.value = apiData.alarm.map((item) => ({
label: item.system_name,
online: item.online || 0,
offline: item.offline || 0,
alarm: item.alarm || 0,
}));
}
} catch (error) {
console.error("Error fetching alarm info:", error);
}
};
watch(
() => store.buildings,
(newBuilding) => {
if (newBuilding) {
getAlarmsInfos();
if (intervalId) {
clearInterval(intervalId);
}
intervalId = setInterval(() => {
getAlarmsInfos();
}, 30000);
}
},
{ immediate: true }
);
onUnmounted(() => {
clearInterval(intervalId);
});
</script>
<template>
<SysProgressModal :onCancel="onCancel" :modalData="modalData" />
<div class="w-full state-box-col relative">
<div class="state-box">
<div class="title">
<img class="state-title01" src="@ASSET/img/state-title01.svg" />
<span class="">{{$t("dashboard.system_status")}}</span>
<img class="state-title02" src="@ASSET/img/state-title02.svg" />
</div>
<table class="table table-sm text-center">
<thead>
<tr class="border-cyan-400 text-cyan-100">
<th></th>
<th>{{ $t("alert.online") }}</th>
<th>{{ $t("alert.offline") }}</th>
<th>{{ $t("alert.alarm") }}</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, index) in equipmentData"
:key="index"
class="border-cyan-400 cursor-pointer hover:text-info"
@click.stop.prevent="openModal(item)"
>
<th class="px-0 text-start">{{ item.label }}</th>
<td>
{{ item.online.length }}
</td>
<td>
{{ item.offline.length }}
</td>
<td>
{{ item.alarm.length }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</template>
<style lang="scss" scoped>
.state-box-col:before {
@apply absolute left-0 right-0 -top-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-top.png);
}
.state-box-col:after {
@apply absolute left-0 right-0 -bottom-0.5 m-auto h-2 w-36 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-box-bottom.png);
}
.state-box-col {
@apply border border-light-info shadow-md shadow-blue-300 rounded-sm py-2 px-6 text-white relative;
}
.state-box:after {
@apply absolute right-3 top-3 w-4 h-4 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-title01.svg);
}
.state-box:before {
@apply absolute right-0.5 bottom-5 w-4 h-32 bg-no-repeat bg-center z-10;
content: "";
background-image: url(@ASSET/img/state-ul-background02.svg);
}
.state-box .title {
@apply relative flex items-center mb-1;
}
.state-box .title .state-title01 {
@apply w-4 mr-1.5;
}
.state-box .title .state-title02 {
@apply w-5 ml-1.5;
}
</style>

View File

@ -0,0 +1,132 @@
<script setup>
import { ref, onMounted, defineProps, inject, watch } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps({
onCancel: Function,
modalData: Object,
});
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
const detailData = ref([]);
onMounted(() => {
setItems([
{
title: t("alert.online"),
key: "online",
active: true,
},
{
title: t("alert.offline"),
key: "offline",
active: false,
},
{
title: t("alert.alarm"),
key: "alarm",
active: false,
},
]);
});
watch(selectedBtn, (newVal, oldVal) => {
if (newVal) {
detailData.value = props.modalData[newVal.key];
}
});
watch(
() => props.modalData,
(newVal, oldVal) => {
if (newVal) {
changeActiveBtn(items.value[0]);
}
},
{ immediate: true }
);
</script>
<template>
<Modal id="system_status_modal" :onCancel="onCancel" :width="600">
<template #modalTitle>
<div class="flex items-center justify-between">
<ButtonGroup
:items="items"
:withLine="true"
className="btn-sm"
:onclick="
(e, item) => {
changeActiveBtn(item);
}
"
/>
<button
type="link"
class="btn-link btn-text-without-border px-2"
@click="onCancel"
>
<font-awesome-icon :icon="['fas', 'times']" class="text-[#a5abb1]" />
</button>
</div>
</template>
<template #modalContent>
<div class="overflow-x-auto">
<table class="table text-base mt-5">
<thead>
<tr>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("table.serial_number") }}
</th>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("history.building_name") }}
</th>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("table.name") }}
</th>
<th
class="text-base border text-white text-center bg-cyan-600 bg-opacity-30"
>
{{ $t("operation.updated_time") }}
</th>
</tr>
</thead>
<tbody>
<tr
v-if="detailData?.length > 0"
v-for="(equipment, index) in detailData"
:key="index"
class="hover:bg-gray-700"
>
<td class="border text-white text-center">
{{ index + 1 }}
</td>
<td class="border text-white text-center">
{{ equipment.building_name }}
</td>
<td class="border text-white text-center">
{{ equipment.name }}
</td>
<td class="border text-white text-center">
{{ equipment.time || "-" }}
</td>
</tr>
<tr v-else>
<td colspan="4" class="border text-white text-center">
{{ $t("table.no_data") }}
</td>
</tr>
</tbody>
</table>
</div>
</template>
</Modal>
</template>
<style lang="scss" scoped></style>

View File

@ -1,6 +1,6 @@
<script setup>
import { computed, defineProps, inject, ref, watch } from "vue";
import { getHistoryData, getHistoryExportData } from "@/apis/history";
import { getHistoryData, getHistoryExportReport } from "@/apis/history";
import useSearchParam from "@/hooks/useSearchParam";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
@ -39,8 +39,7 @@ const submit = async (e, type = "") => {
}
if (type === "export") {
const res = await getHistoryExportData({
type: searchParams.value.selectedType,
const res = await getHistoryExportReport({
...params,
...searchParams.value,
}).catch((err) => {
@ -84,15 +83,15 @@ const submitBtns = computed(() => [
btn: "btn-search",
disabled: isSearchButtonDisabled.value,
},
// {
// title: t("button.export"),
// key: "export",
// icon: "download",
// btn: "btn-export",
// active: false,
// onClick: (e) => submit(e, "export"),
// disabled: isSearchButtonDisabled.value,
// },
{
title: t("button.export"),
key: "export",
icon: "download",
btn: "btn-export",
active: false,
onClick: (e) => submit(e, "export"),
disabled: isSearchButtonDisabled.value,
},
]);
const once = ref(false);

View File

@ -18,7 +18,6 @@ import useBuildingStore from "@/stores/useBuildingStore";
import {
getHistoryPoints,
getHistoryData,
getHistoryExportData,
} from "@/apis/history";
import useSearchParam from "@/hooks/useSearchParam";
import dayjs from "dayjs";

View File

@ -110,7 +110,7 @@ const checkedBuilding = computed(() => {
if (
selectedDeviceNumber.value?.filter(
(d) => d.split("_")[1] === building_tag
(d) => d?.split("_")[1] === building_tag
).length === allDevices.length
) {
selected.push(building_tag);

View File

@ -6,10 +6,12 @@ import * as yup from "yup";
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import { useRouter } from "vue-router";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast } = inject("app_toast");
const store = useUserInfoStore();
const storeBuild = useBuildingStore();
const router = useRouter();
let schema = yup.object({
@ -35,7 +37,15 @@ const doLogin = async () => {
const res = await Login(value);
if (res.isSuccess) {
store.user = res.data;
router.replace({ path: "/dashboard" });
localStorage.setItem(
"CviBuildingList",
JSON.stringify(res.data.building_infos)
);
if (res.data.building_infos.map((b) => b.is_headquarter).includes(true)) {
router.replace({ path: "/headquarters" });
} else {
router.replace({ path: "/dashboard" });
}
} else {
openToast("error", res.msg);
}

View File

@ -7,7 +7,7 @@ import { twMerge } from "tailwind-merge";
import { useI18n } from 'vue-i18n';
const { t, locale } = useI18n();
const props = defineProps({
selected: String,
selected: Object,
});
const { searchParams, changeParams } = useSearchParam();

View File

@ -226,7 +226,7 @@ watch(
</script>
<template>
<Modal id="operation_action_item" :onCancel="onCancel" width="710">
<Modal id="operation_action_item" :onCancel="onCancel" :width="710">
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
<template v-if="searchParams?.work_type < 3">

View File

@ -1,5 +1,5 @@
<script setup>
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, markRaw } from "vue";
import { useRoute } from "vue-router";
import Dept from "./components/Dept.vue";
@ -10,6 +10,7 @@ import Building from "./components/Building.vue";
import ElecPriceManagement from "./components/ElecPriceManagement.vue";
import MQTTList from "./components/MQTTList.vue";
import Demand from "./components/Demand.vue";
import ViewModeSetting from "./components/ViewModeSetting.vue";
// import PointList from "./components/PointList.vue";
const route = useRoute();
@ -19,21 +20,23 @@ const updateComponent = () => {
const { main_system_id, sub_system_id } = route.params;
if (sub_system_id === "Department") {
currentComponent.value = Dept;
currentComponent.value = markRaw(Dept);
} else if (sub_system_id === "ElecType") {
currentComponent.value = ElecType;
currentComponent.value = markRaw(ElecType);
} else if (sub_system_id === "Vendor") {
currentComponent.value = Vendor;
currentComponent.value = markRaw(Vendor);
} else if (sub_system_id === "Floor") {
currentComponent.value = Floors;
currentComponent.value = markRaw(Floors);
} else if (sub_system_id === "Building") {
currentComponent.value = Building;
currentComponent.value = markRaw(Building);
} else if (sub_system_id === "ElecPricing") {
currentComponent.value = ElecPriceManagement;
currentComponent.value = markRaw(ElecPriceManagement);
} else if (sub_system_id === "MQTT_Result") {
currentComponent.value = MQTTList;
currentComponent.value = markRaw(MQTTList);
} else if (sub_system_id === "Demand") {
currentComponent.value = Demand;
currentComponent.value = markRaw(Demand);
} else if (sub_system_id === "ViewModeSetting") {
currentComponent.value = markRaw(ViewModeSetting);
}
};

View File

@ -46,7 +46,7 @@ const onOk = async () => {
id="build_modal"
:title="props.formState?.building_guid ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="400"
:width="400"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -113,7 +113,7 @@ watch(
<template>
<div class="flex justify-start items-center mt-10 mb-5">
<h3 class="text-xl mr-5">電表</h3>
<h3 class="text-xl mr-5">{{$t("setting.electricity_meter")}}</h3>
<button
v-if="!isEditing"
class="btn btn-sm btn-add mr-3"

View File

@ -48,7 +48,7 @@ const onOk = async () => {
id="dept_modal"
:title="props.formState?.id ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="400"
:width="400"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -2,7 +2,7 @@
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
import ElecPriceRes from "./ElecPriceRes.vue";
import ElecPriceStd from "./ElecPriceStd.vue";
import { computed, watch, onBeforeMount, ref, provide } from "vue";
import { computed, watch, onBeforeMount, ref, provide, markRaw } from "vue";
import useActiveBtn from "@/hooks/useActiveBtn";
import { useI18n } from "vue-i18n";
import { getTimeElec } from "@/apis/energy";
@ -26,13 +26,13 @@ const initializeItems = () => {
title: t("energy.residential"),
key: "Residential",
active: true,
component: ElecPriceRes,
component: markRaw(ElecPriceRes),
},
{
title: t("energy.standard"),
key: "Standard",
active: false,
component: ElecPriceStd,
component: markRaw(ElecPriceStd),
},
]);
};
@ -93,6 +93,11 @@ provide("time_elec", { sim2, sim3, stand2, stand3, getData });
<h1 class="text-2xl font-extrabold mb-2">
{{ $t("energy.elec_price_list") }}
</h1>
<div class="content-box-background border-info border text-base p-4">
<span class="font-semibold text-info">說明</span>
本系統使用標準型時間電價二段式來試算電費<br />
本系統所提供之時間電價計算結果係以未超過契約容量為前提所進行之估算僅供用電分析與管理參考之用
</div>
<ButtonGroup
:items="items"
:withLine="true"

View File

@ -46,7 +46,7 @@ const onOk = async () => {
id="elec_modal"
:title="props.formState?.id ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="400"
:width="400"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -66,7 +66,7 @@ const onOk = async () => {
id="floor_modal"
:title="props.formState?.floor_guid ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="400"
:width="400"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -95,6 +95,10 @@ const PointsColumns = computed(() => [
title: t("setting.hide_point"),
key: "is_link",
},
{
title: t("setting.hide_switch"),
key: "show_event_switch_btn",
},
{
title: t("assetManagement.operation"),
key: "operation",
@ -256,6 +260,9 @@ watch(
<template v-else-if="column.key === 'is_link'">
{{ record.is_link === 1 ? t("alert.yes") : t("alert.no") }}
</template>
<template v-else-if="column.key === 'show_event_switch_btn'">
{{ record.show_event_switch_btn === true ? t("alert.yes") : t("alert.no") }}
</template>
<template v-else-if="column.key === 'operation'">
<button
class="btn btn-sm btn-success text-white mr-2"

View File

@ -102,6 +102,10 @@ watch(
);
const onOk = async () => {
if (!schemaName.value) {
openToast("error", t("msg.schema_name_required"), "#MQTT_Parse_item");
return;
}
const points = formStates.value.map((state) => ({
PointOrg: state.PointOrg,
PointSys: props.pointsData.find((point) => point.id === state.item_id)
@ -181,13 +185,14 @@ const onCancel = () => {
type="text"
v-model.text="schemaName"
class="input border-info focus-within:border-info"
required
/>
</div>
<table class="table w-1/2 mt-2">
<thead>
<tr>
<th>{{$t("setting.IoT_point_structure")}}</th>
<th>{{$t("setting.system_point_name")}}</th>
<th>{{ $t("setting.IoT_point_structure") }}</th>
<th>{{ $t("setting.system_point_name") }}</th>
</tr>
</thead>
<tbody>

View File

@ -21,6 +21,9 @@ const formState = ref({
decimals: 0,
is_bool: 0,
is_link: 0,
show_event_switch_btn: false,
event_switch_on_message: "",
event_switch_off_message: "",
});
let schema = ref(
yup.object({
@ -42,10 +45,16 @@ const onOk = async () => {
decimals: Number(values.decimals),
is_bool: Number(values.is_bool),
is_link: Number(values.is_link),
event_switch_on_message: values.show_event_switch_btn
? values.event_switch_on_message
: "",
event_switch_off_message: values.show_event_switch_btn
? values.event_switch_off_message
: "",
});
if (res.isSuccess) {
props.getData(props.variable_id);
onCancel();
props.getData(props.variable_id);
} else {
openToast("error", res.msg, "#point_list_item");
}
@ -151,6 +160,43 @@ watch(
>
<template #topLeft>{{ $t("setting.hide_point") }}</template>
</RadioGroup>
<RadioGroup
class="my-2"
name="show_event_switch_btn"
:value="formState"
:items="[
{
key: 1,
value: true,
title: $t('alert.yes'),
},
{
key: 0,
value: false,
title: $t('alert.no'),
},
]"
:required="true"
>
<template #topLeft>{{ $t("setting.hide_switch") }}</template>
</RadioGroup>
<Textarea
v-if="formState.show_event_switch_btn"
:value="formState"
name="event_switch_on_message"
class="w-full my-2"
>
<template #topLeft>{{ $t("setting.switch_on_message") }}</template>
</Textarea>
<Textarea
v-if="formState.show_event_switch_btn"
:value="formState"
name="event_switch_off_message"
class="w-full my-2"
>
<template #topLeft>{{ $t("setting.switch_off_message") }}</template>
</Textarea>
</form>
</template>
<template #modalAction>

View File

@ -60,7 +60,7 @@ const onOk = async () => {
id="company_modal"
:title="props.formState?.id ? t('button.edit') : t('button.add')"
:onCancel="onCancel"
width="710"
:width="710"
>
<template #modalContent>
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">

View File

@ -0,0 +1,95 @@
<script setup>
import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n";
import { posttDashboard2D3D } from "@/apis/dashboard";
import useBuildingStore from "@/stores/useBuildingStore";
import { tr } from "date-fns/locale";
const { openToast, cancelToastOpen } = inject("app_toast");
const buildingStore = useBuildingStore();
const { t } = useI18n();
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const form = ref(null);
const formState = ref({
showForgeArea: buildingStore.showForgeArea,
lorf: buildingStore.previewImageExt
? [
{
src: `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}${buildingStore.previewImageExt}`,
name: `${buildingStore.selectedBuilding.building_guid}`,
ext: `${buildingStore.previewImageExt}`,
},
]
: [],
});
const updateFileList = (files) => {
formState.value.lorf = files;
};
const onShowForgeAreaChange = (e) => {
formState.value.showForgeArea = e.target.checked;
};
const onOk = async () => {
const formData = new FormData();
formData.delete("file");
formData.append("buildingId", buildingStore.selectedBuilding.building_guid);
formData.append("is3DEnabled", formState.value.showForgeArea);
if (formState.value.lorf && formState.value.lorf.length > 0) {
formData.append("file", formState.value.lorf[0]);
}else {
formData.append("removePreviewImage", true);
}
const res = await posttDashboard2D3D(formData);
if (res.isSuccess) {
await buildingStore.fetchDashboard2D3D(
buildingStore.selectedBuilding.building_guid
);
openToast("success", t("msg.edit_successfully"));
//
formState.value.showForgeArea = buildingStore.showForgeArea;
formState.value.lorf = buildingStore.previewImageExt
? [ {
src: `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}/${buildingStore.previewImageExt}`,
name: `${buildingStore.selectedBuilding.building_guid}`,
ext: `${buildingStore.previewImageExt}`,
},]
: [];
}else{
openToast("error", res.msg);
}
};
</script>
<template>
<div class="flex justify-start items-center mt-10 mb-5">
<h3 class="text-xl mr-5">2D / 3D 顯示設定 :</h3>
</div>
<div class="flex gap-5">
<span class="text-lg">3D 模型顯示 : </span>
<input
type="checkbox"
class="toggle toggle-lg toggle-success"
name="showForgeArea"
:checked="formState.showForgeArea"
@change="onShowForgeAreaChange"
/>
</div>
<Upload
class="mt-2 mb-5 max-w-[600px] w-full"
name="oriFile"
:fileList="formState.lorf"
:getFileList="updateFileList"
:multiple="false"
formats="png、jpg"
>
<template #topLeft>首頁2D圖上傳 :</template>
</Upload>
<button type="submit" class="btn btn-outline-success" @click.prevent="onOk">
{{ $t("button.submit") }}
</button>
</template>
<style lang="css" scoped></style>

View File

@ -1,6 +1,6 @@
<script setup>
import { RouterView, useRoute } from "vue-router";
import { computed, watch, provide, ref, onMounted, onBeforeUnmount } from "vue";
import { computed, watch, provide, ref, onMounted, onUnmounted } from "vue";
import SystemFloorBar from "./components/SystemFloorBar.vue";
import SystemDeptBar from "./components/SystemDeptBar.vue";
import useBuildingStore from "@/stores/useBuildingStore";
@ -16,6 +16,7 @@ import SystemFloor from "./SystemFloor.vue";
import { twMerge } from "tailwind-merge";
import dayjs from "dayjs";
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const buildingStore = useBuildingStore();
const statusList = computed(() => {
@ -41,7 +42,7 @@ const floors = ref([]);
const deptData = ref([]);
const companyOptions = ref([]);
const selected_dbid = ref([]);
const showForgeArea = ref(true); // 2D/3D
const imgBaseUrl = ref("");
const getFloors = async () => {
const res = await getAssetFloorList();
@ -137,6 +138,19 @@ watch(
}
);
watch(
() => buildingStore.previewImageExt,
(newValue) => {
if (buildingStore.selectedBuilding && buildingStore.selectedBuilding.building_guid) {
imgBaseUrl.value = buildingStore.previewImageExt
? `${FILE_BASEURL}/upload/setting/previewImage/${buildingStore.selectedBuilding.building_guid}${newValue}`
: "/build_img.jpg";
}
},
{ immediate: true }
);
watch(
() => deptData.value,
(newVal) => {
@ -171,7 +185,7 @@ const updateCurrentFloor = (floor) => {
};
const realtimeData = ref([]);
const timeId = ref(null);
let timeId = null;
const getAllDeviceRealtime = async () => {
//
const fetchData = async () => {
@ -182,15 +196,22 @@ const getAllDeviceRealtime = async () => {
realtimeData.value = res.data;
};
await fetchData(); //
if (timeId) {
clearInterval(timeId);
timeId = null;
}
// 10
timeId.value = setInterval(fetchData, 10000);
timeId = setInterval(fetchData, 10 * 1000);
};
watch(
subscribeData,
(newValue) => {
timeId.value && clearInterval(timeId.value);
console.log("subscribeData changed:", newValue);
if (timeId) {
clearInterval(timeId);
}
newValue.length > 0 && getAllDeviceRealtime();
},
{ deep: true, immediate: true }
@ -249,13 +270,13 @@ const getCurrentInfoModalData = async (e, position, value) => {
document.getElementById("system_info_modal").showModal();
};
const selectedDeviceRealtime = computed(
() =>
realtimeData.value?.find(
({ device_number }) =>
device_number === selectedDevice.value?.value?.device_number
)?.data
);
const selectedDeviceRealtime = computed(() => {
const deviceNumber = selectedDevice.value?.value?.device_number;
if (!deviceNumber) return [];
return realtimeData.value
.filter(item => item.device_number === deviceNumber && Array.isArray(item.data))
.flatMap(item => item.data.map(dataItem => ({ ...dataItem, topic_publish: item.topic_publish })));
});
const clearSelectedDeviceInfo = () => {
selectedDevice.value.value = null;
@ -271,8 +292,11 @@ provide("system_selectedDevice", {
deptData,
});
onBeforeUnmount(() => {
clearInterval(timeId.value);
onUnmounted(() => {
if (timeId) {
clearInterval(timeId);
timeId = null;
}
});
</script>
@ -327,7 +351,7 @@ onBeforeUnmount(() => {
</div>
</div>
<div class="col-span-1 h-full flex flex-col justify-between">
<template v-if="showForgeArea">
<template v-if="buildingStore.showForgeArea">
<SystemMode />
<div class="h-full relative">
<SystemFloor
@ -354,7 +378,7 @@ onBeforeUnmount(() => {
<div class="h-full relative">
<img
alt="build"
src="/build_img.jpg"
:src="imgBaseUrl"
:class="
twMerge(
'absolute w-full h-full transition-opacity duration-300',

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