水流量動畫 | 首頁小卡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_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"
|
@ -1,3 +1,3 @@
|
||||
VITE_API_BASEURL = "http://220.132.206.5:8008"
|
||||
VITE_FILE_API_BASEURL = "http://220.132.206.5:8085/file"
|
||||
VITE_FORGE_BASEURL = "http://localhost:5173"
|
||||
VITE_API_BASEURL = "https://pccv-api.production.mjmtech.com.tw"
|
||||
VITE_FILE_API_BASEURL = ".."
|
||||
VITE_FORGE_BASEURL = "http://202.39.218.221:8080/file/netzero"
|
@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"stage": "vite build --mode staging"
|
||||
},
|
||||
"dependencies": {
|
||||
"@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_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_TEMP_API = `/SituationRoom/GetTempratureData`;
|
||||
export const GET_DASHBOARD_ROOM_TEMP_API = `/SituationRoom/GetFormulaRoomStatusData`;
|
||||
|
@ -1,6 +1,7 @@
|
||||
import {
|
||||
GET_DASHBOARD_INIT_API,
|
||||
GET_DASHBOARD_DEVICE_API,
|
||||
GET_DASHBOARD_REALTIME_DATA_API,
|
||||
GET_DASHBOARD_PRODUCT_COMPLETE_API,
|
||||
GET_DASHBOARD_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 () => {
|
||||
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`;
|
||||
|
@ -1,21 +1,13 @@
|
||||
import instance from "@/util/request";
|
||||
import apihandler from "@/util/apihandler";
|
||||
import {
|
||||
POST_SETTING_POINT_API,
|
||||
GET_SETTING_TYPE_API,
|
||||
POST_CHANGE_GROUP_VALUE_API,
|
||||
GET_CHECKWEIGHER_API,
|
||||
POST_SETTING_TYPE_API,
|
||||
} from "./api";
|
||||
|
||||
export const postProductSettingPoint = async (type, devices) => {
|
||||
const res = await instance.post(POST_SETTING_POINT_API, {
|
||||
devices: devices.map(({ device_number }) => device_number),
|
||||
values: [
|
||||
{
|
||||
point: "Type",
|
||||
value: type.value,
|
||||
},
|
||||
],
|
||||
});
|
||||
export const postChangeGroupValue = async (data) => {
|
||||
const res = await instance.post(POST_CHANGE_GROUP_VALUE_API,data);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
@ -24,8 +16,8 @@ export const postProductSettingPoint = async (type, devices) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const getProductSettingType = async () => {
|
||||
const res = await instance.post(GET_SETTING_TYPE_API);
|
||||
export const getCheckWeigher = async () => {
|
||||
const res = await instance.get(GET_CHECKWEIGHER_API);
|
||||
|
||||
return apihandler(res.code, res.data, {
|
||||
msg: res.msg,
|
||||
|
@ -20,13 +20,7 @@ const { forgeLock } = inject("app_toggle");
|
||||
const props = defineProps({
|
||||
fullScreen: Boolean,
|
||||
initialData: Object,
|
||||
cubeStyle: {
|
||||
type: Object,
|
||||
default: {
|
||||
right: 25,
|
||||
top: 2,
|
||||
},
|
||||
},
|
||||
realTime: String,
|
||||
});
|
||||
|
||||
const heat_bar_isShow = ref(false);
|
||||
@ -45,6 +39,7 @@ const {
|
||||
loadModel,
|
||||
updateInitialData,
|
||||
subComponents,
|
||||
clearSprites
|
||||
} = useSystemStatusByBaja(updateHeatBarIsShow);
|
||||
|
||||
watch(
|
||||
@ -99,7 +94,8 @@ const initViewer = (container) => {
|
||||
const initForge = () => {
|
||||
initViewer(forgeDom.value).then((viewer) => {
|
||||
const localFilePath =
|
||||
import.meta.env.MODE === "production"
|
||||
import.meta.env.MODE === "production" ||
|
||||
import.meta.env.MODE === "staging"
|
||||
? `${FILE_BASEURL}/upload/forge/0.svf`
|
||||
: "/forge/0.svf";
|
||||
loadModel(viewer, localFilePath).then(() => {
|
||||
@ -112,13 +108,13 @@ const initForge = () => {
|
||||
);
|
||||
updateForgeViewer(viewer);
|
||||
|
||||
const tree = viewer.model.getData().instanceTree;
|
||||
hideAllObjects(tree, visibleDbid.value);
|
||||
visibleDbid.value.forEach((dbid) => {
|
||||
if (dbid === 58) {
|
||||
viewer.setThemingColor(dbid, new THREE.Vector4(1, 0, 0, 1));
|
||||
}
|
||||
});
|
||||
// const tree = viewer.model.getData().instanceTree;
|
||||
// hideAllObjects(tree, visibleDbid.value);
|
||||
// visibleDbid.value.forEach((dbid) => {
|
||||
// if (dbid === 58) {
|
||||
// viewer.setThemingColor(dbid, new THREE.Vector4(1, 0, 0, 1));
|
||||
// }
|
||||
// });
|
||||
// 印出被點選物件的 dbid
|
||||
// viewer.addEventListener(
|
||||
// Autodesk.Viewing.SELECTION_CHANGED_EVENT,
|
||||
@ -179,6 +175,7 @@ onUnmounted(() => {
|
||||
subComponents.value?.unsubscribeAll();
|
||||
subComponents.value?.detach();
|
||||
updateForgeViewer(null);
|
||||
clearSprites();
|
||||
NOP_VIEWER.tearDown();
|
||||
});
|
||||
</script>
|
||||
@ -199,11 +196,12 @@ onUnmounted(() => {
|
||||
ref="forgeDom"
|
||||
:class="
|
||||
twMerge(
|
||||
'relative w-full h-full',
|
||||
'relative w-full h-full overflow-x-hidden',
|
||||
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 class="w-40 flex justify-between text-[10px] mb-1">
|
||||
<span class="text-gradient-1">-20°C</span>
|
||||
@ -225,7 +223,7 @@ onUnmounted(() => {
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<label
|
||||
<!-- <label
|
||||
v-for="(value, key) in subscribeDataWithErrorMsg"
|
||||
:key="key"
|
||||
:data-dbid="value.forge_dbid"
|
||||
@ -255,7 +253,7 @@ onUnmounted(() => {
|
||||
<span class="mr-2">{{ value.full_name }}</span>
|
||||
<span v-if="value.alarmMsg">{{ value.alarmMsg }}</span>
|
||||
<span v-else>{{ value.show_value }}</span>
|
||||
</label>
|
||||
</label> -->
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@ -271,16 +269,16 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.viewcubeWrapper {
|
||||
right: v-bind("`${props.cubeStyle.right}%`") !important;
|
||||
top: v-bind("`${props.cubeStyle.top}%`") !important;
|
||||
right: 25% !important;
|
||||
top: 0% !important;
|
||||
}
|
||||
|
||||
.homeViewWrapper {
|
||||
transform: scale(1.5) !important;
|
||||
transform: scale(1.5) translateX(350%) translateY(0%) !important;
|
||||
}
|
||||
|
||||
.heatbar {
|
||||
right: v-bind("`${props.cubeStyle.right + 2}%`") !important;
|
||||
top: 0% !important;
|
||||
left: 27% !important;
|
||||
top: 9% !important;
|
||||
}
|
||||
</style>
|
||||
|
@ -43,9 +43,9 @@ onMounted(() => {
|
||||
<NavbarItem />
|
||||
</ul>
|
||||
</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" />百家珍
|
||||
</router-link>
|
||||
</div>
|
||||
<NavbarBuilding class="hidden" />
|
||||
</div>
|
||||
|
||||
|
@ -33,27 +33,6 @@ const getSubMonitorPage = async (building) => {
|
||||
const res = await getAllSysSidebar();
|
||||
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(
|
||||
() => buildingStore.selectedBuilding,
|
||||
@ -74,19 +53,8 @@ onMounted(() => {
|
||||
v-for="page in authPages"
|
||||
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
|
||||
v-if="$route.path !== page.navigate"
|
||||
:to="page.navigate"
|
||||
type="link"
|
||||
class="flex flex-col justify-center items-center btn-group text-white"
|
||||
@ -98,32 +66,22 @@ onMounted(() => {
|
||||
/>
|
||||
{{ page.subName }}
|
||||
</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>
|
||||
</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>
|
||||
<style lang="css" scoped>
|
||||
.router-link-active.router-link-exact-active {
|
||||
.router-link-active {
|
||||
color: #7cedc1;
|
||||
}
|
||||
</style>
|
||||
|
@ -47,7 +47,8 @@ import {
|
||||
faTemperatureHigh,
|
||||
faTint,
|
||||
faCircle,
|
||||
faSyncAlt
|
||||
faSyncAlt,
|
||||
faSave
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import { faClock } from "@fortawesome/free-regular-svg-icons";
|
||||
@ -99,7 +100,8 @@ library.add(
|
||||
faClock,
|
||||
faTint,
|
||||
faCircle,
|
||||
faSyncAlt
|
||||
faSyncAlt,
|
||||
faSave
|
||||
);
|
||||
|
||||
export default library;
|
||||
|
@ -14,6 +14,11 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
};
|
||||
const { searchParams } = useSearchParams();
|
||||
|
||||
// DataVisualization 擴充套件的全域變數
|
||||
let dataVizExtn = null;
|
||||
let spriteAnimations = new Map(); // 用 Map 來追蹤多個動畫 {dbId: {interval, arrow, viewable}}
|
||||
let cameraEventAdded = false; // 追蹤是否已經添加了相機事件監聽器
|
||||
|
||||
const initialData = ref(null);
|
||||
|
||||
const updateInitialData = (data = false) => {
|
||||
@ -99,7 +104,13 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
});
|
||||
|
||||
// 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) => {
|
||||
@ -168,7 +179,12 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
};
|
||||
|
||||
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) => {
|
||||
@ -188,15 +204,21 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
window.requirejs(["baja!"], (baja) => {
|
||||
console.log("進入 bajaSubscriber 準備執行BQL訂閱");
|
||||
|
||||
const ordKey = 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()
|
||||
.then((folder) => {
|
||||
console.log("成功獲取 folder:", folder);
|
||||
const batch = new baja.comm.Batch();
|
||||
|
||||
const sub = new baja.Subscriber();
|
||||
sub.attach({
|
||||
changed: function (prop, cx) {
|
||||
console.log("數據變更觸發:", prop.$getDisplayName());
|
||||
if (prop.$getDisplayName() !== "Out") return;
|
||||
if (
|
||||
Object.hasOwn(
|
||||
@ -227,19 +249,27 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
console.log("開始遍歷控制點");
|
||||
folder
|
||||
.getSlots()
|
||||
.is("control:ControlPoint")
|
||||
.eachValue((point) => {
|
||||
console.log("找到控制點:", point.getDisplayName());
|
||||
console.log("配置的點位:", Object.keys(value.points));
|
||||
|
||||
if (
|
||||
Object.keys(value.points).includes(point.getDisplayName())
|
||||
) {
|
||||
console.log(
|
||||
"匹配到點位,開始訂閱:",
|
||||
point.getDisplayName()
|
||||
);
|
||||
baja.Ord.make(
|
||||
`local:|foxs:|station:|slot:/${ordPath}/${point.getDisplayName()}`
|
||||
`local:|foxs:|station:|slot:/Drivers/NiagaraNetwork/PCCV/points/${ordPath}/${ordKey}/${point.getDisplayName()}`
|
||||
)
|
||||
.get()
|
||||
.then((component) => {
|
||||
console.log("獲取到 component:", component);
|
||||
if (
|
||||
point.getType().getTypeSpec() ===
|
||||
"control:BooleanWritable"
|
||||
@ -336,14 +366,14 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
};
|
||||
|
||||
const hideAllObjects = (instanceTree, filDbids = []) => {
|
||||
const tree = instanceTree || forgeViewer.value.model?.getInstanceTree();
|
||||
const allDbIdsStr = Object.keys(tree.nodeAccess.dbIdToIndex);
|
||||
for (var i = 0; i < allDbIdsStr.length; i++) {
|
||||
forgeViewer.value.hide(parseInt(allDbIdsStr[i]));
|
||||
}
|
||||
if (!forgeViewer.value || !instanceTree) return;
|
||||
const allDbIds = Object.keys(instanceTree.nodeAccess.dbIdToIndex).map(
|
||||
Number
|
||||
);
|
||||
|
||||
for (var i = 0; i < filDbids.length; i++) {
|
||||
forgeViewer.value.show(parseInt(filDbids[i]));
|
||||
forgeViewer.value.hide(allDbIds);
|
||||
if (filDbids.length > 0) {
|
||||
forgeViewer.value.show(filDbids);
|
||||
}
|
||||
fitToView();
|
||||
forgeViewer.value.impl.invalidate(true);
|
||||
@ -374,24 +404,97 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
|
||||
const reloadModal = () => {};
|
||||
|
||||
watch(
|
||||
visibleDbid,
|
||||
(newValue) => {
|
||||
if (!forgeViewer.value) return;
|
||||
hideAllObjects(forgeViewer.value.model.getData().instanceTree, newValue);
|
||||
let themingInterval = null;
|
||||
let blinkingInterval = null; // 計時器 ID 的引用
|
||||
|
||||
newValue.forEach((dbid) => {
|
||||
// 根據 dbid 設置主題色
|
||||
if (dbid === 58) {
|
||||
// 紅色 (RGB: 1,0,0)
|
||||
forgeViewer.value.setThemingColor(
|
||||
dbid,
|
||||
new THREE.Vector4(1, 0, 0, 1)
|
||||
// 增強版:停止閃爍並徹底清理
|
||||
const stopBlinking = () => {
|
||||
console.log("正在停止基於 Selection 的閃爍效果...");
|
||||
if (blinkingInterval) {
|
||||
clearInterval(blinkingInterval);
|
||||
blinkingInterval = null;
|
||||
}
|
||||
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) => {
|
||||
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 {
|
||||
subscribeData,
|
||||
visibleDbid,
|
||||
@ -420,5 +776,8 @@ export default function useSystemStatusByBaja(updateHeatBarIsShow) {
|
||||
urn,
|
||||
updateInitialData,
|
||||
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>
|
||||
import { onMounted, ref, watch, onUnmounted } from "vue";
|
||||
import Forge from "@/components/forge/Forge.vue";
|
||||
import DashboardProduct from "./components/DashboardProduct.vue";
|
||||
import DashboardTarget from "./components/DashboardTarget.vue";
|
||||
@ -8,39 +9,81 @@ import DashboardElectricity from "./components/DashboardElectricity.vue";
|
||||
import DashboardAlert from "./components/DashboardAlert.vue";
|
||||
import DashboardForgeOptionButton from "./components/DashboardForgeOptionButton.vue";
|
||||
import DashboardForgeOptionCard from "./components/DashboardForgeOptionCard.vue";
|
||||
import { getDashboardInit } from "@/apis/dashboard";
|
||||
import { onMounted, ref, provide, watch } from "vue";
|
||||
import { getDashboardInit, getDashboardOptionRealTimeData } from "@/apis/dashboard";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
const initialData = ref(null);
|
||||
// const forgeData = ref([]);
|
||||
const realTimeData = ref(null);
|
||||
const realTime = ref(null);
|
||||
const { searchParams } = useSearchParams();
|
||||
let intervalId = null;
|
||||
|
||||
const init = async () => {
|
||||
try {
|
||||
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(() => {
|
||||
init();
|
||||
});
|
||||
|
||||
const intervalOption = ref({});
|
||||
const currentIntervalType = ref("");
|
||||
|
||||
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,
|
||||
onUnmounted(() => {
|
||||
stopInterval();
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -55,9 +98,9 @@ provide("dashboard_items", {
|
||||
<DashboardAlert />
|
||||
</div>
|
||||
</div>
|
||||
<Forge :fullScreen="true" :initialData="initialData" />
|
||||
<Forge :fullScreen="true" :initialData="initialData" :realTime="realTime"/>
|
||||
<DashboardForgeOptionButton :initialData="initialData" />
|
||||
<DashboardForgeOptionCard />
|
||||
<DashboardForgeOptionCard :realTimeData="realTimeData"/>
|
||||
<div class="w-1/4 flex flex-col justify-start border-dashboard z-20">
|
||||
<div class=""><DashboardImmediateTemp /></div>
|
||||
<div class="mt-5">
|
||||
|
@ -1,9 +1,8 @@
|
||||
<template>
|
||||
<div class="card bg-neutral text-neutral-content shadow-sm shadow-gray-400">
|
||||
<div class="card-body text-xs p-3">
|
||||
<p>即時庫存量: {{ inventory }} 頓</p>
|
||||
<div class="card-body text-xs px-3 py-4">
|
||||
<p>安全庫存量: {{ inventory }} 頓</p>
|
||||
<p>目標庫存量: {{ targetInventory }} 頓</p>
|
||||
<p>前次增加: {{ lastIncrease }} 頓</p>
|
||||
<p> {{ updateTime }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -3,76 +3,14 @@ import { ref, watch } from "vue";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
// 假資料
|
||||
const initialData = ref({
|
||||
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 props = defineProps({
|
||||
initialData: Object,
|
||||
});
|
||||
|
||||
const { changeParams, searchParams } = useSearchParams();
|
||||
|
||||
watch(
|
||||
() => initialData.value,
|
||||
() => props.initialData,
|
||||
(newValue) => {
|
||||
if (newValue?.options[0]) {
|
||||
const { option, camera_position, target_position, top } = newValue.options[0];
|
||||
|
@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { ref, defineProps, inject, watch } from "vue";
|
||||
import { ref, defineProps, inject, watch, computed } from "vue";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import ProductionCard from "./dashboardForgeCards/ProductionCard.vue";
|
||||
@ -13,395 +13,95 @@ import MeterCard from "./dashboardForgeCards/MeterCard.vue";
|
||||
import ValveCard from "./dashboardForgeCards/ValveCard.vue";
|
||||
|
||||
const { searchParams, changeParams } = useSearchParams();
|
||||
const props = defineProps({
|
||||
realTimeData: Object,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
{ label: "生產資訊" },
|
||||
{ label: "投料進度" },
|
||||
{ label: "品檢" },
|
||||
{ label: "流量計" },
|
||||
{ label: "SIP" },
|
||||
];
|
||||
|
||||
const tabs2 = [{ label: "生產資訊" }, { label: "品檢" }, { label: "流量計" }];
|
||||
|
||||
// 生產資訊卡片資料
|
||||
const productionData = [
|
||||
{
|
||||
batch: "7",
|
||||
product: "蘋果醋",
|
||||
equipment: "二重釜 800L",
|
||||
status: "投料中",
|
||||
temperature: "70°C",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
];
|
||||
const productionData = computed(() => {
|
||||
return props.realTimeData?.productionData || [];
|
||||
});
|
||||
const cookingData = computed(() => {
|
||||
return props.realTimeData?.cookingData || [];
|
||||
});
|
||||
const heaterData = computed(() => {
|
||||
return props.realTimeData?.heaterData || [];
|
||||
});
|
||||
|
||||
// 二重釜資料
|
||||
const vesselsData = [
|
||||
{
|
||||
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 deviceActiveTabs = ref(new Map());
|
||||
|
||||
// 調理鍋資料
|
||||
const cookingPotData = [
|
||||
{
|
||||
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("生產資訊"),
|
||||
},
|
||||
{
|
||||
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",
|
||||
// 獲取或創建設備的activeTab
|
||||
const getDeviceActiveTab = (deviceName, defaultTab = "生產資訊") => {
|
||||
if (!deviceActiveTabs.value.has(deviceName)) {
|
||||
deviceActiveTabs.value.set(deviceName, ref(defaultTab));
|
||||
}
|
||||
return deviceActiveTabs.value.get(deviceName);
|
||||
};
|
||||
|
||||
// 二重釜資料
|
||||
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 = [
|
||||
{
|
||||
name: "電錶01 汙水區",
|
||||
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 meterData = computed(() => {
|
||||
const data = props.realTimeData?.refrigerationData || [];
|
||||
return data.sort((a, b) => a.name.localeCompare(b.name));
|
||||
});
|
||||
|
||||
// 威鈦閥異常訊號資料
|
||||
const 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" },
|
||||
],
|
||||
}
|
||||
];
|
||||
const valveData = computed(() => {
|
||||
return props.realTimeData?.valveData || {};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute left-1/2 -translate-x-1/2 z-50"
|
||||
<div
|
||||
class="absolute left-1/2 -translate-x-1/2 z-50"
|
||||
:style="{ top: searchParams?.top || '40%' }"
|
||||
>
|
||||
<template v-if="searchParams?.option == '1'">
|
||||
@ -412,7 +112,7 @@ const valveData = [
|
||||
</div>
|
||||
</template>
|
||||
<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
|
||||
v-for="(vessel, index) in vesselsData"
|
||||
:key="index"
|
||||
@ -422,19 +122,18 @@ const valveData = [
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="searchParams?.option == '3'">
|
||||
<div class="grid grid-flow-col grid-rows-2 gap-4">
|
||||
<HeaterPotCard
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- <HeaterPotCard
|
||||
v-for="(heat, index) in heaterPotData"
|
||||
:key="index"
|
||||
:heat="heat"
|
||||
/>
|
||||
/> -->
|
||||
<CookingPotCard
|
||||
v-for="(pot, index) in cookingPotData"
|
||||
:key="index"
|
||||
:pot="pot"
|
||||
:tabs2="tabs2"
|
||||
:tabs="tabs"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="searchParams?.option == '4'">
|
||||
|
@ -7,7 +7,6 @@ import { getDashboardTemp } from "@/apis/dashboard";
|
||||
import useSearchParams from "@/hooks/useSearchParam";
|
||||
|
||||
const { searchParams } = useSearchParams();
|
||||
const { openModal, intervalOption } = inject("dashboard_items");
|
||||
const intervalType = "frozen";
|
||||
|
||||
const defaultChartOption = ref({
|
||||
|
@ -4,7 +4,6 @@ import { ref, onMounted, provide, watch, inject } from "vue";
|
||||
import { getDashboardProductCompletion } from "@/apis/dashboard";
|
||||
import DashboardDescriptionCard from "./DashboardDescriptionCard.vue";
|
||||
|
||||
const isExpanded = ref(false);
|
||||
// 假資料
|
||||
const production_data = ref([]);
|
||||
|
||||
@ -41,10 +40,6 @@ const descriptionCards = [
|
||||
},
|
||||
];
|
||||
|
||||
const openModal = () => {
|
||||
isExpanded.value = !isExpanded.value; // 切換狀態
|
||||
};
|
||||
|
||||
provide("dashboard_product_complete", { getCompletion, progress_data });
|
||||
|
||||
onMounted(() => {
|
||||
@ -57,9 +52,6 @@ onMounted(() => {
|
||||
<div>
|
||||
<div class="flex items-center justify-between">
|
||||
<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 class="w-full grid grid-cols-3">
|
||||
@ -89,7 +81,7 @@ onMounted(() => {
|
||||
</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
|
||||
v-for="(card, index) in descriptionCards"
|
||||
:key="index"
|
||||
|
@ -1,14 +1,12 @@
|
||||
<script setup>
|
||||
import { ref, onMounted } from "vue";
|
||||
import { ref, onMounted, computed } from "vue";
|
||||
import useActiveBtn from "@/hooks/useActiveBtn";
|
||||
|
||||
// 假資料
|
||||
const production_data = ref([]);
|
||||
const { items, changeActiveBtn, setItems, selectedBtn } = useActiveBtn();
|
||||
|
||||
const getCompletion = async () => {
|
||||
// 註解掉 API 呼叫,使用假資料
|
||||
// const res = await getDashboardProductCompletion();
|
||||
|
||||
production_data.value = [
|
||||
// 配方假資料
|
||||
const recipe_data = ref([]);
|
||||
recipe_data.value = [
|
||||
{
|
||||
id: 1,
|
||||
productName: "蘋果醋",
|
||||
@ -44,7 +42,54 @@ const getCompletion = async () => {
|
||||
details: [],
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
// 成品假資料
|
||||
const finished_data = ref([]);
|
||||
finished_data.value = [
|
||||
{
|
||||
id: 1,
|
||||
productName: "111018",
|
||||
status: "已完成",
|
||||
completionRate: 1, // 100%
|
||||
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: "121062N",
|
||||
status: "尚未生產",
|
||||
completionRate: 0,
|
||||
details: [],
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
productName: "113579",
|
||||
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);
|
||||
|
||||
@ -57,7 +102,20 @@ const goBack = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
getCompletion();
|
||||
setItems([
|
||||
{
|
||||
id: 1,
|
||||
title: "配方",
|
||||
key: 1,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "成品",
|
||||
key: 2,
|
||||
active: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
</script>
|
||||
|
||||
@ -73,7 +131,22 @@ onMounted(() => {
|
||||
返回
|
||||
</button>
|
||||
</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">
|
||||
<table class="table table-sm table-pin-rows">
|
||||
<thead v-if="!selectedProduct">
|
||||
|
@ -1,11 +1,11 @@
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
pot: Object,
|
||||
tabs2: Array
|
||||
tabs: Array
|
||||
});
|
||||
</script>
|
||||
<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">
|
||||
<h2 class="card-title">{{ pot.name }}</h2>
|
||||
<div
|
||||
@ -13,7 +13,7 @@ const props = defineProps({
|
||||
class="tabs tabs-boxed tabs-sm bg-opacity-50 shadow-inner shadow-slate-600"
|
||||
>
|
||||
<a
|
||||
v-for="tab in tabs2"
|
||||
v-for="tab in tabs"
|
||||
:key="tab.label"
|
||||
role="tab"
|
||||
class="tab"
|
||||
@ -69,6 +69,42 @@ const props = defineProps({
|
||||
</table>
|
||||
</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>
|
||||
|
@ -6,7 +6,7 @@ const props = defineProps({
|
||||
<template>
|
||||
<div class="card bg-slate-200 text-accent-content rounded-md w-60">
|
||||
<div class="card-body p-3">
|
||||
<h2 class="card-title">{{ heat.name }}</h2>
|
||||
<h2 class="card-title">{{ heat.equipment }}</h2>
|
||||
<div class="p-0">
|
||||
<ul class="leading-7 tracking-wider text-slate-700 px-2">
|
||||
<li><b>溫度:</b> {{ heat.temperature }}</li>
|
||||
|
@ -13,9 +13,6 @@ defineProps({ meter: Object })
|
||||
<li><b>用電量:</b> {{ meter.energyConsumption }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="card-actions justify-end">
|
||||
<button class="btn btn-xs btn-success">詳細資料</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -5,7 +5,7 @@ const props = defineProps({
|
||||
});
|
||||
</script>
|
||||
<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">
|
||||
<h2 class="card-title">{{ vessel.name }}</h2>
|
||||
<div
|
||||
@ -84,6 +84,30 @@ const props = defineProps({
|
||||
</table>
|
||||
</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>
|
||||
|
@ -1,6 +1,7 @@
|
||||
<script setup>
|
||||
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 useActiveBtn from "@/hooks/useActiveBtn";
|
||||
|
||||
@ -14,16 +15,16 @@ onBeforeMount(() => {
|
||||
setItems([
|
||||
{
|
||||
title: "檢重機設定",
|
||||
key: "WeightLossMachineTable",
|
||||
key: "CheckWeigherTable",
|
||||
active: true,
|
||||
component: WeightLossMachineTable,
|
||||
component: CheckWeigherTable,
|
||||
},
|
||||
// {
|
||||
// title: "B 設備管理",
|
||||
// key: "ProducSettingTable2",
|
||||
// active: false,
|
||||
// component: ProducSettingTable,
|
||||
// }
|
||||
{
|
||||
title: "原醋庫存設定",
|
||||
key: "InventorySetting",
|
||||
active: false,
|
||||
component: InventorySetting,
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
|
@ -8,74 +8,78 @@ const { openToast } = inject("app_toast");
|
||||
|
||||
const props = defineProps({
|
||||
formState: Object,
|
||||
getData: Function,
|
||||
currentEditIndex: Number,
|
||||
});
|
||||
const emit = defineEmits(["pushData"]);
|
||||
|
||||
const buildScheme = yup.object({
|
||||
group: yup.string().required("必填"),
|
||||
product_code: yup.string().required("必填"),
|
||||
product_name: yup.string().required("必填"),
|
||||
upper_limit: yup.string().required("必填"),
|
||||
lower_limit: yup.string().required("必填"),
|
||||
const itemScheme = yup.object({
|
||||
groupNum: yup.string().required("必填"),
|
||||
itemName: yup.string().required("必填"),
|
||||
name: yup.string().required("必填"),
|
||||
upperLimit: yup.string().required("必填"),
|
||||
lowerLimit: yup.string().required("必填"),
|
||||
});
|
||||
|
||||
const { formErrorMsg, handleSubmit, handleErrorReset, updateScheme } =
|
||||
useFormErrorMessage(buildScheme);
|
||||
useFormErrorMessage(itemScheme);
|
||||
|
||||
const onCancel = () => {
|
||||
handleErrorReset();
|
||||
weight_loss_machine_modal.close();
|
||||
weight_check_machine_modal.close();
|
||||
};
|
||||
|
||||
const onOk = async () => {
|
||||
const value = await handleSubmit(buildScheme, props.formState);
|
||||
// const res = await postBuildings(value);
|
||||
// if (res.isSuccess) {
|
||||
// props.getData();
|
||||
const value = await handleSubmit(itemScheme, props.formState);
|
||||
emit("pushData", {...value});
|
||||
onCancel();
|
||||
// } else {
|
||||
// openToast("error", res.msg, "#weight_loss_machine_modal");
|
||||
// }
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal
|
||||
id="weight_loss_machine_modal"
|
||||
:title="props.formState?.product_code ? '修改檢重機設備' : '新增檢重機設備'"
|
||||
id="weight_check_machine_modal"
|
||||
:title="props.formState?.itemName ? '修改檢重機設備' : '新增檢重機設備'"
|
||||
:onCancel="onCancel"
|
||||
:width="710"
|
||||
>
|
||||
<template #modalContent>
|
||||
<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 #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.group }}</span>
|
||||
<span class="text-error text-base">{{
|
||||
formErrorMsg.groupNum
|
||||
}}</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input :value="formState" class="my-2" name="product_code">
|
||||
<Input :value="formState" class="my-2" name="itemName">
|
||||
<template #topLeft>成品代號</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.product_code }}</span>
|
||||
<span class="text-error text-base">{{
|
||||
formErrorMsg.itemName
|
||||
}}</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input :value="formState" class="my-2" name="product_name">
|
||||
<Input :value="formState" class="my-2" name="name">
|
||||
<template #topLeft>品名</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.product_name }}</span>
|
||||
<span class="text-error text-base">{{ formErrorMsg.name }}</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input :value="formState" class="my-2" name="upper_limit">
|
||||
<Input :value="formState" class="my-2" name="upperLimit">
|
||||
<template #topLeft>上限</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.upper_limit }}</span>
|
||||
<span class="text-error text-base">{{
|
||||
formErrorMsg.upperLimit
|
||||
}}</span>
|
||||
</template>
|
||||
</Input>
|
||||
<Input :value="formState" class="my-2" name="lower_limit">
|
||||
<Input :value="formState" class="my-2" name="lowerLimit">
|
||||
<template #topLeft>下限</template>
|
||||
<template #bottomLeft>
|
||||
<span class="text-error text-base">{{ formErrorMsg.lower_limit }}</span>
|
||||
<span class="text-error text-base">{{
|
||||
formErrorMsg.lowerLimit
|
||||
}}</span>
|
||||
</template>
|
||||
</Input>
|
||||
</form>
|
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