Răsfoiți Sursa

feat(json-editor): add JSON editor component and integrate into LSS and live stream views

- Introduced a new JsonEditor component for editing JSON data with validation and formatting features.
- Integrated JsonEditor into LSS parameter configuration and runtime parameters for improved user experience.
- Updated live stream view to utilize JsonEditor for command template input, enhancing JSON handling capabilities.
- Added necessary Codemirror dependencies for JSON editing functionality.
yb 1 săptămână în urmă
părinte
comite
6ab7d15546

+ 5 - 0
package.json

@@ -33,9 +33,13 @@
     ]
   },
   "dependencies": {
+    "@codemirror/lang-json": "^6.0.2",
+    "@codemirror/theme-one-dark": "^6.1.3",
+    "@codemirror/view": "^6.39.11",
     "@element-plus/icons-vue": "^2.1.0",
     "@vueuse/core": "^14.1.0",
     "axios": "^1.4.0",
+    "codemirror": "^6.0.2",
     "date-fns": "^4.1.0",
     "date-fns-tz": "^3.2.0",
     "dayjs": "^1.11.19",
@@ -47,6 +51,7 @@
     "pinia": "^2.0.36",
     "sass": "^1.62.1",
     "vue": "^3.5.13",
+    "vue-codemirror": "^6.1.1",
     "vue-i18n": "^11.2.8",
     "vue-router": "^4.2.0"
   },

+ 176 - 0
pnpm-lock.yaml

@@ -8,6 +8,15 @@ importers:
 
   .:
     dependencies:
+      '@codemirror/lang-json':
+        specifier: ^6.0.2
+        version: 6.0.2
+      '@codemirror/theme-one-dark':
+        specifier: ^6.1.3
+        version: 6.1.3
+      '@codemirror/view':
+        specifier: ^6.39.11
+        version: 6.39.11
       '@element-plus/icons-vue':
         specifier: ^2.1.0
         version: 2.3.2(vue@3.5.26(typescript@5.6.3))
@@ -17,6 +26,9 @@ importers:
       axios:
         specifier: ^1.4.0
         version: 1.13.2
+      codemirror:
+        specifier: ^6.0.2
+        version: 6.0.2
       date-fns:
         specifier: ^4.1.0
         version: 4.1.0
@@ -50,6 +62,9 @@ importers:
       vue:
         specifier: ^3.5.13
         version: 3.5.26(typescript@5.6.3)
+      vue-codemirror:
+        specifier: ^6.1.1
+        version: 6.1.1(codemirror@6.0.2)(vue@3.5.26(typescript@5.6.3))
       vue-i18n:
         specifier: ^11.2.8
         version: 11.2.8(vue@3.5.26(typescript@5.6.3))
@@ -325,6 +340,33 @@ packages:
     resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
     engines: {node: '>=18'}
 
