Initial commit: Audio Dashboard Management System

Made-with: Cursor
This commit is contained in:
yangy
2026-03-04 18:17:34 +08:00
commit 75b1d16453
39 changed files with 3771 additions and 0 deletions
+313
View File
@@ -0,0 +1,313 @@
<template>
<div class="brand-container">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="品牌名称">
<el-input
v-model="searchForm.name"
placeholder="请输入品牌名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<div class="card-header">
<span class="title">品牌列表</span>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新增品牌
</el-button>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
>
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="name" label="品牌名称" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="500px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="品牌名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入品牌名称"
maxlength="100"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus } from '@element-plus/icons-vue'
import { getBrands, createBrand, updateBrand, deleteBrand } from '@/api/brand'
const loading = ref(false)
const submitLoading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新增品牌')
const formRef = ref(null)
const searchForm = reactive({
name: ''
})
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0
})
const tableData = ref([])
const formData = reactive({
id: null,
name: ''
})
const formRules = {
name: [
{ required: true, message: '请输入品牌名称', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
]
}
// 加载数据
const loadData = async () => {
loading.value = true
try {
const res = await getBrands({
skip: (pagination.page - 1) * pagination.pageSize,
limit: pagination.pageSize,
name: searchForm.name || undefined
})
if (res.code === 1 && res.data) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
} else if (res.code === 2) {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.page = 1
loadData()
}
// 重置
const handleReset = () => {
searchForm.name = ''
pagination.page = 1
loadData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新增品牌'
dialogVisible.value = true
}
// 编辑
const handleEdit = (row) => {
dialogTitle.value = '编辑品牌'
formData.id = row.id
formData.name = row.name
dialogVisible.value = true
}
// 删除
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除品牌 "${row.name}" 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
submitLoading.value = true
const res = await deleteBrand(row.id)
if (res.code === 1) {
ElMessage.success('删除成功')
loadData()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
} finally {
submitLoading.value = false
}
}).catch(() => {})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const api = formData.id ? updateBrand : createBrand
const res = await api(formData)
if (res.code === 1) {
ElMessage.success(formData.id ? '更新成功' : '创建成功')
dialogVisible.value = false
loadData()
} else {
ElMessage.error(res.msg || '操作失败')
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
} finally {
submitLoading.value = false
}
}
})
}
// 对话框关闭
const handleDialogClose = () => {
formData.id = null
formData.name = ''
if (formRef.value) {
formRef.value.resetFields()
}
}
// 分页变化
const handleSizeChange = () => {
loadData()
}
const handlePageChange = () => {
loadData()
}
onMounted(() => {
loadData()
})
</script>
<style scoped>
.brand-container {
height: 100%;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.table-card {
min-height: 500px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>
+486
View File
@@ -0,0 +1,486 @@
<template>
<div class="model-container">
<el-card class="search-card">
<el-form :inline="true" :model="searchForm" class="search-form">
<el-form-item label="品牌名称">
<el-select
v-model="searchForm.brand_name"
placeholder="请选择品牌"
filterable
clearable
style="width: 200px"
>
<el-option
v-for="brand in brandOptions"
:key="brand.id"
:label="brand.name"
:value="brand.name"
/>
</el-select>
</el-form-item>
<el-form-item label="型号名称">
<el-input
v-model="searchForm.name"
placeholder="请输入型号名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<el-icon><Search /></el-icon>
搜索
</el-button>
<el-button @click="handleReset">
<el-icon><Refresh /></el-icon>
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
<el-card class="table-card">
<div class="card-header">
<span class="title">型号列表</span>
<div class="header-buttons">
<el-button
type="warning"
:disabled="selectedIds.length === 0"
@click="handlePushToSearch"
:loading="pushLoading"
>
<el-icon><Upload /></el-icon>
推送到搜索 ({{ selectedIds.length }})
</el-button>
<el-button type="primary" @click="handleAdd">
<el-icon><Plus /></el-icon>
新型号
</el-button>
</div>
</div>
<el-table
v-loading="loading"
:data="tableData"
border
stripe
style="width: 100%"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="brand_name" label="品牌名称" width="150" />
<el-table-column prop="name" label="型号名称" />
<el-table-column prop="form" label="佩戴方式" width="120">
<template #default="scope">
<el-tag v-if="scope.row.form === 'in-ear'" type="success">入耳式</el-tag>
<el-tag v-else-if="scope.row.form === 'over-ear'" type="primary">头戴式</el-tag>
<el-tag v-else-if="scope.row.form === 'earbud'" type="info">耳塞式</el-tag>
<span v-else>{{ scope.row.form || '-' }}</span>
</template>
</el-table-column>
<el-table-column prop="rig" label="阻抗" width="120" />
<el-table-column prop="eq_key" label="EQ 配置" width="150" show-overflow-tooltip />
<el-table-column prop="create_at" label="创建时间" width="180" />
<el-table-column label="操作" width="200" fixed="right">
<template #default="scope">
<el-button
type="primary"
size="small"
@click="handleEdit(scope.row)"
>
编辑
</el-button>
<el-button
type="danger"
size="small"
@click="handleDelete(scope.row)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handlePageChange"
/>
</div>
</el-card>
<!-- 新增/编辑对话框 -->
<el-dialog
v-model="dialogVisible"
:title="dialogTitle"
width="600px"
@close="handleDialogClose"
>
<el-form
ref="formRef"
:model="formData"
:rules="formRules"
label-width="100px"
>
<el-form-item label="品牌名称" prop="brand_name">
<el-select
v-model="formData.brand_name"
placeholder="请选择品牌"
filterable
allow-create
default-first-option
style="width: 100%"
>
<el-option
v-for="brand in brandOptions"
:key="brand.id"
:label="brand.name"
:value="brand.name"
/>
</el-select>
</el-form-item>
<el-form-item label="型号名称" prop="name">
<el-input
v-model="formData.name"
placeholder="请输入型号名称"
maxlength="100"
/>
</el-form-item>
<el-form-item label="形式" prop="form">
<el-select
v-model="formData.form"
placeholder="请选择佩戴方式"
style="width: 100%"
>
<el-option label="入耳式" value="in-ear" />
<el-option label="头戴式" value="over-ear" />
<el-option label="耳塞式" value="earbud" />
</el-select>
</el-form-item>
<el-form-item label="阻抗" prop="rig">
<el-input
v-model="formData.rig"
placeholder="例如:32Ω"
maxlength="100"
/>
</el-form-item>
<el-form-item label="来源" prop="source">
<el-input
v-model="formData.source"
placeholder="型号来源"
maxlength="100"
/>
</el-form-item>
<el-form-item label="EQ 键" prop="eq_key">
<el-input
v-model="formData.eq_key"
placeholder="EQ 配置键值"
maxlength="255"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSubmit" :loading="submitLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Search, Refresh, Plus, Upload } from '@element-plus/icons-vue'
import { getModels, createModel, updateModel, deleteModel, pushToMeilisearch } from '@/api/model'
import { getBrands } from '@/api/brand'
const loading = ref(false)
const submitLoading = ref(false)
const pushLoading = ref(false)
const dialogVisible = ref(false)
const dialogTitle = ref('新型号')
const formRef = ref(null)
const brandOptions = ref([])
const selectedIds = ref([])
const searchForm = reactive({
brand_name: '',
name: ''
})
const pagination = reactive({
page: 1,
pageSize: 10,
total: 0
})
const tableData = ref([])
const formData = reactive({
id: null,
brand_name: '',
name: '',
form: '',
rig: '',
source: '',
eq_key: ''
})
const formRules = {
brand_name: [
{ required: true, message: '请选择品牌名称', trigger: 'change' }
],
name: [
{ required: true, message: '请输入型号名称', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
]
}
// 加载品牌列表
const loadBrands = async () => {
try {
const res = await getBrands({ skip: 0, limit: 1000 })
if (res.code === 1 && res.data) {
brandOptions.value = res.data.items || []
}
} catch (error) {
console.error('加载品牌列表失败:', error)
}
}
// 加载数据
const loadData = async () => {
loading.value = true
try {
const res = await getModels({
skip: (pagination.page - 1) * pagination.pageSize,
limit: pagination.pageSize,
brand_name: searchForm.brand_name || undefined,
name: searchForm.name || undefined
})
if (res.code === 1 && res.data) {
tableData.value = res.data.items || []
pagination.total = res.data.total || 0
} else if (res.code === 2) {
tableData.value = []
pagination.total = 0
}
} catch (error) {
console.error('加载数据失败:', error)
ElMessage.error('加载数据失败')
} finally {
loading.value = false
}
}
// 搜索
const handleSearch = () => {
pagination.page = 1
loadData()
}
// 重置
const handleReset = () => {
searchForm.brand_name = ''
searchForm.name = ''
pagination.page = 1
loadData()
}
// 新增
const handleAdd = () => {
dialogTitle.value = '新型号'
dialogVisible.value = true
}
// 编辑
const handleEdit = (row) => {
dialogTitle.value = '编辑型号'
formData.id = row.id
formData.brand_name = row.brand_name
formData.name = row.name
formData.form = row.form || ''
formData.rig = row.rig || ''
formData.source = row.source || ''
formData.eq_key = row.eq_key || ''
dialogVisible.value = true
}
// 删除
const handleDelete = (row) => {
ElMessageBox.confirm(
`确定要删除型号 "${row.brand_name} - ${row.name}" 吗?`,
'警告',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
submitLoading.value = true
const res = await deleteModel(row.id)
if (res.code === 1) {
ElMessage.success('删除成功')
loadData()
} else {
ElMessage.error(res.msg || '删除失败')
}
} catch (error) {
console.error('删除失败:', error)
ElMessage.error('删除失败')
} finally {
submitLoading.value = false
}
}).catch(() => {})
}
// 提交表单
const handleSubmit = async () => {
if (!formRef.value) return
await formRef.value.validate(async (valid) => {
if (valid) {
submitLoading.value = true
try {
const api = formData.id ? updateModel : createModel
const res = await api(formData)
if (res.code === 1) {
ElMessage.success(formData.id ? '更新成功' : '创建成功')
dialogVisible.value = false
loadData()
} else {
ElMessage.error(res.msg || '操作失败')
}
} catch (error) {
console.error('操作失败:', error)
ElMessage.error('操作失败')
} finally {
submitLoading.value = false
}
}
})
}
// 对话框关闭
const handleDialogClose = () => {
formData.id = null
formData.brand_name = ''
formData.name = ''
formData.form = ''
formData.rig = ''
formData.source = ''
formData.eq_key = ''
if (formRef.value) {
formRef.value.resetFields()
}
}
// 分页变化
const handleSizeChange = () => {
loadData()
}
const handlePageChange = () => {
loadData()
}
// 处理选择变化
const handleSelectionChange = (selection) => {
selectedIds.value = selection.map(row => row.id)
}
// 推送到 Meilisearch
const handlePushToSearch = () => {
if (selectedIds.value.length === 0) {
ElMessage.warning('请选择要推送的型号')
return
}
ElMessageBox.confirm(
`确定要将选中的 ${selectedIds.value.length} 个型号推送到搜索引擎吗?`,
'推送确认',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
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(() => {})
}
onMounted(() => {
loadBrands()
loadData()
})
</script>
<style scoped>
.model-container {
height: 100%;
}
.search-card {
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
}
.table-card {
min-height: 500px;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.header-buttons {
display: flex;
gap: 10px;
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
</style>