Ver Fonte

feat(live-stream, lss): enhance Live Stream creation flow and UI updates

- Updated localization files to include new messages for Live Stream creation prompts.
- Modified the Live Stream component to automatically open the creation drawer when accessed with the action=create parameter.
- Implemented logic to pre-fill camera information based on the provided cameraId during Live Stream creation.
- Improved user experience by displaying a confirmation dialog when attempting to access a camera without an associated Live Stream.
- Enhanced e2e tests to validate the new Live Stream creation flow and ensure proper functionality.
yb há 6 dias atrás
pai
commit
accdb9bbb4

+ 5 - 3
src/locales/en.json

@@ -80,7 +80,7 @@
   "厂家代码": "Factory Code",
   "厂家名称": "Factory Name",
   "原密码": "Old Password",
-  "参数配置": "Parameter Configuration",
+  "参数配置": "Parameter",
   "取消": "Cancel",
   "取消选择": "Clear Selection",
   "可用通道数量": "Available Channels",
@@ -159,6 +159,8 @@
   "放大": "Zoom In",
   "数据更新时间": "Last Updated",
   "新增": "Add",
+  "尚未建立 Live Stream": "Live Stream Not Created",
+  "请先新增 Live Stream,才能进行后续操作。": "Please create a Live Stream first to continue.",
   "新增 Live Stream": "Add Live Stream",
   "新增厂家": "Add Factory",
   "新增失败": "Add failed",
@@ -249,7 +251,7 @@
   "视频播放测试": "Video Playback Test",
   "记住我": "Remember me",
   "设备ID": "Device ID",
-  "设备列表": "Device List",
+  "设备列表": "Devices",
   "设备控制": "Device Control",
   "请先配置摄像头": "Please configure the camera first",
   "请再次输入新密码": "Please enter the new password again",
@@ -271,7 +273,7 @@
   "请选择视频源并点击播放": "Please select video source and click play",
   "跳转失败": "Jump failed",
   "转换服务地址": "Proxy Service URL",
-  "运行参数": "Run Parameters",
+  "运行参数": "Runtime",
   "退出登录": "Logout",
   "选择测试源": "Select Test Source",
   "通道": "Channel",

+ 2 - 0
src/locales/zh-cn.json

@@ -159,6 +159,8 @@
   "放大": "放大",
   "数据更新时间": "数据更新时间",
   "新增": "新增",
+  "尚未建立 Live Stream": "尚未建立 Live Stream",
+  "请先新增 Live Stream,才能进行后续操作。": "请先新增 Live Stream,才能进行后续操作。",
   "新增 Live Stream": "新增 Live Stream",
   "新增厂家": "新增厂家",
   "新增失败": "新增失败",

+ 30 - 2
src/views/live-stream/index.vue

@@ -1096,15 +1096,43 @@ function handleCurrentChange(val: number) {
   getList()
 }
 