+  '@codemirror/autocomplete@6.20.0':
+    resolution: {integrity: sha512-bOwvTOIJcG5FVo5gUUupiwYh8MioPLQ4UcqbcRf7UQ98X90tCa9E1kZ3Z7tqwpZxYyOvh1YTYbmZE9RTfTp5hg==}
+
+  '@codemirror/commands@6.10.1':
+    resolution: {integrity: sha512-uWDWFypNdQmz2y1LaNJzK7fL7TYKLeUAU0npEC685OKTF3KcQ2Vu3klIM78D7I6wGhktme0lh3CuQLv0ZCrD9Q==}
+
+  '@codemirror/lang-json@6.0.2':
+    resolution: {integrity: sha512-x2OtO+AvwEHrEwR0FyyPtfDUiloG3rnVTSZV1W8UteaLL8/MajQd8DpvUb2YVzC+/T18aSDv0H9mu+xw0EStoQ==}
+
+  '@codemirror/language@6.12.1':
+    resolution: {integrity: sha512-Fa6xkSiuGKc8XC8Cn96T+TQHYj4ZZ7RdFmXA3i9xe/3hLHfwPZdM+dqfX0Cp0zQklBKhVD8Yzc8LS45rkqcwpQ==}
+
+  '@codemirror/lint@6.9.2':
+    resolution: {integrity: sha512-sv3DylBiIyi+xKwRCJAAsBZZZWo82shJ/RTMymLabAdtbkV5cSKwWDeCgtUq3v8flTaXS2y1kKkICuRYtUswyQ==}
+
+  '@codemirror/search@6.6.0':
+    resolution: {integrity: sha512-koFuNXcDvyyotWcgOnZGmY7LZqEOXZaaxD/j6n18TCLx2/9HieZJ5H6hs1g8FiRxBD0DNfs0nXn17g872RmYdw==}
+
+  '@codemirror/state@6.5.4':
+    resolution: {integrity: sha512-8y7xqG/hpB53l25CIoit9/ngxdfoG+fx+V3SHBrinnhOtLvKHRyAJJuHzkWrR4YXXLX8eXBsejgAAxHUOdW1yw==}
+
+  '@codemirror/theme-one-dark@6.1.3':
+    resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==}
+
+  '@codemirror/view@6.39.11':
+    resolution: {integrity: sha512-bWdeR8gWM87l4DB/kYSF9A+dVackzDb/V56Tq7QVrQ7rn86W0rgZFtlL3g3pem6AeGcb9NQNoy3ao4WpW4h5tQ==}
+
   '@commitlint/cli@12.1.4':
     resolution: {integrity: sha512-ZR1WjXLvqEffYyBPT0XdnSxtt3Ty1TMoujEtseW5o3vPnkA1UNashAMjQVg/oELqfaiAMnDw8SERPMN0e/0kLg==}
     engines: {node: '>=v10'}
@@ -789,6 +831,21 @@ packages:
   '@jridgewell/trace-mapping@0.3.31':
     resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
 
+  '@lezer/common@1.5.0':
+    resolution: {integrity: sha512-PNGcolp9hr4PJdXR4ix7XtixDrClScvtSCYW3rQG106oVMOOI+jFb+0+J3mbeL/53g1Zd6s0kJzaw6Ri68GmAA==}
+
+  '@lezer/highlight@1.2.3':
+    resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==}
+
+  '@lezer/json@1.0.3':
+    resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==}
+
+  '@lezer/lr@1.4.7':
+    resolution: {integrity: sha512-wNIFWdSUfX9Jc6ePMzxSPVgTVB4EOfDIwLQLWASyiUdHKaMsiilj9bYiGkGQCKVodd0x6bgQCV207PILGFCF9Q==}
+
+  '@marijn/find-cluster-break@1.0.2':
+    resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==}
+
   '@nodelib/fs.scandir@2.1.5':
     resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
     engines: {node: '>= 8'}
@@ -1598,6 +1655,9 @@ packages:
     resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==}
     engines: {node: '>=0.8'}
 
+  codemirror@6.0.2:
+    resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==}
+
   color-convert@2.0.1:
     resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
     engines: {node: '>=7.0.0'}
@@ -1669,6 +1729,9 @@ packages:
     resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
     engines: {node: '>=10'}
 
+  crelt@1.0.6:
+    resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==}
+
   cross-spawn@7.0.6:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
@@ -3414,6 +3477,9 @@ packages:
   strip-literal@2.1.1:
     resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
 
+  style-mod@4.1.3:
+    resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==}
+
   superjson@2.2.6:
     resolution: {integrity: sha512-H+ue8Zo4vJmV2nRjpx86P35lzwDT3nItnIsocgumgr0hHMQ+ZGq5vrERg9kJBo5AWGmxZDhzDo+WVIJqkB0cGA==}
     engines: {node: '>=16'}
@@ -3782,6 +3848,12 @@ packages:
   vscode-uri@3.1.0:
     resolution: {integrity: sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==}
 
