|
@@ -121,36 +121,160 @@
|
|
|
<!-- 摄像头列表抽屉 -->
|
|
<!-- 摄像头列表抽屉 -->
|
|
|
<el-drawer
|
|
<el-drawer
|
|
|
v-model="cameraDrawerVisible"
|
|
v-model="cameraDrawerVisible"
|
|
|
- :title="`摄像头列表 - ${currentLss?.lssName || ''}`"
|
|
|
|
|
|
|
+ :title="`设备列表 - ${currentLss?.lssName || ''}`"
|
|
|
direction="rtl"
|
|
direction="rtl"
|
|
|
- size="600px"
|
|
|
|
|
|
|
+ size="80%"
|
|
|
destroy-on-close
|
|
destroy-on-close
|
|
|
>
|
|
>
|
|
|
<div v-loading="cameraLoading">
|
|
<div v-loading="cameraLoading">
|
|
|
- <el-empty v-if="!cameraLoading && cameraList.length === 0" description="暂无关联摄像头" />
|
|
|
|
|
- <el-table v-else :data="cameraList" stripe size="small">
|
|
|
|
|
- <el-table-column prop="cameraId" label="摄像头 ID" min-width="120" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="name" :label="t('名称')" min-width="120" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="ip" label="IP" min-width="120" />
|
|
|
|
|
- <el-table-column prop="status" :label="t('状态')" min-width="80" align="center">
|
|
|
|
|
|
|
+ <div class="camera-toolbar">
|
|
|
|
|
+ <el-form :model="cameraSearchForm" inline>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model.trim="cameraSearchForm.keyword"
|
|
|
|
|
+ placeholder="IP / 设备ID / 名称"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ style="width: 200px"
|
|
|
|
|
+ @keyup.enter="handleCameraSearch"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-select v-model="cameraSearchForm.status" placeholder="状态" clearable style="width: 120px">
|
|
|
|
|
+ <el-option label="全部" value="" />
|
|
|
|
|
+ <el-option label="在线" value="ONLINE" />
|
|
|
|
|
+ <el-option label="离线" value="OFFLINE" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button type="primary" :icon="Search" @click="handleCameraSearch">查询</el-button>
|
|
|
|
|
+ <el-button :icon="RefreshRight" @click="handleCameraReset">重置</el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <el-button type="primary" :icon="Plus" @click="handleAddCamera">{{ t('新增') }}</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <el-empty v-if="!cameraLoading && cameraList.length === 0" description="暂无关联设备" />
|
|
|
|
|
+ <el-table v-else :data="cameraList" stripe size="small" border>
|
|
|
|
|
+ <el-table-column prop="ip" label="本地IP" min-width="110" />
|
|
|
|
|
+ <el-table-column prop="cameraId" label="设备ID" min-width="100" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="name" label="名称" min-width="100" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="状态(心跳)" min-width="140">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <el-tag :type="row.status === 'ONLINE' ? 'success' : 'danger'" size="small">
|
|
|
|
|
- {{ row.status === 'ONLINE' ? '在线' : '离线' }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
|
|
+ <span :class="['status-text', row.status === 'ONLINE' ? 'status-active' : 'status-dead']">
|
|
|
|
|
+ {{ formatCameraStatus(row) }}
|
|
|
|
|
+ </span>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column prop="enabled" :label="t('启用')" min-width="70" align="center">
|
|
|
|
|
|
|
+ <el-table-column label="参数配置" min-width="80" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <el-tag :type="row.enabled ? 'success' : 'info'" size="small">
|
|
|
|
|
- {{ row.enabled ? '是' : '否' }}
|
|
|
|
|
- </el-tag>
|
|
|
|
|
|
|
+ <el-button type="primary" link @click="handleViewConfig(row)">查看</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="运行参数" min-width="80" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button type="primary" link @click="handleViewRunParams(row)">查看</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="brand" label="厂商" min-width="90">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ formatBrand(row.brand) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column prop="model" label="型号" min-width="130" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column label="添加时间" min-width="140">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ formatTime(row.createdAt) }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="设备控制" min-width="100" align="center" fixed="right">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
|
|
|
|
|
+ <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
</el-table>
|
|
</el-table>
|
|
|
- <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个摄像头</div>
|
|
|
|
|
|
|
+ <div v-if="cameraList.length > 0" class="camera-count">共 {{ cameraList.length }} 个设备</div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-drawer>
|
|
</el-drawer>
|
|
|
|
|
|
|
|
|
|
+ <!-- 摄像头编辑弹窗 -->
|
|
|
|
|
+ <el-dialog
|
|
|
|
|
+ v-model="cameraDialogVisible"
|
|
|
|
|
+ :title="isEditCamera ? '编辑摄像头' : '绑定摄像头'"
|
|
|
|
|
+ width="500px"
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form ref="cameraFormRef" :model="cameraForm" :rules="cameraRules" label-width="100px">
|
|
|
|
|
+ <!-- 新增时显示摄像头选择下拉 -->
|
|
|
|
|
+ <el-form-item v-if="!isEditCamera" label="选择摄像头" prop="selectedCameraId">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="cameraForm.selectedCameraId"
|
|
|
|
|
+ placeholder="请选择摄像头"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ @change="handleCameraSelect"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="cam in availableCameras"
|
|
|
|
|
+ :key="cam.id"
|
|
|
|
|
+ :label="`${cam.ip} - ${cam.cameraId}`"
|
|
|
|
|
+ :value="cam.id"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="IP 地址" prop="ip">
|
|
|
|
|
+ <el-input v-model="cameraForm.ip" disabled placeholder="IP 地址" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="摄像头 ID" prop="cameraId">
|
|
|
|
|
+ <el-input v-model="cameraForm.cameraId" disabled placeholder="摄像头 ID" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="名称" prop="name">
|
|
|
|
|
+ <el-input v-model="cameraForm.name" placeholder="请输入名称" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="端口" prop="port">
|
|
|
|
|
+ <el-input-number v-model="cameraForm.port" :min="1" :max="65535" style="width: 100%" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="用户名" prop="username">
|
|
|
|
|
+ <el-input v-model="cameraForm.username" placeholder="请输入用户名" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="密码" prop="password">
|
|
|
|
|
+ <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="品牌" prop="brand">
|
|
|
|
|
+ <el-select v-model="cameraForm.brand" placeholder="请选择品牌" style="width: 100%">
|
|
|
|
|
+ <el-option label="海康威视" value="hikvision" />
|
|
|
|
|
+ <el-option label="大华" value="dahua" />
|
|
|
|
|
+ <el-option label="宇视" value="uniview" />
|
|
|
|
|
+ <el-option label="其他" value="other" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="能力" prop="capability">
|
|
|
|
|
+ <el-select v-model="cameraForm.capability" placeholder="请选择能力" style="width: 100%">
|
|
|
|
|
+ <el-option label="仅切换" value="switch_only" />
|
|
|
|
|
+ <el-option label="支持 PTZ" value="ptz_enabled" />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="RTSP 地址" prop="rtspUrl">
|
|
|
|
|
+ <el-input v-model="cameraForm.rtspUrl" placeholder="请输入 RTSP 地址" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="型号" prop="model">
|
|
|
|
|
+ <el-input v-model="cameraForm.model" placeholder="请输入型号" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="通道号" prop="channelNo">
|
|
|
|
|
+ <el-input v-model="cameraForm.channelNo" placeholder="请输入通道号" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="备注" prop="remark">
|
|
|
|
|
+ <el-input v-model="cameraForm.remark" type="textarea" :rows="2" placeholder="请输入备注" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="启用状态" prop="enabled">
|
|
|
|
|
+ <el-switch v-model="cameraForm.enabled" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="cameraDialogVisible = false">{{ t('取消') }}</el-button>
|
|
|
|
|
+ <el-button type="primary" :loading="cameraSubmitting" @click="handleSubmitCamera">{{ t('确定') }}</el-button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-dialog>
|
|
|
|
|
+
|
|
|
<!-- 分页 -->
|
|
<!-- 分页 -->
|
|
|
<div class="pagination-container">
|
|
<div class="pagination-container">
|
|
|
<el-pagination
|
|
<el-pagination
|
|
@@ -168,12 +292,26 @@
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { ref, reactive, onMounted } from 'vue'
|
|
|
|
|
-import { Search, RefreshRight, Delete, View, Edit, VideoCamera } from '@element-plus/icons-vue'
|
|
|
|
|
|
|
+import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
|
|
+import { Search, RefreshRight, Delete, View, Edit, VideoCamera, Plus } from '@element-plus/icons-vue'
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
|
import { listLssNodes, deleteLssNode, setLssNodeEnabled } from '@/api/lss'
|
|
import { listLssNodes, deleteLssNode, setLssNodeEnabled } from '@/api/lss'
|
|
|
-import { adminListCameras } from '@/api/camera'
|
|
|
|
|
-import type { LssNodeDTO, LssNodeStatus, LssNodeListRequest, CameraInfoDTO } from '@/types'
|
|
|
|
|
|
|
+import {
|
|
|
|
|
+ adminListCameras,
|
|
|
|
|
+ adminAddCamera,
|
|
|
|
|
+ adminUpdateCamera,
|
|
|
|
|
+ adminDeleteCamera,
|
|
|
|
|
+ adminListAllCameras
|
|
|
|
|
+} from '@/api/camera'
|
|
|
|
|
+import type {
|
|
|
|
|
+ LssNodeDTO,
|
|
|
|
|
+ LssNodeStatus,
|
|
|
|
|
+ LssNodeListRequest,
|
|
|
|
|
+ CameraInfoDTO,
|
|
|
|
|
+ CameraAddRequest,
|
|
|
|
|
+ CameraUpdateRequest
|
|
|
|
|
+} from '@/types'
|
|
|
|
|
+import type { FormInstance, FormRules } from 'element-plus'
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
|
@@ -217,6 +355,38 @@ function formatTime(time: string | undefined): string {
|
|
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
|
return dayjs(time).format('YYYY-MM-DD HH:mm:ss')
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+// 格式化摄像头状态
|
|
|
|
|
+function formatCameraStatus(row: CameraInfoDTO): string {
|
|
|
|
|
+ if (row.status === 'ONLINE') {
|
|
|
|
|
+ return `active [${formatTime(row.updatedAt)}]`
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return `dead (离线)`
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 格式化品牌
|
|
|
|
|
+function formatBrand(brand: string | undefined): string {
|
|
|
|
|
+ const brandMap: Record<string, string> = {
|
|
|
|
|
+ hikvision: 'HIKVISION',
|
|
|
|
|
+ dahua: 'DAHUA',
|
|
|
|
|
+ uniview: 'UNIVIEW',
|
|
|
|
|
+ other: '其他'
|
|
|
|
|
+ }
|
|
|
|
|
+ return brand ? brandMap[brand] || brand.toUpperCase() : '-'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 查看参数配置
|
|
|
|
|
+function handleViewConfig(row: CameraInfoDTO) {
|
|
|
|
|
+ ElMessage.info(`查看 ${row.name} 的参数配置`)
|
|
|
|
|
+ // TODO: 打开参数配置弹窗
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 查看运行参数
|
|
|
|
|
+function handleViewRunParams(row: CameraInfoDTO) {
|
|
|
|
|
+ ElMessage.info(`查看 ${row.name} 的运行参数`)
|
|
|
|
|
+ // TODO: 打开运行参数弹窗
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
const lssList = ref<(LssNodeDTO & { _switching?: boolean })[]>([])
|
|
const lssList = ref<(LssNodeDTO & { _switching?: boolean })[]>([])
|
|
|
const tableRef = ref()
|
|
const tableRef = ref()
|
|
@@ -230,6 +400,44 @@ const cameraDrawerVisible = ref(false)
|
|
|
const cameraLoading = ref(false)
|
|
const cameraLoading = ref(false)
|
|
|
const cameraList = ref<CameraInfoDTO[]>([])
|
|
const cameraList = ref<CameraInfoDTO[]>([])
|
|
|
|
|
|
|
|
|
|
+// 摄像头搜索表单
|
|
|
|
|
+const cameraSearchForm = reactive({
|
|
|
|
|
+ keyword: '',
|
|
|
|
|
+ status: '' as 'ONLINE' | 'OFFLINE' | ''
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 摄像头编辑弹窗状态
|
|
|
|
|
+const cameraDialogVisible = ref(false)
|
|
|
|
|
+const cameraFormRef = ref<FormInstance>()
|
|
|
|
|
+const isEditCamera = ref(false)
|
|
|
|
|
+const cameraSubmitting = ref(false)
|
|
|
|
|
+const currentCamera = ref<CameraInfoDTO | null>(null)
|
|
|
|
|
+const availableCameras = ref<CameraInfoDTO[]>([])
|
|
|
|
|
+
|
|
|
|
|
+// 摄像头表单
|
|
|
|
|
+const cameraForm = reactive({
|
|
|
|
|
+ selectedCameraId: null as number | null,
|
|
|
|
|
+ cameraId: '',
|
|
|
|
|
+ name: '',
|
|
|
|
|
+ ip: '',
|
|
|
|
|
+ port: 80,
|
|
|
|
|
+ username: '',
|
|
|
|
|
+ password: '',
|
|
|
|
|
+ brand: '',
|
|
|
|
|
+ capability: 'switch_only' as 'switch_only' | 'ptz_enabled',
|
|
|
|
|
+ rtspUrl: '',
|
|
|
|
|
+ model: '',
|
|
|
|
|
+ channelNo: '',
|
|
|
|
|
+ remark: '',
|
|
|
|
|
+ enabled: true
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+// 摄像头表单验证规则(动态)
|
|
|
|
|
+const cameraRules = computed<FormRules>(() => ({
|
|
|
|
|
+ selectedCameraId: isEditCamera.value ? [] : [{ required: true, message: '请选择摄像头', trigger: 'change' }],
|
|
|
|
|
+ name: [{ required: false, message: '请输入名称', trigger: 'blur' }]
|
|
|
|
|
+}))
|
|
|
|
|
+
|
|
|
// 排序状态
|
|
// 排序状态
|
|
|
const sortState = reactive<{
|
|
const sortState = reactive<{
|
|
|
sortBy: string
|
|
sortBy: string
|
|
@@ -334,12 +542,26 @@ function handleEdit(row: LssNodeDTO) {
|
|
|
|
|
|
|
|
async function handleCameraList(row: LssNodeDTO) {
|
|
async function handleCameraList(row: LssNodeDTO) {
|
|
|
currentLss.value = row
|
|
currentLss.value = row
|
|
|
|
|
+ cameraSearchForm.keyword = ''
|
|
|
|
|
+ cameraSearchForm.status = ''
|
|
|
cameraDrawerVisible.value = true
|
|
cameraDrawerVisible.value = true
|
|
|
|
|
+ await loadCameraList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadCameraList() {
|
|
|
|
|
+ if (!currentLss.value) return
|
|
|
cameraLoading.value = true
|
|
cameraLoading.value = true
|
|
|
cameraList.value = []
|
|
cameraList.value = []
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- const res = await adminListCameras({ lssId: row.lssId })
|
|
|
|
|
|
|
+ const params: any = { lssId: currentLss.value.lssId }
|
|
|
|
|
+ if (cameraSearchForm.keyword) {
|
|
|
|
|
+ params.keyword = cameraSearchForm.keyword
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cameraSearchForm.status) {
|
|
|
|
|
+ params.status = cameraSearchForm.status
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await adminListCameras(params)
|
|
|
if (res.success && res.data) {
|
|
if (res.success && res.data) {
|
|
|
cameraList.value = res.data.list || []
|
|
cameraList.value = res.data.list || []
|
|
|
} else {
|
|
} else {
|
|
@@ -353,6 +575,162 @@ async function handleCameraList(row: LssNodeDTO) {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function handleCameraSearch() {
|
|
|
|
|
+ loadCameraList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleCameraReset() {
|
|
|
|
|
+ cameraSearchForm.keyword = ''
|
|
|
|
|
+ cameraSearchForm.status = ''
|
|
|
|
|
+ loadCameraList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function resetCameraForm() {
|
|
|
|
|
+ cameraForm.selectedCameraId = null
|
|
|
|
|
+ cameraForm.cameraId = ''
|
|
|
|
|
+ cameraForm.name = ''
|
|
|
|
|
+ cameraForm.ip = ''
|
|
|
|
|
+ cameraForm.port = 80
|
|
|
|
|
+ cameraForm.username = ''
|
|
|
|
|
+ cameraForm.password = ''
|
|
|
|
|
+ cameraForm.brand = ''
|
|
|
|
|
+ cameraForm.capability = 'switch_only'
|
|
|
|
|
+ cameraForm.rtspUrl = ''
|
|
|
|
|
+ cameraForm.model = ''
|
|
|
|
|
+ cameraForm.channelNo = ''
|
|
|
|
|
+ cameraForm.remark = ''
|
|
|
|
|
+ cameraForm.enabled = true
|
|
|
|
|
+ cameraFormRef.value?.clearValidate()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function loadAvailableCameras() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 获取所有摄像头(不传 lssId 获取未绑定的)
|
|
|
|
|
+ const res = await adminListAllCameras()
|
|
|
|
|
+ if (res.success && res.data) {
|
|
|
|
|
+ // 过滤掉已绑定到当前 LSS 的摄像头
|
|
|
|
|
+ availableCameras.value = res.data.filter((cam) => !cam.lssId || cam.lssId !== currentLss.value?.lssId)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('获取可用摄像头列表失败', error)
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleCameraSelect(cameraId: number) {
|
|
|
|
|
+ const camera = availableCameras.value.find((cam) => cam.id === cameraId)
|
|
|
|
|
+ if (camera) {
|
|
|
|
|
+ cameraForm.cameraId = camera.cameraId
|
|
|
|
|
+ cameraForm.ip = camera.ip
|
|
|
|
|
+ cameraForm.name = camera.name || ''
|
|
|
|
|
+ cameraForm.port = camera.port || 80
|
|
|
|
|
+ cameraForm.username = camera.username || ''
|
|
|
|
|
+ cameraForm.brand = camera.brand || ''
|
|
|
|
|
+ cameraForm.capability = camera.capability || 'switch_only'
|
|
|
|
|
+ cameraForm.rtspUrl = camera.rtspUrl || ''
|
|
|
|
|
+ cameraForm.model = camera.model || ''
|
|
|
|
|
+ cameraForm.channelNo = camera.channelNo || ''
|
|
|
|
|
+ cameraForm.remark = camera.remark || ''
|
|
|
|
|
+ cameraForm.enabled = camera.enabled
|
|
|
|
|
+ currentCamera.value = camera
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleAddCamera() {
|
|
|
|
|
+ isEditCamera.value = false
|
|
|
|
|
+ currentCamera.value = null
|
|
|
|
|
+ resetCameraForm()
|
|
|
|
|
+ await loadAvailableCameras()
|
|
|
|
|
+ cameraDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+function handleEditCamera(row: CameraInfoDTO) {
|
|
|
|
|
+ isEditCamera.value = true
|
|
|
|
|
+ currentCamera.value = row
|
|
|
|
|
+ cameraForm.selectedCameraId = row.id
|
|
|
|
|
+ cameraForm.cameraId = row.cameraId
|
|
|
|
|
+ cameraForm.name = row.name
|
|
|
|
|
+ cameraForm.ip = row.ip
|
|
|
|
|
+ cameraForm.port = row.port || 80
|
|
|
|
|
+ cameraForm.username = row.username || ''
|
|
|
|
|
+ cameraForm.password = ''
|
|
|
|
|
+ cameraForm.brand = row.brand || ''
|
|
|
|
|
+ cameraForm.capability = row.capability || 'switch_only'
|
|
|
|
|
+ cameraForm.rtspUrl = row.rtspUrl || ''
|
|
|
|
|
+ cameraForm.model = row.model || ''
|
|
|
|
|
+ cameraForm.channelNo = row.channelNo || ''
|
|
|
|
|
+ cameraForm.remark = row.remark || ''
|
|
|
|
|
+ cameraForm.enabled = row.enabled
|
|
|
|
|
+ cameraDialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleSubmitCamera() {
|
|
|
|
|
+ if (!cameraFormRef.value) return
|
|
|
|
|
+
|
|
|
|
|
+ await cameraFormRef.value.validate(async (valid) => {
|
|
|
|
|
+ if (!valid) return
|
|
|
|
|
+
|
|
|
|
|
+ cameraSubmitting.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!currentCamera.value) {
|
|
|
|
|
+ ElMessage.error('请选择摄像头')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 无论新增还是编辑,都是更新摄像头信息(绑定 lssId)
|
|
|
|
|
+ const data: CameraUpdateRequest = {
|
|
|
|
|
+ id: currentCamera.value.id,
|
|
|
|
|
+ name: cameraForm.name,
|
|
|
|
|
+ port: cameraForm.port,
|
|
|
|
|
+ username: cameraForm.username,
|
|
|
|
|
+ brand: cameraForm.brand,
|
|
|
|
|
+ capability: cameraForm.capability,
|
|
|
|
|
+ lssId: currentLss.value?.lssId,
|
|
|
|
|
+ rtspUrl: cameraForm.rtspUrl,
|
|
|
|
|
+ model: cameraForm.model,
|
|
|
|
|
+ channelNo: cameraForm.channelNo,
|
|
|
|
|
+ remark: cameraForm.remark,
|
|
|
|
|
+ enabled: cameraForm.enabled
|
|
|
|
|
+ }
|
|
|
|
|
+ if (cameraForm.password) {
|
|
|
|
|
+ data.password = cameraForm.password
|
|
|
|
|
+ }
|
|
|
|
|
+ const res = await adminUpdateCamera(data)
|
|
|
|
|
+ if (res.success) {
|
|
|
|
|
+ ElMessage.success(isEditCamera.value ? '更新成功' : '绑定成功')
|
|
|
|
|
+ cameraDialogVisible.value = false
|
|
|
|
|
+ loadCameraList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.errMessage || '操作失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('保存摄像头失败', error)
|
|
|
|
|
+ ElMessage.error('操作失败')
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ cameraSubmitting.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+async function handleDeleteCamera(row: CameraInfoDTO) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await ElMessageBox.confirm(`确定要删除摄像头 "${row.name}" 吗?`, '提示', {
|
|
|
|
|
+ type: 'warning'
|
|
|
|
|
+ })
|
|
|
|
|
+ const res = await adminDeleteCamera(row.id)
|
|
|
|
|
+ if (res.success) {
|
|
|
|
|
+ ElMessage.success('删除成功')
|
|
|
|
|
+ loadCameraList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.errMessage || '删除失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (error !== 'cancel') {
|
|
|
|
|
+ console.error('删除摄像头失败', error)
|
|
|
|
|
+ ElMessage.error('删除失败')
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
async function handleToggleEnabled(row: LssNodeDTO & { _switching?: boolean }, enabled: boolean) {
|
|
async function handleToggleEnabled(row: LssNodeDTO & { _switching?: boolean }, enabled: boolean) {
|
|
|
row._switching = true
|
|
row._switching = true
|
|
|
try {
|
|
try {
|
|
@@ -462,6 +840,20 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.camera-toolbar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ margin-bottom: 16px;
|
|
|
|
|
+
|
|
|
|
|
+ :deep(.el-form) {
|
|
|
|
|
+ .el-form-item {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+ margin-right: 12px;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.camera-count {
|
|
.camera-count {
|
|
|
margin-top: 16px;
|
|
margin-top: 16px;
|
|
|
text-align: right;
|
|
text-align: right;
|
|
@@ -469,6 +861,22 @@ onMounted(() => {
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.status-text {
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-active {
|
|
|
|
|
+ color: #67c23a;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-hold {
|
|
|
|
|
+ color: #e6a23c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-dead {
|
|
|
|
|
+ color: #f56c6c;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
// 抽屉样式
|
|
// 抽屉样式
|
|
|
:deep(.el-drawer) {
|
|
:deep(.el-drawer) {
|
|
|
.el-drawer__header {
|
|
.el-drawer__header {
|