Просмотр исходного кода

feat(stream-channel): introduce stream channel management with API integration and UI enhancements

- Added new API functions for managing stream channels, including listing, adding, updating, and deleting channels.
- Implemented a new Vue component for stream channel management, featuring a search form and a data table for displaying channel information.
- Updated the form layout to include necessary fields such as channel ID, account ID, and live input ID, enhancing user experience.
- Refactored related types to accommodate the new stream channel structure and ensure consistency across the application.
- Created documentation for the stream channel API to provide clear usage guidelines.
yb 1 неделя назад
Родитель
Сommit
a4cbbe4599
5 измененных файлов с 341 добавлено и 613 удалено
  1. 1 0
      docs/api_torna/live-stream.md
  2. 0 59
      src/api/live-stream.ts
  3. 59 0
      src/api/stream-channel.ts
  4. 39 37
      src/types/index.ts
  5. 242 517
      src/views/live-stream/index.vue

+ 1 - 0
docs/api_torna/live-stream.md

@@ -0,0 +1 @@
+# 文档

+ 0 - 59
src/api/live-stream.ts

@@ -1,59 +0,0 @@
-import { post } from '@/utils/request'
-import type {
-  IPageResponse,
-  IBaseResponse,
-  BaseResponse,
-  LiveStreamDTO,
-  LiveStreamListRequest,
-  LiveStreamAddRequest,
-  LiveStreamUpdateRequest,
-  LssDTO,
-  CameraInfoDTO
-} from '@/types'
-
-// ==================== LiveStream Admin APIs ====================
-
-// 获取 LiveStream 列表 (分页)
-export function listLiveStreams(params?: LiveStreamListRequest): Promise<IPageResponse<LiveStreamDTO>> {
-  return post('/admin/live-stream/list', params || {})
-}
-
-// 获取单个 LiveStream
-export function getLiveStream(id: number): Promise<IBaseResponse<LiveStreamDTO>> {
-  return post('/admin/live-stream/get', { id })
-}
-
-// 创建 LiveStream
-export function addLiveStream(data: LiveStreamAddRequest): Promise<IBaseResponse<LiveStreamDTO>> {
-  return post('/admin/live-stream/add', data)
-}
-
-// 更新 LiveStream
-export function updateLiveStream(data: LiveStreamUpdateRequest): Promise<IBaseResponse<LiveStreamDTO>> {
-  return post('/admin/live-stream/update', data)
-}
-
-// 删除 LiveStream
-export function deleteLiveStream(id: number): Promise<BaseResponse> {
-  return post('/admin/live-stream/delete', { id })
-}
-
-// 启动 LiveStream
-export function startLiveStream(id: number): Promise<BaseResponse> {
-  return post('/admin/live-stream/start', { id })
-}
-
-// 停止 LiveStream
-export function stopLiveStream(id: number): Promise<BaseResponse> {
-  return post('/admin/live-stream/stop', { id })
-}
-
-// 获取 LSS 下拉列表(用于选择)
-export function getLssOptions(): Promise<IBaseResponse<LssDTO[]>> {
-  return post('/admin/lss/options', {})
-}
-
-// 获取摄像头下拉列表(用于选择)
-export function getCameraOptions(): Promise<IBaseResponse<CameraInfoDTO[]>> {
-  return post('/admin/camera/options', {})
-}

+ 59 - 0
src/api/stream-channel.ts

@@ -0,0 +1,59 @@
+import { get, post } from '@/utils/request'
+import type {
+  IPageResponse,
+  IBaseResponse,
+  IListResponse,
+  BaseResponse,
+  StreamChannelDTO,
+  StreamChannelListRequest,
+  StreamChannelAddRequest,
+  StreamChannelUpdateRequest
+} from '@/types'
+
+/**
+ * 获取推流通道列表(分页)
+ * POST /api/admin/stream-channels/list
+ */
+export function listStreamChannels(params?: StreamChannelListRequest): Promise<IPageResponse<StreamChannelDTO>> {
+  return post('/admin/stream-channels/list', params || {})
+}
+
+/**
+ * 获取推流通道列表(全部,不分页)
+ * GET /api/admin/stream-channels/listAll
+ */
+export function listAllStreamChannels(): Promise<IListResponse<StreamChannelDTO>> {
+  return get('/admin/stream-channels/listAll')
+}
+
+/**
+ * 获取推流通道详情
+ * GET /api/admin/stream-channels/detail?id=X
+ */
+export function getStreamChannelDetail(id: number): Promise<IBaseResponse<StreamChannelDTO>> {
+  return get('/admin/stream-channels/detail', { id })
+}
+
+/**
+ * 添加推流通道
+ * POST /api/admin/stream-channels/add
+ */
+export function addStreamChannel(data: StreamChannelAddRequest): Promise<IBaseResponse<StreamChannelDTO>> {
+  return post('/admin/stream-channels/add', data)
+}
+
+/**
+ * 更新推流通道
+ * POST /api/admin/stream-channels/update
+ */
+export function updateStreamChannel(data: StreamChannelUpdateRequest): Promise<IBaseResponse<StreamChannelDTO>> {
+  return post('/admin/stream-channels/update', data)
+}
+
+/**
+ * 删除推流通道
+ * POST /api/admin/stream-channels/delete?id=X
+ */
+export function deleteStreamChannel(id: number): Promise<BaseResponse> {
+  return post('/admin/stream-channels/delete', null, { params: { id } })
+}

