Explorar o código

feat(live-stream): enhance StreamEditForm with ref and improve form handling

- Added a reference to the StreamEditForm component for better form management.
- Updated form reference handling in the live-stream view to streamline submission process.
- Refactored form element references in StreamEditForm for consistency and clarity.
- Improved code readability by consolidating multiple lines into single lines where applicable.
yb hai 1 día
pai
achega
092b58318b

+ 50 - 2
src/views/live-stream/components/StreamEditForm.vue

@@ -1,7 +1,7 @@
 <template>
   <div class="tab-content edit-content">
     <div class="drawer-body">
-      <el-form ref="formRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
+      <el-form ref="elFormRef" :model="form" :rules="rules" label-width="auto" class="stream-form">
         <el-form-item :label="t('名称') + ':'" prop="name">
           <el-input v-model="form.name" :placeholder="t('例如: 测试推流-001')" style="width: 300px" />
         </el-form-item>
@@ -50,6 +50,7 @@
 </template>
 
 <script setup lang="ts">
+import { ref } from 'vue'
 import { useI18n } from 'vue-i18n'
 import type { FormInstance, FormRules } from 'element-plus'
 import CodeEditor from '@/components/CodeEditor.vue'
@@ -60,7 +61,6 @@ const { t } = useI18n({ useScope: 'global' })
 
 defineProps<{
   form: StreamForm
-  formRef: FormInstance | undefined
   rules: FormRules
   isEdit: boolean
   submitLoading: boolean
@@ -72,4 +72,52 @@ defineEmits<{
   submit: []
   cancel: []
 }>()
+
+const elFormRef = ref<FormInstance>()
+
+defineExpose({
+  formRef: elFormRef
+})
 </script>
+<style scoped lang="scss">
+.tab-content {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+.edit-content {
+  .drawer-body {
+    flex: 1;
+    overflow-y: auto;
+  }
+}
+
+.drawer-body {
+  flex: 1;
+  overflow-y: auto;
+  padding: 20px;
+}
+
+.stream-form {
+  :deep(.el-form-item) {
+    margin-bottom: 18px;
+  }
+
+  :deep(.el-form-item__label) {
+    color: #606266;
+    font-size: 14px;
+  }
+}
+
+.drawer-footer {
+  flex-shrink: 0;
+  display: flex;
+  justify-content: flex-end;
+  padding: 12px 20px;
+  border-top: 1px solid #e5e7eb;
+  gap: 12px;
+}
+</style>

+ 664 - 12
src/views/live-stream/components/StreamPlayer.vue

@@ -163,43 +163,67 @@
             </template>
             <div class="ptz-grid">
               <div class="ptz-btn" @mousedown="$emit('ptz', 'UP_LEFT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-top-left" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-top-left" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'UP')">
-                <el-icon size="24"><Icon icon="mdi:arrow-top" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-top" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'UP_RIGHT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-top-right" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-top-right" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'LEFT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-left" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-left" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn ptz-center" @click="$emit('ptzStop')">
-                <el-icon size="24"><Icon icon="mdi:stop" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:stop" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'RIGHT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-right" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-right" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'DOWN_LEFT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-left" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-left" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'DOWN')">
-                <el-icon size="24"><Icon icon="mdi:arrow-down" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-down" width="24" height="24" />
+                </el-icon>
               </div>
               <div class="ptz-btn" @mousedown="$emit('ptz', 'DOWN_RIGHT')">
-                <el-icon size="24"><Icon icon="mdi:arrow-bottom-right" width="24" height="24" /></el-icon>
+                <el-icon size="24">
+                  <Icon icon="mdi:arrow-bottom-right" width="24" height="24" />
+                </el-icon>
               </div>
             </div>
 
             <div class="zoom-buttons">
               <el-button size="small" @mousedown="$emit('zoomIn')">
-                <el-icon size="20"><Icon icon="mdi:zoom-in-outline" width="20" height="20" /></el-icon>
+                <el-icon size="20">
+                  <Icon icon="mdi:zoom-in-outline" width="20" height="20" />
+                </el-icon>
               </el-button>
               <el-button size="small" @mousedown="$emit('zoomOut')">
-                <el-icon size="20"><Icon icon="mdi:zoom-out-outline" width="20" height="20" /></el-icon>
+                <el-icon size="20">
+                  <Icon icon="mdi:zoom-out-outline" width="20" height="20" />
+                </el-icon>
               </el-button>
               <el-button type="primary" size="small" @click="$emit('ptzStop')">
-                <el-icon size="20"><Icon icon="mdi:stop" width="20" height="20" /></el-icon>
+                <el-icon size="20">
+                  <Icon icon="mdi:stop" width="20" height="20" />
+                </el-icon>
               </el-button>
             </div>
 
@@ -400,3 +424,631 @@ const editingPresetNameModel = computed({
   set: (v) => emit('update:editingPresetName', v)
 })
 </script>
+
+<style scoped lang="scss">
+.tab-content {
+  flex: 1;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
+}
+
+// 流媒体播放抽屉样式
+.media-drawer-content {
+  display: flex;
+  height: 100%;
+  padding: 20px;
+  gap: 8px;
+  overflow: hidden;
+}
+
+// 左侧视频区域
+.video-area {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  background-color: #e1e1e1;
+  overflow: hidden;
+
+  .video-header {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    padding: 12px 16px;
+    background-color: #f5f7fa;
+
+    .header-left {
+      display: flex;
+      align-items: center;
+      gap: 12px;
+    }
+
+    .title {
+      font-size: 16px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+
+  .player-container {
+    flex: 1;
+    min-height: 0;
+    background-color: #000;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    position: relative;
+
+    .player-placeholder {
+      display: flex;
+      flex-direction: column;
+      align-items: center;
+      justify-content: center;
+      color: #909399;
+
+      p {
+        margin-top: 15px;
+        font-size: 14px;
+      }
+    }
+
+    .stream-control-overlay {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      z-index: 10;
+
+      .el-button {
+        font-size: 18px;
+        padding: 16px 32px;
+        border-radius: 0;
+      }
+    }
+  }
+}
+
+// 时间轴容器
+.timeline-container {
+  background: #1a1a1a;
+  padding: 12px 16px;
+  flex-shrink: 0;
+
+  .timeline-header {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+    margin-bottom: 12px;
+
+    .timeline-label {
+      color: #fff;
+      font-size: 13px;
+      font-weight: 500;
+    }
+
+    :deep(.el-select) {
+      .el-input__wrapper {
+        background-color: #333;
+        border-color: #444;
+      }
+
+      .el-input__inner {
+        color: #fff;
+      }
+    }
+  }
+
+  .timeline-track {
+    position: relative;
+    height: 12px;
+    background: #374151;
+    border-radius: 6px;
+    margin-bottom: 8px;
+    cursor: pointer;
+
+    &.is-playing {
+      cursor: not-allowed;
+
+      .timeline-point {
+        cursor: not-allowed;
+        pointer-events: none;
+      }
+    }
+
+    .timeline-progress {
+      position: absolute;
+      top: 0;
+      left: 0;
+      height: 100%;
+      background: linear-gradient(to right, #e0ecfc, #d0e4ff);
+      border-radius: 2px 0 0 2px;
+      pointer-events: none;
+      transition: width 0.3s ease;
+
+      .progress-time {
+        position: absolute;
+        right: 0;
+        top: calc(100% + 4px);
+        transform: translateX(50%);
+        background: #ef4444;
+        color: #fff;
+        padding: 2px 6px;
+        border-radius: 4px;
+        font-size: 11px;
+        font-weight: 600;
+        white-space: nowrap;
+        pointer-events: none;
+        z-index: 15;
+
+        &::before {
+          content: '';
+          position: absolute;
+          bottom: 100%;
+          left: 50%;
+          transform: translateX(-50%);
+          border: 4px solid transparent;
+          border-bottom-color: #ef4444;
+        }
+      }
+    }
+
+    .timeline-point {
+      position: absolute;
+      top: 50%;
+      width: 24px;
+      height: 24px;
+      transform: translate(-50%, -50%);
+      background: #6b7280;
+      border-radius: 50%;
+      cursor: grab;
+      z-index: 5;
+      transition: all 0.15s ease;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+
+      &:hover {
+        transform: translate(-50%, -50%) scale(1.15);
+        background: white;
+
+        .point-tooltip {
+          opacity: 1;
+          visibility: visible;
+        }
+      }
+
+      &.active {
+        background: #ffffff;
+      }
+
+      &.selected {
+        transform: translate(-50%, -50%) scale(1.15);
+        background: #3b82f6;
+        box-shadow: 0 0 12px rgba(59, 130, 246, 0.7);
+        color: white;
+        z-index: 10;
+
+        .point-tooltip {
+          opacity: 1;
+          visibility: visible;
+        }
+      }
+
+      &.passed {
+        background: var(--color-primary);
+        color: white;
+        box-shadow: 0 0 8px rgba(var(--color-primary-light-3), 0.6);
+      }
+
+      &.dragging {
+        cursor: grabbing;
+        transform: translate(-50%, -50%) scale(1.2);
+        background: #fbbf24;
+        box-shadow: 0 0 14px rgba(251, 191, 36, 0.8);
+        z-index: 20;
+        transition: none;
+
+        .point-drag-time {
+          opacity: 1;
+          visibility: visible;
+        }
+
+        .point-tooltip {
+          opacity: 0;
+          visibility: hidden;
+        }
+      }
+
+      .point-number {
+        font-size: 11px;
+        font-weight: 600;
+        pointer-events: none;
+        line-height: 1;
+      }
+
+      .point-drag-time {
+        position: absolute;
+        bottom: calc(100% + 6px);
+        left: 50%;
+        transform: translateX(-50%);
+        background: #fbbf24;
+        color: #000;
+        padding: 4px 8px;
+        border-radius: 4px;
+        font-size: 12px;
+        font-weight: 600;
+        white-space: nowrap;
+        opacity: 0;
+        visibility: hidden;
+        pointer-events: none;
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 100%;
+          left: 50%;
+          transform: translateX(-50%);
+          border: 5px solid transparent;
+          border-top-color: #fbbf24;
+        }
+      }
+
+      .point-tooltip {
+        position: absolute;
+        bottom: calc(100% + 6px);
+        left: 50%;
+        transform: translateX(-50%);
+        background: rgba(0, 0, 0, 0.9);
+        color: #fff;
+        padding: 6px 10px;
+        border-radius: 4px;
+        font-size: 11px;
+        white-space: nowrap;
+        opacity: 0;
+        visibility: hidden;
+        transition: all 0.15s ease;
+        pointer-events: none;
+
+        .point-time {
+          color: #9ca3af;
+          font-size: 10px;
+          margin-top: 2px;
+        }
+
+        &::after {
+          content: '';
+          position: absolute;
+          top: 100%;
+          left: 50%;
+          transform: translateX(-50%);
+          border: 5px solid transparent;
+          border-top-color: rgba(0, 0, 0, 0.9);
+        }
+      }
+    }
+  }
+
+  .timeline-scale {
+    display: flex;
+    justify-content: space-between;
+    padding: 0 2px;
+
+    .scale-mark {
+      font-size: 10px;
+      color: #6b7280;
+    }
+  }
+
+  .timeline-context-menu {
+    position: fixed;
+    background: #2a2a2a;
+    border: 1px solid #404040;
+    border-radius: 6px;
+    padding: 4px 0;
+    min-width: 140px;
+    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
+    z-index: 1000;
+
+    .context-menu-item {
+      display: flex;
+      align-items: center;
+      gap: 8px;
+      padding: 8px 12px;
+      color: #e5e7eb;
+      font-size: 13px;
+      cursor: pointer;
+      transition: background 0.15s ease;
+
+      .el-icon {
+        font-size: 16px;
+      }
+
+      &:hover {
+        background: #374151;
+      }
+
+      &.danger {
+        color: #f87171;
+
+        &:hover {
+          background: rgba(239, 68, 68, 0.15);
+        }
+      }
+    }
+  }
+}
+
+// 右侧控制面板
+.control-panel {
+  width: 280px;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  overflow-y: auto;
+
+  .ptz-collapse {
+    border: none;
+
+    :deep(.el-collapse-item) {
+      margin-bottom: 8px;
+      background-color: #fff;
+      border-radius: 8px;
+      overflow: hidden;
+      box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
+
+      .el-collapse-item__header {
+        padding: 0 16px;
+        height: 44px;
+        border-bottom: 1px solid #e5e7eb;
+        background-color: #f5f7fa;
+
+        &.is-active {
+          border-bottom-color: #e5e7eb;
+        }
+      }
+
+      .el-collapse-item__wrap {
+        border-bottom: none;
+      }
+
+      .el-collapse-item__content {
+        padding: 0;
+      }
+    }
+
+    .collapse-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #303133;
+    }
+  }
+}
+
+// PTZ 方向控制网格
+.ptz-grid {
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  gap: 6px;
+  margin: 12px;
+}
+
+.ptz-btn {
+  aspect-ratio: 1;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  background-color: #f5f7fa;
+  border: 1px solid #dcdfe6;
+  border-radius: 6px;
+  cursor: pointer;
+  transition: all 0.2s;
+  color: #606266;
+
+  &:hover {
+    background-color: #ecf5ff;
+    border-color: #4f46e5;
+    color: #4f46e5;
+  }
+
+  &:active {
+    background-color: #4f46e5;
+    color: #fff;
+  }
+
+  .el-icon {
+    font-size: 18px;
+  }
+
+  &.ptz-center {
+    background-color: #e5e7eb;
+
+    &:hover {
+      background-color: #4f46e5;
+      color: #fff;
+    }
+  }
+}
+
+.zoom-buttons {
+  display: flex;
+  justify-content: center;
+  gap: 12px;
+  margin-bottom: 12px;
+
+  .el-button {
+    background-color: #f5f7fa;
+    border-color: #dcdfe6;
+    color: #606266;
+
+    &:hover {
+      background-color: #ecf5ff;
+      border-color: #4f46e5;
+      color: #4f46e5;
+    }
+  }
+}
+
+.speed-slider {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+
+  .label {
+    font-size: 12px;
+    color: #606266;
+    flex-shrink: 0;
+  }
+
+  .value {
+    font-size: 12px;
+    color: #4f46e5;
+    width: 30px;
+    text-align: right;
+  }
+
+  :deep(.el-slider) {
+    flex: 1;
+  }
+}
+
+.preset-list {
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  max-height: 250px;
+  overflow-y: auto;
+}
+
+.preset-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 10px;
+  background-color: #fff;
+  border-bottom: 1px solid #ebeef5;
+  cursor: default;
+  transition: all 0.2s;
+
+  &:hover {
+    background-color: #f5f7fa;
+
+    .preset-actions {
+      opacity: 1;
+      visibility: visible;
+    }
+  }
+
+  &.active {
+    background-color: #ecf5ff;
+
+    .preset-index {
+      background-color: #4f46e5;
+      color: #fff;
+    }
+
+    .preset-name {
+      color: #4f46e5;
+      font-weight: 500;
+    }
+  }
+
+  .preset-index {
+    width: 24px;
+    height: 24px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    background-color: #f0f0f0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 500;
+    color: #606266;
+    flex-shrink: 0;
+  }
+
+  .preset-name {
+    flex: 1;
+    font-size: 13px;
+    color: #606266;
+    overflow: hidden;
+    text-overflow: ellipsis;
+    white-space: nowrap;
+    cursor: pointer;
+    padding: 2px 4px;
+    border-radius: 4px;
+
+    &:hover {
+      background-color: rgba(0, 0, 0, 0.05);
+    }
+  }
+
+  .preset-name-input {
+    flex: 1;
+
+    :deep(.el-input__wrapper) {
+      padding: 0 8px;
+    }
+
+    :deep(.el-input__inner) {
+      font-size: 13px;
+    }
+  }
+
+  .preset-actions {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    opacity: 0;
+    visibility: hidden;
+    transition: all 0.2s;
+
+    .action-icon {
+      width: 20px;
+      height: 20px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      cursor: pointer;
+      color: #909399;
+      transition: color 0.2s;
+
+      &:hover {
+        color: #4f46e5;
+      }
+
+      &.delete:hover {
+        color: #f56c6c;
+      }
+    }
+  }
+}
+
+.camera-info-content {
+  min-height: 60px;
+}
+
+.info-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 6px 0;
+  border-bottom: 1px dashed #e5e7eb;
+
+  &:last-child {
+    border-bottom: none;
+  }
+}
+
+.info-label {
+  font-size: 12px;
+  color: #909399;
+}
+
+.info-value {
+  font-size: 12px;
+  color: #303133;
+  font-weight: 500;
+}
+</style>

+ 6 - 4
src/views/live-stream/composables/useStreamList.ts

@@ -142,6 +142,7 @@ export function useStreamList() {
     currentPage.value = 1
     sortState.prop = ''
     sortState.order = null
+    router.replace({ path: '/live-stream-manage/list', query: {} })
     getList()
   }
 
@@ -208,10 +209,11 @@ export function useStreamList() {
     }
   }
 
