/**
* 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 || '-')}
`
}
${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)} 固件`}
${!isLatest ? `` : ''}
${version.uploaded ? `${!isLatest ? '|' : ''}` : ''}
`;
}).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.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 `${actions.join('|')}
`;
}
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: `
接口: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: `
接口: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: `
任务日志
${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, ' '));
}
});