|
|
@@ -103,7 +103,7 @@
|
|
|
<el-button type="primary" link @click="handleEdit(row)">
|
|
|
<Icon icon="mdi:note-edit-outline" width="20" height="20" />
|
|
|
</el-button>
|
|
|
- <el-button type="primary" link @click="handleViewCloudflare(row)">
|
|
|
+ <el-button data-id="live-stream-play-btn" type="primary" link @click="handleViewCloudflare(row)">
|
|
|
<Icon icon="mdi:play-circle-outline" width="20" height="20" />
|
|
|
</el-button>
|
|
|
<el-button type="danger" link @click="handleDelete(row)">
|
|
|
@@ -281,7 +281,9 @@
|
|
|
:disabled="!hasActivePoints"
|
|
|
@click="playTimeline"
|
|
|
>
|
|
|
- <el-icon v-if="!isTimelinePlaying"><VideoPlay /></el-icon>
|
|
|
+ <el-icon v-if="!isTimelinePlaying">
|
|
|
+ <VideoPlay />
|
|
|
+ </el-icon>
|
|
|
{{ isTimelinePlaying ? t('巡航中...') : t('播放巡航') }}
|
|
|
</el-button>
|
|
|
<el-button v-if="isTimelinePlaying" size="small" type="danger" @click="stopTimeline">
|
|
|
@@ -495,7 +497,9 @@
|
|
|
@click.stop="loadPTZPresets"
|
|
|
:loading="presetsLoading"
|
|
|
>
|
|
|
- <el-icon><Refresh /></el-icon>
|
|
|
+ <el-icon>
|
|
|
+ <Refresh />
|
|
|
+ </el-icon>
|
|
|
</el-button>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -509,13 +513,19 @@
|
|
|
<span class="preset-name">{{ preset.name || `Preset ${preset.id}` }}</span>
|
|
|
<div class="preset-actions">
|
|
|
<el-tooltip :content="t('跳转')" placement="top">
|
|
|
- <el-icon class="action-icon" @click="handleGotoPTZPreset(preset)"><Position /></el-icon>
|
|
|
+ <el-icon class="action-icon" @click="handleGotoPTZPreset(preset)">
|
|
|
+ <Position />
|
|
|
+ </el-icon>
|
|
|
</el-tooltip>
|
|
|
<el-tooltip :content="t('设置')" placement="top">
|
|
|
- <el-icon class="action-icon" @click="handleEditPreset(preset)"><Setting /></el-icon>
|
|
|
+ <el-icon class="action-icon" @click="handleEditPreset(preset)">
|
|
|
+ <Setting />
|
|
|
+ </el-icon>
|
|
|
</el-tooltip>
|
|
|
<el-tooltip :content="t('删除')" placement="top">
|
|
|
- <el-icon class="action-icon delete" @click="handleDeletePreset(preset)"><Close /></el-icon>
|
|
|
+ <el-icon class="action-icon delete" @click="handleDeletePreset(preset)">
|
|
|
+ <Close />
|
|
|
+ </el-icon>
|
|
|
</el-tooltip>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -530,12 +540,7 @@
|
|
|
<!-- 摄像头信息 -->
|
|
|
<el-collapse-item name="camera">
|
|
|
<template #title>
|
|
|
- <div class="collapse-title-with-action">
|
|
|
- <span class="collapse-title">{{ t('摄像头信息') }}</span>
|
|
|
- <el-button type="primary" link size="small" @click.stop="showCameraConfigDialog">
|
|
|
- <el-icon><Setting /></el-icon>
|
|
|
- </el-button>
|
|
|
- </div>
|
|
|
+ <span class="collapse-title">{{ t('摄像头信息') }}</span>
|
|
|
</template>
|
|
|
<div class="camera-info-content" v-loading="capabilitiesLoading">
|
|
|
<template v-if="cameraCapabilities">
|
|
|
@@ -568,7 +573,7 @@
|
|
|
</template>
|
|
|
<el-empty
|
|
|
v-else-if="!capabilitiesLoading"
|
|
|
- :description="cameraConnection.host ? t('点击刷新加载') : t('请配置摄像头连接')"
|
|
|
+ :description="currentMediaStream?.cameraId ? t('点击刷新加载') : t('请选择直播流')"
|
|
|
:image-size="40"
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -599,28 +604,6 @@
|
|
|
</div>
|
|
|
</template>
|
|
|
</el-drawer>
|
|
|
-
|
|
|
- <!-- 摄像头连接配置对话框 -->
|
|
|
- <el-dialog v-model="cameraConfigDialogVisible" :title="t('摄像头连接配置')" width="450px" destroy-on-close>
|
|
|
- <el-form label-width="80px">
|
|
|
- <el-form-item :label="t('IP地址')">
|
|
|
- <el-input v-model="cameraConnection.host" placeholder="192.168.0.64" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item :label="t('用户名')">
|
|
|
- <el-input v-model="cameraConnection.username" placeholder="admin" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item :label="t('密码')">
|
|
|
- <el-input v-model="cameraConnection.password" type="password" show-password placeholder="******" />
|
|
|
- </el-form-item>
|
|
|
- <el-form-item :label="t('通道')">
|
|
|
- <el-input-number v-model="cameraConnection.channel" :min="1" :max="32" />
|
|
|
- </el-form-item>
|
|
|
- </el-form>
|
|
|
- <template #footer>
|
|
|
- <el-button @click="cameraConfigDialogVisible = false">{{ t('取消') }}</el-button>
|
|
|
- <el-button type="primary" @click="saveCameraConnection">{{ t('保存') }}</el-button>
|
|
|
- </template>
|
|
|
- </el-dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
@@ -645,7 +628,6 @@ import {
|
|
|
ZoomIn,
|
|
|
ZoomOut,
|
|
|
Close,
|
|
|
- Setting,
|
|
|
Position
|
|
|
} from '@element-plus/icons-vue'
|
|
|
import { Icon } from '@iconify/vue'
|
|
|
@@ -657,19 +639,13 @@ import { adminListCameras } from '@/api/camera'
|
|
|
import { startStreamTask, stopStreamTask, getStreamPlayback } from '@/api/stream-push'
|
|
|
import VideoPlayer from '@/components/VideoPlayer.vue'
|
|
|
import CodeEditor from '@/components/CodeEditor.vue'
|
|
|
-import { getPresets, gotoPreset, type PresetInfo } from '@/api/camera'
|
|
|
+import { getPresets, gotoPreset, type PresetInfo, presetList, presetGoto, presetSet, presetRemove } from '@/api/camera'
|
|
|
import {
|
|
|
- getPTZPresets,
|
|
|
- gotoPTZPreset,
|
|
|
- removePTZPreset,
|
|
|
- getPTZCapabilities,
|
|
|
- setPTZPreset,
|
|
|
startPTZ,
|
|
|
stopPTZ,
|
|
|
startZoom,
|
|
|
type PTZPresetInfo,
|
|
|
type PTZCapabilities,
|
|
|
- type PTZConfig,
|
|
|
type PTZDirectionKey,
|
|
|
type PTZZoomKey
|
|
|
} from '@/api/ptz'
|
|
|
@@ -726,15 +702,12 @@ const ptzPresetList = ref<PTZPresetInfo[]>([])
|
|
|
const activePresetId = ref<string | null>(null)
|
|
|
const cameraCapabilities = ref<PTZCapabilities | null>(null)
|
|
|
const capabilitiesLoading = ref(false)
|
|
|
-const cameraConfigDialogVisible = ref(false)
|
|
|
-
|
|
|
-// 摄像头连接配置 (从 localStorage 读取)
|
|
|
-const cameraConnection = reactive<PTZConfig>({
|
|
|
- host: localStorage.getItem('ptz_camera_host') || '',
|
|
|
- username: localStorage.getItem('ptz_camera_username') || '',
|
|
|
- password: localStorage.getItem('ptz_camera_password') || '',
|
|
|
- channel: 1
|
|
|
-})
|
|
|
+// PTZ 配置 (使用 device ID)
|
|
|
+const ptzChannel = ref(1)
|
|
|
+const ptzConfig = computed(() => ({
|
|
|
+ host: currentMediaStream.value?.cameraId || '',
|
|
|
+ channel: ptzChannel.value
|
|
|
+}))
|
|
|
|
|
|
// 可折叠面板
|
|
|
const activePanels = ref(['ptz', 'preset', 'camera'])
|
|
|
@@ -1358,13 +1331,12 @@ function handleFullscreen() {
|
|
|
// PTZ 控制 (使用直连 PTZ API)
|
|
|
async function handlePTZ(direction: string) {
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
- const res = await startPTZ(cameraConnection, direction as PTZDirectionKey, ptzSpeed.value)
|
|
|
+ const res = await startPTZ(ptzConfig.value, direction as PTZDirectionKey, ptzSpeed.value)
|
|
|
if (!res.success) {
|
|
|
console.error('PTZ 控制失败', res.error)
|
|
|
}
|
|
|
@@ -1377,7 +1349,7 @@ async function handlePTZStop() {
|
|
|
if (!hasCameraConnection()) return
|
|
|
|
|
|
try {
|
|
|
- await stopPTZ(cameraConnection)
|
|
|
+ await stopPTZ(ptzConfig.value)
|
|
|
} catch (error) {
|
|
|
console.error('PTZ 停止失败', error)
|
|
|
}
|
|
|
@@ -1393,29 +1365,28 @@ async function handleZoomChange(val: number) {
|
|
|
if (!hasCameraConnection()) return
|
|
|
|
|
|
if (val === 0) {
|
|
|
- await stopPTZ(cameraConnection)
|
|
|
+ await stopPTZ(ptzConfig.value)
|
|
|
return
|
|
|
}
|
|
|
|
|
|
const direction: PTZZoomKey = val > 0 ? 'IN' : 'OUT'
|
|
|
- await startZoom(cameraConnection, direction, Math.abs(val))
|
|
|
+ await startZoom(ptzConfig.value, direction, Math.abs(val))
|
|
|
}
|
|
|
|
|
|
async function handleZoomRelease() {
|
|
|
zoomValue.value = 0
|
|
|
if (!hasCameraConnection()) return
|
|
|
- await stopPTZ(cameraConnection)
|
|
|
+ await stopPTZ(ptzConfig.value)
|
|
|
}
|
|
|
|
|
|
// 缩放按钮控制 (使用直连 PTZ API)
|
|
|
async function handleZoomIn() {
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
- const res = await startZoom(cameraConnection, 'IN' as PTZZoomKey, ptzSpeed.value)
|
|
|
+ const res = await startZoom(ptzConfig.value, 'IN' as PTZZoomKey, ptzSpeed.value)
|
|
|
if (!res.success) {
|
|
|
console.error('Zoom in 失败', res.error)
|
|
|
}
|
|
|
@@ -1426,12 +1397,11 @@ async function handleZoomIn() {
|
|
|
|
|
|
async function handleZoomOut() {
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
try {
|
|
|
- const res = await startZoom(cameraConnection, 'OUT' as PTZZoomKey, ptzSpeed.value)
|
|
|
+ const res = await startZoom(ptzConfig.value, 'OUT' as PTZZoomKey, ptzSpeed.value)
|
|
|
if (!res.success) {
|
|
|
console.error('Zoom out 失败', res.error)
|
|
|
}
|
|
|
@@ -1488,20 +1458,19 @@ async function handleGotoPreset(preset: PresetInfo) {
|
|
|
|
|
|
// 检查摄像头连接配置
|
|
|
function hasCameraConnection(): boolean {
|
|
|
- return !!(cameraConnection.host && cameraConnection.username && cameraConnection.password)
|
|
|
+ return !!currentMediaStream.value?.cameraId
|
|
|
}
|
|
|
|
|
|
// 加载 PTZ 预置位 (直连 PTZ 服务)
|
|
|
async function loadPTZPresets() {
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
presetsLoading.value = true
|
|
|
try {
|
|
|
- const res = await getPTZPresets(cameraConnection)
|
|
|
+ const res = await getPTZPresets(ptzConfig.value)
|
|
|
if (res.success && res.data) {
|
|
|
ptzPresetList.value = res.data
|
|
|
} else {
|
|
|
@@ -1527,7 +1496,7 @@ async function handleGotoPTZPreset(preset: PTZPresetInfo) {
|
|
|
|
|
|
try {
|
|
|
activePresetId.value = preset.id
|
|
|
- const res = await gotoPTZPreset(cameraConnection, parseInt(preset.id))
|
|
|
+ const res = await gotoPTZPreset(ptzConfig.value, parseInt(preset.id))
|
|
|
if (res.success) {
|
|
|
ElMessage.success(`${t('已跳转到预置位')}: ${preset.name || preset.id}`)
|
|
|
} else {
|
|
|
@@ -1557,7 +1526,7 @@ async function handleDeletePreset(preset: PTZPresetInfo) {
|
|
|
type: 'warning'
|
|
|
})
|
|
|
|
|
|
- const res = await removePTZPreset(cameraConnection, parseInt(preset.id))
|
|
|
+ const res = await removePTZPreset(ptzConfig.value, parseInt(preset.id))
|
|
|
if (res.success) {
|
|
|
ElMessage.success(t('删除成功'))
|
|
|
// 刷新预置位列表
|
|
|
@@ -1581,7 +1550,7 @@ async function loadCameraCapabilities() {
|
|
|
|
|
|
capabilitiesLoading.value = true
|
|
|
try {
|
|
|
- const res = await getPTZCapabilities(cameraConnection)
|
|
|
+ const res = await getPTZCapabilities(ptzConfig.value)
|
|
|
if (res.success && res.data) {
|
|
|
cameraCapabilities.value = res.data
|
|
|
} else {
|
|
|
@@ -1595,23 +1564,6 @@ async function loadCameraCapabilities() {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 显示摄像头配置对话框
|
|
|
-function showCameraConfigDialog() {
|
|
|
- cameraConfigDialogVisible.value = true
|
|
|
-}
|
|
|
-
|
|
|
-// 保存摄像头连接配置
|
|
|
-function saveCameraConnection() {
|
|
|
- localStorage.setItem('ptz_camera_host', cameraConnection.host)
|
|
|
- localStorage.setItem('ptz_camera_username', cameraConnection.username)
|
|
|
- localStorage.setItem('ptz_camera_password', cameraConnection.password)
|
|
|
- cameraConfigDialogVisible.value = false
|
|
|
- ElMessage.success(t('配置已保存'))
|
|
|
- // 自动加载预置位和能力
|
|
|
- loadPTZPresets()
|
|
|
- loadCameraCapabilities()
|
|
|
-}
|
|
|
-
|
|
|
// ==================== 时间轴功能 ====================
|
|
|
|
|
|
// 格式化时间轴时间 (秒 -> m:ss)
|
|
|
@@ -1717,7 +1669,7 @@ function selectPoint(point: TimelinePoint) {
|
|
|
|
|
|
// 如果已有预置位,跳转到该位置
|
|
|
if (point.presetId && hasCameraConnection()) {
|
|
|
- gotoPTZPreset(cameraConnection, point.presetId).then((res) => {
|
|
|
+ gotoPTZPreset(ptzConfig.value, point.presetId).then((res) => {
|
|
|
if (res.success) {
|
|
|
ElMessage.success(`${t('已跳转到')}: ${point.presetName || `Point ${point.id}`}`)
|
|
|
}
|
|
|
@@ -1730,8 +1682,7 @@ async function saveCurrentPoint() {
|
|
|
if (!selectedPoint.value) return
|
|
|
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -1742,7 +1693,7 @@ async function saveCurrentPoint() {
|
|
|
|
|
|
savingPreset.value = true
|
|
|
try {
|
|
|
- const res = await setPTZPreset(cameraConnection, presetId, presetName)
|
|
|
+ const res = await setPTZPreset(ptzConfig.value, presetId, presetName)
|
|
|
if (res.success) {
|
|
|
point.presetId = presetId
|
|
|
point.presetName = presetName
|
|
|
@@ -1814,8 +1765,7 @@ async function playTimeline() {
|
|
|
}
|
|
|
|
|
|
if (!hasCameraConnection()) {
|
|
|
- ElMessage.warning(t('请先配置摄像头连接'))
|
|
|
- showCameraConfigDialog()
|
|
|
+ ElMessage.warning(t('请先选择直播流'))
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -1844,7 +1794,7 @@ async function playTimeline() {
|
|
|
|
|
|
// 跳转到该预置位
|
|
|
if (point.presetId) {
|
|
|
- await gotoPTZPreset(cameraConnection, point.presetId)
|
|
|
+ await gotoPTZPreset(ptzConfig.value, point.presetId)
|
|
|
selectedPoint.value = point
|
|
|
}
|
|
|
|
|
|
@@ -2277,6 +2227,7 @@ onMounted(async () => {
|
|
|
background-color: #333;
|
|
|
border-color: #444;
|
|
|
}
|
|
|
+
|
|
|
.el-input__inner {
|
|
|
color: #fff;
|
|
|
}
|