/**
* 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}%
`
}
| 设备名称 |
device_id |
serial_number |
${currentCompanyId === 'all' ? '所属公司 | ' : ''}
固件版本 |
状态 |
电量 |
最后活跃 |
操作 |
${renderDeviceRows(filteredDevices, currentCompanyId)}
共 ${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} |
|
${escapeHtml(d.lastSeen)} |
${!isClient ? `
|
` : ''}
${!isClient ? `
|
` : ''}
|
`;
}).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[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 => `| ${escapeHtml(column)} | `).join('')}
${tableRows}
`,
};
}
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 => `| ${escapeHtml(column)} | `).join('')}
${tableRows}
`;
}
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: `
后端接入时建议先调用批量校验接口检查 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: `
`,
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();
};
});