feat(ota): 支持按版本号+版本名+灰度状态检查与分组显示
- 后端新增检查版本是否存在时对版本名称做匹配,避免重复版本名称 - 更新时同步考虑版本名称,防止名称冲突 - 前端实现同型号+版本号+灰度多条记录折叠显示,支持展开查看子记录 - 调整表格列宽及样式,确保树形结构显示正常且细节优化 - 增加分组行视觉区分,弱化折叠子行背景 - 重置展开状态,确保刷新数据后树形结构收起
This commit is contained in:
@@ -215,7 +215,7 @@ router.post('/api/ota/', async (req: Request, res: Response) => {
|
|||||||
const data = parsed.data;
|
const data = parsed.data;
|
||||||
logger.info(`Creating OTA: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}`);
|
logger.info(`Creating OTA: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}`);
|
||||||
|
|
||||||
// 检查版本号是否已存在
|
// 检查版本号+版本名称是否已存在
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(otas)
|
.from(otas)
|
||||||
@@ -223,11 +223,12 @@ router.post('/api/ota/', async (req: Request, res: Response) => {
|
|||||||
eq(otas.verCode, data.verCode),
|
eq(otas.verCode, data.verCode),
|
||||||
eq(otas.model, data.model!),
|
eq(otas.model, data.model!),
|
||||||
eq(otas.beta, data.beta ?? 0),
|
eq(otas.beta, data.beta ?? 0),
|
||||||
|
eq(otas.verName, data.verName),
|
||||||
))
|
))
|
||||||
.limit(1);
|
.limit(1);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
logger.warn(`OTA version already exists: verCode=${data.verCode}, model=${data.model}, beta=${data.beta ?? 0}`);
|
logger.warn(`OTA version already exists: verCode=${data.verCode}, verName=${data.verName}, model=${data.model}, beta=${data.beta ?? 0}`);
|
||||||
res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)'));
|
res.json(ApiResponse.error('该版本已存在(相同版本+名称+灰度状态)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -279,20 +280,21 @@ router.put('/api/ota/:ota_id', async (req: Request, res: Response) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果更新版本号或灰度状态,检查是否冲突
|
// 如果更新版本号、名称或灰度状态,检查是否冲突
|
||||||
const newVerCode = data.verCode !== undefined && data.verCode !== null ? data.verCode : dbOta.verCode;
|
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 newModel = data.model !== undefined && data.model !== null ? data.model : dbOta.model;
|
||||||
const newBeta = data.beta !== undefined && data.beta !== null ? data.beta : dbOta.beta;
|
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
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(otas)
|
.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);
|
.limit(1);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
logger.warn(`OTA version already exists: verCode=${newVerCode}, model=${newModel}, beta=${newBeta}`);
|
logger.warn(`OTA version already exists: verCode=${newVerCode}, verName=${newVerName}, model=${newModel}, beta=${newBeta}`);
|
||||||
res.json(ApiResponse.error('该版本已存在(相同版本+灰度状态)'));
|
res.json(ApiResponse.error('该版本已存在(相同版本+名称+灰度状态)'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,8 +41,6 @@ declare module 'vue' {
|
|||||||
NButton: typeof import('naive-ui')['NButton']
|
NButton: typeof import('naive-ui')['NButton']
|
||||||
NCard: typeof import('naive-ui')['NCard']
|
NCard: typeof import('naive-ui')['NCard']
|
||||||
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
NCheckbox: typeof import('naive-ui')['NCheckbox']
|
||||||
NCollapse: typeof import('naive-ui')['NCollapse']
|
|
||||||
NCollapseItem: typeof import('naive-ui')['NCollapseItem']
|
|
||||||
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||||
NDataTable: typeof import('naive-ui')['NDataTable']
|
NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||||
@@ -134,8 +132,6 @@ declare global {
|
|||||||
const NButton: typeof import('naive-ui')['NButton']
|
const NButton: typeof import('naive-ui')['NButton']
|
||||||
const NCard: typeof import('naive-ui')['NCard']
|
const NCard: typeof import('naive-ui')['NCard']
|
||||||
const NCheckbox: typeof import('naive-ui')['NCheckbox']
|
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 NColorPicker: typeof import('naive-ui')['NColorPicker']
|
||||||
const NDataTable: typeof import('naive-ui')['NDataTable']
|
const NDataTable: typeof import('naive-ui')['NDataTable']
|
||||||
const NDatePicker: typeof import('naive-ui')['NDatePicker']
|
const NDatePicker: typeof import('naive-ui')['NDatePicker']
|
||||||
|
|||||||
@@ -47,6 +47,9 @@ interface OtaRow {
|
|||||||
target?: number;
|
target?: number;
|
||||||
status?: number;
|
status?: number;
|
||||||
create_at?: string;
|
create_at?: string;
|
||||||
|
children?: OtaRow[];
|
||||||
|
__groupChild?: boolean;
|
||||||
|
__groupCount?: number;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,6 +69,8 @@ const packageFileList = ref<UploadFileInfo[]>([]);
|
|||||||
const packageUploading = ref(false);
|
const packageUploading = ref(false);
|
||||||
const skipPackageClearOnModelWatch = ref(false);
|
const skipPackageClearOnModelWatch = ref(false);
|
||||||
const tableData = ref<OtaRow[]>([]);
|
const tableData = ref<OtaRow[]>([]);
|
||||||
|
// 受控的树形展开状态:同组折叠行首列箭头展开,刷新数据后重置为收起
|
||||||
|
const expandedRowKeys = ref<Array<string | number>>([]);
|
||||||
|
|
||||||
const searchForm = reactive<{
|
const searchForm = reactive<{
|
||||||
verName: string;
|
verName: string;
|
||||||
@@ -193,15 +198,20 @@ function moreOptions(): DropdownOption[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const columns = computed<DataTableColumns<OtaRow>>(() => [
|
const columns = computed<DataTableColumns<OtaRow>>(() => [
|
||||||
{ title: 'ID', key: 'id', width: 70 },
|
// 树形子行首列自带库的缩进补偿(indent 16px + placeholder 24px),
|
||||||
|
// 列宽需容纳该补偿 + ID 文本,否则内容换行撑高行高
|
||||||
|
{ title: 'ID', key: 'id', width: 100 },
|
||||||
{ title: '版本号', key: 'verCode', width: 90 },
|
{ title: '版本号', key: 'verCode', width: 90 },
|
||||||
{
|
{
|
||||||
title: '版本名',
|
title: '版本名',
|
||||||
key: 'verName',
|
key: 'verName',
|
||||||
width: 200,
|
width: 280,
|
||||||
render(row) {
|
render(row) {
|
||||||
return h('div', { class: 'flex-y-center gap-8px' }, [
|
return h('div', { class: 'flex-y-center gap-8px' }, [
|
||||||
h('span', { class: 'truncate', title: row.verName }, row.verName || '-'),
|
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)
|
getLatestTag(row)
|
||||||
? h(
|
? h(
|
||||||
NTag,
|
NTag,
|
||||||
@@ -287,6 +297,10 @@ const columns = computed<DataTableColumns<OtaRow>>(() => [
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
function rowClassName(row: OtaRow) {
|
||||||
|
return row.__groupChild ? 'ota-group-child-row' : '';
|
||||||
|
}
|
||||||
|
|
||||||
function buildPayload() {
|
function buildPayload() {
|
||||||
const model = (formData.model == null || formData.model === '' ? '' : String(formData.model)).trim();
|
const model = (formData.model == null || formData.model === '' ? '' : String(formData.model)).trim();
|
||||||
return {
|
return {
|
||||||
@@ -360,6 +374,29 @@ function handlePackageRemove() {
|
|||||||
clearPackageFields();
|
clearPackageFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 同「型号+版本号+灰度」的多条记录折叠为一行:后端按 id 倒序返回,
|
||||||
|
// 组内首次出现即 id 最大的一条,作为主行;其余记录挂到 children,
|
||||||
|
// 点击主行首列箭头展开查看。分页在服务端按行切分,同组记录跨页时
|
||||||
|
// 各页独立分组(数据不丢失,仅不折叠)。
|
||||||
|
const groupedTableData = computed<OtaRow[]>(() => {
|
||||||
|
const mainByKey = new Map<string, OtaRow>();
|
||||||
|
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() {
|
async function loadData() {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
// 同步刷新全局最新版本(latest 标签依据,增删改后保持一致)
|
// 同步刷新全局最新版本(latest 标签依据,增删改后保持一致)
|
||||||
@@ -378,6 +415,7 @@ async function loadData() {
|
|||||||
if (!error && data) {
|
if (!error && data) {
|
||||||
tableData.value = (data.items as OtaRow[]) || [];
|
tableData.value = (data.items as OtaRow[]) || [];
|
||||||
pagination.itemCount = data.total || 0;
|
pagination.itemCount = data.total || 0;
|
||||||
|
expandedRowKeys.value = [];
|
||||||
} else if (!error) {
|
} else if (!error) {
|
||||||
tableData.value = [];
|
tableData.value = [];
|
||||||
pagination.itemCount = 0;
|
pagination.itemCount = 0;
|
||||||
@@ -768,7 +806,11 @@ onMounted(() => {
|
|||||||
<NDataTable
|
<NDataTable
|
||||||
:loading="loading"
|
:loading="loading"
|
||||||
:columns="columns"
|
:columns="columns"
|
||||||
:data="tableData"
|
:data="groupedTableData"
|
||||||
|
:row-key="(row: OtaRow) => row.id"
|
||||||
|
:row-class-name="rowClassName"
|
||||||
|
:expanded-row-keys="expandedRowKeys"
|
||||||
|
@update:expanded-row-keys="keys => (expandedRowKeys = keys)"
|
||||||
:bordered="false"
|
:bordered="false"
|
||||||
:single-line="false"
|
:single-line="false"
|
||||||
size="small"
|
size="small"
|
||||||
@@ -973,4 +1015,9 @@ onMounted(() => {
|
|||||||
:deep(.ota-copy-icon:hover) {
|
:deep(.ota-copy-icon:hover) {
|
||||||
color: var(--n-primary-color);
|
color: var(--n-primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 折叠组内的子行(同版本号下较早的记录):弱化背景以区分主行 */
|
||||||
|
:deep(.ota-group-child-row > td) {
|
||||||
|
background-color: rgba(128, 128, 128, 0.05);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user