fix: 修正異常設定 modal 限定條件上下限必填規則

This commit is contained in:
MJM_2025_05\polly 2025-10-09 10:36:28 +08:00
parent 13d14d0bb6
commit b986d1b8dd
8 changed files with 214 additions and 335 deletions

View File

@ -0,0 +1,7 @@
node_modules
.git
.gitignore
Dockerfile
docker-compose.yml
README.md
.vs

View File

@ -1,13 +1,13 @@
# Project # Project
PROJ_NAME=proj_bims_ils-svc PROJ_NAME=proj_bims_empower
# Network 網路環境 # Network 網路環境
NET_TRAEFIK=net-traefik_svc NET_TRAEFIK=net-traefik_svc
# Image: org/name # Image: org/name
IMAGE_PROJ_NAME=proj_bims_ils IMAGE_PROJ_NAME=proj_bims_empower
IMAGE_NAME=empower-front IMAGE_NAME=empower-front
TAG_VERSION=0.1.0 TAG_VERSION=0.1.11
# Remote # Remote
REMOTE_URL=harbor.mjm-staging.developers-homelab.net REMOTE_URL=harbor.mjm-staging.developers-homelab.net

View 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 "$@"

View File

@ -1,109 +1,34 @@
<script setup> <script setup>
import * as echarts from "echarts"; import * as echarts from "echarts";
import { onMounted, onUnmounted, ref, markRaw } from "vue"; import { onMounted, ref, markRaw } from "vue";
const props = defineProps({ const props = defineProps({
/** 初始 option僅在 init 時套一次;之後請用 ref.chart.setOption 更新) */ option: Object,
option: { type: Object, default: () => ({}) }, class: String,
/** 外層 className保持相容 */ id: String,
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 },
}); });
const dom = ref(null); let chart = ref(null);
/** 暴露給父層的 ECharts instancemarkRaw 避免被 Vue 追蹤) */ let dom = ref(null);
const chart = ref(null);
let resizeObs = null;
let inited = false;
function init() { function init() {
if (inited || !dom.value) return; let echart = echarts;
// init chart.value = markRaw(echart.init(dom.value));
const instance = echarts.init(dom.value, undefined, { chart.value.setOption(props.option);
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();
} }
onMounted(() => { onMounted(() => {
if (!inited && dom.value) init(); if (!chart.value && dom.value) {
}); init();
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibility, false);
if (resizeObs && dom.value) {
try { resizeObs.unobserve(dom.value); } catch {}
} }
resizeObs = null;
if (chart.value) {
try { chart.value.dispose(); } catch {}
}
chart.value = null;
inited = false;
}); });
defineExpose({ defineExpose({
chart, // chart.setOption(...) chart,
setOption, //
resize, // resize
}); });
</script> </script>
<template> <template>
<!-- 建議外層給明確高度否則 ECharts 無法正確量到容器尺寸 --> <div :id="id" :class="class" ref="dom"></div>
<div :id="id" :class="class" ref="dom" style="width: 100%; height: 100%"></div>
</template> </template>
<style lang="scss" scoped></style> <style lang="scss" scoped></style>

View File

