Bladeren bron

创维2.0 功能更新

liujintao 18 uur geleden
bovenliggende
commit
abaf35dbd2
34 gewijzigde bestanden met toevoegingen van 5096 en 1594 verwijderingen
  1. 3 0
      .kiro/steering/language.md
  2. 10 0
      components.d.ts
  3. 308 292
      package-lock.json
  4. 98 2
      src/api/setting.ts
  5. 13 1
      src/api/types/setting.ts
  6. 2 1
      src/assets/main.css
  7. 208 0
      src/components/DefenseTimeSchedule/index.vue
  8. 0 11
      src/views/settings/alarmSettings/components/areaDetection/index.vue
  9. 108 4
      src/views/settings/alarmSettings/components/humanDetection/index.vue
  10. 157 4
      src/views/settings/alarmSettings/components/intrusionDetection/index.vue
  11. 133 0
      src/views/settings/alarmSettings/components/lineCrossingDetection/index.vue
  12. 145 4
      src/views/settings/alarmSettings/components/manualAlarm/index.vue
  13. 108 4
      src/views/settings/alarmSettings/components/motionDetection/index.vue
  14. 20 9
      src/views/settings/alarmSettings/index.vue
  15. 198 4
      src/views/settings/audioVideo/components/audio/index.vue
  16. 346 4
      src/views/settings/audioVideo/components/nightVisionIlluminator/index.vue
  17. 369 5
      src/views/settings/audioVideo/components/video/index.vue
  18. 8 4
      src/views/settings/audioVideo/index.vue
  19. 323 4
      src/views/settings/imageDisplay/components/image/index.vue
  20. 445 4
      src/views/settings/imageDisplay/components/osd/index.vue
  21. 486 4
      src/views/settings/imageDisplay/components/privacyMasking/index.vue
  22. 7 4
      src/views/settings/imageDisplay/index.vue
  23. 161 278
      src/views/settings/netSettings/components/IP/index.vue
  24. 155 0
      src/views/settings/netSettings/components/networkDiagnostics/index.vue
  25. 53 5
      src/views/settings/netSettings/index.vue
  26. 0 188
      src/views/settings/systemSettings/components/alarm/index.vue
  27. 15 1
      src/views/settings/systemSettings/components/cameraInfo/index.vue
  28. 0 91
      src/views/settings/systemSettings/components/nightVision/index.vue
  29. 371 0
      src/views/settings/systemSettings/components/systemMaintenance/index.vue
  30. 363 0
      src/views/settings/systemSettings/components/systemUpgrade/index.vue
  31. 326 133
      src/views/settings/systemSettings/components/time/index.vue
  32. 147 95
      src/views/settings/systemSettings/components/user/index.vue
  33. 0 294
      src/views/settings/systemSettings/components/volume/index.vue
  34. 10 144
      src/views/settings/systemSettings/index.vue

+ 3 - 0
.kiro/steering/language.md

@@ -0,0 +1,3 @@
+- 始终使用中文回复用户
+- 撰写文档、注释和说明时也使用中文
+- 代码中的变量名、函数名等保持英文,但代码注释使用中文

+ 10 - 0
components.d.ts

