|
@@ -3,11 +3,19 @@
|
|
|
<!-- 搜索表单 -->
|
|
<!-- 搜索表单 -->
|
|
|
<div class="search-form">
|
|
<div class="search-form">
|
|
|
<el-form :model="searchForm" inline>
|
|
<el-form :model="searchForm" inline>
|
|
|
- <el-form-item :label="t('Stream SN')">
|
|
|
|
|
- <el-input v-model.trim="searchForm.aoAgent" placeholder="请输入" clearable @keyup.enter="handleSearch" />
|
|
|
|
|
|
|
+ <el-form-item :label="t('关键词')">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model.trim="searchForm.keyword"
|
|
|
|
|
+ placeholder="搜索通道ID/名称"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @keyup.enter="handleSearch"
|
|
|
|
|
+ />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item :label="t('功能')">
|
|
|
|
|
- <el-input v-model.trim="searchForm.feature" placeholder="请输入功能" clearable @keyup.enter="handleSearch" />
|
|
|
|
|
|
|
+ <el-form-item :label="t('状态')">
|
|
|
|
|
+ <el-select v-model="searchForm.enabled" placeholder="全部" clearable style="width: 120px">
|
|
|
|
|
+ <el-option :label="t('启用')" :value="true" />
|
|
|
|
|
+ <el-option :label="t('禁用')" :value="false" />
|
|
|
|
|
+ </el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
<el-form-item>
|
|
<el-form-item>
|
|
|
<el-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
|
|
<el-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
|
|
@@ -22,76 +30,55 @@
|
|
|
<el-table
|
|
<el-table
|
|
|
ref="tableRef"
|
|
ref="tableRef"
|
|
|
v-loading="loading"
|
|
v-loading="loading"
|
|
|
- :data="streamList"
|
|
|
|
|
|
|
+ :data="channelList"
|
|
|
stripe
|
|
stripe
|
|
|
size="default"
|
|
size="default"
|
|
|
height="100%"
|
|
height="100%"
|
|
|
@sort-change="handleSortChange"
|
|
@sort-change="handleSortChange"
|
|
|
>
|
|
>
|
|
|
- <el-table-column prop="streamSn" :label="t('stream sn')" show-overflow-tooltip />
|
|
|
|
|
- <el-table-column prop="name" :label="t('name')" show-overflow-tooltip>
|
|
|
|
|
|
|
+ <el-table-column prop="channelId" :label="t('通道ID')" width="160" show-overflow-tooltip />
|
|
|
|
|
+ <el-table-column prop="name" :label="t('名称')" show-overflow-tooltip>
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
<el-link type="primary" @click="handleEdit(row)">{{ row.name }}</el-link>
|
|
<el-link type="primary" @click="handleEdit(row)">{{ row.name }}</el-link>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column prop="lssName" :label="t('LSS')" show-overflow-tooltip>
|
|
|
|
|
|
|
+ <el-table-column prop="mode" :label="t('推流模式')" width="100" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <span>{{ row.lssName ? `${row.lssId}/${row.lssName}` : '-' }}</span>
|
|
|
|
|
|
|
+ <el-tag size="small" :type="getModeTagType(row.mode)">{{ row.mode || '-' }}</el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column prop="cameraName" :label="t('摄像头编号')" show-overflow-tooltip>
|
|
|
|
|
|
|
+ <el-table-column prop="hlsPlaybackUrl" :label="t('HLS 播放地址')" show-overflow-tooltip>
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <span>{{ row.cameraName || '-' }}</span>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="streamMethod" :label="t('推流方式')" align="center">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-tag size="small">{{ row.streamMethod }}</el-tag>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-table-column>
|
|
|
|
|
- <el-table-column prop="commandTemplate" :label="t('命令模板')" align="center">
|
|
|
|
|
- <template #default="{ row }">
|
|
|
|
|
- <el-link v-if="row.commandTemplate" type="primary" @click="showCommandTemplate(row)">
|
|
|
|
|
- {{ t('查看') }}
|
|
|
|
|
|
|
+ <el-link v-if="row.hlsPlaybackUrl" type="primary" @click="handleCopy(row.hlsPlaybackUrl)">
|
|
|
|
|
+ {{ truncateUrl(row.hlsPlaybackUrl) }}
|
|
|
</el-link>
|
|
</el-link>
|
|
|
<span v-else>-</span>
|
|
<span v-else>-</span>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column :label="t('操作')" align="center">
|
|
|
|
|
|
|
+ <el-table-column prop="recordingEnabled" :label="t('录制')" width="80" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <el-button
|
|
|
|
|
- v-if="row.status !== 'running'"
|
|
|
|
|
- type="primary"
|
|
|
|
|
- link
|
|
|
|
|
- :loading="actionLoading[row.id]"
|
|
|
|
|
- @click="handleStart(row)"
|
|
|
|
|
- >
|
|
|
|
|
- {{ t('启动') }}
|
|
|
|
|
- </el-button>
|
|
|
|
|
- <el-button v-else type="danger" link :loading="actionLoading[row.id]" @click="handleStop(row)">
|
|
|
|
|
- {{ t('关闭') }}
|
|
|
|
|
- </el-button>
|
|
|
|
|
|
|
+ <el-tag size="small" :type="row.recordingEnabled ? 'success' : 'info'">
|
|
|
|
|
+ {{ row.recordingEnabled ? t('是') : t('否') }}
|
|
|
|
|
+ </el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column prop="startedAt" :label="t('启动时间')" align="center">
|
|
|
|
|
|
|
+ <el-table-column prop="enabled" :label="t('状态')" width="80" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- {{ formatDateTime(row.startedAt) }}
|
|
|
|
|
|
|
+ <el-tag size="small" :type="row.enabled ? 'success' : 'danger'">
|
|
|
|
|
+ {{ row.enabled ? t('启用') : t('禁用') }}
|
|
|
|
|
+ </el-tag>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column prop="stoppedAt" :label="t('关闭时间')" align="center">
|
|
|
|
|
|
|
+ <el-table-column prop="createdAt" :label="t('创建时间')" width="160" align="center">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- {{ formatDateTime(row.stoppedAt) }}
|
|
|
|
|
|
|
+ {{ formatDateTime(row.createdAt) }}
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
- <el-table-column :label="t('观看')" align="center">
|
|
|
|
|
|
|
+ <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
|
|
|
<template #default="{ row }">
|
|
<template #default="{ row }">
|
|
|
- <el-button
|
|
|
|
|
- type="primary"
|
|
|
|
|
- link
|
|
|
|
|
- :icon="View"
|
|
|
|
|
- :disabled="row.status !== 'running'"
|
|
|
|
|
- @click="handleWatch(row)"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <el-button type="primary" link @click="handleEdit(row)">{{ t('编辑') }}</el-button>
|
|
|
|
|
+ <el-button type="primary" link :icon="View" @click="handleWatch(row)">{{ t('观看') }}</el-button>
|
|
|
|
|
+ <el-button type="danger" link @click="handleDelete(row)">{{ t('删除') }}</el-button>
|
|
|
</template>
|
|
</template>
|
|
|
</el-table-column>
|
|
</el-table-column>
|
|
|
</el-table>
|
|
</el-table>
|
|
@@ -115,7 +102,7 @@
|
|
|
<el-drawer
|
|
<el-drawer
|
|
|
v-model="drawerVisible"
|
|
v-model="drawerVisible"
|
|
|
direction="rtl"
|
|
direction="rtl"
|
|
|
- size="500px"
|
|
|
|
|
|
|
+ size="550px"
|
|
|
:with-header="false"
|
|
:with-header="false"
|
|
|
destroy-on-close
|
|
destroy-on-close
|
|
|
class="stream-drawer"
|
|
class="stream-drawer"
|
|
@@ -127,39 +114,64 @@
|
|
|
ref="formRef"
|
|
ref="formRef"
|
|
|
:model="form"
|
|
:model="form"
|
|
|
:rules="rules"
|
|
:rules="rules"
|
|
|
- label-width="80px"
|
|
|
|
|
|
|
+ label-width="130px"
|
|
|
label-position="left"
|
|
label-position="left"
|
|
|
class="stream-form"
|
|
class="stream-form"
|
|
|
>
|
|
>
|
|
|
- <el-form-item label="name:" prop="name">
|
|
|
|
|
- <el-input v-model="form.name" placeholder="live-stream name" style="width: 200px" />
|
|
|
|
|
|
|
+ <el-form-item label="通道 ID:" prop="channelId">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="form.channelId"
|
|
|
|
|
+ placeholder="例如: cf_channel_001"
|
|
|
|
|
+ :disabled="isEdit"
|
|
|
|
|
+ style="width: 280px"
|
|
|
|
|
+ />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item label="LSS ID:" prop="lssId">
|
|
|
|
|
- <el-select v-model="form.lssId" placeholder="请选择" clearable filterable style="width: 200px">
|
|
|
|
|
- <el-option v-for="lss in lssOptions" :key="lss.id" :label="lss.lssId" :value="lss.id" />
|
|
|
|
|
- </el-select>
|
|
|
|
|
|
|
+ <el-form-item label="通道名称:" prop="name">
|
|
|
|
|
+ <el-input v-model="form.name" placeholder="例如: 主推流通道" style="width: 280px" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item label="设备ID:" prop="cameraId">
|
|
|
|
|
- <el-select v-model="form.cameraId" placeholder="请选择" clearable filterable style="width: 260px">
|
|
|
|
|
- <el-option
|
|
|
|
|
- v-for="camera in cameraOptions"
|
|
|
|
|
- :key="camera.id"
|
|
|
|
|
- :label="camera.cameraId"
|
|
|
|
|
- :value="camera.id"
|
|
|
|
|
- />
|
|
|
|
|
|
|
+ <el-form-item label="Account ID:" prop="accountId">
|
|
|
|
|
+ <el-input v-model="form.accountId" placeholder="Cloudflare 账户 ID" style="width: 280px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="API Token:" prop="apiToken">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="form.apiToken"
|
|
|
|
|
+ placeholder="Cloudflare API Token"
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ show-password
|
|
|
|
|
+ style="width: 280px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="Live Input ID:" prop="liveInputId">
|
|
|
|
|
+ <el-input v-model="form.liveInputId" placeholder="Cloudflare Live Input ID" style="width: 280px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="Stream Key:" prop="streamKey">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="form.streamKey"
|
|
|
|
|
+ placeholder="流密钥 (用于 RTMPS)"
|
|
|
|
|
+ type="password"
|
|
|
|
|
+ show-password
|
|
|
|
|
+ style="width: 280px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="Customer 子域名:" prop="customerSubdomain">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="form.customerSubdomain"
|
|
|
|
|
+ placeholder="例如: customer-pj89kn2ke2tcuh19"
|
|
|
|
|
+ style="width: 280px"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item label="推流模式:" prop="mode">
|
|
|
|
|
+ <el-select v-model="form.mode" placeholder="请选择" style="width: 280px">
|
|
|
|
|
+ <el-option label="WHIP (WebRTC)" value="WHIP" />
|
|
|
|
|
+ <el-option label="RTMPS" value="RTMPS" />
|
|
|
|
|
+ <el-option label="SRT" value="SRT" />
|
|
|
</el-select>
|
|
</el-select>
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
- <el-form-item label="命令模板:" prop="commandTemplate">
|
|
|
|
|
- <div class="textarea-wrapper">
|
|
|
|
|
- <el-input
|
|
|
|
|
- v-model="form.commandTemplate"
|
|
|
|
|
- type="textarea"
|
|
|
|
|
- :rows="10"
|
|
|
|
|
- placeholder="请输入运行参数内容"
|
|
|
|
|
- maxlength="1000"
|
|
|
|
|
- show-word-limit
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <el-form-item label="启用录制:" prop="recordingEnabled">
|
|
|
|
|
+ <el-switch v-model="form.recordingEnabled" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item v-if="isEdit" label="启用状态:" prop="enabled">
|
|
|
|
|
+ <el-switch v-model="form.enabled" />
|
|
|
</el-form-item>
|
|
</el-form-item>
|
|
|
</el-form>
|
|
</el-form>
|
|
|
</div>
|
|
</div>
|
|
@@ -171,31 +183,16 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-drawer>
|
|
</el-drawer>
|
|
|
-
|
|
|
|
|
- <!-- 命令模板查看弹窗 -->
|
|
|
|
|
- <el-dialog v-model="templateDialogVisible" :title="t('命令模板')" width="600px">
|
|
|
|
|
- <pre class="command-template">{{ currentTemplate }}</pre>
|
|
|
|
|
- <template #footer>
|
|
|
|
|
- <el-button type="primary" @click="templateDialogVisible = false">{{ t('关闭') }}</el-button>
|
|
|
|
|
- </template>
|
|
|
|
|
- </el-dialog>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
import { ref, reactive, onMounted, computed } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
import { useRouter } from 'vue-router'
|
|
|
-import { ElMessage, type FormInstance, type FormRules } from 'element-plus'
|
|
|
|
|
|
|
+import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
import { Search, RefreshRight, View, Plus } from '@element-plus/icons-vue'
|
|
import { Search, RefreshRight, View, Plus } from '@element-plus/icons-vue'
|
|
|
-import {
|
|
|
|
|
- listLiveStreams,
|
|
|
|
|
- addLiveStream,
|
|
|
|
|
- updateLiveStream,
|
|
|
|
|
- startLiveStream,
|
|
|
|
|
- stopLiveStream,
|
|
|
|
|
- getCameraOptions
|
|
|
|
|
-} from '@/api/live-stream'
|
|
|
|
|
-import { listAllLssNodes } from '@/api/lss'
|
|
|
|
|
-import type { LiveStreamDTO, LssNodeDTO, CameraInfoDTO, StreamMethod } from '@/types'
|
|
|
|
|
|
|
+import { listStreamChannels, addStreamChannel, updateStreamChannel, deleteStreamChannel } from '@/api/stream-channel'
|
|
|
|
|
+import type { StreamChannelDTO, StreamChannelMode } from '@/types'
|
|
|
import dayjs from 'dayjs'
|
|
import dayjs from 'dayjs'
|
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useI18n } from 'vue-i18n'
|
|
|
|
|
|
|
@@ -205,21 +202,46 @@ const router = useRouter()
|
|
|
// 格式化时间
|
|
// 格式化时间
|
|
|
function formatDateTime(dateStr: string | undefined): string {
|
|
function formatDateTime(dateStr: string | undefined): string {
|
|
|
if (!dateStr) return '-'
|
|
if (!dateStr) return '-'
|
|
|
- return dayjs(dateStr).format('YYYYMMDD-HH:mm:ss')
|
|
|
|
|
|
|
+ return dayjs(dateStr).format('YYYY-MM-DD HH:mm:ss')
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 获取模式标签颜色
|
|
|
|
|
+function getModeTagType(mode?: string): 'success' | 'warning' | 'info' {
|
|
|
|
|
+ switch (mode) {
|
|
|
|
|
+ case 'WHIP':
|
|
|
|
|
+ return 'success'
|
|
|
|
|
+ case 'RTMPS':
|
|
|
|
|
+ return 'warning'
|
|
|
|
|
+ case 'SRT':
|
|
|
|
|
+ return 'info'
|
|
|
|
|
+ default:
|
|
|
|
|
+ return 'info'
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 截断 URL 显示
|
|
|
|
|
+function truncateUrl(url: string): string {
|
|
|
|
|
+ if (url.length > 50) {
|
|
|
|
|
+ return url.substring(0, 50) + '...'
|
|
|
|
|
+ }
|
|
|
|
|
+ return url
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 复制到剪贴板
|
|
|
|
|
+async function handleCopy(text: string) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await navigator.clipboard.writeText(text)
|
|
|
|
|
+ ElMessage.success(t('已复制到剪贴板'))
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ ElMessage.error(t('复制失败'))
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const loading = ref(false)
|
|
const loading = ref(false)
|
|
|
const submitLoading = ref(false)
|
|
const submitLoading = ref(false)
|
|
|
-const actionLoading = ref<Record<number, boolean>>({})
|
|
|
|
|
-const streamList = ref<LiveStreamDTO[]>([])
|
|
|
|
|
|
|
+const channelList = ref<StreamChannelDTO[]>([])
|
|
|
const drawerVisible = ref(false)
|
|
const drawerVisible = ref(false)
|
|
|
-const templateDialogVisible = ref(false)
|
|
|
|
|
const formRef = ref<FormInstance>()
|
|
const formRef = ref<FormInstance>()
|
|
|
-const currentTemplate = ref('')
|
|
|
|
|
-
|
|
|
|
|
-// 下拉选项
|
|
|
|
|
-const lssOptions = ref<LssNodeDTO[]>([])
|
|
|
|
|
-const cameraOptions = ref<CameraInfoDTO[]>([])
|
|
|
|
|
|
|
|
|
|
// 排序状态
|
|
// 排序状态
|
|
|
const sortState = reactive<{
|
|
const sortState = reactive<{
|
|
@@ -231,9 +253,12 @@ const sortState = reactive<{
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 搜索表单
|
|
// 搜索表单
|
|
|
-const searchForm = reactive({
|
|
|
|
|
- aoAgent: '',
|
|
|
|
|
- feature: ''
|
|
|
|
|
|
|
+const searchForm = reactive<{
|
|
|
|
|
+ keyword: string
|
|
|
|
|
+ enabled: boolean | null
|
|
|
|
|
+}>({
|
|
|
|
|
+ keyword: '',
|
|
|
|
|
+ enabled: null
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
// 分页相关
|
|
// 分页相关
|
|
@@ -244,313 +269,64 @@ const total = ref(0)
|
|
|
// 表单数据
|
|
// 表单数据
|
|
|
const form = reactive<{
|
|
const form = reactive<{
|
|
|
id?: number
|
|
id?: number
|
|
|
- streamSn: string
|
|
|
|
|
|
|
+ channelId: string
|
|
|
name: string
|
|
name: string
|
|
|
- lssId?: number
|
|
|
|
|
- cameraId?: number
|
|
|
|
|
- streamMethod: StreamMethod
|
|
|
|
|
- commandTemplate: string
|
|
|
|
|
|
|
+ accountId: string
|
|
|
|
|
+ apiToken: string
|
|
|
|
|
+ liveInputId: string
|
|
|
|
|
+ streamKey: string
|
|
|
|
|
+ customerSubdomain: string
|
|
|
|
|
+ mode: StreamChannelMode
|
|
|
|
|
+ recordingEnabled: boolean
|
|
|
|
|
+ enabled: boolean
|
|
|
}>({
|
|
}>({
|
|
|
- streamSn: '',
|
|
|
|
|
|
|
+ channelId: '',
|
|
|
name: '',
|
|
name: '',
|
|
|
- lssId: undefined,
|
|
|
|
|
- cameraId: undefined,
|
|
|
|
|
- streamMethod: 'ffmpeg',
|
|
|
|
|
- commandTemplate: ''
|
|
|
|
|
|
|
+ accountId: '',
|
|
|
|
|
+ apiToken: '',
|
|
|
|
|
+ liveInputId: '',
|
|
|
|
|
+ streamKey: '',
|
|
|
|
|
+ customerSubdomain: '',
|
|
|
|
|
+ mode: 'WHIP',
|
|
|
|
|
+ recordingEnabled: false,
|
|
|
|
|
+ enabled: true
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const isEdit = computed(() => !!form.id)
|
|
const isEdit = computed(() => !!form.id)
|
|
|
-const drawerTitle = computed(() => (isEdit.value ? t('编辑live-stream') : t('新增live-stream')))
|
|
|
|
|
|
|
+const drawerTitle = computed(() => (isEdit.value ? t('编辑推流通道') : t('新增推流通道')))
|
|
|
|
|
|
|
|
const rules: FormRules = {
|
|
const rules: FormRules = {
|
|
|
- name: [{ required: true, message: t('请输入名称'), trigger: 'blur' }],
|
|
|
|
|
- streamMethod: [{ required: true, message: t('请选择推流方式'), trigger: 'change' }]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 生成测试数据
|
|
|
|
|
-function generateMockData(): LiveStreamDTO[] {
|
|
|
|
|
- const mockData: LiveStreamDTO[] = [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- streamSn: 'LS-20260119-001',
|
|
|
|
|
- name: '大厅摄像头直播',
|
|
|
|
|
- lssId: 1,
|
|
|
|
|
- lssName: 'LSS-Tokyo-01',
|
|
|
|
|
- cameraId: 101,
|
|
|
|
|
- cameraName: 'CAM-LOBBY-01',
|
|
|
|
|
- streamMethod: 'ffmpeg',
|
|
|
|
|
- commandTemplate:
|
|
|
|
|
- 'ffmpeg -i rtsp://192.168.1.100:554/stream1 -c:v libx264 -preset ultrafast -tune zerolatency -f flv rtmp://live.example.com/app/stream1',
|
|
|
|
|
- status: 'running',
|
|
|
|
|
- startedAt: '2026-01-19T11:11:11',
|
|
|
|
|
- stoppedAt: undefined,
|
|
|
|
|
- playUrl: 'https://live.example.com/hls/stream1.m3u8',
|
|
|
|
|
- createdAt: '2026-01-15T10:00:00',
|
|
|
|
|
- updatedAt: '2026-01-19T11:11:11'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- streamSn: 'LS-20260119-002',
|
|
|
|
|
- name: '入口监控',
|
|
|
|
|
- lssId: 1,
|
|
|
|
|
- lssName: 'LSS-Tokyo-01',
|
|
|
|
|
- cameraId: 102,
|
|
|
|
|
- cameraName: 'CAM-ENTRANCE-01',
|
|
|
|
|
- streamMethod: 'ffmpeg',
|
|
|
|
|
- commandTemplate:
|
|
|
|
|
- 'ffmpeg -i rtsp://192.168.1.101:554/stream1 -c:v libx264 -f flv rtmp://live.example.com/app/stream2',
|
|
|
|
|
- status: 'stopped',
|
|
|
|
|
- startedAt: '2026-01-18T09:00:00',
|
|
|
|
|
- stoppedAt: '2026-01-18T18:00:00',
|
|
|
|
|
- playUrl: undefined,
|
|
|
|
|
- createdAt: '2026-01-15T10:30:00',
|
|
|
|
|
- updatedAt: '2026-01-18T18:00:00'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3,
|
|
|
|
|
- streamSn: 'LS-20260119-003',
|
|
|
|
|
- name: '仓库区域',
|
|
|
|
|
- lssId: 2,
|
|
|
|
|
- lssName: 'LSS-Osaka-01',
|
|
|
|
|
- cameraId: 103,
|
|
|
|
|
- cameraName: 'CAM-WAREHOUSE-01',
|
|
|
|
|
- streamMethod: 'obs',
|
|
|
|
|
- commandTemplate: undefined,
|
|
|
|
|
- status: 'running',
|
|
|
|
|
- startedAt: '2026-01-19T08:30:00',
|
|
|
|
|
- stoppedAt: undefined,
|
|
|
|
|
- playUrl: 'https://live.example.com/hls/stream3.m3u8',
|
|
|
|
|
- createdAt: '2026-01-16T14:00:00',
|
|
|
|
|
- updatedAt: '2026-01-19T08:30:00'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 4,
|
|
|
|
|
- streamSn: 'LS-20260119-004',
|
|
|
|
|
- name: '停车场入口',
|
|
|
|
|
- lssId: 2,
|
|
|
|
|
- lssName: 'LSS-Osaka-01',
|
|
|
|
|
- cameraId: 104,
|
|
|
|
|
- cameraName: 'CAM-PARKING-01',
|
|
|
|
|
- streamMethod: 'ffmpeg',
|
|
|
|
|
- commandTemplate:
|
|
|
|
|
- 'ffmpeg -i rtsp://192.168.1.104:554/ch1 -c:v copy -c:a aac -f flv rtmp://live.example.com/app/parking',
|
|
|
|
|
- status: 'error',
|
|
|
|
|
- startedAt: '2026-01-19T07:00:00',
|
|
|
|
|
- stoppedAt: '2026-01-19T07:15:00',
|
|
|
|
|
- playUrl: undefined,
|
|
|
|
|
- createdAt: '2026-01-17T09:00:00',
|
|
|
|
|
- updatedAt: '2026-01-19T07:15:00'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 5,
|
|
|
|
|
- streamSn: 'LS-20260119-005',
|
|
|
|
|
- name: '会议室A',
|
|
|
|
|
- lssId: 1,
|
|
|
|
|
- lssName: 'LSS-Tokyo-01',
|
|
|
|
|
- cameraId: 105,
|
|
|
|
|
- cameraName: 'CAM-MEETING-A',
|
|
|
|
|
- streamMethod: 'gstreamer',
|
|
|
|
|
- commandTemplate:
|
|
|
|
|
- 'gst-launch-1.0 rtspsrc location=rtsp://192.168.1.105:554/stream ! rtph264depay ! h264parse ! flvmux ! rtmpsink location=rtmp://live.example.com/app/meetingA',
|
|
|
|
|
- status: 'stopped',
|
|
|
|
|
- startedAt: undefined,
|
|
|
|
|
- stoppedAt: undefined,
|
|
|
|
|
- playUrl: undefined,
|
|
|
|
|
- createdAt: '2026-01-18T11:00:00',
|
|
|
|
|
- updatedAt: '2026-01-18T11:00:00'
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
- return mockData
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 生成测试 LSS 选项
|
|
|
|
|
-function generateMockLssOptions(): LssDTO[] {
|
|
|
|
|
- return [
|
|
|
|
|
- {
|
|
|
|
|
- id: 1,
|
|
|
|
|
- lssId: 'LSS-001',
|
|
|
|
|
- name: 'LSS-Tokyo-01',
|
|
|
|
|
- address: '192.168.1.10',
|
|
|
|
|
- publicIp: '203.0.113.10',
|
|
|
|
|
- heartbeat: 'active',
|
|
|
|
|
- heartbeatTime: '2026-01-19T12:00:00'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 2,
|
|
|
|
|
- lssId: 'LSS-002',
|
|
|
|
|
- name: 'LSS-Osaka-01',
|
|
|
|
|
- address: '192.168.2.10',
|
|
|
|
|
- publicIp: '203.0.113.20',
|
|
|
|
|
- heartbeat: 'active',
|
|
|
|
|
- heartbeatTime: '2026-01-19T12:00:00'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 3,
|
|
|
|
|
- lssId: 'LSS-003',
|
|
|
|
|
- name: 'LSS-Nagoya-01',
|
|
|
|
|
- address: '192.168.3.10',
|
|
|
|
|
- publicIp: '203.0.113.30',
|
|
|
|
|
- heartbeat: 'hold',
|
|
|
|
|
- heartbeatTime: '2026-01-19T11:50:00'
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-// 生成测试摄像头选项
|
|
|
|
|
-function generateMockCameraOptions(): CameraInfoDTO[] {
|
|
|
|
|
- return [
|
|
|
|
|
- {
|
|
|
|
|
- id: 101,
|
|
|
|
|
- cameraId: 'CAM-001',
|
|
|
|
|
- name: 'CAM-LOBBY-01',
|
|
|
|
|
- ip: '192.168.1.100',
|
|
|
|
|
- port: 554,
|
|
|
|
|
- username: 'admin',
|
|
|
|
|
- brand: 'HIKVISION',
|
|
|
|
|
- capability: 'ptz_enabled',
|
|
|
|
|
- status: 'ONLINE',
|
|
|
|
|
- machineId: 'M001',
|
|
|
|
|
- machineName: '机器1',
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- channels: [],
|
|
|
|
|
- createdAt: '2026-01-01',
|
|
|
|
|
- updatedAt: '2026-01-19'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 102,
|
|
|
|
|
- cameraId: 'CAM-002',
|
|
|
|
|
- name: 'CAM-ENTRANCE-01',
|
|
|
|
|
- ip: '192.168.1.101',
|
|
|
|
|
- port: 554,
|
|
|
|
|
- username: 'admin',
|
|
|
|
|
- brand: 'DAHUA',
|
|
|
|
|
- capability: 'switch_only',
|
|
|
|
|
- status: 'ONLINE',
|
|
|
|
|
- machineId: 'M001',
|
|
|
|
|
- machineName: '机器1',
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- channels: [],
|
|
|
|
|
- createdAt: '2026-01-01',
|
|
|
|
|
- updatedAt: '2026-01-19'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 103,
|
|
|
|
|
- cameraId: 'CAM-003',
|
|
|
|
|
- name: 'CAM-WAREHOUSE-01',
|
|
|
|
|
- ip: '192.168.1.102',
|
|
|
|
|
- port: 554,
|
|
|
|
|
- username: 'admin',
|
|
|
|
|
- brand: 'HIKVISION',
|
|
|
|
|
- capability: 'ptz_enabled',
|
|
|
|
|
- status: 'ONLINE',
|
|
|
|
|
- machineId: 'M002',
|
|
|
|
|
- machineName: '机器2',
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- channels: [],
|
|
|
|
|
- createdAt: '2026-01-01',
|
|
|
|
|
- updatedAt: '2026-01-19'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 104,
|
|
|
|
|
- cameraId: 'CAM-004',
|
|
|
|
|
- name: 'CAM-PARKING-01',
|
|
|
|
|
- ip: '192.168.1.104',
|
|
|
|
|
- port: 554,
|
|
|
|
|
- username: 'admin',
|
|
|
|
|
- brand: 'AXIS',
|
|
|
|
|
- capability: 'switch_only',
|
|
|
|
|
- status: 'OFFLINE',
|
|
|
|
|
- machineId: 'M002',
|
|
|
|
|
- machineName: '机器2',
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- channels: [],
|
|
|
|
|
- createdAt: '2026-01-01',
|
|
|
|
|
- updatedAt: '2026-01-19'
|
|
|
|
|
- },
|
|
|
|
|
- {
|
|
|
|
|
- id: 105,
|
|
|
|
|
- cameraId: 'CAM-005',
|
|
|
|
|
- name: 'CAM-MEETING-A',
|
|
|
|
|
- ip: '192.168.1.105',
|
|
|
|
|
- port: 554,
|
|
|
|
|
- username: 'admin',
|
|
|
|
|
- brand: 'SONY',
|
|
|
|
|
- capability: 'ptz_enabled',
|
|
|
|
|
- status: 'ONLINE',
|
|
|
|
|
- machineId: 'M001',
|
|
|
|
|
- machineName: '机器1',
|
|
|
|
|
- enabled: true,
|
|
|
|
|
- channels: [],
|
|
|
|
|
- createdAt: '2026-01-01',
|
|
|
|
|
- updatedAt: '2026-01-19'
|
|
|
|
|
- }
|
|
|
|
|
- ]
|
|
|
|
|
|
|
+ channelId: [{ required: true, message: t('请输入通道ID'), trigger: 'blur' }],
|
|
|
|
|
+ name: [{ required: true, message: t('请输入通道名称'), trigger: 'blur' }],
|
|
|
|
|
+ liveInputId: [{ required: true, message: t('请输入 Live Input ID'), trigger: 'blur' }],
|
|
|
|
|
+ customerSubdomain: [{ required: true, message: t('请输入 Customer 子域名'), trigger: 'blur' }]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async function getList() {
|
|
async function getList() {
|
|
|
loading.value = true
|
|
loading.value = true
|
|
|
try {
|
|
try {
|
|
|
- // 使用测试数据(后端 API 准备好后可切换)
|
|
|
|
|
- const useMockData = true
|
|
|
|
|
-
|
|
|
|
|
- if (useMockData) {
|
|
|
|
|
- // 模拟网络延迟
|
|
|
|
|
- await new Promise((resolve) => setTimeout(resolve, 300))
|
|
|
|
|
- let mockData = generateMockData()
|
|
|
|
|
-
|
|
|
|
|
- // 搜索过滤
|
|
|
|
|
- if (searchForm.aoAgent) {
|
|
|
|
|
- mockData = mockData.filter(
|
|
|
|
|
- (item) =>
|
|
|
|
|
- item.streamSn.toLowerCase().includes(searchForm.aoAgent.toLowerCase()) ||
|
|
|
|
|
- item.name.toLowerCase().includes(searchForm.aoAgent.toLowerCase())
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- streamList.value = mockData
|
|
|
|
|
- total.value = mockData.length
|
|
|
|
|
- } else {
|
|
|
|
|
- const params: Record<string, any> = {
|
|
|
|
|
- page: currentPage.value,
|
|
|
|
|
- size: pageSize.value
|
|
|
|
|
- }
|
|
|
|
|
- if (searchForm.aoAgent) {
|
|
|
|
|
- params.aoAgent = searchForm.aoAgent
|
|
|
|
|
- }
|
|
|
|
|
- if (searchForm.feature) {
|
|
|
|
|
- params.feature = searchForm.feature
|
|
|
|
|
- }
|
|
|
|
|
- if (sortState.prop && sortState.order) {
|
|
|
|
|
- params.sortBy = sortState.prop
|
|
|
|
|
- params.sortDir = sortState.order === 'ascending' ? 'ASC' : 'DESC'
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const res = await listLiveStreams(params)
|
|
|
|
|
- if (res.success) {
|
|
|
|
|
- streamList.value = res.data.list
|
|
|
|
|
- total.value = res.data.total || 0
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ const params: Record<string, any> = {
|
|
|
|
|
+ page: currentPage.value,
|
|
|
|
|
+ size: pageSize.value
|
|
|
}
|
|
}
|
|
|
- } finally {
|
|
|
|
|
- loading.value = false
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function loadOptions() {
|
|
|
|
|
- try {
|
|
|
|
|
- // 获取 LSS 节点列表
|
|
|
|
|
- const lssRes = await listAllLssNodes()
|
|
|
|
|
- if (lssRes.success && lssRes.data) {
|
|
|
|
|
- lssOptions.value = lssRes.data.list || []
|
|
|
|
|
|
|
+ if (searchForm.keyword) {
|
|
|
|
|
+ params.keyword = searchForm.keyword
|
|
|
|
|
+ }
|
|
|
|
|
+ if (searchForm.enabled !== null) {
|
|
|
|
|
+ params.enabled = searchForm.enabled
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sortState.prop && sortState.order) {
|
|
|
|
|
+ params.sortBy = sortState.prop
|
|
|
|
|
+ params.sortDir = sortState.order === 'ascending' ? 'ASC' : 'DESC'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 获取摄像头列表(如需要可后续添加)
|
|
|
|
|
- const cameraRes = await getCameraOptions()
|
|
|
|
|
- if (cameraRes.success && cameraRes.data) {
|
|
|
|
|
- cameraOptions.value = cameraRes.data
|
|
|
|
|
|
|
+ const res = await listStreamChannels(params)
|
|
|
|
|
+ if (res.success) {
|
|
|
|
|
+ channelList.value = res.data.list
|
|
|
|
|
+ total.value = res.data.total || 0
|
|
|
}
|
|
}
|
|
|
- } catch (error) {
|
|
|
|
|
- console.error('加载选项失败', error)
|
|
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -560,8 +336,8 @@ function handleSearch() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function handleReset() {
|
|
function handleReset() {
|
|
|
- searchForm.aoAgent = ''
|
|
|
|
|
- searchForm.feature = ''
|
|
|
|
|
|
|
+ searchForm.keyword = ''
|
|
|
|
|
+ searchForm.enabled = null
|
|
|
currentPage.value = 1
|
|
currentPage.value = 1
|
|
|
sortState.prop = ''
|
|
sortState.prop = ''
|
|
|
sortState.order = null
|
|
sortState.order = null
|
|
@@ -577,76 +353,70 @@ function handleSortChange({ prop, order }: { prop: string; order: 'ascending' |
|
|
|
function handleAdd() {
|
|
function handleAdd() {
|
|
|
Object.assign(form, {
|
|
Object.assign(form, {
|
|
|
id: undefined,
|
|
id: undefined,
|
|
|
- streamSn: '',
|
|
|
|
|
|
|
+ channelId: '',
|
|
|
name: '',
|
|
name: '',
|
|
|
- lssId: undefined,
|
|
|
|
|
- cameraId: undefined,
|
|
|
|
|
- streamMethod: 'ffmpeg',
|
|
|
|
|
- commandTemplate: ''
|
|
|
|
|
|
|
+ accountId: '',
|
|
|
|
|
+ apiToken: '',
|
|
|
|
|
+ liveInputId: '',
|
|
|
|
|
+ streamKey: '',
|
|
|
|
|
+ customerSubdomain: '',
|
|
|
|
|
+ mode: 'WHIP' as StreamChannelMode,
|
|
|
|
|
+ recordingEnabled: false,
|
|
|
|
|
+ enabled: true
|
|
|
})
|
|
})
|
|
|
drawerVisible.value = true
|
|
drawerVisible.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleEdit(row: LiveStreamDTO) {
|
|
|
|
|
|
|
+function handleEdit(row: StreamChannelDTO) {
|
|
|
Object.assign(form, {
|
|
Object.assign(form, {
|
|
|
id: row.id,
|
|
id: row.id,
|
|
|
- streamSn: row.streamSn,
|
|
|
|
|
|
|
+ channelId: row.channelId,
|
|
|
name: row.name,
|
|
name: row.name,
|
|
|
- lssId: row.lssId,
|
|
|
|
|
- cameraId: row.cameraId,
|
|
|
|
|
- streamMethod: row.streamMethod,
|
|
|
|
|
- commandTemplate: row.commandTemplate || ''
|
|
|
|
|
|
|
+ accountId: row.accountId || '',
|
|
|
|
|
+ apiToken: '',
|
|
|
|
|
+ liveInputId: row.liveInputId || '',
|
|
|
|
|
+ streamKey: '',
|
|
|
|
|
+ customerSubdomain: row.customerSubdomain || '',
|
|
|
|
|
+ mode: row.mode || 'WHIP',
|
|
|
|
|
+ recordingEnabled: row.recordingEnabled || false,
|
|
|
|
|
+ enabled: row.enabled
|
|
|
})
|
|
})
|
|
|
drawerVisible.value = true
|
|
drawerVisible.value = true
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function showCommandTemplate(row: LiveStreamDTO) {
|
|
|
|
|
- currentTemplate.value = row.commandTemplate || ''
|
|
|
|
|
- templateDialogVisible.value = true
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-async function handleStart(row: LiveStreamDTO) {
|
|
|
|
|
- actionLoading.value[row.id] = true
|
|
|
|
|
- try {
|
|
|
|
|
- const res = await startLiveStream(row.id)
|
|
|
|
|
- if (res.success) {
|
|
|
|
|
- ElMessage.success(t('启动成功'))
|
|
|
|
|
- getList()
|
|
|
|
|
|
|
+function handleWatch(row: StreamChannelDTO) {
|
|
|
|
|
+ // 跳转到 Cloudflare Stream 播放页面
|
|
|
|
|
+ router.push({
|
|
|
|
|
+ path: '/cc',
|
|
|
|
|
+ query: {
|
|
|
|
|
+ channelId: row.channelId,
|
|
|
|
|
+ name: row.name,
|
|
|
|
|
+ hlsUrl: row.hlsPlaybackUrl || '',
|
|
|
|
|
+ whepUrl: row.whepPlaybackUrl || ''
|
|
|
}
|
|
}
|
|
|
- } catch (error: any) {
|
|
|
|
|
- ElMessage.error(error.message || t('启动失败'))
|
|
|
|
|
- } finally {
|
|
|
|
|
- actionLoading.value[row.id] = false
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ })
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-async function handleStop(row: LiveStreamDTO) {
|
|
|
|
|
- actionLoading.value[row.id] = true
|
|
|
|
|
|
|
+async function handleDelete(row: StreamChannelDTO) {
|
|
|
try {
|
|
try {
|
|
|
- const res = await stopLiveStream(row.id)
|
|
|
|
|
|
|
+ await ElMessageBox.confirm(t('确定要删除该推流通道吗?'), t('提示'), {
|
|
|
|
|
+ type: 'warning',
|
|
|
|
|
+ confirmButtonText: t('确定'),
|
|
|
|
|
+ cancelButtonText: t('取消')
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const res = await deleteStreamChannel(row.id)
|
|
|
if (res.success) {
|
|
if (res.success) {
|
|
|
- ElMessage.success(t('已关闭'))
|
|
|
|
|
|
|
+ ElMessage.success(t('删除成功'))
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.errMessage || t('删除失败'))
|
|
|
}
|
|
}
|
|
|
- } catch (error: any) {
|
|
|
|
|
- ElMessage.error(error.message || t('关闭失败'))
|
|
|
|
|
- } finally {
|
|
|
|
|
- actionLoading.value[row.id] = false
|
|
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // 用户取消
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function handleWatch(row: LiveStreamDTO) {
|
|
|
|
|
- // 跳转到 Cloudflare Stream 页面,带上 stream 信息
|
|
|
|
|
- router.push({
|
|
|
|
|
- path: '/cc',
|
|
|
|
|
- query: {
|
|
|
|
|
- streamSn: row.streamSn,
|
|
|
|
|
- name: row.name,
|
|
|
|
|
- playUrl: row.playUrl || ''
|
|
|
|
|
- }
|
|
|
|
|
- })
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
async function handleSubmit() {
|
|
async function handleSubmit() {
|
|
|
if (!formRef.value) return
|
|
if (!formRef.value) return
|
|
|
|
|
|
|
@@ -655,31 +425,43 @@ async function handleSubmit() {
|
|
|
submitLoading.value = true
|
|
submitLoading.value = true
|
|
|
try {
|
|
try {
|
|
|
if (isEdit.value) {
|
|
if (isEdit.value) {
|
|
|
- const res = await updateLiveStream({
|
|
|
|
|
|
|
+ const res = await updateStreamChannel({
|
|
|
id: form.id!,
|
|
id: form.id!,
|
|
|
name: form.name,
|
|
name: form.name,
|
|
|
- lssId: form.lssId,
|
|
|
|
|
- cameraId: form.cameraId,
|
|
|
|
|
- streamMethod: form.streamMethod,
|
|
|
|
|
- commandTemplate: form.commandTemplate || undefined
|
|
|
|
|
|
|
+ accountId: form.accountId || undefined,
|
|
|
|
|
+ apiToken: form.apiToken || undefined,
|
|
|
|
|
+ liveInputId: form.liveInputId || undefined,
|
|
|
|
|
+ streamKey: form.streamKey || undefined,
|
|
|
|
|
+ customerSubdomain: form.customerSubdomain || undefined,
|
|
|
|
|
+ mode: form.mode,
|
|
|
|
|
+ recordingEnabled: form.recordingEnabled,
|
|
|
|
|
+ enabled: form.enabled
|
|
|
})
|
|
})
|
|
|
if (res.success) {
|
|
if (res.success) {
|
|
|
ElMessage.success(t('修改成功'))
|
|
ElMessage.success(t('修改成功'))
|
|
|
drawerVisible.value = false
|
|
drawerVisible.value = false
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.errMessage || t('修改失败'))
|
|
|
}
|
|
}
|
|
|
} else {
|
|
} else {
|
|
|
- const res = await addLiveStream({
|
|
|
|
|
|
|
+ const res = await addStreamChannel({
|
|
|
|
|
+ channelId: form.channelId,
|
|
|
name: form.name,
|
|
name: form.name,
|
|
|
- lssId: form.lssId,
|
|
|
|
|
- cameraId: form.cameraId,
|
|
|
|
|
- streamMethod: form.streamMethod,
|
|
|
|
|
- commandTemplate: form.commandTemplate || undefined
|
|
|
|
|
|
|
+ accountId: form.accountId || undefined,
|
|
|
|
|
+ apiToken: form.apiToken || undefined,
|
|
|
|
|
+ liveInputId: form.liveInputId,
|
|
|
|
|
+ streamKey: form.streamKey || undefined,
|
|
|
|
|
+ customerSubdomain: form.customerSubdomain,
|
|
|
|
|
+ mode: form.mode,
|
|
|
|
|
+ recordingEnabled: form.recordingEnabled
|
|
|
})
|
|
})
|
|
|
if (res.success) {
|
|
if (res.success) {
|
|
|
ElMessage.success(t('新增成功'))
|
|
ElMessage.success(t('新增成功'))
|
|
|
drawerVisible.value = false
|
|
drawerVisible.value = false
|
|
|
getList()
|
|
getList()
|
|
|
|
|
+ } else {
|
|
|
|
|
+ ElMessage.error(res.errMessage || t('新增失败'))
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
} finally {
|
|
} finally {
|
|
@@ -702,7 +484,6 @@ function handleCurrentChange(val: number) {
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
getList()
|
|
getList()
|
|
|
- loadOptions()
|
|
|
|
|
})
|
|
})
|
|
|
</script>
|
|
</script>
|
|
|
|
|
|
|
@@ -726,7 +507,7 @@ onMounted(() => {
|
|
|
|
|
|
|
|
:deep(.el-input),
|
|
:deep(.el-input),
|
|
|
:deep(.el-select) {
|
|
:deep(.el-select) {
|
|
|
- width: 160px;
|
|
|
|
|
|
|
+ width: 180px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
:deep(.el-button--primary) {
|
|
:deep(.el-button--primary) {
|
|
@@ -800,41 +581,6 @@ onMounted(() => {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-: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;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.command-template {
|
|
|
|
|
- background: #f5f7fa;
|
|
|
|
|
- padding: 16px;
|
|
|
|
|
- border-radius: 4px;
|
|
|
|
|
- font-family: monospace;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- white-space: pre-wrap;
|
|
|
|
|
- word-break: break-all;
|
|
|
|
|
- max-height: 400px;
|
|
|
|
|
- overflow: auto;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
// 抽屉样式
|
|
// 抽屉样式
|
|
|
.stream-drawer {
|
|
.stream-drawer {
|
|
|
:deep(.el-drawer__body) {
|
|
:deep(.el-drawer__body) {
|
|
@@ -875,27 +621,6 @@ onMounted(() => {
|
|
|
color: #606266;
|
|
color: #606266;
|
|
|
font-size: 14px;
|
|
font-size: 14px;
|
|
|
}
|
|
}
|
|
|
-
|
|
|
|
|
- .form-value {
|
|
|
|
|
- line-height: 32px;
|
|
|
|
|
- color: #303133;
|
|
|
|
|
- font-size: 14px;
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- .textarea-wrapper {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
-
|
|
|
|
|
- :deep(.el-textarea__inner) {
|
|
|
|
|
- font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
|
|
|
|
- font-size: 13px;
|
|
|
|
|
- background-color: #fafafa;
|
|
|
|
|
- border: 1px solid #dcdfe6;
|
|
|
|
|
-
|
|
|
|
|
- &:focus {
|
|
|
|
|
- border-color: #409eff;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.drawer-footer {
|
|
.drawer-footer {
|
|
@@ -907,13 +632,13 @@ onMounted(() => {
|
|
|
gap: 12px;
|
|
gap: 12px;
|
|
|
|
|
|
|
|
.el-button--primary {
|
|
.el-button--primary {
|
|
|
- background-color: #409eff;
|
|
|
|
|
- border-color: #409eff;
|
|
|
|
|
|
|
+ background-color: #4f46e5;
|
|
|
|
|
+ border-color: #4f46e5;
|
|
|
|
|
|
|
|
&:hover,
|
|
&:hover,
|
|
|
&:focus {
|
|
&:focus {
|
|
|
- background-color: #66b1ff;
|
|
|
|
|
- border-color: #66b1ff;
|
|
|
|
|
|
|
+ background-color: #6366f1;
|
|
|
|
|
+ border-color: #6366f1;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|