fix: 修正異常設定 modal 限定條件上下限必填規則
This commit is contained in:
parent
13d14d0bb6
commit
b986d1b8dd
7
Docker/svc.front/.dockerignore
Normal file
7
Docker/svc.front/.dockerignore
Normal file
@ -0,0 +1,7 @@
|
||||
node_modules
|
||||
.git
|
||||
.gitignore
|
||||
Dockerfile
|
||||
docker-compose.yml
|
||||
README.md
|
||||
.vs
|
@ -1,13 +1,13 @@
|
||||
# Project
|
||||
PROJ_NAME=proj_bims_ils-svc
|
||||
PROJ_NAME=proj_bims_empower
|
||||
|
||||
# Network 網路環境
|
||||
NET_TRAEFIK=net-traefik_svc
|
||||
|
||||
# Image: org/name
|
||||
IMAGE_PROJ_NAME=proj_bims_ils
|
||||
IMAGE_PROJ_NAME=proj_bims_empower
|
||||
IMAGE_NAME=empower-front
|
||||
TAG_VERSION=0.1.0
|
||||
TAG_VERSION=0.1.11
|
||||
|
||||
# Remote
|
||||
REMOTE_URL=harbor.mjm-staging.developers-homelab.net
|
||||
|
24
Docker/svc.front/docker-entrypoint.sh
Normal file
24
Docker/svc.front/docker-entrypoint.sh
Normal file
@ -0,0 +1,24 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# 目標檔案
|
||||
ENV_FILE=/usr/share/nginx/html/env.js
|
||||
|
||||
# 確保目錄存在
|
||||
mkdir -p /usr/share/nginx/html
|
||||
|
||||
# 寫入環境變數
|
||||
cat <<EOF > "$ENV_FILE"
|
||||
window.env = {
|
||||
VITE_API_BASEURL: "${VITE_API_BASEURL:-http://localhost:8080}",
|
||||
VITE_FILE_API_BASEURL: "${VITE_FILE_API_BASEURL:-http://localhost:8081}",
|
||||
VITE_MQTT_BASEURL: "${VITE_MQTT_BASEURL:-ws://localhost:1883}",
|
||||
VITE_APP_TITLE: "${VITE_APP_TITLE:-MyApp}"
|
||||
};
|
||||
EOF
|
||||
|
||||
echo "[Entrypoint] Generated $ENV_FILE:"
|
||||
cat "$ENV_FILE"
|
||||
|
||||
# 執行傳入的 CMD,例如 "nginx -g 'daemon off;'"
|
||||
exec "$@"
|
@ -1,109 +1,34 @@
|
||||
<script setup>
|
||||
import * as echarts from "echarts";
|
||||
import { onMounted, onUnmounted, ref, markRaw } from "vue";
|
||||
import { onMounted, ref, markRaw } from "vue";
|
||||
|
||||
const props = defineProps({
|
||||
/** 初始 option(僅在 init 時套一次;之後請用 ref.chart.setOption 更新) */
|
||||
option: { type: Object, default: () => ({}) },
|
||||
/** 外層 className,保持相容 */
|
||||
class: { type: String, default: "" },
|
||||
/** 容器 id(非必要,但你現有父層有用) */
|
||||
id: { type: String, default: "" },
|
||||
/** 是否自動監聽容器尺寸改變而 resize */
|
||||
autoresize: { type: Boolean, default: true },
|
||||
/** 後續 setOption 預設的 notMerge/lazyUpdate(父層也可自行傳) */
|
||||
defaultNotMerge: { type: Boolean, default: false },
|
||||
defaultLazyUpdate: { type: Boolean, default: true },
|
||||
option: Object,
|
||||
class: String,
|
||||
id: String,
|
||||
});
|
||||
|
||||
const dom = ref(null);
|
||||
/** 暴露給父層的 ECharts instance(markRaw 避免被 Vue 追蹤) */
|
||||
const chart = ref(null);
|
||||
|
||||
let resizeObs = null;
|
||||
let inited = false;
|
||||
let chart = ref(null);
|
||||
let dom = ref(null);
|
||||
|
||||
function init() {
|
||||
if (inited || !dom.value) return;
|
||||
// 初始化一次,不重複 init
|
||||
const instance = echarts.init(dom.value, undefined, {
|
||||
renderer: "canvas",
|
||||
// devicePixelRatio: window.devicePixelRatio || 1, // 如需手動指定
|
||||
});
|
||||
chart.value = markRaw(instance);
|
||||
|
||||
// 初始只套一次 props.option;之後請用 ref.chart.setOption()
|
||||
if (props.option && Object.keys(props.option).length) {
|
||||
chart.value.setOption(props.option, props.defaultNotMerge, props.defaultLazyUpdate);
|
||||
}
|
||||
|
||||
// 自動 resize(容器尺寸或 display 切換)
|
||||
if (props.autoresize) {
|
||||
resizeObs = new ResizeObserver(() => {
|
||||
// 有些面板開闔或 tab 切換會先把容器寬高設為 0,再打開
|
||||
// 用 requestAnimationFrame 確保下一個 frame 再量一次尺寸
|
||||
requestAnimationFrame(() => {
|
||||
if (!chart.value) return;
|
||||
try {
|
||||
chart.value.resize({ animation: { duration: 0 } });
|
||||
} catch {}
|
||||
});
|
||||
});
|
||||
resizeObs.observe(dom.value);
|
||||
}
|
||||
|
||||
// 當頁面從隱藏回到可見時,做一次安全 resize
|
||||
document.addEventListener("visibilitychange", handleVisibility, false);
|
||||
|
||||
inited = true;
|
||||
}
|
||||
|
||||
function handleVisibility() {
|
||||
if (document.visibilityState === "visible" && chart.value) {
|
||||
try {
|
||||
chart.value.resize({ animation: { duration: 0 } });
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
/** 提供一個包裝讓父層可用 ref 調用(可選) */
|
||||
function setOption(option, notMerge = props.defaultNotMerge, lazyUpdate = props.defaultLazyUpdate) {
|
||||
if (!chart.value) return;
|
||||
chart.value.setOption(option, notMerge, lazyUpdate);
|
||||
}
|
||||
function resize() {
|
||||
if (!chart.value) return;
|
||||
chart.value.resize();
|
||||
let echart = echarts;
|
||||
chart.value = markRaw(echart.init(dom.value));
|
||||
chart.value.setOption(props.option);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!inited && dom.value) init();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("visibilitychange", handleVisibility, false);
|
||||
if (resizeObs && dom.value) {
|
||||
try { resizeObs.unobserve(dom.value); } catch {}
|
||||
if (!chart.value && dom.value) {
|
||||
init();
|
||||
}
|
||||
resizeObs = null;
|
||||
|
||||
if (chart.value) {
|
||||
try { chart.value.dispose(); } catch {}
|
||||
}
|
||||
chart.value = null;
|
||||
inited = false;
|
||||
});
|
||||
|
||||
defineExpose({
|
||||
chart, // 父層可直接 chart.setOption(...)
|
||||
setOption, // 或用這個包裝
|
||||
resize, // 需要時手動觸發 resize
|
||||
chart,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 建議外層給明確高度,否則 ECharts 無法正確量到容器尺寸 -->
|
||||
<div :id="id" :class="class" ref="dom" style="width: 100%; height: 100%"></div>
|
||||
<div :id="id" :class="class" ref="dom"></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
@ -202,7 +202,7 @@ watch(
|
||||
class="flex flex-col justify-center w-3 mx-2 relative"
|
||||
@click="() => sort(column.key)"
|
||||
>
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
:icon="['fas', 'sort-up']"
|
||||
:class="
|
||||
twMerge(
|
||||
@ -221,30 +221,19 @@ watch(
|
||||
)
|
||||
"
|
||||
size="lg"
|
||||
/>
|
||||
/> -->
|
||||
</div>
|
||||
<div class="ml-2 relative" v-if="column.filter">
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'filter']"
|
||||
:class="
|
||||
twMerge(
|
||||
filterColumn[column.key] ||
|
||||
selectedFilterItem[column.key].length > 0
|
||||
? 'text-success'
|
||||
: ''
|
||||
)
|
||||
"
|
||||
@click="() => toggleFilterModal(column.key)"
|
||||
/>
|
||||
|
||||
<div class="fixed z-50" v-if="filterColumn[column.key]">
|
||||
<div class="card min-w-max bg-body shadow-xl px-10 py-5">
|
||||
<label
|
||||
class="input input-bordered bg-transparent rounded-lg flex items-center px-2 mb-4 border-success focus-within:border-success"
|
||||
>
|
||||
<font-awesome-icon
|
||||
<!-- <font-awesome-icon
|
||||
:icon="['fas', 'search']"
|
||||
class="w-6 h-6 mr-2 text-success"
|
||||
/>
|
||||
/> -->
|
||||
<input
|
||||
type="text"
|
||||
:placeholder="t('operation.enter_text')"
|
||||
|
@ -1,13 +1,5 @@
|
||||
<script setup>
|
||||
import {
|
||||
ref,
|
||||
defineProps,
|
||||
watch,
|
||||
inject,
|
||||
nextTick,
|
||||
onMounted,
|
||||
toRaw,
|
||||
} from "vue";
|
||||
import { ref, defineProps, watch, inject, onMounted } from "vue";
|
||||
import dayjs from "dayjs";
|
||||
import { postOperationRecord } from "@/apis/alert";
|
||||
import * as yup from "yup";
|
||||
@ -93,28 +85,6 @@ const updateFileList = (files) => {
|
||||
formState.value.lorf = files;
|
||||
};
|
||||
|
||||
// ---------------------- 告警影片儲存位置:顯示 API 的 video_url ----------------------
|
||||
// 重要:你的 <Input name="videoLocation" :value="..."> 會從 value[name] 取值
|
||||
// 所以我們把 videoLocation 寫成 { videoLocation: string } 的物件
|
||||
const videoLocation = ref({ videoLocation: "" });
|
||||
const showTooltip = ref(false);
|
||||
|
||||
async function copyToClipboard() {
|
||||
const text = videoLocation.value.videoLocation || "";
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
// 觸發 tooltip 動畫
|
||||
showTooltip.value = false;
|
||||
await nextTick();
|
||||
showTooltip.value = true;
|
||||
setTimeout(() => {
|
||||
showTooltip.value = false;
|
||||
}, 1500);
|
||||
} catch (err) {
|
||||
console.error("複製失敗:", err);
|
||||
}
|
||||
}
|
||||
|
||||
const onOk = async () => {
|
||||
const formData = new FormData(form.value);
|
||||
formData.delete("oriFile");
|
||||
@ -177,14 +147,12 @@ const onCancel = () => {
|
||||
description: "",
|
||||
lorf: [],
|
||||
};
|
||||
// 重置顯示用的影片路徑
|
||||
videoLocation.value.videoLocation = "";
|
||||
handleErrorReset();
|
||||
updateEditRecord?.(null);
|
||||
alert_action_item.close();
|
||||
};
|
||||
|
||||
// 同步 props.editRecord -> formState / 日期 / 維修項目 / 影片網址
|
||||
// 同步 props.editRecord -> formState / 日期 / 維修項目
|
||||
watch(
|
||||
() => props.editRecord,
|
||||
(newVal) => {
|
||||
@ -203,10 +171,6 @@ watch(
|
||||
formState.value.fix_do = value ?? "";
|
||||
}
|
||||
}
|
||||
// 取 API 回傳的影片位址(與 device_number 同來源物件)
|
||||
videoLocation.value.videoLocation =
|
||||
newVal?.video_url ?? newVal?.videoUrl ?? newVal?.video_path ?? "";
|
||||
debugLog("derived videoLocation", videoLocation.value.videoLocation);
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@ -258,16 +222,8 @@ watch(
|
||||
name="work_type"
|
||||
Attribute="title"
|
||||
:options="[
|
||||
{
|
||||
key: 1,
|
||||
value: 1,
|
||||
title: $t('alert.maintenance'),
|
||||
},
|
||||
{
|
||||
key: 2,
|
||||
value: 2,
|
||||
title: $t('alert.repair'),
|
||||
},
|
||||
{ key: 1, value: 1, title: $t('alert.maintenance') },
|
||||
{ key: 2, value: 2, title: $t('alert.repair') },
|
||||
]"
|
||||
:required="true"
|
||||
:disabled="true"
|
||||
@ -276,8 +232,8 @@ watch(
|
||||
</Select>
|
||||
<Input class="my-2" :value="formState" name="fix_do" :required="true">
|
||||
<template #topLeft>{{ $t("alert.repair_item") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.fix_do }}
|
||||
</span>
|
||||
</template>
|
||||
@ -290,11 +246,11 @@ watch(
|
||||
:disabled="true"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.repair_item_code") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.fix_do_code }}
|
||||
</span></template
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Select
|
||||
:value="formState"
|
||||
@ -306,27 +262,19 @@ watch(
|
||||
:required="true"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.responsible_vendor") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.fix_firm }}
|
||||
</span></template
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</Select>
|
||||
<RadioGroup
|
||||
class="my-2"
|
||||
name="status"
|
||||
:value="formState"
|
||||
:items="[
|
||||
{
|
||||
key: 0,
|
||||
value: 0,
|
||||
title: $t('alert.not_completed'),
|
||||
},
|
||||
{
|
||||
key: 1,
|
||||
value: 1,
|
||||
title: $t('alert.completed'),
|
||||
},
|
||||
{ key: 0, value: 0, title: $t('alert.not_completed') },
|
||||
{ key: 1, value: 1, title: $t('alert.completed') },
|
||||
]"
|
||||
:required="true"
|
||||
>
|
||||
@ -342,11 +290,11 @@ watch(
|
||||
:required="true"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.worker_id") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">
|
||||
{{ formErrorMsg.work_person_id }}
|
||||
</span></template
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</Select>
|
||||
|
||||
<!-- 注意事項 -->
|
||||
@ -354,45 +302,11 @@ watch(
|
||||
<template #topLeft>{{ $t("alert.notice") }}</template>
|
||||
</Textarea>
|
||||
|
||||
<!-- 告警影片儲存位置-->
|
||||
<div class="my-4 w-full">
|
||||
<label class="text-lg">
|
||||
{{ $t("alert.video_storage_location") }}
|
||||
</label>
|
||||
<div class="flex items-center gap-3">
|
||||
<Input
|
||||
class="flex-1"
|
||||
name="videoLocation"
|
||||
:value="videoLocation"
|
||||
readonly
|
||||
/>
|
||||
<div class="relative inline-flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-success"
|
||||
@click.stop="copyToClipboard"
|
||||
>
|
||||
{{ $t("alert.copy") }}
|
||||
</button>
|
||||
|
||||
<transition name="fade">
|
||||
<span
|
||||
v-if="showTooltip"
|
||||
class="absolute left-full ml-4 top-1/2 -translate-y-1/2 text-white text-xs px-2 py-1 bg-gray-800 rounded shadow whitespace-nowrap"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ $t("alert.copied") }}
|
||||
</span>
|
||||
</transition>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 結果描述 -->
|
||||
<Textarea :value="formState" name="description" class="w-full my-2">
|
||||
<template #topLeft>{{ $t("alert.result_description") }}</template>
|
||||
</Textarea>
|
||||
|
||||
<Upload
|
||||
class="my-2"
|
||||
name="oriFile"
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { inject, defineProps, watch, ref, provide } from "vue";
|
||||
import { inject, defineProps, watch, ref } from "vue";
|
||||
import useSearchParam from "@/hooks/useSearchParam";
|
||||
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||
import AlertNoticesTable from "./AlertNoticesTable.vue";
|
||||
@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
|
||||
const { t } = useI18n();
|
||||
const { openToast } = inject("app_toast");
|
||||
const { timesList, noticeList } = inject("notify_table");
|
||||
const { searchParams, changeParams } = useSearchParam();
|
||||
const { searchParams } = useSearchParam();
|
||||
|
||||
const props = defineProps({
|
||||
openModal: Function,
|
||||
@ -19,8 +19,8 @@ const props = defineProps({
|
||||
OptionsData: Object,
|
||||
});
|
||||
|
||||
const form = ref(null);
|
||||
const formState = ref({
|
||||
// ---- 初始表單狀態(統一使用此物件重置) ----
|
||||
const initialFormState = () => ({
|
||||
id: 0,
|
||||
device_number: "",
|
||||
device_name_tag: searchParams.value?.subSys_id,
|
||||
@ -28,7 +28,7 @@ const formState = ref({
|
||||
enable: 0,
|
||||
factor: 1,
|
||||
alarm_value: "",
|
||||
delay: 0,
|
||||
delay: null,
|
||||
highLimit: null,
|
||||
lowLimit: null,
|
||||
highDelay: null,
|
||||
@ -36,20 +36,35 @@ const formState = ref({
|
||||
notices: [],
|
||||
});
|
||||
|
||||
let scheme = yup.object({
|
||||
const form = ref(null);
|
||||
const formState = ref(initialFormState());
|
||||
|
||||
/** 將數字欄位統一做「可空值」轉換:空字串/undefined/null -> null,其餘轉 number */
|
||||
const numNullable = () =>
|
||||
yup
|
||||
.number()
|
||||
.transform((value, originalValue) =>
|
||||
originalValue === "" || originalValue === undefined || originalValue === null
|
||||
? null
|
||||
: value
|
||||
)
|
||||
.nullable();
|
||||
|
||||
// ---- 驗證規則:上下限/延遲一律非必填 ----
|
||||
const scheme = yup.object({
|
||||
device_number: yup.string().required(t("button.required")),
|
||||
points: yup.string().required(t("button.required")),
|
||||
factor: yup.number().nullable(),
|
||||
enable: yup.number().required(),
|
||||
highLimit: yup.number().nullable(),
|
||||
lowLimit: yup.number().nullable(),
|
||||
highDelay: yup.number().nullable(),
|
||||
lowDelay: yup.number().nullable(),
|
||||
factor: numNullable(),
|
||||
enable: numNullable().required(),
|
||||
delay: numNullable(),
|
||||
highLimit: numNullable(),
|
||||
lowLimit: numNullable(),
|
||||
highDelay: numNullable(),
|
||||
lowDelay: numNullable(),
|
||||
// alarm_value 如不需驗證可不放 schema;若要限制型別可在此補上
|
||||
});
|
||||
|
||||
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(
|
||||
scheme.value
|
||||
);
|
||||
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(scheme);
|
||||
|
||||
const SaveCheckAuth = ref([]);
|
||||
const factorNum = ref(1);
|
||||
@ -58,15 +73,30 @@ watch(
|
||||
() => props.editRecord,
|
||||
(newValue) => {
|
||||
if (newValue) {
|
||||
// 將不存在或空字串的數字欄位正規化為 null,避免編輯時被當作「必填」
|
||||
const normalize = (v) =>
|
||||
v === "" || v === undefined ? null : v;
|
||||
|
||||
formState.value = {
|
||||
...initialFormState(),
|
||||
...newValue,
|
||||
delay: normalize(newValue.delay),
|
||||
highLimit: normalize(newValue.highLimit),
|
||||
lowLimit: normalize(newValue.lowLimit),
|
||||
highDelay: normalize(newValue.highDelay),
|
||||
lowDelay: normalize(newValue.lowDelay),
|
||||
};
|
||||
console.log('formState.value',formState.value);
|
||||
|
||||
SaveCheckAuth.value = newValue.notices ? [...newValue.notices] : [];
|
||||
|
||||
SaveCheckAuth.value = Array.isArray(newValue.notices)
|
||||
? [...newValue.notices]
|
||||
: newValue.notices
|
||||
? [newValue.notices]
|
||||
: [];
|
||||
|
||||
if (newValue.factor) {
|
||||
onFactorsChange(newValue.factor);
|
||||
} else {
|
||||
factorNum.value = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -81,7 +111,9 @@ const onNoticesChange = (value, checked) => {
|
||||
if (SaveCheckAuth.value.length === 1 && SaveCheckAuth.value[0] === "") {
|
||||
SaveCheckAuth.value = [];
|
||||
}
|
||||
SaveCheckAuth.value = [...SaveCheckAuth.value, value];
|
||||
if (!SaveCheckAuth.value.includes(value)) {
|
||||
SaveCheckAuth.value = [...SaveCheckAuth.value, value];
|
||||
}
|
||||
} else {
|
||||
SaveCheckAuth.value = SaveCheckAuth.value.filter((v) => v !== value);
|
||||
}
|
||||
@ -96,6 +128,7 @@ const onOk = async () => {
|
||||
device_name_tag: searchParams.value?.subSys_id,
|
||||
notices: SaveCheckAuth.value ? SaveCheckAuth.value : [],
|
||||
});
|
||||
|
||||
if (res.isSuccess) {
|
||||
props.getData();
|
||||
closeModal();
|
||||
@ -105,10 +138,11 @@ const onOk = async () => {
|
||||
};
|
||||
|
||||
const closeModal = () => {
|
||||
formState.value = {};
|
||||
formState.value = initialFormState();
|
||||
SaveCheckAuth.value = [];
|
||||
factorNum.value = 1;
|
||||
handleErrorReset();
|
||||
props.onCancel();
|
||||
factorNum.value = 1;
|
||||
};
|
||||
</script>
|
||||
|
||||
@ -134,12 +168,11 @@ const closeModal = () => {
|
||||
:options="OptionsData.devList"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.device_name") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
{{ formErrorMsg.device_number }}
|
||||
</span></template
|
||||
>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.device_number }}</span>
|
||||
</template>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
:value="formState"
|
||||
class="my-2"
|
||||
@ -149,12 +182,11 @@ const closeModal = () => {
|
||||
:options="OptionsData.alarmPoints"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.item") }}</template>
|
||||
<template #bottomLeft
|
||||
><span class="text-error text-base">
|
||||
{{ formErrorMsg.points }}
|
||||
</span></template
|
||||
>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.points }}</span>
|
||||
</template>
|
||||
</Select>
|
||||
|
||||
<Select
|
||||
:value="formState"
|
||||
class="my-2"
|
||||
@ -166,84 +198,63 @@ const closeModal = () => {
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.qualifications") }}</template>
|
||||
</Select>
|
||||
|
||||
<RadioGroup
|
||||
class="my-2"
|
||||
name="enable"
|
||||
:value="formState"
|
||||
:items="[
|
||||
{
|
||||
key: 1,
|
||||
value: 1,
|
||||
title: $t('alert.enable'),
|
||||
},
|
||||
{
|
||||
key: 0,
|
||||
value: 0,
|
||||
title: $t('alert.not_enabled'),
|
||||
},
|
||||
{ key: 1, value: 1, title: $t('alert.enable') },
|
||||
{ key: 0, value: 0, title: $t('alert.not_enabled') },
|
||||
]"
|
||||
:required="true"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.status") }}</template>
|
||||
<template #topLeft>{{ $t('alert.status') }}</template>
|
||||
</RadioGroup>
|
||||
|
||||
<template v-if="factorNum == 1">
|
||||
<InputNumber :value="formState" class="my-2" name="delay">
|
||||
<template #topLeft>{{ $t("alert.delay") }}</template>
|
||||
<template #topLeft>{{ $t('alert.delay') }}</template>
|
||||
</InputNumber>
|
||||
<span class="text-error text-base ml-1">{{ formErrorMsg.delay }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="factorNum == 2">
|
||||
<div class="flex gap-4 w-full">
|
||||
<InputNumber :value="formState" class="my-2" name="highLimit">
|
||||
<template #topLeft>{{ $t("alert.upper_limit") }}(>=)</template>
|
||||
<template #topLeft>{{ $t('alert.upper_limit') }}(>=)</template>
|
||||
</InputNumber>
|
||||
<InputNumber :value="formState" class="my-2" name="lowLimit">
|
||||
<template #topLeft>{{ $t("alert.lower_limit") }}(<=)</template>
|
||||
<template #topLeft>{{ $t('alert.lower_limit') }}(<=)</template>
|
||||
</InputNumber>
|
||||
</div>
|
||||
<div class="flex gap-4 w-full -mt-2">
|
||||
<span class="text-error text-base flex-1 ml-1">{{ formErrorMsg.highLimit }}</span>
|
||||
<span class="text-error text-base flex-1 ml-1">{{ formErrorMsg.lowLimit }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-4 w-full">
|
||||
<InputNumber :value="formState" class="my-2" name="highDelay">
|
||||
<template #topLeft>{{ $t("alert.highDelay") }}</template>
|
||||
<template #topLeft>{{ $t('alert.highDelay') }}</template>
|
||||
</InputNumber>
|
||||
<InputNumber :value="formState" class="my-2" name="lowDelay">
|
||||
<template #topLeft>{{ $t("alert.lowDelay") }}</template>
|
||||
<template #topLeft>{{ $t('alert.lowDelay') }}</template>
|
||||
</InputNumber>
|
||||
</div>
|
||||
<div class="flex gap-4 w-full -mt-2">
|
||||
<span class="text-error text-base flex-1 ml-1">{{ formErrorMsg.highDelay }}</span>
|
||||
<span class="text-error text-base flex-1 ml-1">{{ formErrorMsg.lowDelay }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-if="factorNum == 3">
|
||||
<Input :value="formState" class="my-2" name="alarm_value">
|
||||
<template #topLeft>{{ $t("alert.warning_value") }}</template>
|
||||
<template #topLeft>{{ $t('alert.warning_value') }}</template>
|
||||
</Input>
|
||||
</template>
|
||||
<!-- <Select
|
||||
:value="formState"
|
||||
class="my-2"
|
||||
selectClass="border-info focus-within:border-info"
|
||||
name="schedule_id"
|
||||
Attribute="schedule_name"
|
||||
:options="timesList"
|
||||
>
|
||||
<template #topLeft>{{ $t("alert.warning_time") }}</template>
|
||||
<template #topRight
|
||||
><button
|
||||
v-if="formState.schedule_id"
|
||||
class="text-base btn-text-without-border"
|
||||
@click="
|
||||
() => {
|
||||
formState.schedule_id = null;
|
||||
}
|
||||
"
|
||||
>
|
||||
<font-awesome-icon
|
||||
:icon="['fas', 'times']"
|
||||
class="text-[#a5abb1] me-1"
|
||||
/>{{ $t("alert.clear") }}
|
||||
</button>
|
||||
</template>
|
||||
</Select> -->
|
||||
|
||||
<div class="w-full mt-5">
|
||||
<p class="text-light text-lg ml-1">
|
||||
{{ $t("alert.warning_method") }}
|
||||
</p>
|
||||
<p class="text-light text-lg ml-1">{{ $t('alert.warning_method') }}</p>
|
||||
<AlertNoticesTable
|
||||
:SaveCheckAuth="SaveCheckAuth"
|
||||
:NoticeData="noticeList"
|
||||
@ -252,19 +263,12 @@ const closeModal = () => {
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<template #modalAction>
|
||||
<button
|
||||
type="reset"
|
||||
class="btn btn-outline-success mr-2"
|
||||
@click.prevent="closeModal"
|
||||
>
|
||||
<button type="reset" class="btn btn-outline-success mr-2" @click.prevent="closeModal">
|
||||
{{ $t("button.cancel") }}
|
||||
</button>
|
||||
<button
|
||||
type="submit"
|
||||
class="btn btn-outline-success"
|
||||
@click.prevent="onOk"
|
||||
>
|
||||
<button type="submit" class="btn btn-outline-success" @click.prevent="onOk">
|
||||
{{ $t("button.submit") }}
|
||||
</button>
|
||||
</template>
|
||||
|
@ -5,15 +5,17 @@ import DashboardSysCard from "./components/DashboardSysCard.vue";
|
||||
import DashboardProduct from "./components/DashboardProduct.vue";
|
||||
import DashboardProductComplete from "./components/DashboardProductComplete.vue";
|
||||
import DashboardIndoor from "./components/DashboardIndoor.vue";
|
||||
// import DashboardRefrigTemp from "./components/DashboardRefrigTemp.vue";
|
||||
// import DashboardIndoorTemp from "./components/DashboardIndoorTemp.vue";
|
||||
import DashboardElectricity from "./components/DashboardElectricity.vue";
|
||||
import DashboardEmission from "./components/DashboardEmission.vue";
|
||||
import DashboardAlert from "./components/DashboardAlert.vue";
|
||||
import DashboardRefrig from "./components/DashboardRefrig.vue";
|
||||
|
||||
import { computed, inject, ref, watch, onMounted, onUnmounted } from "vue";
|
||||
import useBuildingStore from "@/stores/useBuildingStore";
|
||||
import { getSystemDevices, getSystemRealTime } from "@/apis/system";
|
||||
import DashboardRefrig from "./components/DashboardRefrig.vue";
|
||||
// ⭐ 新增:引入 getSystemConfig
|
||||
import { getSystemConfig } from "@/apis/system";
|
||||
|
||||
const FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
|
||||
const buildingStore = useBuildingStore();
|
||||
|
||||
@ -21,22 +23,32 @@ const subscribeData = ref([]);
|
||||
const systemData = ref({});
|
||||
let intervalId = null;
|
||||
|
||||
const productVisible = ref(false); // 產品產量儀表是否顯示
|
||||
const productCompleteVisible = ref(false); // 今日達成率是否顯示
|
||||
const productVisible = ref(false);
|
||||
const productCompleteVisible = ref(false);
|
||||
|
||||
// 開始定時器
|
||||
const startInterval = () => {
|
||||
// 清除之前的定時器(如果存在)
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
// ⭐ 新增:冷凍空調卡片顯示狀態(預設 false)
|
||||
const showRefrigeration = ref(false);
|
||||
|
||||
// ⭐ 新增:讀取系統配置
|
||||
const loadSystemConfig = async () => {
|
||||
const building_guid = buildingStore.selectedBuilding?.building_guid;
|
||||
if (!building_guid) {
|
||||
showRefrigeration.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const res = await getSystemConfig(building_guid);
|
||||
// 你的 apihandler 可能回傳 { data, code, msg } 或直接回 data,兩者都兼容:
|
||||
const data = res?.data ?? res;
|
||||
showRefrigeration.value = !!data?.show_refrigeration;
|
||||
// 若未來還要控制其他區塊(如 show_room、show_production_indicator),也可一併在這裡設定
|
||||
} catch (err) {
|
||||
console.error("[Dashboard] getSystemConfig error:", err);
|
||||
showRefrigeration.value = false; // 失敗時保守處理:不顯示
|
||||
}
|
||||
|
||||
// 每5秒呼叫一次 getData
|
||||
intervalId = setInterval(() => {
|
||||
getData();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 取得設備資料(維持原本邏輯)
|
||||
const getData = async () => {
|
||||
const res = await getSystemDevices({
|
||||
building_guid: buildingStore.selectedBuilding?.building_guid,
|
||||
@ -45,20 +57,16 @@ const getData = async () => {
|
||||
subscribeData.value = res.data;
|
||||
console.log("devices", subscribeData.value);
|
||||
|
||||
// 轉換資料格式
|
||||
const transformedData = {};
|
||||
|
||||
subscribeData.value.forEach((floor) => {
|
||||
if (floor.device_list && floor.device_list.length > 0) {
|
||||
const fullUrl = floor.floor_map_name;
|
||||
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
|
||||
transformedData[uuid] = floor.device_list.map((device) => {
|
||||
// 解析座標
|
||||
const coordinates = JSON.parse(device.device_coordinate || "[0,0]");
|
||||
const x = coordinates[0];
|
||||
const y = coordinates[1];
|
||||
|
||||
// 決定設備狀態和顏色
|
||||
let state = "Online";
|
||||
let bgColor = device.device_normal_color;
|
||||
|
||||
@ -78,10 +86,10 @@ const getData = async () => {
|
||||
x,
|
||||
y,
|
||||
{
|
||||
device_number: device.device_number || "", // 設備編號
|
||||
device_coordinate: device.device_coordinate || "", // 設備座標
|
||||
device_number: device.device_number || "",
|
||||
device_coordinate: device.device_coordinate || "",
|
||||
device_image_url: device.device_image_url,
|
||||
full_name: device.full_name, // 設備名稱
|
||||
full_name: device.full_name,
|
||||
main_id: device.main_id,
|
||||
points: device.points || [],
|
||||
floor: floor.full_name,
|
||||
@ -112,24 +120,32 @@ const getData = async () => {
|
||||
systemData.value = transformedData;
|
||||
};
|
||||
|
||||
// 每 5 秒輪詢設備資料(維持原本邏輯)
|
||||
const startInterval = () => {
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
intervalId = setInterval(() => {
|
||||
getData();
|
||||
}, 5000);
|
||||
};
|
||||
|
||||
// 當切換建物時:同時載入系統設定 + 設備資料
|
||||
watch(
|
||||
() => buildingStore.selectedBuilding,
|
||||
(newBuilding) => {
|
||||
async (newBuilding) => {
|
||||
if (newBuilding) {
|
||||
await loadSystemConfig(); // ⭐ 先載設定(避免先渲染出不該顯示的卡片)
|
||||
getData();
|
||||
startInterval();
|
||||
} else {
|
||||
showRefrigeration.value = false;
|
||||
}
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
// 組件卸載時清除定時器
|
||||
// 卸載時清除定時器
|
||||
onUnmounted(() => {
|
||||
if (intervalId) {
|
||||
clearInterval(intervalId);
|
||||
}
|
||||
if (intervalId) clearInterval(intervalId);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -141,7 +157,9 @@ onUnmounted(() => {
|
||||
<div class="flex flex-col gap-5">
|
||||
<DashboardIndoor />
|
||||
</div>
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- ⭐ 依 show_refrigeration 決定是否渲染 -->
|
||||
<div class="flex flex-col gap-5" v-if="showRefrigeration">
|
||||
<DashboardRefrig />
|
||||
</div>
|
||||
</div>
|
||||
@ -152,9 +170,7 @@ onUnmounted(() => {
|
||||
<DashboardFloorBar />
|
||||
<DashboardEffectScatter :data="systemData" />
|
||||
</div>
|
||||
<!-- <div class="order-2 w-full lg:hidden my-3">
|
||||
<DashboardSysCard :data="systemData" />
|
||||
</div> -->
|
||||
|
||||
<div
|
||||
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-12"
|
||||
>
|
||||
|
Loading…
Reference in New Issue
Block a user