+  vue-codemirror@6.1.1:
+    resolution: {integrity: sha512-rTAYo44owd282yVxKtJtnOi7ERAcXTeviwoPXjIc6K/IQYUsoDkzPvw/JDFtSP6T7Cz/2g3EHaEyeyaQCKoDMg==}
+    peerDependencies:
+      codemirror: 6.x
+      vue: 3.x
+
   vue-component-type-helpers@2.2.12:
     resolution: {integrity: sha512-YbGqHZ5/eW4SnkPNR44mKVc6ZKQoRs/Rux1sxC6rdwXb4qpbOSYfDr9DsTHolOTGmIKgM9j141mZbBeg05R1pw==}
 
@@ -3827,6 +3899,9 @@ packages:
       typescript:
         optional: true
 
+  w3c-keyname@2.2.8:
+    resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==}
+
   webpack-virtual-modules@0.6.2:
     resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==}
 
@@ -4137,6 +4212,64 @@ snapshots:
 
   '@bcoe/v8-coverage@1.0.2': {}
 
+  '@codemirror/autocomplete@6.20.0':
+    dependencies:
+      '@codemirror/language': 6.12.1
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      '@lezer/common': 1.5.0
+
+  '@codemirror/commands@6.10.1':
+    dependencies:
+      '@codemirror/language': 6.12.1
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      '@lezer/common': 1.5.0
+
+  '@codemirror/lang-json@6.0.2':
+    dependencies:
+      '@codemirror/language': 6.12.1
+      '@lezer/json': 1.0.3
+
+  '@codemirror/language@6.12.1':
+    dependencies:
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      '@lezer/common': 1.5.0
+      '@lezer/highlight': 1.2.3
+      '@lezer/lr': 1.4.7
+      style-mod: 4.1.3
+
+  '@codemirror/lint@6.9.2':
+    dependencies:
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      crelt: 1.0.6
+
+  '@codemirror/search@6.6.0':
+    dependencies:
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      crelt: 1.0.6
+
+  '@codemirror/state@6.5.4':
+    dependencies:
+      '@marijn/find-cluster-break': 1.0.2
+
+  '@codemirror/theme-one-dark@6.1.3':
+    dependencies:
+      '@codemirror/language': 6.12.1
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      '@lezer/highlight': 1.2.3
+
+  '@codemirror/view@6.39.11':
+    dependencies:
+      '@codemirror/state': 6.5.4
+      crelt: 1.0.6
+      style-mod: 4.1.3
+      w3c-keyname: 2.2.8
+
   '@commitlint/cli@12.1.4':
     dependencies:
       '@commitlint/format': 12.1.4
@@ -4494,6 +4627,24 @@ snapshots:
       '@jridgewell/resolve-uri': 3.1.2
       '@jridgewell/sourcemap-codec': 1.5.5
 
+  '@lezer/common@1.5.0': {}
+
+  '@lezer/highlight@1.2.3':
+    dependencies:
+      '@lezer/common': 1.5.0
+
+  '@lezer/json@1.0.3':
+    dependencies:
+      '@lezer/common': 1.5.0
+      '@lezer/highlight': 1.2.3
+      '@lezer/lr': 1.4.7
+
+  '@lezer/lr@1.4.7':
+    dependencies:
+      '@lezer/common': 1.5.0
+
+  '@marijn/find-cluster-break@1.0.2': {}
+
   '@nodelib/fs.scandir@2.1.5':
     dependencies:
       '@nodelib/fs.stat': 2.0.5
@@ -5352,6 +5503,16 @@ snapshots:
 
   clone@2.1.2: {}
 
+  codemirror@6.0.2:
+    dependencies:
+      '@codemirror/autocomplete': 6.20.0
+      '@codemirror/commands': 6.10.1
+      '@codemirror/language': 6.12.1
+      '@codemirror/lint': 6.9.2
+      '@codemirror/search': 6.6.0
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+
   color-convert@2.0.1:
     dependencies:
       color-name: 1.1.4
@@ -5424,6 +5585,8 @@ snapshots:
       path-type: 4.0.0
       yaml: 1.10.2
 
