Przeglądaj źródła

feat(live-stream): replace dialog with drawer for live stream management

- Switched from a dialog to a drawer component for adding and editing live streams, enhancing user experience.
- Updated form layout and styling for better accessibility and usability.
- Refactored related state management to accommodate the new drawer interface.
- Improved API integration for LSS node options and camera selection.
yb 1 tydzień temu
rodzic
commit
65d3e89ded
2 zmienionych plików z 199 dodań i 59 usunięć
  1. 140 59
      src/views/live-stream/index.vue
  2. 59 0
      src/views/lss/index.vue

+ 140 - 59
src/views/live-stream/index.vue

@@ -111,46 +111,59 @@
       />
     </div>
 
-    <!-- 新增/编辑弹窗 -->
-    <el-dialog v-model="dialogVisible" :title="dialogTitle" width="550px" destroy-on-close>
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
-        <el-form-item :label="t('Stream SN')" prop="streamSn">
-          <el-input v-model="form.streamSn" placeholder="自动生成" disabled />
-        </el-form-item>
-        <el-form-item :label="t('名称')" prop="name">
-          <el-input v-model="form.name" placeholder="请输入名称" />
-        </el-form-item>
-        <el-form-item :label="t('LSS')" prop="lssId">
-          <el-select v-model="form.lssId" placeholder="请选择 LSS" clearable filterable style="width: 100%">
-            <el-option v-for="lss in lssOptions" :key="lss.id" :label="`${lss.lssId} / ${lss.name}`" :value="lss.id" />
-          </el-select>
-        </el-form-item>
-        <el-form-item :label="t('摄像头')" prop="cameraId">
-          <el-select v-model="form.cameraId" placeholder="请选择摄像头" clearable filterable style="width: 100%">
-            <el-option
-              v-for="camera in cameraOptions"
-              :key="camera.id"
-              :label="`${camera.cameraId} / ${camera.name}`"
-              :value="camera.id"
-            />
-          </el-select>
-        </el-form-item>
-        <el-form-item :label="t('推流方式')" prop="streamMethod">
-          <el-select v-model="form.streamMethod" placeholder="请选择推流方式" style="width: 100%">
-            <el-option label="ffmpeg" value="ffmpeg" />
-            <el-option label="obs" value="obs" />
-            <el-option label="gstreamer" value="gstreamer" />
-          </el-select>
-        </el-form-item>
-        <el-form-item :label="t('命令模板')" prop="commandTemplate">
-          <el-input v-model="form.commandTemplate" type="textarea" :rows="4" placeholder="请输入命令模板" />
-        </el-form-item>
-      </el-form>
-      <template #footer>
-        <el-button @click="dialogVisible = false">{{ t('取消') }}</el-button>
-        <el-button type="primary" :loading="submitLoading" @click="handleSubmit">{{ t('确定') }}</el-button>
-      </template>
-    </el-dialog>
+    <!-- 新增/编辑抽屉 -->
+    <el-drawer
+      v-model="drawerVisible"
+      :title="drawerTitle"
+      direction="rtl"
+      size="500px"
+      destroy-on-close
+      class="stream-drawer"
+    >
+      <div class="drawer-content">
+        <div class="drawer-body">
+          <el-form ref="formRef" :model="form" :rules="rules" label-width="80px" label-position="left">
+            <el-form-item label="name:" prop="name">
+              <el-input v-model="form.name" placeholder="live-stream name" style="width: 200px" />
+            </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>
+            <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-select>
+            </el-form-item>
+            <el-form-item label="命令模板:" prop="commandTemplate">
+              <div class="command-textarea-wrapper">
+                <el-input
+                  v-model="form.commandTemplate"
+                  type="textarea"
+                  :rows="10"
+                  placeholder="请输入运行参数内容"
+                  maxlength="1000"
+                  show-word-limit
+                  class="command-textarea"
+                />
+              </div>
+            </el-form-item>
+          </el-form>
+        </div>
+        <div class="drawer-footer">
+          <el-button @click="drawerVisible = false">{{ t('取消') }}</el-button>
+          <el-button type="primary" :loading="submitLoading" @click="handleSubmit">
+            {{ isEdit ? t('更新') : t('添加') }}
+          </el-button>
+        </div>
+      </div>
+    </el-drawer>
 
     <!-- 命令模板查看弹窗 -->
     <el-dialog v-model="templateDialogVisible" :title="t('命令模板')" width="600px">
@@ -172,10 +185,10 @@ import {
   updateLiveStream,
   startLiveStream,
   stopLiveStream,
-  getLssOptions,
   getCameraOptions
 } from '@/api/live-stream'
-import type { LiveStreamDTO, LssDTO, CameraInfoDTO, StreamMethod } from '@/types'
+import { listAllLssNodes } from '@/api/lss'
+import type { LiveStreamDTO, LssNodeDTO, CameraInfoDTO, StreamMethod } from '@/types'
 import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 
@@ -192,13 +205,13 @@ const loading = ref(false)
 const submitLoading = ref(false)
 const actionLoading = ref<Record<number, boolean>>({})
 const streamList = ref<LiveStreamDTO[]>([])
-const dialogVisible = ref(false)
+const drawerVisible = ref(false)
 const templateDialogVisible = ref(false)
 const formRef = ref<FormInstance>()
 const currentTemplate = ref('')
 
 // 下拉选项
