專案

一般

配置概況

工作單 #200 » P9702001-商店交易,設備統計(台幣) - Normal.json

marlboro chu, 2025-06-30 01:59

 
{
"result" : {
"id" : 623,
"orgId" : 1,
"folderId" : 143,
"folderUid" : "bempo37hjqtq8d",
"uid" : "denaxj25fumf4f",
"name" : "P9702001-商店交易,設備統計(台幣) - Normal",
"kind" : 1,
"type" : "marcusolsson-dynamictext-panel",
"description" : "",
"model" : {
"datasource" : {
"type" : "yesoreyeram-infinity-datasource",
"uid" : "aegaeyjq187b4e"
},
"description" : "",
"fieldConfig" : {
"defaults" : {
"decimals" : 0,
"thresholds" : {
"mode" : "absolute",
"steps" : [ {
"color" : "green",
"value" : null
} ]
},
"unit" : "locale"
},
"overrides" : [ {
"matcher" : {
"id" : "byName",
"options" : "deviceCollection"
},
"properties" : [ {
"id" : "unit",
"value" : "percentunit"
} ]
} ]
},
"gridPos" : {
"h" : 8,
"w" : 24,
"x" : 0,
"y" : 20
},
"id" : 9702001,
"options" : {
"afterRender" : "const merchantHeaders = [\r\n { text: '風險等級', field: 'merchantName' },\r\n { text: '交易數量', field: 'txCount' },\r\n { text: '交易金額(台幣)', field: 'txRealAmount' },\r\n { text: '授權成功數量', field: 'authResultY' },\r\n { text: '設備數量', field: 'txCountUniDevice' }\r\n];\r\n\r\nconst merchantTotals = {\r\n txCount: 0,\r\n txRealAmount: 0,\r\n authResultY: 0,\r\n txCountUniDevice: 0\r\n};\r\n\r\n\r\nfunction setupMerchantHeader() {\r\n const tableHeader = document.getElementById('merchant-table-header');\r\n tableHeader.innerHTML = '';\r\n\r\n const headerRow = document.createElement('tr');\r\n\r\n merchantHeaders.forEach(header => {\r\n const th = document.createElement('th');\r\n th.textContent = header.text;\r\n th.dataset.field = header.field;\r\n headerRow.appendChild(th);\r\n });\r\n\r\n tableHeader.appendChild(headerRow);\r\n}\r\n\r\nfunction resetMerchantTotals() {\r\n for (const key in merchantTotals) {\r\n if (Object.hasOwn(merchantTotals, key)) {\r\n merchantTotals[key] = 0;\r\n }\r\n }\r\n}\r\n\r\nfunction generateMerchantItems(data, sortField = 'merchantName', sortOrder = 'asc') {\r\n\r\n const tableBody = document.getElementById('merchant-table-body');\r\n const tableFooter = document.getElementById('merchant-table-footer');\r\n tableBody.innerHTML = '';\r\n tableFooter.innerHTML = '';\r\n resetMerchantTotals();\r\n\r\n // 排序\r\n data.sort((a, b) => {\r\n const rawA = a[sortField];\r\n const rawB = b[sortField];\r\n\r\n // 嘗試把逗號移除,轉成數字\r\n const numA = typeof rawA === 'string' ? Number(rawA.replace(/,/g, '')) : rawA;\r\n const numB = typeof rawB === 'string' ? Number(rawB.replace(/,/g, '')) : rawB;\r\n\r\n // 自訂非數值風險等級排序\r\n const RISK_ORDER_ASC = ['high', 'mid', 'low', 'nan'];\r\n const RISK_ORDER_DESC = ['low', 'mid', 'high', 'nan'];\r\n\r\n const isMerchantNameField = sortField === 'merchantName';\r\n if (isMerchantNameField) {\r\n // 將數字轉換為風險等級\r\n const mapName = val => {\r\n if (val === '1' || val === 1) return 'low';\r\n if (val === '2' || val === 2) return 'mid';\r\n if (val === '3' || val === 3) return 'high';\r\n if (!val) return 'nan';\r\n return String(val).toLowerCase();\r\n };\r\n const valA = mapName(rawA);\r\n const valB = mapName(rawB);\r\n\r\n const order = sortOrder === 'asc' ? RISK_ORDER_ASC : RISK_ORDER_DESC;\r\n const indexA = order.indexOf(valA);\r\n const indexB = order.indexOf(valB);\r\n return indexA - indexB;\r\n }\r\n\r\n\r\n if (!isNaN(numA) && !isNaN(numB)) {\r\n // 都是有效數字,就用數字比較\r\n return sortOrder === 'asc' ? numA - numB : numB - numA;\r\n } else {\r\n // 有不是數字,就當字串比\r\n return sortOrder === 'asc'\r\n ? String(rawA).localeCompare(String(rawB))\r\n : String(rawB).localeCompare(String(rawA));\r\n }\r\n });\r\n\r\n\r\n data.forEach(item => {\r\n\r\n const row = document.createElement('tr');\r\n\r\n // 風險中文轉換\r\n if (item.merchantName === '1') item.merchantName = 'low';\r\n else if (item.merchantName === '2') item.merchantName = 'mid';\r\n else if (item.merchantName === '3') item.merchantName = 'high';\r\n\r\n // 商店名稱欄位\r\n const nameCell = document.createElement('td');\r\n nameCell.textContent = item.merchantName;\r\n\r\n if (item.merchantName === 'high') {\r\n nameCell.className = 'kpi-merchant-highname';\r\n row.classList.add('kpi-merchant-highrow');\r\n } else\r\n nameCell.className = 'kpi-merchant-name';\r\n\r\n //nameCell.className = item.merchantName === 'high' ? 'kpi-merchant-highname' : 'kpi-merchant-name';\r\n row.appendChild(nameCell);\r\n\r\n // 數值欄位\r\n const fields = ['txCount', 'txRealAmount', 'authResultY', 'txCountUniDevice'];\r\n fields.forEach(field => {\r\n const raw = item[field];\r\n const num = typeof raw === 'string' ? Number(raw.replace(/,/g, '')) : raw;\r\n merchantTotals[field] += isNaN(num) ? 0 : num;\r\n\r\n const cell = document.createElement('td');\r\n cell.textContent = isNaN(num) ? '' : num.toLocaleString();\r\n cell.className = 'kpi-value';\r\n row.appendChild(cell);\r\n });\r\n\r\n tableBody.appendChild(row);\r\n });\r\n\r\n generateMerchantFooter();\r\n syncFooterColumnWidths();\r\n}\r\n\r\nfunction generateMerchantFooter() {\r\n const footerRow = document.createElement('tr');\r\n\r\n // 標題欄\r\n const labelCell = document.createElement('td');\r\n labelCell.textContent = '總計';\r\n labelCell.style.fontWeight = 'bold';\r\n footerRow.appendChild(labelCell);\r\n\r\n // 數值欄\r\n const fields = ['txCount', 'txRealAmount', 'authResultY', 'txCountUniDevice'];\r\n fields.forEach(field => {\r\n const cell = document.createElement('td');\r\n const total = merchantTotals[field];\r\n cell.textContent = total ? total.toLocaleString() : '';\r\n cell.style.fontWeight = 'bold';\r\n footerRow.appendChild(cell);\r\n });\r\n\r\n document.getElementById('merchant-table-footer').appendChild(footerRow);\r\n}\r\n\r\n// 核心:讓 footer 欄位寬度與主表一致\r\nfunction syncFooterColumnWidths() {\r\n const headerThs = document.querySelectorAll('#merchant-table thead th');\r\n const footerTds = document.querySelectorAll('.merchant-table-footer tfoot td');\r\n\r\n if (headerThs.length !== footerTds.length) return;\r\n\r\n headerThs.forEach((th, index) => {\r\n const width = th.offsetWidth;\r\n footerTds[index].style.width = `${width}px`;\r\n });\r\n}\r\n\r\n// 初始化呼叫\r\nif (Array.isArray(context.data) && context.data.length > 0 && Array.isArray(context.data[0])) {\r\n const tableData = context.data[0];\r\n setupMerchantHeader();\r\n generateMerchantItems(tableData);\r\n} else {\r\n// const elements = [...document.querySelectorAll('div, span, p')];\r\n// const target = elements.find(el => el.textContent.trim() === \"The query didn't return any results.\");\r\n\r\n// if (target) {\r\n// target.style.display = 'flex';\r\n// target.style.alignItems = 'center';\r\n// target.style.width = '100%';\r\n// target.style.textAlign = 'center';\r\n// target.textContent = '查無資訊';\r\n// } else {\r\n// //console.warn('找不到元素');\r\n // }\r\n}\r\n",
"content" : "<div class=\"merchant-table-container\">\n \n<table id=\"merchant-table\" class=\"merchant-table\">\n <thead id=\"merchant-table-header\">\n <!-- JS 會產生表頭 -->\n </thead>\n <tbody id=\"merchant-table-body\">\n <!-- Data will be populated by JavaScript -->\n </tbody>\n</table>\n<p>\n<table class=\"merchant-table-footer\">\n <tfoot id=\"merchant-table-footer\"></tfoot>\n</table>\n</div>",
"contentPartials" : [ ],
"defaultContent" : "The query didn't return any results.",
"editor" : {
"format" : "auto",
"language" : "markdown"
},
"editors" : [ "afterRender", "styles" ],
"externalStyles" : [ ],
"helpers" : "",
"renderMode" : "data",
"styles" : "<style>\r\nmerchant-table-container {\r\n width: 98%;\r\n margin: 0 auto;\r\n max-height: 180px; /* 必須設定 */\r\n overflow-y: auto; /* 讓內容可滾動 */\r\n position: relative; \r\n}\r\n\r\n/* 表格本體 */\r\n.merchant-table {\r\n border-collapse: collapse;\r\n width: 100%;\r\n table-layout: fixed;\r\n}\r\n\r\n.merchant-table-footer {\r\n\r\n position: sticky;\r\n bottom: 0;\r\n z-index: 3;\r\n pointer-events: none; /* 若不需互動 */\r\n backdrop-filter: blur(15px);\r\n}\r\n\r\n/* 固定 header 和 footer */\r\n.merchant-table thead th, .merchant-table tfoot td {\r\n position: sticky;\r\n z-index: 2;\r\n}\r\n\r\n/* 表頭固定在頂部 */\r\n.merchant-table thead th {\r\n top: 0;\r\n text-align: center;\r\n}\r\n\r\n/* 表尾固定在底部 */\r\n.merchant-table tfoot td {\r\n bottom: 0;\r\n}\r\n\r\n.merchant-table td:first-child,\r\n.merchant-table th:first-child {\r\n width: 20%;\r\n}\r\n\r\n/* 表格內容欄位 */\r\n.merchant-table tbody td {\r\n padding: 8px;\r\n}\r\n\r\n/* 筆數與百分比靠右 */\r\n.merchant-table tbody td, .merchant-table-footer tbody td, {\r\n text-align: right;\r\n font-variant-numeric: tabular-nums;\r\n}\r\n\r\n.merchant-table tbody td:first-child,\r\n.merchant-table-footer tfoot td:first-child {\r\n text-align: center;\r\n}\r\n\r\n.merchant-table-footer, .merchant-table-footer td {\r\n border: none;\r\n}\r\n.merchant-table-footer {\r\n border-top: 1px solid rgba(204, 204, 220, 0.2);;\r\n}\r\n.merchant-table td.kpi-merchant-highname {\r\n font-size: 14px;\r\n letter-spacing: 1px;\r\n text-align: center;\r\n color: red;\r\n}\r\n.merchant-table tr.kpi-merchant-highrow {\r\n border: 1px solid red;\r\n border-radius: 6px;\r\n box-shadow: 0 0 8px rgba(255, 0, 0, 0.6);\r\n}\r\n/* 滑鼠提示效果 */\r\n.merchant-table thead th {\r\n cursor: pointer;\r\n}\r\n.merchant-table td {\r\n overflow: hidden;\r\n text-overflow: ellipsis;\r\n white-space: nowrap;\r\n}\r\n</style>",
"wrap" : true
},
"pluginVersion" : "5.7.0",
"targets" : [ {
"columns" : [ ],
"datasource" : {
"type" : "yesoreyeram-infinity-datasource",
"uid" : "aegaeyjq187b4e"
},
"filters" : [ ],
"format" : "table",
"global_query_id" : "",
"parser" : "backend",
"refId" : "A",
"root_selector" : "",
"source" : "url",
"type" : "json",
"url" : "/smartfds-adm-web/report/api/decision-aggs",
"url_options" : {
"data" : "",
"method" : "GET",
"params" : [ {
"key" : "operator_id",
"value" : "${operator_id}"
}, {
"key" : "institute_id",
"value" : "${institute_id}"
}, {
"key" : "connector_id",
"value" : "${connector_id}"
}, {
"key" : "from_time",
"value" : "${__from}"
}, {
"key" : "to_time",
"value" : "${__to}"
}, {
"key" : "auth_token",
"value" : "${auth_token}"
}, {
"key" : "merchant_id",
"value" : "${merchant_id}"
} ]
}
} ],
"title" : "B-01. 商店交易,設備統計(台幣)",
"transformations" : [ {
"id" : "calculateField",
"options" : {
"alias" : "deviceCollection",
"binary" : {
"left" : {
"matcher" : {
"id" : "byName",
"options" : "txCountDevice"
}
},
"operator" : "/",
"right" : {
"matcher" : {
"id" : "byName",
"options" : "txCount"
}
}
},
"mode" : "binary",
"reduce" : {
"reducer" : "sum"
}
}
}, {
"disabled" : true,
"id" : "organize",
"options" : {
"excludeByName" : {
"txCountDevice" : false
},
"includeByName" : { },
"indexByName" : {
"Transaction Volume (with Device Info)" : 5,
"decisionHight" : 6,
"decisionMid" : 7,
"merchantName" : 0,
"txAmount" : 3,
"txCount" : 1,
"txCountDevice" : 2,
"txCountUniDevice" : 4
},
"renameByName" : {
"Transaction Volume (with Device Info)" : "設備資訊收集率%",
"decisionHight" : "High-Risk",
"decisionMid" : "Mid-Risk",
"merchantName" : "風險等級",
"txAmount" : "交易金額 (USD)",
"txCount" : "交易數量",
"txCountDevice" : "交易次數 (有設備資訊)",
"txCountUniDevice" : "設備數量"
}
}
} ],
"type" : "marcusolsson-dynamictext-panel"
},
"version" : 13,
"meta" : {
"folderName" : "R97-Library",
"folderUid" : "bempo37hjqtq8d",
"connectedDashboards" : 2,
"created" : "2025-05-29T09:51:23+08:00",
"updated" : "2025-06-12T15:56:16+08:00",
"createdBy" : {
"avatarUrl" : "/avatar/ce6412d58e966caaa26cac12eb99734b",
"id" : 1,
"name" : "admin"
},
"updatedBy" : {
"avatarUrl" : "/avatar/ce6412d58e966caaa26cac12eb99734b",
"id" : 1,
"name" : "admin"
}
}
}
}
(16-16/26)