更新環境變數設定,新增 MQTT 相關 API,並新增總部帳戶管理初版

This commit is contained in:
koko 2025-08-29 10:18:57 +08:00
parent 2dfc2e5297
commit 64f35db51b
17 changed files with 332 additions and 89 deletions

View File

@ -1,4 +1,4 @@
VITE_API_BASEURL = "https://ibms-cvilux-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"

View File

@ -1,4 +1,4 @@
VITE_API_BASEURL = "https://ibms-cvilux-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
# VITE_FORGE_BASEURL = "https://cgems.cvilux-group.com:8088/dist"

View File

@ -1,3 +1,3 @@
VITE_API_BASEURL = "https://ibms-cvilux-demo-api.production.mjmtech.com.tw"
VITE_FILE_API_BASEURL = "https://cgems.cvilux-group.com:8088"
VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"
# VITE_MQTT_BASEURL = "wss://mqttwss.mjm-staging.developers-homelab.net"

View File

@ -36,4 +36,6 @@ export const DELETE_ASSET_ELECTYPE_API = `/AssetManage/DeleteElecType`;
export const POST_ASSET_ELEC_SETTING_API = `/AssetManage/SaveAssetSetting`;
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;
export const POST_ASSET_MQTT_PUBLISH_API = `/api/mqtt/publish`;
export const POST_MQTT_TOPIC_API = `api/Device/MQTTTopicTest`;
export const POST_MQTT_TOPIC_STOP_API = `api/Device/MQTTTopicTestStop`;

View File

@ -27,6 +27,8 @@ import {
DELETE_ASSET_ELECTYPE_API,
POST_ASSET_ELEC_SETTING_API,
POST_ASSET_MQTT_PUBLISH_API,
POST_MQTT_TOPIC_API,
POST_MQTT_TOPIC_STOP_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -359,3 +361,21 @@ export const postMQTTpublish = async ({ Topic, Payload }) => {
code: res.code,
});
};
export const postMqttTopic = async ({ iotTag, Topic }) => {
const res = await instance.post(POST_MQTT_TOPIC_API, { iotTag, Topic });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};
export const postMqttTopicStop = async ({ iotTag, Topic }) => {
const res = await instance.post(POST_MQTT_TOPIC_STOP_API, { iotTag, Topic });
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,
});
};

View File

@ -3,4 +3,6 @@ export const GET_SITES_SYSTEM_ENERGY_COST_RANK_API = `/api/energy-manager/all-si
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`;
export const GET_USER_API = `/api/user/user-list`;

View File

