Browse Source

增加rtsp密码校验
BUG修复和功能优化

liujintao 2 tuần trước cách đây
mục cha
commit
fa9cb790b9

+ 19 - 2
src/api/userManagement.ts

@@ -1,10 +1,27 @@
 import { request } from '@/utils/request'
-import type * as UserManagement from './types/userManagement'
 
-export function putPasswordApi(data: UserManagement.UpdatePasswordRequestData) {
+export function putPasswordApi(data: any) {
   return request({
     url: 'API/V1.0/Auth/WebUser',
     method: 'put',
     data
   })
 }
+
+export function getRtspPasswordApi() {
+  return request({
+    url: '/API/V1.0/Video/RtspPwd',
+    method: 'get'
+  })
+}
+
+export function putRtspPasswordApi(data: any) {
+  return request({
+    url: '/API/V1.0/Video/RtspPwd',
+    method: 'put',
+    data
+  })
+}
+
+
+

+ 1 - 1
src/assets/base.css

@@ -8,7 +8,7 @@
   /** NavigationBar 组件 */
   --v3-navigationbar-height: 50px;
   /** Sidebar 组件(左侧模式全部生效、顶部模式全部不生效、混合模式非颜色部分生效) */
-  --v3-sidebar-width: 168px;
+  --v3-sidebar-width: 164px;
   --v3-sidebar-hide-width: 58px;
   --v3-sidebar-menu-item-height: 60px;
   --v3-sidebar-menu-tip-line-bg-color: var(--el-color-primary);

+ 4 - 5
src/layouts/components/SideBar/index.vue

@@ -62,15 +62,14 @@ $transition-time: 0.35s;
       background-color: #002140;
       color: #fff;
       font-weight: 700;
-      line-height: 45px;
-      font-size: 15px;
+      line-height: 24px;
+      font-size: 16px;
       display: flex;
       align-items: center;
       justify-content: center;
-      //flex-direction: column;
+      flex-direction: column;
       text-align: center;
-
-      //white-space: pre-line;
+      white-space: pre-line;
     }
     .sidebar-none {
       color: #fff;

+ 4 - 26
src/views/login/index.vue

@@ -81,8 +81,10 @@
         <div class="password-input-container">
           <el-input
             v-model="superPassword"
-            placeholder="Enter Super Password"
+            placeholder="Please enter the super password"
             size="default"
+            type="password"
+            show-password
           />
         </div>
       </div>
@@ -123,7 +125,7 @@ const loginFormData: LoginRequestData = reactive({
   code: ''
 })
 
-const devicePassword = ref('二维码获取失败')
+const devicePassword = ref('Failed to get QR code')
 const superPassword = ref('')
 const level = ref('H')
 
@@ -456,30 +458,6 @@ const handleLogin = () => {
   }
 }
 
-/* 对话框关闭按钮 - 关键修复 */
-:deep(.el-dialog__close) {
-  color: white;
-  font-size: 25px;
-  cursor: pointer;
-  transition: all 0.3s ease;
-  position: relative;
-  width: auto;
-  height: auto;
-  padding: 0px;
-  border: none;
-  background: none;
-  outline: none;
-
-  &:hover {
-    opacity: 0.8;
-    transform: rotate(90deg);
-  }
-
-  &:focus {
-    outline: none;
-  }
-}
-
 :deep(.el-dialog__body) {
   padding: 24px 0 0;
   background-color: #fff;

+ 59 - 3
src/views/setting/netSetting/index.vue

@@ -101,6 +101,9 @@ import {
 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";
+const router = useRouter()
 
 const IP_version = ref('IPV4')
 const options = [
@@ -120,14 +123,23 @@ const settingFormData: UpdateSettingRequestData = reactive({
   gateWayAddress: '',
   deviceMac: ''
 })
+
 /** 表单元素的引用 */
 const settingFormRef = ref<FormInstance | null>(null)
+
 /** 表单校验规则 */
 const settingFormRules: FormRules = {
   IP: [
     {
       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'))
+              return
+            }
+          }
           callback()
         } else {
           callback(new Error('Please enter the IP'))
@@ -152,6 +164,13 @@ 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'))
+              return
+            }
+          }
           callback()
         } else {
           callback(new Error('Please enter the default gateway'))
@@ -161,6 +180,42 @@ const settingFormRules: FormRules = {
     }
   ]
 }
