Help
RSS
API
Feed
Maltego
Contact
Domain > agri-api.webduino.io
×
More information on this domain is in
AlienVault OTX
Is this malicious?
Yes
No
DNS Resolutions
Date
IP Address
2025-08-16
104.26.11.80
(
ClassC
)
2026-02-24
172.67.73.169
(
ClassC
)
Port 443
HTTP/1.1 200 OKDate: Tue, 24 Feb 2026 04:57:41 GMTContent-Type: text/html; charsetUTF-8Transfer-Encoding: chunkedConnection: keep-aliveAccept-Ranges: bytesCache-Control: public, max-age0Report-To: {group:cf-nel,max_age:604800,endpoints:{url:https://a.nel.cloudflare.com/report/v4?s7CPRsDkrYk3SpTW%2BdaGLLQE9f4r8TStxtWSVgBqHBs2P%2FsY8SvMUcy4S2dmQ8DAVDPO61Y01Iu8WNa%2B7ahEBSNxd6AORHi7cGLQMz0FDbsJM}}last-modified: Fri, 09 Jan 2026 07:29:46 GMTx-powered-by: Expresscf-cache-status: DYNAMICNel: {report_to:cf-nel,success_fraction:0.0,max_age:604800}Server: cloudflareCF-RAY: 9d2c622c9dd7f896-PDXalt-svc: h3:443; ma86400 !DOCTYPE html>html langzh-TW>head> meta charsetUTF-8> meta nameviewport contentwidthdevice-width, initial-scale1.0> title>農業管理系統/title> link hrefhttps://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css relstylesheet> style> :root { /* 主要色彩系統 - 綠色主題 */ --primary-green: #79b450; --primary-green-light: #79b450; --primary-green-dark: #79b450; --secondary-green: #15803d; --accent-green: #66af0c; --light-green: #dcfce7; --ultra-light-green: #f0fdf4; /* 漸變色彩 */ --gradient-primary: linear-gradient(135deg, #79b450 0%, #79b450 100%); --gradient-secondary: linear-gradient(135deg, #199430 0%, #42953e 100%); --gradient-light: linear-gradient(135deg, #dcfce7 0%, #bbf7d0 100%); --gradient-card: linear-gradient(145deg, #ffffff 0%, #f8fafc 100%); /* 基本色彩變數 */ --panel-border-color: #d1d5db; --panel-header-bg: var(--gradient-light); --panel-hover-bg: #f0fdf4; --panel-active-bg: var(--light-green); --text-primary: #1f2937; --text-secondary: #6b7280; /* 背景色彩 */ --bg-white: #ffffff; --bg-light: #f9fafb; --bg-lighter: var(--ultra-light-green); --bg-gradient: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); --border-light: #e5e7eb; /* 主題色彩 */ --primary-color: var(--primary-green); --primary-light: var(--light-green); --success-color: #059669; --warning-color: #d97706; --danger-color: #dc2626; --info-color: #0891b2; /* 陰影效果 */ --shadow-sm: 0 1px 3px rgba(34, 197, 94, 0.12), 0 1px 2px rgba(34, 197, 94, 0.08); --shadow-md: 0 4px 6px rgba(34, 197, 94, 0.1), 0 2px 4px rgba(34, 197, 94, 0.06); --shadow-lg: 0 10px 15px rgba(34, 197, 94, 0.1), 0 4px 6px rgba(34, 197, 94, 0.05); --shadow-card: 0 4px 12px rgba(34, 197, 94, 0.15); } body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, sans-serif; color: var(--text-primary); background: var(--bg-gradient); line-height: 1.5; } .main-container { display: flex; overflow: hidden; background: var(--bg-gradient); padding: 12px; gap: 8px; } .section { display: flex; flex-direction: column; height: 100%; position: relative; background: var(--gradient-card); border: 1px solid var(--panel-border-color); border-radius: 12px; box-shadow: var(--shadow-card); overflow: hidden; backdrop-filter: blur(10px); } .left-section { flex: 0 0 25%; min-width: 320px; } .middle-section { flex: 0 0 40%; min-width: 350px; } .right-section { flex: 0 0 35%; min-width: 320px; } .resize-handle-vertical { width: 10px; background-color: transparent; cursor: col-resize; margin: 0 -5px; position: relative; z-index: 10; transition: background-color 0.2s; } .resize-handle-vertical:hover { background-color: var(--panel-hover-bg); } .panel-header { position: sticky; top: 0; background: var(--gradient-primary); padding: 6px 10px; border-bottom: none; z-index: 5; display: flex; justify-content: space-between; align-items: center; box-shadow: var(--shadow-md); border-radius: 12px 12px 0 0; } .panel-content { flex: 1; overflow-y: auto; padding: 20px; } .panel-title { margin: 0; font-size: 1rem; font-weight: 700; color: white; text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); letter-spacing: 0.5px; } .resize-handle { height: 10px; background-color: transparent; cursor: row-resize; position: relative; margin: -5px 0; z-index: 10; transition: background-color 0.2s; } .resize-handle:hover { background-color: var(--panel-hover-bg); } .user-panel { flex: 0 0 auto; min-height: 200px; max-height: 60vh; display: flex; flex-direction: column; } .field-panel { flex: 1 1 auto; min-height: 200px; display: flex; flex-direction: column; } .user-list { list-style: none; padding: 0; margin: 0; } .user-item { padding: 16px 20px; margin: 8px 12px; cursor: pointer; border: 1px solid var(--border-light); border-radius: 10px; transition: all 0.3s ease; background: var(--bg-white); box-shadow: var(--shadow-sm); } .user-item:last-child { border-bottom: 1px solid var(--border-light); } .user-item:hover { background: var(--gradient-light); transform: translateY(-2px); box-shadow: var(--shadow-md); border-color: var(--accent-green); } .user-item.active { background: var(--gradient-secondary); color: white; border-color: var(--primary-green-dark); box-shadow: var(--shadow-lg); } .user-item strong { display: block; color: var(--text-primary); margin-bottom: 4px; } .user-item .text-muted { font-size: 0.875rem; color: var(--text-secondary) !important; } .field-list { list-style: none; padding: 0; margin: 0; } .field-item { padding: 18px 20px; margin: 8px 12px 16px; border: 1px solid var(--border-light); border-radius: 12px; background: var(--gradient-card); transition: all 0.3s ease; box-shadow: var(--shadow-sm); cursor: pointer; } .field-item:hover { background: var(--gradient-light); box-shadow: var(--shadow-md); transform: translateY(-3px); border-color: var(--accent-green); } .field-item:last-child { margin-bottom: 8px; } .field-item h5 { margin: 0 0 10px 0; color: var(--text-primary); font-weight: 600; } .field-item .text-muted { font-size: 0.875rem; color: var(--text-secondary) !important; margin-top: 8px; } /* 自定義捲軸樣式 */ .panel-content::-webkit-scrollbar { width: 8px; } .panel-content::-webkit-scrollbar-track { background: #f1f1f1; } .panel-content::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; } .panel-content::-webkit-scrollbar-thumb:hover { background: #a8a8a8; } .equipment-list { display: flex; flex-direction: column; gap: 15px; } .equipment-item { background: white; border: 1px solid var(--panel-border-color); border-radius: 8px; overflow: hidden; transition: all 0.2s ease; } .equipment-item:hover { box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05); transform: translateY(-1px); } .equipment-header { padding: 12px 15px; background-color: var(--panel-header-bg); border-bottom: 1px solid var(--panel-border-color); display: flex; justify-content: space-between; align-items: center; } .equipment-header h5 { margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary); } .equipment-content { padding: 15px; display: flex; flex-direction: column; gap: 10px; } .equipment-info { font-size: 0.9rem; color: var(--text-primary); } .equipment-info > div { margin-bottom: 5px; } .equipment-company { font-size: 0.875rem; color: var(--text-secondary); padding-top: 10px; border-top: 1px solid var(--panel-border-color); } .equipment-company > div { margin-bottom: 3px; } .badge { font-size: 0.75rem; font-weight: 700; padding: 8px 14px; border-radius: 15px; background: var(--gradient-primary); color: white; text-transform: uppercase; letter-spacing: 0.8px; box-shadow: var(--shadow-md); border: 2px solid rgba(255, 255, 255, 0.3); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); } /* 更新田區項目的活動狀態樣式 */ .field-item.active { background: var(--gradient-secondary); color: white; border-color: var(--primary-green-dark); box-shadow: var(--shadow-lg); } .field-item.active h5 { color: white; } .field-item.active .text-muted { color: rgba(255, 255, 255, 0.8) !important; } /* 添加搜尋框樣式 */ .search-box { display: flex; align-items: center; gap: 8px; } .search-input { padding: 10px 16px; border: 2px solid rgba(255, 255, 255, 0.3); border-radius: 20px; font-size: 0.875rem; width: 180px; background: rgba(255, 255, 255, 0.9); transition: all 0.3s ease; backdrop-filter: blur(10px); } .search-input:focus { outline: none; border-color: rgba(255, 255, 255, 0.8); box-shadow: 0 0 20px rgba(255, 255, 255, 0.3); background: white; transform: scale(1.02); } .count-badge { background: var(--gradient-secondary); color: white; padding: 8px 14px; border-radius: 15px; font-size: 0.75rem; font-weight: 700; box-shadow: var(--shadow-md); border: 2px solid rgba(255, 255, 255, 0.2); text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } /* 觀測資料樣式 */ .observation-list { display: flex; flex-direction: column; gap: 15px; } .observation-item { background: white; border: 1px solid var(--panel-border-color); border-radius: 8px; overflow: hidden; } .observation-header { padding: 10px 15px; background-color: var(--panel-header-bg); border-bottom: 1px solid var(--panel-border-color); } .observation-time { font-size: 0.9rem; color: var(--text-secondary); } .observation-content { padding: 15px; } .observation-data { font-size: 0.95rem; color: var(--text-primary); } .observation-data > div { margin-bottom: 5px; } /* 設備項目樣式 */ .device-item { background: var(--gradient-card); border: 1px solid var(--border-light); border-radius: 12px; margin: 8px 12px 16px; overflow: hidden; transition: all 0.3s ease; cursor: pointer; box-shadow: var(--shadow-sm); } .device-item:hover { background: var(--gradient-light); box-shadow: var(--shadow-md); transform: translateY(-3px); border-color: var(--accent-green); } .device-item:last-child { margin-bottom: 8px; } .device-item.active { background: var(--gradient-secondary); color: white; border-color: var(--primary-green-dark); box-shadow: var(--shadow-lg); } .device-header { padding: 15px 20px; background: var(--gradient-light); border-bottom: 1px solid var(--border-light); display: flex; justify-content: space-between; align-items: center; } .device-item.active .device-header { background: rgba(255, 255, 255, 0.1); border-bottom-color: rgba(255, 255, 255, 0.2); } .device-header h6 { margin: 0; font-size: 1rem; font-weight: 600; color: var(--text-primary); } .device-content { padding: 15px; display: flex; flex-direction: column; gap: 10px; } .device-info { font-size: 0.9rem; color: var(--text-primary); } .device-info > div { margin-bottom: 5px; } .device-company { font-size: 0.875rem; color: var(--text-secondary); padding-top: 10px; border-top: 1px solid var(--panel-border-color); } .device-company > div { margin-bottom: 3px; } /* 設備項目的活動狀態樣式 */ .equipment-item.active { background-color: var(--panel-active-bg); border-color: var(--panel-border-color); } /* 響應式設計 */ @media (max-width: 1200px) { .main-container { padding: 4px; gap: 1px; } .left-section { flex: 0 0 30%; min-width: 280px; } .middle-section { flex: 0 0 35%; min-width: 300px; } .right-section { flex: 0 0 35%; min-width: 280px; } .panel-header { padding: 12px 16px; } .search-input { width: 150px; } } @media (max-width: 768px) { .main-container { flex-direction: column; padding: 2px; gap: 2px; } .left-section, .middle-section, .right-section { flex: 1 1 auto; min-width: auto; min-height: 200px; } .resize-handle-vertical { display: none; } .search-input { width: 120px; } .panel-header { padding: 10px 12px; } .panel-title { font-size: 1rem; } } /* 改善互動效果 */ .user-item, .field-item, .device-item { transition: all 0.2s ease; } .user-item:hover, .field-item:hover, .device-item:hover { transform: translateX(2px); } .user-item.active, .field-item.active, .device-item.active { border-left: 4px solid var(--accent-green); transform: translateX(2px) translateY(-2px); } .user-item.active strong, .user-item.active .text-muted { color: white !important; } .device-item.active h6, .device-item.active .device-info, .device-item.active .device-company { color: white !important; } .device-item.active .device-info > div, .device-item.active .device-company > div { color: rgba(255, 255, 255, 0.9) !important; } /* Token 狀態欄樣式 */ .token-status-bar { position: fixed; top: 0; left: 0; right: 0; height: 50px; background: var(--gradient-primary); color: white; display: flex; align-items: center; justify-content: space-between; padding: 0 20px; box-shadow: var(--shadow-md); z-index: 1000; border-bottom: 2px solid rgba(255, 255, 255, 0.2); } .token-info { display: flex; align-items: center; gap: 15px; font-size: 0.9rem; } .token-name { font-weight: 700; background: rgba(255, 255, 255, 0.2); padding: 4px 12px; border-radius: 15px; border: 1px solid rgba(255, 255, 255, 0.3); } .token-scope { font-size: 0.8rem; opacity: 0.9; } .token-actions { display: flex; gap: 10px; align-items: center; } .btn-token { background: rgba(255, 255, 255, 0.2); color: white; border: 1px solid rgba(255, 255, 255, 0.3); padding: 6px 16px; border-radius: 20px; font-size: 0.8rem; font-weight: 600; cursor: pointer; transition: all 0.3s ease; text-decoration: none; display: inline-flex; align-items: center; gap: 5px; } .btn-token:hover { background: rgba(255, 255, 255, 0.3); color: white; transform: translateY(-1px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .btn-logout { background: rgba(220, 38, 38, 0.8); border-color: rgba(220, 38, 38, 0.6); } .btn-logout:hover { background: rgba(220, 38, 38, 1); color: white; } /* 調整主容器,為狀態欄留出空間 */ .main-container { margin-top: 50px; height: calc(100vh - 50px); } /* 響應式調整 */ @media (max-width: 768px) { .token-status-bar { flex-direction: column; height: auto; padding: 8px 15px; gap: 8px; } .token-info { gap: 10px; font-size: 0.8rem; } .token-actions { gap: 8px; } .main-container { margin-top: 70px; height: calc(100vh - 70px); } } /style>/head>body> !-- Token 狀態欄 --> div classtoken-status-bar idtokenStatusBar styledisplay: none;> div classtoken-info> span classtoken-name idcurrentTokenName>Loading.../span> span classtoken-scope idtokenScope>檢查權限中.../span> /div> div classtoken-actions> button classbtn-token onclickswitchToken()> 🔄 切換 Token /button> button classbtn-token btn-logout onclicklogout()> 🚪 登出 /button> /div> /div> div classmain-container> !-- 左側區域 --> div classleft-section section> !-- 用戶清單面板 --> div classuser-panel> div classpanel-header> h4 classpanel-title>用戶清單/h4> div classsearch-box> input typetext classsearch-input iduserSearch placeholder搜尋用戶...> span classcount-badge iduserCount>0/0/span> /div> /div> div classpanel-content> ul iduserList classuser-list> !-- 用戶列表將由 JavaScript 動態填充 --> /ul> /div> /div> !-- 可調整大小的水平分隔線 --> div classresize-handle idresizeHandle>/div> !-- 田區清單面板 --> div classfield-panel> div classpanel-header> h4 classpanel-title>田區清單/h4> div classsearch-box> input typetext classsearch-input idfieldSearch placeholder搜尋田區...> span classcount-badge idfieldCount>0/0/span> /div> /div> div classpanel-content> div idfieldList classfield-list> !-- 田區列表將由 JavaScript 動態填充 --> /div> /div> /div> /div> !-- 左右調整大小的分隔線 1 --> div classresize-handle-vertical idresizeHandle1>/div> !-- 中間區域 --> div classmiddle-section section> div classpanel-header> h4 classpanel-title>裝置清單/h4> div classsearch-box> input typetext classsearch-input iddeviceSearch placeholder搜尋裝置...> span classcount-badge iddeviceCount>0/0/span> /div> /div> div classpanel-content> !-- 面板 2 的內容 --> /div> /div> !-- 左右調整大小的分隔線 2 --> div classresize-handle-vertical idresizeHandle2>/div> !-- 右側區域 --> div classright-section section> div classpanel-header> h4 classpanel-title>感測資料/h4> span classcount-badge iddataCount>0/0/span> /div> div classpanel-content> !-- 面板 3 的內容 --> /div> /div> /div> script> // 當前選中的用戶 ID let currentUserId null; // 全局變數儲存設備對應表 let deviceMapping {}; // 全局 token 變數和權限信息 let accessToken null; let tokenInfo null; // Cookie 操作函數 function getCookie(name) { const nameEQ name + ; const ca document.cookie.split(;); for (let i 0; i ca.length; i++) { let c cai; while (c.charAt(0) ) c c.substring(1, c.length); if (c.indexOf(nameEQ) 0) return c.substring(nameEQ.length, c.length); } return null; } function deleteCookie(name) { document.cookie name + ; expiresThu, 01 Jan 1970 00:00:00 UTC; path/;; } // 檢查並驗證 token async function checkToken() { accessToken getCookie(agri_access_token); if (!accessToken) { console.log(沒有找到 access token,跳轉到登入頁面); window.location.href /login.html; return false; } try { const response await fetch(/api/v1/verify-token, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ token: accessToken }) }); if (!response.ok) { console.log(Token 驗證失敗,跳轉到登入頁面); deleteCookie(agri_access_token); window.location.href /login.html; return false; } const data await response.json(); console.log(Token 驗證成功,允許訪問的用戶:, data.allowedUsers); // 儲存 token 信息並更新狀態欄 tokenInfo data; updateTokenStatusBar(); return true; } catch (error) { console.error(驗證 token 時發生錯誤:, error); window.location.href /login.html; return false; } } // 使用 token 發送 API 請求 async function fetchWithAuth(url, options {}) { if (!accessToken) { throw new Error(沒有有效的 access token); } const authOptions { ...options, headers: { ...options.headers, Authorization: `Bearer ${accessToken}`, Content-Type: application/json } }; const response await fetch(url, authOptions); if (response.status 401 || response.status 403) { console.log(Token 已過期或無效,跳轉到登入頁面); deleteCookie(agri_access_token); window.location.href /login.html; throw new Error(Token 無效); } return response; } // 更新 Token 狀態欄 function updateTokenStatusBar() { const statusBar document.getElementById(tokenStatusBar); const tokenNameEl document.getElementById(currentTokenName); const tokenScopeEl document.getElementById(tokenScope); if (tokenInfo && accessToken) { // 顯示狀態欄 statusBar.style.display flex; // 更新 token 名稱 tokenNameEl.textContent getTokenDisplayName(accessToken); // 更新權限範圍描述 tokenScopeEl.textContent getTokenScopeDescription(tokenInfo.allowedUsers); } else { statusBar.style.display none; } } // 獲取 Token 顯示名稱 (不暴露實際 token 值) function getTokenDisplayName(token) { // 只顯示 token 的前幾個字符,其餘用星號遮蔽 if (token.length > 6) { return token.substring(0, 3) + *** + token.substring(token.length - 2); } else { return token.substring(0, 2) + ***; } } // 獲取權限範圍描述 function getTokenScopeDescription(allowedUsers) { if (allowedUsers *) { return 可訪問所有用戶資料; } else if (typeof allowedUsers string) { return `只能訪問: ${allowedUsers}`; } else if (Array.isArray(allowedUsers)) { return `可訪問 ${allowedUsers.length} 個指定用戶`; } return 權限未知; } // 登出功能 function logout() { if (confirm(確定要登出嗎?)) { // 清除 cookie deleteCookie(agri_access_token); // 清除全局變數 accessToken null; tokenInfo null; currentUserId null; // 隱藏狀態欄 document.getElementById(tokenStatusBar).style.display none; // 跳轉到登入頁面 window.location.href /login.html; } } // 切換 Token 功能 function switchToken() { if (confirm(切換 Token 將會登出當前帳戶,確定要繼續嗎?)) { logout(); } } // 載入用戶列表 async function loadUsers() { try { const response await fetchWithAuth(/api/v1/users); const data await response.json(); const userList document.getElementById(userList); userList.innerHTML ; data.value.forEach(user > { const li document.createElement(li); li.className user-item; li.innerHTML ` div>strong>${user.name || 未命名}/strong>/div> div classtext-muted small>${user.email}/div> ${user.company ? `div classtext-muted small>${user.company}/div>` : } `; li.onclick function() { selectUser(user.id, this); }; userList.appendChild(li); }); // 更新用戶計數 updateCounter(userCount, data.value.length, data.value.length); // 添加搜尋功能 const userSearch document.getElementById(userSearch); userSearch.oninput (e) > { const searchText e.target.value.toLowerCase(); const items userList.getElementsByClassName(user-item); let visibleCount 0; Array.from(items).forEach(item > { const text item.textContent.toLowerCase(); const visible text.includes(searchText); item.style.display visible ? : none; if (visible) visibleCount++; }); updateCounter(userCount, visibleCount, data.value.length); }; } catch (error) { console.error(載入用戶列表失敗:, error); } } // 載入設備對應表 async function loadDeviceMapping() { try { const response await fetch(/devices.json); const data await response.json(); // 建立 agri_type 到設備資訊的映射 deviceMapping {}; for (const key, value of Object.entries(data)) { const agriTypeNumber parseInt(value.agri_type.replace(M, )); deviceMappingagriTypeNumber { name: value.name, type: value.type, agri_type: value.agri_type }; } console.log(設備對應表載入完成:, deviceMapping); } catch (error) { console.error(載入設備對應表失敗:, error); // 使用預設的對應表 deviceMapping { 101: { name: 溫濕度感測器, type: DK_WS_N01_PVC, agri_type: M101 }, 106: { name: 光學雨量計, type: rainfall, agri_type: M106 }, 201: { name: 流量計, type: flowmeter, agri_type: M201 }, 207: { name: 繼電器, type: relay, agri_type: M207 }, 512: { name: 網路攝影機, type: ipcam, agri_type: M512 }, 513: { name: 百葉箱感測器, type: ZTS_300BYH, agri_type: M513 }, 514: { name: 土濕三合一, type: soil, agri_type: M514 }, 515: { name: 土濕三合一PH, type: soil_ph, agri_type: M515 }, 516: { name: 紅外測溫儀, type: ir_SA10R4C, agri_type: M516 }, 517: { name: 網路攝影機, type: ipcam, agri_type: M517 } }; } } // 設備型號對應函數 - 使用載入的對應表 function getDeviceModelName(devicetype) { const device deviceMappingdevicetype; if (device) { return device.name; } // 備用對應表(保留原有的 WiFi/LoRa 系列) const fallbackModels { 202: 二路控制器, 204: 四路控制器, 206: 六路控制器, 208: 土壤溫濕電導控制器, 508: WiFi主機, 509: WiFi感測器, 511: WiFi擴展板, 533: LoRa感測器, 534: LoRa閘道器, 2071: WiFi四路控制器 }; return fallbackModelsdevicetype || `型號 ${devicetype}`; } // 選擇用戶 async function selectUser(userId, clickedElement) { currentUserId userId; // 更新選中狀態 document.querySelectorAll(.user-item).forEach(item > { item.classList.remove(active); }); if (clickedElement) { clickedElement.classList.add(active); } // 清除裝置清單面板 const middlePanel document.querySelector(.middle-section .panel-content); middlePanel.innerHTML div classalert alert-info>請選擇田區以查看裝置/div>; updateCounter(deviceCount, 0, 0); // 清除感測資料面板 const rightPanel document.querySelector(.right-section .panel-content); rightPanel.innerHTML div classalert alert-info>請選擇裝置以查看感測資料/div>; updateCounter(dataCount, 0, 0); // 載入該用戶的田區 await loadFields(userId); } // 載入田區列表 async function loadFields(userId) { try { // 直接使用 fields API 獲取田區資料 const response await fetchWithAuth(`/api/v1/fields/${userId}`); const data await response.json(); const fieldList document.getElementById(fieldList); fieldList.innerHTML ; // 確保 data.value 是陣列 const fieldsArray data.value || ; if (fieldsArray.length 0) { fieldList.innerHTML div classalert alert-info>目前沒有田區資料/div>; updateCounter(fieldCount, 0, 0); return; } // 為每個田區創建一個項目 fieldsArray.forEach(field > { const fieldDiv document.createElement(div); fieldDiv.className field-item; fieldDiv.onclick function() { selectField(field.id, this); }; fieldDiv.innerHTML ` h5>${field.name || 未命名田區}/h5> ${field.memo ? `div>描述: ${field.memo}/div>` : } ${field.area ? `div>面積: ${field.area} 平方公尺/div>` : } div classtext-muted>田區 ID: ${field.id}/div> div classtext-muted>系統 ID: ${userId}/div> `; fieldList.appendChild(fieldDiv); }); // 更新田區計數 updateCounter(fieldCount, fieldsArray.length, fieldsArray.length); // 添加搜尋功能 const fieldSearch document.getElementById(fieldSearch); fieldSearch.oninput (e) > { const searchText e.target.value.toLowerCase(); const items fieldList.getElementsByClassName(field-item); let visibleCount 0; Array.from(items).forEach(item > { const text item.textContent.toLowerCase(); const visible text.includes(searchText); item.style.display visible ? : none; if (visible) visibleCount++; }); updateCounter(fieldCount, visibleCount, fieldsArray.length); }; } catch (error) { console.error(載入田區列表失敗:, error); const fieldList document.getElementById(fieldList); fieldList.innerHTML div classalert alert-danger>載入田區列表失敗/div>; updateCounter(fieldCount, 0, 0); } } // 添加選擇田區的函數 async function selectField(fieldId, clickedElement) { if (!fieldId) { console.error(無效的田區 ID); return; } // 更新選中狀態 document.querySelectorAll(.field-item).forEach(item > { item.classList.remove(active); }); if (clickedElement) { clickedElement.classList.add(active); } // 清除感測資料面板 const rightPanel document.querySelector(.right-section .panel-content); rightPanel.innerHTML div classalert alert-info>請選擇裝置以查看感測資料/div>; updateCounter(dataCount, 0, 0); // 載入設備清單 try { // 使用 field_equipments API 獲取田區的設備清單 const response await fetchWithAuth(`/api/v1/field_equipments/${fieldId}`); const data await response.json(); const devicePanel document.querySelector(.middle-section .panel-content); devicePanel.innerHTML ; // 確保 data.value 是陣列 const devicesArray data.value || ; if (devicesArray.length 0) { devicePanel.innerHTML div classalert alert-info>目前沒有設備資料/div>; updateCounter(deviceCount, 0, 0); return; } // 為每個設備創建一個項目 devicesArray.forEach(device > { const deviceDiv document.createElement(div); deviceDiv.className device-item; deviceDiv.onclick function() { selectDevice(device.deviceid, this); }; const modelName getDeviceModelName(device.devicetype); const connectionType device.connection_type || 未知連接; const companyName device.company_name || 未知製造商; deviceDiv.innerHTML ` div classdevice-header> h6>${device.equipment_name || device.deviceid}/h6> span classbadge>${modelName}/span> /div> div classdevice-content> div classdevice-info> div>設備 ID: ${device.deviceid}/div> div>描述: ${device.memo || 無描述}/div> div>連接方式: ${connectionType}/div> /div> div classdevice-company> div>製造商: ${companyName}/div> /div> /div> `; devicePanel.appendChild(deviceDiv); }); updateCounter(deviceCount, devicesArray.length, devicesArray.length); // 添加設備搜尋功能 const deviceSearch document.getElementById(deviceSearch); deviceSearch.oninput (e) > { const searchText e.target.value.toLowerCase(); const items devicePanel.getElementsByClassName(device-item); let visibleCount 0; Array.from(items).forEach(item > { const text item.textContent.toLowerCase(); const visible text.includes(searchText); item.style.display visible ? : none; if (visible) visibleCount++; }); updateCounter(deviceCount, visibleCount, devicesArray.length); }; } catch (error) { console.error(載入設備清單時發生錯誤:, error); const devicePanel document.querySelector(.middle-section .panel-content); devicePanel.innerHTML div classalert alert-danger>載入設備清單失敗,請重試。/div>; updateCounter(deviceCount, 0, 0); } } // 選擇設備函數 async function selectDevice(deviceId, clickedElement) { if (!deviceId) { console.error(無效的設備 ID); return; } // 更新選中狀態 document.querySelectorAll(.device-item).forEach(item > { item.classList.remove(active); }); if (clickedElement) { clickedElement.classList.add(active); } // 載入感測資料 try { const rightPanel document.querySelector(.right-section .panel-content); rightPanel.innerHTML ` div classalert alert-info> h6>設備 ID: ${deviceId}/h6> p>感測資料載入中.../p> div classspinner-border spinner-border-sm rolestatus> span classvisually-hidden>載入中.../span> /div> /div> `; // 載入歷史觀測資料 const response await fetchWithAuth(`/api/v1/Observations/${deviceId}?$top20&$orderbyphenomenonTime desc`); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data await response.json(); // 顯示感測資料 if (data.value && data.value.length > 0) { let observationHtml div classobservation-list>; data.value.forEach(observation > { const phenomenonTime new Date(observation.phenomenonTime).toLocaleString(zh-TW); const observationValue typeof observation.observationValue object ? JSON.stringify(observation.observationValue, null, 2) : observation.observationValue; // 使用前端的設備映射函數來顯示正確的設備名稱 const deviceName observation.deviceType ? getDeviceModelName(observation.deviceType) : (observation.deviceName || observation.deviceID); // 格式化附加資訊 const additionalInfo observation.additionalInfo ? (typeof observation.additionalInfo object ? JSON.stringify(observation.additionalInfo, null, 2) : observation.additionalInfo) : ; observationHtml + ` div classobservation-item> div classobservation-header> div classobservation-time>${phenomenonTime}/div> /div> div classobservation-content> div classobservation-data> div>strong>設備:/strong> ${deviceName}/div> div>strong>數值:/strong>/div> pre stylebackground: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 0.875rem; overflow-x: auto;>${observationValue}/pre> ${additionalInfo ? `div>strong>附加資訊:/strong>/div>pre stylebackground: #f1f3f4; padding: 10px; border-radius: 4px; font-size: 0.875rem; overflow-x: auto; max-height: 200px;>${additionalInfo}/pre>` : } /div> /div> /div> `; }); observationHtml + /div>; rightPanel.innerHTML observationHtml; updateCounter(dataCount, data.value.length, data.value.length); } else { rightPanel.innerHTML div classalert alert-warning>此設備目前沒有感測資料/div>; updateCounter(dataCount, 0, 0); } } catch (error) { console.error(載入感測資料時發生錯誤:, error); const rightPanel document.querySelector(.right-section .panel-content); rightPanel.innerHTML ` div classalert alert-danger> h6>載入感測資料失敗/h6> p>錯誤訊息: ${error.message}/p> p>請檢查設備 ID 是否正確,或稍後重試。/p> /div> `; updateCounter(dataCount, 0, 0); } } // 實現垂直面板高度調整功能 const resizeHandle document.getElementById(resizeHandle); const userPanel document.querySelector(.user-panel); let isResizing false; let startY; let startHeight; resizeHandle.addEventListener(mousedown, (e) > { isResizing true; startY e.clientY; startHeight userPanel.offsetHeight; document.addEventListener(mousemove, handleMouseMove); document.addEventListener(mouseup, () > { isResizing false; document.removeEventListener(mousemove, handleMouseMove); }); }); function handleMouseMove(e) { if (!isResizing) return; const deltaY e.clientY - startY; const newHeight Math.max(200, Math.min(startHeight + deltaY, window.innerHeight * 0.6)); userPanel.style.height `${newHeight}px`; } // 實現水平面板寬度調整功能 const resizeHandle1 document.getElementById(resizeHandle1); const resizeHandle2 document.getElementById(resizeHandle2); const leftSection document.querySelector(.left-section); const middleSection document.querySelector(.middle-section); const rightSection document.querySelector(.right-section); let isResizingH1 false; let isResizingH2 false; let startX; let startWidths; function initializeHorizontalResize(handle, isFirstHandle) { handle.addEventListener(mousedown, (e) > { e.preventDefault(); if (isFirstHandle) { isResizingH1 true; } else { isResizingH2 true; } startX e.clientX; startWidths { left: leftSection.offsetWidth, middle: middleSection.offsetWidth, right: rightSection.offsetWidth }; document.addEventListener(mousemove, handleHorizontalMove); document.addEventListener(mouseup, () > { isResizingH1 false; isResizingH2 false; document.removeEventListener(mousemove, handleHorizontalMove); }); }); } function handleHorizontalMove(e) { if (!isResizingH1 && !isResizingH2) return; const deltaX e.clientX - startX; const totalWidth leftSection.parentElement.offsetWidth; const minWidth 300; if (isResizingH1) { const newLeftWidth Math.max(minWidth, Math.min(startWidths.left + deltaX, totalWidth - 2 * minWidth)); const remainingWidth totalWidth - newLeftWidth; const newMiddleWidth remainingWidth / 2; const newRightWidth remainingWidth / 2; leftSection.style.flex `0 0 ${newLeftWidth}px`; middleSection.style.flex `0 0 ${newMiddleWidth}px`; rightSection.style.flex `0 0 ${newRightWidth}px`; } else if (isResizingH2) { const newMiddleWidth Math.max(minWidth, Math.min(startWidths.middle + deltaX, totalWidth - minWidth - leftSection.offsetWidth)); const newRightWidth totalWidth - leftSection.offsetWidth - newMiddleWidth; if (newRightWidth > minWidth) { middleSection.style.flex `0 0 ${newMiddleWidth}px`; rightSection.style.flex `0 0 ${newRightWidth}px`; } } } initializeHorizontalResize(resizeHandle1, true); initializeHorizontalResize(resizeHandle2, false); // 頁面載入時檢查 token 並載入資料 document.addEventListener(DOMContentLoaded, async () > { const tokenValid await checkToken(); if (tokenValid) { await loadDeviceMapping(); await loadUsers(); } }); // 更新計數器顯示 function updateCounter(elementId, current, total) { const counter document.getElementById(elementId); if (counter) { counter.textContent `${current}/${total}`; } } /script> script srchttps://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js>/script>/body>/html>
View on OTX
|
View on ThreatMiner
Please enable JavaScript to view the
comments powered by Disqus.
Data with thanks to
AlienVault OTX
,
VirusTotal
,
Malwr
and
others
. [
Sitemap
]