liujintao пре 1 месец
родитељ
комит
9ccc961a03

+ 1 - 1
.env.development

@@ -5,7 +5,7 @@ VITE_PUBLIC_PATH='/'
 ## 路由模式 hash 或 html5
 VITE_ROUTER_HISTORY='html5'
 
-VITE_HOST_IP = '192.168.32.103'
+VITE_HOST_IP = '192.168.32.100'
 
 VITE_VIDEO_PORT = '8000'
 

+ 16 - 0
src/api/protocol.ts

@@ -0,0 +1,16 @@
+import { request } from '@/utils/request'
+
+export function getOnvifProtocolApi() {
+    return request({
+        url: `/API/V1.0/Video/Onvif`,
+        method: 'get'
+    })
+}
+
+export function OnvifProtocolApi(data:any) {
+    return request({
+        url: `/API/V1.0/Video/Onvif`,
+        method: 'put',
+        data
+    })
+}

+ 0 - 5
src/api/types/login.ts

@@ -4,8 +4,3 @@ export interface LoginRequestData {
 }
 
 export type LoginResponseData = ApiResponseData<{ token: string }>
-
-export interface UserInfoResponseData {
-  pageSize: number
-  pageNo: number
-}

+ 0 - 20
src/api/types/preview.ts

@@ -1,21 +1 @@
-export interface initializeSessionRequestData {
-  transPayload: string
-  stream: {
-    meta: number
-    enableAudio: number
-  }
-  transProtocol?: {
-    websocketPort: number
-  }
-}
 
-export type initializeSessionResponseData = ApiResponseData<{
-  sessionsID: string
-  stream: {
-    meta: number
-    enableAudio: number
-  }
-  transProtocol?: {
-    websocketPort: null
-  }
-}>

+ 4 - 1
src/main.ts

@@ -4,7 +4,8 @@ import App from './App.vue'
 import router, { setRouter } from './router'
 import '@/router/permission'
 
-import { View, Setting, User, Lock, Expand, Fold, Refresh } from '@element-plus/icons-vue'
+import { View, Setting, User, Lock,
+    Expand, Fold, Refresh, Document, Monitor } from '@element-plus/icons-vue'
 
 import './assets/main.css'
 import 'element-plus/dist/index.css'
@@ -19,6 +20,8 @@ app.component('Lock', Lock)
 app.component('Expand', Expand)
 app.component('Fold', Fold)
 app.component('Refresh', Refresh)
+app.component('Document', Document)
+app.component('Monitor', Monitor)
 
 app.use(createPinia())
 setRouter()

+ 31 - 5
src/router/index.ts

