Add Luxsin curve API client and validation functionality
- Introduced a new module `curve_client.py` for interacting with the Luxsin curve API, including functions for custom Base64 decoding and parametric EQ validation. - Implemented error handling for API responses and validation checks for curve data. - Removed outdated test files to streamline the codebase. - Updated brand creation and update logic in `brands.py` to ensure brand names are validated and checked for duplicates. - Enhanced model handling in `models.py` with new validation and push-to-search functionality, integrating curve validation before pushing to Meilisearch. - Updated frontend components to support new validation and push processes, including user feedback for validation results and progress tracking. - Changed favicon to SVG format for better scalability and appearance.
This commit is contained in:
+1
-1
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
||||
<text y="0.88em" x="50" text-anchor="middle" font-size="72"><§</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 142 B |
@@ -72,13 +72,29 @@ export function deleteModel(id) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送到 Meilisearch
|
||||
* 推送前校验曲线数据
|
||||
* @param {Array<number>} modelIds - 型号 ID 列表
|
||||
*/
|
||||
export function validatePushToMeilisearch(modelIds) {
|
||||
return request({
|
||||
url: '/models/push-to-search/validate',
|
||||
method: 'post',
|
||||
data: { model_ids: modelIds },
|
||||
timeout: 120000,
|
||||
skipErrorToast: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送到 Meilisearch(需先通过校验)
|
||||
* @param {Array<number>} modelIds - 型号 ID 列表
|
||||
*/
|
||||
export function pushToMeilisearch(modelIds) {
|
||||
return request({
|
||||
url: '/models/push-to-search',
|
||||
method: 'post',
|
||||
data: { model_ids: modelIds }
|
||||
data: { model_ids: modelIds },
|
||||
timeout: 60000,
|
||||
skipErrorToast: true
|
||||
})
|
||||
}
|
||||
|
||||
@@ -29,10 +29,14 @@ request.interceptors.response.use(
|
||||
response => {
|
||||
const res = response.data
|
||||
|
||||
// 如果响应码为 0,表示错误
|
||||
// 如果响应码为 0,表示错误(可由调用方自行提示,如推送进度弹窗)
|
||||
if (res.code === 0) {
|
||||
ElMessage.error(res.msg || '请求失败')
|
||||
return Promise.reject(new Error(res.msg || '请求失败'))
|
||||
if (!response.config.skipErrorToast) {
|
||||
ElMessage.error(res.msg || '请求失败')
|
||||
}
|
||||
const err = new Error(res.msg || '请求失败')
|
||||
err.responseData = res
|
||||
return Promise.reject(err)
|
||||
}
|
||||
|
||||
return res
|
||||
|
||||
@@ -45,9 +45,8 @@
|
||||
<div class="header-buttons">
|
||||
<el-button
|
||||
type="warning"
|
||||
:disabled="selectedIds.length === 0"
|
||||
:disabled="selectedIds.length === 0 || pushProgressRunning"
|
||||
@click="handlePushToSearch"
|
||||
:loading="pushLoading"
|
||||
>
|
||||
<el-icon><Upload /></el-icon>
|
||||
推送到搜索 ({{ selectedIds.length }})
|
||||
@@ -82,7 +81,7 @@
|
||||
<span v-else>{{ scope.row.form || '-' }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="rig" label="阻抗" width="120" />
|
||||
<el-table-column prop="rig" label="阻抗" width="150" />
|
||||
<el-table-column prop="source" label="来源" width="150" show-overflow-tooltip />
|
||||
<el-table-column prop="create_at" label="创建时间" width="180" sortable="custom" />
|
||||
<el-table-column label="操作" width="200" fixed="right">
|
||||
@@ -208,19 +207,121 @@
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<el-dialog
|
||||
v-model="pushProgressVisible"
|
||||
title="推送到搜索"
|
||||
width="480px"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="!pushProgressRunning"
|
||||
:show-close="!pushProgressRunning"
|
||||
@closed="resetPushProgress"
|
||||
>
|
||||
<ul class="push-progress-steps">
|
||||
<li class="push-progress-step push-progress-step--validate">
|
||||
<div class="step-main">
|
||||
<span class="step-label">校验数据</span>
|
||||
<p
|
||||
v-if="pushSteps.validate === 'loading' && validatingCurrent"
|
||||
class="step-detail"
|
||||
>
|
||||
正在校验:{{ validatingCurrent.brand_name }} · {{ validatingCurrent.name }}
|
||||
<span v-if="validateProgress.total > 1" class="step-progress-text">
|
||||
({{ validateProgress.index }}/{{ validateProgress.total }})
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<span class="step-status">
|
||||
<el-icon v-if="pushSteps.validate === 'loading'" class="is-loading step-icon-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.validate === 'success'" class="step-icon-success">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.validate === 'error'" class="step-icon-error">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</span>
|
||||
</li>
|
||||
<li class="push-progress-step push-progress-step--push">
|
||||
<div class="step-main">
|
||||
<span class="step-label">推送</span>
|
||||
<p v-if="pushSteps.push === 'loading' && pushingCurrent" class="step-detail">
|
||||
正在推送:{{ pushingCurrent.brand_name }} · {{ pushingCurrent.name }}
|
||||
</p>
|
||||
<p v-else-if="pushSteps.push === 'success' && pushResultCount > 0" class="step-detail step-detail--muted">
|
||||
已推送 {{ pushResultCount }} 条
|
||||
</p>
|
||||
</div>
|
||||
<span class="step-status">
|
||||
<el-icon v-if="pushSteps.push === 'loading'" class="is-loading step-icon-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.push === 'success'" class="step-icon-success">
|
||||
<CircleCheck />
|
||||
</el-icon>
|
||||
<el-icon v-else-if="pushSteps.push === 'error'" class="step-icon-error">
|
||||
<CircleClose />
|
||||
</el-icon>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="pushValidateErrors.length > 0" class="push-validate-errors">
|
||||
<p class="push-validate-errors-title">校验未通过:</p>
|
||||
<ul>
|
||||
<li v-for="item in pushValidateErrors" :key="item.id">
|
||||
{{ item.brand_name }} {{ item.name }}:{{ formatValidateReason(item.reason) }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<template #footer>
|
||||
<el-button :disabled="pushProgressRunning" type="primary" @click="pushProgressVisible = false">
|
||||
{{ pushProgressRunning ? '处理中…' : '关闭' }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted, nextTick } from 'vue'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { Search, Refresh, Plus, Upload, UploadFilled } from '@element-plus/icons-vue'
|
||||
import { getModels, createModel, updateModel, deleteModel, pushToMeilisearch } from '@/api/model'
|
||||
import {
|
||||
Search,
|
||||
Refresh,
|
||||
Plus,
|
||||
Upload,
|
||||
UploadFilled,
|
||||
Loading,
|
||||
CircleCheck,
|
||||
CircleClose
|
||||
} from '@element-plus/icons-vue'
|
||||
import {
|
||||
getModels,
|
||||
createModel,
|
||||
updateModel,
|
||||
deleteModel,
|
||||
validatePushToMeilisearch,
|
||||
pushToMeilisearch
|
||||
} from '@/api/model'
|
||||
import { getBrands, createBrand } from '@/api/brand'
|
||||
|
||||
const loading = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const pushLoading = ref(false)
|
||||
const pushProgressVisible = ref(false)
|
||||
const pushSteps = reactive({
|
||||
validate: 'wait',
|
||||
push: 'wait'
|
||||
})
|
||||
const pushValidateErrors = ref([])
|
||||
const validatingCurrent = ref(null)
|
||||
const pushingCurrent = ref(null)
|
||||
const pushResultCount = ref(0)
|
||||
const validateProgress = reactive({ index: 0, total: 0 })
|
||||
const selectedRows = ref([])
|
||||
const pushProgressRunning = computed(
|
||||
() => pushSteps.validate === 'loading' || pushSteps.push === 'loading'
|
||||
)
|
||||
const dialogVisible = ref(false)
|
||||
const dialogTitle = ref('新型号')
|
||||
const formRef = ref(null)
|
||||
@@ -579,7 +680,104 @@ const handlePageChange = () => {
|
||||
|
||||
// 处理选择变化
|
||||
const handleSelectionChange = (selection) => {
|
||||
selectedIds.value = selection.map(row => row.id)
|
||||
selectedRows.value = selection
|
||||
selectedIds.value = selection.map((row) => row.id)
|
||||
}
|
||||
|
||||
/** 校验失败原因展示:曲线类错误仅显示「曲线数据异常」 */
|
||||
function formatValidateReason(reason) {
|
||||
if (!reason) return '校验失败'
|
||||
if (reason.includes('曲线数据异常')) return '曲线数据异常'
|
||||
return reason
|
||||
}
|
||||
|
||||
function resetPushProgress() {
|
||||
pushSteps.validate = 'wait'
|
||||
pushSteps.push = 'wait'
|
||||
pushValidateErrors.value = []
|
||||
validatingCurrent.value = null
|
||||
pushingCurrent.value = null
|
||||
pushResultCount.value = 0
|
||||
validateProgress.index = 0
|
||||
validateProgress.total = 0
|
||||
}
|
||||
|
||||
async function runPushToSearch(rows) {
|
||||
pushProgressVisible.value = true
|
||||
resetPushProgress()
|
||||
pushSteps.validate = 'loading'
|
||||
pushSteps.push = 'wait'
|
||||
validateProgress.total = rows.length
|
||||
|
||||
const errors = []
|
||||
let pushedCount = 0
|
||||
let validationFailCount = 0
|
||||
|
||||
for (let i = 0; i < rows.length; i++) {
|
||||
const row = rows[i]
|
||||
validateProgress.index = i + 1
|
||||
validatingCurrent.value = {
|
||||
brand_name: row.brand_name,
|
||||
name: row.name
|
||||
}
|
||||
|
||||
try {
|
||||
await validatePushToMeilisearch([row.id])
|
||||
} catch (error) {
|
||||
validationFailCount += 1
|
||||
const fromApi = error.responseData?.data?.errors
|
||||
if (Array.isArray(fromApi) && fromApi.length > 0) {
|
||||
errors.push(...fromApi)
|
||||
} else {
|
||||
errors.push({
|
||||
id: row.id,
|
||||
brand_name: row.brand_name,
|
||||
name: row.name,
|
||||
reason: error.message || '校验失败'
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pushSteps.push = 'loading'
|
||||
pushingCurrent.value = {
|
||||
brand_name: row.brand_name,
|
||||
name: row.name
|
||||
}
|
||||
try {
|
||||
const res = await pushToMeilisearch([row.id])
|
||||
pushedCount += res.data?.pushed_count ?? 1
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
id: row.id,
|
||||
brand_name: row.brand_name,
|
||||
name: row.name,
|
||||
reason: error.message || '推送失败'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
validatingCurrent.value = null
|
||||
pushingCurrent.value = null
|
||||
pushResultCount.value = pushedCount
|
||||
|
||||
pushSteps.validate = validationFailCount === rows.length ? 'error' : 'success'
|
||||
if (errors.length > 0) {
|
||||
pushValidateErrors.value = errors
|
||||
}
|
||||
|
||||
if (pushedCount > 0) {
|
||||
pushSteps.push = 'success'
|
||||
if (errors.length > 0) {
|
||||
ElMessage.warning(`成功推送 ${pushedCount} 条,${errors.length} 条未通过校验或推送失败`)
|
||||
} else {
|
||||
ElMessage.success(`成功推送 ${pushedCount} 条数据到搜索引擎`)
|
||||
}
|
||||
} else if (errors.some((e) => (e.reason || '').includes('推送'))) {
|
||||
pushSteps.push = 'error'
|
||||
} else {
|
||||
pushSteps.push = 'wait'
|
||||
}
|
||||
}
|
||||
|
||||
// 推送到 Meilisearch
|
||||
@@ -588,33 +786,25 @@ const handlePushToSearch = () => {
|
||||
ElMessage.warning('请选择要推送的型号')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
const rows = selectedRows.value.length
|
||||
? [...selectedRows.value]
|
||||
: tableData.value.filter((row) => selectedIds.value.includes(row.id))
|
||||
if (!rows.length) {
|
||||
ElMessage.warning('请选择要推送的型号')
|
||||
return
|
||||
}
|
||||
ElMessageBox.confirm(
|
||||
`确定要将选中的 ${selectedIds.value.length} 个型号推送到搜索引擎吗?`,
|
||||
`确定要将选中的 ${rows.length} 个型号推送到搜索引擎吗?`,
|
||||
'推送确认',
|
||||
{
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
type: 'warning'
|
||||
}
|
||||
).then(async () => {
|
||||
pushLoading.value = true
|
||||
try {
|
||||
const res = await pushToMeilisearch(selectedIds.value)
|
||||
|
||||
if (res.code === 1) {
|
||||
ElMessage.success(`成功推送 ${res.data.pushed_count} 条数据到搜索引擎`)
|
||||
console.log('推送的数据:', res.data.models)
|
||||
} else {
|
||||
ElMessage.error(res.msg || '推送失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('推送失败:', error)
|
||||
ElMessage.error('推送失败')
|
||||
} finally {
|
||||
pushLoading.value = false
|
||||
}
|
||||
}).catch(() => {})
|
||||
)
|
||||
.then(() => runPushToSearch(rows))
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -667,4 +857,108 @@ onMounted(() => {
|
||||
line-height: 1.5;
|
||||
color: var(--el-color-warning);
|
||||
}
|
||||
|
||||
.push-progress-steps {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 8px 4px 0;
|
||||
}
|
||||
|
||||
.push-progress-step {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(148, 163, 184, 0.08);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.push-progress-step--validate,
|
||||
.push-progress-step--push {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.push-progress-step--validate .step-status,
|
||||
.push-progress-step--push .step-status {
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.step-detail--muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.step-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.step-detail {
|
||||
margin: 6px 0 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: #64748b;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.step-progress-text {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.push-progress-step:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.step-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 24px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.step-icon-loading {
|
||||
font-size: 20px;
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.step-icon-success {
|
||||
font-size: 22px;
|
||||
color: var(--el-color-success);
|
||||
}
|
||||
|
||||
.step-icon-error {
|
||||
font-size: 22px;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.push-validate-errors {
|
||||
margin-top: 12px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(245, 108, 108, 0.08);
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.push-validate-errors-title {
|
||||
margin: 0 0 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
|
||||
.push-validate-errors ul {
|
||||
margin: 0;
|
||||
padding-left: 18px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: #64748b;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user