+  crelt@1.0.6: {}
+
   cross-spawn@7.0.6:
     dependencies:
       path-key: 3.1.1
@@ -7392,6 +7555,8 @@ snapshots:
     dependencies:
       js-tokens: 9.0.1
 
+  style-mod@4.1.3: {}
+
   superjson@2.2.6:
     dependencies:
       copy-anything: 4.0.5
@@ -7795,6 +7960,15 @@ snapshots:
 
   vscode-uri@3.1.0: {}
 
+  vue-codemirror@6.1.1(codemirror@6.0.2)(vue@3.5.26(typescript@5.6.3)):
+    dependencies:
+      '@codemirror/commands': 6.10.1
+      '@codemirror/language': 6.12.1
+      '@codemirror/state': 6.5.4
+      '@codemirror/view': 6.39.11
+      codemirror: 6.0.2
+      vue: 3.5.26(typescript@5.6.3)
+
   vue-component-type-helpers@2.2.12: {}
 
   vue-demi@0.14.10(vue@3.5.26(typescript@5.6.3)):
@@ -7843,6 +8017,8 @@ snapshots:
     optionalDependencies:
       typescript: 5.6.3
 
+  w3c-keyname@2.2.8: {}
+
   webpack-virtual-modules@0.6.2: {}
 
   whatwg-mimetype@3.0.0: {}

+ 1 - 0
src/components.d.ts

@@ -64,6 +64,7 @@ declare module 'vue' {
     IEpLoading: typeof import('~icons/ep/loading')['default']
     IEpTopLeft: typeof import('~icons/ep/top-left')['default']
     IEpTopRight: typeof import('~icons/ep/top-right')['default']
+    JsonEditor: typeof import('./components/JsonEditor.vue')['default']
     LangDropdown: typeof import('./components/LangDropdown.vue')['default']
     MTable: typeof import('./components/mTable/index.vue')['default']
     PTZController: typeof import('./components/PTZController.vue')['default']

+ 188 - 0
src/components/JsonEditor.vue

@@ -0,0 +1,188 @@
+<template>
+  <div class="json-editor">
+    <div class="editor-wrapper">
+      <el-button class="format-btn" size="small" :icon="DocumentCopy" @click="handleFormat" :disabled="!isValidJson">
+        格式化
+      </el-button>
+      <Codemirror
+        v-model="localValue"
+        :style="{ height: height }"
+        :extensions="extensions"
+        :autofocus="autofocus"
+        :indent-with-tab="true"
+        :tab-size="2"
+        @change="handleChange"
+      />
+    </div>
+    <div v-if="!isValidJson && modelValue" class="json-error">
+      <el-icon><WarningFilled /></el-icon>
+      JSON 格式错误
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch } from 'vue'
+import { Codemirror } from 'vue-codemirror'
+import { json } from '@codemirror/lang-json'
+import { oneDark } from '@codemirror/theme-one-dark'
+import { EditorView } from '@codemirror/view'
+import { DocumentCopy, WarningFilled } from '@element-plus/icons-vue'
+
+interface Props {
+  modelValue: string
+  height?: string
+  dark?: boolean
+  autofocus?: boolean
+  placeholder?: string
+}
+
+const props = withDefaults(defineProps<Props>(), {
+  modelValue: '',
+  height: '200px',
+  dark: false,
+  autofocus: false,
+  placeholder: ''
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: string): void
+}>()
+
+const localValue = ref(props.modelValue)
+
+// Watch for external changes
+watch(
+  () => props.modelValue,
+  (newVal) => {
+    if (newVal !== localValue.value) {
+      localValue.value = newVal
+    }
+  }
+)
+
+// Editor extensions
+const extensions = computed(() => {
+  const exts = [
+    json(),
+    EditorView.lineWrapping,
+    EditorView.theme({
+      '&': {
+        fontSize: '13px',
+        border: '1px solid #dcdfe6',
+        borderRadius: '4px'
+      },
+      '.cm-content': {
+        fontFamily: 'Menlo, Monaco, Consolas, "Courier New", 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'
+      }
+    })
+  ]
+
+  if (props.dark) {
+    exts.push(oneDark)
+  }
+
+  return exts
+})
+
+// Check if current value is valid JSON
+const isValidJson = computed(() => {
+  if (!localValue.value || !localValue.value.trim()) {
+    return true // Empty is considered valid
+  }
+  try {
+    JSON.parse(localValue.value)
+    return true
+  } catch {
+    return false
+  }
+})
+
+// Handle value change
+function handleChange(value: string) {
+  localValue.value = value
+  emit('update:modelValue', value)
+}
+
+// Format JSON
+function handleFormat() {
+  if (!localValue.value || !localValue.value.trim()) return
+
+  try {
+    const parsed = JSON.parse(localValue.value)
+    const formatted = JSON.stringify(parsed, null, 2)
+    localValue.value = formatted
+    emit('update:modelValue', formatted)
+  } catch {
+    // Already showing error, do nothing
+  }
+}
+
+// Expose format method
+defineExpose({
+  format: handleFormat,
+  isValid: isValidJson
+})
+</script>
+
+<style lang="scss" scoped>
+.json-editor {
+  width: 100%;
+}
+
+.editor-wrapper {
+  position: relative;
+}
+
+.format-btn {
+  position: absolute;
+  top: 6px;
+  right: 6px;
+  z-index: 10;
+  opacity: 0.85;
+
+  &:hover {
+    opacity: 1;
+  }
+}
+
+.json-error {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-top: 6px;
+  color: #f56c6c;
+  font-size: 12px;
+
+  .el-icon {
+    font-size: 14px;
+  }
+}
+
+:deep(.cm-editor) {
+  height: 100%;
+}
+
+:deep(.cm-scroller) {
+  overflow: auto;
+}
+</style>