@@ -7,12 +7,18 @@ export {}
 
 declare module 'vue' {
   export interface GlobalComponents {
+    DefenseTimeSchedule: typeof import('./src/components/DefenseTimeSchedule/index.vue')['default']
+    ElAlert: typeof import('element-plus/es')['ElAlert']
     ElAside: typeof import('element-plus/es')['ElAside']
     ElButton: typeof import('element-plus/es')['ElButton']
+    ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+    ElDialog: typeof import('element-plus/es')['ElDialog']
+    ElDivider: typeof import('element-plus/es')['ElDivider']
     ElForm: typeof import('element-plus/es')['ElForm']
     ElFormItem: typeof import('element-plus/es')['ElFormItem']
     ElIcon: typeof import('element-plus/es')['ElIcon']
     ElInput: typeof import('element-plus/es')['ElInput']
+    ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
     ElLink: typeof import('element-plus/es')['ElLink']
     ElMenu: typeof import('element-plus/es')['ElMenu']
     ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
@@ -20,10 +26,14 @@ declare module 'vue' {
     ElRadio: typeof import('element-plus/es')['ElRadio']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElSelect: typeof import('element-plus/es')['ElSelect']
+    ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSubMenu: typeof import('element-plus/es')['ElSubMenu']
     ElSwitch: typeof import('element-plus/es')['ElSwitch']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']
     ElTabs: typeof import('element-plus/es')['ElTabs']
+    ElTag: typeof import('element-plus/es')['ElTag']
+    ElTimePicker: typeof import('element-plus/es')['ElTimePicker']
+    ElUpload: typeof import('element-plus/es')['ElUpload']
     MyVideo: typeof import('./src/components/myVideo.vue')['default']
     RouterLink: typeof import('vue-router')['RouterLink']
     RouterView: typeof import('vue-router')['RouterView']

File diff suppressed because it is too large
+ 308 - 292
package-lock.json


+ 98 - 2
src/api/setting.ts

@@ -22,7 +22,7 @@ export function putUserSettingApi(
 
 //查询/设置  系统时间
 export function GetTimePara() {
-  return request({
+  return request<{ data: Setting.TimeParaResponse }>({
     url: `API/V1.0/System/TimePara`,
     method: 'get'
   })
@@ -30,7 +30,7 @@ export function GetTimePara() {
 
 
 export function PutTimePara(data: TimeParaData) {
-  return request({
+  return request<{ data: string }>({
     url: `API/V1.0/System/TimePara`,
     method: 'put',
     data
@@ -46,6 +46,13 @@ export function cameraReset(data: any) {
   })
 }
 
+export function cameraResetGet() {
+  return request({
+    url: `/API/V1.0/Device/ResetReboot`,
+    method: 'get',
+  })
+}
+
 export function getCameraVolume() {
   return request({
     url: `/API/V1.0/Audio/AudioPara`,
@@ -120,3 +127,92 @@ export function cameraResetPassword(data: any) {
   })
 }
 
+// 图像参数
+export function getImagePara() {
+  return request<any>({
+    url: `/API/V1.0/Video/ImagePara`,
+    method: 'get'
+  })
+}
+
+export function putImagePara(data: any) {
+  return request<any>({
+    url: `/API/V1.0/Video/ImagePara`,
+    method: 'put',
+    data
+  })
+}
+
+// 隐私遮挡
+export function getPrivacyMask() {
+  return request<any>({
+    url: `/API/V1.0/Video/PrivacyMask`,
+    method: 'get'
+  })
+}
+
+export function putPrivacyMask(data: any) {
+  return request<any>({
+    url: `/API/V1.0/Video/PrivacyMask`,
+    method: 'put',
+    data
+  })
+}
+
+// OSD设置
+export function getOsdPara() {
+  return request<any>({
+    url: `/API/V1.0/Video/OsdPara`,
+    method: 'get'
+  })
+}
+
+export function putOsdPara(data: any) {
+  return request<any>({
+    url: `/API/V1.0/Video/OsdPara`,
+    method: 'put',
+    data
+  })
+}
+
+// 视频编码参数
+export function getVideoEncodePara() {
+  return request<any>({
+    url: `/API/V1.0/Video/EncodePara`,
+    method: 'get'
+  })
+}
+
+export function putVideoEncodePara(data: any) {
+  return request<any>({
+    url: `/API/V1.0/Video/EncodePara`,
+    method: 'put',
+    data
+  })
+}
+
+// 夜视补光参数
+export function getNightVisionIlluminator() {
+  return request<any>({
+    url: `/API/V1.0/Video/NightVisionPara`,
+    method: 'get'
+  })
+}
+
+export function putNightVisionIlluminator(data: any) {
+  return request<any>({
+    url: `/API/V1.0/Video/NightVisionPara`,
+    method: 'put',
+    data
+  })
+}
+
+// 网络诊断(Ping)
+export function networkDiagnostics(data: { DomainName: string }) {
+  return request<any>({
+    url: `/API/V1.0/Network/NetworkDiag`,
+    method: 'put',
+    data
+  })
+}
+

+ 13 - 1
src/api/types/setting.ts

@@ -9,7 +9,19 @@ export interface UpdateSettingRequestData {
   deviceMac?: string
 }
 
+export interface TimeParaResponse {
+  timeMode?: number
+  timeZoneEn?: boolean
+  timeNTPEn?: boolean
+  timeNTP?: string
+  timeNTP2?: string
+}
+
 export interface TimeParaData {
-  time: string
+  time?: string
   timeMode?: number
+  timeZoneEn?: boolean
+  timeNTPEn?: boolean
+  timeNTP?: string
+  timeNTP2?: string
 }

+ 2 - 1
src/assets/main.css

@@ -16,4 +16,5 @@ html {
 
 #app {
   height: 100%;
-}
+}
+

+ 208 - 0
src/components/DefenseTimeSchedule/index.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="defense-time-schedule">
+    <div class="schedule-header">
+      <span class="schedule-title">{{ title }}</span>
+      <div class="schedule-actions">
+        <el-button link type="primary" @click="clearAll">Clear</el-button>
+        <el-button link type="primary" @click="invertAll">Invert</el-button>
+        <el-button link type="primary" @click="selectAll">Select All</el-button>
+      </div>
+    </div>
+    <div class="schedule-body">
+      <div v-for="(day, dayIndex) in weekDayLabels" :key="dayIndex" class="schedule-row">
+        <span class="day-label">{{ day }}</span>
+        <div class="hour-cells">
+          <span
+            v-for="hour in 24" :key="hour"
+            class="hour-cell"
+            :class="{ active: grid[dayIndex][hour - 1] }"
+            @mousedown="onMouseDown(dayIndex, hour - 1)"
+            @mouseenter="onMouseEnter(dayIndex, hour - 1)"
+            @mouseup="onMouseUp"
+          >{{ String(hour).padStart(2, '0') }}</span>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+
+/** 每天用一个十进制数表示24位的位掩码 */
+export interface DailyArmMasks {
+  monday: number
+  tuesday: number
+  wednesday: number
+  thursday: number
+  friday: number
+  saturday: number
+  sunday: number
+}
+
+const DAY_KEYS: (keyof DailyArmMasks)[] = [
+  'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'
+]
+const weekDayLabels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
+
+const ALL_HOURS = (1 << 24) - 1 // 24位全1
+
+const props = withDefaults(defineProps<{
+  title?: string
+  modelValue?: DailyArmMasks
+}>(), {
+  title: 'Arm Schedule',
+})
+
+const emit = defineEmits<{
+  (e: 'update:modelValue', value: DailyArmMasks): void
+}>()
+
+// 位掩码 → 24位数组
+function maskToArray(mask: number): number[] {
+  const arr = new Array(24).fill(0)
+  for (let i = 0; i < 24; i++) {
+    arr[i] = (mask >> i) & 1
+  }
+  return arr
+}
+
+// 24位数组 → 位掩码
+function arrayToMask(arr: number[]): number {
+  let mask = 0
+  for (let i = 0; i < 24; i++) {
+    if (arr[i]) mask |= (1 << i)
+  }
+  return mask
+}
+
+// 默认值
+function defaultMasks(): DailyArmMasks {
+  return { monday: 0, tuesday: 0, wednesday: 0, thursday: 0, friday: 0, saturday: 0, sunday: 0 }
+}
+
+function masksToGrid(masks: DailyArmMasks): number[][] {
+  return DAY_KEYS.map(key => maskToArray(masks[key]))
+}
+
+function gridToMasks(g: number[][]): DailyArmMasks {
+  const result = {} as DailyArmMasks
+  DAY_KEYS.forEach((key, i) => { result[key] = arrayToMask(g[i]) })
+  return result
+}
+
+// 内部用二维数组操作
+const grid = ref<number[][]>(masksToGrid(props.modelValue ?? defaultMasks()))
+
+const isDragging = ref(false)
+const dragValue = ref(1)
+
+watch(() => props.modelValue, (val) => {
+  grid.value = masksToGrid(val ?? defaultMasks())
+}, { deep: true })
+
+function emitChange() {
+  emit('update:modelValue', gridToMasks(grid.value))
+}
+
+function onMouseDown(dayIndex: number, hourIndex: number) {
+  isDragging.value = true
+  dragValue.value = grid.value[dayIndex][hourIndex] ? 0 : 1
+  grid.value[dayIndex][hourIndex] = dragValue.value
+}
+
+function onMouseEnter(dayIndex: number, hourIndex: number) {
+  if (!isDragging.value) return
+  grid.value[dayIndex][hourIndex] = dragValue.value
+}
+
+function onMouseUp() {
+  if (isDragging.value) {
+    isDragging.value = false
+    emitChange()
+  }
+}
+
+function clearAll() {
+  grid.value = Array.from({ length: 7 }, () => new Array(24).fill(0))
+  emitChange()
+}
+
+function selectAll() {
+  grid.value = Array.from({ length: 7 }, () => new Array(24).fill(1))
+  emitChange()
+}
+
+function invertAll() {
+  grid.value = grid.value.map(row => row.map(v => (v ? 0 : 1)))
+  emitChange()
+}
+
+defineExpose({ clearAll, selectAll, invertAll })
+</script>
+
+<style scoped lang="scss">
+.defense-time-schedule {
+  user-select: none;
+
+  .schedule-header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+    max-width: calc(75px + 24 * 29px);
+
+    .schedule-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+    }
+
+    .schedule-actions {
+      display: flex;
+      gap: 8px;
+      margin-left: auto;
+    }
+  }
+
+  .schedule-body {
+    .schedule-row {
+      display: flex;
+      align-items: center;
+
+      .day-label {
+        width: 50px;
+        font-size: 13px;
+        color: #666;
+        flex-shrink: 0;
+        text-align: center;
+      }
+
+      .hour-cells {
+        display: flex;
+
+        .hour-cell {
+          display: inline-flex;
+          align-items: center;
+          justify-content: center;
+          width: 28px;
+          height: 28px;
+          font-size: 12px;
+          color: #666;
+          background: #fff;
+          border: 1px solid #e0e0e0;
+          cursor: pointer;
+          transition: background-color 0.15s, color 0.15s;
+
+          &:hover { border-color: #f56c6c; }
+
+          &.active {
+            background-color: #f56c6c;
+            color: #fff;
+            border-color: #f56c6c;
+          }
+        }
+      }
+    }
+  }
+}
+</style>

+ 0 - 11
src/views/settings/alarmSettings/components/areaDetection/index.vue

@@ -1,11 +0,0 @@
-<script setup lang="ts">
-
-</script>
-
-<template>
-
-</template>
-
-<style scoped lang="scss">
-
-</style>

+ 108 - 4
src/views/settings/alarmSettings/components/humanDetection/index.vue

@@ -1,11 +1,115 @@
-<script setup lang="ts">
+<template>
+  <div class="motion-detection">
+    <el-checkbox v-model="form.Enable">Enable human detection</el-checkbox>
+    <el-divider class="short-divider"/>
 
-</script>
+    <div class="section">
+      <div class="section-title">Parameter Settings</div>
+      <div class="sensitivity-row">
+        <span class="label">Sensitivity:</span>
+        <el-slider v-model="form.Sensitivity" :min="0" :max="100" :step="1" class="sensitivity-slider"/>
+        <span class="sensitivity-text">{{ sensitivityLabel }}</span>
+      </div>
+    </div>
+    <el-divider class="short-divider"/>
 
-<template>
+    <div class="section">
+      <DefenseTimeSchedule v-model="form.schedule" title="Arm Schedule"/>
+    </div>
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <div class="section-title">Alarm Linkage</div>
+      <el-checkbox v-model="form.AudioAlarm">Sound Alarm</el-checkbox>
+    </div>
 
+    <el-button type="primary" round @click="handleSave">Save</el-button>
+  </div>
 </template>
 
+<script setup lang="ts">
+import {reactive, computed} from 'vue'
+import {ElMessage} from 'element-plus'
+import DefenseTimeSchedule from '@/components/DefenseTimeSchedule/index.vue'
+import type {DailyArmMasks} from '@/components/DefenseTimeSchedule/index.vue'
+
+const form = reactive<{
+  Enable: boolean
+  Sensitivity: number
+  schedule: DailyArmMasks
+  AudioAlarm: boolean
+}>({
+  Enable: false,
+  Sensitivity: 50,
+  schedule: {
+    monday: 0, tuesday: 0, wednesday: 0, thursday: 0,
+    friday: 0, saturday: 0, sunday: 0
+  },
+  AudioAlarm: false
+})
+
+// 灵敏度文字映射
+const sensitivityLabel = computed(() => {
+  const val = form.Sensitivity
+  if (val <= 33) return 'Low'
+  if (val <= 66) return 'Medium'
+  return 'High'
+})
+
+function handleSave() {
+  // TODO: 调用接口保存配置
+  const payload = {
+    Enable: form.Enable,
+    Sensitivity: form.Sensitivity,
+    DailyArmMasks: {...form.schedule},
+    AudioAlarm: form.AudioAlarm
+  }
+  console.log('Save motion detection config:', payload)
+  ElMessage.success('Saved successfully')
+}
+</script>
+
 <style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+  }
+
+  .sensitivity-row {
+    display: flex;
+    align-items: center;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+
+    .sensitivity-slider {
+      width: 240px;
+      margin: 0 8px;
+    }
 
-</style>
+    .sensitivity-text {
+      font-size: 14px;
+      margin-left: 10px;
+      color: #666;
+      flex-shrink: 0;
+    }
+  }
+}
+</style>

+ 157 - 4
src/views/settings/alarmSettings/components/intrusionDetection/index.vue

@@ -1,11 +1,164 @@
-<script setup lang="ts">
+<template>
+  <div class="motion-detection">
+    <el-checkbox v-model="form.Enable">Enable Intrusion Detection</el-checkbox>
+    <el-divider class="short-divider"/>
 
-</script>
+    <div class="section">
+      <div class="section-title">Parameter Settings</div>
+      <div class="section-content">
+        <div class="sensitivity-row">
+          <span class="label">Sensitivity:</span>
+          <el-slider v-model="form.Sensitivity" :min="0" :max="100" :step="1" class="sensitivity-slider"/>
+          <span class="sensitivity-text">{{ sensitivityLabel }}</span>
+        </div>
+        <el-checkbox v-model="form.LightFlashEn">Flash Warning</el-checkbox>
+      </div>
+      <div class="trigger-row">
+        <span class="label">Trigger Mode:</span>
+        <el-select v-model="form.TriggerMode" placeholder="Select" style="width: 160px">
+          <el-option
+              v-for="item in TriggerOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+          />
+        </el-select>
+      </div>
+    </div>
+    <el-divider class="short-divider"/>
 
-<template>
+    <div class="section">
+      <DefenseTimeSchedule v-model="form.schedule" title="Arm Schedule"/>
+    </div>
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <div class="section-title">Alarm Linkage</div>
+      <el-checkbox v-model="form.AudioAlarmEnter">Sound Alarm on Enter</el-checkbox>
+      <el-checkbox v-model="form.AudioAlarmLeave">Sound Alarm on Leave</el-checkbox>
+    </div>
 
+    <el-button type="primary" round @click="handleSave">Save</el-button>
+  </div>
 </template>
 
+<script setup lang="ts">
+import {reactive, computed} from 'vue'
+import {ElMessage} from 'element-plus'
+import DefenseTimeSchedule from '@/components/DefenseTimeSchedule/index.vue'
+import type {DailyArmMasks} from '@/components/DefenseTimeSchedule/index.vue'
+
+const form = reactive<{
+  Enable: boolean
+  Sensitivity: number
+  TriggerMode: number
+  LightFlashEn: boolean
+  schedule: DailyArmMasks
+  AudioAlarmEnter: boolean
+  AudioAlarmLeave: boolean
+}>({
+  Enable: false,
+  Sensitivity: 50,
+  TriggerMode: 0,
+  LightFlashEn: false,
+  schedule: {
+    monday: 0, tuesday: 0, wednesday: 0, thursday: 0,
+    friday: 0, saturday: 0, sunday: 0
+  },
+  AudioAlarmEnter: false,
+  AudioAlarmLeave: false
+})
+
+const TriggerOptions = [
+  {"label": "Trigger on Enter", "value": 0},
+  {"label": "Trigger on Leave", "value": 1},
+  {"label": "Trigger on Both", "value": 2},
+]
+
+// 灵敏度文字映射
+const sensitivityLabel = computed(() => {
+  const val = form.Sensitivity
+  if (val <= 33) return 'Low'
+  if (val <= 66) return 'Medium'
+  return 'High'
+})
+
+function handleSave() {
+  // TODO: 调用接口保存配置
+  const payload = {
+    Enable: form.Enable,
+    Sensitivity: form.Sensitivity,
+    DailyArmMasks: {...form.schedule},
+    AudioAlarmEnter: form.AudioAlarmEnter,
+    AudioAlarmLeave: form.AudioAlarmLeave,
+    LightFlashEn: form.LightFlashEn,
+    TriggerMode: form.TriggerMode,
+  }
+  console.log('Save motion detection config:', payload)
+  ElMessage.success('Saved successfully')
+}
+</script>
+
 <style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+
+    .section-content {
+      display: flex;
+      gap: 100px;
+    }
+  }
+
+  .sensitivity-row {
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    width: 400px;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+
+    .sensitivity-slider {
+      width: 240px;
+      margin: 0 8px;
+    }
+
+    .sensitivity-text {
+      font-size: 14px;
+      margin-left: 10px;
+      color: #666;
+      flex-shrink: 0;
+    }
+  }
+
+  .trigger-row {
+    display: flex;
+    align-items: center;
+    margin-top: 16px;
 
-</style>
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+  }
+}
+</style>

+ 133 - 0
src/views/settings/alarmSettings/components/lineCrossingDetection/index.vue

@@ -0,0 +1,133 @@
+<template>
+  <div class="motion-detection">
+    <el-checkbox v-model="form.Enable">Enable Line Crossing Detection</el-checkbox>
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <div class="section-title">Parameter Settings</div>
+      <div class="section-content">
+        <div class="sensitivity-row">
+          <span class="label">Sensitivity:</span>
+          <el-slider v-model="form.Sensitivity" :min="0" :max="100" :step="1" class="sensitivity-slider"/>
+          <span class="sensitivity-text">{{ sensitivityLabel }}</span>
+        </div>
+        <el-checkbox v-model="form.LightFlashEn">Flash Warning</el-checkbox>
+      </div>
+    </div>
+
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <DefenseTimeSchedule v-model="form.schedule" title="Arm Schedule"/>
+    </div>
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <div class="section-title">Alarm Linkage</div>
+      <el-checkbox v-model="form.AudioAlarmAB">Sound Alarm on A→B</el-checkbox>
+      <el-checkbox v-model="form.AudioAlarmBA">Sound Alarm on B→A</el-checkbox>
+    </div>
+
+    <el-button type="primary" round @click="handleSave">Save</el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {reactive, computed} from 'vue'
+import {ElMessage} from 'element-plus'
+import DefenseTimeSchedule from '@/components/DefenseTimeSchedule/index.vue'
+import type {DailyArmMasks} from '@/components/DefenseTimeSchedule/index.vue'
+
+const form = reactive<{
+  Enable: boolean
+  Sensitivity: number
+  LightFlashEn: boolean
+  schedule: DailyArmMasks
+  AudioAlarmAB: boolean
+  AudioAlarmBA: boolean
+}>({
+  Enable: false,
+  Sensitivity: 50,
+  LightFlashEn: false,
+  schedule: {
+    monday: 0, tuesday: 0, wednesday: 0, thursday: 0,
+    friday: 0, saturday: 0, sunday: 0
+  },
+  AudioAlarmAB: false,
+  AudioAlarmBA: false,
+})
+
+// 灵敏度文字映射
+const sensitivityLabel = computed(() => {
+  const val = form.Sensitivity
+  if (val <= 33) return 'Low'
+  if (val <= 66) return 'Medium'
+  return 'High'
+})
+
+function handleSave() {
+  // TODO: 调用接口保存配置
+  const payload = {
+    Enable: form.Enable,
+    Sensitivity: form.Sensitivity,
+    DailyArmMasks: {...form.schedule},
+    AudioAlarmAB: form.AudioAlarmAB,
+    AudioAlarmBA: form.AudioAlarmBA,
+    LightFlashEn: form.LightFlashEn,
+  }
+  console.log('Save motion detection config:', payload)
+  ElMessage.success('Saved successfully')
+}
+</script>
+
+<style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+
+    .section-content {
+      display: flex;
+      gap: 100px;
+    }
+  }
+
+  .sensitivity-row {
+    display: flex;
+    align-items: center;
+    flex-shrink: 0;
+    width: 400px;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+
+    .sensitivity-slider {
+      width: 240px;
+      margin: 0 8px;
+    }
+
+    .sensitivity-text {
+      font-size: 14px;
+      margin-left: 10px;
+      color: #666;
+      flex-shrink: 0;
+    }
+  }
+}
+</style>

+ 145 - 4
src/views/settings/alarmSettings/components/manualAlarm/index.vue

@@ -1,11 +1,152 @@
-<script setup lang="ts">
+<template>
+  <div class="motion-detection">
+    <div class="section">
+      <div class="section-title">Manual Alarm Control</div>
+      <div class="section-content">
+        <div class="alarm-status-row">
+          <span class="label">Current Status:</span>
+          <el-tag :type="alarmActive ? 'danger' : 'info'" size="large">
+            {{ alarmActive ? 'Alarm Active' : 'Alarm Inactive' }}
+          </el-tag>
+        </div>
+      </div>
+    </div>
 
-</script>
+    <el-divider class="short-divider"/>
 
-<template>
+    <div class="section">
+      <div class="section-title">Alarm Actions</div>
+      <div class="alarm-actions">
+        <el-button
+            type="primary"
+            round
+            @click="handleAlarm"
+            :loading="loading"
+        >
+          Enable Manual Alarm
+        </el-button>
+        <el-button
+            type="info"
+            round
+            @click="handleCloseAlarm"
+            :loading="disableLoading"
+        >
+          Disable Manual Alarm
+        </el-button>
+      </div>
+    </div>
+
+    <el-divider class="short-divider"/>
 
+    <div class="section">
+      <div class="section-title">Notes</div>
+      <div class="alarm-tip">
+        <el-icon class="tip-icon"><InfoFilled/></el-icon>
+        <span>Alarm will remain active for 1 minute after activation.</span>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import {ref} from 'vue'
+import {ElMessage} from 'element-plus'
+import {InfoFilled} from '@element-plus/icons-vue'
+import {cameraAlarm} from '@/api/setting'
+
+const loading = ref(false)
+const disableLoading = ref(false)
+const alarmActive = ref(false)
+
+async function handleCloseAlarm() {
+  if (disableLoading.value) return
+  disableLoading.value = true
+  try {
+    const res = await cameraAlarm({AlarmSettings: 0}) as { data: string }
+    if (res.data === 'ok\n') {
+      alarmActive.value = false
+      ElMessage.success('Manual Alarm Deactivated Successfully')
+    } else {
+      ElMessage.warning('Manual Alarm Deactivation Error')
+    }
+  } catch {
+    ElMessage.error('Manual Alarm Deactivation Error')
+  } finally {
+    disableLoading.value = false
+  }
+}
+
+async function handleAlarm() {
+  if (loading.value) return
+  loading.value = true
+  try {
+    const res = await cameraAlarm({AlarmSettings: 1}) as { data: string }
+    if (res.data === 'ok\n') {
+      alarmActive.value = true
+      ElMessage.success('Manual alarm activated successfully. It will remain active for 1 minute.')
+    } else {
+      ElMessage.warning('Manual Alarm Activation Error')
+    }
+  } catch {
+    ElMessage.error('Failed to Activate Manual Alarm.')
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
 <style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+
+    .section-content {
+      display: flex;
+      gap: 100px;
+    }
+  }
+
+  .alarm-status-row {
+    display: flex;
+    align-items: center;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+  }
+
+  .alarm-actions {
+    display: flex;
+    gap: 16px;
+  }
+
+  .alarm-tip {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
+    color: #909399;
 
-</style>
+    .tip-icon {
+      font-size: 15px;
+      color: #909399;
+    }
+  }
+}
+</style>

+ 108 - 4
src/views/settings/alarmSettings/components/motionDetection/index.vue

@@ -1,11 +1,115 @@
-<script setup lang="ts">
+<template>
+  <div class="motion-detection">
+    <el-checkbox v-model="form.Enable">Enable Motion Detection</el-checkbox>
+    <el-divider class="short-divider"/>
 
-</script>
+    <div class="section">
+      <div class="section-title">Parameter Settings</div>
+      <div class="sensitivity-row">
+        <span class="label">Sensitivity:</span>
+        <el-slider v-model="form.Sensitivity" :min="0" :max="100" :step="1" class="sensitivity-slider"/>
+        <span class="sensitivity-text">{{ sensitivityLabel }}</span>
+      </div>
+    </div>
+    <el-divider class="short-divider"/>
 
-<template>
+    <div class="section">
+      <DefenseTimeSchedule v-model="form.schedule" title="Arm Schedule"/>
+    </div>
+    <el-divider class="short-divider"/>
+
+    <div class="section">
+      <div class="section-title">Alarm Linkage</div>
+      <el-checkbox v-model="form.AudioAlarm">Sound Alarm</el-checkbox>
+    </div>
 
+    <el-button type="primary" round @click="handleSave">Save</el-button>
+  </div>
 </template>
 
+<script setup lang="ts">
+import {reactive, computed} from 'vue'
+import {ElMessage} from 'element-plus'
+import DefenseTimeSchedule from '@/components/DefenseTimeSchedule/index.vue'
+import type {DailyArmMasks} from '@/components/DefenseTimeSchedule/index.vue'
+
+const form = reactive<{
+  Enable: boolean
+  Sensitivity: number
+  schedule: DailyArmMasks
+  AudioAlarm: boolean
+}>({
+  Enable: false,
+  Sensitivity: 50,
+  schedule: {
+    monday: 0, tuesday: 0, wednesday: 0, thursday: 0,
+    friday: 0, saturday: 0, sunday: 0
+  },
+  AudioAlarm: false
+})
+
+// 灵敏度文字映射
+const sensitivityLabel = computed(() => {
+  const val = form.Sensitivity
+  if (val <= 33) return 'Low'
+  if (val <= 66) return 'Medium'
+  return 'High'
+})
+
+function handleSave() {
+  // TODO: 调用接口保存配置
+  const payload = {
+    Enable: form.Enable,
+    Sensitivity: form.Sensitivity,
+    DailyArmMasks: {...form.schedule},
+    AudioAlarm: form.AudioAlarm
+  }
+  console.log('Save motion detection config:', payload)
+  ElMessage.success('Saved successfully')
+}
+</script>
+
 <style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+  }
+
+  .sensitivity-row {
+    display: flex;
+    align-items: center;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+
+    .sensitivity-slider {
+      width: 240px;
+      margin: 0 8px;
+    }
 
-</style>
+    .sensitivity-text {
+      font-size: 14px;
+      margin-left: 10px;
+      color: #666;
+      flex-shrink: 0;
+    }
+  }
+}
+</style>

+ 20 - 9
src/views/settings/alarmSettings/index.vue

@@ -2,30 +2,41 @@
   <div class="settings-container">
     <el-tabs v-model="activeName" class="demo-tabs">
       <el-tab-pane label="Motion Detection" name="first">
-
+        <MotionDetection/>
       </el-tab-pane>
-      <el-tab-pane label="Human Detection" name="second">
 
+      <el-tab-pane label="Human Detection" name="second">
+        <HumanDetection/>
       </el-tab-pane>
-      <el-tab-pane label="Detection Zones" name="third">
 
+      <el-tab-pane label="Intrusion Detection" name="third">
+        <IntrusionDetection/>
       </el-tab-pane>
 
-      <el-tab-pane label="Intrusion Detection" name="fourth">
-
+      <el-tab-pane label="Line Crossing Detection" name="fourth">
+        <LineCrossingDetection/>
       </el-tab-pane>
-      <el-tab-pane label="Video Tampering" name="fifth">
 
-      </el-tab-pane>
-      <el-tab-pane label="Manual Alarm" name="sixth">
+      <!-- <el-tab-pane label="Video Tampering" name="fifth">
+          <VideoTampering />
+      </el-tab-pane> -->
 
+      <el-tab-pane label="Manual Alarm" name="sixth">
+        <ManualAlarm/>
       </el-tab-pane>
+
     </el-tabs>
   </div>
 </template>
 
 <script setup lang="ts">
 import {ref} from "vue";
+import MotionDetection from './components/motionDetection/index.vue'
+import HumanDetection from './components/humanDetection/index.vue'
+import IntrusionDetection from './components/intrusionDetection/index.vue'
+import LineCrossingDetection from './components/lineCrossingDetection/index.vue'
+// import VideoTampering from './components/videoTampering/index.vue'
+import ManualAlarm from './components/manualAlarm/index.vue'
 
 const activeName = ref('first')
 
@@ -33,7 +44,7 @@ const activeName = ref('first')
 
 
 <style scoped lang="scss">
-.settings-container{
+.settings-container {
   display: flex;
   flex-direction: column;
   padding: 20px;

+ 198 - 4
src/views/settings/audioVideo/components/audio/index.vue

@@ -1,11 +1,205 @@
-<script setup lang="ts">
+<template>
+  <div class="audio-settings">
+    <!-- 启用音频输入 -->
+    <div class="form-item">
+      <el-checkbox v-model="form.EnableMic">Enable Audio Input</el-checkbox>
+    </div>
 
-</script>
+    <!-- 启用时才显示音频输入相关设置 -->
+    <template v-if="form.EnableMic">
+      <div class="form-item">
+        <span class="form-item__label">Audio Input:</span>
+        <div class="form-item__control">
+          <el-select v-model="form.audioInputType">
+            <el-option v-for="item in inputTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
 
-<template>
+      <div class="form-item">
+        <span class="form-item__label">Audio Encoding:</span>
+        <div class="form-item__control">
+          <el-select v-model="form.audioEncodeType">
+            <el-option v-for="item in encodeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
 
+      <div class="form-item">
+        <span class="form-item__label">Input Volume:</span>
+        <div class="form-item__control slider-control">
+          <el-slider
+            v-model="form.MicVolume"
+            :min="0"
+            :max="100"
+            :show-tooltip="false"
+          />
+          <span class="slider-value">{{ form.MicVolume }}</span>
+        </div>
+      </div>
+
+    <div class="form-item">
+      <span class="form-item__label">Output Volume:</span>
+      <div class="form-item__control slider-control">
+        <el-slider
+          v-model="form.speakerVolume"
+          :min="0"
+          :max="100"
+          :show-tooltip="false"
+        />
+        <span class="slider-value">{{ form.speakerVolume }}</span>
+      </div>
+    </div>
+    </template>
+
+    <!-- 底部按钮 -->
+    <div class="btn-wrapper">
+      <el-button round class="btn-reset" @click="resetDefaults">Restore Defaults</el-button>
+      <el-button round type="primary" class="btn-save" @click="saveParams">Save</el-button>
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getCameraVolume, cameraVolume } from '@/api/setting'
+
+const form = reactive({
+  EnableMic: true,
+  audioInputType: 0,
+  audioEncodeType: 0,
+  MicVolume: 80,
+  speakerVolume: 80
+})
+
+// 音频输入方式选项
+const inputTypeOptions = [
+  { value: 0, label: 'MIC' },
+]
+
+// 音频编码选项
+const encodeTypeOptions = [
+  { value: 0, label: 'G711A' },
+  // { value: 1, label: 'G711U' },
+  // { value: 2, label: 'AAC' }
+]
+
+// 恢复默认设置
+async function resetDefaults() {
+  form.EnableMic = true
+  // form.audioInputType = 0
+  // form.audioEncodeType = 0
+  form.MicVolume = 80
+  form.speakerVolume = 80
+  await saveParams()
+}
+
+// 获取音频参数
+async function fetchParams() {
+  try {
+    const res = (await getCameraVolume()) as any
+    if (res.data) {
+      const d = res.data
+      if (d.EnableMic !== undefined) form.EnableMic = d.EnableMic
+      // if (d.audioInputType !== undefined) form.audioInputType = d.audioInputType
+      // if (d.audioEncodeType !== undefined) form.audioEncodeType = d.audioEncodeType
+      if (d.MicVolume !== undefined) form.MicVolume = d.MicVolume
+      if (d.speakerVolume !== undefined) form.speakerVolume = d.speakerVolume
+    }
+  } catch {
+    console.error('获取音频参数失败')
+  }
+}
+
+// 保存参数
+async function saveParams() {
+  try {
+    const data = {
+      EnableMic: form.EnableMic,
+      // audioInputType: form.audioInputType,
+      // audioEncodeType: form.audioEncodeType,
+      MicVolume: form.MicVolume,
+      speakerVolume: form.speakerVolume
+    }
+    const res = (await cameraVolume(data)) as any
+    if (res.data === 'ok\n') {
+      ElMessage.success('Saved successfully')
+    }
+  } catch {
+    ElMessage.warning('Save failed')
+  }
+}
+
+onMounted(() => {
+  fetchParams()
+})
+</script>
+
 <style scoped lang="scss">
+.audio-settings {
+  padding: 10px 20px;
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+
+  &__label {
+    flex: 0 0 120px;
+    text-align: right;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 10px;
+  }
+
+  &__control {
+    display: flex;
+    align-items: center;
+
+    .el-select {
+      width: 220px;
+    }
+  }
+}
+
+.slider-control {
+  flex: 1;
+  max-width: 320px;
+  gap: 12px;
+
+  .el-slider {
+    flex: 1;
+  }
+
+  .slider-value {
+    font-size: 13px;
+    color: #606266;
+    min-width: 20px;
+    text-align: center;
+    margin-left: 10px;
+  }
+}
+
+.btn-wrapper {
+  display: flex;
+  gap: 160px;
+  // justify-content: space-between;
+  padding-top: 20px;
+
+  .btn-reset {
+    border-color: #409EFF;
+    color: #409EFF;
+
+    &:hover {
+      background-color: #409EFF;
+      color: #fff;
+    }
+  }
 
-</style>
+  .btn-save {
+    min-width: 100px;
+  }
+}
+</style>

+ 346 - 4
src/views/settings/audioVideo/components/nightVisionIlluminator/index.vue

@@ -1,11 +1,353 @@
-<script setup lang="ts">
+<template>
+  <div class="night-vision-illuminator">
+    <!-- Fill Light -->
+    <div class="section">
+      <div class="section-title">Fill Light</div>
+      <div class="section-body">
+        <!-- 补光方式 -->
+        <div class="form-item">
+          <span class="form-item__label">Light Mode:</span>
+          <div class="form-item__control">
+            <el-select v-model="form.NightMode" style="width: 220px">
+              <el-option
+                v-for="item in fillLightOptions"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </div>
+        </div>
 
-</script>
+        <!-- 黑白夜视(1)时隐藏以下所有配置 -->
+        <template v-if="form.NightMode !== 1">
+          <!-- 自动调节亮度 -->
+          <div class="form-item">
+            <el-checkbox v-model="form.AutoLightEn">Auto Brightness</el-checkbox>
+          </div>
 
-<template>
+          <!-- 灯光亮度(未勾选自动调节亮度时显示) -->
+          <div v-if="!form.AutoLightEn" class="form-item">
+            <span class="form-item__label">Brightness:</span>
+            <div class="form-item__control slider-control">
+              <!-- <span class="slider-min">1</span> -->
+              <el-slider
+                v-model="form.PwmLight"
+                :min="1"
+                :max="100"
+                :show-tooltip="false"
+              />
+              <span class="slider-value">{{ form.PwmLight }}</span>
+              <!-- <span class="slider-max">100</span> -->
+            </div>
+          </div>
+
+          <!-- 全彩夜视(0)时额外显示灵敏度 -->
+          <template v-if="form.NightMode === 0">
+            <!-- 开灯灵敏度 -->
+            <div class="form-item">
+              <span class="form-item__label">Light On Sensitivity:</span>
+              <div class="form-item__control slider-control">
+                <!-- <span class="slider-min">0</span> -->
+                <el-slider
+                  v-model="form.FlOnSens"
+                  :min="0"
+                  :max="100"
+                  :show-tooltip="false"
+                />
+                <span class="slider-value">{{ form.FlOnSens }}</span>
+                <!-- <span class="slider-max">100</span> -->
+              </div>
+            </div>
 
+            <!-- 关灯灵敏度 -->
+            <div class="form-item">
+              <span class="form-item__label">Light Off Sensitivity:</span>
+              <div class="form-item__control slider-control">
+                <!-- <span class="slider-min">0</span> -->
+                <el-slider
+                  v-model="form.FlOffSens "
+                  :min="0"
+                  :max="100"
+                  :show-tooltip="false"
+                />
+                <span class="slider-value">{{ form.FlOffSens  }}</span>
+                <!-- <span class="slider-max">100</span> -->
+              </div>
+            </div>
+          </template>
+        </template>
+      </div>
+    </div>
+
+    <!-- Night Vision(仅全彩夜视时显示) -->
+    <div v-if="form.NightMode === 0" class="section">
+      <div class="section-title">Night Vision</div>
+      <div class="section-body">
+        <el-radio-group v-model="form.ColorfulMode" class="radio-group">
+          <el-radio :value="0">Auto</el-radio>
+          <el-radio :value="1">Night</el-radio>
+          <el-radio :value="2">Day</el-radio>
+          <el-radio :value="3">Scheduled</el-radio>
+        </el-radio-group>
+
+        <!-- 定时打开时间选择 -->
+        <div v-if="form.ColorfulMode === 3" class="schedule-settings">
+          <div class="form-item">
+            <span class="form-item__label">Start Time:</span>
+            <div class="form-item__control time-control">
+              <el-select v-model="form.lightOnHour" style="width: 90px">
+                <el-option v-for="h in hours" :key="h" :label="String(h).padStart(2, '0') + 'h'" :value="h" />
+              </el-select>
+              <span class="time-separator">:</span>
+              <el-select v-model="form.lightOnMinute" style="width: 90px">
+                <el-option v-for="m in minutes" :key="m" :label="String(m).padStart(2, '0') + 'm'" :value="m" />
+              </el-select>
+              <span class="time-note">(Today)</span>
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">End Time:</span>
+            <div class="form-item__control time-control">
+              <el-select v-model="form.lightOffHour" style="width: 90px">
+                <el-option v-for="h in hours" :key="h" :label="String(h).padStart(2, '0') + 'h'" :value="h" />
+              </el-select>
+              <span class="time-separator">:</span>
+              <el-select v-model="form.lightOffMinute" style="width: 90px">
+                <el-option v-for="m in minutes" :key="m" :label="String(m).padStart(2, '0') + 'm'" :value="m" />
+              </el-select>
+              <span class="time-note">(Next Day)</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- 底部按钮 -->
+    <div class="btn-wrapper">
+      <el-button round class="btn-reset" @click="resetDefaults">Restore Defaults</el-button>
+      <el-button round type="primary" class="btn-save" @click="saveParams">Save</el-button>
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { reactive, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getNightVisionIlluminator, putNightVisionIlluminator } from '@/api/setting'
+
+const form = reactive({
+  NightMode: 0,        // 补光方式: 0-全彩夜视 1-黑白夜视 2-智能夜视
+  AutoLightEn: false,   // 自动调节亮度
+  PwmLight: 50,     // 灯光亮度
+  FlOnSens: 50,  // 开灯灵敏度
+  FlOffSens : 50, // 关灯灵敏度
+  ColorfulMode: 0,      // 夜视模式
+  lightOnHour: 0,       // 开灯时间-时
+  lightOnMinute: 0,     // 开灯时间-分
+  lightOffHour: 0,      // 关灯时间-时
+  lightOffMinute: 0     // 关灯时间-分
+})
+
+// 时/分下拉选项(整型值)
+const hours = Array.from({ length: 24 }, (_, i) => i)
+const minutes = Array.from({ length: 60 }, (_, i) => i)
+
+// 补光方式选项
+const fillLightOptions = [
+  { value: 0, label: 'Full Color' },
+  { value: 1, label: 'B&W Night Vision' },
+  { value: 2, label: 'Smart Night Vision' }
+]
+
+// 恢复默认设置
+async function resetDefaults() {
+  form.NightMode = 0
+  form.AutoLightEn = false
+  form.PwmLight = 50
+  form.FlOnSens = 50
+  form.FlOffSens  = 50
+  form.ColorfulMode = 0
+  form.lightOnHour = 0
+  form.lightOnMinute = 0
+  form.lightOffHour = 0
+  form.lightOffMinute = 0
+  await saveParams()
+}
+
+// 获取参数
+async function fetchParams() {
+  try {
+    const res = await getNightVisionIlluminator()
+    if (res.data) {
+      const d = res.data
+      if (d.NightMode !== undefined) form.NightMode = Number(d.NightMode)
+      if (d.AutoLightEn !== undefined) form.AutoLightEn = !!d.AutoLightEn
+      if (d.PwmLight !== undefined) form.PwmLight = Number(d.PwmLight)
+      if (d.FlOnSens !== undefined) form.FlOnSens = Number(d.FlOnSens)
+      if (d.FlOffSens  !== undefined) form.FlOffSens  = Number(d.FlOffSens )
+      if (d.ColorfulMode !== undefined) form.ColorfulMode = Number(d.ColorfulMode)
+      // 解析 LightOnTime / LightOffTime 嵌套格式
+      if (d.LightOnTime) {
+        form.lightOnHour = Number(d.LightOnTime.tm_hour) || 0
+        form.lightOnMinute = Number(d.LightOnTime.tm_min) || 0
+      }
+      if (d.LightOffTime) {
+        form.lightOffHour = Number(d.LightOffTime.tm_hour) || 0
+        form.lightOffMinute = Number(d.LightOffTime.tm_min) || 0
+      }
+    }
+  } catch {
+    console.error('Failed to fetch night vision parameters')
+  }
+}
+
+// 保存参数
+async function saveParams() {
+  try {
+    const data = {
+      NightMode: form.NightMode,
+      AutoLightEn: form.AutoLightEn ? 1 : 0,
+      PwmLight: form.PwmLight,
+      FlOnSens: form.FlOnSens,
+      FlOffSens : form.FlOffSens ,
+      ColorfulMode: form.ColorfulMode,
+      LightOnTime: { tm_hour: form.lightOnHour, tm_min: form.lightOnMinute },
+      LightOffTime: { tm_hour: form.lightOffHour, tm_min: form.lightOffMinute }
+    }
+    const res = await putNightVisionIlluminator(data)
+    if (res.data === 'ok\n') {
+      ElMessage.success('Saved successfully')
+    } else {
+      ElMessage.warning('Save failed')
+    }
+  } catch {
+    ElMessage.warning('Save failed')
+  }
+}
+
+onMounted(() => {
+  fetchParams()
+})
+</script>
+
 <style scoped lang="scss">
+.night-vision-illuminator {
+  padding: 10px 15px;
+}
+
+.section {
+  margin-bottom: 24px;
+  background: #f5f7fa;
+  border-radius: 8px;
+  padding: 0 20px;
+
+  .section-title {
+    font-size: 14px;
+    color: #909399;
+    margin-bottom: 14px;
+    padding-bottom: 10px;
+    border-bottom: 1px solid #e4e7ed;
+  }
+
+  .section-body {
+    padding: 0 10px;
+  }
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 14px;
+
+  &__label {
+    flex: 0 0 150px;
+    text-align: right;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 10px;
+  }
+
+  &__control {
+    display: flex;
+    align-items: center;
+
+    .el-select {
+      width: 220px;
+    }
+  }
+}
+
+.slider-control {
+  flex: 1;
+  max-width: 400px;
+  gap: 8px;
+
+  .el-slider {
+    flex: 1;
+  }
+
+  .slider-value {
+    font-size: 13px;
+    color: #303133;
+    min-width: 28px;
+    text-align: center;
+    font-weight: 500;
+  }
+
+  .slider-min,
+  .slider-max {
+    font-size: 12px;
+    color: #909399;
+    min-width: 20px;
+    text-align: center;
+  }
+}
+
+.radio-group {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+  gap: 12px;
+}
+
+.schedule-settings {
+  margin-top: 16px;
+  padding-left: 24px;
+}
+
+.time-control {
+  gap: 8px;
+
+  .time-separator {
+    font-size: 14px;
+    color: #606266;
+  }
+
+  .time-note {
+    font-size: 13px;
+    color: #909399;
+    margin-left: 8px;
+  }
+}
+
+.btn-wrapper {
+  display: flex;
+  gap: 200px;
+  padding-top: 20px;
+
+  .btn-reset {
+    border-color: #409EFF;
+    color: #409EFF;
+
+    &:hover {
+      background-color: #409EFF;
+      color: #fff;
+    }
+  }
 
-</style>
+  .btn-save {
+    min-width: 100px;
+  }
+}
+</style>

+ 369 - 5
src/views/settings/audioVideo/components/video/index.vue

@@ -1,11 +1,375 @@
-<script setup lang="ts">
+<template>
+  <div class="video-settings">
+    <!-- 主码流 -->
+    <fieldset class="stream-section">
+      <legend>Main Stream</legend>
+      <div class="form-item">
+        <span class="form-item__label">Resolution:</span>
+        <div class="form-item__control">
+          <el-select v-model="mainStream.MainResolution">
+            <el-option v-for="item in mainResolutionOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Encoding Format:</span>
+        <div class="form-item__control">
+          <el-select v-model="mainStream.encodeType">
+            <el-option v-for="item in encodeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Frame Rate:</span>
+        <div class="form-item__control">
+          <el-select v-model="mainStream.MainFps">
+            <el-option v-for="item in frameRateOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Bitrate:</span>
+        <div class="form-item__control">
+          <el-input-number v-model="mainStream.MainBitRate" :min="1024" :max="3072" :controls="false" />
+          <span class="form-item__hint">(1024,3072)</span>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Bitrate Control:</span>
+        <div class="form-item__control">
+          <el-select v-model="mainStream.MainEcdFmat">
+            <el-option v-for="item in bitRateControlOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">I-Frame Interval:</span>
+        <div class="form-item__control">
+          <el-input-number v-model="mainStream.MainFrameTime" :min="100" :max="200" :controls="false" />
+          <span class="form-item__hint">(100,200)</span>
+        </div>
+      </div>
+    </fieldset>
 
-</script>
+    <!-- 子码流 -->
+    <fieldset class="stream-section">
+      <legend>Sub Stream</legend>
+      <div class="form-item">
+        <span class="form-item__label">Resolution:</span>
+        <div class="form-item__control">
+          <el-select v-model="subStream.SecondResolution">
+            <el-option v-for="item in subResolutionOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Encoding Format:</span>
+        <div class="form-item__control">
+          <el-select v-model="subStream.encodeType">
+            <el-option v-for="item in encodeTypeOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Frame Rate:</span>
+        <div class="form-item__control">
+          <el-select v-model="subStream.SecondFps">
+            <el-option v-for="item in frameRateOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Bitrate:</span>
+        <div class="form-item__control">
+          <el-input-number v-model="subStream.SecondBitRate" :min="1024" :max="3072" :controls="false" />
+          <span class="form-item__hint">(1024,3072)</span>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">Bitrate Control:</span>
+        <div class="form-item__control">
+          <el-select v-model="subStream.SecondEcdFmat">
+            <el-option v-for="item in bitRateControlOptions" :key="item.value" :label="item.label" :value="item.value" />
+          </el-select>
+        </div>
+      </div>
+      <div class="form-item">
+        <span class="form-item__label">I-Frame Interval:</span>
+        <div class="form-item__control">
+          <el-input-number v-model="subStream.SecondFrameTime" :min="100" :max="200" :controls="false" />
+          <span class="form-item__hint">(100,200)</span>
+        </div>
+      </div>
+    </fieldset>
 
-<template>
-  
+    <!-- 实时模式 -->
+    <!-- <div class="realtime-section">
+      <span class="form-item__label">实时模式:</span>
+      <el-checkbox v-model="realtimeMode" />
+      <span class="realtime-tip">(开启后,夜视动态效果好,但可能会影响静态图像效果)</span>
+    </div> -->
+
+    <!-- 底部按钮 -->
+    <div class="btn-wrapper">
+      <el-button round class="btn-reset" @click="resetDefaults">Restore Defaults</el-button>
+      <el-button round type="primary" class="btn-save" @click="saveParams">Save</el-button>
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { reactive, ref, onMounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { getVideoEncodePara, putVideoEncodePara } from '@/api/setting'
+
+// 主码流参数
+const mainStream = reactive({
+  MainResolution: 5,
+  encodeType: 0,
+  MainFps: 25,
+  MainBitRate: 3072,
+  MainEcdFmat: 0,
+  MainFrameTime: 100
+})
+
+// 子码流参数
+const subStream = reactive({
+  SecondResolution: 0,
+  encodeType: 0,
+  SecondFps: 25,
+  SecondBitRate: 3072,
+  SecondEcdFmat: 0,
+  SecondFrameTime: 100
+})
+
+// 实时模式
+const realtimeMode = ref(false)
+
+
+// 主码流分辨率选项
+const mainResolutionOptions = [
+  { value: 5, label: '2880*1620 (5M)' },
+  { value: 4, label: '2560*1440 (4M)' },
+  { value: 3, label: '2304*1296 (3M)' },
+  { value: 2, label: '1920*1080 (1080P)' },
+  { value: 1, label: '1280*720 (720P)' },
+  { value: 0, label: '640*360' },
+]
+
+// 子码流分辨率选项
+const subResolutionOptions = [
+  { value: 0, label: '640*360' },
+]
+
+// 编码格式选项
+const encodeTypeOptions = [
+  { value: 0, label: 'H264' },
+  // { value: 1, label: 'H265' }
+]
+
+// 帧率选项
+const frameRateOptions = [
+  { value: 10, label: '10' },
+  { value: 11, label: '11' },
+  { value: 12, label: '12' },
+  { value: 13, label: '13' },
+  { value: 14, label: '14' },
+  { value: 15, label: '15' },
+  { value: 16, label: '16' },
+  { value: 17, label: '17' },
+  { value: 18, label: '18' },
+  { value: 19, label: '19' },
+  { value: 20, label: '20' },
+  { value: 21, label: '21' },
+  { value: 22, label: '22' },
+  { value: 23, label: '23' },
+  { value: 24, label: '24' },
+  { value: 25, label: '25' },
+]
+
+// 码率控制选项
+const bitRateControlOptions = [
+  { value: 0, label: 'AVBR' },
+  { value: 1, label: 'CBR' }
+]
+
+
+// 恢复默认设置
+async function resetDefaults() {
+  // 主码流
+  mainStream.MainResolution = 5
+  mainStream.encodeType = 0
+  mainStream.MainFps = 25
+  mainStream.MainBitRate = 3072
+  mainStream.MainEcdFmat = 0
+  mainStream.MainFrameTime = 100
+  // 子码流
+  subStream.SecondResolution = 0
+  subStream.encodeType = 0
+  subStream.SecondFps = 25
+  subStream.SecondBitRate = 3072
+  subStream.SecondEcdFmat = 0
+  subStream.SecondFrameTime = 100
+
+  // realtimeMode.value = false
+
+  await saveParams()
+}
+
+// 获取视频编码参数
+async function fetchParams() {
+  try {
+    const res = await getVideoEncodePara()
+    if (res.data) {
+      const d = res.data
+      // 主码流
+      if (d.MainResolution !== undefined) mainStream.MainResolution = d.MainResolution
+      // if (d.mainEncodeType !== undefined) mainStream.encodeType = d.mainEncodeType
+      if (d.MainFps !== undefined) mainStream.MainFps = d.MainFps
+      if (d.MainBitRate !== undefined) mainStream.MainBitRate = d.MainBitRate
+      if (d.MainEcdFmat !== undefined) mainStream.MainEcdFmat = d.MainEcdFmat
+      if (d.MainFrameTime !== undefined) mainStream.MainFrameTime = d.MainFrameTime
+      // // 子码流
+      if (d.SecondResolution !== undefined) subStream.SecondResolution = d.SecondResolution
+      // if (d.subEncodeType !== undefined) subStream.encodeType = d.subEncodeType
+      if (d.SecondFps !== undefined) subStream.SecondFps = d.SecondFps
+      if (d.SecondBitRate !== undefined) subStream.SecondBitRate = d.SecondBitRate
+      if (d.SecondEcdFmat !== undefined) subStream.SecondEcdFmat = d.SecondEcdFmat
+      if (d.SecondFrameTime !== undefined) subStream.SecondFrameTime = d.SecondFrameTime
+      // // 实时模式
+      // if (d.realtimeMode !== undefined) realtimeMode.value = !!d.realtimeMode
+    }
+  } catch (e) {
+    console.error('获取视频编码参数失败:', e)
+  }
+}
+
+// 保存参数
+async function saveParams() {
+  try {
+    const data = {
+      MainResolution: mainStream.MainResolution,
+      // mainEncodeType: mainStream.encodeType,
+      MainFps: mainStream.MainFps,
+      MainBitRate: mainStream.MainBitRate,
+      MainEcdFmat: mainStream.MainEcdFmat,
+      MainFrameTime: mainStream.MainFrameTime,
+      SecondResolution: subStream.SecondResolution,
+      // subEncodeType: subStream.encodeType,
+      SecondFps: subStream.SecondFps,
+      SecondBitRate: subStream.SecondBitRate,
+      SecondEcdFmat: subStream.SecondEcdFmat,
+      SecondFrameTime: subStream.SecondFrameTime,
+      // realtimeMode: realtimeMode.value ? 1 : 0
+    }
+    const res = await putVideoEncodePara(data)
+    if (res.data === 'ok\n') {
+      ElMessage.success('Save Successful')
+    }
+  } catch (e) {
+    ElMessage.warning('Save Failed')
+  }
+}
+
+onMounted(() => {
+  fetchParams()
+})
+</script>
+
 <style scoped lang="scss">
+.video-settings {
+  padding: 10px 20px;
+}
+
+.stream-section {
+  border: 1px solid #eee;
+  border-radius: 4px;
+  padding: 20px 30px;
+  margin-bottom: 20px;
+
+  legend {
+    font-size: 14px;
+    color: #606266;
+    padding: 0 8px;
+  }
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+  margin-bottom: 16px;
+
+  &__label {
+    flex: 0 0 120px;
+    text-align: right;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 5px;
+  }
+
+  &__control {
+    display: flex;
+    align-items: center;
+    gap: 10px;
+
+    .el-select {
+      width: 220px;
+    }
+
+    .el-input-number {
+      width: 220px;
+
+      :deep(.el-input__inner) {
+        text-align: left;
+      }
+    }
+  }
+
+  &__hint {
+    font-size: 13px;
+    color: #909399;
+  }
+}
+
+.realtime-section {
+  display: flex;
+  align-items: center;
+  padding: 20px 0;
+  border-top: 1px solid #eee;
+
+  .form-item__label {
+    flex: 0 0 100px;
+    text-align: right;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 10px;
+  }
+
+  .realtime-tip {
+    font-size: 12px;
+    color: #909399;
+    margin-left: 8px;
+  }
+}
+
+.btn-wrapper {
+  display: flex;
+  // justify-content: space-between;
+  gap: 200px;
+  padding-top: 20px;
+
+  .btn-reset {
+    border-color: #409EFF;
+    color: #409EFF;
+
+    &:hover {
+      background-color: #409EFF;
+      color: #fff;
+    }
+  }
 
-</style>
+  .btn-save {
+    min-width: 100px;
+  }
+}
+</style>

+ 8 - 4
src/views/settings/audioVideo/index.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="settings-container">
-    <el-tabs v-model="activeName" class="demo-tabs">
+    <el-tabs v-model="activeName" class="tabs">
       <el-tab-pane label="Video" name="first">
-
+        <Video/>
       </el-tab-pane>
       <el-tab-pane label="Audio" name="second">
-
+        <Audio/>
       </el-tab-pane>
       <el-tab-pane label="Night Vision Lights" name="third">
-
+        <NightVisionIlluminator/>
       </el-tab-pane>
     </el-tabs>
   </div>
@@ -17,6 +17,10 @@
 <script setup lang="ts">
 import {ref} from "vue";
 
+import Video from './components/video/index.vue'
+import Audio from './components/audio/index.vue'
+import NightVisionIlluminator from './components/nightVisionIlluminator/index.vue'
+
 const activeName = ref('first')
 
 </script>

+ 323 - 4
src/views/settings/imageDisplay/components/image/index.vue

@@ -1,11 +1,330 @@
-<script setup lang="ts">
+<template>
+  <div class="image-settings">
+    <!-- 左侧视频预览 -->
+    <div class="image-settings__video">
+      <MyVideo :drag-flag="false" />
+    </div>
 
-</script>
+    <!-- 右侧设置面板 -->
+    <div class="image-settings__panel">
+      <!-- 基本参数 -->
+      <div class="section">
+        <div class="section__title">Basic Parameters</div>
+        <div class="section__body">
+          <div class="form-item">
+            <span class="form-item__label">Brightness</span>
+            <div class="form-item__control">
+              <el-slider v-model="basicParams.CscLuma" :min="0" :max="100" :show-tooltip="true" @change="saveParams" />
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">Hue</span>
+            <div class="form-item__control">
+              <el-slider v-model="basicParams.CscHue" :min="0" :max="100" :show-tooltip="true" @change="saveParams" />
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">Saturation</span>
+            <div class="form-item__control">
+              <el-slider v-model="basicParams.CscSaturation" :min="0" :max="100" :show-tooltip="true" @change="saveParams" />
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">Contrast</span>
+            <div class="form-item__control">
+              <el-slider v-model="basicParams.CscContrast" :min="0" :max="100" :show-tooltip="true" @change="saveParams" />
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">Video Mirror</span>
+            <div class="form-item__control">
+              <el-select v-model="basicParams.ImgMirror" @change="saveParams">
+                <el-option v-for="item in mirrorOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </div>
 
-<template>
+      <!-- 低照度设置 -->
+<!--      <div class="section">-->
+<!--        <div class="section__title">Low-Light Settings</div>-->
+<!--        <div class="section__body">-->
+<!--          <div class="form-item">-->
+<!--            <span class="form-item__label">Gain</span>-->
+<!--            <div class="form-item__control">-->
+<!--              <el-slider v-model="lowLightParams.Gain" :min="0" :max="100" :show-tooltip="true" @change="saveParams" />-->
+<!--            </div>-->
+<!--          </div>-->
+<!--        </div>-->
+<!--      </div>-->
+
+      <!-- 高级图像设置 -->
+      <div class="section">
+        <div class="section__title">Advanced Image Settings</div>
+        <div class="section__body">
+<!--          <div class="form-item">-->
+<!--            <span class="form-item__label">White Balance</span>-->
+<!--            <div class="form-item__control">-->
+<!--              <el-select v-model="advancedParams.WhiteBalance" @change="saveParams">-->
+<!--                <el-option v-for="item in whiteBalanceOptions" :key="item.value" :label="item.label" :value="item.value" />-->
+<!--              </el-select>-->
+<!--            </div>-->
+<!--          </div>-->
+<!--          <div class="form-item">-->
+<!--            <span class="form-item__label">Exposure</span>-->
+<!--            <div class="form-item__control">-->
+<!--              <el-select v-model="advancedParams.Exposure" @change="saveParams">-->
+<!--                <el-option v-for="item in exposureOptions" :key="item.value" :label="item.label" :value="item.value" />-->
+<!--              </el-select>-->
+<!--            </div>-->
+<!--          </div>-->
+          <div class="form-item">
+            <span class="form-item__label">Anti-Flicker</span>
+            <div class="form-item__control">
+              <el-select v-model="advancedParams.Antiflicker" @change="saveParams">
+                <el-option v-for="item in switchOptions" :key="item.value" :label="item.label" :value="item.value" />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </div>
 
+      <!-- 恢复默认设置按钮 -->
+      <div class="reset-btn-wrapper">
+        <el-button round @click="resetDefaults">Restore Defaults</el-button>
+      </div>
+    </div>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { ref, onMounted, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import MyVideo from '@/components/myVideo.vue'
+import { getImagePara, putImagePara } from '@/api/setting'
+
+// 基本参数
+const basicParams = reactive({
+  CscLuma: 50,        // 亮度 
+  CscHue: 50,     // 色调
+  CscSaturation: 50,    // 饱和度 
+  CscContrast: 50,      // 对比度 
+  ImgMirror: 0           // 视频镜像
+})
+
+// 低照度设置
+const lowLightParams = reactive({
+  Gain: 50,              // 增益 0-100
+})
+
+// 高级图像设置
+const advancedParams = reactive({
+  // WhiteBalance: 0,    // 白平衡
+  // Exposure: 0,        // 曝光
+  Antiflicker: 0      // 抗闪烁
+})
+
+// 镜像选项
+const mirrorOptions = [
+  { value: 0, label: 'Normal' },
+  { value: 1, label: 'Horizontal Mirror' },
+  { value: 2, label: 'Vertical Mirror' },
+  { value: 3, label: 'Diagonal Mirror' }
+]
+
+// 通用开关选项
+const switchOptions = [
+  { value: 0, label: 'Off' },
+  { value: 1, label: 'On' }
+]
+
+// 白平衡选项
+const whiteBalanceOptions = [
+  { value: 0, label: 'Manual' },
+  { value: 1, label: 'Auto' }
+
+]
+
+// 曝光选项
+const exposureOptions = [
+  { value: 0, label: 'Manual' },
+  { value: 1, label: 'Auto' }
+]
+
+// 获取图像参数
+async function fetchImagePara() {
+  try {
+    const res = await getImagePara()
+    if (res.data) {
+      const d = res.data
+      // 基本参数
+      if (d.CscLuma !== undefined) basicParams.CscLuma = Number(d.CscLuma)
+      if (d.CscHue !== undefined) basicParams.CscHue = Number(d.CscHue)
+      if (d.CscSaturation !== undefined) basicParams.CscSaturation = Number(d.CscSaturation)
+      if (d.CscContrast !== undefined) basicParams.CscContrast = Number(d.CscContrast)
+      if (d.ImgMirror !== undefined) basicParams.ImgMirror = Number(d.ImgMirror)
+      // 低照度设置
+      // if (d.Gain !== undefined) lowLightParams.Gain = Number(d.Gain)
+      // 高级图像设置
+      // if (d.WhiteBalance !== undefined) advancedParams.WhiteBalance = Number(d.WhiteBalance)
+      // if (d.Exposure !== undefined) advancedParams.Exposure = Number(d.Exposure)
+      if (d.Antiflicker !== undefined) advancedParams.Antiflicker = Number(d.Antiflicker)
+    }
+  } catch (e) {
+    console.error('获取图像参数失败:', e)
+  }
+}
+
+// 保存参数(滑块或下拉变化时自动保存)
+async function saveParams() {
+  try {
+    const data = {
+      ...basicParams,
+      // ...lowLightParams,
+      ...advancedParams
+    }
+    const res = await putImagePara(data)
+    if (res.data === 'ok\n') {
+      ElMessage.success('设置成功')
+    }
+  } catch (e) {
+    ElMessage.warning('设置失败')
+  }
+}
+
+// 恢复默认设置
+async function resetDefaults() {
+  basicParams.CscLuma = 50
+  basicParams.CscHue = 50
+  basicParams.CscSaturation = 50
+  basicParams.CscContrast = 50
+  basicParams.ImgMirror = 0
+
+  // lowLightParams.Gain = 50
+  //
+  // advancedParams.WhiteBalance = 0
+  // advancedParams.Exposure = 0
+  advancedParams.Antiflicker = 0
+
+  await saveParams()
+}
+
+onMounted(() => {
+  fetchImagePara()
+})
+</script>
+
 <style scoped lang="scss">
+.image-settings {
+  display: flex;
+  align-items: flex-start;
+  gap: 30px;
+  width: 100%;
+
+  &__video {
+    // 55% 宽度,高度按 16:9 自动计算
+    width: 48%;
+    flex-shrink: 0;
+    // 用 cqw 近似:55% 父容器宽度的 9/16
+    height: 0;
+    padding-bottom: calc(48% * 9 / 16);
+    background: #000;
+    border-radius: 4px;
+    overflow: hidden;
+    position: relative;
+
+    :deep(.preview-container) {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    :deep(.video_container) {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    :deep(#player) {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+    :deep(.video-control) {
+      display: none;
+    }
+  }
+
+  &__panel {
+    flex: 1;
+    overflow-y: auto;
+    max-height: calc(100vh - 180px);
+    padding-right: 8px;
+  }
+}
+
+.section {
+  margin-bottom: 20px;
+
+  &__title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #4A90E2;;
+    // color: #c0392b;
+    margin-bottom: 10px;
+    padding-bottom: 6px;
+    border-bottom: 1px solid #eee;
+  }
+
+  &__body {
+    display: flex;
+    flex-direction: column;
+    gap: 6px;
+  }
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+
+  &__label {
+    flex: 0 0 100px;
+    text-align: right;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 10px;
+  }
+
+  &__control {
+    flex: 1;
+    max-width: 200px;
+
+    .el-slider {
+      margin: 0;
+    }
+
+    .el-select {
+      width: 100%;
+    }
+  }
+}
+
+.reset-btn-wrapper {
+  display: flex;
+  justify-content: flex-start;
+  margin-top: 30px;
+
+  .el-button {
+    border-color: #409EFF;
+    color: #409EFF;
 
-</style>
+    &:hover {
+      background-color: #409EFF;
+      color: #fff;
+    }
+  }
+}
+</style>

+ 445 - 4
src/views/settings/imageDisplay/components/osd/index.vue

@@ -1,11 +1,452 @@
-<script setup lang="ts">
+<template>
+  <div class="osd-settings">
+    <!-- 左侧:视频预览 + OSD拖拽 -->
+    <div class="osd-settings__left">
+      <div class="osd-settings__video" ref="videoWrapRef">
+        <MyVideo :drag-flag="false" />
+        <!-- OSD叠加层:用于拖拽定位 -->
+        <div class="osd-overlay" ref="overlayRef">
+          <!-- 时间OSD -->
+          <div
+            v-if="form.EnTime"
+            class="osd-tag osd-tag--time"
+            :style="osdTimeStyle"
+            @mousedown.prevent="startDragOsd($event, 'time')"
+          >
+            {{ currentTime }}
+          </div>
+          <!-- 名称OSD -->
+          <div
+            v-if="form.EnName"
+            class="osd-tag osd-tag--name"
+            :style="osdNameStyle"
+            @mousedown.prevent="startDragOsd($event, 'name')"
+          >
+            {{ form.OsdName }}
+          </div>
+        </div>
+      </div>
+      <div class="video-hint">
+        <span class="video-hint__text">Press and hold the red box to drag the OSD position. Changes take effect after saving.</span>
+      </div>
+    </div>
 
-</script>
+    <!-- 右侧设置面板 -->
+    <div class="osd-settings__panel">
+      <!-- 显示设置 -->
+      <div class="section">
+        <div class="section__title">Display Settings</div>
+        <div class="section__body">
+          <!-- 显示名称 -->
+          <div class="form-item">
+            <el-checkbox v-model="form.EnName">Display Name</el-checkbox>
+          </div>
+          <div class="form-item form-item--indent" v-if="form.EnName">
+            <span class="form-item__label">Name</span>
+            <div class="form-item__control name-control">
+              <span class="name-text">{{ form.OsdName }}</span>
+              <el-button link type="primary" @click="showEditName = true">
+                <el-icon><Edit /></el-icon> Modify
+              </el-button>
+            </div>
+          </div>
 
-<template>
+          <!-- 显示时间 -->
+          <div class="form-item">
+            <el-checkbox v-model="form.EnTime">Display Time:</el-checkbox>
+            <span class="form-item__value" v-if="form.EnTime">{{ currentTime }}</span>
+          </div>
+
+          <!-- 显示码率 -->
+          <!-- <div class="form-item">
+            <el-checkbox v-model="form.showBitrate">显示码率:</el-checkbox>
+            <span class="form-item__value" v-if="form.showBitrate">BR={{ form.bitrateText }}</span>
+          </div> -->
+
+          <!-- 自定义 -->
+          <!-- <div class="form-item">
+            <el-checkbox v-model="form.showCustom">自定义</el-checkbox>
+            <el-input
+              v-if="form.showCustom"
+              v-model="form.customText"
+              size="small"
+              style="width: 160px; margin-left: 8px"
+              placeholder="custom"
+            />
+          </div> -->
+        </div>
+      </div>
 
+      <!-- 字体设置 -->
+      <!-- <div class="section">
+        <div class="section__title">字体设置</div>
+        <div class="section__body">
+          <div class="form-item">
+            <span class="form-item__label">字体大小:</span>
+            <div class="form-item__control">
+              <el-select v-model="form.fontSize">
+                <el-option label="小" :value="0" />
+                <el-option label="中" :value="1" />
+                <el-option label="大" :value="2" />
+              </el-select>
+            </div>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">字体颜色:</span>
+            <div class="form-item__control">
+              <el-select v-model="form.fontColor">
+                <el-option label="自动" :value="0" />
+                <el-option label="白色" :value="1" />
+                <el-option label="黑色" :value="2" />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </div> -->
+
+      <!-- 保存按钮 -->
+      <div class="save-btn-wrapper">
+        <el-button type="primary" round @click="saveOsd">Save</el-button>
+      </div>
+    </div>
+
+    <!-- 修改名称弹窗 -->
+    <el-dialog v-model="showEditName" title="Modify Name" width="360px" :close-on-click-modal="false">
+      <el-input v-model="editNameValue" maxlength="32" placeholder="Please enter a name" />
+      <template #footer>
+        <el-button @click="showEditName = false">Cancel</el-button>
+        <el-button type="primary" @click="confirmEditName">Confirm</el-button>
+      </template>
+    </el-dialog>
+  </div>
 </template>
 
+<script setup lang="ts">
+import { ref, reactive, computed, onMounted, onUnmounted } from 'vue'
+import { ElMessage } from 'element-plus'
+import { Edit } from '@element-plus/icons-vue'
+import MyVideo from '@/components/myVideo.vue'
+import { getOsdPara, putOsdPara } from '@/api/setting'
+
+// 表单数据
+const form = reactive({
+  EnName: true,
+  OsdName: 'ANSJER',
+  EnTime: true,
+  // showBitrate: false,
+  // bitrateText: '79K',
+  // showCustom: false,
+  // customText: 'custom',
+  // fontSize: 0,   // 0=小 1=中 2=大
+  // fontColor: 0,  // 0=自动 1=白色 2=黑色
+  // OSD归一化坐标 (0~1)
+  OsdTimeX: 0.01,
+  OsdTimeY: 0.02,
+  OsdNameX: 0.90,
+  OsdNameY: 0.92
+})
+
+// 修改名称弹窗
+const showEditName = ref(false)
+const editNameValue = ref('')
+
+function confirmEditName() {
+  if (!editNameValue.value.trim()) {
+    ElMessage.warning('Name cannot be empty')
+    return
+  }
+  form.OsdName = editNameValue.value.trim()
+  showEditName.value = false
+}
+
+// 实时时间显示
+const currentTime = ref('')
+let timeTimer: ReturnType<typeof setInterval> | null = null
+
+function updateTime() {
+  const now = new Date()
+  const y = now.getFullYear()
+  const M = String(now.getMonth() + 1).padStart(2, '0')
+  const d = String(now.getDate()).padStart(2, '0')
+  const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
+  const w = days[now.getDay()]
+  const h = String(now.getHours()).padStart(2, '0')
+  const m = String(now.getMinutes()).padStart(2, '0')
+  const s = String(now.getSeconds()).padStart(2, '0')
+  currentTime.value = `${y}-${M}-${d} ${w} ${h}:${m}:${s}`
+}
+
+// OSD拖拽相关
+const overlayRef = ref<HTMLElement | null>(null)
+let draggingTarget: 'time' | 'name' | null = null
+let dragStartMouseX = 0
+let dragStartMouseY = 0
+let dragStartPosX = 0
+let dragStartPosY = 0
+
+const osdTimeStyle = computed(() => ({
+  left: `${form.OsdTimeX * 100}%`,
+  top: `${form.OsdTimeY * 100}%`
+}))
+
+const osdNameStyle = computed(() => ({
+  left: `${form.OsdNameX * 100}%`,
+  top: `${form.OsdNameY * 100}%`
+}))
+
+function startDragOsd(e: MouseEvent, target: 'time' | 'name') {
+  draggingTarget = target
+  dragStartMouseX = e.clientX
+  dragStartMouseY = e.clientY
+  dragStartPosX = target === 'time' ? form.OsdTimeX : form.OsdNameX
+  dragStartPosY = target === 'time' ? form.OsdTimeY : form.OsdNameY
+  document.addEventListener('mousemove', onDragOsd)
+  document.addEventListener('mouseup', endDragOsd)
+}
+
+function onDragOsd(e: MouseEvent) {
+  if (!draggingTarget || !overlayRef.value) return
+  const rect = overlayRef.value.getBoundingClientRect()
+  const dx = (e.clientX - dragStartMouseX) / rect.width
+  const dy = (e.clientY - dragStartMouseY) / rect.height
+  let nx = Math.max(0, Math.min(0.95, dragStartPosX + dx)) // 避免文字溢出到画面外
+  let ny = Math.max(0, Math.min(0.95, dragStartPosY + dy)) // 避免文字溢出到画面外
+  if (draggingTarget === 'time') {
+    form.OsdTimeX = nx
+    form.OsdTimeY = ny
+  } else {
+    form.OsdNameX = nx
+    form.OsdNameY = ny
+  }
+}
+
+function endDragOsd() {
+  draggingTarget = null
+  document.removeEventListener('mousemove', onDragOsd)
+  document.removeEventListener('mouseup', endDragOsd)
+}
+
+// 获取OSD参数
+async function fetchOsd() {
+  try {
+    const res = await getOsdPara()
+    if (res.data) {
+      const d = res.data
+      if (d.EnName !== undefined) form.EnName = !!d.EnName
+      if (d.OsdName !== undefined) form.OsdName = d.OsdName
+      if (d.EnTime !== undefined) form.EnTime = !!d.EnTime
+      // if (d.showBitrate !== undefined) form.showBitrate = !!d.showBitrate
+      // if (d.showCustom !== undefined) form.showCustom = !!d.showCustom
+      // if (d.customText !== undefined) form.customText = d.customText
+      // if (d.fontSize !== undefined) form.fontSize = Number(d.fontSize)
+      // if (d.fontColor !== undefined) form.fontColor = Number(d.fontColor)
+      if (d.OsdTimeX !== undefined) form.OsdTimeX = Number(d.OsdTimeX)
+      if (d.OsdTimeY !== undefined) form.OsdTimeY = Number(d.OsdTimeY)
+      if (d.OsdNameX !== undefined) form.OsdNameX = Number(d.OsdNameX)
+      if (d.OsdNameY !== undefined) form.OsdNameY = Number(d.OsdNameY)
+    }
+  } catch {
+    console.error('Failed to get OSD parameters')
+  }
+}
+
+// 保存OSD参数
+async function saveOsd() {
+  try {
+    const data = {
+      EnName: form.EnName ? 1 : 0,
+      OsdName: form.OsdName,
+      EnTime: form.EnTime ? 1 : 0,
+      // showBitrate: form.showBitrate ? 1 : 0,
+      // showCustom: form.showCustom ? 1 : 0,
+      // customText: form.customText,
+      // fontSize: form.fontSize,
+      // fontColor: form.fontColor,
+      OsdTimeX: form.OsdTimeX,
+      OsdTimeY: form.OsdTimeY,
+      OsdNameX: form.OsdNameX,
+      OsdNameY: form.OsdNameY
+    }
+    const res = await putOsdPara(data)
+    if (res.data === 'ok\n') {
+      ElMessage.success('Saved successfully')
+    }
+  } catch {
+    ElMessage.warning('Failed to save')
+  }
+}
+
+onMounted(() => {
+  updateTime()
+//   timeTimer = setInterval(updateTime, 1000)
+  fetchOsd()
+})
+
+onUnmounted(() => {
+  if (timeTimer) clearInterval(timeTimer)
+  endDragOsd()
+})
+</script>
+
 <style scoped lang="scss">
+.osd-settings {
+  display: flex;
+  align-items: flex-start;
+  gap: 30px;
+  width: 100%;
+
+  &__left {
+    width: 48%;
+    flex-shrink: 0;
+  }
+
+  &__video {
+    width: 100%;
+    height: 0;
+    padding-bottom: calc(100% * 9 / 16);
+    background: #000;
+    border-radius: 4px;
+    overflow: hidden;
+    position: relative;
+
+    :deep(.preview-container) {
+      position: absolute;
+      top: 0; left: 0;
+      width: 100%; height: 100%;
+    }
+    :deep(.video_container) {
+      position: absolute;
+      top: 0; left: 0;
+      width: 100%; height: 100%;
+    }
+    :deep(#player) {
+      width: 100%; height: 100%;
+      object-fit: contain;
+    }
+    :deep(.video-control) {
+      display: none;
+    }
+
+    .osd-overlay {
+      position: absolute;
+      top: 0; left: 0;
+      width: 100%; height: 100%;
+      z-index: 10;
+      pointer-events: none;
+    }
+  }
+
+  &__panel {
+    flex: 1;
+    overflow-y: auto;
+    max-height: calc(100vh - 180px);
+    padding-right: 8px;
+  }
+}
+
+.osd-tag {
+  position: absolute;
+  pointer-events: auto;
+  cursor: move;
+  padding: 2px 6px;
+  font-size: 12px;
+  color: #fff;
+  white-space: nowrap;
+  user-select: none;
+  border: 1px solid transparent;
+  transition: border-color 0.2s;
+
+  &:hover,
+  &:active {
+    border-color: #e74c3c;
+  }
+}
+
+.video-hint {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  margin-top: 3px;
+  padding: 8px 12px;
+  background: #f0f2f5;
+  border-radius: 4px;
+  border: 1px solid #e4e7ed;
+
+  &__text {
+    font-size: 12px;
+    color: #7a7d83;
+    line-height: 1.2;
+  }
+}
+
+.section {
+  margin-bottom: 20px;
+
+  &__title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #4A90E2;;
+    // color: #c0392b;
+    margin-bottom: 12px;
+    padding-bottom: 6px;
+    border-bottom: 1px solid #e3e0e0;
+  }
+
+  &__body {
+    display: flex;
+    flex-direction: column;
+    gap: 14px;
+  }
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+
+  &--indent {
+    padding-left: 24px;
+  }
+
+  &__label {
+    flex: 0 0 auto;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 12px;
+  }
+
+  &__value {
+    font-size: 13px;
+    color: #606266;
+    margin-left: 8px;
+  }
+
+  &__control {
+    flex: 1;
+    max-width: 200px;
+
+    .el-select {
+      width: 100%;
+    }
+  }
+}
+
+.name-control {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  max-width: none;
+
+  .name-text {
+    font-size: 13px;
+    color: #303133;
+  }
+}
+
+.save-btn-wrapper {
+  margin-top: 40px;
 
-</style>
+  .el-button {
+    min-width: 80px;
+  }
+}
+</style>

+ 486 - 4
src/views/settings/imageDisplay/components/privacyMasking/index.vue

@@ -1,11 +1,493 @@
+<template>
+  <div class="privacy-masking">
+    <!-- 左侧:视频 + 底部提示 -->
+    <div class="privacy-masking__left">
+      <div class="privacy-masking__video">
+        <MyVideo :drag-flag="false" />
+        <canvas
+          ref="canvasRef"
+          class="mask-canvas"
+          @mousedown="onMouseDown"
+          @mousemove="onMouseMove"
+          @mouseup="onMouseUp"
+          @mouseleave="onMouseUp"
+        />
+      </div>
+      <div class="video-hint">
+        <span class="video-hint__text">
+          Draw mask areas with your mouse. Up to 5 regions supported. Drag to reposition.
+        </span>
+        <el-button round size="small" @click="clearAllMasks">Clear</el-button>
+      </div>
+    </div>
+
+    <!-- 右侧设置面板 -->
+    <div class="privacy-masking__panel">
+      <div class="section">
+        <div class="section__title">Mask Settings</div>
+        <div class="section__body">
+          <div class="form-item">
+            <el-checkbox v-model="enableMask">Enable Privacy Mask:</el-checkbox>
+          </div>
+          <div class="form-item">
+            <span class="form-item__label">Mask Color:</span>
+            <div class="form-item__control color-options">
+              <div
+                class="color-btn"
+                :class="{ active: maskColor === 'black' }"
+                @click="setColor('black')"
+              >
+                <span class="color-block black" />
+                <span>Black</span>
+              </div>
+              <div
+                class="color-btn"
+                :class="{ active: maskColor === 'red' }"
+                @click="setColor('red')"
+              >
+                <span class="color-block red" />
+                <span>Red</span>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="save-btn-wrapper">
+        <el-button type="primary" round @click="saveMasks">Save</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
 <script setup lang="ts">
+import { ref, reactive, onMounted, onUnmounted, nextTick } from 'vue'
+import { ElMessage } from 'element-plus'
+import MyVideo from '@/components/myVideo.vue'
+import { getPrivacyMask, putPrivacyMask } from '@/api/setting'
 
-</script>
+// 遮挡矩形(canvas 像素坐标)
+interface MaskRect {
+  x: number
+  y: number
+  w: number
+  h: number
+}
 
-<template>
+const MAX_MASKS = 5
 
-</template>
+// 状态
+const enableMask = ref(false)
+const maskColor = ref<'black' | 'red'>('red')
+const masks = reactive<MaskRect[]>([])
+const canvasRef = ref<HTMLCanvasElement | null>(null)
+
+// 交互状态
+let isDrawing = false
+let isDragging = false
+let dragIndex = -1
+let startX = 0
+let startY = 0
+let dragOffsetX = 0
+let dragOffsetY = 0
+let tempRect: MaskRect | null = null
+
+// 颜色映射:颜色名 -> 接口十六进制值
+const COLOR_MAP: Record<string, string> = {
+  black: 0x000000,
+  red: 0xFF0000
+}
+
+function setColor(color: 'black' | 'red') {
+  maskColor.value = color
+  drawAll()
+}
+
+function getCanvasPos(e: MouseEvent) {
+  const canvas = canvasRef.value
+  if (!canvas) return { x: 0, y: 0 }
+  const rect = canvas.getBoundingClientRect()
+  return {
+    x: e.clientX - rect.left,
+    y: e.clientY - rect.top
+  }
+}
+
+// 判断点是否在某个矩形内
+function hitTest(px: number, py: number): number {
+  for (let i = masks.length - 1; i >= 0; i--) {
+    const m = masks[i]
+    if (px >= m.x && px <= m.x + m.w && py >= m.y && py <= m.y + m.h) {
+      return i
+    }
+  }
+  return -1
+}
+
+function onMouseDown(e: MouseEvent) {
+  const pos = getCanvasPos(e)
+  const idx = hitTest(pos.x, pos.y)
+  if (idx >= 0) {
+    isDragging = true
+    dragIndex = idx
+    dragOffsetX = pos.x - masks[idx].x
+    dragOffsetY = pos.y - masks[idx].y
+    return
+  }
+  if (masks.length >= MAX_MASKS) {
+    ElMessage.warning('A maximum of 5 mask regions is supported')
+    return
+  }
+  isDrawing = true
+  startX = pos.x
+  startY = pos.y
+  tempRect = { x: pos.x, y: pos.y, w: 0, h: 0 }
+}
+
+function onMouseMove(e: MouseEvent) {
+  const canvas = canvasRef.value
+  if (!canvas) return
+  const pos = getCanvasPos(e)
+
+  if (isDrawing && tempRect) {
+    tempRect.x = Math.min(startX, pos.x)
+    tempRect.y = Math.min(startY, pos.y)
+    tempRect.w = Math.abs(pos.x - startX)
+    tempRect.h = Math.abs(pos.y - startY)
+    drawAll(tempRect)
+    return
+  }
+
+  if (isDragging && dragIndex >= 0) {
+    let nx = pos.x - dragOffsetX
+    let ny = pos.y - dragOffsetY
+    const m = masks[dragIndex]
+    nx = Math.max(0, Math.min(canvas.width - m.w, nx))
+    ny = Math.max(0, Math.min(canvas.height - m.h, ny))
+    masks[dragIndex].x = nx
+    masks[dragIndex].y = ny
+    drawAll()
+    return
+  }
+
+  const idx = hitTest(pos.x, pos.y)
+  canvas.style.cursor = idx >= 0 ? 'move' : 'crosshair'
+}
+
+function onMouseUp() {
+  if (isDrawing && tempRect && tempRect.w > 5 && tempRect.h > 5) {
+    masks.push({ ...tempRect })
+  }
+  isDrawing = false
+  isDragging = false
+  dragIndex = -1
+  tempRect = null
+  drawAll()
+}
+
+function clearAllMasks() {
+  masks.splice(0, masks.length)
+  drawAll()
+}
+
+function drawAll(extra?: MaskRect) {
+  const canvas = canvasRef.value
+  if (!canvas) return
+  const ctx = canvas.getContext('2d')
+  if (!ctx) return
+
+  ctx.clearRect(0, 0, canvas.width, canvas.height)
+
+  const fillColor = maskColor.value === 'red'
+    ? 'rgba(180, 40, 40, 0.75)'
+    : 'rgba(0, 0, 0, 0.75)'
+  const strokeColor = maskColor.value === 'red'
+    ? '#e74c3c'
+    : '#333333'
+
+  for (const m of masks) {
+    ctx.fillStyle = fillColor
+    ctx.fillRect(m.x, m.y, m.w, m.h)
+    ctx.strokeStyle = strokeColor
+    ctx.lineWidth = 2
+    ctx.strokeRect(m.x, m.y, m.w, m.h)
+  }
+
+  if (extra) {
+    ctx.fillStyle = fillColor
+    ctx.fillRect(extra.x, extra.y, extra.w, extra.h)
+    ctx.strokeStyle = strokeColor
+    ctx.lineWidth = 2
+    ctx.strokeRect(extra.x, extra.y, extra.w, extra.h)
+  }
+}
+
+function resizeCanvas() {
+  const canvas = canvasRef.value
+  if (!canvas) return
+  const parent = canvas.parentElement
+  if (!parent) return
+  canvas.width = parent.clientWidth
+  canvas.height = parent.clientHeight
+  drawAll()
+}
+
+// 将 canvas 像素坐标归一化为 0~1 比例值(x/宽, y/高)
+function toNormalized(rect: MaskRect) {
+  const canvas = canvasRef.value
+  if (!canvas) return { CoverX: 0, CoverY: 0, CoverWidth: 0, CoverHeight: 0, CoverColor: 0 }
+  return {
+    CoverX: parseFloat((rect.x / canvas.width).toFixed(4)),
+    CoverY: parseFloat((rect.y / canvas.height).toFixed(4)),
+    CoverWidth: parseFloat((rect.w / canvas.width).toFixed(4)),
+    CoverHeight: parseFloat((rect.h / canvas.height).toFixed(4)),
+    CoverColor: COLOR_MAP[maskColor.value] ?? 0xFF0000
+  }
+}
+
+// 将接口归一化坐标还原为 canvas 像素坐标
+function fromNormalized(item: any): MaskRect {
+  const canvas = canvasRef.value
+  if (!canvas) return { x: 0, y: 0, w: 0, h: 0 }
+  return {
+    x: (item.CoverX ?? 0) * canvas.width,
+    y: (item.CoverY ?? 0) * canvas.height,
+    w: (item.CoverWidth ?? 0) * canvas.width,
+    h: (item.CoverHeight ?? 0) * canvas.height
+  }
+}
+
+// 根据 CoverColor 值反推颜色名
+function colorFromHex(val: number): 'black' | 'red' {
+  return val === 0xFF0000 ? 'red' : 'black'
+}
+
+async function saveMasks() {
+  try {
+    const data = {
+      CoverEnable: enableMask.value ? 1 : 0,
+      CoverList: masks.map(toNormalized)
+    }
+    const res = await putPrivacyMask(data)
+    if (res.data === 'ok\n') {
+      ElMessage.success('Saved successfully')
+    }
+  } catch {
+    ElMessage.warning('Failed to save')
+  }
+}
+
+async function fetchMasks() {
+  try {
+    const res = await getPrivacyMask()
+    if (res.data) {
+      const d = res.data
+      enableMask.value = d.CoverEnable === 1
+      if (Array.isArray(d.CoverList) && d.CoverList.length > 0) {
+        // 从第一个区域读取颜色
+        maskColor.value = colorFromHex(d.CoverList[0].CoverColor)
+        // 过滤掉宽高为 0 的空区域
+        const validRegions = d.CoverList.filter(
+          (item: any) => (item.CoverWidth ?? 0) > 0 && (item.CoverHeight ?? 0) > 0
+        )
+        masks.splice(0, masks.length, ...validRegions.map(fromNormalized))
+      }
+      drawAll()
+    }
+  } catch {
+    console.error('Failed to fetch privacy mask settings')
+  }
+}
+
+let resizeObserver: ResizeObserver | null = null
+
+onMounted(async () => {
+  await nextTick()
+  resizeCanvas()
+
+  const canvas = canvasRef.value
+  if (canvas?.parentElement) {
+    resizeObserver = new ResizeObserver(() => resizeCanvas())
+    resizeObserver.observe(canvas.parentElement)
+  }
+
+  await fetchMasks()
+})
+
+onUnmounted(() => {
+  resizeObserver?.disconnect()
+})
+</script>
 
 <style scoped lang="scss">
+.privacy-masking {
+  display: flex;
+  align-items: flex-start;
+  gap: 30px;
+  width: 100%;
+
+  &__left {
+    width: 48%;
+    flex-shrink: 0;
+  }
+
+  &__video {
+    width: 100%;
+    height: 0;
+    padding-bottom: calc(100% * 9 / 16);
+    background: #000;
+    border-radius: 4px;
+    overflow: hidden;
+    position: relative;
+
+    :deep(.preview-container) {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    :deep(.video_container) {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+    }
+    :deep(#player) {
+      width: 100%;
+      height: 100%;
+      object-fit: contain;
+    }
+    :deep(.video-control) {
+      display: none;
+    }
+
+    .mask-canvas {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 100%;
+      height: 100%;
+      z-index: 10;
+      cursor: crosshair;
+    }
+  }
+
+  .video-hint {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    gap: 12px;
+    margin-top: 3px;
+    padding: 8px 12px;
+    background: #f0f2f5;
+    border-radius: 4px;
+    border: 1px solid #e4e7ed;
+
+    &__text {
+      font-size: 12px;
+      color: #7a7d83;
+      line-height: 1.2;
+    }
+
+    .el-button {
+      flex-shrink: 0;
+      border-color: #dcdfe6;
+      color: #606266;
+
+      &:hover {
+        border-color: #c0392b;
+        color: #c0392b;
+      }
+    }
+  }
+
+  &__panel {
+    flex: 1;
+    overflow-y: auto;
+    max-height: calc(100vh - 180px);
+    padding-right: 8px;
+  }
+}
+
+.section {
+  margin-bottom: 20px;
+
+  &__title {
+    font-size: 15px;
+    font-weight: 600;
+    color: #4A90E2;
+    margin-bottom: 12px;
+    padding-bottom: 6px;
+    border-bottom: 1px solid #e3e0e0;
+  }
+
+  &__body {
+    display: flex;
+    flex-direction: column;
+    gap: 14px;
+  }
+}
+
+.form-item {
+  display: flex;
+  align-items: center;
+
+  &__label {
+    flex: 0 0 auto;
+    font-size: 13px;
+    color: #606266;
+    padding-right: 12px;
+  }
+
+  &__control {
+    flex: 1;
+  }
+}
+
+.color-options {
+  display: flex;
+  gap: 12px;
+}
+
+.color-btn {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding: 4px 14px;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #606266;
+  transition: all 0.2s;
+
+  &.active {
+    border-color: #c0392b;
+    color: #c0392b;
+  }
+
+  .color-block {
+    display: inline-block;
+    width: 14px;
+    height: 14px;
+    border-radius: 2px;
+
+    &.black {
+      background: #000;
+    }
+    &.red {
+      background: #c0392b;
+    }
+  }
+}
+
+.save-btn-wrapper {
+  margin-top: 40px;
 
-</style>
+  .el-button {
+    min-width: 80px;
+  }
+}
+</style>

+ 7 - 4
src/views/settings/imageDisplay/index.vue

@@ -1,14 +1,14 @@
 <template>
   <div class="settings-container">
-    <el-tabs v-model="activeName" class="demo-tabs">
+    <el-tabs v-model="activeName" class="tabs">
       <el-tab-pane label="Image" name="first">
-
+        <Image />
       </el-tab-pane>
       <el-tab-pane label="OSD Settings" name="second">
-
+        <OSD />
       </el-tab-pane>
       <el-tab-pane label="Privacy Masking " name="third">
-
+        <PrivacyMasking />
       </el-tab-pane>
     </el-tabs>
   </div>
@@ -16,6 +16,9 @@
 
 <script setup lang="ts">
 import {ref} from "vue";
+import Image from './components/image/index.vue'
+import OSD from './components/osd/index.vue'
+import PrivacyMasking from './components/privacyMasking/index.vue'
 
 const activeName = ref('first')
 

+ 161 - 278
src/views/settings/netSettings/components/IP/index.vue

@@ -1,93 +1,87 @@
 <template>
-  <div class="setting-container">
-    <div class="setting-card">
-      <div class="card-header">
-        <h2 class="title">Network Settings</h2>
-        <p class="subtitle">Configure device network connection parameters</p>
+  <div class="ip-settings">
+    <div class="section">
+      <div class="section-title">DHCP Settings</div>
+      <div class="dhcp-row">
+        <span class="label">Automatic (DHCP):</span>
+        <el-switch
+          v-model="settingFormData.enableDHCP"
+          :active-value="1"
+          :inactive-value="0"
+          active-color="#409eff"
+          inactive-color="#dcdfe4"
+          @change="switchChange($event)"
+        />
       </div>
-      <div class="content">
-        <el-form
-          ref="settingFormRef"
-          :hide-required-asterisk="true"
-          label-position="left"
-          label-width="125px"
-          :model="settingFormData"
-          :rules="settingFormRules"
-          @keyup.enter="handleSave"
-        >
-          <!-- 自动获取DHCP -->
-          <div class="dhcp-section">
-            <div class="dhcp-header">
-              <span class="dhcp-label">Automatic (DHCP)</span>
-              <el-switch
-                v-model="settingFormData.enableDHCP"
-                :active-value="1"
-                :inactive-value="0"
-                active-color="#409eff"
-                inactive-color="#dcdfe4"
-                @change="switchChange($event)"
-              />
-            </div>
-            <p class="dhcp-tip">The following fields will be disabled when automatic configuration is enabled.</p>
-          </div>
-
-          <!-- IP版本 -->
-          <el-form-item label="IP Version" prop="IP_version" class="form-item-custom">
-            <el-input v-model="IP_version"   disabled></el-input>
-          </el-form-item>
-
-          <!-- IP地址 -->
-          <el-form-item label="IP Address" prop="IP" class="form-item-custom">
-            <IPInputBox
-              :disabled="settingFormData.enableDHCP"
-              :ip-val="settingFormData.ipAddress"
-              @update:ip-val="(updateVal) => (settingFormData.ipAddress = updateVal)"
-            />
-          </el-form-item>
-
-          <!-- 子网掩码 -->
-          <el-form-item label="Subnet Mask" prop="mask" class="form-item-custom">
-            <IPInputBox
-              :disabled="settingFormData.enableDHCP"
-              :ip-val="settingFormData.subNetAddress"
-              @update:ip-val="(updateVal) => (settingFormData.subNetAddress = updateVal)"
-            />
-          </el-form-item>
-
-          <!-- 默认网关 -->
-          <el-form-item label="Default Gateway" prop="gateway" class="form-item-custom">
-            <IPInputBox
-              :disabled="settingFormData.enableDHCP"
-              :ip-val="settingFormData.gateWayAddress"
-              @update:ip-val="(updateVal) => (settingFormData.gateWayAddress = updateVal)"
-            />
-          </el-form-item>
-          <!-- 物理地址 -->
-          <el-form-item label="Physical Address" class="form-item-custom">
-            <el-input
-              v-model="settingFormData.deviceMac"
-              disabled
-              placeholder="MAC地址"
-              class="mac-input"
-            />
-          </el-form-item>
-
-          <!-- 操作按钮 -->
-          <div class="button-group">
-            <el-button
-              :loading="loading"
-              size="large"
-              type="primary"
-              @click="handleSave()"
-              class="save-button"
-            >
-              <span v-if="!loading">Save</span>
-              <span v-else>Saving...</span>
-            </el-button>
-          </div>
-        </el-form>
+      <div class="dhcp-tip">
+        <el-icon class="tip-icon"><InfoFilled /></el-icon>
+        <span>The following fields will be disabled when automatic configuration is enabled.</span>
       </div>
     </div>
+    <el-divider class="short-divider" />
+
+    <div class="section">
+      <div class="section-title">Network Parameters</div>
+      <el-form
+        ref="settingFormRef"
+        :hide-required-asterisk="true"
+        label-position="left"
+        label-width="140px"
+        :model="settingFormData"
+        :rules="settingFormRules"
+        @keyup.enter="handleSave"
+      >
+        <el-form-item label="IP Version" class="form-item-custom">
+          <el-input v-model="IP_version" disabled style="width: 240px;" />
+        </el-form-item>
+
+        <el-form-item label="IP Address" prop="IP" class="form-item-custom">
+          <IPInputBox
+            :disabled="settingFormData.enableDHCP"
+            :ip-val="settingFormData.ipAddress"
+            @update:ip-val="(updateVal) => (settingFormData.ipAddress = updateVal)"
+          />
+        </el-form-item>
+
+        <el-form-item label="Subnet Mask" prop="mask" class="form-item-custom">
+          <IPInputBox
+            :disabled="settingFormData.enableDHCP"
+            :ip-val="settingFormData.subNetAddress"
+            @update:ip-val="(updateVal) => (settingFormData.subNetAddress = updateVal)"
+          />
+        </el-form-item>
+
+        <el-form-item label="Default Gateway" prop="gateway" class="form-item-custom">
+          <IPInputBox
+            :disabled="settingFormData.enableDHCP"
+            :ip-val="settingFormData.gateWayAddress"
+            @update:ip-val="(updateVal) => (settingFormData.gateWayAddress = updateVal)"
+          />
+        </el-form-item>
+
+        <el-form-item label="IPv4 DNS" prop="firstDNSAddress" class="form-item-custom">
+          <IPInputBox
+            :disabled="settingFormData.enableDHCP"
+            :ip-val="settingFormData.firstDNSAddress"
+            @update:ip-val="(updateVal) => (settingFormData.firstDNSAddress = updateVal)"
+          />
+        </el-form-item>
+
+        <el-form-item label="Physical Address" class="form-item-custom">
+          <el-input
+            v-model="settingFormData.deviceMac"
+            disabled
+            placeholder="MAC address"
+            style="width: 240px;"
+          />
+        </el-form-item>
+      </el-form>
+    </div>
+
+    <el-button type="primary" round :loading="loading" @click="handleSave">
+      <span v-if="!loading">Save</span>
+      <span v-else>Saving...</span>
+    </el-button>
   </div>
 </template>
 
@@ -98,20 +92,17 @@ import {
   ElMessage,
   ElMessageBox
 } from 'element-plus'
+import { InfoFilled } from '@element-plus/icons-vue'
 import { getUserSettingApi, putUserSettingApi } from '@/api/setting'
 import { type UpdateSettingRequestData } from '@/api/types/setting'
-import IPInputBox from './components/IPInputBox.vue'
-import {useUserStore} from "@/stores/modules/user";
-import {useRouter} from "vue-router";
+import IPInputBox from '../IPInputBox.vue'
+import { useUserStore } from "@/stores/modules/user"
+import { useRouter } from "vue-router"
+
 const router = useRouter()
 
 const IP_version = ref('IPV4')
-const options = [
-  {
-    value: 'IPV4',
-    label: 'IPV4'
-  }
-]
+
 /** 按钮 Loading */
 const loading = ref(false)
 /** 表单数据 */
@@ -121,6 +112,8 @@ const settingFormData: UpdateSettingRequestData = reactive({
   ipAddress: '',
   subNetAddress: '',
   gateWayAddress: '',
+  firstDNSAddress: '',
+  secondDNSAddress: '',
   deviceMac: ''
 })
 
@@ -133,7 +126,6 @@ const settingFormRules: FormRules = {
     {
       validator: (rule, value, callback) => {
         if (settingFormData.ipAddress) {
-          // 添加网段验证
           if (settingFormData.gateWayAddress && settingFormData.subNetAddress) {
             if (!isSameSubnet(settingFormData.ipAddress, settingFormData.gateWayAddress, settingFormData.subNetAddress)) {
               callback(new Error('IP address and gateway address are not in the same subnet'))
@@ -164,7 +156,6 @@ const settingFormRules: FormRules = {
     {
       validator: (rule, value, callback) => {
         if (settingFormData.gateWayAddress) {
-          // 添加网段验证
           if (settingFormData.ipAddress && settingFormData.subNetAddress) {
             if (!isSameSubnet(settingFormData.ipAddress, settingFormData.gateWayAddress, settingFormData.subNetAddress)) {
               callback(new Error('IP address and gateway address are not in the same subnet'))
@@ -178,36 +169,34 @@ const settingFormRules: FormRules = {
       },
       trigger: 'blur'
     }
-  ]
+  ],
+  firstDNSAddress: [
+    {
+      validator: (rule, value, callback) => {
+        if (settingFormData.firstDNSAddress) {
+          callback()
+        } else {
+          callback(new Error('Please enter the IPv4 DNS'))
+        }
+      },
+      trigger: 'blur'
+    }
+  ],
 }
 
 // 检查IP地址和网关地址是否在同一个网段
 const isSameSubnet = (ip: string, gateway: string, subnetMask: string): boolean => {
-  if (!ip || !gateway || !subnetMask) {
-    return true // 如果任一字段为空,不进行验证
-  }
-
+  if (!ip || !gateway || !subnetMask) return true
   try {
     const ipParts = ip.split('.').map(Number)
     const gatewayParts = gateway.split('.').map(Number)
     const maskParts = subnetMask.split('.').map(Number)
-
-    // 验证IP地址格式
-    if (ipParts.length !== 4 || gatewayParts.length !== 4 || maskParts.length !== 4) {
-      return false
-    }
-
-    // 计算网络地址
+    if (ipParts.length !== 4 || gatewayParts.length !== 4 || maskParts.length !== 4) return false
     const ipNetwork = ipParts.map((part, index) => part & maskParts[index])
     const gatewayNetwork = gatewayParts.map((part, index) => part & maskParts[index])
-
-    // 比较网络地址是否相同
     for (let i = 0; i < 4; i++) {
-      if (ipNetwork[i] !== gatewayNetwork[i]) {
-        return false
-      }
+      if (ipNetwork[i] !== gatewayNetwork[i]) return false
     }
-
     return true
   } catch (error) {
     console.error('网段验证错误:', error)
@@ -215,15 +204,14 @@ const isSameSubnet = (ip: string, gateway: string, subnetMask: string): boolean
   }
 }
 
-
 const switchChange = ($event: number) => {
   if ($event === 1) {
     settingFormRef.value?.clearValidate()
   }
 }
-const param = {
-  NIC: 1
-}
+
+const param = { NIC: 1 }
+
 /** 获取表单数据 */
 const fetchData = () => {
   getUserSettingApi(param.NIC).then((res) => {
@@ -240,193 +228,88 @@ fetchData()
 
 /** 保存逻辑 */
 const handleSave = () => {
-    settingFormRef.value?.validate((valid: boolean, fields) => {
-      if (valid) {
-        ElMessageBox.confirm('Changing the network configuration will restart the device', 'Note', {
-          confirmButtonText: 'OK',
-          cancelButtonText: 'Cancel',
-          type: 'warning',
-          confirmButtonClass: 'el-button--danger'
-        }).then(() => {
-          loading.value = true
-          putUserSettingApi(param.NIC, settingFormData)
-            .then(() => {
-              ElMessage.success('Operation successful. Please wait a moment...')
-              useUserStore().logout()
-              router.push('/login')
-            })
-            .finally(() => {
-              loading.value = false
-            })
-        })
-      }
-    })
+  settingFormRef.value?.validate((valid: boolean, fields) => {
+    if (valid) {
+      ElMessageBox.confirm('Changing the network configuration will restart the device', 'Note', {
+        confirmButtonText: 'OK',
+        cancelButtonText: 'Cancel',
+        type: 'warning',
+        confirmButtonClass: 'el-button--danger'
+      }).then(() => {
+        loading.value = true
+        putUserSettingApi(param.NIC, settingFormData)
+          .then(() => {
+            ElMessage.success('Operation successful. Please wait a moment...')
+            useUserStore().logout()
+            router.push('/login')
+          })
+          .finally(() => {
+            loading.value = false
+          })
+      })
+    }
+  })
 }
 </script>
 
-<style lang="scss" scoped>
-.setting-container {
+<style scoped lang="scss">
+.ip-settings {
   padding: 20px;
-  //background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-  min-height: 85vh;
-  display: flex;
-  //align-items: flex-start;
-  justify-content: center;
-  align-items: center;
-}
-
-.setting-card {
-  width: 100%;
-  max-width: 510px;
-  background: white;
-  border-radius: 12px;
-  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
-  overflow: hidden;
-  transition: all 0.3s ease;
-
-  &:hover {
-    box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12);
-  }
-}
 
-.card-header {
-  color: #000104;
-  padding: 20px 20px 0 20px;
-  text-align: center;
-
-  .title {
-    margin: 0;
-    font-size: 24px;
-    font-weight: 600;
-    margin-bottom: 8px;
+  .el-button{
+    margin-top: 30px;
+    width: 80px;
   }
 
-  .subtitle {
-    margin: 0;
-    font-size: 14px;
-    opacity: 0.9;
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
   }
-}
 
-.content {
-  padding: 30px;
-}
+  .section {
+    margin: 16px 0;
 
-.dhcp-section {
-  background: #f0f9ff;
-  border-left: 4px solid #409eff;
-  padding: 16px;
-  border-radius: 6px;
-  margin-bottom: 24px;
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+  }
 
-  .dhcp-header {
+  .dhcp-row {
     display: flex;
-    justify-content: space-between;
     align-items: center;
     margin-bottom: 8px;
 
-    .dhcp-label {
+    .label {
       font-size: 14px;
-      font-weight: 500;
       color: #333;
+      flex-shrink: 0;
     }
   }
 
   .dhcp-tip {
-    margin: 0;
-    font-size: 12px;
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
     color: #909399;
-  }
-}
-
-.form-item-custom {
-  margin-bottom: 20px;
-
-  :deep(.el-form-item__label) {
-    color: #606266;
-    font-weight: 500;
-    padding-right: 12px;
-  }
-}
-
-.mac-input {
-  :deep(.el-input__wrapper) {
-    background-color: #f5f7fa !important;
-  }
-}
 
-.button-group {
-  display: flex;
-  gap: 12px;
-  margin-top: 32px;
-  padding-top: 20px;
-  border-top: 1px solid #ebeef5;
-}
-
-.save-button {
-  flex: 1;
-  height: 40px;
-  font-size: 16px;
-  font-weight: 500;
-  border-radius: 6px;
-  transition: all 0.3s ease;
-
-  &:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 6px 16px rgba(64, 158, 255, 0.4);
-  }
-
-  &:active {
-    transform: translateY(0);
-  }
-}
-
-// 响应式设计
-@media (max-width: 600px) {
-  .setting-container {
-    padding: 12px;
-  }
-
-  .setting-card {
-    max-width: 100%;
-  }
-
-  .content {
-    padding: 20px;
-  }
-
-  .card-header {
-    padding: 20px 16px;
-
-    .title {
-      font-size: 20px;
-    }
-
-    .subtitle {
-      font-size: 12px;
+    .tip-icon {
+      font-size: 15px;
+      color: #909399;
     }
   }
-}
-
-// 深色主题支持(可选)
-@media (prefers-color-scheme: dark) {
-  .setting-card {
-    background: #1e1e1e;
-  }
-
-  .content {
-    color: #e0e0e0;
-  }
 
   .form-item-custom {
-    :deep(.el-form-item__label) {
-      color: #b0b0b0;
-    }
+    margin-bottom: 18px;
 
-    :deep(.el-input__wrapper) {
-      background-color: #2a2a2a;
-      border-color: #404040;
+    :deep(.el-form-item__label) {
+      color: #606266;
+      font-weight: 500;
     }
   }
 }
-</style>
+
+</style>

+ 155 - 0
src/views/settings/netSettings/components/networkDiagnostics/index.vue

@@ -0,0 +1,155 @@
+<template>
+  <div class="network-diagnostics">
+    <div class="section">
+      <div class="section-title">Diagnostic Parameters</div>
+      <div class="input-row">
+        <span class="label">Address or Domain Name:</span>
+        <el-input
+          v-model="DomainName"
+          placeholder="Please enter an address or domain name"
+          clearable
+          style="width: 280px;"
+          @keyup.enter="handlePing"
+        />
+        <el-button
+          type="primary"
+          round
+          :loading="loading"
+          @click="handlePing"
+        >
+          Send
+        </el-button>
+      </div>
+    </div>
+    <el-divider class="short-divider" />
+
+    <div class="section">
+      <div class="section-title">Diagnostic Results</div>
+      <div v-if="!result" class="empty-tip">
+        <el-icon class="tip-icon"><InfoFilled /></el-icon>
+        <span>Enter an address or domain name above and click Send to start diagnosis.</span>
+      </div>
+      <div v-else class="result-box">
+        <pre class="result-text">{{ result }}</pre>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { InfoFilled } from '@element-plus/icons-vue'
+import { networkDiagnostics } from '@/api/setting'
+
+const DomainName = ref('')
+const result = ref('')
+const loading = ref(false)
+
+/** 校验 IPv4 地址 */
+function isValidIP(value: string): boolean {
+  const parts = value.split('.')
+  if (parts.length !== 4) return false
+  return parts.every((p) => {
+    const num = Number(p)
+    return /^\d{1,3}$/.test(p) && num >= 0 && num <= 255
+  })
+}
+
+/** 校验域名 */
+function isValidDomain(value: string): boolean {
+  return /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/.test(value)
+}
+
+/** 执行网络诊断 */
+const handlePing = async () => {
+  const host = DomainName.value.trim()
+  if (!host) {
+    ElMessage.warning('Please enter an address or domain name')
+    return
+  }
+  if (!isValidIP(host) && !isValidDomain(host)) {
+    ElMessage.warning('Please enter a valid IP address or domain name')
+    return
+  }
+  loading.value = true
+  result.value = ''
+  try {
+    // const res = await networkDiagnostics({ DomainName: host })
+    // result.value = res.data?.NetworkStatus || ''
+    result.value = "PINGwww.baidu.com(183.240.99.224):56databytes\n64bytesfrom183.240.99.224:seq=0ttl=53time=12.822ms\n64bytesfrom183.240.99.224:seq=1ttl=53time=13.056ms\n64bytesfrom183.240.99.224:seq=2ttl=53time=12.501ms\n64bytesfrom183.240.99.224:seq=3ttl=53time=12.116ms\n64bytesfrom183.240.99.224:seq=4ttl=53time=12.488ms\n\n---www.baidu.compingstatistics---\n5packetstransmitted,5packetsreceived,00x62eacketloss\nround-tripmin/avg/max=12.116/12.596/13.056ms\n"
+  } catch {
+    ElMessage.error('Diagnosis failed. Please check your network connection.')
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.network-diagnostics {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+  }
+
+  .input-row {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+  }
+
+  .empty-tip {
+    display: flex;
+    align-items: center;
+    gap: 6px;
+    font-size: 13px;
+    color: #909399;
+
+    .tip-icon {
+      font-size: 15px;
+      color: #909399;
+    }
+  }
+
+  .result-box {
+    background: #f9fafb;
+    border: 1px solid #ebeef5;
+    border-radius: 8px;
+    padding: 16px 20px;
+    max-width: 700px;
+    min-height: 120px;
+    max-height: 400px;
+    overflow-y: auto;
+  }
+
+  .result-text {
+    margin: 0;
+    font-family: 'Courier New', Courier, monospace;
+    font-size: 13px;
+    line-height: 1.8;
+    color: #333;
+    white-space: pre-wrap;
+    word-break: break-all;
+  }
+}
+</style>

+ 53 - 5
src/views/settings/netSettings/index.vue

@@ -2,13 +2,23 @@
   <div class="settings-container">
     <el-tabs v-model="activeName" class="demo-tabs">
       <el-tab-pane label="IP" name="first">
-
+        <IP/>
       </el-tab-pane>
       <el-tab-pane label="Ports" name="second">
-
+        <div class="port-settings">
+          <div class="section">
+            <div class="section-title">Port Settings</div>
+            <div class="port-row">
+              <span class="label">Port:</span>
+              <el-input v-model="Port" placeholder="Enter port number" style="width: 220px;" />
+            </div>
+          </div>
+          <el-divider class="short-divider" />
+          <el-button type="primary" round @click="handleSave">Save</el-button>
+        </div>
       </el-tab-pane>
-      <el-tab-pane label="Network Diagnostics " name="third">
-
+      <el-tab-pane label="Network Diagnostics" name="third">
+          <NetworkDiagnostics />
       </el-tab-pane>
     </el-tabs>
   </div>
@@ -16,16 +26,54 @@
 
 <script setup lang="ts">
 import {ref} from "vue";
+import IP from './components/IP/index.vue'
+import NetworkDiagnostics from './components/networkDiagnostics/index.vue'
 
 const activeName = ref('first')
+const Port = ref('')
+
+function handleSave(){
+
+}
 
 </script>
 
 
 <style scoped lang="scss">
-.settings-container{
+.settings-container {
   display: flex;
   flex-direction: column;
   padding: 20px;
 }
+
+.port-settings {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 16px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 12px;
+    }
+  }
+
+  .port-row {
+    display: flex;
+    align-items: center;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+  }
+}
 </style>

+ 0 - 188
src/views/settings/systemSettings/components/alarm/index.vue

@@ -1,188 +0,0 @@
-<template>
-  <div class="alarm-container">
-    <div class="alarm-header">
-      <span class="alarm-title">Manual Alarm Control</span>
-    </div>
-    <div class="alarm-buttons">
-      <el-button
-        type="primary"
-        size="large"
-        @click="handleAlarm"
-        class="btn-alarm"
-        :loading="loading"
-      >
-        Enable Manual Alarm
-      </el-button>
-      <el-button
-        type="info"
-        size="large"
-        @click="handleCloseAlarm"
-        class="btn-close"
-        :loading="DisableLoading"
-      >
-        Disable Manual Alarm
-      </el-button>
-    </div>
-    <div class="alarm-tip">
-      <span class="tip-icon">ℹ️</span>
-      <span>Alarm will remain active for 1 minute after activation.</span>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import { ElMessage } from 'element-plus'
-import { cameraAlarm } from '@/api/setting'
-
-const loading = ref(false)
-const DisableLoading = ref(false)
-
-async function handleCloseAlarm() {
-  if (loading.value) return
-  DisableLoading.value = true
-  try {
-    const res = await cameraAlarm({
-      AlarmSettings: 0
-    })
-    if (res.data === 'ok\n') {
-      ElMessage.success('Manual Alarm Deactivated Successfully')
-    } else {
-      ElMessage.warning('Manual Alarm Deactivation Error')
-    }
-  } catch (error) {
-    ElMessage.error('Manual Alarm Deactivation Error')
-  } finally {
-    DisableLoading.value = false
-  }
-}
-
-async function handleAlarm() {
-  if (loading.value) return
-  loading.value = true
-  try {
-    const res = await cameraAlarm({
-      AlarmSettings: 1
-    })
-    if (res.data === 'ok\n') {
-      ElMessage.success('Manual alarm activated successfully. It will remain active for 1 minute.')
-    } else {
-      ElMessage.warning('Manual Alarm Activation Error')
-    }
-  } catch (error) {
-    ElMessage.error('Failed to Activate Manual Alarm.')
-  } finally {
-    loading.value = false
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.alarm-container {
-  background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-  border-radius: 12px;
-  padding: 24px;
-  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
-  border-left: 4px solid #ff6b6b;
-  transition: all 0.3s ease;
-
-  &:hover {
-    box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
-    transform: translateY(-2px);
-  }
-}
-
-.alarm-header {
-  margin-bottom: 20px;
-  padding-bottom: 12px;
-  border-bottom: 2px solid rgba(255, 107, 107, 0.2);
-}
-
-.alarm-title {
-  font-size: 16px;
-  font-weight: 600;
-  color: #333;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-
-  &::before {
-    content: '';
-    width: 4px;
-    height: 4px;
-    background: #ff6b6b;
-    border-radius: 50%;
-    animation: pulse 2s ease-in-out infinite;
-  }
-}
-
-.alarm-buttons {
-  display: flex;
-  gap: 16px;
-  margin-bottom: 16px;
-  flex-wrap: wrap;
-
-  @media (max-width: 600px) {
-    flex-direction: column;
-    gap: 12px;
-  }
-}
-
-.btn-alarm,
-.btn-close {
-  flex: 1;
-  min-width: 160px;
-  height: 44px;
-  font-weight: 500;
-  letter-spacing: 0.5px;
-  border-radius: 8px;
-  transition: all 0.3s ease;
-  border: none;
-  font-size: 14px;
-
-  @media (max-width: 600px) {
-    width: 100%;
-  }
-
-  &:hover:not(:disabled) {
-    transform: translateY(-2px);
-    box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
-  }
-
-  &:active:not(:disabled) {
-    transform: translateY(0);
-  }
-}
-
-.btn-icon {
-  margin-right: 6px;
-  font-size: 16px;
-}
-
-.alarm-tip {
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  padding: 12px;
-  background: rgba(255, 193, 7, 0.1);
-  border-radius: 6px;
-  border-left: 3px solid #ffc107;
-  font-size: 13px;
-  color: #666;
-  line-height: 1.5;
-}
-
-.tip-icon {
-  font-size: 16px;
-  flex-shrink: 0;
-}
-
-@keyframes pulse {
-  0%, 100% {
-    opacity: 1;
-  }
-  50% {
-    opacity: 0.5;
-  }
-}
-</style>

+ 15 - 1
src/views/settings/systemSettings/components/cameraInfo/index.vue

@@ -53,7 +53,16 @@ const formRef = ref<FormInstance | null>(null)
 onMounted(() => {
   deviceInfo()
 })
-const formData = ref({})
+
+/** 相机设备信息 */
+export interface CameraDeviceInfo {
+  DeviceName: string
+  SoftwareVer: string
+  HardwareVer: string
+  MacAddr: string
+}
+
+const formData = ref<CameraDeviceInfo>({})
 const loading = ref(false)
 
 const deviceInfo = () => {
@@ -72,4 +81,9 @@ const deviceInfo = () => {
 .el-input {
   width: 210px;
 }
+
+/* 加深表单label颜色 */
+:deep(.el-form-item__label) {
+  color: #1a1a1a; /* 更深的颜色值 */
+}
 </style>

+ 0 - 91
src/views/settings/systemSettings/components/nightVision/index.vue

@@ -1,91 +0,0 @@
-<template>
-  <div class="night-mode">
-    <div class="night-mode-content">
-      <span class="night-mode-label">Night Vision Mode</span>
-      <el-select v-model="NightMode"  style="width: 220px">
-        <el-option
-          v-for="item in NightModeOptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
-        />
-      </el-select>
-    </div>
-    <el-button
-      type="primary"
-      @click="handleNightMode"
-      style="width: 60px"
-    >
-      Save
-    </el-button>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref } from 'vue'
-import { ElMessage } from 'element-plus'
-import { getCameraNightMode, cameraNightMode, getCameraAlarm } from '@/api/setting'
-
-const NightMode = ref(0)
-const NightModeOptions = [
-  {
-    value: 0,
-    label: 'Full Color Night Vision',
-  },
-  {
-    value: 1,
-    label: 'Black & White Night Vision',
-  },
-  {
-    value: 2,
-    label: 'Smart Night Vision',
-  }
-]
-
-async function getNightMode() {
-  const res = await getCameraNightMode()
-  if(res.data) {
-    NightMode.value = Number(res.data.NightMode)
-  }
-}
-
-onMounted( () => {
-  getNightMode()
-})
-
-async function handleNightMode() {
-  const res =await cameraNightMode({
-    NightMode: NightMode.value
-  })
-  if(res.data === 'ok\n'){
-    ElMessage.success('Operation Successful')
-  }else{
-    ElMessage.warning('Operation Failed')
-  }
-}
-</script>
-
-<style scoped lang="scss">
-.night-mode {
-  display: flex;
-  gap: 20px;
-  flex-direction: column;
-  padding: 10px;
-  background: #f5f7fa;
-  border-radius: 8px;
-
-  &-content {
-    display: flex;
-    align-items: center;
-  }
-
-  &-label {
-    font-size: 15px;
-    font-weight: 500;
-    color: #303133;
-    min-width: 80px;
-    margin-right: 20px;
-  }
-
-}
-</style>

+ 371 - 0
src/views/settings/systemSettings/components/systemMaintenance/index.vue

@@ -0,0 +1,371 @@
+<template>
+  <div class="motion-detection">
+    <!-- 定时重启 -->
+    <div class="section">
+      <div class="section-title">Scheduled Restart</div>
+      <div class="setting-row">
+        <span class="label">Enable scheduled restart:</span>
+        <el-switch v-model="TmRstEnable" @change="onEnableChange" />
+      </div>
+      <transition name="slide">
+        <div class="setting-row">
+          <span class="label">Restart time:</span>
+          <el-time-picker
+            v-model="timeValue"
+            placeholder="Select time"
+            format="HH:mm"
+            style="width: 140px"
+          />
+        </div>
+      </transition>
+    </div>
+
+    <el-divider class="short-divider" />
+
+    <!-- 重复计划 -->
+    <transition name="slide">
+      <div class="section">
+        <div class="section-title">Repeat Schedule <span class="required-mark">*</span></div>
+        <div class="days-container">
+          <button
+            v-for="day in weekDays"
+            :key="day.value"
+            :class="['day-btn', { active: selectedDays.includes(day.value) }]"
+            @click="toggleDay(day.value)"
+          >
+            {{ day.label }}
+          </button>
+        </div>
+        <p v-if="showDaysError" class="error-text">Please select at least one day</p>
+        <el-divider class="short-divider" />
+      </div>
+    </transition>
+
+    <!-- 设备操作 -->
+    <div class="section">
+      <div class="section-title">Device Actions</div>
+      <div class="alarm-actions">
+        <el-button class="btn-restart" type="warning" round @click="handleAction('restart')">
+          Restart Now
+        </el-button>
+        <el-button class="btn-restore" type="danger" round @click="handleAction('reset')">
+          Restore Factory
+        </el-button>
+      </div>
+    </div>
+
+    <el-divider class="short-divider" />
+
+    <el-button class="btn-save" type="primary" round  @click="Save" :loading="loading" style="width: 80px">Save</el-button>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import {
+  ElLoading,
+  ElMessage,
+  ElMessageBox
+} from 'element-plus'
+import { cameraReset, cameraResetGet } from '@/api/setting'
+import { useUserStore } from '@/stores/modules/user'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const loading = ref(false)
+const timeValue = ref<Date | string>('')
+const TmRstEnable = ref(false)
+const selectedDays = ref<number[]>([])
+const showDaysError = ref(false)
+
+/**
+ * 将二进制整型转换为选中的星期数组
+ * 例如:30 -> 011110(二进制) -> [1,2,3,4] (Mon~Thu)
+ */
+const weekResetToDays = (weekReset: number): number[] => {
+  const days: number[] = []
+  for (let i = 0; i < 7; i++) {
+    if (weekReset & (1 << i)) days.push(i)
+  }
+  return days
+}
+
+// 页面加载时获取定时重启配置
+const fetchData = async () => {
+  try {
+    const resp = await cameraResetGet() as any
+    const data = resp.data
+    if (data) {
+      TmRstEnable.value = data.TmRstEnable === 1
+      if (data.TimeReset) {
+        const date = new Date()
+        date.setHours(data.TimeReset.tm_hour, data.TimeReset.tm_min, 0, 0)
+        timeValue.value = date
+      }
+      if (data.WeekReset !== undefined) {
+        selectedDays.value = weekResetToDays(data.WeekReset)
+      }
+    }
+  } catch {
+    ElMessage.error('Failed to load scheduled restart settings')
+  }
+}
+
+onMounted(() => {
+  fetchData()
+})
+
+// 开启定时重启时,默认时间设为 00:00
+const onEnableChange = (val: boolean) => {
+  if (val) {
+    const defaultTime = new Date()
+    defaultTime.setHours(0, 0, 0, 0)
+    timeValue.value = defaultTime
+    selectedDays.value = []
+    showDaysError.value = false
+  }
+}
+
+const weekDays = [
+  { label: 'Sun', value: 0 },
+  { label: 'Mon', value: 1 },
+  { label: 'Tue', value: 2 },
+  { label: 'Wed', value: 3 },
+  { label: 'Thu', value: 4 },
+  { label: 'Fri', value: 5 },
+  { label: 'Sat', value: 6 }
+]
+
+const toggleDay = (dayValue: number) => {
+  const idx = selectedDays.value.indexOf(dayValue)
+  if (idx > -1) selectedDays.value.splice(idx, 1)
+  else selectedDays.value.push(dayValue)
+  // 选择后清除错误提示
+  if (selectedDays.value.length > 0) showDaysError.value = false
+}
+
+const executeAction = async (params: object, message: string) => {
+  const resp = await cameraReset(params) as any
+  if (resp.data === 'ok\n') {
+    ElMessage.success(message)
+    useUserStore().logout()
+    await router.push('/login')
+  } else {
+    throw new Error('Operation failed')
+  }
+}
+
+const handleAction = async (type: 'restart' | 'reset') => {
+  const isReset = type === 'reset'
+  const confirmMsg = isReset
+    ? 'This will reset device to factory settings and restart. All settings will be lost. Continue?'
+    : 'This will restart the camera. Continue?'
+  const confirmTitle = isReset ? 'Confirm Factory Reset' : 'Confirm Restart'
+
+  try {
+    await ElMessageBox.confirm(confirmMsg, confirmTitle, {
+      confirmButtonText: 'Yes',
+      cancelButtonText: 'Cancel',
+      type: 'warning'
+    })
+
+    const loader = ElLoading.service()
+    try {
+      await executeAction(
+        { [isReset ? 'reset' : 'reboot']: 1 },
+        isReset
+          ? 'Factory reset initiated. Please wait...'
+          : 'Camera restarted successfully. Please wait...'
+      )
+    } finally {
+      loader.close()
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      ElMessage.error('Operation failed')
+    }
+  }
+}
+
+/**
+ * 将选中的星期数组转换为二进制整型
+ * 例如:选中 Sun(0), Mon(1), Tue(2)... Sat(6) 全选 -> 1111111(二进制) -> 127
+ * 每一位对应一天,bit0=Sun, bit1=Mon, ..., bit6=Sat
+ */
+const daysToWeekReset = (days: number[]): number => {
+  return days.reduce((acc, day) => acc | (1 << day), 0)
+}
+
+const Save = async () => {
+  // 开启定时重启时,校验 Repeat Schedule 必填
+  if (TmRstEnable.value && selectedDays.value.length === 0) {
+    showDaysError.value = true
+    ElMessage.warning('Please select at least one day for repeat schedule')
+    return
+  }
+
+  loading.value = true
+  try {
+    const data: Record<string, any> = {
+      TmRstEnable: TmRstEnable.value ? 1 : 0
+    }
+
+    if (TmRstEnable.value && timeValue.value) {
+      const date = new Date(timeValue.value as string | Date)
+      data.TimeReset = {
+        tm_hour: date.getHours(),
+        tm_min: date.getMinutes()
+      }
+      data.WeekReset = daysToWeekReset(selectedDays.value)
+    }
+
+    const resp = await cameraReset(data) as any
+    if (resp.data === 'ok\n') {
+      ElMessage.success('Save successfully')
+    } else {
+      throw new Error('Save failed')
+    }
+  } catch {
+    ElMessage.error('Save failed')
+  } finally {
+    loading.value = false
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.motion-detection {
+  padding: 20px;
+
+  .short-divider {
+    max-width: 700px;
+    margin: 16px 0;
+  }
+
+  .section {
+    margin: 20px 0;
+
+    .section-title {
+      font-size: 14px;
+      font-weight: 500;
+      color: #333;
+      margin-bottom: 14px;
+    }
+  }
+
+  .setting-row {
+    display: flex;
+    align-items: center;
+    margin-bottom: 12px;
+
+    .label {
+      font-size: 14px;
+      color: #333;
+      flex-shrink: 0;
+    }
+  }
+
+  .days-container {
+    display: flex;
+    gap: 8px;
+  }
+
+  .day-btn {
+    padding: 6px 14px;
+    border: 1px solid #dcdfe6;
+    background: #fff;
+    border-radius: 4px;
+    font-size: 13px;
+    cursor: pointer;
+    transition: all 0.2s;
+
+    &:hover {
+      border-color: #409eff;
+      color: #409eff;
+    }
+
+    &.active {
+      background: #409eff;
+      color: #fff;
+      border-color: #409eff;
+    }
+  }
+
+  .alarm-actions {
+    display: flex;
+    gap: 16px;
+  }
+
+  .required-mark {
+    color: #f56c6c;
+    margin-left: 2px;
+  }
+
+  .error-text {
+    color: #f56c6c;
+    font-size: 12px;
+    margin: 8px 0 0;
+  }
+}
+
+/* Save 按钮 - 主操作,与侧边栏蓝色系统一 */
+.btn-save {
+  background-color: #3B82F6;
+  color: #fff;
+  border: none;
+  padding: 8px 20px;
+  border-radius: 6px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus {
+    background-color: #2563EB;
+  }
+}
+
+/* Restart Now - 中等风险操作,柔和琥珀色 */
+.btn-restart {
+  background-color: #FEF3C7;
+  color: #B45309;
+  border: 1px solid #FCD34D;
+  padding: 8px 20px;
+  border-radius: 6px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus {
+    background-color: #FDE68A;
+    border-color: #F59E0B;
+    color: #92400E;
+  }
+}
+
+/* Restore Factory - 高风险操作,柔和红色 */
+.btn-restore {
+  background-color: #FEE2E2;
+  color: #DC2626;
+  border: 1px solid #FCA5A5;
+  padding: 8px 20px;
+  border-radius: 6px;
+  font-weight: 500;
+
+  &:hover,
+  &:focus {
+    background-color: #FECACA;
+    border-color: #EF4444;
+    color: #B91C1C;
+  }
+}
+
+
+.slide-enter-active,
+.slide-leave-active {
+  transition: all 0.3s ease;
+}
+
+.slide-enter-from,
+.slide-leave-to {
+  opacity: 0;
+  transform: translateY(-10px);
+}
+</style>

+ 363 - 0
src/views/settings/systemSettings/components/systemUpgrade/index.vue

@@ -0,0 +1,363 @@
+<template>
+  <div class="upgrade-container">
+    <div class="upgrade-header">
+      <h2>Device Upgrade</h2>
+      <p class="subtitle">Select and upload the upgrade file to update your device</p>
+    </div>
+
+    <el-form ref="formRef" :model="formData" class="upgrade-form">
+      <div class="upload-section">
+        <el-upload
+          class="file-upload"
+          action="#"
+          :auto-upload="false"
+          :show-file-list="false"
+          :on-change="handleFileChange"
+          drag
+        >
+          <div class="upload-content">
+            <div class="upload-icon">📦</div>
+            <div class="upload-text">
+              <p class="text-primary">Click or drag file here</p>
+              <p class="text-secondary">Supported file format: .bin, .img, .zip</p>
+            </div>
+          </div>
+        </el-upload>
+
+        <div v-if="selectedFile" class="file-info">
+          <div class="file-item">
+            <span class="file-icon">✓</span>
+            <div class="file-details">
+              <p class="file-name">{{ selectedFile.name }}</p>
+              <p class="file-size">{{ formatFileSize(selectedFile.size) }}</p>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <div class="button-group">
+        <el-button
+          type="primary"
+          size="large"
+          round
+          @click="handleUpgrade"
+          :loading="loading"
+          :disabled="!selectedFile"
+          class="upgrade-btn"
+        >
+          {{ loading ? 'Upgrading...' : 'Start Upgrade' }}
+        </el-button>
+        <el-button
+          v-if="selectedFile"
+          size="large"
+          round
+          @click="handleClearFile"
+          :disabled="loading"
+          class="clear-btn"
+        >
+          Clear File
+        </el-button>
+      </div>
+
+      <div class="warning-box">
+        <span class="warning-icon">⚠️</span>
+        <span class="warning-text">
+          The device will restart during the upgrade process. Please ensure stable power supply.
+          This may take several minutes to complete.
+        </span>
+      </div>
+    </el-form>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, reactive } from 'vue'
+import {
+  ElLoading,
+  ElMessage,
+  ElMessageBox,
+  type FormInstance
+} from 'element-plus'
+import { useUserStore } from '@/stores/modules/user'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const loading = ref(false)
+const formRef = ref<FormInstance | null>(null)
+const formData = reactive({})
+const selectedFile = ref<File | null>(null)
+
+const formatFileSize = (bytes: number): string => {
+  if (bytes === 0) return '0 Bytes'
+  const k = 1024
+  const sizes = ['Bytes', 'KB', 'MB', 'GB']
+  const i = Math.floor(Math.log(bytes) / Math.log(k))
+  return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]
+}
+
+const handleFileChange = (file: any) => {
+  selectedFile.value = file.raw
+}
+
+const handleClearFile = () => {
+  selectedFile.value = null
+}
+
+const handleUpgrade = async () => {
+  if (!selectedFile.value) {
+    ElMessage.warning('Please select an upgrade file first')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      'The upgrade operation will restart the device and may take several minutes. Are you sure you want to continue?',
+      'Confirm Upgrade',
+      {
+        confirmButtonText: 'Confirm',
+        cancelButtonText: 'Cancel',
+        type: 'warning',
+        confirmButtonClass: 'el-button--danger'
+      }
+    )
+
+    loading.value = true
+    // Here should call the actual upgrade API
+    // Simulate upgrade process
+    setTimeout(() => {
+      loading.value = false
+      ElMessage.success(
+        'Upgrade successful! The device is restarting, please wait...'
+      )
+      useUserStore().logout()
+      router.push('/login')
+    }, 3000)
+  } catch (error) {
+    console.log(error)
+    if (error !== 'cancel') {
+      ElMessage.error('Upgrade failed')
+    }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.upgrade-container {
+  max-width: 600px;
+  margin: 0 auto;
+  padding: 40px 20px;
+}
+
+.upgrade-header {
+  text-align: center;
+  margin-bottom: 40px;
+
+  h2 {
+    margin: 0 0 8px 0;
+    font-size: 28px;
+    font-weight: 600;
+    color: #1f2937;
+  }
+
+  .subtitle {
+    margin: 0;
+    font-size: 14px;
+    color: #6b7280;
+  }
+}
+
+.upgrade-form {
+  display: flex;
+  flex-direction: column;
+  gap: 24px;
+}
+
+.upload-section {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.file-upload {
+  ::v-deep(.el-upload-dragger) {
+    width: 100%;
+    padding: 40px;
+    border: 2px dashed #d1d5db;
+    border-radius: 8px;
+    background-color: #f9fafb;
+    transition: all 0.3s ease;
+    cursor: pointer;
+
+    &:hover {
+      border-color: #3b82f6;
+      background-color: #eff6ff;
+    }
+
+    &.is-dragover {
+      border-color: #3b82f6;
+      background-color: #eff6ff;
+    }
+  }
+}
+
+.upload-content {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 12px;
+
+  .upload-icon {
+    font-size: 48px;
+  }
+
+  .upload-text {
+    text-align: center;
+
+    p {
+      margin: 4px 0;
+
+      &.text-primary {
+        font-size: 16px;
+        font-weight: 500;
+        color: #1f2937;
+      }
+
+      &.text-secondary {
+        font-size: 13px;
+        color: #9ca3af;
+      }
+    }
+  }
+}
+
+.file-info {
+  padding: 12px 16px;
+  background-color: #f0fdf4;
+  border: 1px solid #dcfce7;
+  border-radius: 6px;
+
+  .file-item {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    .file-icon {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      width: 24px;
+      height: 24px;
+      background-color: #22c55e;
+      color: white;
+      border-radius: 50%;
+      font-size: 14px;
+      flex-shrink: 0;
+    }
+
+    .file-details {
+      flex: 1;
+      min-width: 0;
+
+      p {
+        margin: 0;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+
+        &.file-name {
+          font-size: 14px;
+          font-weight: 500;
+          color: #1f2937;
+          margin-bottom: 2px;
+        }
+
+        &.file-size {
+          font-size: 12px;
+          color: #6b7280;
+        }
+      }
+    }
+  }
+}
+
+.button-group {
+  display: flex;
+  gap: 12px;
+  justify-content: center;
+
+  .upgrade-btn {
+    flex: 1;
+    max-width: 300px;
+    padding: 10px 32px;
+    font-size: 16px;
+    font-weight: 500;
+  }
+
+  .clear-btn {
+    padding: 10px 24px;
+    font-size: 14px;
+  }
+}
+
+.warning-box {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 12px 16px;
+  background-color: #fef3c7;
+  border: 1px solid #fcd34d;
+  border-radius: 6px;
+
+  .warning-icon {
+    font-size: 18px;
+    flex-shrink: 0;
+    margin-top: 2px;
+  }
+
+  .warning-text {
+    font-size: 13px;
+    color: #92400e;
+    line-height: 1.6;
+  }
+}
+
+@media (max-width: 640px) {
+  .upgrade-container {
+    padding: 20px 16px;
+  }
+
+  .upgrade-header {
+    margin-bottom: 32px;
+
+    h2 {
+      font-size: 24px;
+    }
+  }
+
+  .file-upload {
+    ::v-deep(.el-upload-dragger) {
+      padding: 24px;
+    }
+  }
+
+  .upload-content {
+    .upload-icon {
+      font-size: 36px;
+    }
+
+    .upload-text {
+      p.text-primary {
+        font-size: 14px;
+      }
+    }
+  }
+
+  .button-group {
+    flex-direction: column;
+
+    .upgrade-btn {
+      max-width: 100%;
+    }
+  }
+}
+</style>

+ 326 - 133
src/views/settings/systemSettings/components/time/index.vue

@@ -1,18 +1,29 @@
 <template>
-  <div class="time-container">
-    <div class="time-card">
-      <div class="card-header">
-        <h3 class="card-title">Time Settings</h3>
+  <div class="setting-container">
+    <!-- 系统时间 -->
+    <div class="section">
+      <div class="section-title">System Time</div>
+      <div class="form-content">
+        <div class="form-item">
+          <label class="form-label">Current Time</label>
+          <span class="time-value">{{ currentTime }}</span>
+        </div>
+        <div class="form-item">
+          <label class="form-label">Sync to Camera</label>
+          <el-button type="primary" size="small" @click="syncTime" :loading="syncLoading" class="form-btn">
+            Sync Now
+          </el-button>
+        </div>
       </div>
+    </div>
 
-      <div class="card-content">
-        <div class="form-group">
+    <!-- 显示设置 -->
+    <div class="section">
+      <div class="section-title">Display Settings</div>
+      <div class="form-content">
+        <div class="form-item">
           <label class="form-label">Display Mode</label>
-          <el-select
-            v-model="timeMode"
-            class="mode-select"
-            @change="changeTimeMode"
-          >
+          <el-select v-model="timeMode" class="form-control">
             <el-option
               v-for="item in timeModeOptions"
               :key="item.value"
@@ -21,25 +32,55 @@
             />
           </el-select>
         </div>
-
-        <div class="form-group">
-          <label class="form-label">System Time</label>
-          <div class="time-info">
-            <span>{{ currentTime }}</span>
-          </div>
+        <div class="form-item">
+          <label class="form-label">Ignore Time Zone</label>
+          <el-switch v-model="timeZoneEn" />
         </div>
-        <div class="button-group">
-          <el-button
-            type="primary"
-            size="default"
-            @click="syncTime"
-            class="sync-button"
-          >
-            <span>Sync to Camera</span>
-          </el-button>
+      </div>
+    </div>
+
+    <!-- NTP 设置 -->
+    <div class="section">
+      <div class="section-title">NTP Settings</div>
+      <div class="form-content">
+        <div class="form-item">
+          <label class="form-label">Enable Manual NTP</label>
+          <el-switch v-model="timeNTPEn" />
         </div>
+        <transition name="collapse">
+          <div v-if="timeNTPEn" class="ntp-fields">
+            <div class="form-item">
+              <label class="form-label required">NTP Server 1</label>
+              <el-input
+                v-model="timeNTP"
+                placeholder="e.g: pool.ntp.org"
+                class="form-control"
+                clearable
+                @blur="validateNtpServer(timeNTP, 'server1')"
+              />
+              <div v-if="ntpErrors.server1" class="error-message">{{ ntpErrors.server1 }}</div>
+            </div>
+            <div class="form-item">
+              <label class="form-label">NTP Server 2</label>
+              <el-input
+                v-model="timeNTP2"
+                placeholder="e.g: ntp.ubuntu.com"
+                class="form-control"
+                clearable
+                @blur="validateNtpServer(timeNTP2, 'server2')"
+              />
+              <div v-if="ntpErrors.server2" class="error-message">{{ ntpErrors.server2 }}</div>
+            </div>
+          </div>
+        </transition>
       </div>
     </div>
+
+    <!-- 操作按钮 -->
+    <div class="button-group">
+      <el-button type="primary" @click="saveSettings" :loading="saveLoading" class="save-btn">Save</el-button>
+      <el-button @click="resetSettings" class="reset-btn">Reset</el-button>
+    </div>
   </div>
 </template>
 
@@ -50,185 +91,337 @@ import { format } from 'date-fns'
 import { ElMessage } from 'element-plus'
 import type { TimeParaData } from '@/api/types/setting'
 
+// ===== 响应式状态 =====
 const timeMode = ref(1)
 const currentTime = ref('')
 let intervalId: number | null = null
 
+const timeZoneEn = ref(false)
+const timeNTPEn = ref(false)
+const timeNTP = ref('')
+const timeNTP2 = ref('')
+
+const syncLoading = ref(false)
+const saveLoading = ref(false)
+
+const ntpErrors = ref({
+  server1: '',
+  server2: ''
+})
+
+// 初始值存储(用于重置)
+const initialValues = {
+  timeMode: 1,
+  timeZoneEn: false,
+  timeNTPEn: false,
+  timeNTP: '',
+  timeNTP2: ''
+}
+
 const timeModeOptions = [
-  {
-    value: 0,
-    label: '12-Hour Format'
-  },
-  {
-    value: 1,
-    label: '24-Hour Format'
-  }
+  { value: 0, label: '12-Hour Format' },
+  { value: 1, label: '24-Hour Format' }
 ]
 
-function updateTime() {
-  const now = new Date();
-  currentTime.value = timeMode.value === 1
-    ? format(now, 'yyyy-MM-dd HH:mm:ss') // 修改这里以包括年月日
-    : format(now, 'yyyy-MM-dd hh:mm:ss a'); // 和这里
+function resetSettings() {
+  timeMode.value = initialValues.timeMode
+  timeZoneEn.value = initialValues.timeZoneEn
+  timeNTPEn.value = initialValues.timeNTPEn
+  timeNTP.value = initialValues.timeNTP
+  timeNTP2.value = initialValues.timeNTP2
+  ntpErrors.value = { server1: '', server2: '' }
+  ElMessage.info('Settings reset')
 }
 
 async function getTimeMode() {
   try {
     const res = await GetTimePara()
-    timeMode.value = res.data?.timeMode
-    updateTime() // 初始更新
+    timeMode.value = res.data?.timeMode ?? 1
+    timeZoneEn.value = res.data?.timeZoneEn ?? false
+    timeNTPEn.value = res.data?.timeNTPEn ?? false
+    timeNTP.value = res.data?.timeNTP ?? ''
+    timeNTP2.value = res.data?.timeNTP2 ?? ''
+    updateTime()
   } catch (error) {
-    console.error('获取时间模式失败', error)
+    ElMessage.error('Failed to retrieve time settings')
   }
 }
 
+function updateTime() {
+  const now = new Date()
+  currentTime.value =
+    timeMode.value === 1
+      ? format(now, 'yyyy-MM-dd HH:mm:ss')
+      : format(now, 'yyyy-MM-dd hh:mm:ss a')
+}
+
 onMounted(() => {
   getTimeMode()
-  // 每秒更新一次时间
   intervalId = window.setInterval(updateTime, 1000)
 })
 
 onBeforeUnmount(() => {
-  // 组件卸载时清除定时器
   if (intervalId !== null) {
     clearInterval(intervalId)
   }
 })
 
-async function changeTimeMode() {
-  updateTime()
-  const timeParaData: TimeParaData = {
-    timeMode: timeMode.value
-  }
+async function syncTime() {
+  syncLoading.value = true
   try {
+    const timeParaData: TimeParaData = {
+      time: format(new Date(), 'yyyy-MM-dd-HH-mm-ss'),
+      timeMode: timeMode.value
+    }
     const res = await PutTimePara(timeParaData)
     if (res.data === 'ok\n') {
-      ElMessage.success('Update Successful')
+      ElMessage.success('Time synchronization successful')
     } else {
-      ElMessage.error('Update Failed')
+      ElMessage.error('Time synchronization failed')
     }
   } catch (error) {
-    ElMessage.error('Update Failed')
+    ElMessage.error('Time synchronization failed')
+    console.error(error)
+  } finally {
+    syncLoading.value = false
   }
 }
 
-function syncTime() {
-  const timeParaData: TimeParaData = {
-    time: format(new Date(), 'yyyy-MM-dd-HH-mm-ss'),
-    timeMode: timeMode.value
+function validateNtpServer(server: string, field: 'server1' | 'server2') {
+  ntpErrors.value[field] = ''
+  if (field === 'server1' && timeNTPEn.value && !server.trim()) {
+    ntpErrors.value.server1 = 'Primary NTP server cannot be empty'
+    return false
+  }
+  const domainRegex =
+    /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$|^(\d{1,3}\.){3}\d{1,3}$/i
+  if (server.trim() && !domainRegex.test(server.trim())) {
+    ntpErrors.value[field] = 'Please enter a valid domain name or IP address'
+    return false
   }
-  PutTimePara(timeParaData).then(res => {
+  return true
+}
+
+async function saveSettings() {
+  if (timeNTPEn.value) {
+    if (!validateNtpServer(timeNTP.value, 'server1')) return
+    validateNtpServer(timeNTP2.value, 'server2')
+  }
+
+  saveLoading.value = true
+  try {
+    const timeParaData: TimeParaData = {
+      timeMode: timeMode.value,
+      timeZoneEn: timeZoneEn.value,
+      timeNTPEn: timeNTPEn.value,
+      timeNTP: timeNTPEn.value ? timeNTP.value : '',
+      timeNTP2: timeNTPEn.value ? timeNTP2.value : ''
+    }
+    const res = await PutTimePara(timeParaData)
     if (res.data === 'ok\n') {
-      ElMessage.success('Operation Successful')
+      ElMessage.success('Operation successful')
     } else {
-      ElMessage.error('Operation Failed')
+      ElMessage.error('Operation failed')
     }
-  }).catch(() => {
-    ElMessage.error('Operation Failed')
-  })
+  } catch (error) {
+    ElMessage.error('Operation failed, please check your input')
+    console.error(error)
+  } finally {
+    saveLoading.value = false
+  }
 }
 </script>
 
 <style scoped lang="scss">
-.time-container {
-  padding: 0 16px;
-  background: #f5f7fa;
-  min-height: 100%;
-  display: flex;
-  align-items: flex-start;
-  //justify-content: center;
-}
-
-.time-card {
-  width: 100%;
-  max-width: 480px;
-  //background: white;
-  border-radius: 8px;
-  //box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
-  overflow: hidden;
+.setting-container {
+  padding: 20px;
+  background: #f5f5f5;
 
-  .card-header {
-    //background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
-    padding: 20px;
-    color: #000104;
+  .section {
+    margin-bottom: 20px;
+    border-radius: 2px;
+    border-left: 3px solid #409eff;
+    padding: 0;
+    overflow: hidden;
 
-    .card-title {
-      margin: 0;
-      font-size: 18px;
+    .section-title {
+      font-size: 14px;
       font-weight: 600;
+      color: #333333;
+      padding: 14px 20px;
+      background: #fafafa;
+      border-bottom: 1px solid #eeeeee;
+      margin: 0;
+    }
+
+    .form-content {
+      padding: 16px 20px;
+      background: transparent;
     }
   }
 
-  .card-content {
-    padding: 24px;
+  .form-item {
+    display: flex;
+    align-items: center;
+    gap: 16px;
+    margin-bottom: 16px;
 
-    .form-group {
-      margin-bottom: 24px;
+    &:last-child {
+      margin-bottom: 0;
+    }
+
+    .form-label {
+      display: block;
+      min-width: 140px;
+      font-size: 13px;
+      color: #666666;
+      font-weight: 500;
+
+      &.required::after {
+        content: '*';
+        color: #f56c6c;
+        margin-left: 4px;
+      }
+    }
+
+    .form-control {
+      width: 180px;
+    }
+
+    .time-value {
+      font-size: 13px;
+      color: #409eff;
+      font-family: 'Monaco', 'Courier New', monospace;
+      letter-spacing: 0.5px;
+    }
+
+    .form-btn {
+      font-size: 12px;
+      width: auto;
+      padding: 6px 16px;
+      height: 32px;
+      background-color: #409eff;
+      color: #fff;
+      border: none;
+      border-radius: 6px;
+      font-weight: 500;
+      cursor: pointer;
+      transition: all 0.3s ease;
+
+      &:hover {
+        background-color: #66b1ff;
+      }
+
+      &:active {
+        background-color: #3a8ee6;
+      }
+    }
+  }
+
+  .ntp-fields {
+    margin-top: 12px;
+    padding-top: 12px;
+    border-top: 1px solid #eeeeee;
+
+    .form-item {
+      flex-direction: column;
+      align-items: flex-start;
+      margin-bottom: 14px;
 
       &:last-child {
         margin-bottom: 0;
       }
 
       .form-label {
-        display: block;
-        font-size: 14px;
-        font-weight: 500;
-        color: #303133;
-        margin-bottom: 8px;
-        line-height: 1;
+        min-width: auto;
+        margin-bottom: 6px;
       }
 
-      .mode-select {
-        width: 100%;
-      }
-
-      .time-info {
-        display: flex;
-        align-items: center;
-        height: 40px;
-        padding: 0 12px;
-        background: #f5f7fa;
-        border: 1px solid #dcdfe6;
-        border-radius: 4px;
-        font-size: 14px;
-        color: #606266;
-        font-family: 'Courier New', monospace;
+      .form-control {
+        width: 240px;
       }
     }
+  }
 
-    .button-group {
-      display: flex;
-      gap: 12px;
-      margin-top: 32px;
+  .error-message {
+    font-size: 12px;
+    color: #f56c6c;
+    margin-top: 4px;
+    margin-left: 0;
+  }
 
-      .sync-button {
-        flex: 1;
-        height: 40px;
-        font-size: 14px;
-        border-radius: 4px;
+  .button-group {
+    display: flex;
+    gap: 12px;
+    padding: 0;
+
+    .el-button {
+      min-width: 100px;
+      font-size: 13px;
+      height: 36px;
+      border-radius: 6px;
+
+      &.reset-btn {
+        background-color: #ffffff;
+        border-color: #dcdfe6;
+        color: #606266;
+
+        &:hover {
+          color: #409eff;
+          border-color: #409eff;
+        }
       }
     }
   }
 }
 
-// 响应式设计
-@media (max-width: 600px) {
-  .time-container {
-    padding: 12px;
+/* 折叠展开动画 */
+.collapse-enter-active,
+.collapse-leave-active {
+  transition: all 0.3s ease;
+  max-height: 500px;
+  overflow: hidden;
+}
+
+.collapse-enter-from,
+.collapse-leave-to {
+  opacity: 0;
+  max-height: 0;
+}
+
+:deep(.el-input__wrapper) {
+  border-color: #dcdfe6;
+  background-color: #ffffff;
+
+  &:hover {
+    border-color: #b1b3b6;
   }
 
-  .time-card {
-    .card-content {
-      padding: 16px;
+  &.is-focus {
+    border-color: #409eff;
+  }
+}
 
-      .form-group {
-        margin-bottom: 16px;
-      }
+:deep(.el-input__inner) {
+  font-size: 13px;
+  color: #606266;
 
-      .button-group {
-        margin-top: 24px;
-      }
-    }
+  &::placeholder {
+    color: #a8abb2;
+  }
+}
+
+:deep(.el-select__wrapper) {
+  border-color: #dcdfe6;
+  background-color: #ffffff;
+
+  &:hover {
+    border-color: #b1b3b6;
   }
 }
+
+:deep(.el-switch) {
+  --el-switch-on-color: #409eff;
+  --el-switch-off-color: #dcdfe6;
+}
 </style>

+ 147 - 95
src/views/settings/systemSettings/components/user/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="password-form-container">
+  <div class="password-form-container" >
     <!-- RTSP Password Verification Switch -->
     <div class="settings-section" v-loading="RtspLoading">
       <div class="section-header">
@@ -10,38 +10,51 @@
           <div class="switch-info">
             <p class="switch-label">Verification Status</p>
             <p class="switch-description">
-              {{ RtspPwdEn === 1 ? 'Enabled - Username and password required to access video stream' : 'Disabled - No authentication required to access video stream' }}
+              {{
+                RtspPwdEn === 1
+                  ? 'Enabled - Username and password required to access video stream'
+                  : 'Disabled - No authentication required to access video stream'
+              }}
             </p>
           </div>
           <el-switch
-              v-model="RtspPwdEn"
-              style="--el-switch-on-color: #13ce66; --el-switch-off-color: #ff4949"
-              :active-value="1"
-              :inactive-value="0"
-              @change="handleRtspPwdEnChange"
+            v-model="RtspPwdEn"
+            :active-value="1"
+            :inactive-value="0"
+            @change="handleRtspPwdEnChange"
           />
         </div>
       </div>
 
       <!-- RTSP Access Examples -->
-      <div class="tips-box">
+      <!-- <div class="tips-box">
         <div class="tips-title">
           <span class="tips-icon">ℹ️</span>
           <span>Video Stream Access Address Examples</span>
         </div>
         <div class="tips-content">
           <div class="tip-item">
-            <p class="tip-label">Password Verification {{ RtspPwdEn === 0 ? '(Disabled)' : '' }}:</p>
+            <p class="tip-label">
+              Password Verification {{ RtspPwdEn === 0 ? '(Disabled)' : '' }}:
+            </p>
             <code>rtsp://192.168.0.105:554/video1</code>
-            <p class="tip-note">Please change to your camera's real IP address and access on the same local area network</p>
+            <p class="tip-note">
+              Please change to your camera's real IP address and access on the
+              same local area network
+            </p>
           </div>
           <div class="tip-item">
-            <p class="tip-label">Password Verification {{ RtspPwdEn === 1 ? '(Enabled)' : '' }}:</p>
+            <p class="tip-label">
+              Password Verification {{ RtspPwdEn === 1 ? '(Enabled)' : '' }}:
+            </p>
             <code>rtsp://username:password@192.168.0.105:554/video1</code>
-            <p class="tip-note">Please change to your camera's real IP address and credentials, access on the same local area network</p>
+            <p class="tip-note">
+              Please change to your camera's real IP address and credentials,
+              access on the same local area network
+            </p>
           </div>
         </div>
-      </div>
+      </div> -->
     </div>
 
     <!-- Password Change Form -->
@@ -50,52 +63,60 @@
         <h3>Change Password</h3>
       </div>
       <el-form
-          ref="formRef"
-          :model="userManagementFormData"
-          :rules="userManagementFormRules"
-          @keyup.enter="handleConfirm"
-          class="password-form"
+        ref="formRef"
+        :model="userManagementFormData"
+        :rules="userManagementFormRules"
+        @keyup.enter="handleConfirm"
+        class="password-form"
       >
-        <el-form-item prop="oldPassword" label="Old Password" label-width="145px">
+        <el-form-item
+          prop="oldPassword"
+          label="Old Password"
+          label-width="145px"
+        >
           <el-input
-              v-model.trim="userManagementFormData.oldPassword"
-              placeholder="Enter your old password"
-              size="large"
-              tabindex="1"
-              type="password"
-              show-password
+            v-model.trim="userManagementFormData.oldPassword"
+            placeholder="Enter your old password"
+            size="large"
+            tabindex="1"
+            type="password"
+            show-password
           />
         </el-form-item>
 
-        <el-form-item prop="newPassword" label="New Password" label-width="145px">
+        <el-form-item
+          prop="newPassword"
+          label="New Password"
+          label-width="145px"
+        >
           <el-input
-              v-model.trim="userManagementFormData.newPassword"
-              placeholder="Enter your new password"
-              size="large"
-              tabindex="2"
-              type="password"
-              show-password
+            v-model.trim="userManagementFormData.newPassword"
+            placeholder="Enter your new password"
+            size="large"
+            tabindex="2"
+            type="password"
+            show-password
           />
         </el-form-item>
 
-        <el-form-item prop="newPasswordSc" label="Confirm Password" label-width="145px">
+        <el-form-item
+          prop="newPasswordSc"
+          label="Confirm Password"
+          label-width="145px"
+        >
           <el-input
-              v-model.trim="userManagementFormData.newPasswordSc"
-              placeholder="Re-enter your new password"
-              size="large"
-              tabindex="3"
-              type="password"
-              show-password
+            v-model.trim="userManagementFormData.newPasswordSc"
+            placeholder="Re-enter your new password"
+            size="large"
+            tabindex="3"
+            type="password"
+            show-password
           />
         </el-form-item>
 
         <div class="form-actions">
-          <el-button type="primary" size="large" @click.prevent="handleConfirm">
-            Save
-          </el-button>
-          <el-button size="large" @click="resetForm">
-            Reset
-          </el-button>
+          <el-button type="primary"  @click.prevent="handleConfirm">Save</el-button>
+          <el-button  @click="resetForm"> Reset </el-button>
         </div>
       </el-form>
     </div>
@@ -103,12 +124,12 @@
 </template>
 
 <script lang="ts" setup>
+import { type FormInstance, type FormRules, ElMessage } from 'element-plus'
 import {
-  type FormInstance,
-  type FormRules,
-  ElMessage,
-} from 'element-plus'
-import { putPasswordApi, getRtspPasswordApi, putRtspPasswordApi } from '@/api/userManagement'
+  putPasswordApi,
+  getRtspPasswordApi,
+  putRtspPasswordApi
+} from '@/api/userManagement'
 import { useUserStore } from '@/stores/modules/user'
 import { ref, reactive } from 'vue'
 import { useRouter } from 'vue-router'
@@ -118,7 +139,7 @@ const RtspPwdEn = ref(1)
 const loading = ref(false)
 const router = useRouter()
 
-const  RtspLoading = ref(false)
+const RtspLoading = ref(false)
 
 const userManagementFormData = reactive({
   name: useUserStore().username || 'admin',
@@ -129,16 +150,43 @@ const userManagementFormData = reactive({
 
 const userManagementFormRules: FormRules = {
   oldPassword: [
-    { required: true, message: 'Please enter your old password', trigger: 'blur' },
-    { min: 1, max: 16, message: 'Password length must be between 1 and 16 characters', trigger: 'blur' }
+    {
+      required: true,
+      message: 'Please enter your old password',
+      trigger: 'blur'
+    },
+    {
+      min: 1,
+      max: 16,
+      message: 'Password length must be between 1 and 16 characters',
+      trigger: 'blur'
+    }
   ],
   newPassword: [
-    { required: true, message: 'Please enter your new password', trigger: 'blur' },
-    { min: 1, max: 16, message: 'Password length must be between 1 and 16 characters', trigger: 'blur' }
+    {
+      required: true,
+      message: 'Please enter your new password',
+      trigger: 'blur'
+    },
+    {
+      min: 1,
+      max: 16,
+      message: 'Password length must be between 1 and 16 characters',
+      trigger: 'blur'
+    }
   ],
   newPasswordSc: [
-    { required: true, message: 'Please enter your confirm password', trigger: 'blur' },
-    { min: 1, max: 16, message: 'Password length must be between 1 and 16 characters', trigger: 'blur' },
+    {
+      required: true,
+      message: 'Please enter your confirm password',
+      trigger: 'blur'
+    },
+    {
+      min: 1,
+      max: 16,
+      message: 'Password length must be between 1 and 16 characters',
+      trigger: 'blur'
+    },
     {
       validator: (rule, value, callback) => {
         if (value !== userManagementFormData.newPassword) {
@@ -152,9 +200,8 @@ const userManagementFormRules: FormRules = {
   ]
 }
 
-
 function getRtspPassword() {
-  getRtspPasswordApi().then(res => {
+  getRtspPasswordApi().then((res:any) => {
     RtspPwdEn.value = res.data.RtspPwdEn
   })
 }
@@ -163,19 +210,20 @@ onMounted(() => {
   getRtspPassword()
 })
 
-
 const handleRtspPwdEnChange = () => {
   RtspLoading.value = true
-  putRtspPasswordApi({ RtspPwdEn: RtspPwdEn.value }).then(res => {
-    if(res.data == 'ok\n') {
-      ElMessage.success('Operation successful')
-    } else {
-      ElMessage.error('Failed to change RTSP password. Please try again.')
-    }
-  }).finally(() => {
-    RtspLoading.value = false
-    getRtspPassword()
-  })
+  putRtspPasswordApi({ RtspPwdEn: RtspPwdEn.value })
+    .then((res:any) => {
+      if (res.data == 'ok\n') {
+        ElMessage.success('Operation successful')
+      } else {
+        ElMessage.error('Failed to change RTSP password. Please try again.')
+      }
+    })
+    .finally(() => {
+      RtspLoading.value = false
+      getRtspPassword()
+    })
 }
 
 const handleConfirm = () => {
@@ -183,21 +231,23 @@ const handleConfirm = () => {
     if (valid) {
       loading.value = true
       putPasswordApi(userManagementFormData)
-          .then((res) => {
-            if (res.data == 'ok\n') {
-              ElMessage.success('Password changed successfully. Please log in again.')
-              useUserStore().logout()
-              router.push('/login')
-            } else {
-              ElMessage.error('Failed to change password. Please try again.')
-            }
-          })
-          .catch((error) => {
-            ElMessage.error('Change failed: ' + error)
-          })
-          .finally(() => {
-            loading.value = false
-          })
+        .then((res:any) => {
+          if (res.data == 'ok\n') {
+            ElMessage.success(
+              'Password changed successfully. Please log in again.'
+            )
+            useUserStore().logout()
+            router.push('/login')
+          } else {
+            ElMessage.error('Failed to change password. Please try again.')
+          }
+        })
+        .catch((error) => {
+          ElMessage.error('Change failed: ' + error)
+        })
+        .finally(() => {
+          loading.value = false
+        })
     } else {
       ElMessage.error('Please check your input')
     }
@@ -212,17 +262,17 @@ const resetForm = () => {
 <style lang="scss" scoped>
 .password-form-container {
   max-width: 700px;
-  margin: 0 auto;
+  // margin: 0 auto;
   //padding: 30px;
-  background: #f5f7fa;
+  // background: #f5f7fa;
   border-radius: 8px;
 
   .settings-section {
-    background: white;
+    // background: white;
     border-radius: 8px;
-    padding: 25px;
-    margin-bottom: 20px;
-    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+    padding: 20px;
+    // margin-bottom: 15px;
+    // box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
 
     .section-header {
       margin-bottom: 20px;
@@ -230,7 +280,7 @@ const resetForm = () => {
       padding-bottom: 15px;
 
       h3 {
-        font-size: 16px;
+        font-size: 15px;
         font-weight: 600;
         color: #1f2d3d;
         margin: 0;
@@ -334,6 +384,7 @@ const resetForm = () => {
     :deep(.el-form-item__label) {
       color: #606266;
       font-weight: 500;
+      font-size: 13px;
     }
   }
 
@@ -341,10 +392,11 @@ const resetForm = () => {
     display: flex;
     gap: 12px;
     margin-top: 30px;
-    justify-content: center;
+    // justify-content: center;
+    justify-content: flex-start;
 
     .el-button {
-      min-width: 120px;
+      min-width: 100px;
     }
   }
 }
@@ -382,4 +434,4 @@ const resetForm = () => {
     }
   }
 }
-</style>
+</style>

+ 0 - 294
src/views/settings/systemSettings/components/volume/index.vue

@@ -1,294 +0,0 @@
-<template>
-  <div class="volume-container">
-    <div class="volume-card">
-      <!-- 标题 -->
-      <div class="volume-header">
-        <svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
-          <polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"></polygon>
-          <path d="M15.54 3.54a9 9 0 0 1 0 12.72M19.07 4.93a15 15 0 0 1 0 14.14"></path>
-        </svg>
-        <h2>Speaker Volume</h2>
-      </div>
-
-      <!-- 音量显示 -->
-      <div class="volume-display">
-        <span class="label">Current Volume</span>
-        <span class="value">{{ speakerVolume }}%</span>
-      </div>
-
-      <!-- 音量滑块 -->
-      <div class="slider-wrapper">
-        <el-slider
-          v-model="speakerVolume"
-          :max="100"
-          :min="0"
-          show-stops
-          :format-tooltip="formatTooltip"
-        />
-      </div>
-
-      <!-- 音量范围标记 -->
-      <div class="range-markers">
-        <span>0%</span>
-        <span>50%</span>
-        <span>100%</span>
-      </div>
-
-      <!-- 保存按钮 -->
-      <el-button
-        type="primary"
-        :loading="loading"
-        @click="saveVolume"
-        class="save-btn"
-      >
-        Save
-      </el-button>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, onUnmounted } from 'vue'
-import { ElSlider, ElButton, ElMessage } from 'element-plus'
-import { getCameraVolume, cameraVolume } from '@/api/setting'
-
-interface Feedback {
-  type: 'success' | 'error'
-  message: string
-}
-
-const speakerVolume = ref<number>(50)
-const loading = ref<boolean>(false)
-const feedback = ref<Feedback | null>(null)
-let feedbackTimer: ReturnType<typeof setTimeout> | null = null
-
-const formatTooltip = (val: number | number[]): string => {
-  const value = Array.isArray(val) ? val[0] : val
-  return `${value}%`
-}
-
-function getVolume() {
-  getCameraVolume().then(res => {
-    if(res.data) {
-      speakerVolume.value = Number(res.data.speakerVolume)
-    }
-  })
-}
-
-getVolume()
-
-const saveVolume = async () => {
-  loading.value = true
-  feedback.value = null
-  try {
-    const res = await cameraVolume({ speakerVolume: speakerVolume.value })
-    if(res.data == 'ok\n') {
-      ElMessage.success(`Volume Saved successfully`)
-    } else {
-      ElMessage.error('Failed to Save Volume Settings')
-    }
-  } catch (error) {
-    feedback.value = {
-      type: 'error',
-      message: 'Operation failed. Please try again'
-    }
-    console.error('保存音量失败:', error)
-  } finally {
-    loading.value = false
-
-    // 3秒后自动清除反馈
-    if (feedbackTimer) clearTimeout(feedbackTimer)
-    feedbackTimer = setTimeout(() => {
-      feedback.value = null
-    }, 3000)
-  }
-}
-
-// 组件卸载时清理定时器
-onUnmounted(() => {
-  if (feedbackTimer) clearTimeout(feedbackTimer)
-})
-</script>
-
-<style scoped lang="scss">
-.volume-container {
-  //min-height: 100vh;
-  //background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
-  display: flex;
-  //align-items: center;
-  //justify-content: center;
-  padding: 10px;
-}
-
-.volume-card {
-  //background: #ffffff;
-  border-radius: 8px;
-  //box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-  padding: 20px;
-  max-width: 360px;
-  width: 100%;
-}
-
-.volume-header {
-  display: flex;
-  align-items: center;
-  gap: 12px;
-  margin-bottom: 24px;
-
-  .icon {
-    width: 24px;
-    height: 24px;
-    color: #409eff;
-    stroke-width: 2;
-  }
-
-  h2 {
-    font-size: 18px;
-    font-weight: 600;
-    color: #333;
-    margin: 0;
-  }
-}
-
-.volume-display {
-  display: flex;
-  justify-content: space-between;
-  align-items: center;
-  margin-bottom: 16px;
-
-  .label {
-    font-size: 14px;
-    font-weight: 500;
-    color: #606266;
-  }
-
-  .value {
-    font-size: 28px;
-    font-weight: bold;
-    color: #409eff;
-  }
-}
-
-.slider-wrapper {
-  margin-bottom: 12px;
-
-  :deep(.el-slider) {
-    .el-slider__runway {
-      height: 6px;
-      border-radius: 3px;
-      //background: #dcdfe6;
-    }
-
-    .el-slider__bar {
-      background-color: #409eff;
-      height: 6px;
-      border-radius: 3px;
-    }
-
-    .el-slider__button-wrapper {
-      .el-slider__button {
-        width: 18px;
-        height: 18px;
-        background-color: #409eff;
-
-        &:hover {
-          box-shadow: 0 0 8px rgba(64, 158, 255, 0.5);
-        }
-      }
-    }
-  }
-}
-
-.range-markers {
-  display: flex;
-  justify-content: space-between;
-  padding: 0 6px;
-  margin-bottom: 20px;
-  font-size: 12px;
-  color: #909399;
-}
-
-.save-btn {
-  width: 100%;
-  height: 40px;
-  font-size: 16px;
-  font-weight: 500;
-  border-radius: 4px;
-
-  &:hover {
-    transform: translateY(-2px);
-    box-shadow: 0 4px 12px rgba(64, 158, 255, 0.3);
-  }
-
-  &:active {
-    transform: translateY(0);
-  }
-}
-
-.feedback {
-  margin-top: 16px;
-  padding: 12px 16px;
-  border-radius: 4px;
-  display: flex;
-  align-items: center;
-  gap: 8px;
-  font-size: 14px;
-  animation: slideIn 0.3s ease-out;
-
-  .feedback-icon {
-    width: 20px;
-    height: 20px;
-    flex-shrink: 0;
-    stroke-width: 2;
-  }
-
-  &.feedback-success {
-    background-color: #f0f9ff;
-    color: #67c23a;
-    border-left: 4px solid #67c23a;
-
-    .feedback-icon {
-      color: #67c23a;
-    }
-  }
-
-  &.feedback-error {
-    background-color: #fef0f0;
-    color: #f56c6c;
-    border-left: 4px solid #f56c6c;
-
-    .feedback-icon {
-      color: #f56c6c;
-    }
-  }
-}
-
-.help-text {
-  margin-top: 16px;
-  font-size: 12px;
-  color: #909399;
-  text-align: center;
-  margin-bottom: 0;
-}
-
-@keyframes slideIn {
-  from {
-    opacity: 0;
-    transform: translateY(-10px);
-  }
-  to {
-    opacity: 1;
-    transform: translateY(0);
-  }
-}
-
-.fade-enter-active,
-.fade-leave-active {
-  transition: all 0.3s ease;
-}
-
-.fade-enter-from,
-.fade-leave-to {
-  opacity: 0;
-}
-</style>

+ 10 - 144
src/views/settings/systemSettings/index.vue

@@ -4,29 +4,9 @@
       <el-tab-pane label="Basic Information" name="first">
         <CameraInfo />
       </el-tab-pane>
-      <el-tab-pane label="System Maintenance" name="second">
-        <el-form
-          ref="formRef"
-          :model="FormData"
-          style="padding: 20px"
-        >
-          <el-form-item>
-            <el-radio-group
-              v-model="FormData.reset"
-              class="vertical-radio-group"
-            >
-              <el-radio :value="1">Restore Factory Settings</el-radio>
-              <el-radio :value="2">Manual Camera Restart</el-radio>
-            </el-radio-group>
-          </el-form-item>
-          <el-button  type="primary" @click.prevent="handle" :loading="loading">
-            Execute
-          </el-button>
-        </el-form>
-      </el-tab-pane>
-
-      <el-tab-pane label="System Update" name="third">
 
+      <el-tab-pane label="System Maintenance" name="second">
+        <SystemMaintenance />
       </el-tab-pane>
 
       <el-tab-pane label="Time Settings" name="fourth">
@@ -36,143 +16,29 @@
       <el-tab-pane label="Password Management" name="fifth">
         <UserSecurity />
       </el-tab-pane>
+
+      <el-tab-pane label="System Update" name="third">
+        <SystemUpgrade />
+      </el-tab-pane>
     </el-tabs>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref } from 'vue'
-import { ElLoading, ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
 import CameraInfo from './components/cameraInfo/index.vue'
 import UserSecurity from './components/user/index.vue'
 import TimeSetting from './components/time/index.vue'
-import { cameraReset } from '@/api/setting'
-import {useUserStore} from "@/stores/modules/user";
-import {useRouter} from "vue-router";
-
-const router = useRouter()
+import SystemMaintenance from './components/systemMaintenance/index.vue'
+import SystemUpgrade from './components/systemUpgrade/index.vue'
 
 const activeName = ref('first')
-const loading = ref(false)
-
-/** 表单元素的引用 */
-const formRef = ref<FormInstance | null>(null)
-
-// 定义复位选项的常量
-const FormData = reactive({ reset: 0 })
-
-const handleReset = async () => {
-  try {
-    await ElMessageBox.confirm(
-      'This operation will modify the camera configuration and restart it. Continue?',
-      'Warning',
-      {
-        confirmButtonText: 'OK',
-        cancelButtonText: 'Cancel',
-        type: 'warning',
-        confirmButtonClass: 'el-button--danger'
-      }
-    )
-
-    let loading = ElLoading.service()
-    formRef.value?.validate((valid: boolean) => {
-      if (valid) {
-        cameraReset(FormData)
-          .then((res) => {
-            if (res.data === 'ok\n') {
-              ElMessage.success('Operation successful. Please wait a moment...')
-              useUserStore().logout()
-              router.push('/login')
-            } else {
-              ElMessage.error('Operation failed')
-            }
-            loading.close()
-          })
-          .catch((error) => {
-            loading.close()
-            ElMessage.error('操作失败: ' + error)
-          })
-      } else {
-        loading.close()
-      }
-    })
-  } catch (error) {
-    console.log(error)
-    if (error !== 'cancel') {
-      ElMessage.error('Operation failed')
-    }
-  }
-}
-
-
-const handleRestart = async () => {
-  try {
-    await ElMessageBox.confirm(
-      'This operation will restart the camera. Continue?',
-      'Note',
-      {
-        confirmButtonText: 'OK',
-        cancelButtonText: 'Cancel',
-        type: 'warning',
-        confirmButtonClass: 'el-button--danger'
-      }
-    )
-    const resp = await cameraReset({ reboot: 1 })
-    if (resp.data === 'ok\n') {
-        ElMessage.success('Camera restarted successfully. Please wait a moment...')
-        useUserStore().logout()
-        await router.push('/login')
-    } else {
-      ElMessage.error('Operation failed')
-    }
-  } catch (error) {
-    console.log(error)
-    if (error !== 'cancel') {
-      ElMessage.error('Operation failed')
-    }
-  }
-}
-
-
-function handle() {
-  if (FormData.reset === 1) {
-    loading.value = true
-    handleReset()
-      .finally(() => {
-        loading.value = false
-      })
-  } else if (FormData.reset === 2) {
-    loading.value = true
-    handleRestart()
-      .finally(() => {
-        loading.value = false
-      })
-  }
-}
 </script>
 
-
 <style scoped lang="scss">
-.settings-container{
+.settings-container {
   display: flex;
   flex-direction: column;
   padding: 20px;
 }
-
-// 添加垂直排列的radio样式
-.vertical-radio-group {
-  display: flex;
-  flex-direction: column;
-  gap: 10px; // 设置选项之间的间距
-  align-items: flex-start; // 左对齐
-}
-
-.el-radio {
-  margin-right: 0; // 移除默认的右边距
-  margin-bottom: 8px; // 添加底部间距
-}
-
-.el-button {
-  margin-top: 10px;
-}
-</style>
+</style>

Some files were not shown because too many files changed in this diff