From 19ea48ffc86aafe5b54cadaa2c6c84e4cb1f05ab Mon Sep 17 00:00:00 2001 From: eafonyang Date: Mon, 17 Aug 2026 15:11:50 +0800 Subject: [PATCH] =?UTF-8?q?feat(ota):=20=E6=96=B0=E5=A2=9E=E8=8E=B7?= =?UTF-8?q?=E5=8F=96=E5=90=84=E3=80=8C=E5=9E=8B=E5=8F=B7+=E7=81=B0?= =?UTF-8?q?=E5=BA=A6=E3=80=8D=E5=88=86=E7=BB=84=E5=85=A8=E5=B1=80=E6=9C=80?= =?UTF-8?q?=E5=A4=A7=E7=89=88=E6=9C=AC=E5=8F=B7=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 /api/ota/latest-versions 接口,返回所有型号和灰度的最大版本号 - 添加数据库查询,基于全量数据分组聚合版本号,避免分页误判最新版本 - 前端新增 fetchGetOtaLatestVersions 接口调用该后端接口 - 在 OTA 页面加入全局最新版本数据加载与缓存,用于 latest 标签判断 - 修改标签判定逻辑,避免仅基于当前页数据导致的错误标记 - 调用时机调整,增删改 OTA 后刷新全局最新版本数据保持同步 --- dashboard/backend/src/routes/ota.ts | 25 +++++++++++++++++- dashboard/frontend/src/service/api/ota.ts | 8 ++++++ .../frontend/src/typings/api/dashboard.d.ts | 7 +++++ .../frontend/src/views/upgrade/ota/index.vue | 26 ++++++++++++------- 4 files changed, 56 insertions(+), 10 deletions(-) diff --git a/dashboard/backend/src/routes/ota.ts b/dashboard/backend/src/routes/ota.ts index 5d46b62..201cbaa 100644 --- a/dashboard/backend/src/routes/ota.ts +++ b/dashboard/backend/src/routes/ota.ts @@ -1,5 +1,5 @@ import { Router, type Request, type Response, type Router as RouterType } from 'express'; -import { eq, gt, like, and, desc, count } from 'drizzle-orm'; +import { eq, gt, like, and, desc, count, max } from 'drizzle-orm'; import multer from 'multer'; import { db } from '../config/database.js'; import { otas } from '../schemas/index.js'; @@ -159,6 +159,29 @@ router.get('/api/ota/', async (req: Request, res: Response) => { } }); +// GET /api/ota/latest-versions —— 各「型号+灰度」分组的全局最大版本号 +// 供前端 latest / latest beta 标签判断:必须基于全量数据, +// 否则分页后「当前页内」的最大版本会被误标为 latest +router.get('/api/ota/latest-versions', async (_req: Request, res: Response) => { + try { + const rows = await db + .select({ model: otas.model, beta: otas.beta, maxVerCode: max(otas.verCode) }) + .from(otas) + .groupBy(otas.model, otas.beta); + + const items = rows.map(r => ({ + model: r.model ?? '', + beta: r.beta ?? 0, + ver_code: Number(r.maxVerCode), + })); + res.json(ApiResponse.success({ items })); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + logger.error(`Error getting OTA latest versions: ${msg}`); + res.json(ApiResponse.error('error')); + } +}); + // GET /api/ota/:ota_id router.get('/api/ota/:ota_id', async (req: Request, res: Response) => { try { diff --git a/dashboard/frontend/src/service/api/ota.ts b/dashboard/frontend/src/service/api/ota.ts index 6894d20..298a37c 100644 --- a/dashboard/frontend/src/service/api/ota.ts +++ b/dashboard/frontend/src/service/api/ota.ts @@ -24,6 +24,14 @@ export function fetchGetOta(id: number) { }); } +/** 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据,不受分页/筛选影响) */ +export function fetchGetOtaLatestVersions() { + return request<{ items: Api.Ota.LatestVersionItem[] }>({ + url: '/ota/latest-versions', + method: 'get' + }); +} + export function fetchCreateOta(data: Record) { return request({ url: '/ota/', diff --git a/dashboard/frontend/src/typings/api/dashboard.d.ts b/dashboard/frontend/src/typings/api/dashboard.d.ts index 60610d1..f991b94 100644 --- a/dashboard/frontend/src/typings/api/dashboard.d.ts +++ b/dashboard/frontend/src/typings/api/dashboard.d.ts @@ -89,6 +89,13 @@ declare namespace Api { id: number; [key: string]: unknown; } + + /** 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据) */ + interface LatestVersionItem { + model: string; + beta: number; + ver_code: number; + } } namespace Blacklist { diff --git a/dashboard/frontend/src/views/upgrade/ota/index.vue b/dashboard/frontend/src/views/upgrade/ota/index.vue index 7a6b0a1..a705e28 100644 --- a/dashboard/frontend/src/views/upgrade/ota/index.vue +++ b/dashboard/frontend/src/views/upgrade/ota/index.vue @@ -18,6 +18,7 @@ import { fetchDeleteOta, fetchDeleteOtaTargetDevice, fetchGetBlackList, + fetchGetOtaLatestVersions, fetchGetOtaList, fetchGetOtaTargetDeviceList, fetchUpdateOta, @@ -141,20 +142,25 @@ const formRules: FormRules = { ] }; -const latestVersionMap = computed(() => { - const map: Record = {}; - for (const row of tableData.value) { - const key = `${row.model || ''}__${row.beta}`; - if (map[key] === undefined || row.verCode > map[key]) { - map[key] = row.verCode; +// 各「型号+灰度」分组的全局最大版本号(latest 标签判断依据)。 +// 必须基于全量数据(后端 /ota/latest-versions):若基于当前页数据计算, +// 翻页后「页内」最大版本也会被误标为 latest / latest beta +const latestVersions = ref>({}); + +async function loadLatestVersions() { + const { data, error } = await fetchGetOtaLatestVersions(); + if (!error && data) { + const map: Record = {}; + for (const it of data.items || []) { + map[`${it.model || ''}__${it.beta}`] = it.ver_code; } + latestVersions.value = map; } - return map; -}); +} function getLatestTag(row: OtaRow) { const key = `${row.model || ''}__${row.beta}`; - return latestVersionMap.value[key] === row.verCode; + return latestVersions.value[key] === row.verCode; } function renderCopyUrlCell(url: string) { @@ -356,6 +362,8 @@ function handlePackageRemove() { async function loadData() { loading.value = true; + // 同步刷新全局最新版本(latest 标签依据,增删改后保持一致) + loadLatestVersions(); const params: Record = { skip: (pagination.page - 1) * pagination.pageSize, limit: pagination.pageSize,