+ 1 - 1
src/views/live-stream/index.vue

@@ -234,7 +234,7 @@
     <el-drawer
       v-model="mediaDrawerVisible"
       direction="rtl"
-      size="50%"
+      size="90%"
       :with-header="false"
       destroy-on-close
       class="media-drawer"

+ 5 - 5
src/views/lss/index.vue

@@ -466,10 +466,10 @@
           <el-input v-model="cameraForm.password" type="password" show-password placeholder="请输入密码" />
         </el-form-item> -->
         <el-form-item label="参数配置">
-          <el-input v-model="cameraForm.paramConfig" type="textarea" :rows="5" placeholder="请输入参数配置" />
+          <JsonEditor v-model="cameraForm.paramConfig" height="150px" placeholder="请输入参数配置 (JSON)" />
         </el-form-item>
         <el-form-item label="设备运行参数">
-          <el-input v-model="cameraForm.runtimeParams" type="textarea" :rows="5" placeholder="设备运行参数" />
+          <JsonEditor v-model="cameraForm.runtimeParams" height="150px" placeholder="设备运行参数 (JSON)" />
         </el-form-item>
       </el-form>
       <template #footer>
@@ -490,10 +490,9 @@
       destroy-on-close
       class="params-drawer"
     >
-      <el-input
+      <JsonEditor
         v-model="paramsContent"
-        type="textarea"
-        :rows="20"
+        height="500px"
         :placeholder="paramsDialogType === 'config' ? '请输入参数配置(JSON 格式)' : '请输入运行参数(JSON 格式)'"
       />
       <template #footer>
@@ -528,6 +527,7 @@ import { listLssNodes, deleteLssNode, setLssNodeEnabled, updateLssNode } from '@
 import { adminListCameras, adminAddCamera, adminUpdateCamera, adminDeleteCamera, adminGetCamera } from '@/api/camera'
 import { listCameraVendors } from '@/api/camera-vendor'
 import { Icon } from '@iconify/vue'
+import JsonEditor from '@/components/JsonEditor.vue'
 import type {
   LssNodeDTO,
   LssNodeStatus,