@ -3,6 +3,7 @@ import {
GET_SITES_SYSTEM_ENERGY_COST_RANK_API,
GET_SITES_SYSTEM_ENERGY_COST_TREND_API,
GET_SITES_SYSTEM_ENERGY_COST_GROWTH_API,
GET_USER_API,
} from "./api";
import instance from "@/util/request";
import apihandler from "@/util/apihandler";
@ -37,6 +38,25 @@ export const getSystemEnergyCostTrend = async (building_ids) => {
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,
});
}
export const getUserList = async (params = {}) => {
const {
page = 1,
pageSize = 9999999
} = params;
const requestData = {
Page: page,
PageSize: pageSize
};
const res = await instance.post(GET_USER_API, requestData);
return apihandler(res.code, res.data, {
msg: res.msg,
code: res.code,

View File

@ -23,6 +23,7 @@
"graphManagement": "图资管理",
"AssetManagement": "资产管理",
"accountManagement": "帐号管理",
"UserManagement": "帐号管理",
"Setting": "系统设定",
"energy_analysis": "能源分析",
"consumption_report": "用电报表",
@ -430,6 +431,7 @@
},
"msg": {
"sure_to_delete": "是否确认删除该项目?",
"is_headquarters": "该帐号为总部帐号,是否仍要删除该项目?",
"sure_to_delete_permanent": "是否确认永久删除该项目?",
"delete_success": "删除成功",
"delete_failed": "删除失败",

View File

@ -23,6 +23,7 @@
"graphManagement": "圖資管理",
"AssetManagement": "資產管理",
"accountManagement": "帳號管理",
"UserManagement": "帳號管理",
"Setting": "系統設定",
"energy_analysis": "能耗分析",
"consumption_report": "用電報表",
@ -430,6 +431,7 @@
},
"msg": {
"sure_to_delete": "是否確認刪除該項目?",
"is_headquarters": "該帳號為總部帳號,是否仍要刪除該項目?",
"sure_to_delete_permanent": "是否確認永久刪除該項目?",
"delete_success": "刪除成功",
"delete_failed": "刪除失敗",

View File

@ -23,8 +23,9 @@
"graphManagement": "Graph",
"AssetManagement": "Devices",
"accountManagement": "Account",
"UserManagement": "Account",
"Setting": "Setting",
"energy_analysis": "Energy Analysis",
"energy_analysis": "Energy Analysis",
"consumption_report": "Consumption Report",
"chart_analysis": "Chart Analysis",
"historical_curve": "Historical Curve",
@ -430,6 +431,7 @@
},
"msg": {
"sure_to_delete": "Are you sure to delete this item?",
"is_headquarters": "This account is a headquarters account. Are you sure you want to delete this item?",
"sure_to_delete_permanent": "Are you sure you want to permanently delete this item?",
"delete_success": "Delete successfully",
"delete_failed": "Delete failed",

View File

@ -18,6 +18,7 @@ export default function useForgeHeatmap() {
//create the heatmap
function getSensorValue(device, sensorType, pointData) {
console.log("heatmap", device, realtimeData.value);
const dev = realtimeData.value.find(
({ device_number }) => device_number === device.id
);

View File

@ -10,6 +10,7 @@ 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 UserManagement from "@/views/headquarters/HeadquartersAccountManagement.vue";
import Login from "@/views/login/Login.vue";
import useUserInfoStore from "@/stores/useUserInfoStore";
import useBuildingStore from "@/stores/useBuildingStore";
@ -97,6 +98,11 @@ const router = createRouter({
name: "headquarters",
component: HeadquartersManagement,
},
{
path: "/UserManagement",
name: "UserManagement",
component: UserManagement,
},
{
path: "/mytestfile/mjm",
name: "mytestfile",

View File

@ -12,13 +12,23 @@ const { searchParams, changeParams } = useSearchParam();
const companyOptions = ref([]);
const iotSchemaOptions = ref([]);
const elecTypeOptions = ref([]);
const iotSchemaTag = ref(""); // IOT Schema tagIoT
const getCompany = async () => {
const res = await getOperationCompanyList();
companyOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
};
const getIOTSchemaOptions = async (id) => {
const res = await getIOTSchema(Number(id));
iotSchemaOptions.value = res.data.map((d) => ({ ...d, key: d.id }));
const data = res.data || [];
iotSchemaOptions.value = data.map((d) => ({ ...d, key: d.id }));
// tagIoT使
if (data.length > 0 && data[0].tagIoT) {
iotSchemaTag.value = data[0].tagIoT;
} else {
iotSchemaTag.value = "";
}
};
const getElecType = async () => {
const res = await getElecTypeList();
@ -51,6 +61,7 @@ provide("asset_modal_options", {
elecTypeOptions,
departmentList,
floors,
iotSchemaTag
});
</script>

View File

@ -1,98 +1,122 @@
<script setup>
import { onMounted, ref, inject, watch, computed } from "vue";
import { useI18n } from "vue-i18n";
import { postMQTTpublish } from "@/apis/asset";
import { postMQTTpublish, postMqttTopic, postMqttTopicStop } from "@/apis/asset";
import mqtt from "mqtt";
import dayjs from "dayjs";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const { formState } = inject("asset_table_modal_form");
const BASEURL = import.meta.env.VITE_MQTT_BASEURL;
const { iotSchemaTag } = inject("asset_modal_options");
// MQTT
const mqttClient = ref(null); // MQTT
const receivedMessages = ref([]); //
const countdown = ref(60); // 60
const hasStartedCountdown = ref(false); //
let timer = null; //
let mqttInterval = null;
const mqttCardDataList = ref([]); //
const openModal = () => {
if (!mqttClient.value) {
connectMqtt();
}
const openModal = async () => {
mqtt_test.showModal();
startCountdown(); //
};
const connectMqtt = () => {
const topic = formState.value.topic || ""; //
const mqttHost = `${BASEURL}`;
const protocol = "wss"; // "ws" "wss"
mqttClient.value = mqtt.connect(mqttHost, {
protocol,
reconnectPeriod: 1000, //
username: "admin", // MQTT
password: "mjmadmin@99", // MQTT
port: 443,
});
mqttClient.value.on("connect", () => {
console.log("MQTT 已連接");
if (topic) {
mqttClient.value.subscribe(topic, (err) => {
if (!err) {
console.log(`已訂閱主題: ${topic}`);
} else {
console.error("訂閱失敗: ", err);
}
});
}
});
mqttClient.value.on("message", (topic, message) => {
//
const now = dayjs(); // 使 dayjs()
const timestamp = now.format("YYYY-MM-DD HH:mm:ss");
receivedMessages.value.push({
topic,
message: message.toString(),
timestamp: timestamp,
//
try {
await postMqttTopic({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
clearInterval(timer); //
});
// console.log(" postMqttTopic API ");
} catch (error) {
console.error("首次 postMqttTopic 發送失敗", error);
}
mqttClient.value.on("error", (err) => {
console.error("MQTT 連線錯誤: ", err);
});
// 5
mqttInterval = setInterval(async () => {
try {
const res = await postMqttTopic({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
// console.log("postMqttTopic ", res?.data);
const payload = res?.data;
// payload
if (payload && payload.data && payload.time) {
const timeAlreadyExists = mqttCardDataList.value.some(
(item) => item.time === payload.time
);
if (!timeAlreadyExists) {
mqttCardDataList.value.unshift({
...payload.data,
time: payload.time,
});
//
if (!hasStartedCountdown.value) {
hasStartedCountdown.value = true;
startCountdown();
}
} else {
// console.log("", payload.time);
}
} else {
// console.warn(" time");
}
} catch (error) {
console.error("postMqttTopic 呼叫失敗:", error);
}
}, 5000);
};
const startCountdown = () => {
if (timer) return; //
countdown.value = 60;
timer = setInterval(() => {
if (countdown.value > 0) {
if (countdown.value > 1) {
countdown.value--;
} else {
onCancel(); // 1 onCancel
clearInterval(timer);
timer = null;
onCancel(); // 60
}
}, 1000);
};
const onCancel = () => {
const onCancel = async () => {
//
receivedMessages.value = [];
mqttCardDataList.value = [];
countdown.value = 60;
hasStartedCountdown.value = false;
mqtt_test.close();
// MQTT
if (mqttClient.value) {
mqttClient.value.end();
mqttClient.value = null;
// API
try {
await postMqttTopicStop({
iotTag: iotSchemaTag?.value,
Topic: formState.value.topic,
});
} catch (error) {
console.error("postMqttTopicStop 發送失敗", error);
}
//
// API Interval
if (mqttInterval) {
clearInterval(mqttInterval);
mqttInterval = null;
}
// Timer
if (timer) {
clearInterval(timer);
timer = null; //
timer = null;
}
countdown.value = 60;
};
const onSubmit = async () => {
@ -142,42 +166,69 @@ const onSubmit = async () => {
</button>
</div>
<Modal id="mqtt_test" title="MQTT Topic" :onCancel="onCancel" :width="400">
<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">
<div v-if="mqttCardDataList.length > 0" class="overflow-y-auto h-96 mt-4">
<ul>
<li
v-for="(message, index) in receivedMessages"
v-for="(item, index) in mqttCardDataList"
:key="index"
class="bg-base-200 rounded-md text-wrap shadow shadow-slate-400 p-4 my-2 me-2"
>
<strong class="text-base block text-info mb-2"
>{{ message.topic }} :</strong
<strong
class="text-base block text-info mb-2 flex justify-between items-center"
>
<p class="text-sm break-words">{{ message.message }}</p>
<p class="text-xs text-slate-200 pt-2">
<FontAwesomeIcon :icon="['fas', 'clock']" class="me-1" />
{{ message.timestamp }}
</p>
<span>
<FontAwesomeIcon :icon="['fas', 'clock']" class="me-1" />
{{ dayjs(item.time).format("YYYY-MM-DD HH:mm:ss") }}
</span>
<!-- 只在第一筆資料加上最新標籤 -->
<span
v-if="index === 0"
class="text-xs text-white bg-green-600 px-2 py-1 rounded"
>
New
</span>
</strong>
<!-- 動態顯示除了 time 以外的所有欄位 -->
<template v-for="[key, value] in Object.entries(item)" :key="key">
<p v-if="key !== 'time'" class="text-sm break-words">
<strong>{{ key }}</strong>{{ value }}
</p>
</template>
</li>
</ul>
</div>
<!-- 顯示 loading 和倒計時 -->
<p v-else class="text-center mt-20">
<!-- 顯示 loading 和倒計時只有沒資料才顯示 -->
<p v-if="mqttCardDataList.length === 0" class="text-center mt-20">
<Loading />
<br />
<span class="text-base">{{ countdown }} seconds</span>
<span class="text-base">Loading...</span>
</p>
</template>
<template #modalAction>
<button
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="onCancel"
>
{{ t("button.cancel") }}
</button>
<div class="relative w-full flex justify-end items-center gap-12">
<!-- 資料出現後才顯示倒數計時置中顯示 -->
<div
v-if="mqttCardDataList.length > 0"
class="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 flex items-center gap-2 text-sm"
>
<span>Auto close in</span>
<span>{{ countdown }}s</span>
</div>
<button
type="reset"
class="btn btn-outline-success"
@click.prevent="onCancel"
>
{{ t("button.cancel") }}
</button>
</div>
</template>
</Modal>
</template>

View File

@ -127,8 +127,9 @@ const resetModalForm = () => {
};
};
const removeAccount = async (id) => {
openToast("warning", t("msg.sure_to_delete"), "body", async () => {
const removeAccount = async (id, notHeadquarters) => {
const message = notHeadquarters ? t("msg.sure_to_delete") : t("msg.is_headquarters");
openToast("warning", message, "body", async () => {
await cancelToastOpen();
const res = await delAccount(id);
if (res.isSuccess) {
@ -202,7 +203,7 @@ const removeAccount = async (id) => {
</button>
<button
class="btn btn-sm btn-error text-white"
@click.stop.prevent="() => removeAccount(record.userinfo_guid)"
@click.stop.prevent="() => removeAccount(record.userinfo_guid,record.canDeleted)"
>
{{ $t("button.delete") }}
</button>

View File

@ -0,0 +1,123 @@
<script setup>
import Table from "@/components/customUI/Table.vue";
// import AccountModal from "./AccountModal.vue";
import {
getUserList
} from "@/apis/headquarters";
import { onMounted, ref, inject, computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const { openToast, cancelToastOpen } = inject("app_toast");
const dataSource = ref([]);
const loading = ref(false);
const searchData = ref({
Full_name: "",
Role_full_name: "",
});
const columns = computed(() => [
{
title: t("accountManagement.index"),
key: "index",
},
{
title: t("accountManagement.name"),
key: "full_name",
filter: true,
},
{
title: t("accountManagement.account"),
key: "account",
},
{
title: t("accountManagement.role"),
key: "role_full_name",
},
{
title: t("accountManagement.email"),
key: "email",
},
{
title: t("accountManagement.phone"),
key: "phone",
},
{
title: t("accountManagement.created_at"),
key: "created_at",
},
{
title: t("accountManagement.operation"),
key: "operation",
},
]);
const getDataSource = async () => {
loading.value = true;
const res = await getUserList();
dataSource.value = res.data?.users || [];
loading.value = false;
};
const onSearch = () => {
getDataSource();
};
const onReset = () => {
getDataSource();
};
const getUser = async (id) => {
// const res = await getAccountOneUser(id);
openModal(res.data);
};
onMounted(() => {
getDataSource();
});
</script>
<template>
<h1 class="text-2xl font-extrabold mb-2">
{{ $t("accountManagement.account_title") }}
</h1>
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
<template #beforeTable>
<div class="flex items-center mb-8">
<Input
:placeholder="t('accountManagement.name_placeholder')"
name="Full_name"
:value="searchData"
class="mr-3 w-96"
/>
<Input
:placeholder="t('accountManagement.role_placeholder')"
name="Role_full_name"
:value="searchData"
/>
<button class="btn btn-search ml-5" @click.stop.prevent="onSearch">
<font-awesome-icon :icon="['fas', 'search']" />
{{ $t("button.search") }}
</button>
<button class="btn btn-neutral mx-4" @click.stop.prevent="onReset">
{{ $t("button.reset") }}
</button>
</div>
</template>
<template #bodyCell="{ record, column, index }">
<template v-if="column.key === 'index'">{{ index + 1 }}</template>
<template v-else-if="column.key === 'operation'">
<button
class="btn btn-sm btn-success text-white mr-2"
@click.stop.prevent="() => getUser(record.userinfo_guid)"
>
{{ $t("button.edit") }}
</button>
</template>
<template v-else>
{{ record[column.key] }}
</template>
</template>
</Table>
</template>
<style lang="scss" scoped></style>

View File

@ -191,7 +191,7 @@ const getAllDeviceRealtime = async () => {
const res = await getSystemRealTime(
subscribeData.value.map((d) => d.device_number)
);
console.log(res.data);
console.log("realtimeData",res.data);
realtimeData.value = res.data;
};
await fetchData(); //