+ 39 - 37
src/types/index.ts

@@ -409,58 +409,60 @@ export interface LssListRequest extends PageRequest {
   heartbeat?: LssHeartbeatStatus
 }
 
-// ==================== LiveStream 相关类型 ====================
+// ==================== StreamChannel 推流通道相关类型 (Cloudflare Stream) ====================
 
-// 推流
-export type StreamMethod = 'ffmpeg' | 'obs' | 'gstreamer'
+// 推流
+export type StreamChannelMode = 'WHIP' | 'RTMPS' | 'SRT'
 
-// LiveStream 状态
-export type LiveStreamStatus = 'running' | 'stopped' | 'error'
-
-// LiveStream 信息
-export interface LiveStreamDTO {
+// 推流通道信息 (StreamChannelInfoDTO)
+export interface StreamChannelDTO {
   id: number
-  streamSn: string
+  channelId: string
   name: string
-  lssId?: number
-  lssName?: string
-  cameraId?: number
-  cameraName?: string
-  streamMethod: StreamMethod
-  commandTemplate?: string
-  status: LiveStreamStatus
-  startedAt?: string
-  stoppedAt?: string
-  playUrl?: string
+  accountId?: string
+  liveInputId?: string
+  customerSubdomain?: string
+  mode?: StreamChannelMode
+  whipUrl?: string
+  rtmpsUrl?: string
+  hlsPlaybackUrl?: string
+  whepPlaybackUrl?: string
+  recordingEnabled?: boolean
+  enabled: boolean
   createdAt: string
   updatedAt: string
 }
 
-// LiveStream 列表请求参数
-export interface LiveStreamListRequest extends PageRequest {
-  aoAgent?: string
-  feature?: string
-  status?: LiveStreamStatus
-  lssId?: number
+// 推流通道列表请求参数
+export interface StreamChannelListRequest extends PageRequest {
+  // 继承 PageRequest 的所有属性 (page, size, keyword, enabled, sortBy, sortDir)
 }
 
-// LiveStream 创建请求
-export interface LiveStreamAddRequest {
+// 创建推流通道请求
+export interface StreamChannelAddRequest {
+  channelId: 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
 }
 
-// LiveStream 更新请求
-export interface LiveStreamUpdateRequest {
+// 更新推流通道请求
+export interface StreamChannelUpdateRequest {
   id: number
   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
 }
 
 // ==================== 摄像头厂家相关类型 ====================

+ 242 - 517
src/views/live-stream/index.vue

@@ -3,11 +3,19 @@
     <!-- 搜索表单 -->
     <div class="search-form">
       <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 :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-button type="primary" :icon="Search" @click="handleSearch">{{ t('查询') }}</el-button>
@@ -22,76 +30,55 @@
       <el-table
         ref="tableRef"
         v-loading="loading"
-        :data="streamList"
+        :data="channelList"
         stripe
         size="default"
         height="100%"
         @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 }">
             <el-link type="primary" @click="handleEdit(row)">{{ row.name }}</el-link>
           </template>
         </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 }">
-            <span>{{ row.lssName ? `${row.lssId}/${row.lssName}` : '-' }}</span>
+            <el-tag size="small" :type="getModeTagType(row.mode)">{{ row.mode || '-' }}</el-tag>
           </template>
         </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 }">
-            <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>
             <span v-else>-</span>
           </template>
         </el-table-column>
-        <el-table-column :label="t('操作')" align="center">
+        <el-table-column prop="recordingEnabled" :label="t('录制')" width="80" align="center">
           <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>
         </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 }">
-            {{ formatDateTime(row.startedAt) }}
+            <el-tag size="small" :type="row.enabled ? 'success' : 'danger'">
+              {{ row.enabled ? t('启用') : t('禁用') }}
+            </el-tag>
           </template>
         </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 }">
-            {{ formatDateTime(row.stoppedAt) }}
+            {{ formatDateTime(row.createdAt) }}
           </template>
         </el-table-column>
-        <el-table-column :label="t('观看')" align="center">
+        <el-table-column :label="t('操作')" width="150" align="center" fixed="right">
           <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>
         </el-table-column>
       </el-table>