@@ -14,7 +14,7 @@ export const constantRoutes: RouteRecordRaw[] = [
     redirect: '/preview',
     children: [
       {
-        path: 'preview',
+        path: '/preview',
         component: () => import('@/views/preview/index.vue'),
         name: 'Preview',
         meta: {
@@ -75,13 +75,39 @@ export const constantRoutes: RouteRecordRaw[] = [
         meta: {
           title: 'Alarm Settings'
         }
-      },
+      }
+    ]
+  },
+  {
+    path: '/',
+    component: Layouts,
+    redirect: '/remoteViewing',
+    children: [
       {
-        path: '/settings/remoteViewing',
-        component: () => import('@/views/settings/remoteViewing/index.vue'),
+        path: '/remoteViewing',
+        component: () => import('@/views/remoteViewing/index.vue'),
         name: 'RemoteViewing',
         meta: {
-          title: 'Remote Viewing'
+          title: 'Remote',
+          isShow: true,
+          icon: 'Monitor'
+        }
+      }
+    ]
+  },
+  {
+    path: '/',
+    component: Layouts,
+    redirect: '/standardProtocol',
+    children: [
+      {
+        path: '/standardProtocol',
+        component: () => import('@/views/standardProtocol/index.vue'),
+        name: 'StandardProtocol',
+        meta: {
+          title: 'Protocol',
+          isShow: true,
+          icon: 'Document'
         }
       }
     ]

+ 3 - 3
src/stores/modules/user.ts

@@ -13,12 +13,12 @@ export const useUserStore = defineStore('user', () => {
 
   /** 登录 */
   const login = async ({ name, password }: LoginRequestData) => {
-    // const { data } = await loginApi({ name, password })
+    const { data } = await loginApi({ name, password })
     username.value = name
     token.value = unToken
     setUsername(username.value)
-    // return data
-    return 'ok\n'
+    return data
+    // return 'ok\n' // 调试时候用
   }
 
   /** 登出 */

+ 335 - 0
src/views/remoteViewing/index.vue

@@ -0,0 +1,335 @@
+<template>
+  <div class="remote-viewing-container">
+    <div class="settings-section">
+
+      <!-- Title -->
+      <div class="section-header">
+        <h3>Remote Monitoring</h3>
+      </div>
+
+      <!-- Status Section -->
+      <div class="status-section">
+        <div class="status-content">
+          <label>Status</label>
+          <span class="status-badge" :class="{ 'online': isOnline, 'offline': !isOnline }">
+            {{ deviceStatus }}
+          </span>
+        </div>
+        <button
+            class="refresh-btn"
+            @click="refreshStatus"
+            :disabled="isRefreshing"
+            :class="{ 'is-loading': isRefreshing }"
+        >
+          <Refresh />
+        </button>
+      </div>
+
+      <!-- Device SN Section -->
+      <div class="info-item">
+        <label>Device Serial Number</label>
+        <div class="code-display">
+          <span>{{ deviceCode }}</span>
+          <button class="copy-btn" @click="copyDeviceSN">Copy</button>
+        </div>
+      </div>
+
+      <!-- QR Code Section -->
+      <div class="info-item">
+        <label>QR Code</label>
+        <div class="qrcode-box">
+          <QrcodeVue :value="qrCodeValue" :size="150" />
+        </div>
+      </div>
+
+      <!-- Instructions Section -->
+      <div class="info-item">
+        <label>Instructions</label>
+        <ol class="instructions-list">
+          <li>Ensure the device is online. If offline, check your network connection and refresh.</li>
+          <li>Use the ZOSI mobile app to scan the QR code to add this device for remote viewing.</li>
+        </ol>
+      </div>
+    </div>
+
+    <!-- Toast Notification -->
+    <Transition name="toast">
+      <div v-if="showNotification" class="toast">
+        {{ notificationMessage }}
+      </div>
+    </Transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { Refresh } from '@element-plus/icons-vue'
+import QrcodeVue from 'qrcode.vue'
+
+const deviceStatus = ref('Online')
+const isOnline = ref(true)
+const isRefreshing = ref(false)
+const showNotification = ref(false)
+const notificationMessage = ref('')
+const deviceCode = ref('0314153000122700030900')
+const qrCodeValue = ref('0314153000122700030900')
+
+const refreshStatus = async () => {
+  isRefreshing.value = true
+  try {
+    await new Promise(resolve => setTimeout(resolve, 1500))
+    isOnline.value = true
+    deviceStatus.value = 'Online'
+    notificationMessage.value = 'Device status refreshed'
+    showNotification.value = true
+    setTimeout(() => {
+      showNotification.value = false
+    }, 2000)
+  } catch (error) {
+    notificationMessage.value = 'Refresh failed'
+    showNotification.value = true
+    setTimeout(() => {
+      showNotification.value = false
+    }, 2000)
+  } finally {
+    isRefreshing.value = false
+  }
+}
+
+const copyDeviceSN = async () => {
+  try {
+    await navigator.clipboard.writeText(deviceCode.value)
+    notificationMessage.value = 'Copied to clipboard'
+    showNotification.value = true
+    setTimeout(() => {
+      showNotification.value = false
+    }, 2000)
+  } catch (error) {
+    notificationMessage.value = 'Copy failed'
+    showNotification.value = true
+    setTimeout(() => {
+      showNotification.value = false
+    }, 2000)
+  }
+}
+
+onMounted(() => {
+  // API call location
+})
+</script>
+
+<style scoped lang="scss">
+.remote-viewing-container {
+  max-width: 800px;
+  padding: 20px;
+
+  .settings-section {
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 24px;
+    border: 1px solid #e5e7eb;
+  }
+  .section-header {
+    margin-bottom: 24px;
+    padding-bottom: 12px;
+    border-bottom: 1px solid #f0f0f0;
+
+    h3 {
+      font-size: 18px;
+      font-weight: 600;
+      color: #1f2937;
+      margin: 0;
+      letter-spacing: -0.3px;
+    }
+  }
+}
+
+// Status Section
+.status-section {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 16px;
+  margin-bottom: 24px;
+
+  .status-content {
+    display: flex;
+    align-items: center;
+    gap: 12px;
+
+    label {
+      font-weight: 600;
+      color: #6b7280;
+      font-size: 12px;
+      text-transform: uppercase;
+      letter-spacing: 0.8px;
+    }
+
+    .status-badge {
+      padding: 6px 12px;
+      border-radius: 4px;
+      font-size: 13px;
+      font-weight: 500;
+
+      &.online {
+        background: #dbeafe;
+        color: #1e40af;
+      }
+
+      &.offline {
+        background: #fef3c7;
+        color: #b45309;
+      }
+    }
+  }
+
+  .refresh-btn {
+    width: 36px;
+    height: 36px;
+    padding: 0;
+    background: #f3f4f6;
+    border: 1px solid #d1d5db;
+    border-radius: 6px;
+    cursor: pointer;
+    transition: all 0.3s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    color: #6b7280;
+
+    &:hover:not(:disabled) {
+      background: #e5e7eb;
+      color: #374151;
+    }
+
+    &:disabled {
+      opacity: 0.6;
+      cursor: not-allowed;
+    }
+
+    &.is-loading {
+      animation: spin 1.5s linear infinite;
+    }
+
+    :deep(svg) {
+      width: 18px;
+      height: 18px;
+    }
+  }
+}
+
+// Info Items
+.info-item {
+  margin-bottom: 20px;
+
+  label {
+    display: block;
+    margin-bottom: 8px;
+    font-weight: 600;
+    color: #6b7280;
+    font-size: 12px;
+    text-transform: uppercase;
+    letter-spacing: 0.8px;
+  }
+}
+
+// Code Display
+.code-display {
+  display: flex;
+  gap: 10px;
+  background: #f9fafb;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  padding: 12px 14px;
+  align-items: center;
+
+  span {
+    font-family: 'Monaco', 'Courier New', monospace;
+    font-size: 13px;
+    color: #1f2937;
+    flex: 1;
+    word-break: break-all;
+  }
+
+  .copy-btn {
+    background: #3b82f6;
+    color: white;
+    border: none;
+    border-radius: 4px;
+    padding: 6px 12px;
+    font-size: 12px;
+    font-weight: 500;
+    cursor: pointer;
+    white-space: nowrap;
+    transition: background 0.3s ease;
+
+    &:hover {
+      background: #2563eb;
+    }
+
+    &:active {
+      transform: scale(0.95);
+    }
+  }
+}
+
+// QR Code Box
+.qrcode-box {
+  display: flex;
+  justify-content: center;
+  background: #f9fafb;
+  border: 1px solid #e5e7eb;
+  border-radius: 6px;
+  padding: 20px;
+}
+
+// Instructions List
+.instructions-list {
+  margin: 0;
+  padding-left: 20px;
+
+  li {
+    margin-bottom: 8px;
+    font-size: 13px;
+    line-height: 1.6;
+    color: #4b5563;
+
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+}
+
+// Toast
+.toast {
+  position: fixed;
+  bottom: 24px;
+  right: 24px;
+  padding: 12px 16px;
+  background: #1f2937;
+  color: white;
+  border-radius: 4px;
+  font-size: 13px;
+  z-index: 1000;
+  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+}
+
+@keyframes spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+.toast-enter-active,
+.toast-leave-active {
+  transition: opacity 0.3s ease;
+}
+
+.toast-enter-from,
+.toast-leave-to {
+  opacity: 0;
+}
+</style>

+ 5 - 2
src/views/settings/netSettings/components/IP/index.vue

@@ -3,7 +3,7 @@
     <div class="section">
       <div class="section-title">DHCP Settings</div>
       <div class="dhcp-row">
-        <span class="label">Automatic (DHCP)</span>
+        <span class="label">Automatic ( DHCP )</span>
         <el-switch
           v-model="settingFormData.enableDHCP"
           :active-value="1"
@@ -78,6 +78,8 @@
       </el-form>
     </div>
 
+    <el-divider class="short-divider" />
+
     <el-button type="primary" round :loading="loading" @click="handleSave">
       <span v-if="!loading">Save</span>
       <span v-else>Saving...</span>
@@ -257,7 +259,7 @@ const handleSave = () => {
   padding: 20px;
 
   .el-button{
-    margin-top: 30px;
+    margin-top: 10px;
     width: 80px;
   }
 
@@ -283,6 +285,7 @@ const handleSave = () => {
     margin-bottom: 8px;
 
     .label {
+      margin-right: 14px;
       font-size: 14px;
       color: #333;
       flex-shrink: 0;

+ 0 - 11
src/views/settings/remoteViewing/index.vue

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

+ 232 - 79
src/views/settings/systemSettings/components/systemMaintenance/index.vue

@@ -1,5 +1,8 @@
 <template>
   <div class="motion-detection">
+    <div class="section-header">
+      <h3>System Maintenance</h3>
+    </div>
     <!-- 定时重启 -->
     <div class="section">
       <div class="section-title">Scheduled Restart</div>
@@ -8,13 +11,13 @@
         <el-switch v-model="TmRstEnable" @change="onEnableChange" />
       </div>
       <transition name="slide">
-        <div class="setting-row">
+        <div class="setting-row" v-if="TmRstEnable">
           <span class="label">Restart time:</span>
           <el-time-picker
-            v-model="timeValue"
-            placeholder="Select time"
-            format="HH:mm"
-            style="width: 140px"
+              v-model="timeValue"
+              placeholder="Select time"
+              format="HH:mm"
+              style="width: 140px"
           />
         </div>
       </transition>
@@ -24,14 +27,14 @@
 
     <!-- 重复计划 -->
     <transition name="slide">
-      <div class="section">
+      <div class="section" v-if="TmRstEnable">
         <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)"
+              v-for="day in weekDays"
+              :key="day.value"
+              :class="['day-btn', { active: selectedDays.includes(day.value) }]"
+              @click="toggleDay(day.value)"
           >
             {{ day.label }}
           </button>
@@ -45,10 +48,18 @@
     <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')">
+        <el-button
+            class="btn-restart"
+            round
+            @click="handleAction('restart')"
+        >
           Restart Now
         </el-button>
-        <el-button class="btn-restore" type="danger" round @click="handleAction('reset')">
+        <el-button
+            class="btn-restore"
+            round
+            @click="handleAction('reset')"
+        >
           Restore Factory
         </el-button>
       </div>
@@ -56,7 +67,14 @@
 
     <el-divider class="short-divider" />
 
-    <el-button class="btn-save" type="primary" round  @click="Save" :loading="loading" style="width: 80px">Save</el-button>
+    <el-button
+        class="btn-save"
+        round
+        @click="Save"
+        :loading="loading"
+    >
+      Save
+    </el-button>
   </div>
 </template>
 
@@ -158,8 +176,8 @@ const executeAction = async (params: object, message: string) => {
 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?'
+      ? '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 {
@@ -172,10 +190,10 @@ const handleAction = async (type: 'restart' | 'reset') => {
     const loader = ElLoading.service()
     try {
       await executeAction(
-        { [isReset ? 'reset' : 'reboot']: 1 },
-        isReset
-          ? 'Factory reset initiated. Please wait...'
-          : 'Camera restarted successfully. Please wait...'
+          { [isReset ? 'reset' : 'reboot']: 1 },
+          isReset
+              ? 'Factory reset initiated. Please wait...'
+              : 'Camera restarted successfully. Please wait...'
       )
     } finally {
       loader.close()
@@ -234,138 +252,273 @@ const Save = async () => {
 </script>
 
 <style scoped lang="scss">
+// 颜色和尺寸变量定义
+$primary-color: #3b82f6;
+$primary-hover: #2563eb;
+$warning-color: #f59e0b;
+$warning-light: #fef3c7;
+$danger-color: #dc2626;
+$danger-light: #fee2e2;
+$text-primary: #1f2d3d;
+$text-secondary: #333;
+$text-tertiary: #666;
+$border-color: #dcdfe6;
+$divider-color: #f0f0f0;
+$error-color: #f56c6c;
+
+$spacing-xs: 6px;
+$spacing-sm: 8px;
+$spacing-md: 12px;
+$spacing-lg: 16px;
+$spacing-xl: 20px;
+
+$radius-sm: 4px;
+$radius-md: 6px;
+
 .motion-detection {
-  padding: 20px;
+  max-width: 700px;
+  padding: $spacing-xl;
+  background: #fff;
+  border-radius: 8px;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+
+  .section-header {
+    margin-bottom: $spacing-xl;
+    border-bottom: 2px solid $divider-color;
+    padding-bottom: 15px;
+
+    h3 {
+      font-size: 15px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
+    }
+  }
 
   .short-divider {
-    max-width: 700px;
-    margin: 16px 0;
+    max-width: 100%;
+    margin: $spacing-lg 0;
   }
 
   .section {
-    margin: 20px 0;
+    margin: $spacing-xl 0;
 
     .section-title {
       font-size: 14px;
       font-weight: 500;
-      color: #333;
-      margin-bottom: 14px;
+      color: $text-secondary;
+      margin-bottom: $spacing-md;
+      display: flex;
+      align-items: center;
     }
   }
 
   .setting-row {
     display: flex;
     align-items: center;
-    margin-bottom: 12px;
+    gap: $spacing-lg;
+    margin-bottom: 18px;
 
     .label {
       font-size: 14px;
-      color: #333;
+      color: $text-secondary;
       flex-shrink: 0;
+      min-width: fit-content;
     }
   }
 
+  // 日期选择器按钮组
   .days-container {
     display: flex;
-    gap: 8px;
+    flex-wrap: wrap;
+    gap: $spacing-sm;
+    margin-bottom: $spacing-md;
   }
 
   .day-btn {
-    padding: 6px 14px;
-    border: 1px solid #dcdfe6;
+    padding: $spacing-xs $spacing-sm * 1.75;
+    border: 1px solid $border-color;
     background: #fff;
-    border-radius: 4px;
+    border-radius: $radius-sm;
     font-size: 13px;
+    font-weight: 500;
+    color: $text-secondary;
     cursor: pointer;
-    transition: all 0.2s;
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+    user-select: none;
 
     &:hover {
-      border-color: #409eff;
-      color: #409eff;
+      border-color: $primary-color;
+      color: $primary-color;
+      background: rgba($primary-color, 0.05);
     }
 
     &.active {
-      background: #409eff;
+      background: $primary-color;
       color: #fff;
-      border-color: #409eff;
+      border-color: $primary-color;
+      box-shadow: 0 2px 4px rgba($primary-color, 0.3);
+    }
+
+    &:active {
+      transform: scale(0.98);
     }
   }
 
   .alarm-actions {
     display: flex;
-    gap: 16px;
+    gap: $spacing-lg;
+    flex-wrap: wrap;
   }
 
   .required-mark {
-    color: #f56c6c;
+    color: $error-color;
     margin-left: 2px;
   }
 
   .error-text {
-    color: #f56c6c;
+    color: $error-color;
     font-size: 12px;
-    margin: 8px 0 0;
+    margin: $spacing-sm 0 0;
+    font-weight: 500;
   }
 }
 
-/* Save 按钮 - 主操作,与侧边栏蓝色系统一 */
+// 按钮样式 - 通过覆盖 Element Plus 默认样式
+
+/* Save 按钮 - 主操作 */
 .btn-save {
-  background-color: #3B82F6;
-  color: #fff;
-  border: none;
-  padding: 8px 20px;
-  border-radius: 6px;
+  background-color: $primary-color !important;
+  color: #fff !important;
+  border: none !important;
+  min-width: 80px;
+  height: 36px;
   font-weight: 500;
+  font-size: 14px;
+  box-shadow: 0 2px 4px rgba($primary-color, 0.2);
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+
+  &:hover {
+    background-color: $primary-hover !important;
+    box-shadow: 0 4px 8px rgba($primary-hover, 0.3);
+  }
 
-  &:hover,
-  &:focus {
-    background-color: #2563EB;
+  &:active {
+    box-shadow: 0 1px 2px rgba($primary-hover, 0.2);
+    transform: translateY(1px);
+  }
+
+  &:disabled,
+  &.is-disabled {
+    background-color: #c0c4cc !important;
+    cursor: not-allowed;
+    box-shadow: none;
   }
 }
 
-/* Restart Now - 中等风险操作,柔和琥珀色 */
+/* Restart Now - 中等风险操作,琥珀色 */
 .btn-restart {
-  background-color: #FEF3C7;
-  color: #B45309;
-  border: 1px solid #FCD34D;
-  padding: 8px 20px;
-  border-radius: 6px;
+  //background-color: $warning-light !important;
+  //color: #b45309 !important;
+  //border: 1px solid #fcd34d !important;
   font-weight: 500;
-
-  &:hover,
-  &:focus {
-    background-color: #FDE68A;
-    border-color: #F59E0B;
-    color: #92400E;
-  }
+  font-size: 14px;
+  height: 36px;
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+  //box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+
+  //&:hover {
+  //  background-color: #fde68a !important;
+  //  border-color: $warning-color !important;
+  //  color: #92400e !important;
+  //  box-shadow: 0 2px 4px rgba($warning-color, 0.2);
+  //}
+  //
+  //&:active {
+  //  transform: translateY(1px);
+  //  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+  //}
+  //
+  //&:disabled,
+  //&.is-disabled {
+  //  opacity: 0.5;
+  //  cursor: not-allowed;
+  //}
 }
 
-/* Restore Factory - 高风险操作,柔和红色 */
+/* Restore Factory - 高风险操作,红色 */
 .btn-restore {
-  background-color: #FEE2E2;
-  color: #DC2626;
-  border: 1px solid #FCA5A5;
-  padding: 8px 20px;
-  border-radius: 6px;
+  //background-color: $danger-light !important;
+  //color: $danger-color !important;
+  //border: 1px solid #fca5a5 !important;
   font-weight: 500;
-
-  &:hover,
-  &:focus {
-    background-color: #FECACA;
-    border-color: #EF4444;
-    color: #B91C1C;
-  }
+  font-size: 14px;
+  height: 36px;
+  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+  //box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+
+  //&:hover {
+  //  background-color: #fecaca !important;
+  //  border-color: #ef4444 !important;
+  //  color: #b91c1c !important;
+  //  box-shadow: 0 2px 4px rgba($danger-color, 0.2);
+  //}
+  //
+  //&:active {
+  //  transform: translateY(1px);
+  //  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
+  //}
+  //
+  //&:disabled,
+  //&.is-disabled {
+  //  opacity: 0.5;
+  //  cursor: not-allowed;
+  //}
 }
 
-
+// 过渡动画 - 改进缓动函数
 .slide-enter-active,
 .slide-leave-active {
-  transition: all 0.3s ease;
+  transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+.slide-enter-from {
+  opacity: 0;
+  transform: translateY(-10px);
 }
 
-.slide-enter-from,
 .slide-leave-to {
   opacity: 0;
   transform: translateY(-10px);
 }
-</style>
+
+// 响应式设计
+@media (max-width: 600px) {
+  .motion-detection {
+    padding: $spacing-lg;
+
+    .setting-row {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: $spacing-sm;
+    }
+
+    .days-container {
+      gap: $spacing-xs;
+    }
+
+    .day-btn {
+      padding: $spacing-xs $spacing-sm;
+      font-size: 12px;
+    }
+
+    .alarm-actions {
+      flex-direction: column;
+
+      button {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

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

@@ -39,7 +39,6 @@
         <el-button
           type="primary"
           size="large"
-          round
           @click="handleUpgrade"
           :loading="loading"
           :disabled="!selectedFile"
@@ -50,7 +49,6 @@
         <el-button
           v-if="selectedFile"
           size="large"
-          round
           @click="handleClearFile"
           :disabled="loading"
           class="clear-btn"

+ 394 - 167
src/views/settings/systemSettings/components/time/index.vue

@@ -1,85 +1,111 @@
 <template>
-  <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 class="time-settings">
+    <!-- 系统时间 - 独立部分 -->
+    <div class="time-section-wrapper">
+      <div class="section">
+        <div class="section-header">
+          <h3>System Time</h3>
         </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 class="section-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"
+                @click="syncTime"
+                :loading="syncLoading"
+                class="sync-btn"
+            >
+              Sync Now
+            </el-button>
+          </div>
         </div>
       </div>
     </div>
 
-    <!-- 显示设置 -->
-    <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="form-control">
-            <el-option
-              v-for="item in timeModeOptions"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
-          </el-select>
+    <!-- 显示设置 + NTP 设置 + 按钮 - 组合部分 -->
+    <div class="settings-section-wrapper">
+      <!-- 显示设置 -->
+      <div class="section">
+        <div class="section-header">
+          <h3>Display Settings</h3>
         </div>
-        <div class="form-item">
-          <label class="form-label">Ignore Time Zone</label>
-          <el-switch v-model="timeZoneEn" />
+        <div class="section-content">
+          <div class="form-item">
+            <label class="form-label">Display Mode</label>
+            <el-select v-model="timeMode" class="form-control" placeholder="Select display mode">
+              <el-option
+                  v-for="item in timeModeOptions"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+              />
+            </el-select>
+          </div>
+          <div class="form-item">
+            <label class="form-label">Ignore Time Zone</label>
+            <el-switch v-model="timeZoneEn" />
+          </div>
         </div>
       </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" />
+      <!-- NTP 设置 -->
+      <div class="section">
+        <div class="section-header">
+          <h3>NTP Settings</h3>
         </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 class="section-content">
+          <div class="form-item">
+            <label class="form-label">Enable Manual NTP</label>
+            <el-switch v-model="timeNTPEn" />
           </div>
-        </transition>
+          <transition name="slide-ntp">
+            <div v-if="timeNTPEn" class="ntp-fields">
+              <div class="form-item">
+                <label class="form-label required">NTP Server 1</label>
+                <div class="control-wrapper">
+                  <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>
+              <div class="form-item">
+                <label class="form-label">NTP Server 2</label>
+                <div class="control-wrapper">
+                  <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>
+            </div>
+          </transition>
+        </div>
       </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 class="button-group">
+        <el-button
+            round
+            @click="saveSettings"
+            :loading="saveLoading"
+            class="save-btn"
+        >
+          Save
+        </el-button>
+      </div>
     </div>
   </div>
 </template>
@@ -91,7 +117,6 @@ 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
@@ -150,9 +175,9 @@ async function getTimeMode() {
 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')
+      timeMode.value === 1
+          ? format(now, 'yyyy-MM-dd HH:mm:ss')
+          : format(now, 'yyyy-MM-dd hh:mm:ss a')
 }
 
 onMounted(() => {
@@ -194,7 +219,7 @@ function validateNtpServer(server: string, field: 'server1' | 'server2') {
     return false
   }
   const domainRegex =
-    /^([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)+[a-z]{2,}$|^(\d{1,3}\.){3}\d{1,3}$/i
+      /^([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
@@ -233,100 +258,170 @@ async function saveSettings() {
 </script>
 
 <style scoped lang="scss">
-.setting-container {
-  padding: 20px;
-  background: #f5f5f5;
-
-  .section {
-    margin-bottom: 20px;
-    border-radius: 2px;
-    border-left: 3px solid #409eff;
-    padding: 0;
+// 颜色和尺寸变量定义
+$primary-color: #3b82f6;
+$primary-hover: #2563eb;
+$text-primary: #1f2d3d;
+$text-secondary: #333;
+$text-tertiary: #666;
+$border-color: #dcdfe6;
+$border-light: #eeeeee;
+$divider-color: #f0f0f0;
+$background-light: #fafafa;
+$error-color: #f56c6c;
+$success-color: #67c23a;
+
+$spacing-xs: 6px;
+$spacing-sm: 8px;
+$spacing-md: 12px;
+$spacing-lg: 16px;
+$spacing-xl: 20px;
+
+$radius-sm: 4px;
+$radius-md: 6px;
+
+$transition-easing: cubic-bezier(0.4, 0, 0.2, 1);
+
+.time-settings {
+  max-width: 700px;
+  //margin: 0 auto;
+  padding: 0;
+  display: flex;
+  flex-direction: column;
+  gap: 15px;
+
+  // 系统时间部分 - 独立卡片
+  .time-section-wrapper {
+    .section {
+      padding: $spacing-xl;
+      background: #fff;
+      border-radius: 8px;
+      box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+      margin: 0;
+    }
+  }
+
+  // 设置部分 - 组合卡片(一个整体)
+  .settings-section-wrapper {
+    background: #fff;
+    border-radius: 8px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
     overflow: hidden;
 
-    .section-title {
-      font-size: 14px;
-      font-weight: 600;
-      color: #333333;
-      padding: 14px 20px;
-      background: #fafafa;
-      border-bottom: 1px solid #eeeeee;
+    .section {
+      padding: $spacing-xl;
+      background: transparent;
+      border-radius: 0;
+      box-shadow: none;
       margin: 0;
+      border-bottom: 1px solid $divider-color;
+
+      &:last-of-type {
+        border-bottom: none;
+      }
     }
 
-    .form-content {
-      padding: 16px 20px;
+    .button-group {
       background: transparent;
+      border-radius: 0;
+      box-shadow: none;
+      margin: 0;
+      padding: $spacing-xl;
+      border-top: 1px solid $divider-color;
+      display: flex;
+      gap: $spacing-lg;
+      flex-wrap: wrap;
+    }
+  }
+
+  .section-header {
+    padding-bottom: $spacing-lg;
+    margin-bottom: $spacing-lg;
+    border-bottom: 2px solid $divider-color;
+
+    h3 {
+      font-size: 15px;
+      font-weight: 600;
+      color: $text-primary;
+      margin: 0;
     }
   }
 
+  .section-content {
+    display: flex;
+    flex-direction: column;
+    gap: $spacing-lg;
+  }
+
   .form-item {
     display: flex;
     align-items: center;
-    gap: 16px;
-    margin-bottom: 16px;
-
-    &:last-child {
-      margin-bottom: 0;
-    }
+    gap: $spacing-lg;
 
     .form-label {
-      display: block;
       min-width: 140px;
-      font-size: 13px;
-      color: #666666;
+      font-size: 14px;
       font-weight: 500;
+      color: $text-secondary;
+      flex-shrink: 0;
 
       &.required::after {
         content: '*';
-        color: #f56c6c;
+        color: $error-color;
         margin-left: 4px;
       }
     }
 
     .form-control {
-      width: 180px;
+      width: 200px;
+      font-size: 13px;
     }
 
     .time-value {
-      font-size: 13px;
-      color: #409eff;
+      font-size: 14px;
+      color: $primary-color;
       font-family: 'Monaco', 'Courier New', monospace;
       letter-spacing: 0.5px;
+      font-weight: 500;
     }
 
-    .form-btn {
-      font-size: 12px;
-      width: auto;
-      padding: 6px 16px;
-      height: 32px;
-      background-color: #409eff;
-      color: #fff;
-      border: none;
-      border-radius: 6px;
+    .sync-btn {
+      background-color: $primary-color !important;
+      color: #fff !important;
+      border: none !important;
+      height: 30px;
+      min-width: 80px;
       font-weight: 500;
-      cursor: pointer;
-      transition: all 0.3s ease;
+      font-size: 12px;
+      box-shadow: 0 2px 4px rgba($primary-color, 0.2);
+      transition: all 0.2s $transition-easing;
 
       &:hover {
-        background-color: #66b1ff;
+        background-color: $primary-hover !important;
+        box-shadow: 0 4px 8px rgba($primary-hover, 0.3);
       }
 
       &:active {
-        background-color: #3a8ee6;
+        box-shadow: 0 1px 2px rgba($primary-hover, 0.2);
+        transform: translateY(1px);
       }
     }
   }
 
+  // NTP 字段展开区域
   .ntp-fields {
-    margin-top: 12px;
-    padding-top: 12px;
-    border-top: 1px solid #eeeeee;
+    padding: $spacing-lg;
+    margin-top: $spacing-md;
+    background: rgba($primary-color, 0.03);
+    border: 1px solid rgba($primary-color, 0.1);
+    border-radius: $radius-md;
+    animation: slideDown 0.3s $transition-easing;
 
     .form-item {
       flex-direction: column;
       align-items: flex-start;
-      margin-bottom: 14px;
+      gap: $spacing-sm;
+      margin-bottom: $spacing-lg;
 
       &:last-child {
         margin-bottom: 0;
@@ -334,94 +429,226 @@ async function saveSettings() {
 
       .form-label {
         min-width: auto;
-        margin-bottom: 6px;
+        margin-bottom: 0;
+      }
+
+      .control-wrapper {
+        width: 100%;
       }
 
       .form-control {
-        width: 240px;
+        width: 100%;
       }
     }
   }
 
+  // 控制元素样式
+  .control-wrapper {
+    width: 100%;
+  }
+
   .error-message {
     font-size: 12px;
-    color: #f56c6c;
-    margin-top: 4px;
-    margin-left: 0;
+    color: $error-color;
+    margin-top: $spacing-xs;
+    font-weight: 500;
   }
 
-  .button-group {
-    display: flex;
-    gap: 12px;
-    padding: 0;
+  .save-btn {
+    background-color: $primary-color !important;
+    color: #fff !important;
+    border: none !important;
+    min-width: 80px;
+    height: 36px;
+    font-weight: 500;
+    font-size: 14px;
+    box-shadow: 0 2px 4px rgba($primary-color, 0.2);
+    transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+
+    &:hover {
+      background-color: $primary-hover !important;
+      box-shadow: 0 4px 8px rgba($primary-hover, 0.3);
+    }
 
-    .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;
-        }
-      }
+    &:active {
+      box-shadow: 0 1px 2px rgba($primary-hover, 0.2);
+      transform: translateY(1px);
     }
-  }
-}
 
-/* 折叠展开动画 */
-.collapse-enter-active,
-.collapse-leave-active {
-  transition: all 0.3s ease;
-  max-height: 500px;
-  overflow: hidden;
+    &:disabled,
+    &.is-disabled {
+      background-color: #c0c4cc !important;
+      cursor: not-allowed;
+      box-shadow: none;
+    }
+  }
 }
 
-.collapse-enter-from,
-.collapse-leave-to {
-  opacity: 0;
-  max-height: 0;
-}
+// Element Plus 组件样式覆盖
 
+// Input 样式
 :deep(.el-input__wrapper) {
-  border-color: #dcdfe6;
-  background-color: #ffffff;
+  border-color: $border-color;
+  background-color: #fff;
+  transition: all 0.2s $transition-easing;
 
   &:hover {
     border-color: #b1b3b6;
   }
 
   &.is-focus {
-    border-color: #409eff;
+    border-color: $primary-color;
+    box-shadow: 0 0 0 2px rgba($primary-color, 0.1);
   }
 }
 
 :deep(.el-input__inner) {
   font-size: 13px;
-  color: #606266;
+  color: $text-secondary;
 
   &::placeholder {
     color: #a8abb2;
   }
 }
 
+// Select 样式
 :deep(.el-select__wrapper) {
-  border-color: #dcdfe6;
-  background-color: #ffffff;
+  border-color: $border-color;
+  background-color: #fff;
+  transition: all 0.2s $transition-easing;
 
   &:hover {
     border-color: #b1b3b6;
   }
+
+  &.is-focus {
+    border-color: $primary-color;
+    box-shadow: 0 0 0 2px rgba($primary-color, 0.1);
+  }
+}
+
+:deep(.el-select__selection-text) {
+  font-size: 13px;
+  color: $text-secondary;
 }
 
+:deep(.el-option__label) {
+  font-size: 13px;
+  color: $text-secondary;
+}
+
+// Switch 样式
 :deep(.el-switch) {
-  --el-switch-on-color: #409eff;
-  --el-switch-off-color: #dcdfe6;
+  --el-switch-on-color: $primary-color;
+  --el-switch-off-color: #909399; // 使用更深的灰色以提高对比度
+  height: 22px;
+
+  .el-switch__core {
+    border-color: #909399; // 设置边框颜色与关闭状态一致
+  }
+
+  &.is-checked .el-switch__core {
+    background-color: $primary-color;
+    border-color: $primary-color; // 选中状态边框与背景一致
+  }
+}
+
+// 按钮通用样式
+//:deep(.el-button) {
+//  //border-radius: $radius-md;
+//  transition: all 0.2s $transition-easing;
+//  font-weight: 500;
+//
+//  &:focus,
+//  &:hover {
+//    // 由 !important 样式覆盖处理
+//  }
+//}
+
+// 过渡动画 - NTP 字段展开
+.slide-ntp-enter-active,
+.slide-ntp-leave-active {
+  transition: all 0.3s $transition-easing;
+}
+
+.slide-ntp-enter-from {
+  opacity: 0;
+  transform: translateY(-10px);
+  max-height: 0;
+}
+
+.slide-ntp-leave-to {
+  opacity: 0;
+  transform: translateY(-10px);
+  max-height: 0;
+}
+
+@keyframes slideDown {
+  from {
+    opacity: 0;
+    transform: translateY(-8px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+// 响应式设计
+@media (max-width: 768px) {
+  .time-settings {
+    gap: $spacing-lg * 1.5;
+
+    .time-section-wrapper {
+      .section {
+        padding: $spacing-lg;
+        border-radius: 4px;
+      }
+    }
+
+    .settings-section-wrapper {
+      border-radius: 4px;
+
+      .section {
+        padding: $spacing-lg;
+      }
+
+      .button-group {
+        padding: $spacing-lg;
+      }
+    }
+
+    .form-item {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: $spacing-sm;
+
+      .form-label {
+        min-width: auto;
+      }
+
+      .form-control,
+      .sync-btn {
+        width: 100%;
+      }
+    }
+
+    .ntp-fields {
+      padding: $spacing-md;
+      margin-top: $spacing-sm;
+
+      .form-item {
+        margin-bottom: $spacing-md;
+      }
+    }
+
+    .button-group {
+      flex-direction: column;
+
+      .save-btn {
+        width: 100%;
+      }
+    }
+  }
 }
 </style>

+ 45 - 10
src/views/settings/systemSettings/components/user/index.vue

@@ -115,8 +115,8 @@
         </el-form-item>
 
         <div class="form-actions">
-          <el-button type="primary"  @click.prevent="handleConfirm">Save</el-button>
-          <el-button  @click="resetForm"> Reset </el-button>
+<!--          <el-button  @click="resetForm"> Reset </el-button>-->
+          <el-button class="btn-save" round @click.prevent="handleConfirm">Save</el-button>
         </div>
       </el-form>
     </div>
@@ -215,7 +215,11 @@ const handleRtspPwdEnChange = () => {
   putRtspPasswordApi({ RtspPwdEn: RtspPwdEn.value })
     .then((res:any) => {
       if (res.data == 'ok\n') {
-        ElMessage.success('Operation successful')
+        if(RtspPwdEn.value == 1) {
+          ElMessage.success('RTSP password authentication successfully enabled.')
+        } else {
+          ElMessage.success('RTSP password authentication successfully disabled.')
+        }
       } else {
         ElMessage.error('Failed to change RTSP password. Please try again.')
       }
@@ -260,19 +264,21 @@ const resetForm = () => {
 </script>
 
 <style lang="scss" scoped>
+$primary-color: #3b82f6;
+$primary-hover: #2563eb;
+
 .password-form-container {
   max-width: 700px;
+  padding: 10px;
   // margin: 0 auto;
-  //padding: 30px;
-  // background: #f5f7fa;
   border-radius: 8px;
 
   .settings-section {
-    // background: white;
+    background: white;
     border-radius: 8px;
     padding: 20px;
-    // margin-bottom: 15px;
-    // box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+     margin-bottom: 15px;
+     box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
 
     .section-header {
       margin-bottom: 20px;
@@ -395,9 +401,38 @@ const resetForm = () => {
     // justify-content: center;
     justify-content: flex-start;
 
-    .el-button {
-      min-width: 100px;
+    .btn-save {
+      background-color: $primary-color !important;
+      color: #fff !important;
+      border: none !important;
+      min-width: 80px;
+      height: 36px;
+      font-weight: 500;
+      font-size: 14px;
+      box-shadow: 0 2px 4px rgba($primary-color, 0.2);
+      transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+
+      &:hover {
+        background-color: $primary-hover !important;
+        box-shadow: 0 4px 8px rgba($primary-hover, 0.3);
+      }
+
+      &:active {
+        box-shadow: 0 1px 2px rgba($primary-hover, 0.2);
+        transform: translateY(1px);
+      }
+
+      &:disabled,
+      &.is-disabled {
+        background-color: #c0c4cc !important;
+        cursor: not-allowed;
+        box-shadow: none;
+      }
     }
+
+    //.el-button {
+    //  min-width: 100px;
+    //}
   }
 }
 

+ 297 - 0
src/views/standardProtocol/components/onvif/index.vue

@@ -0,0 +1,297 @@
+<template>
+  <div class="onvif-container">
+    <div class="settings-section">
+      <!-- Title -->
+      <div class="section-header">
+        <h3>ONVIF Protocol Configuration</h3>
+      </div>
+
+      <!-- ONVIF Settings Form -->
+      <el-form :model="onvifConfig" label-width="125px" class="onvif-form">
+        <!-- Enable ONVIF -->
+        <el-form-item label="Enable ONVIF">
+          <el-switch v-model="onvifConfig.OnvifEnable" />
+        </el-form-item>
+
+        <!-- IP Address -->
+        <el-form-item label="IP Address">
+          <el-input v-model="onvifConfig.IpAddr" placeholder="Enter IP address" />
+        </el-form-item>
+
+        <!-- Port -->
+        <el-form-item label="Port">
+          <el-input v-model="onvifConfig.Port" placeholder="Enter port number" />
+        </el-form-item>
+
+        <!-- ONVIF Username -->
+        <el-form-item label="ONVIF Username">
+          <el-input v-model="onvifConfig.OnvifName" placeholder="Enter ONVIF username" />
+        </el-form-item>
+
+        <!-- ONVIF Password -->
+        <el-form-item label="ONVIF Password">
+          <el-input v-model="onvifConfig.Password" type="password" placeholder="Enter ONVIF password" show-password />
+        </el-form-item>
+
+        <!-- Save Button -->
+        <el-form-item>
+          <el-button type="primary" @click="saveOnvifConfig" :loading="saving">Save</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- Access Example -->
+<!--      <div class="examples-section" v-if="onvifConfig.OnvifEnable">-->
+<!--        <h4>ONVIF Device Manager Access Example</h4>-->
+<!--        <div class="example-box">-->
+<!--          <div class="example-title">Using ONVIF Device Manager</div>-->
+<!--          <div class="code-block">-->
+<!--            <code>{{ onvifAccessUrl }}</code>-->
+<!--            <el-button type="primary" @click="copyToClipboard(onvifAccessUrl)">-->
+<!--              Copy-->
+<!--            </el-button>-->
+<!--          </div>-->
+<!--          <div class="tip-text">-->
+<!--            Use the above URL with your ONVIF Device Manager software to connect to the camera.-->
+<!--          </div>-->
+<!--        </div>-->
+<!--      </div>-->
+    </div>
+
+    <!-- Save Success Toast -->
+    <Transition name="toast">
+      <div v-if="showSaveToast" class="toast">
+        ✓ Configuration saved successfully
+      </div>
+    </Transition>
+
+    <!-- Copy Success Toast -->
+    <Transition name="toast">
+      <div v-if="showCopyToast" class="toast">
+        ✓ Copied to clipboard
+      </div>
+    </Transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import { getOnvifProtocolApi, OnvifProtocolApi } from '@/api/protocol'
+import {ElMessage} from 'element-plus'
+
+
+const onvifConfig = ref({
+  OnvifEnable: false,
+  IpAddr: '',
+  Port: '',
+  OnvifName: '',
+  Password: ''
+})
+
+const saving = ref(false)
+const showSaveToast = ref(false)
+const showCopyToast = ref(false)
+
+// Computed ONVIF access URL
+// const onvifAccessUrl = computed(() => {
+//   return `http://${onvifConfig.value.IpAddr}:${onvifConfig.value.Port}/onvif/device_service`
+// })
+
+onMounted(async () => {
+  try {
+    const resp = await getOnvifProtocolApi()
+    if (resp.data) {
+      onvifConfig.value = {
+        OnvifEnable: resp.data.OnvifEnable || false,
+        IpAddr: resp.data.IpAddr,
+        Port: resp.data.Port,
+        OnvifName: resp.data.OnvifName,
+        Password: resp.data.Password
+      }
+    }
+  } catch (error) {
+    console.error('Failed to fetch ONVIF configuration:', error)
+  }
+})
+
+const saveOnvifConfig = async () => {
+  saving.value = true
+  try {
+    const response = await OnvifProtocolApi(onvifConfig.value)
+    if (response.data === 'ok\n') {
+      ElMessage.success('Configuration saved successfully')
+    } else {
+      ElMessage.error('Failed to save ONVIF configuration')
+    }
+  } catch (error) {
+    ElMessage.error('Failed to save ONVIF configuration:', error)
+  } finally {
+    saving.value = false
+  }
+}
+
+// // Copy to clipboard
+// const copyToClipboard = async (text: string) => {
+//   try {
+//     await navigator.clipboard.writeText(text)
+//     showCopySuccess()
+//   } catch (error) {
+//     console.error('Failed to copy:', error)
+//   }
+// }
+//
+// // Show copy success toast
+// const showCopySuccess = () => {
+//   showCopyToast.value = true
+//   setTimeout(() => {
+//     showCopyToast.value = false
+//   }, 2000)
+// }
+</script>
+
+<style lang="scss" scoped>
+.onvif-container {
+  max-width: 800px;
+
+  .settings-section {
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 24px;
+    border: 1px solid #e0e0e0;
+
+    .section-header {
+      margin-bottom: 24px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid #f0f0f0;
+
+      h3 {
+        font-size: 18px;
+        font-weight: 600;
+        color: #1f2937;
+        margin: 0;
+        letter-spacing: -0.3px;
+      }
+    }
+  }
+}
+
+// ONVIF Form
+.onvif-form {
+  margin-bottom: 28px;
+
+  .el-form-item {
+    margin-bottom: 16px;
+  }
+
+  .el-button {
+    margin-top: 8px;
+  }
+}
+
+// Examples Section
+.examples-section {
+  h4 {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1f2937;
+    margin: 0 0 14px 0;
+    letter-spacing: -0.2px;
+  }
+
+  .example-box {
+    background: #f9fafb;
+    border: 1px solid #e5e7eb;
+    border-radius: 6px;
+    padding: 18px;
+    margin-bottom: 14px;
+    transition: all 0.3s ease;
+
+    &:hover {
+      background: #ffffff;
+      border-color: #d1d5db;
+    }
+
+    .example-title {
+      font-size: 13px;
+      font-weight: 600;
+      color: #1f2937;
+      margin-bottom: 14px;
+      letter-spacing: -0.2px;
+    }
+
+    .code-block {
+      background: #ffffff;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      padding: 14px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      gap: 12px;
+      margin-bottom: 12px;
+
+      code {
+        font-size: 12px;
+        color: #0ea5e9;
+        font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+        flex: 1;
+        word-break: break-all;
+        letter-spacing: 0.2px;
+      }
+    }
+
+    .tip-text {
+      font-size: 12px;
+      color: #4b5563;
+      line-height: 1.7;
+      letter-spacing: 0.2px;
+
+      code {
+        background: #f0f1f3;
+        padding: 3px 7px;
+        border-radius: 3px;
+        font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+        font-size: 11px;
+        color: #0284c7;
+      }
+    }
+  }
+}
+
+// Toast Notification
+.toast {
+  position: fixed;
+  bottom: 24px;
+  right: 24px;
+  background-color: rgba(38, 38, 38, 0.95);
+  color: white;
+  padding: 12px 20px;
+  border-radius: 6px;
+  font-size: 13px;
+  font-weight: 500;
+  z-index: 1000;
+  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
+  animation: slideInUp 0.3s ease;
+}
+
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.toast-enter-active,
+.toast-leave-active {
+  transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.toast-enter-from,
+.toast-leave-to {
+  opacity: 0;
+  transform: translateY(20px);
+}
+</style>

+ 417 - 0
src/views/standardProtocol/components/rtsp/index.vue

@@ -0,0 +1,417 @@
+<template>
+  <div class="rtsp-container">
+    <div class="settings-section">
+      <!-- Title -->
+      <div class="section-header">
+        <h3>RTSP Stream Media Access Configuration</h3>
+      </div>
+
+      <!-- Status and IP Information -->
+      <div class="info-section">
+        <div class="info-item">
+          <div class="label-with-tooltip">
+            <label>Password Verification Status</label>
+            <span class="info-icon" title="This feature can be configured in System Settings > Password Management">
+              ?
+            </span>
+          </div>
+          <span class="status-badge" :class="{ enabled: passwordVerificationEnabled }">
+            {{ passwordVerificationEnabled ? 'Enabled' : 'Disabled' }}
+          </span>
+        </div>
+        <div class="info-item">
+          <label>Camera IP Address</label>
+          <span class="ip-address">{{ cameraIp }}</span>
+        </div>
+      </div>
+
+      <!-- Access Examples -->
+      <div class="examples-section">
+        <h4>Access URL Examples</h4>
+
+        <!-- No Password Example -->
+        <div v-if="!passwordVerificationEnabled" class="example-box">
+          <div class="example-title">Without Password Authentication</div>
+          <div class="code-block">
+            <code>rtsp://{{ cameraIp }}:554/video1</code>
+            <el-button type="primary" @click="copyToClipboard(`rtsp://${cameraIp}:554/video1`)">
+              Copy
+            </el-button>
+          </div>
+          <div class="tip-text">
+            Replace the IP address with your actual camera address. Ensure access from the same local network.
+          </div>
+        </div>
+
+        <!-- Password Example -->
+        <div v-else class="example-box password-example">
+          <div class="example-title">With Password Authentication (Web Admin Credentials Required)</div>
+          <div class="code-block">
+            <code>rtsp://username:password@{{ cameraIp }}:554/video1</code>
+            <el-button type="primary" @click="copyExample()">
+              Copy
+            </el-button>
+          </div>
+          <div class="tip-text warning">
+            ⚠️ Replace <strong>username</strong> and <strong>password</strong> with your Web admin interface credentials.<br>
+            Example: <code>rtsp://admin:password123@192.168.0.105:554/video1</code>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Copy Success Toast -->
+    <Transition name="toast">
+      <div v-if="showCopyToast" class="toast">
+        ✓ Copied to clipboard
+      </div>
+    </Transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+
+const passwordVerificationEnabled = ref(false)
+const cameraIp = ref('192.168.0.105')
+const showCopyToast = ref(false)
+
+// Fetch configuration
+onMounted(async () => {
+  try {
+    const response = await fetch('/api/camera/config')
+    if (response.ok) {
+      const data = await response.json()
+      passwordVerificationEnabled.value = data.passwordEnabled === true
+      cameraIp.value = data.cameraIp || '192.168.0.105'
+    }
+  } catch (error) {
+    console.error('Failed to fetch configuration:', error)
+  }
+})
+
+// Copy to clipboard
+const copyToClipboard = async (text: string) => {
+  try {
+    await navigator.clipboard.writeText(text)
+    showToast()
+  } catch (error) {
+    console.error('Failed to copy:', error)
+  }
+}
+
+// Copy example
+const copyExample = () => {
+  const exampleText = `rtsp://username:password@${cameraIp.value}:554/video1`
+  copyToClipboard(exampleText)
+}
+
+// Show toast notification
+const showToast = () => {
+  showCopyToast.value = true
+  setTimeout(() => {
+    showCopyToast.value = false
+  }, 2000)
+}
+</script>
+
+<style lang="scss" scoped>
+.rtsp-container {
+  max-width: 800px;
+
+  .settings-section {
+    background: #ffffff;
+    border-radius: 8px;
+    padding: 24px;
+    border: 1px solid #e0e0e0;
+
+    .section-header {
+      margin-bottom: 24px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid #f0f0f0;
+
+      h3 {
+        font-size: 18px;
+        font-weight: 600;
+        color: #1f2937;
+        margin: 0;
+        letter-spacing: -0.3px;
+      }
+    }
+  }
+}
+
+// Info Section
+.info-section {
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: 20px;
+  margin-bottom: 28px;
+
+  @media (max-width: 600px) {
+    grid-template-columns: 1fr;
+    gap: 16px;
+  }
+
+  .info-item {
+    display: flex;
+    flex-direction: column;
+    gap: 8px;
+
+    .label-with-tooltip {
+      display: flex;
+      align-items: center;
+      gap: 6px;
+    }
+
+    label {
+      font-size: 12px;
+      color: #6b7280;
+      text-transform: uppercase;
+      letter-spacing: 0.8px;
+      font-weight: 600;
+    }
+
+    .info-icon {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 18px;
+      height: 18px;
+      border-radius: 50%;
+      background: #dbeafe;
+      color: #1e40af;
+      font-size: 12px;
+      font-weight: 700;
+      cursor: help;
+      transition: all 0.3s ease;
+      position: relative;
+
+      &:hover {
+        background: #bfdbfe;
+        transform: scale(1.1);
+
+        &::after {
+          content: attr(title);
+          position: absolute;
+          bottom: 130%;
+          left: 50%;
+          transform: translateX(-50%);
+          background: #1f2937;
+          color: white;
+          padding: 8px 12px;
+          border-radius: 6px;
+          font-size: 12px;
+          font-weight: 400;
+          white-space: nowrap;
+          z-index: 1000;
+          box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
+          text-transform: none;
+          letter-spacing: normal;
+        }
+
+        &::before {
+          content: '';
+          position: absolute;
+          bottom: 120%;
+          left: 50%;
+          transform: translateX(-50%);
+          border: 6px solid transparent;
+          border-top-color: #1f2937;
+          z-index: 1000;
+        }
+      }
+    }
+
+    .status-badge {
+      padding: 8px 12px;
+      border-radius: 4px;
+      background: #f3f4f6;
+      color: #6b7280;
+      font-size: 13px;
+      font-weight: 500;
+      width: fit-content;
+      transition: all 0.3s ease;
+
+      &.enabled {
+        background: #dbeafe;
+        color: #1e40af;
+      }
+    }
+
+    .ip-address {
+      font-size: 14px;
+      color: #1f2937;
+      font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+      font-weight: 500;
+      letter-spacing: 0.3px;
+    }
+  }
+}
+
+// Examples Section
+.examples-section {
+  h4 {
+    font-size: 14px;
+    font-weight: 600;
+    color: #1f2937;
+    margin: 0 0 14px 0;
+    letter-spacing: -0.2px;
+  }
+
+  .example-box {
+    background: #f9fafb;
+    border: 1px solid #e5e7eb;
+    border-radius: 6px;
+    padding: 18px;
+    margin-bottom: 14px;
+    transition: all 0.3s ease;
+
+    &:hover {
+      background: #ffffff;
+      border-color: #d1d5db;
+    }
+
+    &.password-example {
+      background: #fffbf0;
+      border-color: #fed7aa;
+
+      &:hover {
+        background: #fffbf0;
+        border-color: #fdba74;
+      }
+    }
+
+    .example-title {
+      font-size: 13px;
+      font-weight: 600;
+      color: #1f2937;
+      margin-bottom: 14px;
+      letter-spacing: -0.2px;
+    }
+
+    .code-block {
+      background: #ffffff;
+      border: 1px solid #e5e7eb;
+      border-radius: 6px;
+      padding: 14px;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      gap: 12px;
+      margin-bottom: 12px;
+
+      code {
+        font-size: 12px;
+        color: #0ea5e9;
+        font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+        flex: 1;
+        word-break: break-all;
+        letter-spacing: 0.2px;
+      }
+
+      //.copy-btn {
+      //  background: linear-gradient(135deg, #0ea5e9 0%, #0284c7 100%);
+      //  color: white;
+      //  border: none;
+      //  border-radius: 6px;
+      //  padding: 7px 14px;
+      //  font-size: 12px;
+      //  font-weight: 600;
+      //  cursor: pointer;
+      //  white-space: nowrap;
+      //  flex-shrink: 0;
+      //  transition: all 0.3s ease;
+      //  box-shadow: 0 2px 4px rgba(6, 182, 212, 0.2);
+      //
+      //  &:hover {
+      //    background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
+      //    box-shadow: 0 4px 8px rgba(6, 182, 212, 0.3);
+      //    transform: translateY(-1px);
+      //  }
+      //
+      //  &:active {
+      //    transform: scale(0.98);
+      //  }
+      //}
+    }
+
+    .tip-text {
+      font-size: 12px;
+      color: #4b5563;
+      line-height: 1.7;
+      letter-spacing: 0.2px;
+
+      &.warning {
+        color: #b45309;
+        background: rgba(180, 83, 9, 0.05);
+        padding: 12px;
+        border-radius: 4px;
+        border-left: 3px solid #b45309;
+
+        strong {
+          color: #92400e;
+          font-weight: 600;
+        }
+
+        code {
+          background: rgba(180, 83, 9, 0.1);
+          padding: 3px 7px;
+          border-radius: 3px;
+          font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+          font-size: 11px;
+          color: #b45309;
+        }
+      }
+
+      code {
+        background: #f0f1f3;
+        padding: 3px 7px;
+        border-radius: 3px;
+        font-family: 'Monaco', 'Courier New', 'Menlo', monospace;
+        font-size: 11px;
+        color: #0284c7;
+      }
+    }
+  }
+}
+
+// Toast Notification
+.toast {
+  position: fixed;
+  bottom: 24px;
+  right: 24px;
+  background-color: rgba(38, 38, 38, 0.95);
+  //background: linear-gradient(135deg, #10b981 0%, #059669 100%);
+  color: white;
+  padding: 12px 20px;
+  border-radius: 6px;
+  font-size: 13px;
+  font-weight: 500;
+  z-index: 1000;
+  box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
+  animation: slideInUp 0.3s ease;
+}
+
+@keyframes slideInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+.toast-enter-active,
+.toast-leave-active {
+  transition: opacity 0.3s ease, transform 0.3s ease;
+}
+
+.toast-enter-from,
+.toast-leave-to {
+  opacity: 0;
+  transform: translateY(20px);
+}
+</style>

+ 29 - 0
src/views/standardProtocol/index.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="settings-container">
+    <el-tabs v-model="activeName" class="tabs">
+      <el-tab-pane label="RTSP" name="first">
+        <RTSP/>
+      </el-tab-pane>
+
+      <el-tab-pane label="ONVIF" name="second">
+        <Onvif/>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<script setup lang="ts">
+import {ref} from 'vue'
+import RTSP from './components/rtsp/index.vue'
+import Onvif from './components/onvif/index.vue'
+
+const activeName = ref('first')
+</script>
+
+<style scoped lang="scss">
+.settings-container {
+  display: flex;
+  flex-direction: column;
+  padding: 20px;
+}
+</style>