@ -202,7 +202,7 @@ watch(
class="flex flex-col justify-center w-3 mx-2 relative" class="flex flex-col justify-center w-3 mx-2 relative"
@click="() => sort(column.key)" @click="() => sort(column.key)"
> >
<font-awesome-icon <!-- <font-awesome-icon
:icon="['fas', 'sort-up']" :icon="['fas', 'sort-up']"
:class=" :class="
twMerge( twMerge(
@ -221,30 +221,19 @@ watch(
) )
" "
size="lg" size="lg"
/> /> -->
</div> </div>
<div class="ml-2 relative" v-if="column.filter"> <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="fixed z-50" v-if="filterColumn[column.key]">
<div class="card min-w-max bg-body shadow-xl px-10 py-5"> <div class="card min-w-max bg-body shadow-xl px-10 py-5">
<label <label
class="input input-bordered bg-transparent rounded-lg flex items-center px-2 mb-4 border-success focus-within:border-success" 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']" :icon="['fas', 'search']"
class="w-6 h-6 mr-2 text-success" class="w-6 h-6 mr-2 text-success"
/> /> -->
<input <input
type="text" type="text"
:placeholder="t('operation.enter_text')" :placeholder="t('operation.enter_text')"

View File

@ -1,13 +1,5 @@
<script setup> <script setup>
import { import { ref, defineProps, watch, inject, onMounted } from "vue";
ref,
defineProps,
watch,
inject,
nextTick,
onMounted,
toRaw,
} from "vue";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { postOperationRecord } from "@/apis/alert"; import { postOperationRecord } from "@/apis/alert";
import * as yup from "yup"; import * as yup from "yup";
@ -93,28 +85,6 @@ const updateFileList = (files) => {
formState.value.lorf = 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 onOk = async () => {
const formData = new FormData(form.value); const formData = new FormData(form.value);
formData.delete("oriFile"); formData.delete("oriFile");
@ -177,14 +147,12 @@ const onCancel = () => {
description: "", description: "",
lorf: [], lorf: [],
}; };
//
videoLocation.value.videoLocation = "";
handleErrorReset(); handleErrorReset();
updateEditRecord?.(null); updateEditRecord?.(null);
alert_action_item.close(); alert_action_item.close();
}; };
// props.editRecord -> formState / / / // props.editRecord -> formState / /
watch( watch(
() => props.editRecord, () => props.editRecord,
(newVal) => { (newVal) => {
@ -203,10 +171,6 @@ watch(
formState.value.fix_do = value ?? ""; 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 } { immediate: true }
@ -258,16 +222,8 @@ watch(
name="work_type" name="work_type"
Attribute="title" Attribute="title"
:options="[ :options="[
{ { key: 1, value: 1, title: $t('alert.maintenance') },
key: 1, { key: 2, value: 2, title: $t('alert.repair') },
value: 1,
title: $t('alert.maintenance'),
},
{
key: 2,
value: 2,
title: $t('alert.repair'),
},
]" ]"
:required="true" :required="true"
:disabled="true" :disabled="true"
@ -276,8 +232,8 @@ watch(
</Select> </Select>
<Input class="my-2" :value="formState" name="fix_do" :required="true"> <Input class="my-2" :value="formState" name="fix_do" :required="true">
<template #topLeft>{{ $t("alert.repair_item") }}</template> <template #topLeft>{{ $t("alert.repair_item") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">
{{ formErrorMsg.fix_do }} {{ formErrorMsg.fix_do }}
</span> </span>
</template> </template>
@ -290,11 +246,11 @@ watch(
:disabled="true" :disabled="true"
> >
<template #topLeft>{{ $t("alert.repair_item_code") }}</template> <template #topLeft>{{ $t("alert.repair_item_code") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">
{{ formErrorMsg.fix_do_code }} {{ formErrorMsg.fix_do_code }}
</span></template </span>
> </template>
</Input> </Input>
<Select <Select
:value="formState" :value="formState"
@ -306,27 +262,19 @@ watch(
:required="true" :required="true"
> >
<template #topLeft>{{ $t("alert.responsible_vendor") }}</template> <template #topLeft>{{ $t("alert.responsible_vendor") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">
{{ formErrorMsg.fix_firm }} {{ formErrorMsg.fix_firm }}
</span></template </span>
> </template>
</Select> </Select>
<RadioGroup <RadioGroup
class="my-2" class="my-2"
name="status" name="status"
:value="formState" :value="formState"
:items="[ :items="[
{ { key: 0, value: 0, title: $t('alert.not_completed') },
key: 0, { key: 1, value: 1, title: $t('alert.completed') },
value: 0,
title: $t('alert.not_completed'),
},
{
key: 1,
value: 1,
title: $t('alert.completed'),
},
]" ]"
:required="true" :required="true"
> >
@ -342,11 +290,11 @@ watch(
:required="true" :required="true"
> >
<template #topLeft>{{ $t("alert.worker_id") }}</template> <template #topLeft>{{ $t("alert.worker_id") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">
{{ formErrorMsg.work_person_id }} {{ formErrorMsg.work_person_id }}
</span></template </span>
> </template>
</Select> </Select>
<!-- 注意事項 --> <!-- 注意事項 -->
@ -354,45 +302,11 @@ watch(
<template #topLeft>{{ $t("alert.notice") }}</template> <template #topLeft>{{ $t("alert.notice") }}</template>
</Textarea> </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"> <Textarea :value="formState" name="description" class="w-full my-2">
<template #topLeft>{{ $t("alert.result_description") }}</template> <template #topLeft>{{ $t("alert.result_description") }}</template>
</Textarea> </Textarea>
<Upload <Upload
class="my-2" class="my-2"
name="oriFile" name="oriFile"

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { inject, defineProps, watch, ref, provide } from "vue"; import { inject, defineProps, watch, ref } from "vue";
import useSearchParam from "@/hooks/useSearchParam"; import useSearchParam from "@/hooks/useSearchParam";
import useFormErrorMessage from "@/hooks/useFormErrorMessage"; import useFormErrorMessage from "@/hooks/useFormErrorMessage";
import AlertNoticesTable from "./AlertNoticesTable.vue"; import AlertNoticesTable from "./AlertNoticesTable.vue";
@ -9,7 +9,7 @@ import { useI18n } from "vue-i18n";
const { t } = useI18n(); const { t } = useI18n();
const { openToast } = inject("app_toast"); const { openToast } = inject("app_toast");
const { timesList, noticeList } = inject("notify_table"); const { timesList, noticeList } = inject("notify_table");
const { searchParams, changeParams } = useSearchParam(); const { searchParams } = useSearchParam();
const props = defineProps({ const props = defineProps({
openModal: Function, openModal: Function,
@ -19,8 +19,8 @@ const props = defineProps({
OptionsData: Object, OptionsData: Object,
}); });
const form = ref(null); // ---- 使 ----
const formState = ref({ const initialFormState = () => ({
id: 0, id: 0,
device_number: "", device_number: "",
device_name_tag: searchParams.value?.subSys_id, device_name_tag: searchParams.value?.subSys_id,
@ -28,7 +28,7 @@ const formState = ref({
enable: 0, enable: 0,
factor: 1, factor: 1,
alarm_value: "", alarm_value: "",
delay: 0, delay: null,
highLimit: null, highLimit: null,
lowLimit: null, lowLimit: null,
highDelay: null, highDelay: null,
@ -36,20 +36,35 @@ const formState = ref({
notices: [], 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")), device_number: yup.string().required(t("button.required")),
points: yup.string().required(t("button.required")), points: yup.string().required(t("button.required")),
factor: yup.number().nullable(), factor: numNullable(),
enable: yup.number().required(), enable: numNullable().required(),
highLimit: yup.number().nullable(), delay: numNullable(),
lowLimit: yup.number().nullable(), highLimit: numNullable(),
highDelay: yup.number().nullable(), lowLimit: numNullable(),
lowDelay: yup.number().nullable(), highDelay: numNullable(),
lowDelay: numNullable(),
// alarm_value schema
}); });
const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage( const { formErrorMsg, handleSubmit, handleErrorReset } = useFormErrorMessage(scheme);
scheme.value
);
const SaveCheckAuth = ref([]); const SaveCheckAuth = ref([]);
const factorNum = ref(1); const factorNum = ref(1);
@ -58,15 +73,30 @@ watch(
() => props.editRecord, () => props.editRecord,
(newValue) => { (newValue) => {
if (newValue) { if (newValue) {
// null
const normalize = (v) =>
v === "" || v === undefined ? null : v;
formState.value = { formState.value = {
...initialFormState(),
...newValue, ...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 = Array.isArray(newValue.notices)
SaveCheckAuth.value = newValue.notices ? [...newValue.notices] : []; ? [...newValue.notices]
: newValue.notices
? [newValue.notices]
: [];
if (newValue.factor) { if (newValue.factor) {
onFactorsChange(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] === "") { if (SaveCheckAuth.value.length === 1 && SaveCheckAuth.value[0] === "") {
SaveCheckAuth.value = []; SaveCheckAuth.value = [];
} }
SaveCheckAuth.value = [...SaveCheckAuth.value, value]; if (!SaveCheckAuth.value.includes(value)) {
SaveCheckAuth.value = [...SaveCheckAuth.value, value];
}
} else { } else {
SaveCheckAuth.value = SaveCheckAuth.value.filter((v) => v !== value); SaveCheckAuth.value = SaveCheckAuth.value.filter((v) => v !== value);
} }
@ -96,6 +128,7 @@ const onOk = async () => {
device_name_tag: searchParams.value?.subSys_id, device_name_tag: searchParams.value?.subSys_id,
notices: SaveCheckAuth.value ? SaveCheckAuth.value : [], notices: SaveCheckAuth.value ? SaveCheckAuth.value : [],
}); });
if (res.isSuccess) { if (res.isSuccess) {
props.getData(); props.getData();
closeModal(); closeModal();
@ -105,10 +138,11 @@ const onOk = async () => {
}; };
const closeModal = () => { const closeModal = () => {
formState.value = {}; formState.value = initialFormState();
SaveCheckAuth.value = [];
factorNum.value = 1;
handleErrorReset(); handleErrorReset();
props.onCancel(); props.onCancel();
factorNum.value = 1;
}; };
</script> </script>
@ -134,12 +168,11 @@ const closeModal = () => {
:options="OptionsData.devList" :options="OptionsData.devList"
> >
<template #topLeft>{{ $t("alert.device_name") }}</template> <template #topLeft>{{ $t("alert.device_name") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">{{ formErrorMsg.device_number }}</span>
{{ formErrorMsg.device_number }} </template>
</span></template
>
</Select> </Select>
<Select <Select
:value="formState" :value="formState"
class="my-2" class="my-2"
@ -149,12 +182,11 @@ const closeModal = () => {
:options="OptionsData.alarmPoints" :options="OptionsData.alarmPoints"
> >
<template #topLeft>{{ $t("alert.item") }}</template> <template #topLeft>{{ $t("alert.item") }}</template>
<template #bottomLeft <template #bottomLeft>
><span class="text-error text-base"> <span class="text-error text-base">{{ formErrorMsg.points }}</span>
{{ formErrorMsg.points }} </template>
</span></template
>
</Select> </Select>
<Select <Select
:value="formState" :value="formState"
class="my-2" class="my-2"
@ -166,84 +198,63 @@ const closeModal = () => {
> >
<template #topLeft>{{ $t("alert.qualifications") }}</template> <template #topLeft>{{ $t("alert.qualifications") }}</template>
</Select> </Select>
<RadioGroup <RadioGroup
class="my-2" class="my-2"
name="enable" name="enable"
:value="formState" :value="formState"
:items="[ :items="[
{ { key: 1, value: 1, title: $t('alert.enable') },
key: 1, { key: 0, value: 0, title: $t('alert.not_enabled') },
value: 1,
title: $t('alert.enable'),
},
{
key: 0,
value: 0,
title: $t('alert.not_enabled'),
},
]" ]"
:required="true" :required="true"
> >
<template #topLeft>{{ $t("alert.status") }}</template> <template #topLeft>{{ $t('alert.status') }}</template>
</RadioGroup> </RadioGroup>
<template v-if="factorNum == 1"> <template v-if="factorNum == 1">
<InputNumber :value="formState" class="my-2" name="delay"> <InputNumber :value="formState" class="my-2" name="delay">
<template #topLeft>{{ $t("alert.delay") }}</template> <template #topLeft>{{ $t('alert.delay') }}</template>
</InputNumber> </InputNumber>
<span class="text-error text-base ml-1">{{ formErrorMsg.delay }}</span>
</template> </template>
<template v-if="factorNum == 2"> <template v-if="factorNum == 2">
<div class="flex gap-4 w-full"> <div class="flex gap-4 w-full">
<InputNumber :value="formState" class="my-2" name="highLimit"> <InputNumber :value="formState" class="my-2" name="highLimit">
<template #topLeft>{{ $t("alert.upper_limit") }}(>=)</template> <template #topLeft>{{ $t('alert.upper_limit') }}(>=)</template>
</InputNumber> </InputNumber>
<InputNumber :value="formState" class="my-2" name="lowLimit"> <InputNumber :value="formState" class="my-2" name="lowLimit">
<template #topLeft>{{ $t("alert.lower_limit") }}(&lt;=)</template> <template #topLeft>{{ $t('alert.lower_limit') }}(&lt;=)</template>
</InputNumber> </InputNumber>
</div> </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"> <div class="flex gap-4 w-full">
<InputNumber :value="formState" class="my-2" name="highDelay"> <InputNumber :value="formState" class="my-2" name="highDelay">
<template #topLeft>{{ $t("alert.highDelay") }}</template> <template #topLeft>{{ $t('alert.highDelay') }}</template>
</InputNumber> </InputNumber>
<InputNumber :value="formState" class="my-2" name="lowDelay"> <InputNumber :value="formState" class="my-2" name="lowDelay">
<template #topLeft>{{ $t("alert.lowDelay") }}</template> <template #topLeft>{{ $t('alert.lowDelay') }}</template>
</InputNumber> </InputNumber>
</div> </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>
<template v-if="factorNum == 3"> <template v-if="factorNum == 3">
<Input :value="formState" class="my-2" name="alarm_value"> <Input :value="formState" class="my-2" name="alarm_value">
<template #topLeft>{{ $t("alert.warning_value") }}</template> <template #topLeft>{{ $t('alert.warning_value') }}</template>
</Input> </Input>
</template> </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"> <div class="w-full mt-5">
<p class="text-light text-lg ml-1"> <p class="text-light text-lg ml-1">{{ $t('alert.warning_method') }}</p>
{{ $t("alert.warning_method") }}
</p>
<AlertNoticesTable <AlertNoticesTable
:SaveCheckAuth="SaveCheckAuth" :SaveCheckAuth="SaveCheckAuth"
:NoticeData="noticeList" :NoticeData="noticeList"
@ -252,19 +263,12 @@ const closeModal = () => {
</div> </div>
</form> </form>
</template> </template>
<template #modalAction> <template #modalAction>
<button <button type="reset" class="btn btn-outline-success mr-2" @click.prevent="closeModal">
type="reset"
class="btn btn-outline-success mr-2"
@click.prevent="closeModal"
>
{{ $t("button.cancel") }} {{ $t("button.cancel") }}
</button> </button>
<button <button type="submit" class="btn btn-outline-success" @click.prevent="onOk">
type="submit"
class="btn btn-outline-success"
@click.prevent="onOk"
>
{{ $t("button.submit") }} {{ $t("button.submit") }}
</button> </button>
</template> </template>

View File

@ -5,15 +5,17 @@ import DashboardSysCard from "./components/DashboardSysCard.vue";
import DashboardProduct from "./components/DashboardProduct.vue"; import DashboardProduct from "./components/DashboardProduct.vue";
import DashboardProductComplete from "./components/DashboardProductComplete.vue"; import DashboardProductComplete from "./components/DashboardProductComplete.vue";
import DashboardIndoor from "./components/DashboardIndoor.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 DashboardElectricity from "./components/DashboardElectricity.vue";
import DashboardEmission from "./components/DashboardEmission.vue"; import DashboardEmission from "./components/DashboardEmission.vue";
import DashboardAlert from "./components/DashboardAlert.vue"; import DashboardAlert from "./components/DashboardAlert.vue";
import DashboardRefrig from "./components/DashboardRefrig.vue";
import { computed, inject, ref, watch, onMounted, onUnmounted } from "vue"; import { computed, inject, ref, watch, onMounted, onUnmounted } from "vue";
import useBuildingStore from "@/stores/useBuildingStore"; import useBuildingStore from "@/stores/useBuildingStore";
import { getSystemDevices, getSystemRealTime } from "@/apis/system"; 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 FILE_BASEURL = import.meta.env.VITE_FILE_API_BASEURL;
const buildingStore = useBuildingStore(); const buildingStore = useBuildingStore();
@ -21,22 +23,32 @@ const subscribeData = ref([]);
const systemData = ref({}); const systemData = ref({});
let intervalId = null; let intervalId = null;
const productVisible = ref(false); // const productVisible = ref(false);
const productCompleteVisible = ref(false); // const productCompleteVisible = ref(false);
// // 調 false
const startInterval = () => { const showRefrigeration = ref(false);
//
if (intervalId) { //
clearInterval(intervalId); 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_roomshow_production_indicator
} catch (err) {
console.error("[Dashboard] getSystemConfig error:", err);
showRefrigeration.value = false; //
} }
// 5 getData
intervalId = setInterval(() => {
getData();
}, 5000);
}; };
//
const getData = async () => { const getData = async () => {
const res = await getSystemDevices({ const res = await getSystemDevices({
building_guid: buildingStore.selectedBuilding?.building_guid, building_guid: buildingStore.selectedBuilding?.building_guid,
@ -45,20 +57,16 @@ const getData = async () => {
subscribeData.value = res.data; subscribeData.value = res.data;
console.log("devices", subscribeData.value); console.log("devices", subscribeData.value);
//
const transformedData = {}; const transformedData = {};
subscribeData.value.forEach((floor) => { subscribeData.value.forEach((floor) => {
if (floor.device_list && floor.device_list.length > 0) { if (floor.device_list && floor.device_list.length > 0) {
const fullUrl = floor.floor_map_name; const fullUrl = floor.floor_map_name;
const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : ""; const uuid = fullUrl ? fullUrl.replace(/\.svg$/, "") : "";
transformedData[uuid] = floor.device_list.map((device) => { transformedData[uuid] = floor.device_list.map((device) => {
//
const coordinates = JSON.parse(device.device_coordinate || "[0,0]"); const coordinates = JSON.parse(device.device_coordinate || "[0,0]");
const x = coordinates[0]; const x = coordinates[0];
const y = coordinates[1]; const y = coordinates[1];
//
let state = "Online"; let state = "Online";
let bgColor = device.device_normal_color; let bgColor = device.device_normal_color;
@ -78,10 +86,10 @@ const getData = async () => {
x, x,
y, y,
{ {
device_number: device.device_number || "", // device_number: device.device_number || "",
device_coordinate: device.device_coordinate || "", // device_coordinate: device.device_coordinate || "",
device_image_url: device.device_image_url, device_image_url: device.device_image_url,
full_name: device.full_name, // full_name: device.full_name,
main_id: device.main_id, main_id: device.main_id,
points: device.points || [], points: device.points || [],
floor: floor.full_name, floor: floor.full_name,
@ -112,24 +120,32 @@ const getData = async () => {
systemData.value = transformedData; systemData.value = transformedData;
}; };
// 5
const startInterval = () => {
if (intervalId) clearInterval(intervalId);
intervalId = setInterval(() => {
getData();
}, 5000);
};
// +
watch( watch(
() => buildingStore.selectedBuilding, () => buildingStore.selectedBuilding,
(newBuilding) => { async (newBuilding) => {
if (newBuilding) { if (newBuilding) {
await loadSystemConfig(); //
getData(); getData();
startInterval(); startInterval();
} else {
showRefrigeration.value = false;
} }
}, },
{ { immediate: true }
immediate: true,
}
); );
// //
onUnmounted(() => { onUnmounted(() => {
if (intervalId) { if (intervalId) clearInterval(intervalId);
clearInterval(intervalId);
}
}); });
</script> </script>
@ -141,7 +157,9 @@ onUnmounted(() => {
<div class="flex flex-col gap-5"> <div class="flex flex-col gap-5">
<DashboardIndoor /> <DashboardIndoor />
</div> </div>
<div class="flex flex-col gap-5">
<!-- show_refrigeration 決定是否渲染 -->
<div class="flex flex-col gap-5" v-if="showRefrigeration">
<DashboardRefrig /> <DashboardRefrig />
</div> </div>
</div> </div>
@ -152,9 +170,7 @@ onUnmounted(() => {
<DashboardFloorBar /> <DashboardFloorBar />
<DashboardEffectScatter :data="systemData" /> <DashboardEffectScatter :data="systemData" />
</div> </div>
<!-- <div class="order-2 w-full lg:hidden my-3">
<DashboardSysCard :data="systemData" />
</div> -->
<div <div
class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-12" class="order-last w-full lg:w-1/4 flex flex-col justify-start border-dashboard z-20 gap-12"
> >