@@ -115,7 +102,7 @@
     <el-drawer
       v-model="drawerVisible"
       direction="rtl"
-      size="500px"
+      size="550px"
       :with-header="false"
       destroy-on-close
       class="stream-drawer"
@@ -127,39 +114,64 @@
             ref="formRef"
             :model="form"
             :rules="rules"
-            label-width="80px"
+            label-width="130px"
             label-position="left"
             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 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 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-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>
         </div>
@@ -171,31 +183,16 @@
         </div>
       </div>
     </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>
 </template>
+
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed } from 'vue'
 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 {
-  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 { useI18n } from 'vue-i18n'
 
@@ -205,21 +202,46 @@ const router = useRouter()
 // 格式化时间
 function formatDateTime(dateStr: string | undefined): string {
   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 submitLoading = ref(false)
-const actionLoading = ref<Record<number, boolean>>({})
-const streamList = ref<LiveStreamDTO[]>([])
+const channelList = ref<StreamChannelDTO[]>([])
 const drawerVisible = ref(false)
-const templateDialogVisible = ref(false)
 const formRef = ref<FormInstance>()
-const currentTemplate = ref('')
-
-// 下拉选项
-const lssOptions = ref<LssNodeDTO[]>([])
-const cameraOptions = ref<CameraInfoDTO[]>([])
 
 // 排序状态
 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<{
   id?: number
-  streamSn: string
+  channelId: 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: '',
-  lssId: undefined,
-  cameraId: undefined,
-  streamMethod: 'ffmpeg',
-  commandTemplate: ''
+  accountId: '',
+  apiToken: '',
+  liveInputId: '',
+  streamKey: '',
+  customerSubdomain: '',
+  mode: 'WHIP',
+  recordingEnabled: false,
+  enabled: true
 })
 
 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 = {
-  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() {
   loading.value = true
   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() {
-  searchForm.aoAgent = ''
-  searchForm.feature = ''
+  searchForm.keyword = ''
+  searchForm.enabled = null
   currentPage.value = 1
   sortState.prop = ''
   sortState.order = null
@@ -577,76 +353,70 @@ function handleSortChange({ prop, order }: { prop: string; order: 'ascending' |
 function handleAdd() {
   Object.assign(form, {
     id: undefined,
-    streamSn: '',
+    channelId: '',
     name: '',
-    lssId: undefined,
-    cameraId: undefined,
-    streamMethod: 'ffmpeg',
-    commandTemplate: ''
+    accountId: '',
+    apiToken: '',
+    liveInputId: '',
+    streamKey: '',
+    customerSubdomain: '',
+    mode: 'WHIP' as StreamChannelMode,
+    recordingEnabled: false,
+    enabled: true
   })
   drawerVisible.value = true
 }
 
-function handleEdit(row: LiveStreamDTO) {
+function handleEdit(row: StreamChannelDTO) {
   Object.assign(form, {
     id: row.id,
-    streamSn: row.streamSn,
+    channelId: row.channelId,
     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
 }
 
-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 {
-    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) {
-      ElMessage.success(t('已关闭'))
+      ElMessage.success(t('删除成功'))
       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() {
   if (!formRef.value) return
 
@@ -655,31 +425,43 @@ async function handleSubmit() {
       submitLoading.value = true
       try {
         if (isEdit.value) {
-          const res = await updateLiveStream({
+          const res = await updateStreamChannel({
             id: form.id!,
             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) {
             ElMessage.success(t('修改成功'))
             drawerVisible.value = false
             getList()
+          } else {
+            ElMessage.error(res.errMessage || t('修改失败'))
           }
         } else {
-          const res = await addLiveStream({
+          const res = await addStreamChannel({
+            channelId: form.channelId,
             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) {
             ElMessage.success(t('新增成功'))
             drawerVisible.value = false
             getList()
+          } else {
+            ElMessage.error(res.errMessage || t('新增失败'))
           }
         }
       } finally {
@@ -702,7 +484,6 @@ function handleCurrentChange(val: number) {
 
 onMounted(() => {
   getList()
-  loadOptions()
 })
 </script>
 
@@ -726,7 +507,7 @@ onMounted(() => {
 
   :deep(.el-input),
   :deep(.el-select) {
-    width: 160px;
+    width: 180px;
   }
 
   :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 {
   :deep(.el-drawer__body) {
@@ -875,27 +621,6 @@ onMounted(() => {
     color: #606266;
     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 {
@@ -907,13 +632,13 @@ onMounted(() => {
   gap: 12px;
 
   .el-button--primary {
-    background-color: #409eff;
-    border-color: #409eff;
+    background-color: #4f46e5;
+    border-color: #4f46e5;
 
     &:hover,
     &:focus {
-      background-color: #66b1ff;
-      border-color: #66b1ff;
+      background-color: #6366f1;
+      border-color: #6366f1;
     }
   }
 }