|
|
@@ -1,16 +1,29 @@
|
|
|
<template>
|
|
|
<div class="page-container">
|
|
|
- <!-- 搜索区域 -->
|
|
|
+ <!-- 搜索表单 -->
|
|
|
<div class="search-form">
|
|
|
- <el-form :model="queryParams" inline>
|
|
|
- <el-form-item label="机器">
|
|
|
- <el-select
|
|
|
- v-model="queryParams.machineId"
|
|
|
- placeholder="请选择机器"
|
|
|
- style="width: 120px"
|
|
|
+ <el-form :model="searchForm" inline data-id="search-form">
|
|
|
+ <el-form-item :label="t('摄像头ID')">
|
|
|
+ <el-input
|
|
|
+ v-model.trim="searchForm.cameraId"
|
|
|
+ placeholder="请输入摄像头ID"
|
|
|
clearable
|
|
|
- @change="handleQuery"
|
|
|
- >
|
|
|
+ data-id="search-camera-id"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="t('名称')">
|
|
|
+ <el-input
|
|
|
+ v-model.trim="searchForm.name"
|
|
|
+ placeholder="请输入名称"
|
|
|
+ clearable
|
|
|
+ data-id="search-name"
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="t('所属机器')">
|
|
|
+ <el-select v-model="searchForm.machineId" placeholder="全部" clearable data-id="search-machine">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
<el-option
|
|
|
v-for="machine in machineList"
|
|
|
:key="machine.machineId"
|
|
|
@@ -19,108 +32,204 @@
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="状态">
|
|
|
- <el-select
|
|
|
- v-model="queryParams.status"
|
|
|
- placeholder="请选择"
|
|
|
- style="width: 120px"
|
|
|
- clearable
|
|
|
- @change="handleQuery"
|
|
|
- >
|
|
|
+ <el-form-item :label="t('状态')">
|
|
|
+ <el-select v-model="searchForm.status" placeholder="全部" clearable data-id="search-status">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
<el-option label="在线" value="ONLINE" />
|
|
|
<el-option label="离线" value="OFFLINE" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
+ <el-form-item :label="t('启用状态')">
|
|
|
+ <el-select v-model="searchForm.enabled" placeholder="全部" clearable data-id="search-enabled">
|
|
|
+ <el-option label="全部" value="" />
|
|
|
+ <el-option label="已启用" :value="true" />
|
|
|
+ <el-option label="已禁用" :value="false" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item :label="t('创建时间')">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="searchForm.dateRange"
|
|
|
+ type="daterange"
|
|
|
+ range-separator="至"
|
|
|
+ start-placeholder="开始日期"
|
|
|
+ end-placeholder="结束日期"
|
|
|
+ value-format="YYYY-MM-DD"
|
|
|
+ data-id="search-date-range"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
<el-form-item>
|
|
|
- <el-button type="primary" :icon="Search" @click="handleQuery">搜索</el-button>
|
|
|
- <el-button :icon="Refresh" @click="resetQuery">重置</el-button>
|
|
|
+ <el-button type="primary" :icon="Search" data-id="btn-search" @click="handleSearch">
|
|
|
+ {{ t('查询') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button :icon="RefreshRight" data-id="btn-reset" @click="handleReset">{{ t('重置') }}</el-button>
|
|
|
+ <el-button type="primary" :icon="Plus" data-id="btn-add-camera" @click="handleAdd">
|
|
|
+ {{ t('新增') }}
|
|
|
+ </el-button>
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 操作按钮 -->
|
|
|
- <div class="table-actions">
|
|
|
- <el-button type="primary" :icon="Plus" @click="handleAdd">新增摄像头</el-button>
|
|
|
- <el-button plain :icon="Refresh" @click="getList">刷新列表</el-button>
|
|
|
+ <!-- 数据表格 -->
|
|
|
+ <div class="table-wrapper">
|
|
|
+ <el-table
|
|
|
+ ref="tableRef"
|
|
|
+ v-loading="loading"
|
|
|
+ :data="sortedList"
|
|
|
+ stripe
|
|
|
+ size="default"
|
|
|
+ data-id="camera-table"
|
|
|
+ height="100%"
|
|
|
+ @sort-change="handleSortChange"
|
|
|
+ >
|
|
|
+ <el-table-column
|
|
|
+ prop="cameraId"
|
|
|
+ :label="t('摄像头ID')"
|
|
|
+ min-width="120"
|
|
|
+ sortable="custom"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ <el-table-column prop="name" :label="t('名称')" min-width="120" sortable="custom" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-link type="primary" :data-id="`link-edit-${row.cameraId}`" @click="handleEdit(row)">
|
|
|
+ {{ row.name }}
|
|
|
+ </el-link>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="ip" :label="t('IP地址')" min-width="130" sortable="custom" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="port" :label="t('端口')" width="80" sortable="custom" align="center" />
|
|
|
+ <el-table-column prop="brand" :label="t('品牌')" min-width="100" sortable="custom" show-overflow-tooltip>
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ getBrandLabel(row.brand) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column
|
|
|
+ prop="machineName"
|
|
|
+ :label="t('所属机器')"
|
|
|
+ min-width="100"
|
|
|
+ sortable="custom"
|
|
|
+ show-overflow-tooltip
|
|
|
+ />
|
|
|
+ <el-table-column prop="capability" :label="t('能力')" width="100" sortable="custom" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.capability === 'ptz_enabled' ? 'success' : 'info'">
|
|
|
+ {{ row.capability === 'ptz_enabled' ? 'PTZ' : t('仅切换') }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="status" :label="t('状态')" width="80" sortable="custom" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
|
|
|
+ {{ row.status === 'ONLINE' ? t('在线') : t('离线') }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="enabled" :label="t('启用')" width="80" sortable="custom" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="row.enabled ? 'success' : 'info'">
|
|
|
+ {{ row.enabled ? t('是') : t('否') }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column prop="createdAt" :label="t('创建时间')" width="160" sortable="custom" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatDateTime(row.createdAt) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column :label="t('操作')" min-width="200" align="center" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button
|
|
|
+ type="primary"
|
|
|
+ link
|
|
|
+ :icon="View"
|
|
|
+ :data-id="`btn-channel-${row.cameraId}`"
|
|
|
+ @click="handleChannel(row)"
|
|
|
+ >
|
|
|
+ {{ t('通道') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="success"
|
|
|
+ link
|
|
|
+ :icon="Connection"
|
|
|
+ :data-id="`btn-check-${row.cameraId}`"
|
|
|
+ @click="handleCheck(row)"
|
|
|
+ >
|
|
|
+ {{ t('检测') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button type="primary" link :icon="Edit" :data-id="`btn-edit-${row.cameraId}`" @click="handleEdit(row)">
|
|
|
+ {{ t('编辑') }}
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ type="danger"
|
|
|
+ link
|
|
|
+ :icon="Delete"
|
|
|
+ :disabled="deleteLoading"
|
|
|
+ :data-id="`btn-delete-${row.cameraId}`"
|
|
|
+ @click="handleDelete(row)"
|
|
|
+ >
|
|
|
+ {{ t('删除') }}
|
|
|
+ </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
</div>
|
|
|
|
|
|
- <!-- 数据表格 -->
|
|
|
- <el-table v-loading="loading" :data="filteredList" border>
|
|
|
- <el-table-column type="index" label="序号" width="60" align="center" />
|
|
|
- <el-table-column prop="cameraId" label="摄像头ID" min-width="120" show-overflow-tooltip />
|
|
|
- <el-table-column prop="name" label="名称" min-width="120" show-overflow-tooltip />
|
|
|
- <el-table-column prop="ip" label="IP地址" min-width="130" show-overflow-tooltip />
|
|
|
- <el-table-column prop="port" label="端口" width="80" align="center" />
|
|
|
- <el-table-column prop="brand" label="品牌" min-width="100" show-overflow-tooltip />
|
|
|
- <el-table-column prop="machineName" label="所属机器" min-width="100" show-overflow-tooltip />
|
|
|
- <el-table-column prop="capability" label="能力" width="100" align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="row.capability === 'ptz_enabled' ? 'success' : 'info'">
|
|
|
- {{ row.capability === 'ptz_enabled' ? 'PTZ' : '仅切换' }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="status" label="状态" width="80" align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
|
|
|
- {{ row.status === 'ONLINE' ? '在线' : '离线' }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column prop="enabled" label="启用" width="80" align="center">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-tag :type="row.enabled ? 'success' : 'info'">
|
|
|
- {{ row.enabled ? '是' : '否' }}
|
|
|
- </el-tag>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- <el-table-column label="操作" width="260" align="center" fixed="right">
|
|
|
- <template #default="{ row }">
|
|
|
- <el-button type="primary" link :icon="View" @click="handleChannel(row)">通道</el-button>
|
|
|
- <el-button type="success" link :icon="Connection" @click="handleCheck(row)">检测</el-button>
|
|
|
- <el-button type="primary" link :icon="Edit" @click="handleEdit(row)">编辑</el-button>
|
|
|
- <el-button type="danger" link :icon="Delete" @click="handleDelete(row)">删除</el-button>
|
|
|
- </template>
|
|
|
- </el-table-column>
|
|
|
- </el-table>
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination-container">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="currentPage"
|
|
|
+ v-model:page-size="pageSize"
|
|
|
+ :page-sizes="[10, 20, 50, 100]"
|
|
|
+ :total="total"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ background
|
|
|
+ @size-change="handleSizeChange"
|
|
|
+ @current-change="handleCurrentChange"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
|
|
|
<!-- 新增/编辑弹窗 -->
|
|
|
- <el-dialog v-model="dialogVisible" :title="dialogTitle" width="650px" destroy-on-close>
|
|
|
- <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
|
|
|
- <el-form-item label="摄像头ID" prop="cameraId">
|
|
|
- <el-input v-model="form.cameraId" placeholder="请输入摄像头ID" :disabled="isEdit" />
|
|
|
+ <el-dialog v-model="dialogVisible" :title="dialogTitle" width="650px" destroy-on-close data-id="dialog-camera">
|
|
|
+ <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" data-id="form-camera">
|
|
|
+ <el-form-item :label="t('摄像头ID')" prop="cameraId">
|
|
|
+ <el-input v-model="form.cameraId" placeholder="请输入摄像头ID" :disabled="isEdit" data-id="input-camera-id" />
|
|
|
</el-form-item>
|
|
|
- <el-form-item label="名称" prop="name">
|
|
|
- <el-input v-model="form.name" placeholder="请输入名称" />
|
|
|
+ <el-form-item :label="t('名称')" prop="name">
|
|
|
+ <el-input v-model="form.name" placeholder="请输入名称" data-id="input-name" />
|
|
|
</el-form-item>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="IP地址" prop="ip">
|
|
|
- <el-input v-model="form.ip" placeholder="请输入IP地址" />
|
|
|
+ <el-form-item :label="t('IP地址')" prop="ip">
|
|
|
+ <el-input v-model="form.ip" placeholder="请输入IP地址" data-id="input-ip" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="端口" prop="port">
|
|
|
- <el-input-number v-model="form.port" :min="1" :max="65535" />
|
|
|
+ <el-form-item :label="t('端口')" prop="port">
|
|
|
+ <el-input-number v-model="form.port" :min="1" :max="65535" data-id="input-port" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="用户名" prop="username">
|
|
|
- <el-input v-model="form.username" placeholder="请输入用户名" />
|
|
|
+ <el-form-item :label="t('用户名')" prop="username">
|
|
|
+ <el-input v-model="form.username" placeholder="请输入用户名" data-id="input-username" />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="密码" prop="password">
|
|
|
- <el-input v-model="form.password" type="password" placeholder="请输入密码" show-password />
|
|
|
+ <el-form-item :label="t('密码')" prop="password">
|
|
|
+ <el-input
|
|
|
+ v-model="form.password"
|
|
|
+ type="password"
|
|
|
+ placeholder="请输入密码"
|
|
|
+ show-password
|
|
|
+ data-id="input-password"
|
|
|
+ />
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="20">
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="品牌" prop="brand">
|
|
|
- <el-select v-model="form.brand" placeholder="请选择">
|
|
|
+ <el-form-item :label="t('品牌')" prop="brand">
|
|
|
+ <el-select v-model="form.brand" placeholder="请选择" data-id="select-brand">
|
|
|
<el-option label="海康威视" value="hikvision" />
|
|
|
<el-option label="大华" value="dahua" />
|
|
|
<el-option label="其他" value="other" />
|
|
|
@@ -128,16 +237,16 @@
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
<el-col :span="12">
|
|
|
- <el-form-item label="能力" prop="capability">
|
|
|
- <el-select v-model="form.capability" placeholder="请选择">
|
|
|
+ <el-form-item :label="t('能力')" prop="capability">
|
|
|
+ <el-select v-model="form.capability" placeholder="请选择" data-id="select-capability">
|
|
|
<el-option label="仅切换" value="switch_only" />
|
|
|
<el-option label="支持PTZ" value="ptz_enabled" />
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
</el-col>
|
|
|
</el-row>
|
|
|
- <el-form-item label="所属机器" prop="machineId">
|
|
|
- <el-select v-model="form.machineId" placeholder="请选择机器" clearable>
|
|
|
+ <el-form-item :label="t('所属机器')" prop="machineId">
|
|
|
+ <el-select v-model="form.machineId" placeholder="请选择机器" clearable data-id="select-machine">
|
|
|
<el-option
|
|
|
v-for="machine in machineList"
|
|
|
:key="machine.machineId"
|
|
|
@@ -146,33 +255,41 @@
|
|
|
/>
|
|
|
</el-select>
|
|
|
</el-form-item>
|
|
|
- <el-form-item v-if="isEdit" label="启用状态">
|
|
|
- <el-switch v-model="form.enabled" />
|
|
|
+ <el-form-item v-if="isEdit" :label="t('启用状态')">
|
|
|
+ <el-switch v-model="form.enabled" data-id="switch-enabled" />
|
|
|
</el-form-item>
|
|
|
</el-form>
|
|
|
<template #footer>
|
|
|
- <el-button @click="dialogVisible = false">取消</el-button>
|
|
|
- <el-button type="primary" :loading="submitLoading" @click="handleSubmit">确定</el-button>
|
|
|
+ <el-button data-id="btn-cancel" @click="dialogVisible = false">{{ t('取消') }}</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" data-id="btn-submit" @click="handleSubmit">
|
|
|
+ {{ t('确定') }}
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
</el-dialog>
|
|
|
|
|
|
<!-- 通道弹窗 -->
|
|
|
- <el-dialog v-model="channelDialogVisible" title="通道列表" width="700px" destroy-on-close>
|
|
|
- <el-table :data="currentChannels" border>
|
|
|
- <el-table-column prop="channelId" label="通道ID" min-width="100" />
|
|
|
- <el-table-column prop="name" label="名称" min-width="100" />
|
|
|
- <el-table-column prop="rtspUrl" label="RTSP地址" min-width="200" show-overflow-tooltip />
|
|
|
- <el-table-column prop="defaultView" label="默认视角" width="90" align="center">
|
|
|
+ <el-dialog
|
|
|
+ v-model="channelDialogVisible"
|
|
|
+ :title="t('通道列表')"
|
|
|
+ width="700px"
|
|
|
+ destroy-on-close
|
|
|
+ data-id="dialog-channel"
|
|
|
+ >
|
|
|
+ <el-table :data="currentChannels" border stripe>
|
|
|
+ <el-table-column prop="channelId" :label="t('通道ID')" min-width="100" />
|
|
|
+ <el-table-column prop="name" :label="t('名称')" min-width="100" />
|
|
|
+ <el-table-column prop="rtspUrl" :label="t('RTSP地址')" min-width="200" show-overflow-tooltip />
|
|
|
+ <el-table-column prop="defaultView" :label="t('默认视角')" width="90" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.defaultView ? 'success' : 'info'">
|
|
|
- {{ row.defaultView ? '是' : '否' }}
|
|
|
+ {{ row.defaultView ? t('是') : t('否') }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
- <el-table-column prop="status" label="状态" width="80" align="center">
|
|
|
+ <el-table-column prop="status" :label="t('状态')" width="80" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'">
|
|
|
- {{ row.status === 'ONLINE' ? '在线' : '离线' }}
|
|
|
+ {{ row.status === 'ONLINE' ? t('在线') : t('离线') }}
|
|
|
</el-tag>
|
|
|
</template>
|
|
|
</el-table-column>
|
|
|
@@ -184,13 +301,34 @@
|
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
-import { Search, Refresh, Plus, View, Edit, Delete, Connection } from '@element-plus/icons-vue'
|
|
|
+import { Plus, Edit, Delete, Search, RefreshRight, View, Connection } from '@element-plus/icons-vue'
|
|
|
import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminCheckCamera } from '@/api/camera'
|
|
|
import { listAllMachines } from '@/api/machine'
|
|
|
import type { CameraInfoDTO, ChannelInfoDTO, CameraAddRequest, CameraUpdateRequest, MachineDTO } from '@/types'
|
|
|
+import dayjs from 'dayjs'
|
|
|
+import { useI18n } from 'vue-i18n'
|
|
|
+
|
|
|
+const { t } = useI18n({ useScope: 'global' })
|
|
|
+
|
|
|
+// 格式化时间
|
|
|
+function formatDateTime(dateStr: string | undefined): string {
|
|
|
+ if (!dateStr) return '-'
|
|
|
+ return dayjs(dateStr).format('YYYY-MM-DD HH:mm')
|
|
|
+}
|
|
|
+
|
|
|
+// 获取品牌标签
|
|
|
+function getBrandLabel(brand: string): string {
|
|
|
+ const brandMap: Record<string, string> = {
|
|
|
+ hikvision: '海康威视',
|
|
|
+ dahua: '大华',
|
|
|
+ other: '其他'
|
|
|
+ }
|
|
|
+ return brandMap[brand] || brand
|
|
|
+}
|
|
|
|
|
|
const loading = ref(false)
|
|
|
const submitLoading = ref(false)
|
|
|
+const deleteLoading = ref(false)
|
|
|
const cameraList = ref<CameraInfoDTO[]>([])
|
|
|
const machineList = ref<MachineDTO[]>([])
|
|
|
const dialogVisible = ref(false)
|
|
|
@@ -198,9 +336,64 @@ const channelDialogVisible = ref(false)
|
|
|
const currentChannels = ref<ChannelInfoDTO[]>([])
|
|
|
const formRef = ref<FormInstance>()
|
|
|
|
|
|
-const queryParams = reactive({
|
|
|
+// 排序状态
|
|
|
+const sortState = reactive<{
|
|
|
+ prop: string
|
|
|
+ order: 'ascending' | 'descending' | null
|
|
|
+}>({
|
|
|
+ prop: '',
|
|
|
+ order: null
|
|
|
+})
|
|
|
+
|
|
|
+// 搜索表单
|
|
|
+const searchForm = reactive<{
|
|
|
+ cameraId: string
|
|
|
+ name: string
|
|
|
+ machineId: string
|
|
|
+ status: '' | 'ONLINE' | 'OFFLINE'
|
|
|
+ enabled: boolean | ''
|
|
|
+ dateRange: [string, string] | null
|
|
|
+}>({
|
|
|
+ cameraId: '',
|
|
|
+ name: '',
|
|
|
machineId: '',
|
|
|
- status: '' as '' | 'ONLINE' | 'OFFLINE'
|
|
|
+ status: '',
|
|
|
+ enabled: '',
|
|
|
+ dateRange: null
|
|
|
+})
|
|
|
+
|
|
|
+// 分页相关
|
|
|
+const currentPage = ref(1)
|
|
|
+const pageSize = ref(20)
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+// 排序后的数据
|
|
|
+const sortedList = computed(() => {
|
|
|
+ const list = [...cameraList.value]
|
|
|
+ if (sortState.prop && sortState.order) {
|
|
|
+ list.sort((a, b) => {
|
|
|
+ const aVal = a[sortState.prop as keyof CameraInfoDTO]
|
|
|
+ const bVal = b[sortState.prop as keyof CameraInfoDTO]
|
|
|
+
|
|
|
+ // 处理空值
|
|
|
+ if (aVal == null && bVal == null) return 0
|
|
|
+ if (aVal == null) return sortState.order === 'ascending' ? -1 : 1
|
|
|
+ if (bVal == null) return sortState.order === 'ascending' ? 1 : -1
|
|
|
+
|
|
|
+ // 比较
|
|
|
+ let result = 0
|
|
|
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
|
|
|
+ result = aVal - bVal
|
|
|
+ } else if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
|
|
|
+ result = aVal === bVal ? 0 : aVal ? 1 : -1
|
|
|
+ } else {
|
|
|
+ result = String(aVal).localeCompare(String(bVal))
|
|
|
+ }
|
|
|
+
|
|
|
+ return sortState.order === 'ascending' ? result : -result
|
|
|
+ })
|
|
|
+ }
|
|
|
+ return list
|
|
|
})
|
|
|
|
|
|
const form = reactive<{
|
|
|
@@ -229,24 +422,16 @@ const form = reactive<{
|
|
|
})
|
|
|
|
|
|
const isEdit = computed(() => !!form.id)
|
|
|
-const dialogTitle = computed(() => (isEdit.value ? '编辑摄像头' : '新增摄像头'))
|
|
|
-
|
|
|
-const filteredList = computed(() => {
|
|
|
- let list = cameraList.value
|
|
|
- if (queryParams.status) {
|
|
|
- list = list.filter((item) => item.status === queryParams.status)
|
|
|
- }
|
|
|
- return list
|
|
|
-})
|
|
|
+const dialogTitle = computed(() => (isEdit.value ? t('编辑摄像头') : t('新增摄像头')))
|
|
|
|
|
|
const rules: FormRules = {
|
|
|
- cameraId: [{ required: true, message: '请输入摄像头ID', trigger: 'blur' }],
|
|
|
- name: [{ required: true, message: '请输入名称', trigger: 'blur' }],
|
|
|
+ cameraId: [{ required: true, message: t('请输入摄像头ID'), trigger: 'blur' }],
|
|
|
+ name: [{ required: true, message: t('请输入名称'), trigger: 'blur' }],
|
|
|
ip: [
|
|
|
- { required: true, message: '请输入IP地址', trigger: 'blur' },
|
|
|
+ { required: true, message: t('请输入IP地址'), trigger: 'blur' },
|
|
|
{
|
|
|
pattern: /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/,
|
|
|
- message: '请输入正确的IP地址',
|
|
|
+ message: t('请输入正确的IP地址'),
|
|
|
trigger: 'blur'
|
|
|
}
|
|
|
]
|
|
|
@@ -259,33 +444,73 @@ async function getMachines() {
|
|
|
machineList.value = res.data
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('获取机器列表失败', error)
|
|
|
+ console.error(t('获取机器列表失败'), error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function getList() {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
- const params = {
|
|
|
- machineId: queryParams.machineId || undefined,
|
|
|
- status: queryParams.status || undefined
|
|
|
+ // 构建查询参数
|
|
|
+ const params: Record<string, any> = {
|
|
|
+ page: currentPage.value,
|
|
|
+ size: pageSize.value
|
|
|
+ }
|
|
|
+ // 搜索关键词(摄像头ID或名称)
|
|
|
+ if (searchForm.cameraId || searchForm.name) {
|
|
|
+ params.keyword = searchForm.cameraId || searchForm.name
|
|
|
+ }
|
|
|
+ // 机器过滤
|
|
|
+ if (searchForm.machineId) {
|
|
|
+ params.machineId = searchForm.machineId
|
|
|
}
|
|
|
+ // 状态过滤
|
|
|
+ if (searchForm.status) {
|
|
|
+ params.status = searchForm.status
|
|
|
+ }
|
|
|
+ // 启用状态过滤
|
|
|
+ if (searchForm.enabled !== '') {
|
|
|
+ params.enabled = searchForm.enabled
|
|
|
+ }
|
|
|
+ // 排序
|
|
|
+ if (sortState.prop && sortState.order) {
|
|
|
+ params.sortBy = sortState.prop
|
|
|
+ params.sortDir = sortState.order === 'ascending' ? 'ASC' : 'DESC'
|
|
|
+ }
|
|
|
+
|
|
|
const res = await adminListCameras(params)
|
|
|
if (res.success) {
|
|
|
cameraList.value = res.data.list
|
|
|
+ total.value = res.data.total || 0
|
|
|
}
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-function handleQuery() {
|
|
|
+function handleSearch() {
|
|
|
+ currentPage.value = 1 // 搜索时重置到第一页
|
|
|
getList()
|
|
|
}
|
|
|
|
|
|
-function resetQuery() {
|
|
|
- queryParams.machineId = ''
|
|
|
- queryParams.status = ''
|
|
|
+function handleReset() {
|
|
|
+ searchForm.cameraId = ''
|
|
|
+ searchForm.name = ''
|
|
|
+ searchForm.machineId = ''
|
|
|
+ searchForm.status = ''
|
|
|
+ searchForm.enabled = ''
|
|
|
+ searchForm.dateRange = null
|
|
|
+ currentPage.value = 1
|
|
|
+ // 重置排序
|
|
|
+ sortState.prop = ''
|
|
|
+ sortState.order = null
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+// 排序变化处理
|
|
|
+function handleSortChange({ prop, order }: { prop: string; order: 'ascending' | 'descending' | null }) {
|
|
|
+ sortState.prop = prop || ''
|
|
|
+ sortState.order = order
|
|
|
getList()
|
|
|
}
|
|
|
|
|
|
@@ -333,30 +558,35 @@ async function handleCheck(row: CameraInfoDTO) {
|
|
|
const res = await adminCheckCamera(row.id)
|
|
|
if (res.success) {
|
|
|
if (res.data) {
|
|
|
- ElMessage.success('摄像头连接正常')
|
|
|
+ ElMessage.success(t('摄像头连接正常'))
|
|
|
} else {
|
|
|
- ElMessage.warning('摄像头连接失败')
|
|
|
+ ElMessage.warning(t('摄像头连接失败'))
|
|
|
}
|
|
|
}
|
|
|
} catch (error) {
|
|
|
- console.error('检测失败', error)
|
|
|
+ console.error(t('检测失败'), error)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function handleDelete(row: CameraInfoDTO) {
|
|
|
+ if (deleteLoading.value) return
|
|
|
+
|
|
|
try {
|
|
|
- await ElMessageBox.confirm(`确定要删除摄像头 "${row.name}" 吗?`, '提示', {
|
|
|
+ await ElMessageBox.confirm(`${t('确定要删除摄像头')} "${row.name}" ${t('吗?')}`, t('提示'), {
|
|
|
type: 'warning'
|
|
|
})
|
|
|
+ deleteLoading.value = true
|
|
|
const res = await adminDeleteCamera(row.id)
|
|
|
if (res.success) {
|
|
|
- ElMessage.success('删除成功')
|
|
|
+ ElMessage.success(t('删除成功'))
|
|
|
getList()
|
|
|
}
|
|
|
} catch (error) {
|
|
|
if (error !== 'cancel') {
|
|
|
- console.error('删除失败', error)
|
|
|
+ console.error(t('删除失败'), error)
|
|
|
}
|
|
|
+ } finally {
|
|
|
+ deleteLoading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -382,7 +612,7 @@ async function handleSubmit() {
|
|
|
}
|
|
|
const res = await adminUpdateCamera(updateData)
|
|
|
if (res.success) {
|
|
|
- ElMessage.success('修改成功')
|
|
|
+ ElMessage.success(t('修改成功'))
|
|
|
dialogVisible.value = false
|
|
|
getList()
|
|
|
}
|
|
|
@@ -400,7 +630,7 @@ async function handleSubmit() {
|
|
|
}
|
|
|
const res = await adminAddCamera(addData)
|
|
|
if (res.success) {
|
|
|
- ElMessage.success('新增成功')
|
|
|
+ ElMessage.success(t('新增成功'))
|
|
|
dialogVisible.value = false
|
|
|
getList()
|
|
|
}
|
|
|
@@ -412,6 +642,17 @@ async function handleSubmit() {
|
|
|
})
|
|
|
}
|
|
|
|
|
|
+function handleSizeChange(val: number) {
|
|
|
+ pageSize.value = val
|
|
|
+ currentPage.value = 1
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
+function handleCurrentChange(val: number) {
|
|
|
+ currentPage.value = val
|
|
|
+ getList()
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
getMachines()
|
|
|
getList()
|
|
|
@@ -420,17 +661,159 @@ onMounted(() => {
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
.page-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
padding: 1rem;
|
|
|
+ box-sizing: border-box;
|
|
|
+ overflow: hidden;
|
|
|
}
|
|
|
|
|
|
.search-form {
|
|
|
- background-color: #fff;
|
|
|
- border-radius: var(--radius-base);
|
|
|
- padding: 0;
|
|
|
- margin-bottom: 0;
|
|
|
+ flex-shrink: 0;
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 16px 16px 4px 16px;
|
|
|
+ background: #f5f7fa;
|
|
|
+
|
|
|
+ :deep(.el-form-item) {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ margin-right: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-input),
|
|
|
+ :deep(.el-select) {
|
|
|
+ width: 160px;
|
|
|
+ }
|
|
|
+
|
|
|
+ :deep(.el-date-editor--daterange) {
|
|
|
+ width: 280px;
|
|
|
+
|
|
|
+ .el-range-input {
|
|
|
+ width: 90px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-range-separator {
|
|
|
+ width: 30px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Indigo 主题按钮
|
|
|
+ :deep(.el-button--primary) {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ background-color: #6366f1;
|
|
|
+ border-color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.table-wrapper {
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination-container {
|
|
|
+ flex-shrink: 0;
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+ padding-top: 16px;
|
|
|
+
|
|
|
+ // Indigo 主题分页
|
|
|
+ :deep(.el-pagination) {
|
|
|
+ .el-pager li.is-active {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ color: #fff;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-pager li:not(.is-active):hover {
|
|
|
+ color: #4f46e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .btn-prev:hover,
|
|
|
+ .btn-next:hover {
|
|
|
+ color: #4f46e5;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 表格样式
|
|
|
+:deep(.el-table) {
|
|
|
+ // 斑马纹对比度
|
|
|
+ --el-table-row-hover-bg-color: #f0f0ff;
|
|
|
+
|
|
|
+ .el-table__row--striped td.el-table__cell {
|
|
|
+ background-color: #f8f9fc;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 表头样式
|
|
|
+ .el-table__header th {
|
|
|
+ background-color: #f5f7fa;
|
|
|
+ color: #333;
|
|
|
+ font-weight: 600;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 链接颜色
|
|
|
+ .el-link--primary {
|
|
|
+ color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 主要按钮颜色
|
|
|
+ .el-button--primary.is-link {
|
|
|
+ color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序图标颜色
|
|
|
+ .el-table__column-filter-trigger,
|
|
|
+ .caret-wrapper {
|
|
|
+ .sort-caret.ascending {
|
|
|
+ border-bottom-color: #4f46e5;
|
|
|
+ }
|
|
|
+
|
|
|
+ .sort-caret.descending {
|
|
|
+ border-top-color: #4f46e5;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
-.table-actions {
|
|
|
- margin-bottom: 15px;
|
|
|
+// 弹窗 Indigo 主题
|
|
|
+:deep(.el-dialog) {
|
|
|
+ .el-dialog__header {
|
|
|
+ border-bottom: 1px solid #e5e7eb;
|
|
|
+ padding-bottom: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-dialog__footer {
|
|
|
+ border-top: 1px solid #e5e7eb;
|
|
|
+ padding-top: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ .el-button--primary {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+
|
|
|
+ &:hover,
|
|
|
+ &:focus {
|
|
|
+ background-color: #6366f1;
|
|
|
+ border-color: #6366f1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Switch Indigo 主题
|
|
|
+ .el-switch.is-checked .el-switch__core {
|
|
|
+ background-color: #4f46e5;
|
|
|
+ border-color: #4f46e5;
|
|
|
+ }
|
|
|
}
|
|
|
</style>
|