/** * TalkingQ Platform - Device Management Page * 按公司分类的设备管理 */ const DEVICE_BASE_DEVICES = [ // 智慧星科技 (company_a) { id: 'c2b00f75-e7e7-581e-90f1-390d97d0dee6', name: '智慧星 #A001', companyId: 'company_a', companyName: '智慧星科技', sn: 'TQ_07DF6F77F19C0A55', fw: 'v2.3.1', status: 'online', battery: 85, lastSeen: '刚刚', region: '广东广州' }, { id: 'f13a62b9-5c2e-5bb9-9f7b-4d9a3a7c2d01', name: '智慧星 #A002', companyId: 'company_a', companyName: '智慧星科技', sn: 'TQ_0B8A6C21E94F3D10', fw: 'v2.3.0', status: 'online', battery: 62, lastSeen: '1分钟前', region: '广东深圳' }, { id: '9d7a4c26-8f13-5c2d-a764-7f90c5b118a2', name: '智慧星 #A003', companyId: 'company_a', companyName: '智慧星科技', sn: 'TQ_14C9F02A77D85B61', fw: 'v2.2.5', status: 'offline', battery: 0, lastSeen: '3天前', region: '广东广州' }, { id: 'ad0f462a-2166-5148-9af3-64e711c3df78', name: '智慧星 #A004', companyId: 'company_a', companyName: '智慧星科技', sn: 'TQ_6F91A2C8B0D44E73', fw: 'v2.3.1', status: 'error', battery: 45, lastSeen: '10分钟前', region: '广东佛山' }, { id: 'e51f9c0b-7d1a-58d4-8c03-1ac9f6b2e945', name: '智慧星 #A005', companyId: 'company_a', companyName: '智慧星科技', sn: 'TQ_A03C5E79B1D2468F', fw: 'v2.3.1', status: 'online', battery: 33, lastSeen: '5分钟前', region: '广东东莞' }, // 未来玩具厂 (company_b) { id: 'b0d5e7c2-6f3a-5a19-9c87-21b64f9e0a33', name: '未来玩具 #B001', companyId: 'company_b', companyName: '未来玩具厂', sn: 'TQ_3E6A9B12C8D074F5', fw: 'v2.0.2', status: 'online', battery: 90, lastSeen: '刚刚', region: '浙江杭州' }, { id: '0a91d4e5-2c6f-53b8-8e14-97d23c5f61a0', name: '未来玩具 #B002', companyId: 'company_b', companyName: '未来玩具厂', sn: 'TQ_D4F0219A6B8C35E7', fw: 'v2.0.0', status: 'online', battery: 55, lastSeen: '10分钟前', region: '浙江宁波' }, { id: '6cc1b8a3-9a2d-5f71-8d42-f0a7b3c9d5e2', name: '未来玩具 #B003', companyId: 'company_b', companyName: '未来玩具厂', sn: 'TQ_82A4F6D13C9E0B75', fw: 'v1.8.2', status: 'offline', battery: 0, lastSeen: '2天前', region: '浙江温州' }, { id: '4f82a7c9-b1d3-5e06-9a54-3c1d8f7b02a6', name: '未来玩具 #B004', companyId: 'company_b', companyName: '未来玩具厂', sn: 'TQ_59C0E7A31D8B246F', fw: 'v2.0.2', status: 'online', battery: 78, lastSeen: '1分钟前', region: '浙江义乌' }, // 童趣电子 (company_c) { id: '7b31f9e2-08d6-5a44-9b2c-6e1a0f93c58d', name: '童趣电子 #C001', companyId: 'company_c', companyName: '童趣电子', sn: 'TQ_B7D04A6E2C9138F5', fw: 'v1.8.2', status: 'online', battery: 72, lastSeen: '刚刚', region: '江苏苏州' }, { id: '2c9e6a41-d0b7-53f8-8a15-e6c37b9402df', name: '童趣电子 #C002', companyId: 'company_c', companyName: '童趣电子', sn: 'TQ_1F8D3B60A7C9E452', fw: 'v1.8.2', status: 'offline', battery: 0, lastSeen: '5天前', region: '江苏南京' }, { id: 'd6a0c7e8-39f4-50b2-8d91-4a6e27c5f0b3', name: '童趣电子 #C003', companyId: 'company_c', companyName: '童趣电子', sn: 'TQ_E2B8C14F09A673D5', fw: 'v1.8.2', status: 'error', battery: 15, lastSeen: '1小时前', region: '江苏无锡' }, ]; try { [ 'talkingq_added_devices_v1', 'talkingq_removed_device_ids_v1', 'talkingq_device_company_overrides_v1', ].forEach(key => localStorage.removeItem(key)); } catch (error) { console.warn('Failed to clear legacy device cache', error); } function saveStoredDevices() { // Device changes are intentionally kept in memory for the demo. } const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i; const SERIAL_NUMBER_PATTERN = /^TQ_[0-9A-F]{16}$/i; function normalizeSerialNumber(value) { return String(value || '').trim().toUpperCase(); } function normalizeDeviceRecord(device) { const deviceId = String(device.device_id || device.id || '').trim(); const serialNumber = normalizeSerialNumber(device.serial_number || device.sn); return { ...device, id: deviceId, device_id: deviceId, sn: serialNumber, serial_number: serialNumber, }; } function generateDeviceId(companyId, allDevices) { const usedIds = new Set((allDevices || []).map(device => String(device.id || device.device_id || '').toLowerCase())); if (window.crypto?.randomUUID) { let nextId = window.crypto.randomUUID(); while (usedIds.has(nextId.toLowerCase())) { nextId = window.crypto.randomUUID(); } return nextId; } let nextId = ''; do { nextId = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, char => { const value = Math.floor(Math.random() * 16); return (char === 'x' ? value : (value & 0x3) | 0x8).toString(16); }); } while (usedIds.has(nextId.toLowerCase())); return nextId; } function generateSerialNumber(allDevices) { const usedSerials = new Set((allDevices || []).map(device => normalizeSerialNumber(device.sn || device.serial_number))); let serialNumber = ''; do { const hex = Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join('').toUpperCase(); serialNumber = `TQ_${hex}`; } while (usedSerials.has(serialNumber)); return serialNumber; } window.__deviceBaseDevices = window.__deviceBaseDevices || DEVICE_BASE_DEVICES.map(device => normalizeDeviceRecord(device)); window.__deviceAddedDevices = window.__deviceAddedDevices || []; window.__deviceRemovedIds = window.__deviceRemovedIds || []; window.__deviceCompanyOverrides = window.__deviceCompanyOverrides || {}; window.__saveDeviceState = window.__saveDeviceState || saveStoredDevices; function applyDeviceCompanyOverride(device) { const override = window.__deviceCompanyOverrides?.[device.id]; return normalizeDeviceRecord(override ? { ...device, ...override } : device); } function getEffectiveDeviceCompanyId(device) { return window.__deviceCompanyOverrides?.[device.id]?.companyId || device.companyId; } window.__getAllDevices = window.__getAllDevices || function() { const removedSet = new Set(window.__deviceRemovedIds || []); return (window.__deviceBaseDevices || []) .concat(window.__deviceAddedDevices || []) .filter(device => !removedSet.has(device.id)) .map(device => applyDeviceCompanyOverride(device)); }; window.__reassignCompanyDevices = window.__reassignCompanyDevices || function(sourceCompanyId, targetCompanyId) { if (!sourceCompanyId || !targetCompanyId || sourceCompanyId === targetCompanyId) return 0; const targetCompanyName = window.__getCompanyNameById ? window.__getCompanyNameById(targetCompanyId) : targetCompanyId; let movedCount = 0; const addedIds = new Set((window.__deviceAddedDevices || []).map(device => device.id)); const updateList = list => { list.forEach(device => { if (getEffectiveDeviceCompanyId(device) === sourceCompanyId) { if (addedIds.has(device.id)) { delete window.__deviceCompanyOverrides[device.id]; } else { window.__deviceCompanyOverrides[device.id] = { companyId: targetCompanyId, companyName: targetCompanyName, }; } device.companyId = targetCompanyId; device.companyName = targetCompanyName; movedCount += 1; } }); }; updateList(window.__deviceBaseDevices || []); updateList(window.__deviceAddedDevices || []); window.__saveDeviceState?.(); return movedCount; }; window.__resetDeviceMockData = window.__resetDeviceMockData || function() { window.__deviceBaseDevices = DEVICE_BASE_DEVICES.map(device => normalizeDeviceRecord(device)); window.__deviceAddedDevices = []; window.__deviceRemovedIds = []; window.__deviceCompanyOverrides = {}; window.__saveDeviceState?.(); window.dispatchEvent(new CustomEvent('devicesChanged', { detail: { companyId: 'all', reset: true }, })); }; registerPage('device', function(container) { // 获取当前公司 const currentCompanyId = window.__getCurrentCompanyId ? window.__getCurrentCompanyId() : (window.currentCompany || 'all'); const currentCompany = window.getCurrentCompany ? window.getCurrentCompany() : { id: 'all', name: '全部公司' }; const currentRole = window.__getUserRole ? window.__getUserRole() : 'admin'; const isClient = currentRole === 'client'; const baseDevices = window.__deviceBaseDevices || []; window.__deviceAddedDevices = window.__deviceAddedDevices || []; window.__deviceRemovedIds = window.__deviceRemovedIds || []; window.__deviceCompanyOverrides = window.__deviceCompanyOverrides || {}; function getCompanyInfo(companyId) { const company = window.__getCompanyById ? window.__getCompanyById(companyId) : null; return company || { id: companyId, name: companyId, latestVersion: 'v1.0.0' }; } function getCompanyName(companyId) { return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : getCompanyInfo(companyId).name; } function getLatestFirmware(companyId) { return getCompanyInfo(companyId).latestVersion || 'v1.0.0'; } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[char])); } function escapeInlineValue(value) { return String(value ?? '') .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/\r?\n/g, ' '); } function adjustCompanyDeviceCount(companyId, delta) { const company = window.companies?.find(item => item.id === companyId); if (!company) return; company.deviceCount = Math.max(0, (company.deviceCount || 0) + delta); } function getStatusLabel(status) { return { online: '在线', offline: '离线', error: '异常' }[status] || '未知'; } function getLiveDevices() { const removedSet = new Set(window.__deviceRemovedIds || []); return (window.__deviceBaseDevices || []) .concat(window.__deviceAddedDevices || []) .filter(device => !removedSet.has(device.id)) .map(device => applyDeviceCompanyOverride(device)); } function getDefaultDeviceCompanyId(selectableCompanies) { if (currentCompanyId !== 'all') return currentCompanyId; if (window.currentCompany && window.currentCompany !== 'all') return window.currentCompany; return selectableCompanies[0]?.id || 'company_a'; } function buildDeviceCompanyField(fieldId, selectedCompanyId) { const fallbackCompanies = [ { id: 'company_a', name: '智慧星科技' }, { id: 'company_b', name: '未来玩具厂' }, { id: 'company_c', name: '童趣电子' }, ]; const selectableCompanies = (window.companies?.length ? window.companies : fallbackCompanies) .filter(company => company.id !== 'all'); const safeSelectedId = selectableCompanies.some(company => company.id === selectedCompanyId) ? selectedCompanyId : getDefaultDeviceCompanyId(selectableCompanies); const selectedCompany = getCompanyInfo(safeSelectedId); if (isClient) { return ` `; } return ` `; } function createDeviceRecord({ name, sn, companyId }, allDevicesSnapshot) { const companyInfo = getCompanyInfo(companyId); const nextId = generateDeviceId(companyId, allDevicesSnapshot); const serialNumber = normalizeSerialNumber(sn) || generateSerialNumber(allDevicesSnapshot); const displayName = name || `${companyInfo.name} #${serialNumber.slice(-4)}`; const record = normalizeDeviceRecord({ id: nextId, device_id: nextId, name: displayName, companyId, companyName: companyInfo.name, sn: serialNumber, serial_number: serialNumber, fw: getLatestFirmware(companyId), status: 'online', battery: 100, lastSeen: '刚刚', region: '-', }); allDevicesSnapshot.push(record); return record; } function syncDeviceMutation(companyId, addedCount = 0) { window.__saveDeviceState?.(); window.__syncCompanyDeviceCounts?.(); window.__refreshCompanyNavigation?.(); window.dispatchEvent(new CustomEvent('devicesChanged', { detail: { companyId, addedCount }, })); } function parseBatchDeviceInput(rawText) { const lines = String(rawText || '') .split(/\r?\n/) .map(line => line.trim()) .filter(Boolean); return lines .filter((line, index) => !(index === 0 && /设备名称|sn\s*码|SN\s*码|serial|serial_number|device_id/i.test(line))) .map((line, index) => { const parts = line.split(/[,,\t]/).map(part => part.trim()).filter(Boolean); if (parts.length === 1) { return { lineNo: index + 1, name: '', sn: parts[0] }; } return { lineNo: index + 1, name: parts[0] || '', sn: parts[1] || '' }; }); } const devices = getLiveDevices(); // 根据当前公司过滤设备 const filteredDevices = currentCompanyId === 'all' ? devices : devices.filter(d => d.companyId === currentCompanyId); // 统计信息 const stats = { total: filteredDevices.length, online: filteredDevices.filter(d => d.status === 'online').length, offline: filteredDevices.filter(d => d.status === 'offline').length, error: filteredDevices.filter(d => d.status === 'error').length, }; container.innerHTML = ` ${currentCompanyId === 'all' ? `
设备范围
全部公司设备
汇总展示所有客户公司的设备状态,支持按公司下钻管理。
设备总数${stats.total} 在线率${stats.total ? Math.round((stats.online / stats.total) * 100) : 0}%
` : `
当前公司
${escapeHtml(currentCompany.name)}
当前仅展示该公司的设备数据,新增设备会默认绑定到该公司。
最新固件${escapeHtml(getLatestFirmware(currentCompanyId))} 在线率${stats.total ? Math.round((stats.online / stats.total) * 100) : 0}%
` }
${stats.total}
设备总数
${stats.online}
在线数量
${stats.offline}
离线数量
${stats.error}
异常数量
${currentCompanyId === 'all' ? '' : ''} ${renderDeviceRows(filteredDevices, currentCompanyId)}
设备名称 device_id serial_number所属公司固件版本 状态 电量 最后活跃 操作
${filteredDevices.length} 台设备
`; lucide.createIcons({ elements: [container] }); // 渲染设备行 function renderDeviceRows(deviceList, companyId) { if (deviceList.length === 0) { return `暂无设备数据`; } return deviceList.map(d => { const safeActionId = escapeHtml(escapeInlineValue(d.id)); const safeName = escapeHtml(d.name); const safeDeviceId = escapeHtml(d.device_id || d.id); const safeSn = escapeHtml(d.sn); const safeCompanyName = escapeHtml(getCompanyName(d.companyId)); const isLatest = d.fw === 'v2.3.1' || d.fw === 'v2.0.2' || d.fw === 'v1.8.2'; const statusMap = { online: { label: '在线', class: 'badge-success' }, offline: { label: '离线', class: 'badge-warning' }, error: { label: '异常', class: 'badge-danger' }, }; const status = statusMap[d.status] || statusMap.offline; return `
${safeName}
${safeDeviceId} ${safeSn} ${companyId === 'all' ? `${safeCompanyName}` : ''} ${escapeHtml(d.fw)} ${isLatest ? '' : '(可升级)'} ${status.label}
${d.battery}%
${escapeHtml(d.lastSeen)} `; }).join(''); } // 筛选设备 window.filterDevices = function() { const searchText = document.getElementById('deviceSearchInput')?.value.toLowerCase() || ''; const statusFilter = document.getElementById('statusFilter')?.value || ''; const versionFilter = document.getElementById('versionFilter')?.value || ''; const tableBody = document.getElementById('deviceTableBody'); document.getElementById('deviceFilterEmptyRow')?.remove(); const rows = document.querySelectorAll('#deviceTableBody tr.device-data-row'); let visibleCount = 0; rows.forEach(row => { const name = row.dataset.name || ''; const sn = row.dataset.sn || ''; const deviceUid = row.dataset.deviceUid || ''; const status = row.dataset.status || ''; const version = row.dataset.version || ''; const matchSearch = !searchText || name.includes(searchText) || sn.includes(searchText) || deviceUid.includes(searchText); const matchStatus = !statusFilter || status === statusFilter; const matchVersion = !versionFilter || (versionFilter === 'latest' && (version === 'v2.3.1' || version === 'v2.0.2' || version === 'v1.8.2')) || (versionFilter === 'outdated' && version !== 'v2.3.1' && version !== 'v2.0.2' && version !== 'v1.8.2'); const visible = matchSearch && matchStatus && matchVersion; row.style.display = visible ? '' : 'none'; if (visible) visibleCount++; }); if (visibleCount === 0 && rows.length > 0 && tableBody) { const emptyRow = document.createElement('tr'); emptyRow.id = 'deviceFilterEmptyRow'; emptyRow.innerHTML = `没有匹配的设备,请调整筛选条件`; tableBody.appendChild(emptyRow); } document.getElementById('deviceCount').textContent = visibleCount; }; // 重置筛选 window.resetDeviceFilters = function() { const searchInput = document.getElementById('deviceSearchInput'); const statusSelect = document.getElementById('statusFilter'); const versionSelect = document.getElementById('versionFilter'); if (searchInput) searchInput.value = ''; if (statusSelect) statusSelect.value = ''; if (versionSelect) versionSelect.value = ''; filterDevices(); }; // 导出设备 window.exportDevices = function() { const currentScopeLabel = currentCompanyId === 'all' ? '全部公司' : currentCompany.name; const exportScopeOptions = getDeviceExportScopeOptions(); const savePickerTip = window.showSaveFilePicker ? '当前浏览器支持选择保存位置' : '当前浏览器不支持选择保存位置,将自动改为浏览器下载'; const modal = createModal({ title: '导出设备', size: 'modal-lg', content: `
${exportScopeOptions.map((option, index) => ` `).join('')}
当前公司范围:${escapeHtml(currentScopeLabel)}。导出分类不受页面搜索和表格筛选影响。
${savePickerTip}
预计导出数量 ${exportScopeOptions[0].count} 台
导出字段 device_id、设备名称、serial_number、公司、固件、状态、电量、最后活跃
`, onConfirm: () => runDeviceExport(), confirmText: '开始导出', }); const syncExportModal = () => { const destination = modal.overlay.querySelector('input[name="deviceExportDestination"]:checked')?.value || 'download'; const format = modal.overlay.querySelector('input[name="deviceExportFormat"]:checked')?.value || 'xls'; const scope = modal.overlay.querySelector('input[name="deviceExportScope"]:checked')?.value || 'all'; const emailWrap = modal.overlay.querySelector('#deviceExportEmailWrap'); const hint = modal.overlay.querySelector('#deviceExportHint'); const previewCount = modal.overlay.querySelector('#deviceExportPreviewCount'); const selectedCount = getDevicesForExportScope(scope).length; if (emailWrap) emailWrap.style.display = destination === 'email' ? '' : 'none'; if (previewCount) previewCount.textContent = `${selectedCount} 台`; if (hint) { hint.textContent = format === 'pdf' && destination !== 'email' ? 'PDF 会打开打印预览,可在系统打印窗口中保存为 PDF' : destination === 'save-as' ? savePickerTip : destination === 'email' ? '邮箱发送会走后端导出任务,当前前端先模拟任务提交' : '文件会保存到浏览器默认下载位置'; } }; modal.overlay.querySelectorAll('input[name="deviceExportDestination"], input[name="deviceExportScope"], input[name="deviceExportFormat"]').forEach(input => { input.addEventListener('change', syncExportModal); }); syncExportModal(); lucide.createIcons({ elements: [modal.overlay] }); }; function getDeviceExportScopeOptions() { return [ { value: 'all', label: '全部设备', count: filteredDevices.length }, { value: 'online', label: '正常设备', count: filteredDevices.filter(device => device.status === 'online').length }, { value: 'offline', label: '离线设备', count: filteredDevices.filter(device => device.status === 'offline').length }, { value: 'error', label: '异常设备', count: filteredDevices.filter(device => device.status === 'error').length }, ]; } function getDevicesForExportScope(scope) { if (scope === 'all') { return filteredDevices; } return filteredDevices.filter(device => device.status === scope); } function getExportRows(deviceList) { return deviceList.map(device => ({ 'device_id': device.device_id || device.id, '设备名称': device.name, 'serial_number': device.serial_number || device.sn, '所属公司': getCompanyName(device.companyId), '固件版本': device.fw, '状态': getStatusLabel(device.status), '电量': `${device.battery}%`, '最后活跃': device.lastSeen, })); } function csvCell(value) { const text = String(value ?? ''); return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text; } function buildExportFile(format, deviceList, scopeLabel) { const rows = getExportRows(deviceList); const columns = Object.keys(rows[0] || { 'device_id': '', '设备名称': '', 'serial_number': '', '所属公司': '', '固件版本': '', '状态': '', '电量': '', '最后活跃': '', }); const timestamp = new Date().toISOString().slice(0, 19).replace(/[:T]/g, '-'); const baseName = `TalkingQ设备导出_${scopeLabel}_${timestamp}`.replace(/[\\/:*?"<>|]/g, '-'); if (format === 'json') { return { filename: `${baseName}.json`, mime: 'application/json;charset=utf-8', content: JSON.stringify({ exportedAt: new Date().toISOString(), scope: scopeLabel, total: deviceList.length, devices: rows, }, null, 2), }; } if (format === 'csv') { const csv = [ columns.map(csvCell).join(','), ...rows.map(row => columns.map(column => csvCell(row[column])).join(',')), ].join('\n'); return { filename: `${baseName}.csv`, mime: 'text/csv;charset=utf-8', content: `\ufeff${csv}`, }; } if (format === 'pdf') { return { filename: `${baseName}.html`, mime: 'text/html;charset=utf-8', content: buildPrintableReport(rows, columns, scopeLabel), printOnly: true, }; } const tableRows = rows.map(row => ` ${columns.map(column => `${escapeHtml(row[column])}`).join('')} `).join(''); return { filename: `${baseName}.xls`, mime: 'application/vnd.ms-excel;charset=utf-8', content: ` ${columns.map(column => ``).join('')}${tableRows}
${escapeHtml(column)}
`, }; } function buildPrintableReport(rows, columns, scopeLabel) { const tableRows = rows.map(row => ` ${columns.map(column => `${escapeHtml(row[column])}`).join('')} `).join(''); return ` TalkingQ 设备导出报告

TalkingQ 设备导出报告

范围:${escapeHtml(scopeLabel)} · 数量:${rows.length} 台 · 时间:${new Date().toLocaleString()}
${columns.map(column => ``).join('')}${tableRows}
${escapeHtml(column)}
`; } function downloadExportFile(file) { const blob = new Blob([file.content], { type: file.mime }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = file.filename; document.body.appendChild(link); link.click(); link.remove(); setTimeout(() => URL.revokeObjectURL(url), 500); } async function saveExportFile(file) { if (!window.showSaveFilePicker) { Toast.warning('当前浏览器不支持选择保存位置,已改为浏览器下载'); downloadExportFile(file); return; } const handle = await window.showSaveFilePicker({ suggestedName: file.filename, types: [{ description: 'TalkingQ export', accept: { [file.mime.split(';')[0]]: [`.${file.filename.split('.').pop()}`] }, }], }); const writable = await handle.createWritable(); await writable.write(new Blob([file.content], { type: file.mime })); await writable.close(); } function openPrintReport(file, printWindow = window.open('', '_blank')) { if (!printWindow) { Toast.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return false; } printWindow.document.open(); printWindow.document.write(file.content); printWindow.document.close(); setTimeout(() => printWindow.print(), 300); return true; } async function runDeviceExport() { const scope = document.querySelector('input[name="deviceExportScope"]:checked')?.value || 'all'; const format = document.querySelector('input[name="deviceExportFormat"]:checked')?.value || 'xls'; const destination = document.querySelector('input[name="deviceExportDestination"]:checked')?.value || 'download'; const email = document.getElementById('deviceExportEmail')?.value.trim() || ''; const exportList = getDevicesForExportScope(scope); const currentScopeLabel = currentCompanyId === 'all' ? '全部公司' : currentCompany.name; const scopeLabelMap = { all: `${currentScopeLabel}-全部设备`, online: `${currentScopeLabel}-正常设备`, offline: `${currentScopeLabel}-离线设备`, error: `${currentScopeLabel}-异常设备`, }; const scopeLabel = scopeLabelMap[scope] || '设备列表'; if (exportList.length === 0) { Toast.warning('当前范围没有可导出的设备'); return false; } if (destination === 'email' && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { Toast.warning('请输入有效的接收邮箱'); return false; } const file = destination !== 'email' ? buildExportFile(format, exportList, scopeLabel) : null; const printWindow = file?.printOnly ? window.open('', '_blank') : null; if (file?.printOnly && !printWindow) { Toast.warning('浏览器阻止了打印窗口,请允许弹窗后重试'); return false; } try { await API.devices.export({ ...(window.__getTenantScopeParams ? window.__getTenantScopeParams() : { companyId: currentCompanyId }), scope, format, destination, email: destination === 'email' ? email : undefined, status: scope === 'all' ? undefined : scope, count: exportList.length, }); } catch (error) { if (printWindow && !printWindow.closed) printWindow.close(); Toast.error('设备列表导出失败,请稍后重试'); return false; } if (destination === 'email') { Toast.success(`导出任务已提交,完成后发送到 ${escapeHtml(email)}`); return true; } try { if (file.printOnly) { const opened = openPrintReport(file, printWindow); if (!opened) return false; Toast.success('已打开 PDF 打印预览,可选择保存为 PDF'); return true; } if (destination === 'save-as') { await saveExportFile(file); } else { downloadExportFile(file); } Toast.success(`已导出 ${exportList.length} 台设备`); } catch (error) { if (error?.name === 'AbortError') { Toast.info('已取消导出'); } else { Toast.error('文件保存失败,请稍后重试'); } return false; } }; // 添加设备弹窗 window.openAddDeviceModal = function() { if (isClient) { Toast.warning('当前角色无权添加设备'); return; } const defaultCompanyId = getDefaultDeviceCompanyId((window.companies || []).filter(company => company.id !== 'all')); const companyField = buildDeviceCompanyField('newDeviceCompany', defaultCompanyId); createModal({ title: '添加设备', content: `
${companyField}
`, onConfirm: async () => { const name = document.getElementById('newDeviceName')?.value.trim(); const sn = normalizeSerialNumber(document.getElementById('newDeviceSN')?.value); const company = document.getElementById('newDeviceCompany')?.value; const remark = document.getElementById('newDeviceRemark')?.value.trim() || ''; if (!name || !sn || !company) { Toast.warning('请填写必填项'); return false; } if (!SERIAL_NUMBER_PATTERN.test(sn)) { Toast.warning('serial_number 格式应为 TQ_ 加 16 位十六进制字符'); return false; } if (devices.some(device => normalizeSerialNumber(device.sn || device.serial_number) === sn)) { Toast.warning('serial_number 已存在,请检查后再添加'); return false; } const allDevicesSnapshot = getLiveDevices().slice(); const newDevice = createDeviceRecord({ name, sn, companyId: company }, allDevicesSnapshot); try { await API.devices.create({ ...newDevice, remark }); } catch (error) { Toast.error('设备添加失败,请稍后重试'); return false; } window.__deviceAddedDevices.push(newDevice); window.__deviceRemovedIds = window.__deviceRemovedIds.filter(id => id !== newDevice.id); Toast.success(`设备 ${escapeHtml(name)} 添加成功`); if (currentCompanyId !== 'all' && company !== currentCompanyId && window.switchCompany) { syncDeviceMutation(company, 1); window.switchCompany(company); } else { if (currentCompanyId !== 'all') window.currentCompany = company; syncDeviceMutation(company, 1); } }, confirmText: '添加', }); lucide.createIcons(); }; window.openBatchAddDeviceModal = function() { if (isClient) { Toast.warning('当前角色无权批量添加设备'); return; } const defaultCompanyId = getDefaultDeviceCompanyId((window.companies || []).filter(company => company.id !== 'all')); const companyField = buildDeviceCompanyField('batchDeviceCompany', defaultCompanyId); createModal({ title: '批量添加设备', size: 'modal-lg', content: `
${companyField}
批量导入的设备会统一绑定到该公司,新公司首次添加设备后会自动出现在设备统计与数据仪表盘中。
支持逗号、中文逗号或 Tab 分隔;serial_number 格式为 TQ_ 加 16 位十六进制字符。
后端接入时建议先调用批量校验接口检查 serial_number 唯一性,再写入设备表并按 companyId 建立归属关系。
`, confirmText: '批量添加', onConfirm: async () => { const company = document.getElementById('batchDeviceCompany')?.value; const rows = parseBatchDeviceInput(document.getElementById('batchDeviceText')?.value || ''); if (!company) { Toast.warning('请选择所属公司'); return false; } if (rows.length === 0) { Toast.warning('请填写至少一台设备'); return false; } if (rows.length > 500) { Toast.warning('单次最多批量添加 500 台设备'); return false; } const existingSnSet = new Set(getLiveDevices().map(device => normalizeSerialNumber(device.sn || device.serial_number))); const batchSnSet = new Set(); const invalidRows = []; rows.forEach(row => { const sn = normalizeSerialNumber(row.sn); const snKey = sn; if (!sn || !SERIAL_NUMBER_PATTERN.test(sn)) { invalidRows.push(`第 ${row.lineNo} 行 serial_number 格式不正确`); } else if (existingSnSet.has(snKey)) { invalidRows.push(`第 ${row.lineNo} 行 serial_number 已存在`); } else if (batchSnSet.has(snKey)) { invalidRows.push(`第 ${row.lineNo} 行 serial_number 重复`); } batchSnSet.add(snKey); }); if (invalidRows.length) { Toast.warning(invalidRows.slice(0, 3).join(';') + (invalidRows.length > 3 ? '…' : '')); return false; } const allDevicesSnapshot = getLiveDevices().slice(); const newDevices = rows.map(row => createDeviceRecord({ name: row.name.trim(), sn: normalizeSerialNumber(row.sn), companyId: company, }, allDevicesSnapshot)); try { if (API.devices.batchCreate) { await API.devices.batchCreate({ companyId: company, devices: newDevices, tenantScope: window.__getTenantScopeParams ? window.__getTenantScopeParams() : {}, }); } else { await Promise.all(newDevices.map(device => API.devices.create(device))); } } catch (error) { Toast.error('设备批量添加失败,请稍后重试'); return false; } window.__deviceAddedDevices.push(...newDevices); newDevices.forEach(device => { window.__deviceRemovedIds = window.__deviceRemovedIds.filter(id => id !== device.id); }); Toast.success(`已批量添加 ${newDevices.length} 台设备`); if (currentCompanyId !== 'all' && company !== currentCompanyId && window.switchCompany) { syncDeviceMutation(company, newDevices.length); window.switchCompany(company); } else { if (currentCompanyId !== 'all') window.currentCompany = company; syncDeviceMutation(company, newDevices.length); } }, }); lucide.createIcons(); }; // 单设备OTA升级 window.openSingleOTA = function(deviceId) { if ((window.__getUserRole ? window.__getUserRole() : 'admin') === 'client') { Toast.warning('当前角色无权访问 OTA 升级'); return; } const device = devices.find(d => d.id === deviceId); if (!device) { Toast.warning('未找到该设备'); return; } if (device?.companyId) { window.currentCompany = device.companyId; } // 跳转到OTA页面进行升级 Toast.info(`准备升级 ${escapeHtml(device.name)},当前版本: ${escapeHtml(device.fw)}`); navigateTo('ota'); }; // 查看设备详情 window.viewDeviceDetail = function(deviceId) { const device = devices.find(d => d.id === deviceId); if (!device) { Toast.warning('未找到该设备'); return; } const safeDevice = { id: escapeHtml(device.id), name: escapeHtml(device.name), deviceId: escapeHtml(device.device_id || device.id), serialNumber: escapeHtml(device.serial_number || device.sn), companyName: escapeHtml(getCompanyName(device.companyId)), fw: escapeHtml(device.fw), lastSeen: escapeHtml(device.lastSeen), }; createModal({ title: `设备详情 - ${safeDevice.name}`, content: `
${safeDevice.deviceId}
${safeDevice.name}
${safeDevice.serialNumber}
${safeDevice.companyName}
${safeDevice.fw}
${device.status === 'online' ? '在线' : device.status === 'error' ? '异常' : '离线'}
${device.battery}%
${safeDevice.lastSeen}
`, confirmText: '关闭', showCancel: false, }); }; // 删除设备 window.deleteDevice = function(deviceId) { const device = devices.find(d => d.id === deviceId); if (!device) return; const safeName = escapeHtml(device.name); createModal({ title: '确认删除', confirmClass: 'btn-danger', content: `

确认删除设备 ${safeName}?

设备将从当前列表移除,并无法再接收 OTA 升级。

`, onConfirm: async () => { try { await API.devices.delete(deviceId); } catch (error) { Toast.error('设备删除失败,请稍后重试'); return false; } if (!window.__deviceRemovedIds.includes(deviceId)) { window.__deviceRemovedIds.push(deviceId); adjustCompanyDeviceCount(device.companyId, -1); } syncDeviceMutation(device.companyId, -1); Toast.success(`设备 ${safeName} 已删除`); }, confirmText: '确认删除', }); lucide.createIcons(); }; });