水流量動畫 | 首頁小卡data串接 | 生產設定 : 原醋庫存列表靜態頁面
This commit is contained in:
parent
33dab54820
commit
49560b3dd5
@ -1,3 +1,3 @@
|
|||||||
VITE_API_BASEURL = "https://pccv-api.production.mjmtech.com.tw"
|
VITE_API_BASEURL = "http://192.168.1.10:8010"
|
||||||
VITE_FILE_API_BASEURL = ".."
|
VITE_FILE_API_BASEURL = ".."
|
||||||
VITE_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"
|
VITE_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"
|
@ -1,3 +1,3 @@
|
|||||||
VITE_API_BASEURL = "http://220.132.206.5:8008"
|
VITE_API_BASEURL = "https://pccv-api.production.mjmtech.com.tw"
|
||||||
VITE_FILE_API_BASEURL = "http://220.132.206.5:8085/file"
|
VITE_FILE_API_BASEURL = ".."
|
||||||
VITE_FORGE_BASEURL = "http://localhost:5173"
|
VITE_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"
|
@ -6,7 +6,8 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"preview": "vite preview"
|
"preview": "vite preview",
|
||||||
|
"stage": "vite build --mode staging"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ant-design/icons-vue": "^7.0.1",
|
"@ant-design/icons-vue": "^7.0.1",
|
||||||
|
BIN
public/arrow.png
Normal file
BIN
public/arrow.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 104 KiB |
BIN
public/forge.zip
Normal file
BIN
public/forge.zip
Normal file
Binary file not shown.
3
public/spot.svg
Normal file
3
public/spot.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="164" height="164" viewBox="0 0 164 164" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
|
||||||
|
</svg>
|
After Width: | Height: | Size: 107 B |
@ -1,5 +1,6 @@
|
|||||||
export const GET_DASHBOARD_INIT_API = `/SituationRoom/Initialize`;
|
export const GET_DASHBOARD_INIT_API = `/SituationRoom/Initialize`;
|
||||||
export const GET_DASHBOARD_DEVICE_API = `/SituationRoom/GetDeviceList`;
|
export const GET_DASHBOARD_DEVICE_API = `/SituationRoom/GetDeviceList`;
|
||||||
|
export const GET_DASHBOARD_REALTIME_DATA_API = `SituationRoom/GetOptionRealTimeData`;
|
||||||
export const GET_DASHBOARD_PRODUCT_COMPLETE_API = `/SituationRoom/GetProductionStatus`;
|
export const GET_DASHBOARD_PRODUCT_COMPLETE_API = `/SituationRoom/GetProductionStatus`;
|
||||||
export const GET_DASHBOARD_TEMP_API = `/SituationRoom/GetTempratureData`;
|
export const GET_DASHBOARD_TEMP_API = `/SituationRoom/GetTempratureData`;
|
||||||
export const GET_DASHBOARD_ROOM_TEMP_API = `/SituationRoom/GetFormulaRoomStatusData`;
|
export const GET_DASHBOARD_ROOM_TEMP_API = `/SituationRoom/GetFormulaRoomStatusData`;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
GET_DASHBOARD_INIT_API,
|
GET_DASHBOARD_INIT_API,
|
||||||
GET_DASHBOARD_DEVICE_API,
|
GET_DASHBOARD_DEVICE_API,
|
||||||
|
GET_DASHBOARD_REALTIME_DATA_API,
|
||||||
GET_DASHBOARD_PRODUCT_COMPLETE_API,
|
GET_DASHBOARD_PRODUCT_COMPLETE_API,
|
||||||
GET_DASHBOARD_TEMP_API,
|
GET_DASHBOARD_TEMP_API,
|
||||||
GET_DASHBOARD_ROOM_TEMP_API,
|
GET_DASHBOARD_ROOM_TEMP_API,
|
||||||
@ -34,6 +35,17 @@ export const getDashboardDevice = async ({ option }) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getDashboardOptionRealTimeData = async ({ option }) => {
|
||||||
|
const res = await instance.post( GET_DASHBOARD_REALTIME_DATA_API, {
|
||||||
|
option: parseInt(option),
|
||||||
|
});
|
||||||
|
|
||||||
|
return apihandler(res.code, res.data, {
|
||||||
|
msg: res.msg,
|
||||||
|
code: res.code,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const getDashboardProductCompletion = async () => {
|
export const getDashboardProductCompletion = async () => {
|
||||||
const res = await instance.post(GET_DASHBOARD_PRODUCT_COMPLETE_API);
|
const res = await instance.post(GET_DASHBOARD_PRODUCT_COMPLETE_API);
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
export const POST_SETTING_POINT_API = `/SituationRoom/SetPointSetting`;
|
export const POST_CHANGE_GROUP_VALUE_API = `api/Weight/ChangeGroupValue`;
|
||||||
|
|
||||||
export const GET_SETTING_TYPE_API = `/SituationRoom/GetProducts`;
|
export const GET_CHECKWEIGHER_API = `/api/HistoryData/GetCheckWeigherNow`;
|
||||||
export const POST_SETTING_TYPE_API = `/SituationRoom/SetProduct`;
|
export const POST_SETTING_TYPE_API = `/SituationRoom/SetProduct`;
|
||||||
|
@ -1,21 +1,13 @@
|
|||||||
import instance from "@/util/request";
|
import instance from "@/util/request";
|
||||||
import apihandler from "@/util/apihandler";
|
import apihandler from "@/util/apihandler";
|
||||||
import {
|
import {
|
||||||
POST_SETTING_POINT_API,
|
POST_CHANGE_GROUP_VALUE_API,
|
||||||
GET_SETTING_TYPE_API,
|
GET_CHECKWEIGHER_API,
|
||||||
POST_SETTING_TYPE_API,
|
POST_SETTING_TYPE_API,
|
||||||
} from "./api";
|
} from "./api";
|
||||||
|
|
||||||
export const postProductSettingPoint = async (type, devices) => {
|
export const postChangeGroupValue = async (data) => {
|
||||||
const res = await instance.post(POST_SETTING_POINT_API, {
|
const res = await instance.post(POST_CHANGE_GROUP_VALUE_API,data);
|
||||||
devices: devices.map(({ device_number }) => device_number),
|
|
||||||
values: [
|
|
||||||
{
|
|
||||||
point: "Type",
|
|
||||||
value: type.value,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
return apihandler(res.code, res.data, {
|
return apihandler(res.code, res.data, {
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
@ -24,8 +16,8 @@ export const postProductSettingPoint = async (type, devices) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getProductSettingType = async () => {
|
export const getCheckWeigher = async () => {
|
||||||
const res = await instance.post(GET_SETTING_TYPE_API);
|
const res = await instance.get(GET_CHECKWEIGHER_API);
|
||||||
|
|
||||||
return apihandler(res.code, res.data, {
|
return apihandler(res.code, res.data, {
|
||||||
msg: res.msg,
|
msg: res.msg,
|
||||||
|
@ -20,13 +20,7 @@ const { forgeLock } = inject("app_toggle");
|
|||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
fullScreen: Boolean,
|
fullScreen: Boolean,
|
||||||
initialData: Object,
|
initialData: Object,
|
||||||
cubeStyle: {
|
realTime: String,
|
||||||
type: Object,
|
|
||||||
default: {
|
|
||||||
right: 25,
|
|
||||||
top: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const heat_bar_isShow = ref(false);
|
const heat_bar_isShow = ref(false);
|
||||||
@ -45,6 +39,7 @@ const {
|
|||||||
loadModel,
|
loadModel,
|
||||||
updateInitialData,
|
updateInitialData,
|
||||||
subComponents,
|
subComponents,
|
||||||
|
clearSprites
|
||||||
} = useSystemStatusByBaja(updateHeatBarIsShow);
|
} = useSystemStatusByBaja(updateHeatBarIsShow);
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@ -99,7 +94,8 @@ const initViewer = (container) => {
|
|||||||
const initForge = () => {
|
const initForge = () => {
|
||||||
initViewer(forgeDom.value).then((viewer) => {
|
initViewer(forgeDom.value).then((viewer) => {
|
||||||
const localFilePath =
|
const localFilePath =
|
||||||
import.meta.env.MODE === "production"
|
import.meta.env.MODE === "production" ||
|
||||||
|
import.meta.env.MODE === "staging"
|
||||||
? `${FILE_BASEURL}/upload/forge/0.svf`
|
? `${FILE_BASEURL}/upload/forge/0.svf`
|
||||||
: "/forge/0.svf";
|
: "/forge/0.svf";
|
||||||
loadModel(viewer, localFilePath).then(() => {
|
loadModel(viewer, localFilePath).then(() => {
|
||||||
@ -112,13 +108,13 @@ const initForge = () => {
|
|||||||
);
|
);
|
||||||
updateForgeViewer(viewer);
|
updateForgeViewer(viewer);
|
||||||
|
|
||||||
const tree = viewer.model.getData().instanceTree;
|
// const tree = viewer.model.getData().instanceTree;
|
||||||
hideAllObjects(tree, visibleDbid.value);
|
// hideAllObjects(tree, visibleDbid.value);
|
||||||
visibleDbid.value.forEach((dbid) => {
|
// visibleDbid.value.forEach((dbid) => {
|
||||||
if (dbid === 58) {
|
// if (dbid === 58) {
|
||||||
viewer.setThemingColor(dbid, new THREE.Vector4(1, 0, 0, 1));
|
// viewer.setThemingColor(dbid, new THREE.Vector4(1, 0, 0, 1));
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
// 印出被點選物件的 dbid
|
// 印出被點選物件的 dbid
|
||||||
// viewer.addEventListener(
|
// viewer.addEventListener(
|
||||||
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
|
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
|
||||||
@ -179,6 +175,7 @@ onUnmounted(() => {
|
|||||||
subComponents.value?.unsubscribeAll();
|
subComponents.value?.unsubscribeAll();
|
||||||
subComponents.value?.detach();
|
subComponents.value?.detach();
|
||||||
updateForgeViewer(null);
|
updateForgeViewer(null);
|
||||||
|
clearSprites();
|
||||||
NOP_VIEWER.tearDown();
|
NOP_VIEWER.tearDown();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
@ -199,11 +196,12 @@ onUnmounted(() => {
|
|||||||
ref="forgeDom"
|
ref="forgeDom"
|
||||||
:class="
|
:class="
|
||||||
twMerge(
|
twMerge(
|
||||||
'relative w-full h-full',
|
'relative w-full h-full overflow-x-hidden',
|
||||||
fullScreen ? 'min-h-screen ' : 'min-h-[600px]'
|
fullScreen ? 'min-h-screen ' : 'min-h-[600px]'
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
<p class="absolute z-10 top-14 left-[27%]">更新時間 : {{ props.realTime }}</p>
|
||||||
<div v-show="heat_bar_isShow" class="absolute z-10 heatbar">
|
<div v-show="heat_bar_isShow" class="absolute z-10 heatbar">
|
||||||
<div class="w-40 flex justify-between text-[10px] mb-1">
|
<div class="w-40 flex justify-between text-[10px] mb-1">
|
||||||
<span class="text-gradient-1">-20°C</span>
|
<span class="text-gradient-1">-20°C</span>
|
||||||
@ -225,7 +223,7 @@ onUnmounted(() => {
|
|||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<label
|
<!-- <label
|
||||||
v-for="(value, key) in subscribeDataWithErrorMsg"
|
v-for="(value, key) in subscribeDataWithErrorMsg"
|
||||||
:key="key"
|
:key="key"
|
||||||
:data-dbid="value.forge_dbid"
|
:data-dbid="value.forge_dbid"
|
||||||
@ -255,7 +253,7 @@ onUnmounted(() => {
|
|||||||
<span class="mr-2">{{ value.full_name }}</span>
|
<span class="mr-2">{{ value.full_name }}</span>
|
||||||
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
|
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
|
||||||
<span v-else>{{ value.show_value }}</span>
|
<span v-else>{{ value.show_value }}</span>
|
||||||
</label>
|
</label> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -271,16 +269,16 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.viewcubeWrapper {
|
.viewcubeWrapper {
|
||||||
right: v-bind("`${props.cubeStyle.right}%`") !important;
|
right: 25% !important;
|
||||||
top: v-bind("`${props.cubeStyle.top}%`") !important;
|
top: 0% !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.homeViewWrapper {
|
.homeViewWrapper {
|
||||||
transform: scale(1.5) !important;
|
transform: scale(1.5) translateX(350%) translateY(0%) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heatbar {
|
.heatbar {
|
||||||
right: v-bind("`${props.cubeStyle.right + 2}%`") !important;
|
left: 27% !important;
|
||||||
top: 0% !important;
|
top: 9% !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -43,9 +43,9 @@ onMounted(() => {
|
|||||||
<NavbarItem />
|
<NavbarItem />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<router-link to="/dashboard" class="rounded-lg pl-14 text-2xl flex items-center text-stone-100">
|
<div class="rounded-lg pl-14 text-2xl flex items-center text-stone-100">
|
||||||
<img src="/logo.png" alt="logo" class="w-12 me-2" />百家珍
|
<img src="/logo.png" alt="logo" class="w-12 me-2" />百家珍
|
||||||
</router-link>
|
</div>
|
||||||
<NavbarBuilding class="hidden" />
|
<NavbarBuilding class="hidden" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -33,27 +33,6 @@ const getSubMonitorPage = async (building) => {
|
|||||||
const res = await getAllSysSidebar();
|
const res = await getAllSysSidebar();
|
||||||
buildingStore.mainSubSys = res.data.history_Main_Systems;
|
buildingStore.mainSubSys = res.data.history_Main_Systems;
|
||||||
};
|
};
|
||||||
const showDrawer = () => {
|
|
||||||
getSubMonitorPage();
|
|
||||||
open.value = true;
|
|
||||||
};
|
|
||||||
const onClose = () => {
|
|
||||||
open.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToSub = (sub) => {
|
|
||||||
// console.log("navigateToSub", sub);
|
|
||||||
// let pageAct = JSON.parse(sessionStorage.getItem("pageAct"));
|
|
||||||
// pageAct = {
|
|
||||||
// ...pageAct,
|
|
||||||
// sysMainTag: sub.main_system_tag,
|
|
||||||
// sysSubTag: sub.sub_system_tag,
|
|
||||||
// sysSubName: sub.full_name,
|
|
||||||
// };
|
|
||||||
// sessionStorage.setItem("lastPage", "systemMonitor");
|
|
||||||
// sessionStorage.setItem("pageAct", JSON.stringify(pageAct));
|
|
||||||
// window.location.href = "/file/index.html";
|
|
||||||
};
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => buildingStore.selectedBuilding,
|
() => buildingStore.selectedBuilding,
|
||||||
@ -74,19 +53,8 @@ onMounted(() => {
|
|||||||
v-for="page in authPages"
|
v-for="page in authPages"
|
||||||
class="flex flex-col items-center justify-center"
|
class="flex flex-col items-center justify-center"
|
||||||
>
|
>
|
||||||
<!-- <a
|
|
||||||
v-if="page.authCode === 'PF1'"
|
|
||||||
@click="showDrawer"
|
|
||||||
class="flex flex-col justify-center items-center btn-group text-white"
|
|
||||||
>
|
|
||||||
<font-awesome-icon
|
|
||||||
:icon="['fas', page.icon]"
|
|
||||||
size="2x"
|
|
||||||
class="menu-icon"
|
|
||||||
/>
|
|
||||||
{{ page.subName }}
|
|
||||||
</a> -->
|
|
||||||
<router-link
|
<router-link
|
||||||
|
v-if="$route.path !== page.navigate"
|
||||||
:to="page.navigate"
|
:to="page.navigate"
|
||||||
type="link"
|
type="link"
|
||||||
class="flex flex-col justify-center items-center btn-group text-white"
|
class="flex flex-col justify-center items-center btn-group text-white"
|
||||||
@ -98,32 +66,22 @@ onMounted(() => {
|
|||||||
/>
|
/>
|
||||||
{{ page.subName }}
|
{{ page.subName }}
|
||||||
</router-link>
|
</router-link>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col justify-center items-center btn-group text-white router-link-active cursor-pointer"
|
||||||
|
>
|
||||||
|
<font-awesome-icon
|
||||||
|
:icon="['fas', page.icon]"
|
||||||
|
size="2x"
|
||||||
|
class="menu-icon"
|
||||||
|
/>
|
||||||
|
{{ page.subName }}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<a-drawer
|
|
||||||
:width="200"
|
|
||||||
placement="left"
|
|
||||||
:open="open"
|
|
||||||
:closable="false"
|
|
||||||
@close="onClose"
|
|
||||||
class="sub-drawer"
|
|
||||||
:maskStyle="{ opacity: 0.5 }"
|
|
||||||
:bodyStyle="{ paddingLeft: 0, paddingRight: 0 }"
|
|
||||||
>
|
|
||||||
<ul>
|
|
||||||
<li
|
|
||||||
v-for="sub in buildingStore.subSys"
|
|
||||||
:key="sub.sub_system_tag"
|
|
||||||
@click.prevent="() => navigateToSub(sub)"
|
|
||||||
class="text-xl text-center py-3 hover:bg-black hover:text-info"
|
|
||||||
>
|
|
||||||
{{ sub.full_name }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</a-drawer>
|
|
||||||
</template>
|
</template>
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
.router-link-active.router-link-exact-active {
|
.router-link-active {
|
||||||
color: #7cedc1;
|
color: #7cedc1;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -47,7 +47,8 @@ import {
|
|||||||
faTemperatureHigh,
|
faTemperatureHigh,
|
||||||
faTint,
|
faTint,
|
||||||
faCircle,
|
faCircle,
|
||||||
faSyncAlt
|
faSyncAlt,
|
||||||
|
faSave
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
|
|
||||||
import { faClock } from "@fortawesome/free-regular-svg-icons";
|
import { faClock } from "@fortawesome/free-regular-svg-icons";
|
||||||
@ -99,7 +100,8 @@ library.add(
|
|||||||
faClock,
|
faClock,
|
||||||
faTint,
|
faTint,
|
||||||
faCircle,
|
faCircle,
|
||||||
faSyncAlt
|
faSyncAlt,
|
||||||
|
faSave
|
||||||
);
|
);
|
||||||
|
|
||||||
export default library;
|
export default library;
|
||||||
|
@ -14,6 +14,11 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
};
|
};
|
||||||
const { searchParams } = useSearchParams();
|
const { searchParams } = useSearchParams();
|
||||||
|
|
||||||
|
// DataVisualization 擴充套件的全域變數
|
||||||
|
let dataVizExtn = null;
|
||||||
|
let spriteAnimations = new Map(); // 用 Map 來追蹤多個動畫 {dbId: {interval, arrow, viewable}}
|
||||||
|
let cameraEventAdded = false; // 追蹤是否已經添加了相機事件監聽器
|
||||||
|
|
||||||
const initialData = ref(null);
|
const initialData = ref(null);
|
||||||
|
|
||||||
const updateInitialData = (data = false) => {
|
const updateInitialData = (data = false) => {
|
||||||
@ -99,7 +104,13 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// return visible;
|
// return visible;
|
||||||
return [26, 32, 38, 43, 48, 53, 58, 63, 70, 76];
|
return [
|
||||||
|
26, 32, 38, 43, 48, 53, 58, 63, 70, 76, 879, 1068, 1011, 1065, 855, 1109,
|
||||||
|
867,
|
||||||
|
963,1044,966,1139,936,1136,957,1133,954,610,1130,951,1041,939,
|
||||||
|
1145,987,999,1148,1002,1151,981,
|
||||||
|
813,1088,825,1091,804
|
||||||
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
const getDevice = async (option = 1) => {
|
const getDevice = async (option = 1) => {
|
||||||
@ -168,7 +179,12 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const transformDeviceNumber = (device_number) => {
|
const transformDeviceNumber = (device_number) => {
|
||||||
return device_number.replaceAll("_", "/");
|
const transformed = device_number.replaceAll("_", "/");
|
||||||
|
// 找到最後一個 / 的位置,並截取到該位置之前
|
||||||
|
const lastSlashIndex = transformed.lastIndexOf("/");
|
||||||
|
return lastSlashIndex !== -1
|
||||||
|
? transformed.substring(0, lastSlashIndex)
|
||||||
|
: transformed;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateLabelText = (key, point, value) => {
|
const updateLabelText = (key, point, value) => {
|
||||||
@ -188,15 +204,21 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
window.requirejs(["baja!"], (baja) => {
|
window.requirejs(["baja!"], (baja) => {
|
||||||
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
|
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
|
||||||
|
|
||||||
|
const ordKey = key;
|
||||||
const ordPath = transformDeviceNumber(key);
|
const ordPath = transformDeviceNumber(key);
|
||||||
baja.Ord.make(`local:|foxs:|station:|slot:/${ordPath}`)
|
console.log("ordPath", ordPath, "ordKey", ordKey, value);
|
||||||
|
const fullOrdPath = `local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}`; // 完整路徑
|
||||||
|
console.log("嘗試訪問路徑:", fullOrdPath); // 打印完整路徑
|
||||||
|
|
||||||
|
baja.Ord.make(fullOrdPath)
|
||||||
.get()
|
.get()
|
||||||
.then((folder) => {
|
.then((folder) => {
|
||||||
|
console.log("成功獲取 folder:", folder);
|
||||||
const batch = new baja.comm.Batch();
|
const batch = new baja.comm.Batch();
|
||||||
|
|
||||||
const sub = new baja.Subscriber();
|
const sub = new baja.Subscriber();
|
||||||
sub.attach({
|
sub.attach({
|
||||||
changed: function (prop, cx) {
|
changed: function (prop, cx) {
|
||||||
|
console.log("數據變更觸發:", prop.$getDisplayName());
|
||||||
if (prop.$getDisplayName() !== "Out") return;
|
if (prop.$getDisplayName() !== "Out") return;
|
||||||
if (
|
if (
|
||||||
Object.hasOwn(
|
Object.hasOwn(
|
||||||
@ -227,19 +249,27 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
console.log("開始遍歷控制點");
|
||||||
folder
|
folder
|
||||||
.getSlots()
|
.getSlots()
|
||||||
.is("control:ControlPoint")
|
.is("control:ControlPoint")
|
||||||
.eachValue((point) => {
|
.eachValue((point) => {
|
||||||
|
console.log("找到控制點:", point.getDisplayName());
|
||||||
|
console.log("配置的點位:", Object.keys(value.points));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
Object.keys(value.points).includes(point.getDisplayName())
|
Object.keys(value.points).includes(point.getDisplayName())
|
||||||
) {
|
) {
|
||||||
|
console.log(
|
||||||
|
"匹配到點位,開始訂閱:",
|
||||||
|
point.getDisplayName()
|
||||||
|
);
|
||||||
baja.Ord.make(
|
baja.Ord.make(
|
||||||
`local:|foxs:|station:|slot:/${ordPath}/${point.getDisplayName()}`
|
`local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}/${point.getDisplayName()}`
|
||||||
)
|
)
|
||||||
.get()
|
.get()
|
||||||
.then((component) => {
|
.then((component) => {
|
||||||
|
console.log("獲取到 component:", component);
|
||||||
if (
|
if (
|
||||||
point.getType().getTypeSpec() ===
|
point.getType().getTypeSpec() ===
|
||||||
"control:BooleanWritable"
|
"control:BooleanWritable"
|
||||||
@ -336,14 +366,14 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const hideAllObjects = (instanceTree, filDbids = []) => {
|
const hideAllObjects = (instanceTree, filDbids = []) => {
|
||||||
const tree = instanceTree || forgeViewer.value.model?.getInstanceTree();
|
if (!forgeViewer.value || !instanceTree) return;
|
||||||
const allDbIdsStr = Object.keys(tree.nodeAccess.dbIdToIndex);
|
const allDbIds = Object.keys(instanceTree.nodeAccess.dbIdToIndex).map(
|
||||||
for (var i = 0; i < allDbIdsStr.length; i++) {
|
Number
|
||||||
forgeViewer.value.hide(parseInt(allDbIdsStr[i]));
|
);
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < filDbids.length; i++) {
|
forgeViewer.value.hide(allDbIds);
|
||||||
forgeViewer.value.show(parseInt(filDbids[i]));
|
if (filDbids.length > 0) {
|
||||||
|
forgeViewer.value.show(filDbids);
|
||||||
}
|
}
|
||||||
fitToView();
|
fitToView();
|
||||||
forgeViewer.value.impl.invalidate(true);
|
forgeViewer.value.impl.invalidate(true);
|
||||||
@ -374,24 +404,97 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
|
|
||||||
const reloadModal = () => {};
|
const reloadModal = () => {};
|
||||||
|
|
||||||
watch(
|
let themingInterval = null;
|
||||||
visibleDbid,
|
let blinkingInterval = null; // 計時器 ID 的引用
|
||||||
(newValue) => {
|
|
||||||
if (!forgeViewer.value) return;
|
|
||||||
hideAllObjects(forgeViewer.value.model.getData().instanceTree, newValue);
|
|
||||||
|
|
||||||
newValue.forEach((dbid) => {
|
// 增強版:停止閃爍並徹底清理
|
||||||
// 根據 dbid 設置主題色
|
const stopBlinking = () => {
|
||||||
if (dbid === 58) {
|
console.log("正在停止基於 Selection 的閃爍效果...");
|
||||||
// 紅色 (RGB: 1,0,0)
|
if (blinkingInterval) {
|
||||||
forgeViewer.value.setThemingColor(
|
clearInterval(blinkingInterval);
|
||||||
dbid,
|
blinkingInterval = null;
|
||||||
new THREE.Vector4(1, 0, 0, 1)
|
}
|
||||||
);
|
if (forgeViewer.value) {
|
||||||
}
|
// 核心清理:取消所有選取
|
||||||
|
forgeViewer.value.clearSelection();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const startBlinking = (dbidToBlink) => {
|
||||||
|
console.log(`正在為 dbId: ${dbidToBlink} 啟動基於 Selection 的閃爍...`);
|
||||||
|
if (!forgeViewer.value) return;
|
||||||
|
|
||||||
|
// 步驟 1: 設定一次我們希望的閃爍顏色和樣式
|
||||||
|
const blinkColor = new THREE.Color(0x00aaff); // 水藍色
|
||||||
|
forgeViewer.value.setSelectionColor(
|
||||||
|
blinkColor,
|
||||||
|
Autodesk.Viewing.SelectionType.OVERLAYED
|
||||||
|
);
|
||||||
|
|
||||||
|
let isSelected = false; // 用一個旗標來追蹤目前的選取狀態
|
||||||
|
|
||||||
|
blinkingInterval = setInterval(() => {
|
||||||
|
// 在計時器內部也要檢查 viewer 是否還存在
|
||||||
|
if (!forgeViewer.value) {
|
||||||
|
stopBlinking();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSelected) {
|
||||||
|
// 如果已選取,就取消選取 (閃爍 OFF)
|
||||||
|
forgeViewer.value.clearSelection();
|
||||||
|
} else {
|
||||||
|
// 如果未選取,就選取目標物件 (閃爍 ON)
|
||||||
|
// 注意:select 方法需要傳入 model 物件
|
||||||
|
forgeViewer.value.select(
|
||||||
|
dbidToBlink,
|
||||||
|
forgeViewer.value.model,
|
||||||
|
Autodesk.Viewing.SelectionType.OVERLAYED
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切換狀態,為下一次循環做準備
|
||||||
|
isSelected = !isSelected;
|
||||||
|
}, 700); // 我們可以稍微調整一下頻率,例如 700 毫秒
|
||||||
|
};
|
||||||
|
|
||||||
|
watch([forgeViewer, visibleDbid], ([viewer, dbids]) => {
|
||||||
|
console.log("監聽到 forgeViewer 或 visibleDbid 的變化", viewer, dbids);
|
||||||
|
stopBlinking();
|
||||||
|
clearSprites(); // 清理之前的 Sprites
|
||||||
|
|
||||||
|
if (viewer) {
|
||||||
|
hideAllObjects(viewer.model.getData().instanceTree, dbids);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判斷新狀態是否需要閃爍和 Sprites
|
||||||
|
if (viewer && dbids.includes(879)) {
|
||||||
|
// startBlinking([879, 1068, 1011, 1065, 855, 1109, 867,963,1044,966,1139,936,1136,957,1133,954,610,1130,951,1041,939,1145,987,999,1148,1002,1151,981,813,1088,825,1091,804]);
|
||||||
|
const spriteConfigs = [
|
||||||
|
{ dbId: 879, reverse: false },
|
||||||
|
{ dbId: 1011, reverse: false },
|
||||||
|
{ dbId: 855, reverse: true },
|
||||||
|
{ dbId: 867, reverse: true },
|
||||||
|
{ dbId: 963, reverse: false },
|
||||||
|
{ dbId: 966, reverse: true },
|
||||||
|
{ dbId: 936, reverse: true },
|
||||||
|
{ dbId: 957, reverse: true },
|
||||||
|
{ dbId: 954, reverse: true },
|
||||||
|
{ dbId: 951, reverse: true },
|
||||||
|
{ dbId: 939, reverse: true },
|
||||||
|
{ dbId: 987, reverse: true },
|
||||||
|
{ dbId: 999, reverse: true },
|
||||||
|
{ dbId: 1002, reverse: true },
|
||||||
|
{ dbId: 981, reverse: true },
|
||||||
|
{ dbId: 813, reverse: true },
|
||||||
|
{ dbId: 825, reverse: false },
|
||||||
|
{ dbId: 804, reverse: false },
|
||||||
|
];
|
||||||
|
spriteConfigs.forEach(({ dbId, reverse }) => {
|
||||||
|
createSprites(viewer, dbId, reverse);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
});
|
||||||
|
|
||||||
watch(initialData, (newValue) => {
|
watch(initialData, (newValue) => {
|
||||||
if (newValue) {
|
if (newValue) {
|
||||||
@ -409,6 +512,259 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 動態Sprites創建函數
|
||||||
|
const createSprites = async (viewer, dbId, reverse = false) => {
|
||||||
|
try {
|
||||||
|
// 先清理這個 dbId 的舊 Sprite(如果存在)
|
||||||
|
clearSingleSprite(dbId);
|
||||||
|
|
||||||
|
// 1. 載入 DataVisualization 擴充套件
|
||||||
|
if (!dataVizExtn) {
|
||||||
|
dataVizExtn = await viewer.loadExtension("Autodesk.DataVisualization");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 建立 ViewableStyle
|
||||||
|
const DataVizCore = Autodesk.DataVisualization.Core;
|
||||||
|
const viewableType = DataVizCore.ViewableType.SPRITE;
|
||||||
|
const spriteColor = new THREE.Color(0x00ffff);
|
||||||
|
const spriteIconUrl = "spot.svg";
|
||||||
|
const style = new DataVizCore.ViewableStyle(
|
||||||
|
viewableType,
|
||||||
|
spriteColor,
|
||||||
|
spriteIconUrl
|
||||||
|
);
|
||||||
|
const viewableData = new DataVizCore.ViewableData();
|
||||||
|
viewableData.spriteSize = 12;
|
||||||
|
|
||||||
|
// 3. 取得 dbId 的 3D 位置
|
||||||
|
viewer.getObjectTree(function (instanceTree) {
|
||||||
|
const fragList = viewer.model.getFragmentList();
|
||||||
|
let fragIds = [];
|
||||||
|
instanceTree.enumNodeFragments(dbId, function (fragId) {
|
||||||
|
fragIds.push(fragId);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (fragIds.length > 0) {
|
||||||
|
let box = new THREE.Box3();
|
||||||
|
fragIds.forEach((fragId) => {
|
||||||
|
let fragBox = new THREE.Box3();
|
||||||
|
fragList.getWorldBounds(fragId, fragBox);
|
||||||
|
box.union(fragBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 算出主軸方向
|
||||||
|
const size = new THREE.Vector3();
|
||||||
|
box.getSize(size);
|
||||||
|
// 找出最長的軸
|
||||||
|
let axis = "x";
|
||||||
|
if (size.y >= size.x && size.y >= size.z) axis = "y";
|
||||||
|
if (size.z >= size.x && size.z >= size.y) axis = "z";
|
||||||
|
|
||||||
|
// 取得所有 fragment 的中心點
|
||||||
|
let centers = fragIds.map((fragId) => {
|
||||||
|
let fragBox = new THREE.Box3();
|
||||||
|
fragList.getWorldBounds(fragId, fragBox);
|
||||||
|
return fragBox.getCenter(new THREE.Vector3());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 找出距離最遠的兩點作為動畫路徑
|
||||||
|
let maxDist = 0;
|
||||||
|
let animStart = centers[0],
|
||||||
|
animEnd = centers[0];
|
||||||
|
for (let i = 0; i < centers.length; i++) {
|
||||||
|
for (let j = i + 1; j < centers.length; j++) {
|
||||||
|
let dist = centers[i].distanceTo(centers[j]);
|
||||||
|
if (dist > maxDist) {
|
||||||
|
maxDist = dist;
|
||||||
|
animStart = centers[i];
|
||||||
|
animEnd = centers[j];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fallback: 若只有一個 fragment 或所有中心點重疊,改用主軸方向的端點
|
||||||
|
if (centers.length < 2 || maxDist === 0) {
|
||||||
|
const boxCenter = box.getCenter(new THREE.Vector3());
|
||||||
|
const halfSize = size.clone().multiplyScalar(0.5);
|
||||||
|
|
||||||
|
animStart = boxCenter.clone();
|
||||||
|
animEnd = boxCenter.clone();
|
||||||
|
|
||||||
|
// 根據主軸設定起點和終點
|
||||||
|
if (axis === "x") {
|
||||||
|
animStart.x -= halfSize.x;
|
||||||
|
animEnd.x += halfSize.x;
|
||||||
|
} else if (axis === "y") {
|
||||||
|
animStart.y -= halfSize.y;
|
||||||
|
animEnd.y += halfSize.y;
|
||||||
|
} else {
|
||||||
|
// z軸
|
||||||
|
animStart.z -= halfSize.z;
|
||||||
|
animEnd.z += halfSize.z;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 依主軸方向排序,確保動畫方向一致 ===
|
||||||
|
// 根據主軸來排序起點和終點,確保動畫方向一致
|
||||||
|
if (axis === "x" && animStart.x > animEnd.x) {
|
||||||
|
const tmp = animStart;
|
||||||
|
animStart = animEnd;
|
||||||
|
animEnd = tmp;
|
||||||
|
}
|
||||||
|
if (axis === "y" && animStart.y > animEnd.y) {
|
||||||
|
const tmp = animStart;
|
||||||
|
animStart = animEnd;
|
||||||
|
animEnd = tmp;
|
||||||
|
}
|
||||||
|
if (axis === "z" && animStart.z > animEnd.z) {
|
||||||
|
const tmp = animStart;
|
||||||
|
animStart = animEnd;
|
||||||
|
animEnd = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === 若 reverse 為 true,則反向 ===
|
||||||
|
if (reverse) {
|
||||||
|
const tmp = animStart;
|
||||||
|
animStart = animEnd;
|
||||||
|
animEnd = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`dbId ${dbId} 動畫資訊:`, {
|
||||||
|
axis: axis,
|
||||||
|
size: size,
|
||||||
|
animStart: animStart,
|
||||||
|
animEnd: animEnd,
|
||||||
|
distance: animStart.distanceTo(animEnd),
|
||||||
|
reverse: reverse,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4. 建立 SpriteViewable,初始在 animStart
|
||||||
|
const viewable = new DataVizCore.SpriteViewable(
|
||||||
|
animStart.clone(),
|
||||||
|
style,
|
||||||
|
dbId
|
||||||
|
);
|
||||||
|
viewableData.addViewable(viewable);
|
||||||
|
viewableData.finish().then(() => {
|
||||||
|
dataVizExtn.addViewables(viewableData);
|
||||||
|
|
||||||
|
// 5. 單向從起點移動到終點,然後重頭再來
|
||||||
|
// === 新增 HTML箭頭動畫,會跟隨Sprite移動並指向終點 ===
|
||||||
|
let arrow = document.createElement("img");
|
||||||
|
arrow.src = "arrow.png";
|
||||||
|
arrow.style.position = "absolute";
|
||||||
|
arrow.style.width = "22px";
|
||||||
|
arrow.style.height = "22px";
|
||||||
|
arrow.style.pointerEvents = "none";
|
||||||
|
arrow.style.zIndex = 10;
|
||||||
|
arrow.className = `custom-3d-arrow-${dbId}`; // 加上 dbId 作為唯一識別
|
||||||
|
viewer.container.appendChild(arrow);
|
||||||
|
|
||||||
|
function updateArrow(pos) {
|
||||||
|
const pos2d = viewer.worldToClient(pos.clone());
|
||||||
|
const end2d = viewer.worldToClient(animEnd.clone());
|
||||||
|
arrow.style.left = `${pos2d.x - 16}px`;
|
||||||
|
arrow.style.top = `${pos2d.y - 16}px`;
|
||||||
|
// 算角度:pos 指向 animEnd
|
||||||
|
const dx = end2d.x - pos2d.x;
|
||||||
|
const dy = end2d.y - pos2d.y;
|
||||||
|
const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
|
||||||
|
arrow.style.transform = `rotate(${angle}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// camera 移動時也要更新所有箭頭(只添加一次事件監聽器)
|
||||||
|
if (!cameraEventAdded) {
|
||||||
|
viewer.addEventListener(
|
||||||
|
Autodesk.Viewing.CAMERA_CHANGE_EVENT,
|
||||||
|
() => {
|
||||||
|
spriteAnimations.forEach((animation) => {
|
||||||
|
animation.updateArrow(animation.currentPos);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
cameraEventAdded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 動畫主程式
|
||||||
|
let t = 0;
|
||||||
|
let currentPos = animStart.clone();
|
||||||
|
const animationInterval = setInterval(() => {
|
||||||
|
t += 0.01;
|
||||||
|
if (t > 1) t = 0; // 到終點就重頭
|
||||||
|
currentPos = animStart.clone().lerp(animEnd, t);
|
||||||
|
dataVizExtn.invalidateViewables([dbId], (v) => {
|
||||||
|
return {
|
||||||
|
position: {
|
||||||
|
x: currentPos.x,
|
||||||
|
y: currentPos.y,
|
||||||
|
z: currentPos.z,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
updateArrow(currentPos);
|
||||||
|
|
||||||
|
// 更新保存的 currentPos
|
||||||
|
if (spriteAnimations.has(dbId)) {
|
||||||
|
spriteAnimations.get(dbId).currentPos = currentPos;
|
||||||
|
}
|
||||||
|
}, 30);
|
||||||
|
|
||||||
|
// 保存這個 dbId 的動畫資訊
|
||||||
|
spriteAnimations.set(dbId, {
|
||||||
|
interval: animationInterval,
|
||||||
|
arrow: arrow,
|
||||||
|
viewable: viewable,
|
||||||
|
updateArrow: updateArrow,
|
||||||
|
currentPos: currentPos,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("創建動態Sprites時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理單個 Sprite 的函數
|
||||||
|
const clearSingleSprite = (dbId) => {
|
||||||
|
if (spriteAnimations.has(dbId)) {
|
||||||
|
const animation = spriteAnimations.get(dbId);
|
||||||
|
|
||||||
|
// 清理動畫計時器
|
||||||
|
if (animation.interval) {
|
||||||
|
clearInterval(animation.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理箭頭元素
|
||||||
|
if (animation.arrow && animation.arrow.parentNode) {
|
||||||
|
animation.arrow.parentNode.removeChild(animation.arrow);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除這個 dbId 的記錄
|
||||||
|
spriteAnimations.delete(dbId);
|
||||||
|
|
||||||
|
console.log(`已清理 dbId ${dbId} 的 Sprite 資源`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理所有 Sprites 和相關資源的函數
|
||||||
|
const clearSprites = () => {
|
||||||
|
// 清理所有動畫
|
||||||
|
spriteAnimations.forEach((animation, dbId) => {
|
||||||
|
clearSingleSprite(dbId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清理所有 viewables
|
||||||
|
if (dataVizExtn) {
|
||||||
|
dataVizExtn.removeAllViewables();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置相機事件標記
|
||||||
|
cameraEventAdded = false;
|
||||||
|
|
||||||
|
console.log("已清理所有 Sprites 和相關資源");
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribeData,
|
subscribeData,
|
||||||
visibleDbid,
|
visibleDbid,
|
||||||
@ -420,5 +776,8 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
|||||||
urn,
|
urn,
|
||||||
updateInitialData,
|
updateInitialData,
|
||||||
subComponents,
|
subComponents,
|
||||||
|
createSprites,
|
||||||
|
clearSprites,
|
||||||
|
clearSingleSprite,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
201
src/hooks/useDashboardDevice.js
Normal file
201
src/hooks/useDashboardDevice.js
Normal file
@ -0,0 +1,201 @@
|
|||||||
|
import { ref, onUnmounted } from "vue";
|
||||||
|
import { getDashboardDevice } from "@/apis/dashboard";
|
||||||
|
|
||||||
|
export function useDashboardDevice() {
|
||||||
|
const rawData = ref([]);
|
||||||
|
const subscribers = ref([]);
|
||||||
|
|
||||||
|
// 獲取設備資料
|
||||||
|
const getDevice = async (option = 1) => {
|
||||||
|
try {
|
||||||
|
const res = await getDashboardDevice({
|
||||||
|
option: parseInt(option),
|
||||||
|
});
|
||||||
|
|
||||||
|
rawData.value = res.data.map((d) => ({
|
||||||
|
...d,
|
||||||
|
key: d.subSys,
|
||||||
|
// 初始化點位值物件
|
||||||
|
device: d.device.map((device) => ({
|
||||||
|
...device,
|
||||||
|
pointValues: {},
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
console.error("獲取設備資料失敗:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理所有訂閱者
|
||||||
|
const clearSubscribers = () => {
|
||||||
|
subscribers.value.forEach((subscriber) => {
|
||||||
|
try {
|
||||||
|
subscriber.detach("changed");
|
||||||
|
subscriber.unsubscribeAll();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("清理訂閱者失敗:", err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
subscribers.value = [];
|
||||||
|
console.log("所有設備訂閱已清除");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新設備點位值
|
||||||
|
const updateDevicePointValue = (deviceNumber, pointName, value) => {
|
||||||
|
rawData.value.forEach((category) => {
|
||||||
|
category.device.forEach((device) => {
|
||||||
|
if (device.device_number === deviceNumber) {
|
||||||
|
if (!device.pointValues) {
|
||||||
|
device.pointValues = {};
|
||||||
|
}
|
||||||
|
device.pointValues[pointName] = value;
|
||||||
|
console.log(`設備 ${deviceNumber} 的 ${pointName} 更新為:`, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 訂閱單個設備
|
||||||
|
const subscribeToDevice = (item) => {
|
||||||
|
const slotPath = item.slotPath;
|
||||||
|
const ordString = `local:|foxs:|station:|${slotPath}`;
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.require &&
|
||||||
|
// @ts-ignore
|
||||||
|
window.requirejs(["baja!"], (baja) => {
|
||||||
|
// 建立訂閱器
|
||||||
|
const subscriber = new baja.Subscriber();
|
||||||
|
|
||||||
|
// 定義 changed 事件的處理函數
|
||||||
|
subscriber.attach("changed", (prop) => {
|
||||||
|
try {
|
||||||
|
if (prop && prop.getName() === "out") {
|
||||||
|
// 取得 out 的新值
|
||||||
|
const match = prop.$display.match(/^(\d+(\.\d+)?)/);
|
||||||
|
const newValue = match ? parseFloat(match[0]) : prop.newValue || 0;
|
||||||
|
|
||||||
|
// 更新對應設備的點位值
|
||||||
|
updateDevicePointValue(item.displayName, item.name, newValue);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`處理 ${item.displayName} 的 ${item.name} 變化失敗:`, error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
baja.Ord.make(ordString)
|
||||||
|
.get({ subscriber })
|
||||||
|
.then(() => {
|
||||||
|
console.log(`成功訂閱設備: ${item.displayName} - ${item.name}`);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error(`訂閱 ${item.displayName} 失敗:`, err.message);
|
||||||
|
subscriber.detach("changed");
|
||||||
|
});
|
||||||
|
|
||||||
|
subscribers.value.push(subscriber);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 訂閱 Niagara 資料
|
||||||
|
const subscribeNiagaraData = async (option) => {
|
||||||
|
try {
|
||||||
|
// 先清理舊的訂閱者
|
||||||
|
clearSubscribers();
|
||||||
|
|
||||||
|
const ordString = `local:|foxs:|station:|slot:/Drivers|neql:IBMS:PD0${option}|bql:select slotPath,parent.displayName,name,out`;
|
||||||
|
console.log("訂閱 Niagara 資料:", ordString);
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
window.require &&
|
||||||
|
// @ts-ignore
|
||||||
|
window.requirejs(["baja!"], (baja) => {
|
||||||
|
let deviceList = [];
|
||||||
|
|
||||||
|
baja.Ord.make(ordString).get({
|
||||||
|
cursor: {
|
||||||
|
before: () => {
|
||||||
|
console.log("開始獲取設備清單");
|
||||||
|
},
|
||||||
|
each: (record) => {
|
||||||
|
const newItem = {
|
||||||
|
slotPath: record.get("slotPath"),
|
||||||
|
displayName: record.get("parent$2edisplayName"),
|
||||||
|
name: record.get("name"),
|
||||||
|
out: record.get("out")?.get("value") ?? 0,
|
||||||
|
};
|
||||||
|
deviceList.push(newItem);
|
||||||
|
},
|
||||||
|
after: () => {
|
||||||
|
console.log(`找到 ${deviceList.length} 個設備點位`);
|
||||||
|
// 對每個設備點位進行訂閱
|
||||||
|
deviceList.forEach((item) => {
|
||||||
|
subscribeToDevice(item);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("訂閱 Niagara 資料失敗:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 處理選項變更
|
||||||
|
const handleOptionChange = async (newOption) => {
|
||||||
|
try {
|
||||||
|
// 先獲取設備資料,再訂閱 Niagara 資料
|
||||||
|
await getDevice(newOption);
|
||||||
|
await subscribeNiagaraData(newOption);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("處理選項變更失敗:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取得特定設備的點位值
|
||||||
|
const getDevicePointValue = (deviceNumber, pointName) => {
|
||||||
|
for (const category of rawData.value) {
|
||||||
|
const device = category.device.find(
|
||||||
|
(d) => d.device_number === deviceNumber
|
||||||
|
);
|
||||||
|
if (device && device.pointValues) {
|
||||||
|
return device.pointValues[pointName];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 取得特定類別的所有設備
|
||||||
|
const getCategoryDevices = (subSys) => {
|
||||||
|
const category = rawData.value.find((c) => c.subSys === subSys);
|
||||||
|
return category ? category.device : [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 清理函數
|
||||||
|
const cleanup = () => {
|
||||||
|
clearSubscribers();
|
||||||
|
rawData.value = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
// 元件卸載時自動清理
|
||||||
|
onUnmounted(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 狀態
|
||||||
|
rawData,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
getDevice,
|
||||||
|
subscribeNiagaraData,
|
||||||
|
handleOptionChange,
|
||||||
|
getDevicePointValue,
|
||||||
|
getCategoryDevices,
|
||||||
|
cleanup,
|
||||||
|
|
||||||
|
// 內部方法(如果需要的話)
|
||||||
|
updateDevicePointValue,
|
||||||
|
clearSubscribers,
|
||||||
|
};
|
||||||
|
}
|
@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { onMounted, ref, watch, onUnmounted } from "vue";
|
||||||
import Forge from "@/components/forge/Forge.vue";
|
import Forge from "@/components/forge/Forge.vue";
|
||||||
import DashboardProduct from "./components/DashboardProduct.vue";
|
import DashboardProduct from "./components/DashboardProduct.vue";
|
||||||
import DashboardTarget from "./components/DashboardTarget.vue";
|
import DashboardTarget from "./components/DashboardTarget.vue";
|
||||||
@ -8,39 +9,81 @@ import DashboardElectricity from "./components/DashboardElectricity.vue";
|
|||||||
import DashboardAlert from "./components/DashboardAlert.vue";
|
import DashboardAlert from "./components/DashboardAlert.vue";
|
||||||
import DashboardForgeOptionButton from "./components/DashboardForgeOptionButton.vue";
|
import DashboardForgeOptionButton from "./components/DashboardForgeOptionButton.vue";
|
||||||
import DashboardForgeOptionCard from "./components/DashboardForgeOptionCard.vue";
|
import DashboardForgeOptionCard from "./components/DashboardForgeOptionCard.vue";
|
||||||
import { getDashboardInit } from "@/apis/dashboard";
|
import { getDashboardInit, getDashboardOptionRealTimeData } from "@/apis/dashboard";
|
||||||
import { onMounted, ref, provide, watch } from "vue";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
const initialData = ref(null);
|
const initialData = ref(null);
|
||||||
// const forgeData = ref([]);
|
const realTimeData = ref(null);
|
||||||
|
const realTime = ref(null);
|
||||||
|
const { searchParams } = useSearchParams();
|
||||||
|
let intervalId = null;
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
const res = await getDashboardInit();
|
try {
|
||||||
initialData.value = res.data;
|
const res = await getDashboardInit();
|
||||||
|
initialData.value = res.data;
|
||||||
|
} catch (err) {
|
||||||
|
console.error("初始化失敗:", err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getDevice = async (option = 1) => {
|
||||||
|
try {
|
||||||
|
const res = await getDashboardOptionRealTimeData({
|
||||||
|
option: parseInt(option),
|
||||||
|
});
|
||||||
|
realTimeData.value = res.data;
|
||||||
|
realTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
console.log("實時數據:", realTimeData.value);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("獲取實時數據失敗:", err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 開始定時器
|
||||||
|
const startInterval = (option) => {
|
||||||
|
// 清除之前的定時器
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 立即執行一次
|
||||||
|
getDevice(option);
|
||||||
|
|
||||||
|
// 設定每5秒執行一次
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
getDevice(option);
|
||||||
|
}, 5000);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 停止定時器
|
||||||
|
const stopInterval = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
intervalId = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
()=>searchParams.value.option,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
startInterval(newValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
deep: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
|
|
||||||
const intervalOption = ref({});
|
onUnmounted(() => {
|
||||||
const currentIntervalType = ref("");
|
stopInterval();
|
||||||
|
|
||||||
const openModal = (type) => {
|
|
||||||
currentIntervalType.value = type;
|
|
||||||
dashboard_more.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const decideIntervalOption = (option) => {
|
|
||||||
intervalOption.value[currentIntervalType.value] = option;
|
|
||||||
};
|
|
||||||
|
|
||||||
provide("dashboard_items", {
|
|
||||||
initialData,
|
|
||||||
// forgeData,
|
|
||||||
openModal,
|
|
||||||
decideIntervalOption,
|
|
||||||
intervalOption,
|
|
||||||
currentIntervalType,
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -55,9 +98,9 @@ provide("dashboard_items", {
|
|||||||
<DashboardAlert />
|
<DashboardAlert />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Forge :fullScreen="true" :initialData="initialData" />
|
<Forge :fullScreen="true" :initialData="initialData" :realTime="realTime"/>
|
||||||
<DashboardForgeOptionButton :initialData="initialData" />
|
<DashboardForgeOptionButton :initialData="initialData" />
|
||||||
<DashboardForgeOptionCard />
|
<DashboardForgeOptionCard :realTimeData="realTimeData"/>
|
||||||
<div class="w-1/4 flex flex-col justify-start border-dashboard z-20">
|
<div class="w-1/4 flex flex-col justify-start border-dashboard z-20">
|
||||||
<div class=""><DashboardImmediateTemp /></div>
|
<div class=""><DashboardImmediateTemp /></div>
|
||||||
<div class="mt-5">
|
<div class="mt-5">
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="card bg-neutral text-neutral-content shadow-sm shadow-gray-400">
|
<div class="card bg-neutral text-neutral-content shadow-sm shadow-gray-400">
|
||||||
<div class="card-body text-xs p-3">
|
<div class="card-body text-xs px-3 py-4">
|
||||||
<p>即時庫存量: {{ inventory }} 頓</p>
|
<p>安全庫存量: {{ inventory }} 頓</p>
|
||||||
<p>目標庫存量: {{ targetInventory }} 頓</p>
|
<p>目標庫存量: {{ targetInventory }} 頓</p>
|
||||||
<p>前次增加: {{ lastIncrease }} 頓</p>
|
|
||||||
<p> {{ updateTime }}</p>
|
<p> {{ updateTime }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -3,76 +3,14 @@ import { ref, watch } from "vue";
|
|||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
// 假資料
|
const props = defineProps({
|
||||||
const initialData = ref({
|
initialData: Object,
|
||||||
options: [
|
|
||||||
{
|
|
||||||
option: 1,
|
|
||||||
text: "生產資訊",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -50.037341934259175,\r\n "y": 4.193144220651064,\r\n "z": 48.82382986271525\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -48.60645671639422,\r\n "y": -243.44871185400785,\r\n "z": -209.89087363853187\r\n}',
|
|
||||||
top: "40%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
option: 2,
|
|
||||||
text: "第一調理室",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -21.45136358561956,\r\n "y": -36.616360736377224,\r\n "z": 24.06657791386018\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -13.443148103609595,\r\n "y": -285.69705764358827,\r\n "z": -233.14249858492653\r\n}',
|
|
||||||
top: "40%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
option: 3,
|
|
||||||
text: "第二調理室",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -46.43016370491481,\r\n "y": -30.764006433151266,\r\n "z": 29.75302179892112\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -48.66787230158924,\r\n "y": -253.50633028228958,\r\n "z": -200.14573739171746\r\n}',
|
|
||||||
top: "40%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
option: 4,
|
|
||||||
text: "冷藏室",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -159.1333742651323,\r\n "y": 0.20725923658232803,\r\n "z": 57.03929922462568\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -153.80612704653015,\r\n "y": -182.43894674945912,\r\n "z": -176.99952023763058\r\n}',
|
|
||||||
top: "40%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
option: 5,
|
|
||||||
text: "用電",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -50.037341934259175,\r\n "y": 4.193144220651064,\r\n "z": 48.82382986271525\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -48.60645671639422,\r\n "y": -243.44871185400785,\r\n "z": -209.89087363853187\r\n}',
|
|
||||||
top: "40%",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
option: 6,
|
|
||||||
text: "威鈦閥狀態",
|
|
||||||
visible: true,
|
|
||||||
camera_position:
|
|
||||||
'{\r\n "x": -50.037341934259175,\r\n "y": 4.193144220651064,\r\n "z": 48.82382986271525\r\n}',
|
|
||||||
target_position:
|
|
||||||
'{\r\n "x": -48.60645671639422,\r\n "y": -243.44871185400785,\r\n "z": -209.89087363853187\r\n}',
|
|
||||||
top: "25%",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { changeParams, searchParams } = useSearchParams();
|
const { changeParams, searchParams } = useSearchParams();
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => initialData.value,
|
() => props.initialData,
|
||||||
(newValue) => {
|
(newValue) => {
|
||||||
if (newValue?.options[0]) {
|
if (newValue?.options[0]) {
|
||||||
const { option, camera_position, target_position, top } = newValue.options[0];
|
const { option, camera_position, target_position, top } = newValue.options[0];
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, defineProps, inject, watch } from "vue";
|
import { ref, defineProps, inject, watch, computed } from "vue";
|
||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import ProductionCard from "./dashboardForgeCards/ProductionCard.vue";
|
import ProductionCard from "./dashboardForgeCards/ProductionCard.vue";
|
||||||
@ -13,396 +13,96 @@ import MeterCard from "./dashboardForgeCards/MeterCard.vue";
|
|||||||
import ValveCard from "./dashboardForgeCards/ValveCard.vue";
|
import ValveCard from "./dashboardForgeCards/ValveCard.vue";
|
||||||
|
|
||||||
const { searchParams, changeParams } = useSearchParams();
|
const { searchParams, changeParams } = useSearchParams();
|
||||||
|
const props = defineProps({
|
||||||
|
realTimeData: Object,
|
||||||
|
});
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: "生產資訊" },
|
{ label: "生產資訊" },
|
||||||
{ label: "投料進度" },
|
{ label: "投料進度" },
|
||||||
|
{ label: "品檢" },
|
||||||
{ label: "流量計" },
|
{ label: "流量計" },
|
||||||
|
{ label: "SIP" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabs2 = [{ label: "生產資訊" }, { label: "品檢" }, { label: "流量計" }];
|
|
||||||
|
|
||||||
// 生產資訊卡片資料
|
// 生產資訊卡片資料
|
||||||
const productionData = [
|
const productionData = computed(() => {
|
||||||
{
|
return props.realTimeData?.productionData || [];
|
||||||
batch: "7",
|
});
|
||||||
product: "蘋果醋",
|
const cookingData = computed(() => {
|
||||||
equipment: "二重釜 800L",
|
return props.realTimeData?.cookingData || [];
|
||||||
status: "投料中",
|
});
|
||||||
temperature: "70°C",
|
const heaterData = computed(() => {
|
||||||
},
|
return props.realTimeData?.heaterData || [];
|
||||||
{
|
});
|
||||||
batch: "7",
|
|
||||||
product: "蘋果醋",
|
|
||||||
equipment: "二重釜 400L",
|
|
||||||
status: "待機中",
|
|
||||||
temperature: "60°C",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const cookingData = [
|
|
||||||
{
|
|
||||||
batch: "3",
|
|
||||||
product: "蘋果醋",
|
|
||||||
equipment: "調理鍋 甲",
|
|
||||||
status: "調理中",
|
|
||||||
temperature: "70°C",
|
|
||||||
sterilization: "殺菌OK",
|
|
||||||
sugar: "6%",
|
|
||||||
material: "品管",
|
|
||||||
sugarStatus: "",
|
|
||||||
materialColor: "text-gray-200",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
batch: "4",
|
|
||||||
product: "蘋果醋",
|
|
||||||
equipment: "調理鍋 乙",
|
|
||||||
status: "檢驗中",
|
|
||||||
temperature: "70°C",
|
|
||||||
sterilization: "殺菌OK",
|
|
||||||
sugar: "10%",
|
|
||||||
material: "品管",
|
|
||||||
sugarStatus: "warning",
|
|
||||||
materialColor: "text-yellow-400",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
batch: "5",
|
|
||||||
product: "蘋果醋",
|
|
||||||
equipment: "調理鍋 丙",
|
|
||||||
status: "充填中",
|
|
||||||
temperature: "60°C",
|
|
||||||
sterilization: "殺菌NG",
|
|
||||||
sugar: "6%",
|
|
||||||
material: "品管",
|
|
||||||
sugarStatus: "",
|
|
||||||
materialColor: "text-green-500",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
batch: "6",
|
|
||||||
product: "蘋果醋",
|
|
||||||
equipment: "調理鍋 丁",
|
|
||||||
status: "待機中",
|
|
||||||
temperature: "80°C",
|
|
||||||
sterilization: "殺菌OK",
|
|
||||||
sugar: "6%",
|
|
||||||
material: "品管",
|
|
||||||
sugarStatus: "",
|
|
||||||
materialColor: "text-red-500",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const heaterData = [
|
|
||||||
{
|
|
||||||
equipment: "加熱器 板式",
|
|
||||||
temperature: "70°C",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
equipment: "加熱器 管式",
|
|
||||||
temperature: "70°C",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 二重釜資料
|
// 管理所有設備的activeTab狀態
|
||||||
const vesselsData = [
|
const deviceActiveTabs = ref(new Map());
|
||||||
{
|
|
||||||
name: "二重釜 800L",
|
|
||||||
product: "蘋果醋",
|
|
||||||
batch: "7",
|
|
||||||
status: "投料中",
|
|
||||||
feedProgress: [
|
|
||||||
{ order: "1-1", action: "人工投料", material: "液糖", state: "完成" },
|
|
||||||
{
|
|
||||||
order: "1-2",
|
|
||||||
action: "人工投料",
|
|
||||||
material: "浸泡梅子醋",
|
|
||||||
state: "完成",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: "1-3",
|
|
||||||
action: "流量計投料",
|
|
||||||
material: "梅精醋",
|
|
||||||
state: "正在進行",
|
|
||||||
},
|
|
||||||
{ order: "1-4", action: "攪拌", material: "-", state: "尚未開始" },
|
|
||||||
],
|
|
||||||
flowMeterData: [
|
|
||||||
{ material: "水", current: "200L", total: "1000L" },
|
|
||||||
{ material: "原醋", current: "20L", total: "1000L" },
|
|
||||||
{ material: "梅精醋", current: "10L", total: "50L" },
|
|
||||||
{ material: "醋酸鈣液", current: "10L", total: "50L" },
|
|
||||||
],
|
|
||||||
activeTab: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "二重釜 400L",
|
|
||||||
product: "蘋果醋",
|
|
||||||
batch: "7",
|
|
||||||
status: "待機中",
|
|
||||||
feedProgress: [
|
|
||||||
{ order: "1-1", action: "人工投料", material: "液糖", state: "完成" },
|
|
||||||
{
|
|
||||||
order: "1-2",
|
|
||||||
action: "人工投料",
|
|
||||||
material: "浸泡梅子醋",
|
|
||||||
state: "完成",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: "1-3",
|
|
||||||
action: "流量計投料",
|
|
||||||
material: "梅精醋",
|
|
||||||
state: "正在進行",
|
|
||||||
},
|
|
||||||
{ order: "1-4", action: "攪拌", material: "-", state: "尚未開始" },
|
|
||||||
],
|
|
||||||
flowMeterData: [
|
|
||||||
{ material: "水", current: "200L", total: "1000L" },
|
|
||||||
{ material: "原醋", current: "20L", total: "1000L" },
|
|
||||||
{ material: "梅精醋", current: "10L", total: "50L" },
|
|
||||||
{ material: "醋酸鈣液", current: "10L", total: "50L" },
|
|
||||||
],
|
|
||||||
activeTab: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 調理鍋資料
|
// 獲取或創建設備的activeTab
|
||||||
const cookingPotData = [
|
const getDeviceActiveTab = (deviceName, defaultTab = "生產資訊") => {
|
||||||
{
|
if (!deviceActiveTabs.value.has(deviceName)) {
|
||||||
name: "調理鍋 甲",
|
deviceActiveTabs.value.set(deviceName, ref(defaultTab));
|
||||||
product: "蘋果醋",
|
}
|
||||||
batch: "7",
|
return deviceActiveTabs.value.get(deviceName);
|
||||||
temperature: "80°C",
|
|
||||||
sugar: "6Brix(%)",
|
|
||||||
status: "調理中",
|
|
||||||
inspectionTime: "08:23",
|
|
||||||
flavor: "正常",
|
|
||||||
salinity: "正常",
|
|
||||||
acidity: "ph5.5",
|
|
||||||
waterConsumption: "0L",
|
|
||||||
aceticAcidConsumption: "100L",
|
|
||||||
activeTab2: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "調理鍋 乙",
|
|
||||||
product: "蘋果醋",
|
|
||||||
batch: "7",
|
|
||||||
temperature: "80°C",
|
|
||||||
sugar: "6Brix(%)",
|
|
||||||
status: "調理中",
|
|
||||||
inspectionTime: "08:23",
|
|
||||||
flavor: "正常",
|
|
||||||
salinity: "正常",
|
|
||||||
acidity: "ph5.5",
|
|
||||||
waterConsumption: "0L",
|
|
||||||
aceticAcidConsumption: "100L",
|
|
||||||
activeTab2: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "調理鍋 丙",
|
|
||||||
product: "蘋果醋",
|
|
||||||
batch: "7",
|
|
||||||
temperature: "80°C",
|
|
||||||
sugar: "6Brix(%)",
|
|
||||||
status: "調理中",
|
|
||||||
inspectionTime: "08:23",
|
|
||||||
flavor: "正常",
|
|
||||||
salinity: "正常",
|
|
||||||
acidity: "ph5.5",
|
|
||||||
waterConsumption: "0L",
|
|
||||||
aceticAcidConsumption: "100L",
|
|
||||||
activeTab2: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "調理鍋 丁",
|
|
||||||
product: "蘋果醋",
|
|
||||||
batch: "7",
|
|
||||||
temperature: "80°C",
|
|
||||||
sugar: "6Brix(%)",
|
|
||||||
status: "調理中",
|
|
||||||
inspectionTime: "08:23",
|
|
||||||
flavor: "正常",
|
|
||||||
salinity: "正常",
|
|
||||||
acidity: "ph5.5",
|
|
||||||
waterConsumption: "0L",
|
|
||||||
aceticAcidConsumption: "100L",
|
|
||||||
activeTab2: ref("生產資訊"),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 加熱器資料
|
|
||||||
const heaterPotData = [
|
|
||||||
{ name: "加熱器 板式", temperature: "3.1°C" },
|
|
||||||
{ name: "加熱器 管式", temperature: "3.1°C" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 冷藏室資料
|
|
||||||
const refrigerationData = {
|
|
||||||
name: "冷藏室",
|
|
||||||
temperature: "-1°C",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 二重釜資料
|
||||||
|
const vesselsData = computed(() => {
|
||||||
|
const data = props.realTimeData?.productionData || [];
|
||||||
|
return data.map((vessel) => ({
|
||||||
|
...vessel,
|
||||||
|
activeTab: getDeviceActiveTab(vessel.name),
|
||||||
|
SIP: [
|
||||||
|
{
|
||||||
|
count: "1",
|
||||||
|
startTime: "08:00",
|
||||||
|
endTime: "08:30",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
count: "2",
|
||||||
|
startTime: "14:00",
|
||||||
|
endTime: "14:30",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 加熱器資料
|
||||||
|
const heaterPotData = computed(() => {
|
||||||
|
return props.realTimeData?.heaterData || [];
|
||||||
|
});
|
||||||
|
|
||||||
|
// 調理鍋資料
|
||||||
|
const cookingPotData = computed(() => {
|
||||||
|
const data = props.realTimeData?.cookingData || [];
|
||||||
|
return data.map((pot) => ({
|
||||||
|
...pot,
|
||||||
|
activeTab2: getDeviceActiveTab(pot.name),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
// 冷藏室資料
|
||||||
|
const refrigerationData = computed(() => {
|
||||||
|
return props.realTimeData?.refrigerationData[0] || {};
|
||||||
|
});
|
||||||
|
|
||||||
// 電錶資料
|
// 電錶資料
|
||||||
const meterData = [
|
const meterData = computed(() => {
|
||||||
{
|
const data = props.realTimeData?.refrigerationData || [];
|
||||||
name: "電錶01 汙水區",
|
return data.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
current: "18.6A",
|
});
|
||||||
voltage: "225.6V",
|
|
||||||
power: "5936.0W",
|
|
||||||
energyConsumption: "3429.4kWh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "電錶02 發酵槽",
|
|
||||||
current: "18.6A",
|
|
||||||
voltage: "225.6V",
|
|
||||||
power: "5936.0W",
|
|
||||||
energyConsumption: "3429.4kWh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "電錶03 調理室",
|
|
||||||
current: "18.6A",
|
|
||||||
voltage: "225.6V",
|
|
||||||
power: "5936.0W",
|
|
||||||
energyConsumption: "3429.4kWh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "電錶04 冷藏室",
|
|
||||||
current: "18.6A",
|
|
||||||
voltage: "225.6V",
|
|
||||||
power: "5936.0W",
|
|
||||||
energyConsumption: "3429.4kWh",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "電錶05 空壓機",
|
|
||||||
current: "18.6A",
|
|
||||||
voltage: "225.6V",
|
|
||||||
power: "5936.0W",
|
|
||||||
energyConsumption: "3429.4kWh",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
// 威鈦閥異常訊號資料
|
// 威鈦閥異常訊號資料
|
||||||
const valveData = [
|
const valveData = computed(() => {
|
||||||
{
|
return props.realTimeData?.valveData || {};
|
||||||
title: "二重釜800L",
|
});
|
||||||
groups: [
|
|
||||||
{ code: "SV3", status: "ON" },
|
|
||||||
{ code: "SV4", status: "ON" },
|
|
||||||
{ code: "SV5", status: "ON" },
|
|
||||||
{ code: "SV6", status: "ON" },
|
|
||||||
{ code: "SV7", status: "ON" },
|
|
||||||
{ code: "SV8", status: "ON" },
|
|
||||||
{ code: "SV9", status: "ON" },
|
|
||||||
{ code: "SV10", status: "ON" },
|
|
||||||
{ code: "SV11", status: "ON" },
|
|
||||||
{ code: "SV20", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "二重釜400L",
|
|
||||||
groups: [
|
|
||||||
{ code: "SV12", status: "ON" },
|
|
||||||
{ code: "SV13", status: "ON" },
|
|
||||||
{ code: "SV14", status: "ON" },
|
|
||||||
{ code: "SV15", status: "ON" },
|
|
||||||
{ code: "SV16", status: "ON" },
|
|
||||||
{ code: "SV17", status: "ON" },
|
|
||||||
{ code: "SV18", status: "ON" },
|
|
||||||
{ code: "SV19", status: "ON" },
|
|
||||||
{ code: "SV21", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "補料",
|
|
||||||
groups: [
|
|
||||||
{ code: "SV1", status: "ON" },
|
|
||||||
{ code: "SV2", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "調配槽1",
|
|
||||||
groups: [
|
|
||||||
{ code: "BV1", status: "ON" },
|
|
||||||
{ code: "BV2", status: "ON" },
|
|
||||||
{ code: "BV3", status: "ON" },
|
|
||||||
{ code: "BV4", status: "ON" },
|
|
||||||
{ code: "BV5", status: "ON" },
|
|
||||||
{ code: "BV6", status: "ON" },
|
|
||||||
{ code: "BV7", status: "ON" },
|
|
||||||
{ code: "BV8", status: "ON" },
|
|
||||||
{ code: "BV46", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "調配槽2",
|
|
||||||
groups: [
|
|
||||||
{ code: "BV9", status: "ON" },
|
|
||||||
{ code: "BV10", status: "ON" },
|
|
||||||
{ code: "BV11", status: "ON" },
|
|
||||||
{ code: "BV12", status: "ON" },
|
|
||||||
{ code: "BV13", status: "ON" },
|
|
||||||
{ code: "BV14", status: "ON" },
|
|
||||||
{ code: "BV15", status: "ON" },
|
|
||||||
{ code: "BV16", status: "ON" },
|
|
||||||
{ code: "BV47", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "調配槽3",
|
|
||||||
groups: [
|
|
||||||
{ code: "BV17", status: "ON" },
|
|
||||||
{ code: "BV18", status: "ON" },
|
|
||||||
{ code: "BV19", status: "ON" },
|
|
||||||
{ code: "BV20", status: "ON" },
|
|
||||||
{ code: "BV21", status: "ON" },
|
|
||||||
{ code: "BV22", status: "ON" },
|
|
||||||
{ code: "BV23", status: "ON" },
|
|
||||||
{ code: "BV24", status: "ON" },
|
|
||||||
{ code: "BV48", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "調配槽4",
|
|
||||||
groups: [
|
|
||||||
{ code: "BV25", status: "ON" },
|
|
||||||
{ code: "BV26", status: "ON" },
|
|
||||||
{ code: "BV27", status: "ON" },
|
|
||||||
{ code: "BV28", status: "ON" },
|
|
||||||
{ code: "BV29", status: "ON" },
|
|
||||||
{ code: "BV30", status: "ON" },
|
|
||||||
{ code: "BV31", status: "ON" },
|
|
||||||
{ code: "BV32", status: "ON" },
|
|
||||||
{ code: "BV49", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "熱水",
|
|
||||||
groups: [
|
|
||||||
{ code: "HV1", status: "ON" },
|
|
||||||
{ code: "HV2", status: "ON" },
|
|
||||||
{ code: "HV3", status: "ON" },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "其他",
|
|
||||||
groups: [
|
|
||||||
{ code: "BV33", status: "ON" },
|
|
||||||
{ code: "BV34", status: "ON" },
|
|
||||||
{ code: "BV35", status: "ON" },
|
|
||||||
{ code: "BV36", status: "ON" },
|
|
||||||
{ code: "BV37", status: "ON" },
|
|
||||||
{ code: "BV38", status: "ON" },
|
|
||||||
{ code: "BV39", status: "ON" },
|
|
||||||
{ code: "BV40", status: "ON" },
|
|
||||||
{ code: "BV41", status: "ON" },
|
|
||||||
{ code: "BV42", status: "ON" },
|
|
||||||
{ code: "BV43", status: "ON" },
|
|
||||||
{ code: "BV44", status: "ON" },
|
|
||||||
{ code: "BV45", status: "ON" },
|
|
||||||
],
|
|
||||||
}
|
|
||||||
];
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="absolute left-1/2 -translate-x-1/2 z-50"
|
<div
|
||||||
:style="{ top: searchParams?.top || '40%' }"
|
class="absolute left-1/2 -translate-x-1/2 z-50"
|
||||||
|
:style="{ top: searchParams?.top || '40%' }"
|
||||||
>
|
>
|
||||||
<template v-if="searchParams?.option == '1'">
|
<template v-if="searchParams?.option == '1'">
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
@ -412,7 +112,7 @@ const valveData = [
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="searchParams?.option == '2'">
|
<template v-else-if="searchParams?.option == '2'">
|
||||||
<div class="grid grid-flow-col grid-rows-1 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<VesselCard
|
<VesselCard
|
||||||
v-for="(vessel, index) in vesselsData"
|
v-for="(vessel, index) in vesselsData"
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -422,19 +122,18 @@ const valveData = [
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="searchParams?.option == '3'">
|
<template v-else-if="searchParams?.option == '3'">
|
||||||
<div class="grid grid-flow-col grid-rows-2 gap-4">
|
<div class="grid grid-cols-2 gap-4">
|
||||||
<HeaterPotCard
|
<!-- <HeaterPotCard
|
||||||
v-for="(heat, index) in heaterPotData"
|
v-for="(heat, index) in heaterPotData"
|
||||||
:key="index"
|
:key="index"
|
||||||
:heat="heat"
|
:heat="heat"
|
||||||
/>
|
/> -->
|
||||||
<CookingPotCard
|
<CookingPotCard
|
||||||
v-for="(pot, index) in cookingPotData"
|
v-for="(pot, index) in cookingPotData"
|
||||||
:key="index"
|
:key="index"
|
||||||
:pot="pot"
|
:pot="pot"
|
||||||
:tabs2="tabs2"
|
:tabs="tabs"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template v-else-if="searchParams?.option == '4'">
|
<template v-else-if="searchParams?.option == '4'">
|
||||||
|
@ -7,7 +7,6 @@ import { getDashboardTemp } from "@/apis/dashboard";
|
|||||||
import useSearchParams from "@/hooks/useSearchParam";
|
import useSearchParams from "@/hooks/useSearchParam";
|
||||||
|
|
||||||
const { searchParams } = useSearchParams();
|
const { searchParams } = useSearchParams();
|
||||||
const { openModal, intervalOption } = inject("dashboard_items");
|
|
||||||
const intervalType = "frozen";
|
const intervalType = "frozen";
|
||||||
|
|
||||||
const defaultChartOption = ref({
|
const defaultChartOption = ref({
|
||||||
|
@ -4,7 +4,6 @@ import { ref, onMounted, provide, watch, inject } from "vue";
|
|||||||
import { getDashboardProductCompletion } from "@/apis/dashboard";
|
import { getDashboardProductCompletion } from "@/apis/dashboard";
|
||||||
import DashboardDescriptionCard from "./DashboardDescriptionCard.vue";
|
import DashboardDescriptionCard from "./DashboardDescriptionCard.vue";
|
||||||
|
|
||||||
const isExpanded = ref(false);
|
|
||||||
// 假資料
|
// 假資料
|
||||||
const production_data = ref([]);
|
const production_data = ref([]);
|
||||||
|
|
||||||
@ -41,10 +40,6 @@ const descriptionCards = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const openModal = () => {
|
|
||||||
isExpanded.value = !isExpanded.value; // 切換狀態
|
|
||||||
};
|
|
||||||
|
|
||||||
provide("dashboard_product_complete", { getCompletion, progress_data });
|
provide("dashboard_product_complete", { getCompletion, progress_data });
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@ -57,9 +52,6 @@ onMounted(() => {
|
|||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<h3 class="text-info font-bold text-xl text-center">原醋即時庫存量</h3>
|
<h3 class="text-info font-bold text-xl text-center">原醋即時庫存量</h3>
|
||||||
<button type="button" class="btn-xs btn btn-info" @click.stop="openModal">
|
|
||||||
{{ isExpanded ? "關閉" : "開啟" }}
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="w-full grid grid-cols-3">
|
<div class="w-full grid grid-cols-3">
|
||||||
@ -89,7 +81,7 @@ onMounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- 說明卡片區域 -->
|
<!-- 說明卡片區域 -->
|
||||||
<div v-if="isExpanded" class="grid grid-cols-3 gap-2 -mt-6">
|
<div class="grid grid-cols-3 gap-2 -mt-6">
|
||||||
<DashboardDescriptionCard
|
<DashboardDescriptionCard
|
||||||
v-for="(card, index) in descriptionCards"
|
v-for="(card, index) in descriptionCards"
|
||||||
:key="index"
|
:key="index"
|
||||||
|
@ -1,50 +1,95 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted } from "vue";
|
import { ref, onMounted, computed } from "vue";
|
||||||
|
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||||
|
|
||||||
// 假資料
|
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||||
const production_data = ref([]);
|
|
||||||
|
|
||||||
const getCompletion = async () => {
|
// 配方假資料
|
||||||
// 註解掉 API 呼叫,使用假資料
|
const recipe_data = ref([]);
|
||||||
// const res = await getDashboardProductCompletion();
|
recipe_data.value = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
productName: "蘋果醋",
|
||||||
|
status: "調理中",
|
||||||
|
completionRate: 0.7, // 70%
|
||||||
|
details: [
|
||||||
|
{ pot: "第1鍋", location: "包裝室", status: "生產中" },
|
||||||
|
{ pot: "第2鍋", location: "充填室", status: "生產中" },
|
||||||
|
{ pot: "第3鍋", location: "調理桶甲", status: "生產中" },
|
||||||
|
{ pot: "第4鍋", location: "調理桶乙", status: "生產中" },
|
||||||
|
{ pot: "第5鍋", location: "調理桶丙", status: "生產中" },
|
||||||
|
{ pot: "第6鍋", location: "調理桶丁", status: "生產中" },
|
||||||
|
{
|
||||||
|
pot: "第7鍋",
|
||||||
|
location: "二重釜大-梅精醋\n二重釜小-加熱",
|
||||||
|
status: "生產中",
|
||||||
|
},
|
||||||
|
{ pot: "第8鍋", location: "", status: "備料" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
productName: "梅子醋",
|
||||||
|
status: "尚未生產",
|
||||||
|
completionRate: 0,
|
||||||
|
details: [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
productName: "桑葚醋",
|
||||||
|
status: "尚未生產",
|
||||||
|
completionRate: 0,
|
||||||
|
details: [],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
production_data.value = [
|
// 成品假資料
|
||||||
{
|
const finished_data = ref([]);
|
||||||
id: 1,
|
finished_data.value = [
|
||||||
productName: "蘋果醋",
|
{
|
||||||
status: "調理中",
|
id: 1,
|
||||||
completionRate: 0.7, // 70%
|
productName: "111018",
|
||||||
details: [
|
status: "已完成",
|
||||||
{ pot: "第1鍋", location: "包裝室", status: "生產中" },
|
completionRate: 1, // 100%
|
||||||
{ pot: "第2鍋", location: "充填室", status: "生產中" },
|
details: [
|
||||||
{ pot: "第3鍋", location: "調理桶甲", status: "生產中" },
|
{ pot: "第1鍋", location: "包裝室", status: "生產中" },
|
||||||
{ pot: "第4鍋", location: "調理桶乙", status: "生產中" },
|
{ pot: "第2鍋", location: "充填室", status: "生產中" },
|
||||||
{ pot: "第5鍋", location: "調理桶丙", status: "生產中" },
|
// { pot: "第3鍋", location: "調理桶甲", status: "生產中" },
|
||||||
{ pot: "第6鍋", location: "調理桶丁", status: "生產中" },
|
// { pot: "第4鍋", location: "調理桶乙", status: "生產中" },
|
||||||
{
|
// { pot: "第5鍋", location: "調理桶丙", status: "生產中" },
|
||||||
pot: "第7鍋",
|
// { pot: "第6鍋", location: "調理桶丁", status: "生產中" },
|
||||||
location: "二重釜大-梅精醋\n二重釜小-加熱",
|
// {
|
||||||
status: "生產中",
|
// pot: "第7鍋",
|
||||||
},
|
// location: "二重釜大-梅精醋\n二重釜小-加熱",
|
||||||
{ pot: "第8鍋", location: "", status: "備料" },
|
// status: "生產中",
|
||||||
],
|
// },
|
||||||
},
|
// { pot: "第8鍋", location: "", status: "備料" },
|
||||||
{
|
],
|
||||||
id: 2,
|
},
|
||||||
productName: "梅子醋",
|
{
|
||||||
status: "尚未生產",
|
id: 2,
|
||||||
completionRate: 0,
|
productName: "121062N",
|
||||||
details: [],
|
status: "尚未生產",
|
||||||
},
|
completionRate: 0,
|
||||||
{
|
details: [],
|
||||||
id: 3,
|
},
|
||||||
productName: "桑葚醋",
|
{
|
||||||
status: "尚未生產",
|
id: 3,
|
||||||
completionRate: 0,
|
productName: "113579",
|
||||||
details: [],
|
status: "尚未生產",
|
||||||
},
|
completionRate: 0,
|
||||||
];
|
details: [],
|
||||||
};
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const production_data = computed(() => {
|
||||||
|
if (selectedBtn.value?.key === 1) {
|
||||||
|
return recipe_data.value; // 返回配方資料
|
||||||
|
} else if (selectedBtn.value?.key === 2) {
|
||||||
|
return finished_data.value; // 返回成品資料
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
|
||||||
const selectedProduct = ref(null);
|
const selectedProduct = ref(null);
|
||||||
|
|
||||||
@ -57,7 +102,20 @@ const goBack = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
getCompletion();
|
setItems([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "配方",
|
||||||
|
key: 1,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "成品",
|
||||||
|
key: 2,
|
||||||
|
active: false,
|
||||||
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@ -73,7 +131,22 @@ onMounted(() => {
|
|||||||
返回
|
返回
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="text-center mb-3">
|
||||||
|
<template v-if="selectedProduct === null">
|
||||||
|
<ButtonConnectedGroup
|
||||||
|
:items="items"
|
||||||
|
:onclick="
|
||||||
|
(e, item) => {
|
||||||
|
changeActiveBtn(item);
|
||||||
|
}
|
||||||
|
"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span class="text-base ">{{ selectedProduct.productName }}</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
<div className="h-60 overflow-x-auto">
|
<div className="h-60 overflow-x-auto">
|
||||||
<table class="table table-sm table-pin-rows">
|
<table class="table table-sm table-pin-rows">
|
||||||
<thead v-if="!selectedProduct">
|
<thead v-if="!selectedProduct">
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
pot: Object,
|
pot: Object,
|
||||||
tabs2: Array
|
tabs: Array
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card bg-slate-200 text-accent-content rounded-md w-60">
|
<div class="card bg-slate-200 text-accent-content rounded-md w-[25rem]">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h2 class="card-title">{{ pot.name }}</h2>
|
<h2 class="card-title">{{ pot.name }}</h2>
|
||||||
<div
|
<div
|
||||||
@ -13,7 +13,7 @@ const props = defineProps({
|
|||||||
class="tabs tabs-boxed tabs-sm bg-opacity-50 shadow-inner shadow-slate-600"
|
class="tabs tabs-boxed tabs-sm bg-opacity-50 shadow-inner shadow-slate-600"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
v-for="tab in tabs2"
|
v-for="tab in tabs"
|
||||||
:key="tab.label"
|
:key="tab.label"
|
||||||
role="tab"
|
role="tab"
|
||||||
class="tab"
|
class="tab"
|
||||||
@ -69,6 +69,42 @@ const props = defineProps({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="pot.activeTab2.value === 'SIP'">
|
||||||
|
<div class="">
|
||||||
|
<table class="table table-sm whitespace-nowrap">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-0 bg-slate-300 text-slate-800">
|
||||||
|
<th>次數</th>
|
||||||
|
<th>開始時間</th>
|
||||||
|
<th>結束時間</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr class="border-0">
|
||||||
|
<th>
|
||||||
|
第一次
|
||||||
|
</th>
|
||||||
|
<td>2025-06-01T08:00:00</td>
|
||||||
|
<td>2025-06-01T08:30:00</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-0">
|
||||||
|
<th>
|
||||||
|
第二次
|
||||||
|
</th>
|
||||||
|
<td>2025-06-01T09:00:00</td>
|
||||||
|
<td>2025-06-01T09:30:00</td>
|
||||||
|
</tr>
|
||||||
|
<tr class="border-0">
|
||||||
|
<th>
|
||||||
|
第二次
|
||||||
|
</th>
|
||||||
|
<td>2025-06-01T10:00:00</td>
|
||||||
|
<td>2025-06-01T10:30:00</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,7 +6,7 @@ const props = defineProps({
|
|||||||
<template>
|
<template>
|
||||||
<div class="card bg-slate-200 text-accent-content rounded-md w-60">
|
<div class="card bg-slate-200 text-accent-content rounded-md w-60">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h2 class="card-title">{{ heat.name }}</h2>
|
<h2 class="card-title">{{ heat.equipment }}</h2>
|
||||||
<div class="p-0">
|
<div class="p-0">
|
||||||
<ul class="leading-7 tracking-wider text-slate-700 px-2">
|
<ul class="leading-7 tracking-wider text-slate-700 px-2">
|
||||||
<li><b>溫度:</b> {{ heat.temperature }}</li>
|
<li><b>溫度:</b> {{ heat.temperature }}</li>
|
||||||
|
@ -13,9 +13,6 @@ defineProps({ meter: Object })
|
|||||||
<li><b>用電量:</b> {{ meter.energyConsumption }}</li>
|
<li><b>用電量:</b> {{ meter.energyConsumption }}</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions justify-end">
|
|
||||||
<button class="btn btn-xs btn-success">詳細資料</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -5,7 +5,7 @@ const props = defineProps({
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<div class="card bg-slate-200 text-accent-content rounded-md w-[22rem]">
|
<div class="card bg-slate-200 text-accent-content rounded-md w-[25rem]">
|
||||||
<div class="card-body p-3">
|
<div class="card-body p-3">
|
||||||
<h2 class="card-title">{{ vessel.name }}</h2>
|
<h2 class="card-title">{{ vessel.name }}</h2>
|
||||||
<div
|
<div
|
||||||
@ -84,6 +84,30 @@ const props = defineProps({
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="vessel.activeTab.value === 'SIP'">
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="table table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr class="border-0 bg-slate-300 text-slate-800">
|
||||||
|
<th>次數</th>
|
||||||
|
<th>開始時間</th>
|
||||||
|
<th>結束時間</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="(value, index) in vessel.SIP"
|
||||||
|
:key="index"
|
||||||
|
class="border-0"
|
||||||
|
>
|
||||||
|
<th >{{ value.count }}</th>
|
||||||
|
<td>{{ value.startTime }}</td>
|
||||||
|
<td>{{ value.endTime }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
|
import ButtonGroup from "@/components/customUI/ButtonGroup.vue";
|
||||||
import WeightLossMachineTable from "./components/WeightLossMachineSettingTable.vue";
|
import CheckWeigherTable from "./components/CheckWeigherSettingTable.vue";
|
||||||
|
import InventorySetting from "./components/InventorySettingTable.vue";
|
||||||
import { computed, onMounted, ref, provide, onBeforeMount } from "vue";
|
import { computed, onMounted, ref, provide, onBeforeMount } from "vue";
|
||||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||||
|
|
||||||
@ -14,16 +15,16 @@ onBeforeMount(() => {
|
|||||||
setItems([
|
setItems([
|
||||||
{
|
{
|
||||||
title: "檢重機設定",
|
title: "檢重機設定",
|
||||||
key: "WeightLossMachineTable",
|
key: "CheckWeigherTable",
|
||||||
active: true,
|
active: true,
|
||||||
component: WeightLossMachineTable,
|
component: CheckWeigherTable,
|
||||||
},
|
},
|
||||||
// {
|
{
|
||||||
// title: "B 設備管理",
|
title: "原醋庫存設定",
|
||||||
// key: "ProducSettingTable2",
|
key: "InventorySetting",
|
||||||
// active: false,
|
active: false,
|
||||||
// component: ProducSettingTable,
|
component: InventorySetting,
|
||||||
// }
|
}
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -8,74 +8,78 @@ const { openToast } = inject("app_toast");
|
|||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
formState: Object,
|
formState: Object,
|
||||||
getData: Function,
|
currentEditIndex: Number,
|
||||||
});
|
});
|
||||||
|
const emit = defineEmits(["pushData"]);
|
||||||
|
|
||||||
const buildScheme = yup.object({
|
const itemScheme = yup.object({
|
||||||
group: yup.string().required("必填"),
|
groupNum: yup.string().required("必填"),
|
||||||
product_code: yup.string().required("必填"),
|
itemName: yup.string().required("必填"),
|
||||||
product_name: yup.string().required("必填"),
|
name: yup.string().required("必填"),
|
||||||
upper_limit: yup.string().required("必填"),
|
upperLimit: yup.string().required("必填"),
|
||||||
lower_limit: yup.string().required("必填"),
|
lowerLimit: yup.string().required("必填"),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
|
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
|
||||||
useFormErrorMessage(buildScheme);
|
useFormErrorMessage(itemScheme);
|
||||||
|
|
||||||
const onCancel = () => {
|
const onCancel = () => {
|
||||||
handleErrorReset();
|
handleErrorReset();
|
||||||
weight_loss_machine_modal.close();
|
weight_check_machine_modal.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOk = async () => {
|
const onOk = async () => {
|
||||||
const value = await handleSubmit(buildScheme, props.formState);
|
const value = await handleSubmit(itemScheme, props.formState);
|
||||||
// const res = await postBuildings(value);
|
emit("pushData", {...value});
|
||||||
// if (res.isSuccess) {
|
onCancel();
|
||||||
// props.getData();
|
|
||||||
onCancel();
|
|
||||||
// } else {
|
|
||||||
// openToast("error", res.msg, "#weight_loss_machine_modal");
|
|
||||||
// }
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Modal
|
<Modal
|
||||||
id="weight_loss_machine_modal"
|
id="weight_check_machine_modal"
|
||||||
:title="props.formState?.product_code ? '修改檢重機設備' : '新增檢重機設備'"
|
:title="props.formState?.itemName ? '修改檢重機設備' : '新增檢重機設備'"
|
||||||
:onCancel="onCancel"
|
:onCancel="onCancel"
|
||||||
:width="710"
|
:width="710"
|
||||||
>
|
>
|
||||||
<template #modalContent>
|
<template #modalContent>
|
||||||
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||||
<Input :value="formState" class="my-2" name="group">
|
<Input :value="formState" class="my-2" name="groupNum" :disabled="props.currentEditIndex >= 0">
|
||||||
<template #topLeft>組別</template>
|
<template #topLeft>組別</template>
|
||||||
<template #bottomLeft>
|
<template #bottomLeft>
|
||||||
<span class="text-error text-base">{{ formErrorMsg.group }}</span>
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.groupNum
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<Input :value="formState" class="my-2" name="product_code">
|
<Input :value="formState" class="my-2" name="itemName">
|
||||||
<template #topLeft>成品代號</template>
|
<template #topLeft>成品代號</template>
|
||||||
<template #bottomLeft>
|
<template #bottomLeft>
|
||||||
<span class="text-error text-base">{{ formErrorMsg.product_code }}</span>
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.itemName
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<Input :value="formState" class="my-2" name="product_name">
|
<Input :value="formState" class="my-2" name="name">
|
||||||
<template #topLeft>品名</template>
|
<template #topLeft>品名</template>
|
||||||
<template #bottomLeft>
|
<template #bottomLeft>
|
||||||
<span class="text-error text-base">{{ formErrorMsg.product_name }}</span>
|
<span class="text-error text-base">{{ formErrorMsg.name }}</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<Input :value="formState" class="my-2" name="upper_limit">
|
<Input :value="formState" class="my-2" name="upperLimit">
|
||||||
<template #topLeft>上限</template>
|
<template #topLeft>上限</template>
|
||||||
<template #bottomLeft>
|
<template #bottomLeft>
|
||||||
<span class="text-error text-base">{{ formErrorMsg.upper_limit }}</span>
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.upperLimit
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
<Input :value="formState" class="my-2" name="lower_limit">
|
<Input :value="formState" class="my-2" name="lowerLimit">
|
||||||
<template #topLeft>下限</template>
|
<template #topLeft>下限</template>
|
||||||
<template #bottomLeft>
|
<template #bottomLeft>
|
||||||
<span class="text-error text-base">{{ formErrorMsg.lower_limit }}</span>
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.lowerLimit
|
||||||
|
}}</span>
|
||||||
</template>
|
</template>
|
||||||
</Input>
|
</Input>
|
||||||
</form>
|
</form>
|
||||||
@ -86,7 +90,7 @@ const onOk = async () => {
|
|||||||
class="btn btn-outline-success mr-2"
|
class="btn btn-outline-success mr-2"
|
||||||
@click.prevent="onCancel"
|
@click.prevent="onCancel"
|
||||||
>
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
192
src/views/productSetting/components/CheckWeigherSettingTable.vue
Normal file
192
src/views/productSetting/components/CheckWeigherSettingTable.vue
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
<script setup>
|
||||||
|
import Table from "@/components/customUI/Table.vue";
|
||||||
|
import CheckWeigherSettingAddModal from "./CheckWeigherSettingAddModal.vue";
|
||||||
|
import { postChangeGroupValue, getCheckWeigher } from "@/apis/productSetting";
|
||||||
|
import { ref, onMounted, inject } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const { openToast } = inject("app_toast");
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "組別",
|
||||||
|
key: "groupNum",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "成品代號",
|
||||||
|
key: "itemName",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "品名",
|
||||||
|
key: "name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "上限",
|
||||||
|
key: "upperLimit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "下限",
|
||||||
|
key: "lowerLimit",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "operation",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = ref([]);
|
||||||
|
const updateTime = ref("");
|
||||||
|
const currentEditIndex = ref(-1);
|
||||||
|
|
||||||
|
const getDataSource = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
const res = await getCheckWeigher();
|
||||||
|
dataSource.value = res.data;
|
||||||
|
updateTime.value = res?.data[0]?.callingTime
|
||||||
|
? dayjs(res.data[0].callingTime).format("YYYY-MM-DD HH:mm:ss")
|
||||||
|
: dayjs().format("YYYY-MM-DD HH:mm:ss");
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushDataSource = async (data) => {
|
||||||
|
loading.value = true;
|
||||||
|
// 檢查是新增還是修改
|
||||||
|
if (currentEditIndex.value >= 0) {
|
||||||
|
// 修改
|
||||||
|
dataSource.value[currentEditIndex.value] = data;
|
||||||
|
openToast("success", "修改成功,請記得儲存");
|
||||||
|
} else {
|
||||||
|
//篩選如果 groupNum 重複則不新增
|
||||||
|
const existingIndex = dataSource.value.findIndex(
|
||||||
|
(item) => item.groupNum === data.groupNum
|
||||||
|
);
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
openToast("error", "組別已存在,請修改組別後再新增");
|
||||||
|
} else {
|
||||||
|
// 新增
|
||||||
|
dataSource.value.push(data);
|
||||||
|
openToast("success", "新增成功,請記得儲存");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentEditIndex.value = -1; // 重置編輯索引
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const postChangeGroup = async () => {
|
||||||
|
const res = await postChangeGroupValue(dataSource.value);
|
||||||
|
if (res.isSuccess) {
|
||||||
|
openToast("success", "儲存成功");
|
||||||
|
} else {
|
||||||
|
openToast("error", res.msg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeAll = () => {
|
||||||
|
dataSource.value = [];
|
||||||
|
openToast("success", "已清空列表,請記得儲存");
|
||||||
|
};
|
||||||
|
|
||||||
|
// 修改 onMounted
|
||||||
|
onMounted(() => {
|
||||||
|
getDataSource();
|
||||||
|
});
|
||||||
|
|
||||||
|
const formState = ref({
|
||||||
|
groupNum: "",
|
||||||
|
itemName: "",
|
||||||
|
name: "",
|
||||||
|
upperLimit: "",
|
||||||
|
lowerLimit: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const openModal = (record, index = -1) => {
|
||||||
|
currentEditIndex.value = index;
|
||||||
|
if (record) {
|
||||||
|
formState.value = { ...record };
|
||||||
|
} else {
|
||||||
|
resetModalForm();
|
||||||
|
}
|
||||||
|
weight_check_machine_modal.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetModalForm = () => {
|
||||||
|
formState.value = {
|
||||||
|
groupNum: "",
|
||||||
|
itemName: "",
|
||||||
|
name: "",
|
||||||
|
upperLimit: "",
|
||||||
|
lowerLimit: "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
// 新增刪除功能時也要更新 sessionStorage
|
||||||
|
const removeAccount = async (groupNum) => {
|
||||||
|
// 找到要刪除的項目索引
|
||||||
|
const index = dataSource.value.findIndex(
|
||||||
|
(item) => item.groupNum === groupNum
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index > -1) {
|
||||||
|
dataSource.value.splice(index, 1);
|
||||||
|
|
||||||
|
openToast("success", "刪除成功,請記得儲存");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-start items-center mb-3">
|
||||||
|
<h3 class="text-xl mr-5">檢重機設備列表</h3>
|
||||||
|
<CheckWeigherSettingAddModal
|
||||||
|
@pushData="pushDataSource"
|
||||||
|
:formState="formState"
|
||||||
|
:currentEditIndex="currentEditIndex"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success mr-3"
|
||||||
|
@click.stop.prevent="openModal(null)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'plus']" />新增
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error text-white mr-3"
|
||||||
|
@click.stop.prevent="removeAll()"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'trash-alt']" />全部刪除
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-info mr-3"
|
||||||
|
@click.stop.prevent="postChangeGroup"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'save']" />儲存
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
|
||||||
|
<template #beforeTable>
|
||||||
|
<p class="text-info text-xl pb-5">更新時間 : {{ updateTime }}</p>
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="{ record, column, index }">
|
||||||
|
<template v-if="column.key === 'operation'">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success text-white mr-2"
|
||||||
|
@click.stop.prevent="() => openModal(record, index)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error text-white"
|
||||||
|
@click.stop.prevent="() => removeAccount(record.groupNum)"
|
||||||
|
>
|
||||||
|
刪除
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ record[column.key] }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
124
src/views/productSetting/components/InventorySettingAddModal.vue
Normal file
124
src/views/productSetting/components/InventorySettingAddModal.vue
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, defineProps, inject, watch } from "vue";
|
||||||
|
import * as yup from "yup";
|
||||||
|
import "yup-phone-lite";
|
||||||
|
import useFormErrorMessage from "@/hooks/useFormErrorMessage";
|
||||||
|
// import { postBuildings } from "@/apis/building";
|
||||||
|
const { openToast } = inject("app_toast");
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
formState: Object,
|
||||||
|
itemData: Array,
|
||||||
|
});
|
||||||
|
const emit = defineEmits(["pushData"]);
|
||||||
|
|
||||||
|
const dateItem = ref([
|
||||||
|
{
|
||||||
|
key: "start_time",
|
||||||
|
name: "start_time",
|
||||||
|
value: dayjs(),
|
||||||
|
dateFormat: "yyyy-MM-dd",
|
||||||
|
placeholder: "請輸入日期",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const itemScheme = yup.object({
|
||||||
|
start_time: yup.date().required("必填"),
|
||||||
|
itemName: yup.string().required("必填"),
|
||||||
|
safety_stock: yup.number().required("必填").min(0, "不能小於0"),
|
||||||
|
target_stock: yup.number().required("必填").min(0, "不能小於0"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
|
||||||
|
useFormErrorMessage(itemScheme);
|
||||||
|
|
||||||
|
const onCancel = () => {
|
||||||
|
handleErrorReset();
|
||||||
|
inventory_setting_modal.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOk = async () => {
|
||||||
|
const value = await handleSubmit(itemScheme, props.formState);
|
||||||
|
emit("pushData", { ...value });
|
||||||
|
onCancel();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Modal
|
||||||
|
id="inventory_setting_modal"
|
||||||
|
:title="
|
||||||
|
props.formState?.start_time ? '修改原醋庫存列表' : '新增原醋庫存列表'
|
||||||
|
"
|
||||||
|
:onCancel="onCancel"
|
||||||
|
:width="710"
|
||||||
|
>
|
||||||
|
<template #modalContent>
|
||||||
|
<form ref="form" class="mt-5 w-full flex flex-wrap justify-between">
|
||||||
|
<DateGroup
|
||||||
|
class="my-2"
|
||||||
|
:items="dateItem"
|
||||||
|
inputClass="w-full shadow-none"
|
||||||
|
:required="true"
|
||||||
|
>
|
||||||
|
<template #topLeft>日期</template>
|
||||||
|
<template #bottomLeft
|
||||||
|
><span class="text-error text-base">
|
||||||
|
{{ formErrorMsg.start_time }}
|
||||||
|
</span></template
|
||||||
|
>
|
||||||
|
</DateGroup>
|
||||||
|
<Select
|
||||||
|
:value="formState"
|
||||||
|
class="my-2"
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="itemName"
|
||||||
|
Attribute="full_name"
|
||||||
|
:options="props.itemData"
|
||||||
|
>
|
||||||
|
<template #topLeft>品名</template>
|
||||||
|
<template #bottomLeft>
|
||||||
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.itemName
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</Select>
|
||||||
|
<Input :value="formState" class="my-2" name="safety_stock">
|
||||||
|
<template #topLeft>安全庫存量</template>
|
||||||
|
<template #bottomLeft>
|
||||||
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.safety_stock
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
<Input :value="formState" class="my-2" name="target_stock">
|
||||||
|
<template #topLeft>目標庫存量</template>
|
||||||
|
<template #bottomLeft>
|
||||||
|
<span class="text-error text-base">{{
|
||||||
|
formErrorMsg.target_stock
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
</Input>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
<template #modalAction>
|
||||||
|
<button
|
||||||
|
type="reset"
|
||||||
|
class="btn btn-outline-success mr-2"
|
||||||
|
@click.prevent="onCancel"
|
||||||
|
>
|
||||||
|
取消
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-outline-success"
|
||||||
|
@click.stop.prevent="onOk"
|
||||||
|
>
|
||||||
|
確定
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</Modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
178
src/views/productSetting/components/InventorySettingTable.vue
Normal file
178
src/views/productSetting/components/InventorySettingTable.vue
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
<script setup>
|
||||||
|
import Table from "@/components/customUI/Table.vue";
|
||||||
|
import InventorySettingAddModal from "./InventorySettingAddModal.vue";
|
||||||
|
import { postChangeGroupValue, getCheckWeigher } from "@/apis/productSetting";
|
||||||
|
import { ref, onMounted, inject } from "vue";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
|
||||||
|
const { openToast } = inject("app_toast");
|
||||||
|
|
||||||
|
const columns = [
|
||||||
|
{
|
||||||
|
title: "日期",
|
||||||
|
key: "date",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "品名",
|
||||||
|
key: "full_name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "安全庫存量",
|
||||||
|
key: "safety",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "目標庫存量",
|
||||||
|
key: "target",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: "操作",
|
||||||
|
key: "operation",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const dataSource = ref([]);
|
||||||
|
const itemData = ref([
|
||||||
|
{ full_name: "發酵槽", key: 1 },
|
||||||
|
{ full_name: "醋池", key: 2 },
|
||||||
|
{ full_name: "澄清醋", key: 3 },
|
||||||
|
]);
|
||||||
|
|
||||||
|
const dateRange = ref([
|
||||||
|
{
|
||||||
|
key: "start_at",
|
||||||
|
value: dayjs().subtract(30, "day").valueOf(),
|
||||||
|
dateFormat: "yyyy-MM-dd",
|
||||||
|
placeholder: "起始日期",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "end_at",
|
||||||
|
value: dayjs().valueOf(),
|
||||||
|
dateFormat: "yyyy-MM-dd",
|
||||||
|
placeholder: "結束日期",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const searchState = ref({
|
||||||
|
itemName: "all",
|
||||||
|
start_time: dayjs().subtract(30, "day").format("YYYY-MM-DD"),
|
||||||
|
end_time: dayjs().format("YYYY-MM-DD"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const formState = ref({
|
||||||
|
start_time: "",
|
||||||
|
itemName: 1,
|
||||||
|
safety_stock: 0,
|
||||||
|
target_stock: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const getDataSource = async () => {
|
||||||
|
loading.value = true;
|
||||||
|
// const res = await getCheckWeigher();
|
||||||
|
// dataSource.value = res.data;
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const pushDataSource = async (data) => {
|
||||||
|
loading.value = true;
|
||||||
|
// 檢查是新增還是修改
|
||||||
|
|
||||||
|
loading.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSearch = () => {
|
||||||
|
// 搜尋邏輯
|
||||||
|
searchState.value.start_time = dayjs(dateRange.value[0].value).format(
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
);
|
||||||
|
searchState.value.end_time = dayjs(dateRange.value[1].value).format(
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log("搜尋條件:", searchState.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const openModal = (record) => {
|
||||||
|
if (record) {
|
||||||
|
formState.value = { ...record };
|
||||||
|
} else {
|
||||||
|
resetModalForm();
|
||||||
|
}
|
||||||
|
inventory_setting_modal.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetModalForm = () => {
|
||||||
|
formState.value = {
|
||||||
|
start_time: "",
|
||||||
|
itemName: 1,
|
||||||
|
safety_stock: 0,
|
||||||
|
target_stock: 0,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 刪除功能
|
||||||
|
const removeAccount = async () => {};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getDataSource();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="flex justify-start items-center mb-3">
|
||||||
|
<h3 class="text-xl mr-5">原醋庫存列表</h3>
|
||||||
|
<InventorySettingAddModal
|
||||||
|
@pushData="pushDataSource"
|
||||||
|
:formState="formState"
|
||||||
|
:itemData="itemData"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success mr-3"
|
||||||
|
@click.stop.prevent="openModal(null)"
|
||||||
|
>
|
||||||
|
<font-awesome-icon :icon="['fas', 'plus']" />新增
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
|
||||||
|
<template #beforeTable>
|
||||||
|
<div class="flex items-center gap-5 mb-8">
|
||||||
|
<Select
|
||||||
|
:value="searchState"
|
||||||
|
class=""
|
||||||
|
selectClass="border-info focus-within:border-info"
|
||||||
|
name="itemName"
|
||||||
|
Attribute="full_name"
|
||||||
|
:options="[{ full_name: '全品項', key: 'all' }, ...itemData]"
|
||||||
|
>
|
||||||
|
</Select>
|
||||||
|
<DateGroup :items="dateRange" :withLine="true" />
|
||||||
|
<button class="btn btn-outline-success" @click.stop.prevent="onSearch">
|
||||||
|
搜尋
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #bodyCell="{ record, column, index }">
|
||||||
|
<template v-if="column.key === 'operation'">
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-success text-white mr-2"
|
||||||
|
@click.stop.prevent="() => openModal(record)"
|
||||||
|
>
|
||||||
|
修改
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn btn-sm btn-error text-white"
|
||||||
|
@click.stop.prevent="() => removeAccount(record.id)"
|
||||||
|
>
|
||||||
|
刪除
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ record[column.key] }}
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</Table>
|
||||||
|
<!-- <LineChart /> -->
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="css" scoped></style>
|
@ -1,239 +0,0 @@
|
|||||||
<script setup>
|
|
||||||
import Table from "@/components/customUI/Table.vue";
|
|
||||||
import WeightLossMachineSettingAddModal from "./WeightLossMachineSettingAddModal.vue";
|
|
||||||
import { ref, onMounted } from "vue";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const columns = [
|
|
||||||
{
|
|
||||||
title: "組別",
|
|
||||||
key: "group",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "成品代號",
|
|
||||||
key: "product_code",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "品名",
|
|
||||||
key: "product_name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "上限",
|
|
||||||
key: "upper_limit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "下限",
|
|
||||||
key: "lower_limit",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "操作",
|
|
||||||
key: "operation",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const dataSource = ref([]);
|
|
||||||
const updateTime = ref("");
|
|
||||||
|
|
||||||
const fake_data = [
|
|
||||||
{
|
|
||||||
group: 1,
|
|
||||||
product_code: "113807",
|
|
||||||
product_name: "58藜麥高粱醋590mL",
|
|
||||||
upper_limit: 13.52,
|
|
||||||
lower_limit: 12.61,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 2,
|
|
||||||
product_code: "111018",
|
|
||||||
product_name: "陳年醋600mL(1*12)",
|
|
||||||
upper_limit: 11.752,
|
|
||||||
lower_limit: 10.961,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 3,
|
|
||||||
product_code: "121062N",
|
|
||||||
product_name: "梅子醋600mL",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 4,
|
|
||||||
product_code: "111629B",
|
|
||||||
product_name: "蘋果醋600mL",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 5,
|
|
||||||
product_code: "112404N",
|
|
||||||
product_name: "一斤果醋-青蘋果醋600mL",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 6,
|
|
||||||
product_code: "112503N",
|
|
||||||
product_name: "一斤果醋-蜂蜜蘋果醋600mL",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 7,
|
|
||||||
product_code: "113579",
|
|
||||||
product_name: "蜂蜜蘋果醋600mL",
|
|
||||||
upper_limit: 13.312,
|
|
||||||
lower_limit: 12.416,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 8,
|
|
||||||
product_code: "113784",
|
|
||||||
product_name: "紅石榴醋600毫升",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 9,
|
|
||||||
product_code: "211046-5",
|
|
||||||
product_name: "一斤壽司醋280mL(1*12)",
|
|
||||||
upper_limit: 6.76,
|
|
||||||
lower_limit: 6.305,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 10,
|
|
||||||
product_code: "211046N",
|
|
||||||
product_name: "百家珍壽司醋270ml",
|
|
||||||
upper_limit: 6.24,
|
|
||||||
lower_limit: 5.82,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 11,
|
|
||||||
product_code: "112268N",
|
|
||||||
product_name: "一斤果醋-無糖蘋果醋600mL",
|
|
||||||
upper_limit: 11.752,
|
|
||||||
lower_limit: 10.961,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 12,
|
|
||||||
product_code: "112282N",
|
|
||||||
product_name: "一斤果醋-低糖蘋果醋600mL",
|
|
||||||
upper_limit: 11.96,
|
|
||||||
lower_limit: 11.155,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 13,
|
|
||||||
product_code: "112442N",
|
|
||||||
product_name: "一斤果醋-水蜜桃醋600mL",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 14,
|
|
||||||
product_code: "113722",
|
|
||||||
product_name: "清心福全-蘋果醋600ml*6瓶",
|
|
||||||
upper_limit: 6.552,
|
|
||||||
lower_limit: 6.111,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
group: 15,
|
|
||||||
product_code: "111933N",
|
|
||||||
product_name: "蘆薈醋600ml",
|
|
||||||
upper_limit: 13.104,
|
|
||||||
lower_limit: 12.222,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const loading = ref(false);
|
|
||||||
|
|
||||||
const getDataSource = async () => {
|
|
||||||
loading.value = true;
|
|
||||||
dataSource.value = fake_data;
|
|
||||||
updateTime.value = dayjs().format("YYYY-MM-DD HH:mm:ss");
|
|
||||||
loading.value = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
getDataSource();
|
|
||||||
});
|
|
||||||
|
|
||||||
const formState = ref({
|
|
||||||
group: "",
|
|
||||||
product_code: "",
|
|
||||||
product_name: "",
|
|
||||||
upper_limit: "",
|
|
||||||
lower_limit: "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const openModal = (record) => {
|
|
||||||
if (record) {
|
|
||||||
formState.value = record;
|
|
||||||
} else {
|
|
||||||
resetModalForm();
|
|
||||||
}
|
|
||||||
weight_loss_machine_modal.showModal();
|
|
||||||
};
|
|
||||||
|
|
||||||
const resetModalForm = () => {
|
|
||||||
formState.value = {
|
|
||||||
group: "",
|
|
||||||
product_code: "",
|
|
||||||
product_name: "",
|
|
||||||
upper_limit: "",
|
|
||||||
lower_limit: "",
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeAccount = async (id) => {
|
|
||||||
// openToast("warning", "是否確認刪除該項目?", "body", async () => {
|
|
||||||
// await cancelToastOpen();
|
|
||||||
// const res = await delAccount(id);
|
|
||||||
// if (res.isSuccess) {
|
|
||||||
// getDataSource();
|
|
||||||
// openToast("success", "刪除成功");
|
|
||||||
// } else {
|
|
||||||
// openToast("error", res.msg);
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex justify-start items-center mb-3">
|
|
||||||
<h3 class="text-xl mr-5">檢重機設備列表</h3>
|
|
||||||
<WeightLossMachineSettingAddModal :getData="getDataSource" :formState="formState" />
|
|
||||||
<button class="btn btn-sm btn-success mr-3" @click.stop.prevent="openModal">
|
|
||||||
<font-awesome-icon :icon="['fas', 'plus']" />新增
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-info mr-3"
|
|
||||||
@click.stop.prevent="getDataSource()"
|
|
||||||
>
|
|
||||||
<font-awesome-icon :icon="['fas', 'sync-alt']" />更新
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Table :columns="columns" :dataSource="dataSource" :loading="loading">
|
|
||||||
<template #beforeTable>
|
|
||||||
<p class="text-info text-xl pb-5">更新時間 : {{ updateTime }}</p>
|
|
||||||
</template>
|
|
||||||
<template #bodyCell="{ record, column, index }">
|
|
||||||
<template v-if="column.key === 'operation'">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-success text-white mr-2"
|
|
||||||
@click.stop.prevent="() => openModal(record)"
|
|
||||||
>
|
|
||||||
修改
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-error text-white"
|
|
||||||
@click.stop.prevent="() => removeAccount(record)"
|
|
||||||
>
|
|
||||||
刪除
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
{{ record[column.key] }}
|
|
||||||
</template>
|
|
||||||
</template>
|
|
||||||
</Table>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="css" scoped></style>
|
|
Loading…
Reference in New Issue
Block a user