水流量動畫 | 首頁小卡data串接 | 生產設定 : 原醋庫存列表靜態頁面

This commit is contained in:
koko 2025-07-30 09:55:09 +08:00
parent 33dab54820
commit 49560b3dd5
33 changed files with 1532 additions and 945 deletions

View File

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

View File

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

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

BIN
public/forge.zip Normal file

Binary file not shown.

3
public/spot.svg Normal file
View 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

View File

@ -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`;

View File

@ -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);

View File

@ -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`;

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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,
}; };
} }

View 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,
};
}

View File

@ -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 () => {
try {
const res = await getDashboardInit(); const res = await getDashboardInit();
initialData.value = res.data; 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">

View File

@ -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>

View File

@ -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];

View File

@ -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,395 +13,95 @@ 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
class="absolute left-1/2 -translate-x-1/2 z-50"
:style="{ top: searchParams?.top || '40%' }" :style="{ top: searchParams?.top || '40%' }"
> >
<template v-if="searchParams?.option == '1'"> <template v-if="searchParams?.option == '1'">
@ -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'">

View File

@ -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({

View File

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

View File

@ -1,14 +1,12 @@
<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 = [
production_data.value = [
{ {
id: 1, id: 1,
productName: "蘋果醋", productName: "蘋果醋",
@ -44,7 +42,54 @@ const getCompletion = async () => {
details: [], 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); 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">

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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,
// } }
]); ]);
}); });

View File

@ -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) {
// props.getData();
onCancel(); 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>

View 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>

View 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>

View 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>

View File

@ -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>