-onMounted(() => {
+onMounted(async () => {
   // 读取 URL 查询参数
   const queryCameraId = route.query.cameraId as string
+  const queryAction = route.query.action as string
+
   if (queryCameraId) {
     searchForm.cameraId = queryCameraId
   }
 
+  // 先加载选项数据
+  await loadOptions()
   getList()
-  loadOptions()
+
+  // 如果是创建操作,自动打开新增抽屉
+  if (queryAction === 'create') {
+    handleAdd()
+
+    // 如果提供了 cameraId,尝试查找摄像头信息以自动填充 lssId
+    if (queryCameraId) {
+      try {
+        const res = await adminListCameras({ cameraId: queryCameraId, size: 1 })
+        if (res.success && res.data?.list?.length > 0) {
+          const camera = res.data.list[0]
+          if (camera.lssId) {
+            form.lssId = camera.lssId
+            // lssId 变化会触发 watch,加载该 LSS 下的摄像头列表
+            // 等待摄像头列表加载完成后再设置 cameraId
+            setTimeout(() => {
+              form.cameraId = queryCameraId
+            }, 500)
+          }
+        }
+      } catch (error) {
+        console.error('获取摄像头信息失败', error)
+      }
+    }
+  }
 })
 </script>
 

+ 20 - 4
src/views/lss/index.vue

@@ -81,7 +81,7 @@
             {{ row.ablyClientId }}
           </template>
         </el-table-column>
-        <el-table-column :label="t('操作')" align="center" fixed="right">
+        <el-table-column :label="t('操作')" width="130" align="center" fixed="right">
           <template #default="{ row }">
             <el-button type="primary" link @click="handleEdit(row, 'detail')">
               <Icon icon="mdi:note-edit-outline" width="20" height="20" />
@@ -263,7 +263,7 @@
                   {{ formatTime(row.createdAt) }}
                 </template>
               </el-table-column>
-              <el-table-column :label="t('设备控制')" min-width="100" align="center" fixed="right">
+              <el-table-column :label="t('设备控制')" width="130" align="center">
                 <template #default="{ row }">
                   <el-button type="primary" link @click="handleEditCamera(row)">
                     <Icon icon="mdi:note-edit-outline" width="20" height="20" />
@@ -644,8 +644,24 @@ function formatCameraStatus(row: CameraInfoDTO): string {
 // 当前激活的摄像头 ID
 const activeCameraId = ref<number | null>(null)
 
-function handleViewCamera(row: CameraInfoDTO) {
-  // 切换激活状态
+async function handleViewCamera(row: CameraInfoDTO) {
+  // 如果没有 streamSn,显示提示对话框
+  if (!row.streamSn) {
+    try {
+      await ElMessageBox.confirm(t('请先新增 Live Stream,才能进行后续操作。'), t('尚未建立 Live Stream'), {
+        confirmButtonText: t('新增 Live Stream'),
+        cancelButtonText: t('取消'),
+        type: 'warning',
+        center: true
+      })
+      // 用户点击了"新增 Live Stream",跳转到直播管理页面
+      router.push(`/live-stream?cameraId=${row.cameraId}&lssId=${row.lssId}&action=create`)
+    } catch {
+      // 用户点击了取消,不做任何操作
+    }
+    return
+  }
+  // 有 streamSn,正常跳转
   router.push(`/live-stream?cameraId=${row.cameraId}`)
 }
 

+ 138 - 0
tests/e2e/live-stream.spec.ts

@@ -596,3 +596,141 @@ ffmpeg -i {RTSP_URL} -c copy output.mp4`
     }
   })
 })
+
+test.describe('LiveStream 管理 - 从 LSS 页面创建流程', () => {
+  /**
+   * 测试带 action=create 参数时自动打开新增抽屉
+   */
+  test('带 action=create 参数访问时自动打开新增抽屉', async ({ page }) => {
+    await login(page)
+
+    // 直接导航到带有 action=create 参数的页面
+    await page.goto('/live-stream?action=create')
+
+    // 等待页面加载
+    await page.waitForTimeout(1000)
+
+    // 验证新增抽屉已打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 验证抽屉标题
+    await expect(drawer.locator('.drawer-header')).toContainText('新增 Live Stream')
+
+    // 验证表单字段存在
+    await expect(drawer.locator('label:has-text("名称")')).toBeVisible()
+    await expect(drawer.locator('label:has-text("LSS 节点")')).toBeVisible()
+    await expect(drawer.locator('label:has-text("摄像头")')).toBeVisible()
+  })
+
+  /**
+   * 测试带 cameraId 和 action=create 参数时自动填充摄像头信息
+   */
+  test('带 cameraId 和 action=create 参数访问时尝试填充摄像头信息', async ({ page }) => {
+    await login(page)
+
+    // 模拟 API 响应,返回带有 lssId 的摄像头数据
+    await page.route('**/admin/camera/list*', async (route) => {
+      const request = route.request()
+      const url = request.url()
+
+      // 检查是否是查询特定 cameraId 的请求
+      if (url.includes('cameraId=TEST_CAM_001')) {
+        await route.fulfill({
+          status: 200,
+          contentType: 'application/json',
+          body: JSON.stringify({
+            success: true,
+            errCode: 0,
+            data: {
+              list: [
+                {
+                  id: 1,
+                  cameraId: 'TEST_CAM_001',
+                  cameraName: '测试摄像头',
+                  lssId: 'LSS_001',
+                  status: 'active'
+                }
+              ],
+              total: 1
+            }
+          })
+        })
+      } else {
+        await route.continue()
+      }
+    })
+
+    // 导航到带有 cameraId 和 action=create 参数的页面
+    await page.goto('/live-stream?cameraId=TEST_CAM_001&action=create')
+
+    // 等待页面和抽屉加载
+    await page.waitForTimeout(1500)
+
+    // 验证新增抽屉已打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 等待表单自动填充
+    await page.waitForTimeout(1000)
+
+    // 验证抽屉中的表单元素可见(LSS 节点选择器)
+    // 由于是 mock 数据,这里主要验证流程不会出错,抽屉能正常打开
+    await expect(drawer.locator('label:has-text("LSS")')).toBeVisible()
+  })
+
+  /**
+   * 测试从 LSS 页面点击未创建 Stream 的摄像头时显示提示对话框
+   * 注:这个测试需要在 LSS 页面进行,但这里测试的是对话框确认后的跳转结果
+   */
+  test('取消新增抽屉后返回列表页面', async ({ page }) => {
+    await login(page)
+
+    // 导航到创建页面
+    await page.goto('/live-stream?action=create')
+
+    // 等待抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击取消按钮
+    await drawer.locator('button:has-text("取消"), button:has-text("Cancel")').click()
+
+    // 验证抽屉关闭
+    await expect(drawer).not.toBeVisible({ timeout: 3000 })
+
+    // 验证仍在 live-stream 页面
+    await expect(page).toHaveURL(/\/live-stream/)
+  })
+
+  /**
+   * 测试 URL 参数中的 cameraId 同时作为搜索条件
+   */
+  test('cameraId 参数同时作为搜索条件', async ({ page }) => {
+    await login(page)
+
+    // 设置 API 拦截来验证参数
+    let listRequestBody: any = null
+    await page.route('**/admin/live-stream/list', async (route) => {
+      const request = route.request()
+      if (request.method() === 'POST') {
+        listRequestBody = request.postDataJSON()
+      }
+      await route.continue()
+    })
+
+    // 导航到带有 cameraId 参数的页面(不带 action=create)
+    await page.goto('/live-stream?cameraId=CAM_SEARCH_TEST')
+
+    // 等待列表请求完成
+    await page.waitForTimeout(1500)
+
+    // 验证搜索框中已填入 cameraId
+    const cameraIdInput = page.getByPlaceholder('设备ID')
+    await expect(cameraIdInput).toHaveValue('CAM_SEARCH_TEST')
+
+    // 验证 API 请求中包含 cameraId
+    expect(listRequestBody).not.toBeNull()
+    expect(listRequestBody.cameraId).toBe('CAM_SEARCH_TEST')
+  })
+})

+ 212 - 0
tests/e2e/lss.spec.ts

@@ -811,3 +811,215 @@ test.describe('LSS管理 - 摄像头列表搜索测试', () => {
     }
   })
 })
+
+test.describe('LSS管理 - 摄像头未创建 Live Stream 对话框测试', () => {
+  // 登录辅助函数
+  async function login(page: Page) {
+    await page.goto('/login')
+    await page.evaluate(() => {
+      localStorage.clear()
+      document.cookie.split(';').forEach((c) => {
+        document.cookie = c.replace(/^ +/, '').replace(/=.*/, '=;expires=' + new Date().toUTCString() + ';path=/')
+      })
+    })
+    await page.reload()
+
+    await page.getByPlaceholder('用户名').fill(TEST_USERNAME)
+    await page.getByPlaceholder('密码').fill(TEST_PASSWORD)
+    await page.getByRole('button', { name: '登录' }).click()
+    await expect(page).not.toHaveURL(/\/login/, { timeout: 15000 })
+  }
+
+  // 打开摄像头列表辅助函数
+  async function openCameraList(page: Page) {
+    await page.goto('/lss')
+    await page.waitForTimeout(1000)
+
+    // 点击编辑按钮进入 LSS 编辑抽屉
+    const editButton = page.locator('tbody tr').first().locator('button').nth(1)
+    await expect(editButton).toBeVisible({ timeout: 10000 })
+    await editButton.click()
+
+    // 等待 LSS 编辑抽屉打开
+    const drawer = page.locator('.el-drawer').filter({ hasText: 'LSS详情' })
+    await expect(drawer).toBeVisible({ timeout: 5000 })
+
+    // 点击"摄像头列表" Tab
+    await drawer.locator('.el-tabs__item').filter({ hasText: '摄像头列表' }).click()
+    await page.waitForTimeout(500)
+
+    return drawer
+  }
+
+  /**
+   * 测试点击无 streamSn 的摄像头时显示对话框
+   */
+  test('点击无 streamSn 的摄像头时显示"尚未建立 Live Stream"对话框', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头列表 API 返回无 streamSn 的数据
+    await page.route('**/admin/camera/list*', async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: JSON.stringify({
+          success: true,
+          errCode: 0,
+          data: {
+            list: [
+              {
+                id: 1,
+                cameraId: 'CAM_NO_STREAM',
+                cameraName: '无推流摄像头',
+                lssId: 'LSS_001',
+                status: 'active',
+                streamSn: null // 没有 streamSn
+              }
+            ],
+            total: 1
+          }
+        })
+      })
+    })
+
+    const drawer = await openCameraList(page)
+
+    // 等待表格数据加载
+    await page.waitForTimeout(500)
+
+    // 点击摄像头控制按钮(crosshairs-btn)
+    const crosshairsBtn = drawer.locator('.crosshairs-btn').first()
+    await expect(crosshairsBtn).toBeVisible({ timeout: 5000 })
+    await crosshairsBtn.click()
+
+    // 验证对话框显示
+    const messageBox = page.locator('.el-message-box')
+    await expect(messageBox).toBeVisible({ timeout: 5000 })
+
+    // 验证对话框标题
+    await expect(messageBox.locator('.el-message-box__title')).toContainText('尚未建立 Live Stream')
+
+    // 验证对话框内容
+    await expect(messageBox.locator('.el-message-box__message')).toContainText('请先新增 Live Stream')
+
+    // 验证按钮存在
+    await expect(messageBox.locator('button:has-text("新增 Live Stream")')).toBeVisible()
+    await expect(messageBox.locator('button:has-text("取消")')).toBeVisible()
+  })
+
+  /**
+   * 测试点击"取消"按钮关闭对话框
+   */
+  test('点击"取消"按钮关闭对话框', async ({ page }) => {
+    await login(page)
+
+    // Mock 摄像头列表 API 返回无 streamSn 的数据
+    await page.route('**/admin/camera/list*', async (route) => {
+      await route.fulfill({
+        status: 200,
+        contentType: 'application/json',
+        body: JSON.stringify({
+          success: true,
+          errCode: 0,
+          data: {
+            list: [
+              {
+                id: 1,
+                cameraId: 'CAM_NO_STREAM',
+                cameraName: '无推流摄像头',
+                lssId: 'LSS_001',
+                status: 'active',
+                streamSn: null
+              }
+            ],
+            total: 1
+          }
+        })
+      })
+    })
+
+    const drawer = await openCameraList(page)
+    await page.waitForTimeout(500)
+
+    // 点击摄像头控制按钮
+    const crosshairsBtn = drawer.locator('.crosshairs-btn').first()
+    await crosshairsBtn.click()
+
+    // 等待对话框显示
+    const messageBox = page.locator('.el-message-box')
+    await expect(messageBox).toBeVisible({ timeout: 5000 })
+
+    // 点击取消按钮
+    await messageBox.locator('button:has-text("取消")').click()
+
+    // 验证对话框关闭
+    await expect(messageBox).not.toBeVisible({ timeout: 3000 })
+
+    // 验证仍在 LSS 页面
+    await expect(page).toHaveURL(/\/lss/)
+  })
+
+  /**
+   * 测试点击"新增 Live Stream"按钮跳转到创建页面
+   */
+  test('点击"新增 Live Stream"按钮跳转到 live-stream 创建页面', async ({ page }) => {
+    await login(page)
+    const drawer = await openCameraList(page)
+    await page.waitForTimeout(500)
+
+    // 找到没有 streamSn 的摄像头(crosshairs-btn 没有 active 类的)
+    const inactiveCrosshairsBtn = drawer.locator('.crosshairs-btn:not(.active)').first()
+
+    // 如果存在无 streamSn 的摄像头
+    if ((await inactiveCrosshairsBtn.count()) > 0) {
+      await inactiveCrosshairsBtn.click()
+
+      // 等待对话框显示
+      const messageBox = page.locator('.el-message-box')
+      await expect(messageBox).toBeVisible({ timeout: 5000 })
+
+      // 点击"新增 Live Stream"按钮
+      await messageBox.locator('button:has-text("新增 Live Stream")').click()
+
+      // 验证跳转到 live-stream 页面并带有 action=create 参数
+      await expect(page).toHaveURL(/\/live-stream\?cameraId=.*&action=create/, { timeout: 5000 })
+
+      // 验证新增抽屉自动打开
+      const liveStreamDrawer = page.locator('.el-drawer').filter({ hasText: '新增 Live Stream' })
+      await expect(liveStreamDrawer).toBeVisible({ timeout: 5000 })
+    } else {
+      // 如果没有无 streamSn 的摄像头,跳过测试
+      test.skip()
+    }
+  })
+
+  /**
+   * 测试有 streamSn 的摄像头直接跳转(不显示对话框)
+   */
+  test('有 streamSn 的摄像头直接跳转到 live-stream 页面', async ({ page }) => {
+    await login(page)
+    const drawer = await openCameraList(page)
+    await page.waitForTimeout(500)
+
+    // 找到有 streamSn 的摄像头(crosshairs-btn 有 active 类的)
+    const activeCrosshairsBtn = drawer.locator('.crosshairs-btn.active').first()
+
+    // 如果存在有 streamSn 的摄像头
+    if ((await activeCrosshairsBtn.count()) > 0) {
+      await activeCrosshairsBtn.click()
+
+      // 等待一小段时间检查是否有对话框
+      await page.waitForTimeout(500)
+
+      // 验证没有显示对话框,直接跳转
+      const messageBox = page.locator('.el-message-box')
+
+      // 验证跳转到 live-stream 页面(不带 action=create)
+      await expect(page).toHaveURL(/\/live-stream\?cameraId=/, { timeout: 5000 })
+      await expect(page).not.toHaveURL(/action=create/)
+    } else {
+      // 如果没有有 streamSn 的摄像头,跳过测试
+      test.skip()
+    }
+  })
+})