-const lssOptions = ref<LssDTO[]>([])
+const lssOptions = ref<LssNodeDTO[]>([])
 const cameraOptions = ref<CameraInfoDTO[]>([])
 
 // 排序状态
@@ -240,7 +253,7 @@ const form = reactive<{
 })
 
 const isEdit = computed(() => !!form.id)
-const dialogTitle = computed(() => (isEdit.value ? t('编辑 LiveStream') : t('新增 LiveStream')))
+const drawerTitle = computed(() => (isEdit.value ? t('编辑live-stream') : t('新增live-stream')))
 
 const rules: FormRules = {
   name: [{ required: true, message: t('请输入名称'), trigger: 'blur' }],
@@ -517,19 +530,15 @@ async function getList() {
 }
 
 async function loadOptions() {
-  const useMockData = true
-
-  if (useMockData) {
-    lssOptions.value = generateMockLssOptions()
-    cameraOptions.value = generateMockCameraOptions()
-    return
-  }
-
   try {
-    const [lssRes, cameraRes] = await Promise.all([getLssOptions(), getCameraOptions()])
+    // 获取 LSS 节点列表
+    const lssRes = await listAllLssNodes()
     if (lssRes.success && lssRes.data) {
-      lssOptions.value = lssRes.data
+      lssOptions.value = lssRes.data.list || []
     }
+
+    // 获取摄像头列表(如需要可后续添加)
+    const cameraRes = await getCameraOptions()
     if (cameraRes.success && cameraRes.data) {
       cameraOptions.value = cameraRes.data
     }
@@ -568,7 +577,7 @@ function handleAdd() {
     streamMethod: 'ffmpeg',
     commandTemplate: ''
   })
-  dialogVisible.value = true
+  drawerVisible.value = true
 }
 
 function handleEdit(row: LiveStreamDTO) {
@@ -581,7 +590,7 @@ function handleEdit(row: LiveStreamDTO) {
     streamMethod: row.streamMethod,
     commandTemplate: row.commandTemplate || ''
   })
-  dialogVisible.value = true
+  drawerVisible.value = true
 }
 
 function showCommandTemplate(row: LiveStreamDTO) {
@@ -649,7 +658,7 @@ async function handleSubmit() {
           })
           if (res.success) {
             ElMessage.success(t('修改成功'))
-            dialogVisible.value = false
+            drawerVisible.value = false
             getList()
           }
         } else {
@@ -662,7 +671,7 @@ async function handleSubmit() {
           })
           if (res.success) {
             ElMessage.success(t('新增成功'))
-            dialogVisible.value = false
+            drawerVisible.value = false
             getList()
           }
         }
@@ -818,4 +827,76 @@ onMounted(() => {
   max-height: 400px;
   overflow: auto;
 }
+
+// 抽屉样式
+.stream-drawer {
+  :deep(.el-drawer__header) {
+    margin-bottom: 0;
+    padding: 16px 20px;
+    border-bottom: 1px solid #e5e7eb;
+  }
+
+  :deep(.el-drawer__body) {
+    padding: 0;
+    display: flex;
+    flex-direction: column;
+    height: 100%;
+  }
+}
+
+.drawer-content {
+  display: flex;
+  flex-direction: column;
+  height: 100%;
+}
+
+.drawer-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
+
+  :deep(.el-form-item) {
+    margin-bottom: 18px;
+  }
+
+  :deep(.el-form-item__label) {
+    color: #606266;
+    font-size: 14px;
+  }
+}
+
+.command-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 {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding: 12px 20px;
+  border-top: 1px solid #e5e7eb;
+  gap: 12px;
+
+  .el-button--primary {
+    background-color: #409eff;
+    border-color: #409eff;
+
+    &:hover,
+    &:focus {
+      background-color: #66b1ff;
+      border-color: #66b1ff;
+    }
+  }
+}
 </style>

+ 59 - 0
src/views/lss/index.vue

@@ -1423,6 +1423,65 @@ onMounted(() => {
   min-height: 200px;
 }
 
+// 设备列表抽屉样式
+.device-drawer {
+  :deep(.el-drawer__body) {
+    padding: 0;
+  }
+}
+
+.device-tabs {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+
+  :deep(.el-tabs__header) {
+    margin: 0;
+    padding: 0 20px;
+    flex-shrink: 0;
+    border-bottom: 1px solid #e5e7eb;
+  }
+
+  :deep(.el-tabs__nav-wrap::after) {
+    display: none;
+  }
+
+  :deep(.el-tabs__item) {
+    height: 48px;
+    line-height: 48px;
+    font-size: 14px;
+    color: #606266;
+
+    &.is-active {
+      color: #409eff;
+      font-weight: 500;
+    }
+
+    &:hover {
+      color: #409eff;
+    }
+  }
+
+  :deep(.el-tabs__active-bar) {
+    background-color: #409eff;
+  }
+
+  :deep(.el-tabs__content) {
+    flex: 1;
+    overflow: hidden;
+    padding: 16px;
+  }
+
+  :deep(.el-tab-pane) {
+    height: 100%;
+    overflow-y: auto;
+  }
+}
+
+.tab-content-wrapper {
+  height: 100%;
+}
+
 .drawer-footer {
   flex-shrink: 0;
   display: flex;