diff --git a/dashboard/backend/src/routes/ota.ts b/dashboard/backend/src/routes/ota.ts index 201cbaa..b12c3ae 100644 --- a/dashboard/backend/src/routes/ota.ts +++ b/dashboard/backend/src/routes/ota.ts @@ -215,7 +215,7 @@ router.post('/api/ota/', async (req: Request, res: Response) => { const data = parsed.data; logger.info(`Creating OTA: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}`); - // 检查版本号是否已存在 + // 检查版本号+版本名称是否已存在 const [existing] = await db .select() .from(otas) @@ -223,11 +223,12 @@ router.post('/api/ota/', async (req: Request, res: Response) => { eq(otas.verCode, data.verCode), eq(otas.model, data.model!), eq(otas.beta, data.beta ?? 0), + eq(otas.verName, data.verName), )) .limit(1); if (existing) { - logger.warn(`OTA version already exists: verCode=${data.verCode}, model=${data.model}, beta=${data.beta ?? 0}`); - res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)')); + logger.warn(`OTA version already exists: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}, beta=${data.beta ?? 0}`); + res.json(ApiResponse.error('该版本已存在(相同版本+名称+灰度状态)')); return; } @@ -279,20 +280,21 @@ router.put('/api/ota/:ota_id', async (req: Request, res: Response) => { return; } - // 如果更新版本号或灰度状态,检查是否冲突 + // 如果更新版本号、名称或灰度状态,检查是否冲突 const newVerCode = data.verCode !== undefined && data.verCode !== null ? data.verCode : dbOta.verCode; const newModel = data.model !== undefined && data.model !== null ? data.model : dbOta.model; const newBeta = data.beta !== undefined && data.beta !== null ? data.beta : dbOta.beta; + const newVerName = data.verName !== undefined && data.verName !== null ? data.verName : dbOta.verName; - if (newVerCode !== dbOta.verCode || newModel !== dbOta.model || newBeta !== dbOta.beta) { + if (newVerCode !== dbOta.verCode || newModel !== dbOta.model || newBeta !== dbOta.beta || newVerName !== dbOta.verName) { const [existing] = await db .select() .from(otas) - .where(and(eq(otas.verCode, newVerCode), eq(otas.model, newModel!), eq(otas.beta, newBeta))) + .where(and(eq(otas.verCode, newVerCode), eq(otas.model, newModel!), eq(otas.beta, newBeta), eq(otas.verName, newVerName))) .limit(1); if (existing) { - logger.warn(`OTA version already exists: verCode=${newVerCode}, model=${newModel}, beta=${newBeta}`); - res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)')); + logger.warn(`OTA version already exists: verCode=${newVerCode}, verName=${newVerName}, model=${newModel}, beta=${newBeta}`); + res.json(ApiResponse.error('该版本已存在(相同版本+名称+灰度状态)')); return; } } diff --git a/dashboard/frontend/src/typings/components.d.ts b/dashboard/frontend/src/typings/components.d.ts index fff6479..8e2e459 100644 --- a/dashboard/frontend/src/typings/components.d.ts +++ b/dashboard/frontend/src/typings/components.d.ts @@ -41,8 +41,6 @@ declare module 'vue' { NButton: typeof import('naive-ui')['NButton'] NCard: typeof import('naive-ui')['NCard'] NCheckbox: typeof import('naive-ui')['NCheckbox'] - NCollapse: typeof import('naive-ui')['NCollapse'] - NCollapseItem: typeof import('naive-ui')['NCollapseItem'] NColorPicker: typeof import('naive-ui')['NColorPicker'] NDataTable: typeof import('naive-ui')['NDataTable'] NDatePicker: typeof import('naive-ui')['NDatePicker'] @@ -134,8 +132,6 @@ declare global { const NButton: typeof import('naive-ui')['NButton'] const NCard: typeof import('naive-ui')['NCard'] const NCheckbox: typeof import('naive-ui')['NCheckbox'] - const NCollapse: typeof import('naive-ui')['NCollapse'] - const NCollapseItem: typeof import('naive-ui')['NCollapseItem'] const NColorPicker: typeof import('naive-ui')['NColorPicker'] const NDataTable: typeof import('naive-ui')['NDataTable'] const NDatePicker: typeof import('naive-ui')['NDatePicker'] diff --git a/dashboard/frontend/src/views/upgrade/ota/index.vue b/dashboard/frontend/src/views/upgrade/ota/index.vue index a705e28..2137f25 100644 --- a/dashboard/frontend/src/views/upgrade/ota/index.vue +++ b/dashboard/frontend/src/views/upgrade/ota/index.vue @@ -47,6 +47,9 @@ interface OtaRow { target?: number; status?: number; create_at?: string; + children?: OtaRow[]; + __groupChild?: boolean; + __groupCount?: number; [key: string]: unknown; } @@ -66,6 +69,8 @@ const packageFileList = ref([]); const packageUploading = ref(false); const skipPackageClearOnModelWatch = ref(false); const tableData = ref([]); +// 受控的树形展开状态:同组折叠行首列箭头展开,刷新数据后重置为收起 +const expandedRowKeys = ref>([]); const searchForm = reactive<{ verName: string; @@ -193,15 +198,20 @@ function moreOptions(): DropdownOption[] { } const columns = computed>(() => [ - { title: 'ID', key: 'id', width: 70 }, + // 树形子行首列自带库的缩进补偿(indent 16px + placeholder 24px), + // 列宽需容纳该补偿 + ID 文本,否则内容换行撑高行高 + { title: 'ID', key: 'id', width: 100 }, { title: '版本号', key: 'verCode', width: 90 }, { title: '版本名', key: 'verName', - width: 200, + width: 280, render(row) { return h('div', { class: 'flex-y-center gap-8px' }, [ h('span', { class: 'truncate', title: row.verName }, row.verName || '-'), + row.__groupCount && row.__groupCount > 1 + ? h(NTag, { type: 'info', size: 'small', bordered: false }, { default: () => `${row.__groupCount ?? 1} 条版本` }) + : null, getLatestTag(row) ? h( NTag, @@ -287,6 +297,10 @@ const columns = computed>(() => [ } ]); +function rowClassName(row: OtaRow) { + return row.__groupChild ? 'ota-group-child-row' : ''; +} + function buildPayload() { const model = (formData.model == null || formData.model === '' ? '' : String(formData.model)).trim(); return { @@ -360,6 +374,29 @@ function handlePackageRemove() { clearPackageFields(); } +// 同「型号+版本号+灰度」的多条记录折叠为一行:后端按 id 倒序返回, +// 组内首次出现即 id 最大的一条,作为主行;其余记录挂到 children, +// 点击主行首列箭头展开查看。分页在服务端按行切分,同组记录跨页时 +// 各页独立分组(数据不丢失,仅不折叠)。 +const groupedTableData = computed(() => { + const mainByKey = new Map(); + const result: OtaRow[] = []; + for (const row of tableData.value) { + const key = `${row.model || ''}__${row.verCode}__${row.beta ?? 0}`; + const main = mainByKey.get(key); + if (!main) { + const copy: OtaRow = { ...row, __groupCount: 1 }; + mainByKey.set(key, copy); + result.push(copy); + } else { + main.__groupCount = (main.__groupCount ?? 1) + 1; + if (!main.children) main.children = []; + main.children.push({ ...row, __groupChild: true }); + } + } + return result; +}); + async function loadData() { loading.value = true; // 同步刷新全局最新版本(latest 标签依据,增删改后保持一致) @@ -378,6 +415,7 @@ async function loadData() { if (!error && data) { tableData.value = (data.items as OtaRow[]) || []; pagination.itemCount = data.total || 0; + expandedRowKeys.value = []; } else if (!error) { tableData.value = []; pagination.itemCount = 0; @@ -768,7 +806,11 @@ onMounted(() => { { :deep(.ota-copy-icon:hover) { color: var(--n-primary-color); } + +/* 折叠组内的子行(同版本号下较早的记录):弱化背景以区分主行 */ +:deep(.ota-group-child-row > td) { + background-color: rgba(128, 128, 128, 0.05); +}