新增耳机阻抗

This commit is contained in:
eafonyang
2026-08-17 15:41:32 +08:00
parent 19ea48ffc8
commit acd3d4b92c
15 changed files with 733 additions and 0 deletions
@@ -233,6 +233,7 @@ const local: App.I18n.Schema = {
headphone: 'Headphones',
headphone_brand: 'Brands',
headphone_model: 'Models',
headphone_impedance: 'Impedance',
upgrade: 'Upgrade',
upgrade_ota: 'OTA',
'upgrade_ota-target-device': 'Target Devices',
@@ -229,6 +229,7 @@ const local: App.I18n.Schema = {
headphone: '耳机管理',
headphone_brand: '品牌管理',
headphone_model: '型号管理',
headphone_impedance: '耳机阻抗',
upgrade: '升级管理',
upgrade_ota: 'OTA 管理',
'upgrade_ota-target-device': '定向升级',
@@ -21,6 +21,7 @@ export const views: Record<LastLevelRouteKey, RouteComponent | (() => Promise<Ro
"iframe-page": () => import("@/views/_builtin/iframe-page/[url].vue"),
login: () => import("@/views/_builtin/login/index.vue"),
headphone_brand: () => import("@/views/headphone/brand/index.vue"),
headphone_impedance: () => import("@/views/headphone/impedance/index.vue"),
headphone_model: () => import("@/views/headphone/model/index.vue"),
home: () => import("@/views/home/index.vue"),
meilisearch: () => import("@/views/meilisearch/index.vue"),
@@ -62,6 +62,18 @@ export const generatedRoutes: GeneratedRoute[] = [
keepAlive: true
}
},
{
name: 'headphone_impedance',
path: '/headphone/impedance',
component: 'view.headphone_impedance',
meta: {
title: 'headphone_impedance',
i18nKey: 'route.headphone_impedance',
icon: 'mdi:omega',
order: 3,
keepAlive: true
}
},
{
name: 'headphone_model',
path: '/headphone/model',
@@ -168,6 +168,7 @@ const routeMap: RouteMap = {
"500": "/500",
"headphone": "/headphone",
"headphone_brand": "/headphone/brand",
"headphone_impedance": "/headphone/impedance",
"headphone_model": "/headphone/model",
"home": "/home",
"iframe-page": "/iframe-page/:url",
@@ -0,0 +1,47 @@
import { request } from '../request';
import type { PageParams } from './brand';
export function fetchGetHeadphoneImpedanceList(
params?: PageParams & {
mac_addr?: string;
device_model?: string;
headphone_brand?: string;
headphone_model?: string;
}
) {
return request<Api.Common.PageResult<Api.HeadphoneImpedance.Item>>({
url: '/headphone-impedance/',
method: 'get',
params
});
}
export function fetchGetHeadphoneImpedance(id: number) {
return request<Api.HeadphoneImpedance.Item>({
url: `/headphone-impedance/${id}`,
method: 'get'
});
}
export function fetchCreateHeadphoneImpedance(data: Record<string, unknown>) {
return request<Api.HeadphoneImpedance.Item>({
url: '/headphone-impedance/',
method: 'post',
data
});
}
export function fetchUpdateHeadphoneImpedance(id: number, data: Record<string, unknown>) {
return request<Api.HeadphoneImpedance.Item>({
url: `/headphone-impedance/${id}`,
method: 'put',
data
});
}
export function fetchDeleteHeadphoneImpedance(id: number) {
return request({
url: `/headphone-impedance/${id}`,
method: 'delete'
});
}
@@ -6,6 +6,7 @@ export * from './user';
export * from './ota';
export * from './blacklist';
export * from './ota-target-device';
export * from './headphone-impedance';
export * from './share-code-log';
export * from './model';
export * from './meilisearch';
+16
View File
@@ -116,6 +116,22 @@ declare namespace Api {
}
}
namespace HeadphoneImpedance {
interface Item {
id: number;
mac_addr: string;
device_model: string;
impedance_ohm: number;
headphone_brand: string;
headphone_model: string;
headphone_brand_norm?: string;
headphone_model_norm?: string;
ip_addr?: string;
create_at?: string | null;
update_at?: string | null;
}
}
namespace ShareCodeLog {
interface Item {
id: number;
+2
View File
@@ -22,6 +22,7 @@ declare module "@elegant-router/types" {
"500": "/500";
"headphone": "/headphone";
"headphone_brand": "/headphone/brand";
"headphone_impedance": "/headphone/impedance";
"headphone_model": "/headphone/model";
"home": "/home";
"iframe-page": "/iframe-page/:url";
@@ -130,6 +131,7 @@ declare module "@elegant-router/types" {
| "iframe-page"
| "login"
| "headphone_brand"
| "headphone_impedance"
| "headphone_model"
| "home"
| "meilisearch"
@@ -0,0 +1,355 @@
<script setup lang="ts">
import { h, onMounted, reactive, ref } from 'vue';
import {
NButton,
NPopconfirm,
NSpace,
type DataTableColumns,
type FormInst,
type FormRules
} from 'naive-ui';
import {
fetchCreateHeadphoneImpedance,
fetchDeleteHeadphoneImpedance,
fetchGetHeadphoneImpedanceList,
fetchUpdateHeadphoneImpedance
} from '@/service/api';
import SvgIcon from '@/components/custom/svg-icon.vue';
import { MAC_PATTERN, formatDateTime } from '../../upgrade/shared';
/**
* 耳机阻抗管理
*
* 管理设备端上报的 user_headphone_impedance 记录:
* 连接耳机后测得的阻抗 + 识别出的耳机品牌/型号,支持筛选、修正与删除。
*/
defineOptions({ name: 'HeadphoneImpedance' });
// 表数据里出现过的设备型号(X8 / X9 / B9)
const modelOptions = [
{ label: 'Luxsin-X8', value: 'Luxsin-X8' },
{ label: 'Luxsin-X9', value: 'Luxsin-X9' },
{ label: 'Luxsin-B9', value: 'Luxsin-B9' }
];
const loading = ref(false);
const submitLoading = ref(false);
const tableData = ref<Api.HeadphoneImpedance.Item[]>([]);
const searchForm = reactive({
mac_addr: '',
device_model: null as string | null,
headphone_brand: '',
headphone_model: ''
});
const pagination = reactive({
page: 1,
pageSize: 10,
itemCount: 0,
pageSizes: [10, 20, 50, 100]
});
function emptyForm() {
return {
id: null as number | null,
mac_addr: '',
device_model: 'Luxsin-X8',
impedance_ohm: 0,
headphone_brand: '',
headphone_model: '',
ip_addr: ''
};
}
const formData = reactive(emptyForm());
const formRef = ref<FormInst | null>(null);
const dialogVisible = ref(false);
const dialogTitle = ref('新增记录');
const formRules: FormRules = {
mac_addr: [
{ required: true, message: '请输入 MAC 地址', trigger: ['blur', 'input'] },
{
validator: (_rule, value: string) => !value || MAC_PATTERN.test(value),
message: 'MAC 格式不正确,如 40:D9:5A:D8:06:9C',
trigger: ['blur', 'input']
}
],
device_model: [{ required: true, message: '请选择设备型号', trigger: ['change', 'blur'] }],
impedance_ohm: [
{ type: 'number', required: true, message: '请输入阻抗值', trigger: ['change', 'blur'] },
{
validator: (_rule, value: number) => value >= 0 && value <= 10000,
message: '阻抗范围 0 ~ 10000 Ω',
trigger: ['change', 'blur']
}
],
headphone_brand: [{ required: true, message: '请输入耳机品牌', trigger: ['blur', 'input'] }],
headphone_model: [{ required: true, message: '请输入耳机型号', trigger: ['blur', 'input'] }]
};
const columns: DataTableColumns<Api.HeadphoneImpedance.Item> = [
{ title: 'ID', key: 'id', width: 70 },
{ title: 'MAC 地址', key: 'mac_addr', width: 170 },
{ title: '设备型号', key: 'device_model', width: 110 },
{
title: '阻抗',
key: 'impedance_ohm',
width: 90,
render: row => `${row.impedance_ohm} Ω`
},
{ title: '耳机品牌', key: 'headphone_brand', minWidth: 120, ellipsis: { tooltip: true } },
{ title: '耳机型号', key: 'headphone_model', minWidth: 140, ellipsis: { tooltip: true } },
{ title: 'IP', key: 'ip_addr', width: 140, ellipsis: { tooltip: true } },
{
title: '上报时间',
key: 'create_at',
width: 170,
render: row => formatDateTime(row.create_at)
},
{
title: '更新时间',
key: 'update_at',
width: 170,
render: row => formatDateTime(row.update_at)
},
{
title: '操作',
key: 'actions',
width: 130,
render(row) {
return h(NSpace, { size: 'small', wrap: false }, () => [
h(
NButton,
{ size: 'small', type: 'primary', ghost: true, onClick: () => openEdit(row) },
{ default: () => '编辑' }
),
h(
NPopconfirm,
{ onPositiveClick: () => handleDelete(row) },
{
trigger: () =>
h(
NButton,
{ size: 'small', type: 'error', ghost: true },
{ default: () => '删除' }
),
default: () => `确认删除 ID=${row.id} 的记录?`
}
)
]);
}
}
];
async function loadData() {
loading.value = true;
const params: Record<string, unknown> = {
skip: (pagination.page - 1) * pagination.pageSize,
limit: pagination.pageSize,
mac_addr: searchForm.mac_addr.trim() || undefined,
device_model: searchForm.device_model || undefined,
headphone_brand: searchForm.headphone_brand.trim() || undefined,
headphone_model: searchForm.headphone_model.trim() || undefined
};
const { data, error } = await fetchGetHeadphoneImpedanceList(params as any);
if (!error && data) {
tableData.value = data.items || [];
pagination.itemCount = data.total || 0;
} else if (!error) {
tableData.value = [];
pagination.itemCount = 0;
}
loading.value = false;
}
function handleSearch() {
pagination.page = 1;
loadData();
}
function handleReset() {
searchForm.mac_addr = '';
searchForm.device_model = null;
searchForm.headphone_brand = '';
searchForm.headphone_model = '';
pagination.page = 1;
loadData();
}
function handlePageChange(page: number) {
pagination.page = page;
loadData();
}
function handlePageSizeChange(pageSize: number) {
pagination.pageSize = pageSize;
pagination.page = 1;
loadData();
}
function openCreate() {
Object.assign(formData, emptyForm());
dialogTitle.value = '新增记录';
dialogVisible.value = true;
}
function openEdit(row: Api.HeadphoneImpedance.Item) {
Object.assign(formData, {
id: row.id,
mac_addr: row.mac_addr,
device_model: row.device_model,
impedance_ohm: row.impedance_ohm,
headphone_brand: row.headphone_brand,
headphone_model: row.headphone_model,
ip_addr: row.ip_addr || ''
});
dialogTitle.value = '编辑记录';
dialogVisible.value = true;
}
function handleSubmit() {
formRef.value?.validate(async errors => {
if (errors) return;
submitLoading.value = true;
const payload = {
mac_addr: formData.mac_addr.trim(),
device_model: formData.device_model,
impedance_ohm: formData.impedance_ohm,
headphone_brand: formData.headphone_brand.trim(),
headphone_model: formData.headphone_model.trim(),
ip_addr: formData.ip_addr.trim()
};
const { error } = formData.id
? await fetchUpdateHeadphoneImpedance(formData.id, payload)
: await fetchCreateHeadphoneImpedance(payload);
submitLoading.value = false;
if (!error) {
window.$message?.success(formData.id ? '保存成功' : '新增成功');
dialogVisible.value = false;
loadData();
}
});
}
async function handleDelete(row: Api.HeadphoneImpedance.Item) {
const { error } = await fetchDeleteHeadphoneImpedance(row.id);
if (!error) {
window.$message?.success('删除成功');
// 当前页删空后回退一页
if (tableData.value.length === 1 && pagination.page > 1) {
pagination.page -= 1;
}
loadData();
}
}
onMounted(loadData);
</script>
<template>
<NSpace vertical :size="16">
<NCard :bordered="false" class="card-wrapper" size="small">
<div class="grid grid-cols-1 gap-12px md:grid-cols-4">
<NInput v-model:value="searchForm.mac_addr" placeholder="MAC 地址" clearable @keyup.enter="handleSearch" />
<NSelect
v-model:value="searchForm.device_model"
:options="modelOptions"
placeholder="设备型号"
clearable
@update:value="handleSearch"
/>
<NInput v-model:value="searchForm.headphone_brand" placeholder="耳机品牌" clearable @keyup.enter="handleSearch" />
<NInput v-model:value="searchForm.headphone_model" placeholder="耳机型号" clearable @keyup.enter="handleSearch" />
</div>
<div class="mt-12px flex items-center justify-between">
<NSpace>
<NButton type="primary" @click="handleSearch">
<template #icon>
<SvgIcon icon="ic:round-search" />
</template>
搜索
</NButton>
<NButton @click="handleReset">
<template #icon>
<SvgIcon icon="mdi:refresh" />
</template>
重置
</NButton>
</NSpace>
<NButton type="primary" @click="openCreate">
<template #icon>
<SvgIcon icon="mdi:plus" />
</template>
新增记录
</NButton>
</div>
</NCard>
<NCard :bordered="false" class="card-wrapper" size="small" title="阻抗记录">
<NDataTable
remote
:columns="columns"
:data="tableData"
:loading="loading"
:pagination="{
...pagination,
showSizePicker: true,
onChange: handlePageChange,
onUpdatePageSize: handlePageSizeChange
}"
:row-key="(row: Api.HeadphoneImpedance.Item) => row.id"
:scroll-x="1400"
/>
</NCard>
<NModal
v-model:show="dialogVisible"
preset="card"
:title="dialogTitle"
class="w-500px"
:mask-closable="false"
>
<NForm
ref="formRef"
:model="formData"
:rules="formRules"
label-placement="left"
label-width="90"
>
<NFormItem label="MAC 地址" path="mac_addr">
<NInput v-model:value="formData.mac_addr" placeholder=" 40:D9:5A:D8:06:9C" />
</NFormItem>
<NFormItem label="设备型号" path="device_model">
<NSelect v-model:value="formData.device_model" :options="modelOptions" />
</NFormItem>
<NFormItem label="阻抗 (Ω)" path="impedance_ohm">
<NInputNumber
v-model:value="formData.impedance_ohm"
:min="0"
:max="10000"
:precision="0"
class="w-full"
placeholder="0 ~ 10000"
/>
</NFormItem>
<NFormItem label="耳机品牌" path="headphone_brand">
<NInput v-model:value="formData.headphone_brand" placeholder=" Sennheiser" />
</NFormItem>
<NFormItem label="耳机型号" path="headphone_model">
<NInput v-model:value="formData.headphone_model" placeholder=" HD800" />
</NFormItem>
<NFormItem label="IP 地址" path="ip_addr">
<NInput v-model:value="formData.ip_addr" placeholder="可选" />
</NFormItem>
</NForm>
<template #footer>
<NSpace justify="end">
<NButton @click="dialogVisible = false">取消</NButton>
<NButton type="primary" :loading="submitLoading" @click="handleSubmit">保存</NButton>
</NSpace>
</template>
</NModal>
</NSpace>
</template>