Add OTA package upload functionality and UI enhancements
- Introduced a new endpoint for uploading OTA packages in backend/routes/ota.py. - Implemented file handling and MD5 checksum calculation for OTA packages. - Updated frontend API to support OTA package uploads with a new uploadOtaPackage function. - Enhanced the OTA management UI to include file upload options and improved layout. - Updated requirements.txt to include boto3 for S3 interactions. - Made various UI adjustments for better user experience and consistency.
This commit is contained in:
@@ -47,3 +47,21 @@ export function deleteOta(id) {
|
||||
method: 'delete'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传 OTA 升级包(Luxsin-X9)
|
||||
* @param {File} file
|
||||
* @param {string} model 设备型号
|
||||
*/
|
||||
export function uploadOtaPackage(file, model) {
|
||||
const formData = new FormData()
|
||||
formData.append('package_file', file)
|
||||
formData.append('model', model)
|
||||
return request({
|
||||
url: '/ota/upload-package',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: { 'Content-Type': 'multipart/form-data' },
|
||||
timeout: 300000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<el-container class="lux-shell-body">
|
||||
<el-aside width="220px" class="sidebar-glass">
|
||||
<div class="sidebar-logo">
|
||||
<h2>耳机管理平台</h2>
|
||||
<h2>Luxsin CMS</h2>
|
||||
</div>
|
||||
<el-menu
|
||||
class="lux-menu"
|
||||
@@ -48,16 +48,23 @@
|
||||
|
||||
<el-container class="lux-right-col">
|
||||
<el-header class="lux-header">
|
||||
<div class="lux-header-title">耳机品牌与型号管理系统</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" size="small" class="logout-btn" @click="handleLogout">
|
||||
退出登录
|
||||
</el-button>
|
||||
</div>
|
||||
<el-button
|
||||
circle
|
||||
type="primary"
|
||||
class="lux-header-power-btn"
|
||||
title="退出登录"
|
||||
aria-label="退出登录"
|
||||
@click="handleLogout"
|
||||
>
|
||||
<el-icon :size="18"><SwitchButton /></el-icon>
|
||||
</el-button>
|
||||
</el-header>
|
||||
<el-main class="lux-main">
|
||||
<router-view />
|
||||
</el-main>
|
||||
<el-footer class="lux-footer" height="auto">
|
||||
<span class="lux-footer-copy">©Powered By EafonYang</span>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</div>
|
||||
@@ -67,7 +74,7 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Headset, Collection, List, Upload, Document } from '@element-plus/icons-vue'
|
||||
import { Headset, Collection, List, Upload, Document, SwitchButton } from '@element-plus/icons-vue'
|
||||
import { clearToken } from '@/utils/auth'
|
||||
|
||||
const route = useRoute()
|
||||
@@ -85,14 +92,8 @@ const handleLogout = () => {
|
||||
.lux-right-col {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logout-btn {
|
||||
color: #ffffff !important;
|
||||
font-weight: 600;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -89,7 +89,10 @@ body {
|
||||
.lux-shell .sidebar-glass {
|
||||
position: relative;
|
||||
width: 220px !important;
|
||||
overflow-x: hidden;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--lux-glass-sidebar) !important;
|
||||
backdrop-filter: blur(18px);
|
||||
-webkit-backdrop-filter: blur(18px);
|
||||
@@ -119,6 +122,9 @@ body {
|
||||
}
|
||||
|
||||
.lux-shell .lux-menu.el-menu {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
border-right: none !important;
|
||||
background: transparent !important;
|
||||
padding: 8px 6px 16px;
|
||||
@@ -159,36 +165,71 @@ body {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
/* —— 顶栏 —— */
|
||||
/* —— 顶栏(与内容卡片一致的浅色底) —— */
|
||||
.lux-shell .lux-header {
|
||||
height: 56px !important;
|
||||
height: 44px !important;
|
||||
min-height: 44px !important;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 0 22px !important;
|
||||
margin: 12px 16px 0 8px;
|
||||
border-radius: 14px;
|
||||
background: var(--lux-glass-header);
|
||||
justify-content: flex-end;
|
||||
padding: 0 14px !important;
|
||||
margin: 8px 16px 0 8px;
|
||||
border-radius: 10px;
|
||||
background: var(--lux-card-grad) !important;
|
||||
backdrop-filter: blur(14px);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
border: 1px solid rgba(148, 163, 184, 0.2);
|
||||
border: 1px solid rgba(148, 163, 184, 0.22) !important;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(15, 23, 42, 0.06),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.7) inset;
|
||||
0 2px 4px -1px rgba(15, 23, 42, 0.05),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.65) inset;
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-title {
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.01em;
|
||||
color: #0f172a;
|
||||
.lux-shell .lux-header-power-btn {
|
||||
width: 34px !important;
|
||||
height: 34px !important;
|
||||
padding: 0 !important;
|
||||
box-shadow: 0 4px 12px rgba(56, 189, 248, 0.22) !important;
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-power-btn .el-icon {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lux-shell .lux-header-power-btn:hover,
|
||||
.lux-shell .lux-header-power-btn:focus {
|
||||
box-shadow: 0 6px 16px rgba(56, 189, 248, 0.3) !important;
|
||||
}
|
||||
|
||||
/* —— 主内容区 —— */
|
||||
.lux-shell .lux-main {
|
||||
background: transparent !important;
|
||||
padding: 16px 16px 24px 12px !important;
|
||||
padding: 12px 16px 24px 12px !important;
|
||||
overflow: auto;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.lux-shell .lux-footer {
|
||||
flex-shrink: 0;
|
||||
height: auto !important;
|
||||
padding: 10px 16px 14px !important;
|
||||
margin: 0 16px 12px 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: rgba(15, 23, 42, 0.35) !important;
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-radius: 12px;
|
||||
border: none !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.lux-shell .lux-footer-copy {
|
||||
font-size: 12px;
|
||||
color: rgba(203, 213, 225, 0.85);
|
||||
letter-spacing: 0.03em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.lux-shell .card-header .title {
|
||||
@@ -219,6 +260,20 @@ body {
|
||||
color: var(--lux-text-strong);
|
||||
}
|
||||
|
||||
/* 列表页顶部筛选栏:压低整体高度 */
|
||||
.lux-shell .el-card.search-card {
|
||||
margin-bottom: 14px !important;
|
||||
}
|
||||
|
||||
.lux-shell .el-card.search-card .el-card__body {
|
||||
padding: 8px 14px 10px !important;
|
||||
}
|
||||
|
||||
.lux-shell .search-form.el-form--inline .el-form-item {
|
||||
margin-bottom: 4px;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.lux-shell .el-table {
|
||||
--el-table-border-color: rgba(148, 163, 184, 0.22);
|
||||
--el-table-header-bg-color: #eef2f7;
|
||||
|
||||
@@ -279,10 +279,6 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -628,10 +628,6 @@ onMounted(() => {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
@@ -88,11 +88,10 @@
|
||||
<el-tag v-else type="info" size="small">停用</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="startTime" label="开始时间" width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="endTime" label="结束时间" width="160" show-overflow-tooltip />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<el-table-column label="操作" width="230" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button type="primary" size="small" @click="handleEdit(scope.row)">编辑</el-button>
|
||||
<el-button type="success" size="small" class="ota-copy-btn" @click="handleCopy(scope.row)">复制</el-button>
|
||||
<el-button type="danger" size="small" @click="handleDelete(scope.row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
@@ -126,7 +125,6 @@
|
||||
label-width="120px"
|
||||
class="ota-form"
|
||||
>
|
||||
<el-divider content-position="left">主升级包</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="版本号" prop="verCode">
|
||||
@@ -146,83 +144,92 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="下载地址" prop="url">
|
||||
<el-input v-model="formData.url" maxlength="255" show-word-limit />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="MD5(32位)" prop="md5">
|
||||
<el-input v-model="formData.md5" maxlength="32" show-word-limit placeholder="32 位 MD5" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="强升" prop="force">
|
||||
<el-select v-model="formData.force" style="width: 100%">
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-select v-model="formData.status" style="width: 100%">
|
||||
<el-option label="可用" :value="1" />
|
||||
<el-option label="停用" :value="0" />
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="硬件版本" prop="hw">
|
||||
<el-input-number v-model="formData.hw" :min="0" :step="1" style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="定向" prop="target">
|
||||
<el-select v-model="formData.target" style="width: 100%">
|
||||
<el-option label="否(面向所有用户)" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
<el-col :span="24" v-if="isPackageUploadModel">
|
||||
<el-form-item label="升级包" prop="url">
|
||||
<el-upload
|
||||
ref="packageUploadRef"
|
||||
:auto-upload="false"
|
||||
:limit="1"
|
||||
accept=".pkg,.bin,.zip"
|
||||
:file-list="packageFileList"
|
||||
:on-change="handlePackageChange"
|
||||
:on-remove="handlePackageRemove"
|
||||
:disabled="packageUploading"
|
||||
>
|
||||
<el-button type="primary" :loading="packageUploading">选择文件</el-button>
|
||||
</el-upload>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="灰度" prop="beta">
|
||||
<el-select v-model="formData.beta" style="width: 100%">
|
||||
<el-option label="否" :value="0" />
|
||||
<el-option label="是" :value="1" />
|
||||
</el-select>
|
||||
<el-col :span="24" v-if="isPackageUploadModel && formData.url">
|
||||
<el-form-item label="下载地址">
|
||||
<el-input v-model="formData.url" readonly style="width: 100%" />
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24">
|
||||
<el-form-item label="描述" prop="desc">
|
||||
<el-input v-model="formData.desc" type="textarea" :rows="2" maxlength="255" show-word-limit />
|
||||
</el-form-item>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="MD5(32位)" prop="md5">
|
||||
<el-input
|
||||
v-model="formData.md5"
|
||||
maxlength="32"
|
||||
show-word-limit
|
||||
style="width: 100%"
|
||||
:readonly="isPackageUploadModel"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-divider content-position="left">时间窗口</el-divider>
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-form-item label="开始时间" prop="startTime">
|
||||
<el-date-picker
|
||||
v-model="formData.startTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
<el-col :span="6">
|
||||
<el-form-item label="强升" prop="force">
|
||||
<el-switch
|
||||
v-model="formData.force"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item label="结束时间" prop="endTime">
|
||||
<el-date-picker
|
||||
v-model="formData.endTime"
|
||||
type="datetime"
|
||||
value-format="YYYY-MM-DDTHH:mm:ss"
|
||||
placeholder="可选"
|
||||
style="width: 100%"
|
||||
clearable
|
||||
<el-col :span="6">
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-switch
|
||||
v-model="formData.status"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="可用"
|
||||
inactive-text="停用"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="定向" prop="target">
|
||||
<el-switch
|
||||
v-model="formData.target"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="定向"
|
||||
inactive-text="全量"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item label="灰度" prop="beta">
|
||||
<el-switch
|
||||
v-model="formData.beta"
|
||||
:active-value="1"
|
||||
:inactive-value="0"
|
||||
inline-prompt
|
||||
active-text="是"
|
||||
inactive-text="否"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
@@ -238,18 +245,25 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, computed, onMounted, watch, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
|
||||
import { getOtaList, createOta, updateOta, deleteOta } from '@/api/ota'
|
||||
import { getOtaList, createOta, updateOta, deleteOta, uploadOtaPackage } from '@/api/ota'
|
||||
|
||||
const PAW_MD5_PLACEHOLDER = '0'.repeat(32)
|
||||
|
||||
const PACKAGE_UPLOAD_MODELS = ['Luxsin-X8', 'Luxsin-X9']
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新增 OTA')
|
||||
const formRef = ref(null)
|
||||
const packageUploadRef = ref(null)
|
||||
const packageFileList = ref([])
|
||||
const packageUploading = ref(false)
|
||||
/** 从列表行填充表单时跳过 model watch,避免误清 url/md5(默认型号与行型号不一致时) */
|
||||
const skipPackageClearOnModelWatch = ref(false)
|
||||
|
||||
const searchForm = reactive({
|
||||
verName: '',
|
||||
@@ -274,23 +288,24 @@ function emptyForm() {
|
||||
url: '',
|
||||
md5: '',
|
||||
force: 0,
|
||||
desc: '',
|
||||
model: 'Luxsin-X8',
|
||||
hw: 0,
|
||||
hw: 6,
|
||||
target: 0,
|
||||
beta: 0,
|
||||
pawVerCode: 0,
|
||||
pawVerName: '',
|
||||
pawUrl: '',
|
||||
pawMd5: '',
|
||||
startTime: '',
|
||||
endTime: '',
|
||||
status: 1
|
||||
}
|
||||
}
|
||||
|
||||
const formData = reactive(emptyForm())
|
||||
|
||||
const isPackageUploadModel = computed(() =>
|
||||
PACKAGE_UPLOAD_MODELS.includes(formData.model)
|
||||
)
|
||||
|
||||
const formRules = {
|
||||
verCode: [{ required: true, message: '请输入版本号', trigger: 'change' }],
|
||||
verName: [
|
||||
@@ -298,9 +313,25 @@ const formRules = {
|
||||
{ min: 1, max: 20, message: '1~20 字符', trigger: 'blur' }
|
||||
],
|
||||
model: [{ required: true, message: '请选择设备型号', trigger: 'change' }],
|
||||
url: [{ required: true, message: '请输入下载地址', trigger: 'blur' }],
|
||||
url: [
|
||||
{
|
||||
validator: (_rule, value, callback) => {
|
||||
if (isPackageUploadModel.value) {
|
||||
if (!value || !String(value).trim()) {
|
||||
callback(new Error('请上传升级包'))
|
||||
return
|
||||
}
|
||||
} else if (!value || !String(value).trim()) {
|
||||
callback(new Error('请输入下载地址'))
|
||||
return
|
||||
}
|
||||
callback()
|
||||
},
|
||||
trigger: 'change'
|
||||
}
|
||||
],
|
||||
md5: [
|
||||
{ required: true, message: '请输入 MD5', trigger: 'blur' },
|
||||
{ required: true, message: '请填写 MD5', trigger: 'blur' },
|
||||
{ len: 32, message: '须为 32 位', trigger: 'blur' },
|
||||
{
|
||||
pattern: /^[0-9a-fA-F]{32}$/,
|
||||
@@ -315,7 +346,6 @@ function buildPayload() {
|
||||
if (pawMd5.length !== 32) {
|
||||
pawMd5 = PAW_MD5_PLACEHOLDER
|
||||
}
|
||||
const desc = (formData.desc || '').trim()
|
||||
const model = (formData.model == null || formData.model === '') ? '' : String(formData.model).trim()
|
||||
return {
|
||||
verCode: Number(formData.verCode),
|
||||
@@ -323,7 +353,7 @@ function buildPayload() {
|
||||
url: (formData.url || '').trim(),
|
||||
md5: (formData.md5 || '').trim(),
|
||||
force: Number(formData.force ?? 0),
|
||||
desc: desc === '' ? null : desc,
|
||||
desc: null,
|
||||
model: model === '' ? null : model,
|
||||
hw: Number(formData.hw ?? 0),
|
||||
target: Number(formData.target ?? 0),
|
||||
@@ -332,14 +362,71 @@ function buildPayload() {
|
||||
pawVerName: (formData.pawVerName || '').trim(),
|
||||
pawUrl: (formData.pawUrl || '').trim(),
|
||||
pawMd5,
|
||||
startTime: formData.startTime || null,
|
||||
endTime: formData.endTime || null,
|
||||
startTime: null,
|
||||
endTime: null,
|
||||
status: Number(formData.status ?? 1)
|
||||
}
|
||||
}
|
||||
|
||||
function resetForm() {
|
||||
Object.assign(formData, emptyForm())
|
||||
packageFileList.value = []
|
||||
packageUploadRef.value?.clearFiles?.()
|
||||
}
|
||||
|
||||
function clearPackageFields() {
|
||||
formData.url = ''
|
||||
formData.md5 = ''
|
||||
packageFileList.value = []
|
||||
packageUploadRef.value?.clearFiles?.()
|
||||
}
|
||||
|
||||
watch(
|
||||
() => formData.model,
|
||||
(val, oldVal) => {
|
||||
if (skipPackageClearOnModelWatch.value) return
|
||||
const wasUpload = PACKAGE_UPLOAD_MODELS.includes(oldVal)
|
||||
const isUpload = PACKAGE_UPLOAD_MODELS.includes(val)
|
||||
if (wasUpload && (!isUpload || val !== oldVal)) {
|
||||
clearPackageFields()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
const handlePackageChange = async (file) => {
|
||||
if (!file?.raw) return
|
||||
if (!isPackageUploadModel.value) {
|
||||
ElMessage.warning('当前型号不支持上传升级包')
|
||||
packageFileList.value = []
|
||||
return
|
||||
}
|
||||
packageFileList.value = [file]
|
||||
packageUploading.value = true
|
||||
try {
|
||||
const res = await uploadOtaPackage(file.raw, formData.model)
|
||||
if (res.code === 1 && res.data) {
|
||||
formData.md5 = res.data.md5 || ''
|
||||
formData.url = res.data.url || res.data.filename || ''
|
||||
ElMessage.success('升级包上传成功')
|
||||
formRef.value?.validateField?.('url')
|
||||
formRef.value?.validateField?.('md5')
|
||||
} else {
|
||||
packageFileList.value = []
|
||||
ElMessage.error(res.msg || '上传失败')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
packageFileList.value = []
|
||||
ElMessage.error('上传失败')
|
||||
} finally {
|
||||
packageUploading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handlePackageRemove = () => {
|
||||
formData.url = ''
|
||||
formData.md5 = ''
|
||||
packageFileList.value = []
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
@@ -393,17 +480,14 @@ const handleAdd = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑 OTA'
|
||||
resetForm()
|
||||
function applyOtaRowToForm(row, isCopy) {
|
||||
Object.assign(formData, {
|
||||
id: row.id,
|
||||
id: isCopy ? null : row.id,
|
||||
verCode: row.verCode,
|
||||
verName: row.verName ?? '',
|
||||
url: row.url ?? '',
|
||||
md5: row.md5 ?? '',
|
||||
force: row.force ?? 0,
|
||||
desc: row.desc ?? '',
|
||||
model:
|
||||
row.model && ['Luxsin-X8', 'Luxsin-X9'].includes(row.model)
|
||||
? row.model
|
||||
@@ -415,11 +499,32 @@ const handleEdit = (row) => {
|
||||
pawVerName: row.pawVerName ?? '',
|
||||
pawUrl: row.pawUrl ?? '',
|
||||
pawMd5: row.pawMd5 || '',
|
||||
startTime: row.startTime || '',
|
||||
endTime: row.endTime || '',
|
||||
status: row.status ?? 1
|
||||
})
|
||||
}
|
||||
|
||||
const handleEdit = (row) => {
|
||||
dialogTitle.value = '编辑 OTA'
|
||||
skipPackageClearOnModelWatch.value = true
|
||||
resetForm()
|
||||
applyOtaRowToForm(row, false)
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
skipPackageClearOnModelWatch.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const handleCopy = (row) => {
|
||||
dialogTitle.value = '新增 OTA'
|
||||
skipPackageClearOnModelWatch.value = true
|
||||
resetForm()
|
||||
applyOtaRowToForm(row, true)
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
skipPackageClearOnModelWatch.value = false
|
||||
formRef.value?.clearValidate?.()
|
||||
})
|
||||
ElMessage.info('已复制当前行数据,请修改版本号等信息后保存')
|
||||
}
|
||||
|
||||
const handleDelete = (row) => {
|
||||
@@ -494,9 +599,6 @@ onMounted(() => {
|
||||
.ota-container {
|
||||
height: 100%;
|
||||
}
|
||||
.search-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.search-form {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -521,6 +623,21 @@ onMounted(() => {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.ota-form {
|
||||
padding-top: 16px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
/* 复制:绿底白字(避免主题覆盖为渐变) */
|
||||
.ota-copy-btn {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--el-color-success) !important;
|
||||
border-color: var(--el-color-success) !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ota-copy-btn:hover,
|
||||
.ota-copy-btn:focus {
|
||||
color: #ffffff !important;
|
||||
background-color: var(--el-color-success-light-3) !important;
|
||||
border-color: var(--el-color-success-light-3) !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user