+
+// 检查IP地址和网关地址是否在同一个网段
+const isSameSubnet = (ip: string, gateway: string, subnetMask: string): boolean => {
+  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
+    }
+
+    // 计算网络地址
+    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
+      }
+    }
+
+    return true
+  } catch (error) {
+    console.error('网段验证错误:', error)
+    return false
+  }
+}
+
+
 const switchChange = ($event: number) => {
   if ($event === 1) {
     settingFormRef.value?.clearValidate()
@@ -196,7 +251,9 @@ const handleSave = () => {
           loading.value = true
           putUserSettingApi(param.NIC, settingFormData)
             .then(() => {
-              ElMessage.success('Operation successful')
+              ElMessage.success('Operation successful. Please wait a moment...')
+              useUserStore().logout()
+              router.push('/login')
             })
             .finally(() => {
               loading.value = false
@@ -221,7 +278,7 @@ const handleSave = () => {
 .setting-card {
   width: 100%;
   max-width: 510px;
-  //background: white;
+  background: white;
   border-radius: 12px;
   box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
   overflow: hidden;
@@ -233,7 +290,6 @@ const handleSave = () => {
 }
 
 .card-header {
-  //background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
   color: #000104;
   padding: 20px 20px 0 20px;
   text-align: center;

+ 4 - 3
src/views/setting/systemSetting/components/alarm/index.vue

@@ -18,7 +18,7 @@
         size="large"
         @click="handleCloseAlarm"
         class="btn-close"
-        :loading="loading"
+        :loading="DisableLoading"
       >
         Disable Manual Alarm
       </el-button>
@@ -36,10 +36,11 @@ 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
-  loading.value = true
+  DisableLoading.value = true
   try {
     const res = await cameraAlarm({
       AlarmSettings: 0
@@ -52,7 +53,7 @@ async function handleCloseAlarm() {
   } catch (error) {
     ElMessage.error('Manual Alarm Deactivation Error')
   } finally {
-    loading.value = false
+    DisableLoading.value = false
   }
 }
 

+ 328 - 80
src/views/setting/systemSetting/components/user/index.vue

@@ -1,47 +1,104 @@
 <template>
-  <div v-loading="loading" class="user-password-form">
-    <h3>Camera Password Settings</h3>
-    <el-form
-      ref="formRef"
-      :model="userManagementFormData"
-      :rules="userManagementFormRules"
-      @keyup.enter="handleConfirm"
-    >
-      <el-form-item prop="oldPassword">
-        <el-input
-          v-model.trim="userManagementFormData.oldPassword"
-          placeholder="Old Password"
-          size="large"
-          tabindex="1"
-          type="password"
-          show-password
-          class="el-input"
-        />
-      </el-form-item>
-      <el-form-item prop="newPassword">
-        <el-input
-          v-model.trim="userManagementFormData.newPassword"
-          placeholder="New Password"
-          size="large"
-          tabindex="2"
-          type="password"
-          show-password
-        />
-      </el-form-item>
-      <el-form-item prop="newPasswordSc">
-        <el-input
-          v-model.trim="userManagementFormData.newPasswordSc"
-          placeholder="Confirm New Password"
-          size="large"
-          tabindex="3"
-          type="password"
-          show-password
-        />
-      </el-form-item>
-      <el-button type="primary" @click.prevent="handleConfirm">
-        Save
-      </el-button>
-    </el-form>
+  <div class="password-form-container">
+    <!-- RTSP Password Verification Switch -->
+    <div class="settings-section" v-loading="RtspLoading">
+      <div class="section-header">
+        <h3>RTSP Password Verification</h3>
+      </div>
+      <div class="switch-container">
+        <div class="switch-content">
+          <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' }}
+            </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"
+          />
+        </div>
+      </div>
+
+      <!-- RTSP Access Examples -->
+      <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>
+            <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>
+          </div>
+          <div class="tip-item">
+            <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>
+          </div>
+        </div>
+      </div>
+    </div>
+
+    <!-- Password Change Form -->
+    <div v-loading="loading" class="settings-section">
+      <div class="section-header">
+        <h3>Change Password</h3>
+      </div>
+      <el-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-input
+              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-input
+              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-input
+              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>
+        </div>
+      </el-form>
+    </div>
   </div>
 </template>
 
@@ -50,16 +107,18 @@ import {
   type FormInstance,
   type FormRules,
   ElMessage,
-  ElLoading
 } from 'element-plus'
-import { putPasswordApi } from '@/api/userManagement'
+import { putPasswordApi, getRtspPasswordApi, putRtspPasswordApi } from '@/api/userManagement'
 import { useUserStore } from '@/stores/modules/user'
-import { ref } from 'vue'
+import { ref, reactive } from 'vue'
 import { useRouter } from 'vue-router'
 
-/** 表单元素的引用 */
 const formRef = ref<FormInstance | null>(null)
-const version = import.meta.env.VITE_APP_TITLE
+const RtspPwdEn = ref(1)
+const loading = ref(false)
+const router = useRouter()
+
+const  RtspLoading = ref(false)
 
 const userManagementFormData = reactive({
   name: useUserStore().username || 'admin',
@@ -67,17 +126,19 @@ const userManagementFormData = reactive({
   newPassword: '',
   newPasswordSc: ''
 })
-/** 表单校验规则 */
+
 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' }
   ],
   newPassword: [
-    { required: true, message: 'Please enter your new password.', 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' },
     {
       validator: (rule, value, callback) => {
         if (value !== userManagementFormData.newPassword) {
@@ -91,47 +152,234 @@ const userManagementFormRules: FormRules = {
   ]
 }
 
-const router = useRouter()
-const loading = ref(false)
 
-/** 登录按钮 Loading */
+function getRtspPassword() {
+  getRtspPasswordApi().then(res => {
+    RtspPwdEn.value = res.data.RtspPwdEn
+  })
+}
+
+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()
+  })
+}
+
 const handleConfirm = () => {
-  formRef.value?.validate((valid: boolean, fields) => {
+  formRef.value?.validate((valid: boolean) => {
     if (valid) {
-      let loading = ElLoading.service()
+      loading.value = true
       putPasswordApi(userManagementFormData)
-        .then((res) => {
-          if (res.data == 'ok\n') {
-            ElMessage.success('Success')
-            useUserStore().logout() // 退出登录
-            router.push('/login')
-          } else {
-            ElMessage.error('Failed')
-          }
-          loading.close()
-        })
-        .catch((error) => {
-          loading.close()
-          ElMessage.success('Failed' + error)
-        })
+          .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
+          })
     } else {
       ElMessage.error('Please check your input')
     }
   })
 }
+
+const resetForm = () => {
+  formRef.value?.resetFields()
+}
 </script>
 
 <style lang="scss" scoped>
-.user-password-form{
-  //margin-top: 20px;
-  margin-left: 10px;
-  display: flex;
-  flex-direction: column;
-  //align-items: center;
-  //justify-content: center;
-}
-.el-input{
-  width: 300px;
+.password-form-container {
+  max-width: 700px;
+  margin: 0 auto;
+  //padding: 30px;
+  background: #f5f7fa;
+  border-radius: 8px;
+
+  .settings-section {
+    background: white;
+    border-radius: 8px;
+    padding: 25px;
+    margin-bottom: 20px;
+    box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
+
+    .section-header {
+      margin-bottom: 20px;
+      border-bottom: 2px solid #f0f0f0;
+      padding-bottom: 15px;
+
+      h3 {
+        font-size: 16px;
+        font-weight: 600;
+        color: #1f2d3d;
+        margin: 0;
+      }
+    }
+  }
+
+  .switch-container {
+    .switch-content {
+      display: flex;
+      align-items: center;
+      justify-content: space-between;
+      padding: 15px 0;
+
+      .switch-info {
+        flex: 1;
+
+        .switch-label {
+          font-size: 15px;
+          font-weight: 500;
+          color: #1f2d3d;
+          margin: 0 0 5px 0;
+        }
+
+        .switch-description {
+          font-size: 13px;
+          color: #909399;
+          margin: 0;
+        }
+      }
+    }
+  }
+
+  .tips-box {
+    background: #f5f7fa;
+    border-left: 4px solid #409eff;
+    border-radius: 4px;
+    padding: 15px;
+    margin-top: 20px;
+
+    .tips-title {
+      font-size: 14px;
+      font-weight: 600;
+      color: #1f2d3d;
+      margin-bottom: 12px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .tips-icon {
+        font-size: 16px;
+      }
+    }
+
+    .tips-content {
+      .tip-item {
+        margin-bottom: 15px;
+
+        &:last-child {
+          margin-bottom: 0;
+        }
+
+        .tip-label {
+          font-size: 13px;
+          font-weight: 500;
+          color: #606266;
+          margin: 0 0 8px 0;
+        }
+
+        code {
+          display: block;
+          background: white;
+          border: 1px solid #e4e7eb;
+          border-radius: 4px;
+          padding: 10px 12px;
+          font-family: 'Monaco', 'Courier New', monospace;
+          font-size: 12px;
+          color: #d63384;
+          word-break: break-all;
+          margin-bottom: 8px;
+        }
+
+        .tip-note {
+          font-size: 12px;
+          color: #909399;
+          margin: 0;
+        }
+      }
+    }
+  }
+
+  .password-form {
+    :deep(.el-form-item) {
+      margin-bottom: 20px;
+
+      &:last-of-type {
+        margin-bottom: 0;
+      }
+    }
+
+    :deep(.el-form-item__label) {
+      color: #606266;
+      font-weight: 500;
+    }
+  }
+
+  .form-actions {
+    display: flex;
+    gap: 12px;
+    margin-top: 30px;
+    justify-content: center;
+
+    .el-button {
+      min-width: 120px;
+    }
+  }
 }
-</style>
 
+/* Responsive Design */
+@media (max-width: 768px) {
+  .password-form-container {
+    padding: 20px;
+    max-width: 100%;
+
+    .form-header {
+      margin-bottom: 30px;
+
+      .form-title {
+        font-size: 20px;
+      }
+    }
+
+    .settings-section {
+      padding: 20px;
+    }
+
+    .switch-container .switch-content {
+      flex-direction: column;
+      align-items: flex-start;
+      gap: 15px;
+    }
+
+    .form-actions {
+      flex-direction: column;
+
+      .el-button {
+        width: 100%;
+      }
+    }
+  }
+}
+</style>

+ 5 - 7
src/views/setting/systemSetting/components/volume/index.vue

@@ -43,9 +43,6 @@
       >
         Save
       </el-button>
-
-      <!-- 提示信息 -->
-<!--      <p class="help-text">调整音量后点击保存按钮生效</p>-->
     </div>
   </div>
 </template>
@@ -86,7 +83,7 @@ const saveVolume = async () => {
   try {
     const res = await cameraVolume({ speakerVolume: speakerVolume.value })
     if(res.data == 'ok\n') {
-      ElMessage.success(`Volume Saved`)
+      ElMessage.success(`Volume Saved successfully`)
     } else {
       ElMessage.error('Failed to Save Volume Settings')
     }
@@ -120,14 +117,14 @@ onUnmounted(() => {
   display: flex;
   //align-items: center;
   //justify-content: center;
-  padding: 20px;
+  padding: 10px;
 }
 
 .volume-card {
-  //background: white;
+  //background: #ffffff;
   border-radius: 8px;
   //box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
-  padding: 10px;
+  padding: 20px;
   max-width: 360px;
   width: 100%;
 }
@@ -179,6 +176,7 @@ onUnmounted(() => {
     .el-slider__runway {
       height: 6px;
       border-radius: 3px;
+      //background: #dcdfe6;
     }
 
     .el-slider__bar {

+ 12 - 4
src/views/setting/systemSetting/index.vue

@@ -41,8 +41,8 @@
         <NightVision />
       </el-tab-pane>
 
-      <el-tab-pane label="Password Management" name="seventh">
-        <UserPassword />
+      <el-tab-pane label="Security Settings" name="seventh">
+        <UserSecurity />
       </el-tab-pane>
     </el-tabs>
   </div>
@@ -52,12 +52,16 @@
 import { ref } from 'vue'
 import { ElLoading, ElMessage, ElMessageBox, type FormInstance } from 'element-plus'
 import CameraInfo from './components/cameraInfo/index.vue'
-import UserPassword from './components/user/index.vue'
+import UserSecurity from './components/user/index.vue'
 import TimeSetting from './components/time/index.vue'
 import VolumeSetting from './components/volume/index.vue'
 import AlarmSetting from './components/alarm/index.vue'
 import NightVision from './components/nightVision/index.vue'
 import { cameraReset } from '@/api/setting'
+import {useUserStore} from "@/stores/modules/user";
+import {useRouter} from "vue-router";
+
+const router = useRouter()
 
 const activeName = ref('first')
 const loading = ref(false)
@@ -88,6 +92,8 @@ const handleReset = async () => {
           .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')
             }
@@ -124,7 +130,9 @@ const handleRestart = async () => {
     )
     const resp = await cameraReset({ reboot: 1 })
     if (resp.data === 'ok\n') {
-      ElMessage.success('Camera restarted successfully. Please wait a moment...')
+        ElMessage.success('Camera restarted successfully. Please wait a moment...')
+        useUserStore().logout()
+        await router.push('/login')
     } else {
       ElMessage.error('Operation failed')
     }