/** * TalkingQ Platform - OTA Upgrade Page * 按公司分类的OTA升级管理 */ const OTA_INITIAL_FIRMWARES = { company_a: { latestVersion: 'v2.3.1', versions: [ { version: 'v2.3.1', date: '2026-05-10', size: '24.5 MB', changelog: '性能优化,修复已知问题', uploaded: false }, { version: 'v2.3.0', date: '2026-04-28', size: '23.8 MB', changelog: '新增语音识别功能', uploaded: false }, { version: 'v2.2.5', date: '2026-03-15', size: '22.1 MB', changelog: '稳定性修复', uploaded: false }, ], }, company_b: { latestVersion: 'v2.0.2', versions: [ { version: 'v2.0.2', date: '2026-05-12', size: '20.2 MB', changelog: 'Bug修复版本', uploaded: false }, { version: 'v2.0.0', date: '2026-04-01', size: '19.8 MB', changelog: '全新界面设计', uploaded: false }, { version: 'v1.8.2', date: '2026-02-20', size: '18.5 MB', changelog: '基础功能版本', uploaded: false }, ], }, company_c: { latestVersion: 'v1.8.2', versions: [ { version: 'v1.8.2', date: '2026-04-15', size: '18.5 MB', changelog: '安全补丁', uploaded: false }, { version: 'v1.8.0', date: '2026-03-01', size: '18.2 MB', changelog: '功能更新', uploaded: false }, { version: 'v1.7.5', date: '2026-01-10', size: '17.8 MB', changelog: '初始版本', uploaded: false }, ], }, }; const OTA_INITIAL_TASKS = [ { id: 'OTA-001', name: '智慧星-批量升级', companyId: 'company_a', companyName: '智慧星科技', targetVersion: 'v2.3.1', strategy: 'all', status: 'in_progress', progress: 68, total: 3200, success: 2176, failed: 12, createdAt: '2026-05-12 10:30', note: '全量灰度升级' }, { id: 'OTA-002', name: '智慧星-指定版本推送', companyId: 'company_a', companyName: '智慧星科技', targetVersion: 'v2.3.0', strategy: 'version', strategyValue: 'v2.2.5', status: 'completed', progress: 100, total: 150, success: 148, failed: 2, createdAt: '2026-05-10 14:20', note: '修复旧版本语音识别问题' }, { id: 'OTA-003', name: '未来玩具-全量升级', companyId: 'company_b', companyName: '未来玩具厂', targetVersion: 'v2.0.2', strategy: 'all', status: 'in_progress', progress: 35, total: 1500, success: 525, failed: 3, createdAt: '2026-05-12 09:00', note: '新版 UI 推送' }, { id: 'OTA-004', name: '童趣-安全补丁', companyId: 'company_c', companyName: '童趣电子', targetVersion: 'v1.8.2', strategy: 'outdated', status: 'completed', progress: 100, total: 800, success: 795, failed: 5, createdAt: '2026-05-11 16:00', note: '安全补丁' }, { id: 'OTA-005', name: '未来玩具-异常设备补发', companyId: 'company_b', companyName: '未来玩具厂', targetVersion: 'v2.0.2', strategy: 'specific', status: 'failed', progress: 42, total: 80, success: 33, failed: 47, createdAt: '2026-05-13 11:10', note: '部分设备下载固件失败,需要重试' }, ]; registerPage('ota', function(container) { const currentCompanyId = window.__getCurrentCompanyId ? window.__getCurrentCompanyId() : (window.currentCompany || 'all'); const currentCompany = window.getCurrentCompany ? window.getCurrentCompany() : { id: 'all', name: '全部公司' }; const companies = (window.companies?.length ? window.companies : [ { id: 'company_a', name: '智慧星科技', deviceCount: 3200, latestVersion: 'v2.3.1' }, { id: 'company_b', name: '未来玩具厂', deviceCount: 1500, latestVersion: 'v2.0.2' }, { id: 'company_c', name: '童趣电子', deviceCount: 800, latestVersion: 'v1.8.2' }, ]).filter(company => company.id !== 'all'); window.__otaFirmwareStore = window.__otaFirmwareStore || JSON.parse(JSON.stringify(OTA_INITIAL_FIRMWARES)); window.__otaTaskStore = window.__otaTaskStore || JSON.parse(JSON.stringify(OTA_INITIAL_TASKS)); const firmwareStore = window.__otaFirmwareStore; const otaTasks = window.__otaTaskStore; const currentFirmware = currentCompanyId === 'all' ? null : firmwareStore[currentCompanyId]; const filteredTasks = currentCompanyId === 'all' ? otaTasks : otaTasks.filter(task => task.companyId === currentCompanyId); function getDisplayCompanyName(companyId, fallbackName) { return window.__getCompanyNameById ? window.__getCompanyNameById(companyId) : fallbackName; } const stats = { total: filteredTasks.length, inProgress: filteredTasks.filter(task => task.status === 'in_progress').length, completed: filteredTasks.filter(task => task.status === 'completed').length, failed: filteredTasks.filter(task => task.status === 'failed').length, stopped: filteredTasks.filter(task => task.status === 'stopped').length, }; container.innerHTML = ` ${currentCompanyId === 'all' ? `
OTA 范围
全部公司升级任务
统一查看所有客户公司的固件任务进度,选择公司可进入对应版本管理。
任务总数${stats.total}
` : `
当前公司 OTA
${escapeHtml(currentCompany.name)}
升级任务会作用于该公司设备范围,当前最新固件为 ${escapeHtml(currentFirmware?.latestVersion || '-')}。
最新固件${escapeHtml(currentFirmware?.latestVersion || '-')}
` }
${stats.total}
升级任务
${stats.inProgress}
进行中
${stats.completed}
已完成
${stats.failed}
失败
${stats.stopped}
已停止
${currentFirmware ? renderFirmwarePanel(currentCompanyId, currentCompany.name, currentFirmware) : renderFirmwareOverview()} ${renderTaskTable(filteredTasks)} `; lucide.createIcons({ elements: [container] }); function renderFirmwareOverview() { return `
固件概览 选择公司可查看全部可用固件版本
${companies.map(company => { const firmware = firmwareStore[company.id]; return `
${escapeHtml(company.name)}
最新版本
${escapeHtml(firmware?.latestVersion || '-')}
`; }).join('')}
`; } function renderFirmwarePanel(companyId, companyName, firmware) { const versions = sortFirmwareVersions(firmware.versions, firmware.latestVersion); return `
可用固件版本 最新: ${escapeHtml(firmware.latestVersion)}
设为最新版本默认只更新版本基线;如需同步升级设备,可在确认弹窗中选择升级范围。
${versions.map(version => { const isLatest = version.version === firmware.latestVersion; return `
${escapeHtml(version.version)}${isLatest ? '(最新)' : ''} ${escapeHtml(version.size)}
发布日期:${escapeHtml(version.date)}
${escapeHtml(version.changelog || '-')}
${version.uploaded ? `文件:${escapeHtml(version.filename || '-')}` : `${escapeHtml(companyName)} 固件`}
`; }).join('')}
`; } function renderTaskTable(taskList) { return `
升级任务列表 共 ${taskList.length} 个任务
${currentCompanyId === 'all' ? '' : ''} ${taskList.length === 0 ? ` ` : taskList.map(renderTaskRow).join('')}
任务名称所属公司目标版本 升级策略 状态 进度 结果 创建时间 操作
暂无升级任务
`; } function renderTaskRow(task) { const status = getStatusMeta(task.status); return `
${escapeHtml(task.name)}
ID: ${escapeHtml(task.id)}
${currentCompanyId === 'all' ? `${escapeHtml(getDisplayCompanyName(task.companyId, task.companyName))}` : ''} ${escapeHtml(task.targetVersion)} ${escapeHtml(formatStrategy(task))} ${status.label}
${task.progress}%
成功 ${task.success} ${task.failed > 0 ? ` / 失败 ${task.failed}` : ''} ${escapeHtml(task.createdAt)} ${renderTaskActions(task)} `; } function renderTaskActions(task) { const safeId = escapeInlineValue(task.id); const actions = [``]; if (task.status === 'in_progress') { actions.push(``); actions.push(``); } if (task.status === 'failed') { actions.push(``); } if (task.status === 'stopped') { actions.push(``); } if (task.status === 'completed' && task.failed > 0) { actions.push(``); } return ``; } function formatStrategy(task) { const strategyMap = { all: '全部设备', version: `版本 ${task.strategyValue || '-'}`, outdated: '过期版本', specific: '指定设备', }; if (task.strategy === 'specific' && task.deviceIds?.length) { return `指定设备(${task.deviceIds.length} 台)`; } return strategyMap[task.strategy] || task.strategy; } window.switchOTACompany = function(companyId) { if (window.switchCompany) { window.switchCompany(companyId); return; } window.currentCompany = companyId; navigateTo('ota'); }; window.openCreateOTATask = function() { const defaultCompanyId = currentCompanyId === 'all' ? companies[0]?.id : currentCompanyId; const companyField = currentCompanyId === 'all' ? ` ` : ` `; const modal = createModal({ title: '创建升级任务', size: 'modal-lg', content: `
${companyField}
当前最新版本会优先展示;设为最新版本不会自动升级设备。
预计目标设备 -
接口:POST /api/v1/ota/tasks
`, onConfirm: async () => createOTATaskFromModal(), confirmText: '创建任务', }); const companySelect = modal.overlay.querySelector('#otaCompanySelect'); const strategySelect = modal.overlay.querySelector('#otaStrategy'); const deviceIds = modal.overlay.querySelector('#deviceIds'); const refreshForm = () => { const companyId = companySelect?.value || defaultCompanyId; const strategy = strategySelect?.value || 'all'; const firmware = firmwareStore[companyId]; const targetSelect = modal.overlay.querySelector('#otaTargetVersion'); const deviceGroup = modal.overlay.querySelector('#deviceIdGroup'); const preview = modal.overlay.querySelector('#otaTargetPreview'); if (targetSelect) { targetSelect.innerHTML = firmware?.versions?.length ? sortFirmwareVersions(firmware.versions, firmware.latestVersion).map(item => ` `).join('') : ''; } if (deviceGroup) deviceGroup.style.display = strategy === 'specific' ? '' : 'none'; if (preview) { const ids = parseDeviceIds(deviceIds?.value || ''); preview.textContent = `${estimateTargetCount(companyId, strategy, ids)} 台`; } }; companySelect?.addEventListener('change', refreshForm); strategySelect?.addEventListener('change', refreshForm); deviceIds?.addEventListener('input', refreshForm); refreshForm(); lucide.createIcons({ elements: [modal.overlay] }); }; async function createOTATaskFromModal() { const name = document.getElementById('otaTaskName')?.value.trim(); const companyId = document.getElementById('otaCompanySelect')?.value; const version = document.getElementById('otaTargetVersion')?.value; const strategy = document.getElementById('otaStrategy')?.value || 'all'; const deviceIds = parseDeviceIds(document.getElementById('deviceIds')?.value || ''); const note = document.getElementById('otaTaskNote')?.value.trim() || ''; const company = getCompanyInfo(companyId); const targetCount = estimateTargetCount(companyId, strategy, deviceIds); if (!name || !companyId || !version) { Toast.warning('请填写必填项'); return false; } if (strategy === 'specific' && deviceIds.length === 0) { Toast.warning('请输入 device_id'); return false; } if (strategy === 'specific' && deviceIds.some(id => !isDeviceId(id))) { Toast.warning('device_id 需为 UUID 格式'); return false; } if (!firmwareStore[companyId]?.versions?.some(item => item.version === version)) { Toast.warning('目标固件版本不存在'); return false; } const newTask = { id: generateTaskId(), name, companyId, companyName: company.name, targetVersion: version, strategy, status: 'in_progress', progress: 0, total: targetCount, success: 0, failed: 0, createdAt: formatDateTime(new Date()), deviceIds, note, }; try { await API.ota.create(newTask); } catch (error) { Toast.error('升级任务创建失败,请稍后重试'); return false; } otaTasks.unshift(newTask); Toast.success('升级任务创建成功'); navigateTo('ota'); } window.openUploadFirmware = function(preselectedCompanyId) { const defaultCompanyId = preselectedCompanyId || (currentCompanyId === 'all' ? companies[0]?.id : currentCompanyId); const companyField = currentCompanyId === 'all' && !preselectedCompanyId ? ` ` : ` `; const modal = createModal({ title: '上传新固件', size: 'modal-lg', content: `
${companyField}
点击选择文件或拖拽到此处
支持 .bin, .hex, .zip 格式,最大 100MB
接口:POST /api/v1/ota/firmwares
`, onConfirm: async () => uploadFirmwareFromModal(), confirmText: '上传', }); const fileInput = modal.overlay.querySelector('#fwFile'); const dropzone = modal.overlay.querySelector('#fwDropzone'); const fileName = modal.overlay.querySelector('#fwFileName'); const showFile = () => { const file = fileInput?.files?.[0]; if (fileName) fileName.textContent = file ? `已选择:${file.name}(${formatFileSize(file.size)})` : ''; }; dropzone?.addEventListener('click', () => fileInput?.click()); fileInput?.addEventListener('change', showFile); dropzone?.addEventListener('dragover', event => { event.preventDefault(); dropzone.style.borderColor = 'var(--primary)'; }); dropzone?.addEventListener('dragleave', () => { dropzone.style.borderColor = 'var(--border)'; }); dropzone?.addEventListener('drop', event => { event.preventDefault(); dropzone.style.borderColor = 'var(--border)'; if (event.dataTransfer?.files?.[0] && fileInput) { fileInput.files = event.dataTransfer.files; showFile(); } }); lucide.createIcons({ elements: [modal.overlay] }); }; async function uploadFirmwareFromModal() { const companyId = document.getElementById('fwCompany')?.value; const version = document.getElementById('fwVersion')?.value.trim(); const file = document.getElementById('fwFile')?.files?.[0]; const changelog = document.getElementById('fwChangelog')?.value.trim() || '新固件版本'; if (!companyId || !version) { Toast.warning('请填写固件版本和所属公司'); return false; } if (!/^v?\d+\.\d+\.\d+([-.][A-Za-z0-9]+)?$/.test(version)) { Toast.warning('固件版本格式应类似 v2.3.2'); return false; } const normalizedVersion = version.startsWith('v') ? version : `v${version}`; if (!file) { Toast.warning('请选择固件文件'); return false; } if (!/\.(bin|hex|zip)$/i.test(file.name)) { Toast.warning('固件文件仅支持 .bin、.hex、.zip 格式'); return false; } if (file.size > 100 * 1024 * 1024) { Toast.warning('固件文件不能超过 100MB'); return false; } if (firmwareStore[companyId]?.versions?.some(item => item.version.toLowerCase() === normalizedVersion.toLowerCase())) { Toast.warning('该固件版本已存在'); return false; } const newFirmware = { version: normalizedVersion, date: formatDateOnly(new Date()), size: formatFileSize(file.size), changelog, uploaded: true, filename: file.name, }; try { await API.ota.uploadFirmware(file, { companyId, version: normalizedVersion, changelog }); } catch (error) { Toast.error('固件上传失败,请稍后重试'); return false; } firmwareStore[companyId] = firmwareStore[companyId] || { latestVersion: normalizedVersion, versions: [] }; firmwareStore[companyId].versions.unshift(newFirmware); firmwareStore[companyId].latestVersion = normalizedVersion; updateCompanyLatestVersion(companyId, normalizedVersion); Toast.success(`固件 ${escapeHtml(normalizedVersion)} 上传成功`); if (currentCompanyId === 'all' && window.switchCompany) { window.switchCompany(companyId); } else { if (currentCompanyId === 'all') window.currentCompany = companyId; navigateTo('ota'); } } window.setLatestFirmware = function(companyId, version) { const firmware = firmwareStore[companyId]; const target = firmware?.versions?.find(item => item.version === version); if (!target) { Toast.warning('固件版本不存在'); return; } if (firmware.latestVersion === version) { Toast.info(`${escapeHtml(version)} 已经是最新版本`); return; } const company = getCompanyInfo(companyId); const currentLatest = firmware.latestVersion || '-'; const defaultTaskName = `${company.name}-${version}-版本升级`; const modal = createModal({ title: '设为最新版本', size: 'modal-lg', content: `
将 ${escapeHtml(version)} 设为 ${escapeHtml(company.name)} 的最新版本
此操作会更新该公司的最新固件基线;是否同时升级设备由下方升级范围决定。
当前最新 ${escapeHtml(currentLatest)}
调整为 ${escapeHtml(version)}
是否同时更新设备
接口:仅设为最新 POST /api/v1/ota/firmwares/{id}/set-latest;同时升级 POST /api/v1/ota/firmwares/{id}/set-latest-and-upgrade
`, confirmText: '确认操作', onConfirm: async () => { const mode = modal.overlay.querySelector('input[name="latestUpgradeMode"]:checked')?.value || 'none'; const shouldCreateTask = mode !== 'none'; const deviceIds = parseDeviceIds(modal.overlay.querySelector('#latestSpecificDeviceIds')?.value || ''); const taskName = modal.overlay.querySelector('#latestUpgradeTaskName')?.value.trim() || defaultTaskName; const note = modal.overlay.querySelector('#latestUpgradeNote')?.value.trim() || `由设为最新版本操作创建,原最新版本 ${currentLatest}`; const targetCount = estimateTargetCount(companyId, mode, deviceIds); if (mode === 'specific' && deviceIds.length === 0) { Toast.warning('请输入需要升级的 device_id'); return false; } if (mode === 'specific' && deviceIds.some(id => !isDeviceId(id))) { Toast.warning('device_id 需为 UUID 格式'); return false; } if (shouldCreateTask && !taskName) { Toast.warning('请填写升级任务名称'); return false; } const upgradeTask = shouldCreateTask ? { id: generateTaskId(), name: taskName, companyId, companyName: company.name, targetVersion: version, strategy: mode, status: 'in_progress', progress: 0, total: targetCount, success: 0, failed: 0, createdAt: formatDateTime(new Date()), deviceIds, note, source: 'set_latest_version', previousLatestVersion: currentLatest, } : null; try { if (shouldCreateTask) { await API.ota.setLatestAndCreateTask(`${companyId}:${version}`, { companyId, version, previousLatestVersion: currentLatest, upgradeTask, }); } else { await API.ota.setLatestFirmware(`${companyId}:${version}`, { companyId, version, previousLatestVersion: currentLatest, }); } } catch (error) { Toast.error(shouldCreateTask ? '最新版本设置或升级任务创建失败,请稍后重试' : '最新版本设置失败,请稍后重试'); return false; } firmware.latestVersion = version; updateCompanyLatestVersion(companyId, version); if (upgradeTask) { otaTasks.unshift(upgradeTask); Toast.success(`${escapeHtml(version)} 已设为最新版本,并已创建升级任务`); } else { Toast.success(`${escapeHtml(version)} 已设为最新版本`); } navigateTo('ota'); }, }); const refreshLatestUpgradeForm = () => { const mode = modal.overlay.querySelector('input[name="latestUpgradeMode"]:checked')?.value || 'none'; const shouldCreateTask = mode !== 'none'; const config = modal.overlay.querySelector('#latestUpgradeConfig'); const specificWrap = modal.overlay.querySelector('#latestSpecificDevicesWrap'); const preview = modal.overlay.querySelector('#latestUpgradePreview'); const ids = parseDeviceIds(modal.overlay.querySelector('#latestSpecificDeviceIds')?.value || ''); modal.overlay.querySelectorAll('.ota-upgrade-option').forEach(option => { option.classList.toggle('active', option.dataset.upgradeOption === mode); }); if (config) config.style.display = shouldCreateTask ? '' : 'none'; if (specificWrap) specificWrap.style.display = mode === 'specific' ? '' : 'none'; if (preview) { preview.textContent = shouldCreateTask ? `${estimateTargetCount(companyId, mode, ids)} 台设备` : '不创建升级任务'; } }; modal.overlay.querySelectorAll('input[name="latestUpgradeMode"]').forEach(input => { input.addEventListener('change', refreshLatestUpgradeForm); }); modal.overlay.querySelector('#latestSpecificDeviceIds')?.addEventListener('input', refreshLatestUpgradeForm); refreshLatestUpgradeForm(); lucide.createIcons({ elements: [modal.overlay] }); }; window.deleteFirmware = function(companyId, version) { const firmware = firmwareStore[companyId]; if (!firmware) return; const target = firmware.versions.find(item => item.version === version); if (!target) return; if (firmware.latestVersion === version) { Toast.warning('最新固件不能删除,请先设置其他版本为最新'); return; } createModal({ title: '确认删除固件', content: `

确认删除固件 ${escapeHtml(version)}?

删除后将不能再创建指向该版本的新升级任务

`, onConfirm: async () => { try { await API.ota.deleteFirmware(`${companyId}:${version}`); } catch (error) { Toast.error('固件删除失败,请稍后重试'); return false; } firmware.versions = firmware.versions.filter(item => item.version !== version); Toast.success(`固件 ${escapeHtml(version)} 已删除`); navigateTo('ota'); }, confirmText: '确认删除', }); lucide.createIcons(); }; window.refreshOTATask = async function(taskId) { const task = otaTasks.find(item => item.id === taskId); if (!task) { Toast.warning('未找到该任务'); return; } if (task.status !== 'in_progress') { Toast.info('该任务已结束'); return; } try { await API.ota.getProgress(taskId); } catch (error) { Toast.error('进度刷新失败,请稍后重试'); return; } const nextProgress = Math.min(100, task.progress + Math.max(8, Math.round((100 - task.progress) * 0.22))); const processed = Math.round(task.total * nextProgress / 100); const newFailures = nextProgress >= 100 ? 0 : Math.floor(Math.random() * 3); task.progress = nextProgress; task.failed = Math.min((task.failed || 0) + newFailures, processed); task.success = Math.max(0, Math.min(task.total, processed - task.failed)); if (nextProgress >= 100) { task.status = 'completed'; task.success = Math.max(0, task.total - task.failed); } Toast.success(task.status === 'completed' ? '升级任务已完成' : '任务进度已刷新'); navigateTo('ota'); }; window.retryOTATask = async function(taskId, mode) { const task = otaTasks.find(item => item.id === taskId); if (!task) { Toast.warning('未找到该任务'); return; } if (mode === 'failed-devices') { if (task.failed <= 0) { Toast.info('该任务没有失败设备'); return; } const retryTask = { id: generateTaskId(), name: `${task.name}-失败设备重试`, companyId: task.companyId, companyName: getDisplayCompanyName(task.companyId, task.companyName), targetVersion: task.targetVersion, strategy: 'specific', status: 'in_progress', progress: 0, total: task.failed, success: 0, failed: 0, createdAt: formatDateTime(new Date()), deviceIds: [], note: `来自 ${task.id} 的失败设备重试`, retryOf: task.id, }; try { await API.ota.create(retryTask); } catch (error) { Toast.error('失败设备重试任务创建失败,请稍后重试'); return; } otaTasks.unshift(retryTask); Toast.success('失败设备重试任务已创建'); navigateTo('ota'); return; } try { if (mode === 'resume' && API.ota.resume) { await API.ota.resume(taskId); } else { await API.ota.create({ retryOf: taskId, mode }); } } catch (error) { Toast.error('任务重试失败,请稍后重试'); return; } task.status = 'in_progress'; if (mode === 'failed') { task.progress = 0; task.success = 0; task.failed = 0; task.createdAt = formatDateTime(new Date()); task.note = `${task.note || ''} 已重新发起失败任务重试`.trim(); } Toast.success(mode === 'resume' ? '升级任务已继续' : '升级任务已重新发起'); navigateTo('ota'); }; window.viewOTATask = function(taskId) { const task = otaTasks.find(item => item.id === taskId); if (!task) { Toast.warning('未找到该任务'); return; } const status = getStatusMeta(task.status); createModal({ title: `任务详情 - ${escapeHtml(task.name)}`, size: 'modal-lg', content: `
${escapeHtml(task.id)}
${escapeHtml(getDisplayCompanyName(task.companyId, task.companyName))}
${escapeHtml(task.targetVersion)}
${escapeHtml(formatStrategy(task))}
${escapeHtml(task.createdAt)}
${status.label}
升级进度 ${task.progress}%
${task.total}
目标设备
${task.success}
升级成功
${task.failed}
升级失败
任务日志
${renderTaskLogs(task)}
`, confirmText: '关闭', showCancel: false, }); lucide.createIcons(); }; window.stopOTATask = function(taskId) { const task = otaTasks.find(item => item.id === taskId); if (!task) { Toast.warning('未找到该任务'); return; } createModal({ title: '确认停止', content: `

确认停止升级任务 ${escapeHtml(task.name)}?

已升级的设备不受影响,未升级的设备将不再升级

`, onConfirm: async () => { try { await API.ota.cancel(taskId); } catch (error) { Toast.error('停止任务失败,请稍后重试'); return false; } task.status = 'stopped'; task.progress = Math.max(task.progress, Math.round(task.success / Math.max(task.total, 1) * 100)); Toast.success('升级任务已停止'); navigateTo('ota'); }, confirmText: '确认停止', }); lucide.createIcons(); }; function renderTaskLogs(task) { const rows = [ `${escapeHtml(task.createdAt)} 创建任务,目标版本 ${escapeHtml(task.targetVersion)}`, `${escapeHtml(task.createdAt)} 下发升级策略:${escapeHtml(formatStrategy(task))}`, ]; if (task.source === 'set_latest_version') { rows.push(`任务来源:设为最新版本操作${task.previousLatestVersion ? `,原最新版本 ${escapeHtml(task.previousLatestVersion)}` : ''}`); } if (task.note) rows.push(`升级说明:${escapeHtml(task.note)}`); if (task.progress > 0) rows.push(`当前已处理 ${task.success + task.failed}/${task.total} 台设备`); if (task.status === 'completed') rows.push('任务已完成,等待设备心跳确认最终版本'); if (task.status === 'stopped') rows.push('任务已停止,未升级设备不会继续推送'); if (task.failed > 0) rows.push(`发现 ${task.failed} 台设备升级失败,可在后端接入后查看失败明细`); return rows.map(row => `
${row}
`).join(''); } function getStatusMeta(status) { const map = { in_progress: { label: '进行中', className: 'badge-warning' }, completed: { label: '已完成', className: 'badge-success' }, failed: { label: '失败', className: 'badge-danger' }, stopped: { label: '已停止', className: 'badge-default' }, }; return map[status] || { label: status || '未知', className: 'badge-default' }; } function sortFirmwareVersions(versions, latestVersion) { return [...(versions || [])].sort((a, b) => { if (a.version === latestVersion) return -1; if (b.version === latestVersion) return 1; return new Date(b.date) - new Date(a.date); }); } function parseDeviceIds(value) { return value .split(/[\n,,\s]+/) .map(item => item.trim().toLowerCase()) .filter(Boolean); } function isDeviceId(value) { return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(String(value || '').trim()); } function estimateTargetCount(companyId, strategy, deviceIds) { const deviceCount = getCompanyInfo(companyId).deviceCount || 100; if (strategy === 'specific') return deviceIds.length; if (strategy === 'outdated') return Math.max(1, Math.round(deviceCount * 0.28)); return deviceCount; } function getCompanyInfo(companyId) { return companies.find(company => company.id === companyId) || { id: companyId, name: companyId || '未知公司', deviceCount: 100 }; } function updateCompanyLatestVersion(companyId, version) { const company = window.companies?.find(item => item.id === companyId); if (company) company.latestVersion = version; } function generateTaskId() { const max = otaTasks .map(task => parseInt(String(task.id).replace('OTA-', ''), 10)) .filter(Number.isFinite) .reduce((value, next) => Math.max(value, next), 0); return `OTA-${String(max + 1).padStart(3, '0')}`; } function formatDateOnly(date) { const pad = value => String(value).padStart(2, '0'); return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`; } function formatDateTime(date) { const pad = value => String(value).padStart(2, '0'); return `${formatDateOnly(date)} ${pad(date.getHours())}:${pad(date.getMinutes())}`; } function formatFileSize(bytes) { if (!Number.isFinite(bytes) || bytes <= 0) return '0 KB'; if (bytes >= 1024 * 1024) return `${(bytes / 1024 / 1024).toFixed(1)} MB`; return `${Math.ceil(bytes / 1024)} KB`; } function escapeHtml(value) { return String(value ?? '').replace(/[&<>"']/g, char => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', }[char])); } function escapeInlineValue(value) { return escapeHtml(String(value ?? '') .replace(/\\/g, '\\\\') .replace(/'/g, "\\'") .replace(/\r?\n/g, ' ')); } });