-  async function handleSubmit(drawerVisibleRef: { value: boolean }) {
-    if (!formRef.value) return
+  async function handleSubmit(drawerVisibleRef: { value: boolean }, externalFormRef?: FormInstance) {
+    const form$ = externalFormRef || formRef.value
+    if (!form$) return
 
-    await formRef.value.validate(async (valid) => {
+    await form$.validate(async (valid) => {
       if (valid) {
         submitLoading.value = true
         try {
@@ -253,7 +255,7 @@ export function useStreamList() {
                 const newQuery = { ...route.query }
                 delete newQuery.lssId
                 delete newQuery.action
-                router.replace({ path: '/live-stream', query: newQuery })
+                router.replace({ path: '/live-stream-manage/list', query: newQuery })
               }
               getList()
             } else {

+ 5 - 4
src/views/live-stream/index.vue

@@ -146,9 +146,9 @@
 
         <!-- 编辑 Tab 内容 -->
         <StreamEditForm
+          ref="editFormComponent"
           v-show="activeDrawerTab === 'edit'"
           :form="form"
-          :form-ref="formRef"
           :rules="rules"
           :is-edit="isEdit"
           :submit-loading="submitLoading"
@@ -240,7 +240,7 @@
 </template>
 
 <script setup lang="ts">
-import { onMounted } from 'vue'
+import { ref, onMounted } from 'vue'
 import { useRoute } from 'vue-router'
 import { Search, RefreshRight, Plus } from '@element-plus/icons-vue'
 import { Icon } from '@iconify/vue'
@@ -266,7 +266,6 @@ const {
   loading,
   submitLoading,
   streamList,
-  formRef,
   commandDialogVisible,
   currentCommandTemplate,
   commandUpdateLoading,
@@ -392,8 +391,10 @@ function onPlay(row: LiveStreamDTO) {
   })
 }
 
+const editFormComponent = ref<InstanceType<typeof StreamEditForm>>()
+
 function onSubmit() {
-  handleSubmit(drawerVisible)
+  handleSubmit(drawerVisible, editFormComponent.value?.formRef)
 }
 
 onMounted(async () => {