Преглед изворни кода

feat: add BashEditor component for enhanced script editing

- Introduced a new BashEditor component to facilitate Bash script editing with syntax highlighting and clipboard functionality.
- Updated the live-stream view to utilize the new BashEditor for command template editing, replacing the previous JsonEditor.
- Enhanced the global components declaration to include the new BashEditor for better accessibility across the application.
yb пре 1 недеља
родитељ
комит
8bef332171
4 измењених фајлова са 235 додато и 24 уклоњено
  1. 1 0
      src/components.d.ts
  2. 214 0
      src/components/BashEditor.vue
  3. 11 21
      src/views/live-stream/index.vue
  4. 9 3
      src/views/lss/index.vue

+ 1 - 0
src/components.d.ts

@@ -7,6 +7,7 @@ export {}
 /* prettier-ignore */
 declare module 'vue' {
   export interface GlobalComponents {
+    BashEditor: typeof import('./components/BashEditor.vue')['default']
     CameraSelector: typeof import('./components/monitor/CameraSelector.vue')['default']
     ElAlert: typeof import('element-plus/es')['ElAlert']
     ElButton: typeof import('element-plus/es')['ElButton']

+ 214 - 0
src/components/BashEditor.vue

@@ -0,0 +1,214 @@
+<template>
+  <div class="bash-editor" :style="{ height: height }">
+    <div class="editor-wrapper">
+      <div class="editor-header">
+        <span class="file-type">
+          <el-icon><Monitor /></el-icon>
+          Bash Script
+        </span>
+        <el-button class="copy-btn" size="small" :icon="DocumentCopy" @click="handleCopy">
+          {{ t('复制') }}
+        </el-button>
+      </div>
+      <Codemirror
+        v-model="localValue"
+        :style="{ height: editorHeight }"
+        :extensions="extensions"
+        :autofocus="autofocus"
+        :indent-with-tab="true"
+        :tab-size="2"
+        :placeholder="placeholder"
+        @change="handleChange"
+      />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { Codemirror } from 'vue-codemirror'
+import { EditorView } from '@codemirror/view'
+import { DocumentCopy, Monitor } from '@element-plus/icons-vue'
+import { ElMessage } from 'element-plus'
+import { useI18n } from 'vue-i18n'
+
+const { t } = useI18n({ useScope: 'global' })
+
+interface Props {
+  modelValue: string
+  height?: string
+  autofocus?: boolean
+  placeholder?: string
+  readonly?: boolean
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: '',
+  height: '400px',
+  autofocus: false,
+  placeholder: '#!/bin/bash\n# Enter your script here...',
+  readonly: false
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+}>()
+
+const localValue = ref(props.modelValue)
+
+// Calculate editor height (minus header)
+const editorHeight = computed(() => {
+  const totalHeight = parseInt(props.height)
+  return `${totalHeight - 40}px`
+})
+
+// Watch for external changes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal !== localValue.value) {
+      localValue.value = newVal
+    }
+  }
+)
+
+// Editor extensions with light theme (similar to JsonEditor)
+const extensions = computed(() => {
+  const exts = [
+    EditorView.lineWrapping,
+    EditorView.theme({
+      '&': {
+        fontSize: '13px',
+        border: '1px solid #dcdfe6',
+        borderRadius: '4px'
+      },
+      '.cm-content': {
+        fontFamily: "'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace",
+        padding: '8px 0'
+      },
+      '.cm-gutters': {
+        backgroundColor: '#f5f7fa',
+        color: '#909399',
+        border: 'none',
+        borderRight: '1px solid #e4e7ed'
+      },
+      '.cm-activeLineGutter': {
+        backgroundColor: '#e8eaed'
+      },
+      '.cm-activeLine': {
+        backgroundColor: '#f5f7fa'
+      },
+      '&.cm-focused': {
+        outline: 'none'
+      },
+      '&.cm-focused .cm-selectionBackground, ::selection': {
+        backgroundColor: '#d7d4f0 !important'
+      },
+      '.cm-placeholder': {
+        color: '#909399'
+      }
+    })
+  ]
+
+  if (props.readonly) {
+    exts.push(EditorView.editable.of(false))
+  }
+
+  return exts
+})
+
+// Handle value change
+function handleChange(value: string) {
+  localValue.value = value
+  emit('update:modelValue', value)
+}
+
+// Copy to clipboard
+async function handleCopy() {
+  if (!localValue.value) return
+
+  try {
+    await navigator.clipboard.writeText(localValue.value)
+    ElMessage.success(t('已复制到剪贴板'))
+  } catch {
+    ElMessage.error(t('复制失败'))
+  }
+}
+
+// Expose methods
+defineExpose({
+  getValue: () => localValue.value,
+  setValue: (val: string) => {
+    localValue.value = val
+    emit('update:modelValue', val)
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.bash-editor {
+  width: 100%;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.editor-wrapper {
+  position: relative;
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  background-color: #fff;
+}
+
+.editor-header {
+  flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  height: 32px;
+  padding: 0 8px;
+  background-color: #f5f7fa;
+  border: 1px solid #dcdfe6;
+  border-bottom: none;
+  border-radius: 4px 4px 0 0;
+
+  .file-type {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 12px;
+    color: #909399;
+    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
+
+    .el-icon {
+      font-size: 14px;
+      color: #67c23a;
+    }
+  }
+
+  .copy-btn {
+    background-color: transparent;
+    border-color: #dcdfe6;
+    color: #606266;
+
+    &:hover {
+      background-color: #ecf5ff;
+      border-color: #c6e2ff;
+      color: #409eff;
+    }
+  }
+}
+
+:deep(.cm-editor) {
+  height: 100%;
+  flex: 1;
+}
+
+:deep(.cm-scroller) {
+  overflow: auto;
+}
+
+:deep(.cm-line) {
+  padding-left: 8px;
+}
+</style>

+ 11 - 21
src/views/live-stream/index.vue

@@ -187,10 +187,8 @@
     </el-drawer>
 
     <!-- 命令模板查看/编辑弹窗 -->
-    <el-dialog v-model="commandDialogVisible" :title="t('命令模板')" width="700px" destroy-on-close>
-      <div class="command-content">
-        <JsonEditor v-model="currentCommandTemplate" height="400px" />
-      </div>
+    <el-dialog v-model="commandDialogVisible" :title="t('命令模板')" width="800px" destroy-on-close>
+      <BashEditor v-model="currentCommandTemplate" height="450px" placeholder="#!/bin/bash&#10;# FFmpeg 推流命令模板" />
       <template #footer>
         <el-button @click="commandDialogVisible = false">{{ t('关闭') }}</el-button>
         <el-button type="primary" :loading="commandUpdateLoading" @click="handleUpdateCommandTemplate">
@@ -381,6 +379,7 @@
 
 <script setup lang="ts">
 import { ref, reactive, onMounted, computed, watch } from 'vue'
+import { useRoute } from 'vue-router'
 import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
 import {
   Search,
@@ -407,13 +406,14 @@ import { adminListCameras } from '@/api/camera'
 
 import { startStreamTask, stopStreamTask, getStreamPlayback } from '@/api/stream-push'
 import VideoPlayer from '@/components/VideoPlayer.vue'
+import BashEditor from '@/components/BashEditor.vue'
 import { ptzStart, ptzStop, getPresets, gotoPreset, type PresetInfo } from '@/api/camera'
-import JsonEditor from '@/components/JsonEditor.vue'
 import type { LiveStreamDTO, LiveStreamStatus, LssNodeDTO, CameraInfoDTO, StreamChannelDTO } from '@/types'
 import dayjs from 'dayjs'
 import { useI18n } from 'vue-i18n'
 
 const { t } = useI18n({ useScope: 'global' })
+const route = useRoute()
 
 // 格式化时间
 function formatDateTime(dateStr: string | undefined): string {
@@ -1075,6 +1075,12 @@ function handleCurrentChange(val: number) {
 }
 
 onMounted(() => {
+  // 读取 URL 查询参数
+  const queryCameraId = route.query.cameraId as string
+  if (queryCameraId) {
+    searchForm.cameraId = queryCameraId
+  }
+
   getList()
   loadOptions()
 })
@@ -1251,22 +1257,6 @@ onMounted(() => {
   }
 }
 
-// 命令模板弹窗样式
-.command-content {
-  :deep(.el-textarea__inner) {
-    font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
-    font-size: 13px;
-    line-height: 1.6;
-    background-color: #1e1e1e;
-    color: #d4d4d4;
-    border-radius: 6px;
-
-    &:focus {
-      border-color: #4f46e5;
-    }
-  }
-}
-
 // 流媒体播放抽屉样式
 .media-drawer {
   :deep(.el-drawer__body) {

+ 9 - 3
src/views/lss/index.vue

@@ -253,7 +253,12 @@
                 <template #default="{ row }">
                   <el-button type="primary" link :icon="Edit" @click="handleEditCamera(row)" />
                   <el-button type="danger" link :icon="Delete" @click="handleDeleteCamera(row)" />
-                  <el-button link :class="['crosshairs-btn', { active: !row.streamSn }]" @click="handleViewCamera(row)">
+                  <el-button
+                    :tooltip="t('查看Cloudflare Stream')"
+                    link
+                    :class="['crosshairs-btn', { active: !row.streamSn }]"
+                    @click="handleViewCamera(row)"
+                  >
                     <Icon icon="mdi:crosshairs" />
                   </el-button>
                 </template>
@@ -474,10 +479,11 @@
           <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
         </el-form-item> -->
         <el-form-item label="参数配置">
-          <JsonEditor v-model="cameraForm.paramConfig" height="200px" placeholder="请输入参数配置 (JSON)" />
+          <JsonEditor v-model="cameraForm.paramConfig" placeholder="请输入参数配置 (JSON)" />
         </el-form-item>
+        <br />
         <el-form-item label="设备运行参数">
-          <JsonEditor v-model="cameraForm.runtimeParams" height="200px" placeholder="设备运行参数 (JSON)" />
+          <JsonEditor v-model="cameraForm.runtimeParams" placeholder="设备运行参数 (JSON)" />
         </el-form-item>
       </el-form>
       <template #footer>