新增耳机阻抗

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
@@ -0,0 +1,239 @@
import { Router, type Request, type Response, type Router as RouterType } from 'express';
import { eq, like, and, desc, count } from 'drizzle-orm';
import { db } from '../config/database.js';
import { userHeadphoneImpedances } from '../schemas/index.js';
import logger from '../config/logger.js';
import { ApiResponse } from '../utils/response.js';
import { authMiddleware } from '../middleware/auth.js';
import { HeadphoneImpedanceCreateSchema, HeadphoneImpedanceUpdateSchema } from '../validators/headphoneImpedance.js';
const router: RouterType = Router();
// 所有接口需登录
router.use('/api/headphone-impedance', authMiddleware);
function toDict(item: typeof userHeadphoneImpedances.$inferSelect) {
return {
id: item.id,
mac_addr: item.macAddr,
device_model: item.deviceModel,
impedance_ohm: item.impedanceOhm,
headphone_brand: item.headphoneBrand,
headphone_model: item.headphoneModel,
headphone_brand_norm: item.headphoneBrandNorm,
headphone_model_norm: item.headphoneModelNorm,
ip_addr: item.ipAddr,
create_at: item.createAt ? item.createAt.toISOString() : null,
update_at: item.updateAt ? item.updateAt.toISOString() : null,
};
}
/** norm 字段生成规则:原始值 trim + 小写(与设备端上报行为一致) */
function norm(value: string) {
return value.trim().toLowerCase();
}
// GET /api/headphone-impedance/ —— 分页列表(支持 mac/型号/品牌/耳机型号筛选)
router.get('/api/headphone-impedance/', async (req: Request, res: Response) => {
try {
const skip = parseInt(req.query.skip as string || '0', 10);
const limit = Math.min(parseInt(req.query.limit as string || '100', 10), 1000);
const macAddr = req.query.mac_addr as string | undefined;
const deviceModel = req.query.device_model as string | undefined;
const headphoneBrand = req.query.headphone_brand as string | undefined;
const headphoneModel = req.query.headphone_model as string | undefined;
const conditions = [];
if (macAddr) conditions.push(like(userHeadphoneImpedances.macAddr, `%${macAddr}%`));
if (deviceModel) conditions.push(eq(userHeadphoneImpedances.deviceModel, deviceModel));
// 品牌筛选对原始值与 norm 值分别模糊匹配(输入小写时也能命中大写原始值)
if (headphoneBrand) {
conditions.push(
like(userHeadphoneImpedances.headphoneBrand, `%${headphoneBrand}%`),
);
}
if (headphoneModel) {
conditions.push(
like(userHeadphoneImpedances.headphoneModel, `%${headphoneModel}%`),
);
}
const whereClause = conditions.length > 0 ? and(...conditions) : undefined;
const [totalResult] = await db
.select({ value: count() })
.from(userHeadphoneImpedances)
.where(whereClause);
const total = totalResult?.value ?? 0;
const rows = await db
.select()
.from(userHeadphoneImpedances)
.where(whereClause)
.orderBy(desc(userHeadphoneImpedances.id))
.offset(skip)
.limit(limit);
logger.info(`Found ${rows.length} headphone impedance records, total=${total}`);
if (!rows.length) {
res.json(ApiResponse.noData('empty'));
return;
}
res.json(ApiResponse.success({ items: rows.map(toDict), total, skip, limit }));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting headphone impedance list: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// GET /api/headphone-impedance/:id
router.get('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
logger.info(`Getting headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
res.json(ApiResponse.success(toDict(item)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error getting headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// POST /api/headphone-impedance/
router.post('/api/headphone-impedance/', async (req: Request, res: Response) => {
try {
const parsed = HeadphoneImpedanceCreateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Creating headphone impedance: mac=${data.mac_addr}, model=${data.device_model}, ohm=${data.impedance_ohm}`);
const result = await db.insert(userHeadphoneImpedances).values({
macAddr: data.mac_addr.trim(),
deviceModel: data.device_model.trim(),
impedanceOhm: data.impedance_ohm,
headphoneBrand: data.headphone_brand.trim(),
headphoneModel: data.headphone_model.trim(),
headphoneBrandNorm: norm(data.headphone_brand),
headphoneModelNorm: norm(data.headphone_model),
ipAddr: data.ip_addr ?? '',
});
const [created] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, result[0].insertId))
.limit(1);
logger.info(`Headphone impedance created: id=${created.id}`);
res.json(ApiResponse.success(toDict(created)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error creating headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// PUT /api/headphone-impedance/:id
router.put('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
const parsed = HeadphoneImpedanceUpdateSchema.safeParse(req.body);
if (!parsed.success) {
const errors = parsed.error.issues.map((e: { message: string }) => e.message).join('; ');
res.json(ApiResponse.error(errors));
return;
}
const data = parsed.data;
logger.info(`Updating headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
const updateData: Partial<typeof userHeadphoneImpedances.$inferInsert> = {};
if (data.mac_addr !== undefined && data.mac_addr !== null) updateData.macAddr = data.mac_addr.trim();
if (data.device_model !== undefined && data.device_model !== null) updateData.deviceModel = data.device_model.trim();
if (data.impedance_ohm !== undefined && data.impedance_ohm !== null) updateData.impedanceOhm = data.impedance_ohm;
if (data.headphone_brand !== undefined && data.headphone_brand !== null) {
updateData.headphoneBrand = data.headphone_brand.trim();
updateData.headphoneBrandNorm = norm(data.headphone_brand);
}
if (data.headphone_model !== undefined && data.headphone_model !== null) {
updateData.headphoneModel = data.headphone_model.trim();
updateData.headphoneModelNorm = norm(data.headphone_model);
}
if (data.ip_addr !== undefined && data.ip_addr !== null) updateData.ipAddr = data.ip_addr;
if (Object.keys(updateData).length > 0) {
await db.update(userHeadphoneImpedances).set(updateData).where(eq(userHeadphoneImpedances.id, id));
}
logger.info(`Headphone impedance updated: id=${id}`);
const [updated] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
res.json(ApiResponse.success(toDict(updated)));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error updating headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
// DELETE /api/headphone-impedance/:id
router.delete('/api/headphone-impedance/:id', async (req: Request, res: Response) => {
try {
const id = parseInt(req.params.id as string, 10);
logger.info(`Deleting headphone impedance: id=${id}`);
const [item] = await db
.select()
.from(userHeadphoneImpedances)
.where(eq(userHeadphoneImpedances.id, id))
.limit(1);
if (!item) {
logger.warn(`Headphone impedance not found: id=${id}`);
res.json(ApiResponse.noData('empty'));
return;
}
await db.delete(userHeadphoneImpedances).where(eq(userHeadphoneImpedances.id, id));
logger.info(`Headphone impedance deleted: id=${id}`);
res.json(ApiResponse.success(null, 'success'));
} catch (e: unknown) {
const msg = e instanceof Error ? e.message : String(e);
logger.error(`Error deleting headphone impedance: ${msg}`);
res.json(ApiResponse.error('error'));
}
});
export default router;
+2
View File
@@ -6,6 +6,7 @@ import otaRouter from './ota.js';
import blacklistRouter from './blacklist.js';
import otaTargetDeviceRouter from './otaTargetDevice.js';
import shareCodeLogsRouter from './shareCodeLogs.js';
import headphoneImpedanceRouter from './headphoneImpedance.js';
import usersRouter from './users.js';
import dashboardRouter from './dashboard.js';
import siteRouter from './site.js';
@@ -35,6 +36,7 @@ const routes: Router[] = [
blacklistRouter,
otaTargetDeviceRouter,
shareCodeLogsRouter,
headphoneImpedanceRouter,
usersRouter,
dashboardRouter,
meilisearchRouter,
@@ -0,0 +1,22 @@
import { mysqlTable, int, varchar, datetime } from 'drizzle-orm/mysql-core';
import { sql } from 'drizzle-orm';
/**
* 用户耳机阻抗上报记录(表已存在于数据库,无需建表)
*
* 设备端上报:连接耳机后测得的阻抗 + 识别出的耳机品牌/型号。
* *_norm 字段为对应原始值的小写规范化形式,由写入方生成。
*/
export const userHeadphoneImpedances = mysqlTable('user_headphone_impedance', {
id: int('id').primaryKey().autoincrement(),
macAddr: varchar('mac_addr', { length: 255 }).notNull(),
deviceModel: varchar('device_model', { length: 50 }).notNull(),
impedanceOhm: int('impedance_ohm').notNull(),
headphoneBrand: varchar('headphone_brand', { length: 255 }).notNull(),
headphoneModel: varchar('headphone_model', { length: 255 }).notNull(),
headphoneBrandNorm: varchar('headphone_brand_norm', { length: 255 }).notNull(),
headphoneModelNorm: varchar('headphone_model_norm', { length: 255 }).notNull(),
ipAddr: varchar('ip_addr', { length: 100 }).notNull(),
createAt: datetime('create_at').notNull().default(sql`NOW()`),
updateAt: datetime('update_at').notNull().default(sql`NOW()`),
});
+1
View File
@@ -10,6 +10,7 @@ export { dashboardUsers } from './dashboardUser.js';
export { shareCodeLogs } from './shareCodeLog.js';
export { userActives } from './userActive.js';
export { userDevices } from './userDevice.js';
export { userHeadphoneImpedances } from './headphoneImpedance.js';
// 官网 CMS
export * from './www/index.js';
@@ -0,0 +1,32 @@
import { z } from 'zod';
export const HeadphoneImpedanceCreateSchema = z.object({
mac_addr: z.string().min(1, 'MAC 地址不能为空').max(255, 'MAC 地址最多 255 字符'),
device_model: z.string().min(1, '设备型号不能为空').max(50, '设备型号最多 50 字符'),
impedance_ohm: z
.number({ message: '阻抗须为数值' })
.int('阻抗须为整数')
.min(0, '阻抗不能为负')
.max(10000, '阻抗不能超过 10000'),
headphone_brand: z.string().min(1, '耳机品牌不能为空').max(255, '耳机品牌最多 255 字符'),
headphone_model: z.string().min(1, '耳机型号不能为空').max(255, '耳机型号最多 255 字符'),
ip_addr: z.string().max(100, 'IP 地址最多 100 字符').optional().default(''),
});
export const HeadphoneImpedanceUpdateSchema = z.object({
mac_addr: z.string().min(1).max(255).optional().nullable(),
device_model: z.string().min(1).max(50).optional().nullable(),
impedance_ohm: z
.number({ message: '阻抗须为数值' })
.int('阻抗须为整数')
.min(0, '阻抗不能为负')
.max(10000, '阻抗不能超过 10000')
.optional()
.nullable(),
headphone_brand: z.string().min(1).max(255).optional().nullable(),
headphone_model: z.string().min(1).max(255).optional().nullable(),
ip_addr: z.string().max(100).optional().nullable(),
});
export type HeadphoneImpedanceCreateInput = z.infer<typeof HeadphoneImpedanceCreateSchema>;
export type HeadphoneImpedanceUpdateInput = z.infer<typeof HeadphoneImpedanceUpdateSchema>;
@@ -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>