新增耳机阻抗
This commit is contained in:
@@ -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;
|
||||
@@ -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()`),
|
||||
});
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user