Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/adachissss_ecological-environment-data-space.json

1 line
34 KiB
JSON

{"repo": "adachissss/ecological-environment-data-space", "files": {"public/receiver/index.js": {"ee121648c0a86c43d21f273b21df7b9492756ddc": {"t": 1785768939.3012965, "n": 31833, "h": "7c3901c66d38b85e", "c": "// 配置\n// 优先使用 config.js 的配置,否则使用当前域名\nconst API_BASE_URL = (typeof API_BASE !== 'undefined' ? API_BASE : window.location.origin);\nlet currentTab = \"animal\";\nlet currentPage = 1;\nconst pageLimit = 10;\nlet totalPages = 1;\n\n// 页面加载时初始化\ndocument.addEventListener(\"DOMContentLoaded\", () => {\n checkAuth();\n loadUserInfo();\n loadAllCounts(); // 加载所有资产统计\n loadAssets(currentTab);\n});\n\n// 检查登录状态\nfunction checkAuth() {\n const token = localStorage.getItem(\"token\");\n if (!token) {\n alert(\"请先登录\");\n window.location.href = \"../login.html\";\n return;\n }\n}\n\n// 获取 token\nfunction getToken() {\n return localStorage.getItem(\"token\");\n}\n\n// 加载用户信息\nasync function loadUserInfo() {\n try {\n const userId = localStorage.getItem(\"userId\");\n if (userId) {\n document.getElementById(\"username\").textContent = userId;\n }\n } catch (error) {\n console.error(\"加载用户信息失败:\", error);\n }\n}\n\n// 退出登录\nfunction logout() {\n localStorage.removeItem(\"token\");\n localStorage.removeItem(\"userId\");\n localStorage.removeItem(\"institution\");\n window.location.href = \"../login.html\";\n}\n\n// 加载所有资产类型的统计数字\nasync function loadAllCounts() {\n const types = ['animal', 'lake', 'air', 'plant'];\n \n for (const type of types) {\n try {\n const response = await fetch(`${API_BASE_URL}/org2/assets?type=${type}&page=1&limit=1`, {\n headers: {\n Authorization: `Bearer ${getToken()}`,\n },\n });\n\n if (response.ok) {\n const result = await response.json();\n if (result.success && result.data) {\n document.getElementById(`${type}-count`).textContent = result.data.total;\n }\n }\n } catch (error) {\n console.error(`加载${type}统计失败:`, error);\n document.getElementById(`${type}-count`).textContent = '0';\n }\n }\n}\n\n// 切换标签\nfunction switchTab(tab) {\n currentTab = tab;\n currentPage = 1;\n\n // 更新按钮状态\n const buttons = document.querySelectorAll(\".tab-btn\");\n buttons.forEach((btn) => {\n btn.classList.remove(\"active\");\n // 简单的匹配逻辑\n if (btn.textContent.toLowerCase().includes(tab.replace('-verify',''))) {\n btn.classList.add(\"active\");\n } else if (tab === 'permissions' && btn.textContent.includes('申请')) {\n btn.classList.add(\"active\");\n } else if (tab === 'hash-verify' && btn.textContent.includes('验证')) {\n btn.classList.add(\"active\");\n }\n });\n\n // 加载对应数据\n if (tab === 'permissions') {\n loadMyPermissions();\n } else if (tab === 'hash-verify') {\n loadHashVerifyTool();\n } else {\n loadAssets(tab);\n }\n}\n\n// 加载资产数据\nasync function loadAssets(type) {\n const tableContent = document.getElementById(\"table-content\");\n tableContent.innerHTML = `\n <div class=\"loading\">\n <div class=\"spinner\"></div>\n <p>正在加载数据...</p>\n </div>\n `;\n\n try {\n const response = await fetch(`${API_BASE_URL}/org2/assets?type=${type}&page=${currentPage}&limit=${pageLimit}`, {\n headers: {\n Authorization: `Bearer ${getToken()}`,\n },\n });\n\n if (response.status === 401) {\n alert(\"登录已过期,请重新登录\");\n logout();\n return;\n }\n\n const result = await response.json();\n\n if (result.success && result.data) {\n const { assets, total } = result.data;\n totalPages = Math.ceil(total / pageLimit);\n \n // 更新统计数字\n document.getElementById(`${type}-count`).textContent = total;\n \n // 渲染表格\n renderTable(assets, type);\n \n // 显示分页\n updatePagination();\n } else {\n throw new Error(result.message || '加载失败');\n }\n } catch (error) {\n console.error(\"加载资产数据失败:\", error);\n tableContent.innerHTML = `\n <div class=\"error\" style=\"padding:40px; text-align:center;\">\n <p>❌ 加载失败: ${error.message}</p>\n <button class=\"btn-secondary\" onclick=\"loadAssets('${type}')\">重试</button>\n </div>\n `;\n }\n}\n\n// 渲染表格\nfunction renderTable(assets, type) {\n const tableContent = document.getElementById(\"table-content\");\n\n if (assets.length === 0) {\n tableContent.innerHTML = `\n <div class=\"empty-state\" style=\"padding:60px; text-align:center; color:#999;\">\n <p>暂无数据 / No Data Available</p>\n </div>\n `;\n return;\n }\n\n let tableHTML = `\n <table class=\"data-table\">\n <thead>\n <tr>\n <th>ID</th>\n <th>Business ID</th>\n <th>Name</th>\n ${type === 'animal' || type === 'plant' ? '<th>Species</th>' : ''}\n <th>Permission</th>\n <th>Action</th>\n </tr>\n </thead>\n <tbody>\n `;\n\n assets.forEach(asset => {\n const permissionBadge = getPermissionBadge(asset.permissionStatus);\n tableHTML += `\n <tr>\n <td><strong>${asset.globalId}</strong></td>\n <td>${asset.businessId}</td>\n <td>${asset.name}</td>\n ${type === 'animal' || type === 'plant' ? `<td>${asset.species || '-'}</td>` : ''}\n <td>${permissionBadge}</td>\n <td>\n <button class=\"btn-view\" onclick=\"viewAssetDetail('${type}', ${asset.globalId})\">Detail</button>\n </td>\n </tr>\n `;\n });\n\n tableHTML += `\n </tbody>\n </table>\n `;\n\n tableContent.innerHTML = tableHTML;\n}\n\n// 获取权限状态徽章\nfunction getPermissionBadge(status) {\n if (status === null || status === undefined) {\n return '<span class=\"badge badge-gray\">未申请 None</span>';\n } else if (status === 0) {\n return '<span class=\"badge badge-warning\">审核中 Pending</span>';\n } else if (status === 1) {\n return '<span class=\"badge badge-success\">已通过 Approved</span>';\n } else if (status === 2) {\n return '<span class=\"badge badge-danger\">已驳回 Rejected</span>';\n }\n return '<span class=\"badge badge-gray\">Unknown</span>';\n}\n\n// 查看资产详情\nasync function viewAssetDetail(type, globalId) {\n const modal = document.getElementById(\"detailModal\");\n modal.style.display = \"flex\";\n modal.classList.add(\"show\"); // 添加 show 类触发动画\n\n // 显示加载状态\n document.getElementById(\"detail-basic-info\").innerHTML = `\n <div class=\"loading\"><div class=\"spinner\"></div><p>加载中...</p></div>\n `;\n document.getElementById(\"permission-status-section\").innerHTML = `\n <div class=\"loading\"><div class=\"spinner\"></div><p>加载中...</p></div>\n `;\n document.getElementById(\"chain-data-section\").style.display = \"none\";\n\n try {\n const response = await fetch(`${API_BASE_URL}/org2/assets/${type}/${globalId}`, {\n headers: {\n Authorization: `Bearer ${getToken()}`,\n },\n });\n\n const result = await response.json();\n\n if (result.success && result.data) {\n const asset = result.data;\n \n // 显示基本信息\n let basicInfoHTML = `\n <div class=\"info-row\">\n <span class=\"info-label\">GLOBAL ID</span>\n <span class=\"info-value\">${asset.globalId}</span>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">BUSINESS ID</span>\n <span class=\"info-value\">${asset.businessId}</span>\n </div>\n <div class=\"info-row\">\n <span class=\"info-label\">NAME</span>\n <span class=\"info-value\">${asset.name}</span>\n </div>\n `;\n \n if (asset.species) {\n basicInfoHTML += `\n <div class=\"info-row\">\n <span class=\"info-label\">SPECIES</span>\n <span class=\"info-value\">${asset.species}</span>\n </div>\n `;\n }\n \n document.getElementById(\"detail-basic-info\").innerHTML = basicInfoHTML;\n\n // 显示权限状态\n renderPermissionStatus(asset, type);\n\n } else {\n throw new Error(result.message || '加载失败');\n }\n } catch (error) {\n console.error(\"加载资产详情失败:\", error);\n document.getElementById(\"detail-basic-info\").innerHTML = `\n <p style=\"color: red;\">加载失败: ${error.message}</p>\n `;\n }\n}\n\n// 渲染权限状态\nfunction renderPermissionStatus(asset, type) {\n const section = document.getElementById(\"permission-status-section\");\n \n if (asset.permissionStatus === null || asset.permissionStatus === undefined) {\n // 未申请\n section.innerHTML = `\n <p style=\"font-size:13px; color:#666; margin-bottom:15px;\">您还未申请访问该资产的权限 / No access permission</p>\n <button class=\"btn-primary\" onclick=\"applyPermission(${asset.globalId}, '${type}')\">Apply Permission</button>\n `;\n } else if (asset.permissionStatus === 0) {\n // 审核中\n section.innerHTML = `\n <p>⏳ 权限申请审核中 / Audit Pending...</p>\n `;\n } else if (asset.permissionStatus === 1) {\n // 已通过\n section.innerHTML = `\n <p style=\"margin-bottom:10px; font-size:13px;\">✅ 权限审核已通过 / Access Granted</p>\n <button class=\"btn-primary\" onclick=\"loadChainData('${type}', '${asset.businessId}', '${asset.name || ''}')\">View Chain Data</button>\n `;\n } else if (asset.permissionStatus === 2) {\n // 已驳回\n section.innerHTML = `\n <p style=\"color:#e74c3c\">❌ 权限申请已被驳回 / Request Rejected</p>\n `;\n }\n}\n\n// 申请权限\nasync function applyPermission(globalId, type) {\n if (!confirm('Confirm to apply for access?')) {\n return;\n }\n\n try {\n const response = await fetch(`${API_BASE_URL}/org2/permission/apply`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${getToken()}`,\n },\n body: JSON.stringify({ globalId })\n });\n\n const result = await response.json();\n\n if (result.success) {\n alert('✅ ' + result.message);\n // 重新加载详情\n viewAssetDetail(type, globalId);\n } else {\n alert('❌ ' + result.message);\n }\n } catch (error) {\n console.error(\"申请权限失败:\", error);\n alert('申请失败: ' + error.message);\n }\n}\n\n// ==================== AI 功能模块 ====================\n// 保持逻辑不变,仅修改生成的HTML样式以适应黑白灰主题\n\nlet currentChainData = []; \nlet currentAssetType = '';\nlet currentBusinessId = '';\nlet conversationHistory = []; \n\n// 修改原有的 loadChainData 函数\nwindow.loadChainData = async function(type, businessId, assetName) {\n const section = document.getElementById(\"chain-data-section\");\n const content = document.getElementById(\"chain-data-content\");\n \n section.style.display = \"block\";\n content.innerHTML = `\n <div class=\"loading\">\n <div class=\"spinner\"></div>\n <p>Loading Blockchain Data...</p>\n </div>\n `;\n\n try {\n const response = await fetch(`${API_BASE_URL}/org2/chain-data/${type}/${businessId}`, {\n headers: {\n Authorization: `Bearer ${getToken()}`,\n },\n });\n\n const result = await response.json();\n\n if (result.success) {\n const chainData = result.data;\n currentChainData = chainData;\n currentAssetType = type;\n currentBusinessId = businessId;\n \n if (!chainData || chainData.length === 0) {\n content.innerHTML = `<p style=\"padding:20px; color:#999;\">暂无链上数据 / No Data on Chain</p>`;\n return;\n }\n\n // 渲染链上数据\n let dataHTML = `<div class=\"chain-data-list\">`;\n dataHTML += `<h4 style=\"margin-bottom:20px; font-family:'Playfair Display', serif;\">Blockchain Records (${chainData.length})</h4>`;\n \n chainData.forEach((item, index) => {\n const timestamp = item.Timestamp || item.timestamp || item.timestampMs ? new Date(item.timestampMs || item.timestamp).toLocaleString('zh-CN') : 'Unknown Time';\n const recordId = `record-${businessId}-${index}`;\n window[recordId] = item;\n \n dataHTML += `\n <div class=\"chain-data-item\">\n <div style=\"display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;\">\n <h5 style=\"margin:0; font-weight:600;\">Record #${index + 1} - <span style=\"font-weight:400; color:#888\">${timestamp}</span></h5>\n <button class=\"btn-export-small\" onclick=\"exportSingleRecord('${recordId}', '${businessId}', ${index + 1}, '${type}', '${(assetName || '').replace(/'/g, \"\\\\'\")}')\">\n 📥 Export\n </button>\n </div>\n <pre>${JSON.stringify(item, null, 2)}</pre>\n </div>\n `;\n });\n dataHTML += `</div>`;\n \n content.innerHTML = dataHTML;\n showAIFeatures(); // 显示 AI 按钮\n } else {\n content.innerHTML = `<p style=\"color: red;\">${result.message}</p>`;\n }\n } catch (error) {\n console.error(\"加载链上数据失败:\", error);\n content.innerHTML = `<p style=\"color: red;\">加载失败: ${error.message}</p>`;\n }\n};\n\n// 显示 AI 功能按钮(样式已修改为适应新主题)\nfunction showAIFeatures() {\n const section = document.getElementById(\"chain-data-section\");\n const existingAI = document.getElementById('ai-features-container');\n if (existingAI) existingAI.remove();\n \n const aiContainer = document.createElement('div');\n aiContainer.id = 'ai-features-container';\n // 内联样式被移除,转而在 CSS 中定义 #ai-features-container\n aiContainer.innerHTML = `\n <div style=\"display: flex; gap: 12px; align-items: center; flex-wrap: wrap;\">\n <button id=\"ai-analysis-btn\" class=\"ai-btn\">\n 🤖 AI Intelligent Analysis\n </button>\n <button id=\"ai-chat-btn\" class=\"ai-btn\">\n 💬 Ask AI Assistant\n </button>\n <span id=\"ai-loading\" style=\"display: none; color: #666; font-size: 13px; margin-left:10px;\">\n <span class=\"spinner\" style=\"width:16px; height:16px; display:inline-block; margin:0; vertical-align:middle; border-top-color:#666;\"></span> Analyzing...\n </span>\n </div>\n \n <div id=\"ai-analysis-result\" style=\"display: none; margin-top: 20px; padding: 20px; background: white; border-left: 3px solid #000; border-radius: 8px; box-shadow: 0 5px 15px rgba(0,0,0,0.05);\">\n <h4 style=\"margin: 0 0 15px 0; font-family: 'Playfair Display', serif; font-size:18px;\">\n AI Analysis Report\n </h4>\n <div id=\"ai-analysis-content\" style=\"line-height: 1.8; white-space: pre-wrap; color: #333; font-size:14px;\"></div>\n <div style=\"margin-top: 15px; padding-top: 15px; border-top: 1px solid #eee; font-size: 12px; color: #999;\">\n <span id=\"ai-tokens-info\"></span>\n </div>\n </div>\n \n <div id=\"ai-chat-interface\" style=\"display: none; margin-top: 20px;\">\n <div style=\"background: white; border-radius: 12px; box-shadow: 0 10px 30px rgba(0,0,0,0.1); overflow: hidden; border:1px solid #eee;\">\n <div style=\"padding: 15px 20px; background: #1a1a1a; color: white; display: flex; justify-content: space-between; align-items: center;\">\n <h4 style=\"margin: 0; display: flex; align-items: center; gap: 8px; font-size:14px;\">\n <span>💬</span> AI Assistant\n </h4>\n <button id=\"ai-chat-close-top\" style=\"background: rgba(255,255,255,0.2); border: none; color: white; padding: 5px 12px; border-radius: 4px; cursor: pointer; font-size:12px;\">\n ✕\n </button>\n </div>\n <div id=\"ai-chat-messages\" style=\"height: 350px; overflow-y: auto; padding: 20px; background: #f9f9f9;\">\n <div style=\"text-align: center; color: #999; padding: 40px 20px;\">\n <div style=\"font-size: 32px; margin-bottom: 10px; opacity:0.5;\">🤖</div>\n <p style=\"font-size:13px;\">Feel free to ask questions about the data.</p>\n </div>\n </div>\n <div style=\"padding: 15px; background: #fff; border-top: 1px solid #eee; display: flex; gap: 10px;\">\n <input type=\"text\" id=\"ai-chat-input\" placeholder=\"Type your question...\" style=\"flex: 1; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px;\" />\n <button id=\"ai-chat-send\" style=\"padding: 0 24px; background: #1a1a1a; color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; font-size:13px;\">\n Send\n </button>\n </div>\n </div>\n </div>\n `;\n \n section.appendChild(aiContainer);\n \n // 绑定事件\n document.getElementById('ai-analysis-btn').onclick = performAIAnalysis;\n document.getElementById('ai-chat-btn').onclick = openAIChat;\n document.getElementById('ai-chat-send').onclick = sendAIQuestion;\n document.getElementById('ai-chat-close-top').onclick = closeAIChat;\n document.getElementById('ai-chat-input').onkeypress = (e) => {\n if (e.key === 'Enter') sendAIQuestion();\n };\n}\n\n// 执行 AI 分析\nasync function performAIAnalysis() {\n const resultDiv = document.getElementById('ai-analysis-result');\n const contentDiv = document.getElementById('ai-analysis-content');\n const loadingSpan = document.getElementById('ai-loading');\n const tokensInfo = document.getElementById('ai-tokens-info');\n \n try {\n loadingSpan.style.display = 'inline-block';\n resultDiv.style.display = 'none';\n \n const response = await fetch(`${API_BASE_URL}/ai/analyze-chain-data`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${getToken()}`\n },\n body: JSON.stringify({\n assetType: currentAssetType,\n businessId: currentBusinessId,\n chainData: currentChainData\n })\n });\n \n const result = await response.json();\n \n if (result.success) {\n contentDiv.textContent = result.analysis;\n tokensInfo.textContent = `Token Usage: ${result.tokensUsed}`;\n resultDiv.style.display = 'block';\n } else {\n alert('AI Analysis Failed: ' + result.message);\n }\n \n } catch (error) {\n console.error('AI Error:', error);\n alert('Network error, please try again later.');\n } finally {\n loadingSpan.style.display = 'none';\n }\n}\n\n// 打开 AI 聊天\nfunction openAIChat() {\n const chatInterface = document.getElementById('ai-chat-interface');\n const chatMessages = document.getElementById('ai-chat-messages');\n \n conversationHistory = [];\n chatMessages.innerHTML = `\n <div style=\"text-align: center; color: #999; padding: 40px 20px;\">\n <div style=\"font-size: 32px; margin-bottom: 10px; opacity:0.5;\">🤖</div>\n <p style=\"font-size:13px;\">Feel free to ask questions about the data.</p>\n </div>\n `;\n \n chatInterface.style.display = 'block';\n document.getElementById('ai-chat-input').focus();\n chatInterface.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n}\n\nfunction closeAIChat() {\n document.getElementById('ai-chat-interface').style.display = 'none';\n}\n\n// 发送 AI 问题\nasync function sendAIQuestion() {\n const input = document.getElementById('ai-chat-input');\n const question = input.value.trim();\n \n if (!question) return;\n \n addChatMessage('user', question);\n input.value = '';\n \n const loadingId = addChatMessage('assistant', 'Thinking...');\n \n try {\n const assetNames = { 'animal': 'Animal', 'lake': 'Lake', 'plant': 'Plant', 'air': 'Air' };\n \n let context = `Asset Type: ${assetNames[currentAssetType] || currentAssetType}\\n`;\n context += `ID: ${currentBusinessId}\\n`;\n context += `Record Count: ${currentChainData.length}\\n`;\n context += `Data Summary: ${JSON.stringify(currentChainData.slice(0, 3))}`;\n \n const response = await fetch(`${API_BASE_URL}/ai/chat`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'Authorization': `Bearer ${getToken()}`\n },\n body: JSON.stringify({\n question,\n context,\n conversationHistory\n })\n });\n \n const result = await response.json();\n document.getElementById(loadingId).remove();\n \n if (result.success) {\n addChatMessage('assistant', result.answer);\n conversationHistory.push(\n { role: 'user', content: question },\n { role: 'assistant', content: result.answer }\n );\n } else {\n addChatMessage('assistant', '❌ Error: ' + result.message);\n }\n } catch (error) {\n document.getElementById(loadingId).remove();\n addChatMessage('assistant', '❌ Connection failed');\n }\n}\n\n// 添加聊天消息(适配黑白配色)\nfunction addChatMessage(role, content) {\n const chatMessages = document.getElementById('ai-chat-messages');\n const welcomeDiv = chatMessages.querySelector('div[style*=\"text-align: center\"]');\n if (welcomeDiv) welcomeDiv.remove();\n \n const messageId = 'msg-' + Date.now();\n const messageDiv = document.createElement('div');\n messageDiv.id = messageId;\n \n // 只有这里为了简便使用了内联样式,但配色已改为黑白\n const userStyle = 'background: #1a1a1a; color: white; margin-left: auto; border-radius: 12px 12px 0 12px;';\n const aiStyle = 'background: white; color: #333; margin-right: auto; border: 1px solid #eee; border-radius: 12px 12px 12px 0;';\n \n messageDiv.style.cssText = `\n margin-bottom: 15px; padding: 12px 16px; max-width: 80%; font-size:13px;\n ${role === 'user' ? userStyle : aiStyle}\n animation: fadeIn 0.3s ease-in;\n `;\n \n messageDiv.innerHTML = `\n <div style=\"font-size: 10px; opacity: 0.6; margin-bottom: 4px; font-weight: 600;\">\n ${role === 'user' ? 'ME' : 'AI'}\n </div>\n <div style=\"line-height: 1.6; white-space: pre-wrap;\">${content}</div>\n `;\n \n chatMessages.appendChild(messageDiv);\n chatMessages.scrollTop = chatMessages.scrollHeight;\n return messageId;\n}\n\n// 导出单条记录 (保持原有逻辑)\nasync function exportSingleRecord(recordId, businessId, recordIndex, assetType, assetName) {\n try {\n const record = window[recordId];\n if (!record) throw new Error('Data not found');\n const token = localStorage.getItem(\"token\");\n if (!token) { alert(\"Please log in\"); return; }\n\n // 下载JSON\n const jsonResponse = await fetch('/org2/export/single-record', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },\n body: JSON.stringify({ assetType, businessId, recordIndex, recordData: record })\n });\n\n if (!jsonResponse.ok) throw new Error('Export failed');\n const jsonBlob = await jsonResponse.blob();\n const jsonUrl = window.URL.createObjectURL(jsonBlob);\n const jsonLink = document.createElement('a');\n jsonLink.href = jsonUrl;\n jsonLink.download = `${businessId}-record-${recordIndex}.json`;\n document.body.appendChild(jsonLink);\n jsonLink.click();\n document.body.removeChild(jsonLink);\n window.URL.revokeObjectURL(jsonUrl);\n\n await new Promise(r => setTimeout(r, 500));\n\n // 下载PDF\n const pdfResponse = await fetch('/org2/export/single-record?type=certificate', {\n method: 'POST',\n headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },\n body: JSON.stringify({ assetType, businessId, recordIndex, recordData: record })\n });\n \n if (!pdfResponse.ok) throw new Error('Certificate failed');\n const pdfBlob = await pdfResponse.blob();\n const pdfUrl = window.URL.createObjectURL(pdfBlob);\n const pdfLink = document.createElement('a');\n pdfLink.href = pdfUrl;\n pdfLink.download = `certificate-${businessId}-record-${recordIndex}.pdf`;\n document.body.appendChild(pdfLink);\n pdfLink.click();\n document.body.removeChild(pdfLink);\n window.URL.revokeObjectURL(pdfUrl);\n\n alert(`✅ Record #${recordIndex} Exported Successfully!`);\n } catch (error) {\n console.error('Export failed:', error);\n alert('❌ Export Failed: ' + error.message);\n }\n}\n\n// 加载我的权限申请列表\nasync function loadMyPermissions() {\n const tableContent = document.getElementById(\"table-content\");\n tableContent.innerHTML = `<div class=\"loading\"><div class=\"spinner\"></div><p>Loading Permissions...</p></div>`;\n\n try {\n const response = await fetch(`${API_BASE_URL}/org2/permissions`, {\n headers: { Authorization: `Bearer ${getToken()}` },\n });\n const result = await response.json();\n if (result.success && result.data) {\n renderPermissionsTable(result.data);\n } else {\n throw new Error(result.message || 'Failed');\n }\n } catch (error) {\n tableContent.innerHTML = `<div class=\"error\"><p>❌ Failed: ${error.message}</p></div>`;\n }\n}\n\n// 渲染权限申请表格\nfunction renderPermissionsTable(permissions) {\n const tableContent = document.getElementById(\"table-content\");\n if (permissions.length === 0) {\n tableContent.innerHTML = `<div class=\"empty-state\" style=\"padding:40px; text-align:center; color:#999\"><p>No Applications Found</p></div>`;\n return;\n }\n\n let tableHTML = `\n <table class=\"data-table\">\n <thead>\n <tr>\n <th>App ID</th>\n <th>Type</th>\n <th>Business ID</th>\n <th>Name</th>\n <th>Status</th>\n <th>Action</th>\n </tr>\n </thead>\n <tbody>\n `;\n\n permissions.forEach(perm => {\n const permissionBadge = getPermissionBadge(perm.permissionStatus);\n tableHTML += `\n <tr>\n <td>${perm.id}</td>\n <td style=\"text-transform:capitalize\">${perm.assetType}</td>\n <td>${perm.businessId}</td>\n <td>${perm.name}</td>\n <td>${permissionBadge}</td>\n <td>\n <button class=\"btn-view\" onclick=\"viewAssetDetail('${perm.assetType}', ${perm.globalId})\">Detail</button>\n </td>\n </tr>\n `;\n });\n\n tableHTML += `</tbody></table>`;\n tableContent.innerHTML = tableHTML;\n document.getElementById(\"pagination\").style.display = \"none\";\n}\n\nfunction closeDetailModal() {\n const modal = document.getElementById(\"detailModal\");\n modal.classList.remove(\"show\"); // 移除 show 类触发淡出动画\n setTimeout(() => {\n modal.style.display = \"none\";\n }, 300);\n}\n\nfunction updatePagination() {\n const pagination = document.getElementById(\"pagination\");\n const pageInfo = document.getElementById(\"page-info\");\n const prevBtn = document.getElementById(\"prev-btn\");\n const nextBtn = document.getElementById(\"next-btn\");\n\n if (totalPages <= 1) {\n pagination.style.display = \"none\";\n return;\n }\n\n pagination.style.display = \"flex\";\n pageInfo.textContent = `Page ${currentPage} / ${totalPages}`;\n prevBtn.disabled = currentPage <= 1;\n nextBtn.disabled = currentPage >= totalPages;\n}\n\nfunction prevPage() {\n if (currentPage > 1) {\n currentPage--;\n loadAssets(currentTab);\n }\n}\n\nfunction nextPage() {\n if (currentPage < totalPages) {\n currentPage++;\n loadAssets(currentTab);\n }\n}\n\n// 哈希验证工具 (保持逻辑,美化HTML输出)\nfunction loadHashVerifyTool() {\n const tableContent = document.getElementById(\"table-content\");\n document.getElementById(\"pagination\").style.display = \"none\";\n\n tableContent.innerHTML = `\n <div class=\"hash-verify-container\">\n <div class=\"verify-section\">\n <h2 class=\"section-title\" style=\"font-size:24px; margin-bottom:10px;\">🔐 Data Integrity Verification</h2>\n <p class=\"section-desc\" style=\"color:#666; margin-bottom:30px;\">Upload the JSON file to verify its SHA-256 hash against the certificate.</p>\n \n <div class=\"upload-section\">\n <div class=\"upload-area\" id=\"upload-area\">\n <div class=\"upload-icon\">📁</div>\n <p class=\"upload-text\">Click or Drag JSON file here</p>\n <input type=\"file\" id=\"file-input\" accept=\".json\" style=\"display: none;\">\n </div>\n </div>\n\n <div id=\"result-section\" style=\"display: none;\">\n <div class=\"result-card\">\n <h3 style=\"font-family:'Playfair Display'; margin-bottom:15px;\">File Information</h3>\n <div class=\"info-row\"><span class=\"info-label\">Name:</span><span class=\"info-value\" id=\"file-name\">-</span></div>\n <div class=\"info-row\"><span class=\"info-label\">Size:</span><span class=\"info-value\" id=\"file-size\">-</span></div>\n <div class=\"info-row\"><span class=\"info-label\">Time:</span><span class=\"info-value\" id=\"calc-time\">-</span></div>\n </div>\n\n <div class=\"hash-result-card\">\n <h3 style=\"font-family:'Playfair Display'; margin-bottom:15px;\">SHA-256 Hash</h3>\n <div class=\"hash-display\" id=\"hash-value\">-</div>\n <button class=\"btn-copy\" onclick=\"copyHash()\">📋 Copy Hash</button>\n </div>\n\n <div class=\"compare-section\" style=\"margin-top:30px; text-align:left;\">\n <h3 style=\"font-family:'Playfair Display'; margin-bottom:10px;\">Compare Hash</h3>\n <p style=\"font-size:13px; color:#666; margin-bottom:10px;\">Paste certificate hash below:</p>\n <textarea id=\"compare-hash\" placeholder=\"Paste hash here (with or without 0x)...\" rows=\"2\"></textarea>\n <button class=\"btn-compare\" onclick=\"compareHash()\" style=\"width:100%; margin-top:10px;\">Verify Match</button>\n <div id=\"compare-result\" style=\"display: none; margin-top:20px;\"></div>\n </div>\n </div>\n </div>\n </div>\n `;\n\n const uploadArea = document.getElementById('upload-area');\n const fileInput = document.getElementById('file-input');\n\n uploadArea.onclick = () => fileInput.click();\n fileInput.onchange = (e) => { const file = e.target.files[0]; if (file) handleFileUpload(file); };\n\n uploadArea.ondragover = (e) => { e.preventDefault(); uploadArea.style.borderColor = '#000'; };\n uploadArea.ondragleave = (e) => { e.preventDefault(); uploadArea.style.borderColor = '#eee'; };\n uploadArea.ondrop = (e) => { e.preventDefault(); uploadArea.style.borderColor = '#eee'; const file = e.dataTransfer.files[0]; if (file) handleFileUpload(file); };\n}\n\n// 保持原有的 handleFileUpload, copyHash, compareHash, formatFileSize 函数逻辑\n// 仅微调 compareHash 的输出样式\nasync function handleFileUpload(file) {\n if (!file.name.endsWith('.json')) { alert('⚠️ Please upload JSON file'); return; }\n const startTime = Date.now();\n try {\n const fileContent = await file.text();\n let contentToHash = fileContent;\n try {\n const jsonData = JSON.parse(fileContent);\n if (jsonData.exportInfo && jsonData.exportInfo.dataHash) {\n contentToHash = JSON.stringify(jsonData.data, null, 2);\n }\n } catch (e) {}\n \n const encoder = new TextEncoder();\n const data = encoder.encode(contentToHash);\n const hashBuffer = await crypto.subtle.digest('SHA-256', data);\n const hashArray = Array.from(new Uint8Array(hashBuffer));\n const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');\n \n const calcTime = Date.now() - startTime;\n document.getElementById('result-section').style.display = 'block';\n document.getElementById('file-name').textContent = file.name;\n document.getElementById('file-size').textContent = formatFileSize(file.size);\n document.getElementById('calc-time').textContent = calcTime + ' ms';\n document.getElementById('hash-value').textContent = '0x' + hashHex;\n document.getElementById('compare-result').style.display = 'none';\n document.getElementById('compare-hash').value = '';\n document.getElementById('result-section').scrollIntoView({ behavior: 'smooth' });\n } catch (error) { alert('❌ Error: ' + error.message); }\n}\n\nfunction copyHash() {\n const hashValue = document.getElementById('hash-value').textContent;\n navigator.clipboard.writeText(hashValue).then(() => { alert('Copied!'); });\n}\n\nfunction compareHash() {\n const calculatedHash = document.getElementById('hash-value').textContent.toLowerCase();\n let inputHash = document.getElementById('compare-hash').value.trim().toLowerCase();\n if (!inputHash) { alert('Please input hash'); return; }\n if (inputHash.startsWith('0x')) inputHash = inputHash.substring(2);\n const calculatedHashClean = calculatedHash.startsWith('0x') ? calculatedHash.substring(2) : calculatedHash;\n \n const resultDiv = document.getElementById('compare-result');\n resultDiv.style.display = 'block';\n \n if (calculatedHashClean === inputHash) {\n resultDiv.innerHTML = `\n <div style=\"background:#f0fdf4; border:1px solid #15803d; padding:15px; border-radius:8px; color:#15803d;\">\n <h4 style=\"margin:0\">✅ Verification Passed</h4>\n <p style=\"margin:5px 0 0 0; font-size:13px;\">Hashes match perfectly. Data is intact.</p>\n </div>\n `;\n } else {\n resultDiv.innerHTML = `\n <div style=\"background:#fef2f2; border:1px solid #b91c1c; padding:15px; border-radius:8px; color:#b91c1c;\">\n <h4 style=\"margin:0\">❌ Verification Failed</h4>\n <p style=\"margin:5px 0 0 0; font-size:13px;\">Hash mismatch. Data might be modified.</p>\n </div>\n `;\n }\n}\n\nfunction formatFileSize(bytes) {\n if (bytes < 1024) return bytes + ' B';\n if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';\n return (bytes / (1024 * 1024)).toFixed(2) + ' MB';\n}"}}}}