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,