Pārlūkot izejas kodu

聚合点,位置优化

heyifan 1 mēnesi atpakaļ
vecāks
revīzija
ad3f7756b0

+ 26 - 1
languages/en.js

@@ -914,6 +914,9 @@ module.exports = {
     partDirector: 'Department Director',
     partNurseHead: 'Nurse Head',
     organization: 'Organization',
+     uiVersionType: "UI Type",
+     institutionUiType:'Institution UI Type',
+     selectUiVersionType:'Please select the institution UI type',
     shopMemberName: 'Administrator account',
     shopMemberNameMsg: 'The administrator account must be filled in',
     inputShopMemberName: 'Please enter the administrator account',
@@ -1787,7 +1790,7 @@ module.exports = {
     voiceType: 'Communication Protocol',
     chooseVoiceType: 'Select Communication Protocol',
     voiceTypeError: 'Different communication protocol with the department, cannot play',
-    huayiSleep: 'HuaYi Sleep Monitoring',
+    huayiSleep: 'Sleep Monitoring',
     sleepWakeful: 'Awake',
     sleepLight: 'Light Sleep',
     sleepDeep: 'Deep Sleep',
@@ -1834,5 +1837,27 @@ module.exports = {
     outBedCount: 'Out-of-Bed Count',
     movementCount: 'Movement Count',
     noThirdPartyId: 'Device data error, unable to save device settings'
+  },
+  he20250514:{
+    versionTypeHospital: 'Hospital Edition',
+    versionTypeMaternity: 'Maternity Center Edition',
+    versionTypeNursingHome: 'Nursing Home Edition',
+    currentPosition:'Current Position',
+    aiarmArea:  'Alarm Zone',
+    safetyFence: 'Safety Fence',
+    pleaseSelectDevice: 'Please select device',
+    radius: 'Radius',
+    meter: 'Meter',
+    noLocationDevice: 'No location device',
+    noDeviceInfo: 'No device information available',
+    startPoint: "Start Point",
+    endPoint: "End Point",
+    speed: 'Speed',
+  lastLocation: 'Last Location',
+  deviceOffline: 'Device is offline, operation failed',
+  locating: 'Locating, please wait...',
+  radiusTooSmall: 'The radius must be no less than 200',
+  radiusTooLarge: 'The radius cannot exceed 2000 meters',
+  inputLocationKeyword: 'Please enter a location keyword'
   }
 }

+ 26 - 1
languages/es.js

@@ -914,6 +914,9 @@ module.exports = {
     organization: 'Organización',
     partDirector: 'Director de Departamento',
     partNurseHead: 'Jefe de enfermeria',
+    uiVersionType: 'Tipo de interfaz',
+    institutionUiType:'Tipo de interfaz de institución',
+    selectUiVersionType:'Por favor, seleccione el tipo de interfaz de la institución',
     shopMemberName: 'Cuenta de administrador',
     shopMemberNameMsg: 'Se debe completar la cuenta de administrador',
     inputShopMemberName: 'introduzca la cuenta de administrador',
@@ -1787,7 +1790,7 @@ module.exports = {
     voiceType: 'Protocolo de comunicación',
     chooseVoiceType: 'Seleccionar protocolo de comunicación',
     voiceTypeError: 'Protocolo de comunicación diferente con el departamento, no se puede reproducir',
-    huayiSleep: 'Monitoreo de Sueño HuaYi',
+    huayiSleep: 'Monitoreo de Sueño',
     sleepWakeful: 'Despierto',
     sleepLight: 'Sueño Ligero',
     sleepDeep: 'Sueño Profundo',
@@ -1834,5 +1837,27 @@ module.exports = {
     outBedCount: 'Recuento de Salida de Cama',
     movementCount: 'Recuento de Movimiento',
     noThirdPartyId: 'Error en los datos del dispositivo, no se puede guardar la configuración del dispositivo'
+  },
+  he20250514:{
+    versionTypeHospital: 'Edición Hospital',
+    versionTypeMaternity: 'Edición Centro de Postparto',
+    versionTypeNursingHome: 'Edición Residencia para Ancianos',
+    currentPosition:'Posición Actual',
+    aiarmArea: 'Zona de Alarma',
+    safetyFence: 'Valla de Seguridad',
+    pleaseSelectDevice: 'Seleccione dispositivo',
+    radius: 'Radio',
+    meter: 'Metro',
+    noLocationDevice: 'Sin dispositivo de ubicación',
+    noDeviceInfo: 'No hay información del dispositivo',
+    startPoint: "Punto de inicio",
+    endPoint: "Punto final",
+    speed: 'Velocidad',
+    lastLocation: 'Última ubicación',
+    deviceOffline: 'El dispositivo está desconectado, operación fallida',
+    locating: 'Localizando, por favor espere...',
+    radiusTooSmall: 'El radio no puede ser menor a 200',
+    radiusTooLarge: 'El radio no puede exceder los 2000 metros',
+    inputLocationKeyword: 'Por favor, ingrese una palabra clave para la ubicación',
   }
 }

+ 26 - 1
languages/ru-RU.js

@@ -923,6 +923,9 @@ module.exports = {
     partDirector: 'Директор департамента',
     partNurseHead: 'Главная медсестра',
     organization: 'Организация',
+    uiVersionType: 'Тип интерфейса',
+    institutionUiType: 'Тип интерфейса учреждения',
+    selectUiVersionType: 'Пожалуйста, выберите тип интерфейса учреждения',
     shopMemberName: 'Аккаунт администратора',
     shopMemberNameMsg: 'Необходимо заполнить учетную запись администратора',
     inputShopMemberName: 'Пожалуйста, введите учетную запись администратора',
@@ -1810,7 +1813,7 @@ module.exports = {
     voiceType: 'Протокол связи',
     chooseVoiceType: 'Выбор протокола связи',
     voiceTypeError: 'Разный протокол связи с отделением, невозможно воспроизвести',
-    huayiSleep: 'Мониторинг сна HuaYi',
+    huayiSleep: 'Мониторинг сна',
     sleepWakeful: 'Бодрствующий',
     sleepLight: 'Лёгкий сон',
     sleepDeep: 'Глубокий сон',
@@ -1857,5 +1860,27 @@ module.exports = {
     outBedCount: 'Количество покиданий кровати',
     movementCount: 'Количество движений',
     noThirdPartyId: 'Ошибка данных устройства, невозможно сохранить настройки устройства'
+  },
+  he20250514: {
+    versionTypeHospital: 'Версия для больницы',
+    versionTypeMaternity: 'Версия для центра по уходу за роженицами',
+    versionTypeNursingHome: 'Версия для дома престарелых',
+    currentPosition: 'Текущая позиция',
+    aiarmArea: 'Зона тревоги',
+    safetyFence: 'Ограждение безопасности',
+    pleaseSelectDevice: 'Выберите устройство',
+    radius: 'Радиус',
+    meter: 'Метр',
+    noLocationDevice: 'Нет устройства локации',
+    noDeviceInfo: 'Информация об устройстве отсутствует',
+    startPoint: "Начальная точка",
+    endPoint: "Конечная точка",
+    speed: 'Скорость',
+    lastLocation: 'Последнее местоположение',
+    deviceOffline: 'Устройство оффлайн, операция не удалась',
+    locating: 'Определение местоположения, подождите...',
+    radiusTooSmall: 'Радиус не может быть меньше 200',
+    radiusTooLarge: 'Радиус не может превышать 2000 метров',
+    inputLocationKeyword: 'Пожалуйста, введите ключевое слово для места'
   }
 }

+ 27 - 1
languages/zh-CN.js

@@ -912,6 +912,9 @@ module.exports = {
     part: '科室',
     shop: '机构',
     organization: '组织',
+    uiVersionType: 'UI类型',
+    institutionUiType: '机构UI类型',
+    selectUiVersionType: '请选择机构UI类型',
     shopMemberName: '管理员账号',
     shopMemberNameMsg: '管理员账号必须填写',
     inputShopMemberName: '请输入管理员账号',
@@ -1787,7 +1790,8 @@ module.exports = {
     voiceType: '通信协议',
     chooseVoiceType: '选择通信协议',
     voiceTypeError: '与科室的通讯协议不同,无法播放',
-    huayiSleep: '华屹睡眠监测',
+    // huayiSleep: '华屹睡眠监测',
+    huayiSleep: '睡眠监测',
     sleepWakeful: '清醒',
     sleepLight: '浅睡',
     sleepDeep: '熟睡',
@@ -1834,5 +1838,27 @@ module.exports = {
     outBedCount: '离床次数',
     movementCount: '体动次数',
     noThirdPartyId: '设备数据错误,无法保存设备设置'
+  },
+  he20250514: {
+    versionTypeHospital: '医院版',
+    versionTypeMaternity: '月子中心版',
+    versionTypeNursingHome: '养老院版',
+    currentPosition: '当前位置',
+    aiarmArea: '报警区域',
+    safetyFence: '安全围栏',
+    pleaseSelectDevice: '请选择设备',
+    radius: '半径',
+    meter: '米',
+    noLocationDevice: '没有定位设备',
+    noDeviceInfo: '没有获取到设备信息',
+    startPoint: "起点",
+    endPoint: "终点",
+    speed: '速度',
+    lastLocation: '最后位置',
+    deviceOffline: '设备离线,操作失败',
+    locating: '正在定位,请稍后',
+    radiusTooSmall: '围栏半径不能小于200',
+    radiusTooLarge: '围栏半径不能超过2000米',
+    inputLocationKeyword:'请输入地名关键字',
   }
 }

+ 26 - 1
languages/zh-TW.js

@@ -910,6 +910,9 @@ module.exports = {
     part: '科室',
     shop: '機構',
     organization: '組織',
+    uiVersionType: 'UI類型',
+    institutionUiType: '機構UI類型',
+    selectUiVersionType: '請選擇機構UI類型',
     shopMemberName: '管理員帳號',
     shopMemberNameMsg: '必須填寫管理員帳號',
     inputShopMemberName: '請輸入管理員帳號',
@@ -1750,7 +1753,7 @@ module.exports = {
     voiceType: '通訊協議',
     chooseVoiceType: '選擇通訊協議',
     voiceTypeError: '與科室的通訊協議不同,無法播放',
-    huayiSleep: '華屹睡眠監測',
+    huayiSleep: '睡眠監測',
     sleepWakeful: '清醒',
     sleepLight: '淺睡',
     sleepDeep: '熟睡',
@@ -1797,5 +1800,27 @@ module.exports = {
     outBedCount: '離床次數',
     movementCount: '體動次數',
     noThirdPartyId: '裝置數據錯誤,無法儲存裝置設定'
+  },
+  he20250514: {
+    versionTypeHospital: '醫院版',
+    versionTypeMaternity: '月子中心版',
+    versionTypeNursingHome: '養老院版',
+    currentPosition: '當前位置',
+    aiarmArea: '報警區域',
+    safetyFence: '安全圍欄',
+    pleaseSelectDevice: '請選擇設備',
+    radius: '半徑',
+    meter: '米',
+    noLocationDevice: '沒有定位設備',
+    noDeviceInfo: '沒有獲取到設備信息',
+    startPoint: "起點",
+    endPoint: "終點",
+    speed: '速度',
+    lastLocation: '最後位置',
+    deviceOffline: '設備離線,操作失敗',
+    locating: '正在定位,請稍後',
+    radiusTooSmall: '圍欄半徑不能小於200',
+    radiusTooLarge: '圍欄半徑不能超過2000米',
+    inputLocationKeyword: '請輸入地名關鍵字',
   }
 }

+ 1 - 1
package.json

@@ -90,7 +90,7 @@
     "vue-router": "3.0.2",
     "vue-seamless-scroll": "^1.1.23",
     "vue-splitpane": "1.0.4",
-    "vuedraggable": "^2.20.0",
+    "vuedraggable": "^2.24.3",
     "vuex": "3.1.0",
     "xlsx": "0.14.1"
   },

+ 4 - 2
public/domain.js

@@ -1,6 +1,8 @@
 const domain = {
-  serverUrl: 'http://8.129.220.143:8005',
-  DeviceUrl: 'http://8.129.220.143:8006',
+  // serverUrl: 'http://8.129.220.143:8005',
+  // DeviceUrl: 'http://8.129.220.143:8006',
+  serverUrl: 'http://192.168.1.187:8005',
+  DeviceUrl: 'http://192.168.1.187:8006',
   mediaUrl: 'http://8.129.220.143:8004',
   OnlineSystemUrl: 'http://api.base.wdklian.com',
   gateWayUrl:'http://8.129.220.143:8014',

+ 17 - 1
src/api/ncs_device.js

@@ -292,7 +292,23 @@ export function getDeviceList(frameId, type) {
     loading: false
   })
 }
-
+export function getDevicePositionList(frameId, types) {
+  return request({
+    url: `/ncs/device/getDeviceListByTypes/${frameId}`,
+    method: 'get',
+    params: { types },
+    paramsSerializer: params => {
+      // 确保数组参数正确序列化
+      return Object.keys(params).map(key => {
+        if (Array.isArray(params[key])) {
+          return params[key].map(val => `${key}=${val}`).join('&');
+        }
+        return `${key}=${params[key]}`;
+      }).join('&');
+    },
+    loading: false
+  })
+}
 /** 获取转换盒存储的按钮 */
 export function getLoraButtons(mac) {
   return request({

BIN
src/icons/images/point.png


BIN
src/icons/images/walk.png


+ 164 - 12
src/views/customer/allMap.vue

@@ -2,13 +2,40 @@
   <div style="height: 1000px">
     <baidu-map :center="center" :zoom="zoom" scroll-wheel-zoom @ready="handler">
       <bm-view class="map"></bm-view>
-      <bm-control :offset="{width: '10px', height: '10px'}">
+      <bm-control >
         <bm-auto-complete v-model="keyword" :sugStyle="{zIndex: 1}">
-          <input v-model="keyword" placeholder="请输入地名关键字" style="width: 300px;margin-left: 20px;margin-top: 20px;">
+          <input v-model="keyword" :placeholder="$t('he20250514.inputLocationKeyword')" :style="inputStyle">
         </bm-auto-complete>
       </bm-control>
       <bm-local-search :keyword="keyword" :auto-viewport="true" :location="location" :panel="false"></bm-local-search>
-      <bm-marker v-for="marker of markers" :position="{lng: marker.lng, lat: marker.lat}" :dragging="true" animation="BMAP_ANIMATION_BOUNCE" clicking></bm-marker>
+      <!-- <bm-marker v-for="marker of markers" :position="{lng: marker.lng, lat: marker.lat}" :dragging="true" animation="BMAP_ANIMATION_BOUNCE" clicking></bm-marker> -->
+      <bml-marker-clusterer :averageCenter="true" :styles="clusterStyles"   @click="showMarkerInfo(marker)">
+        <bm-marker 
+          v-for="marker of markers" 
+          :key="marker.id"
+          :position="{lng: marker.lng, lat: marker.lat}" 
+          :dragging="true" 
+          animation="BMAP_ANIMATION_BOUNCE"
+          @click="showMarkerInfo(marker)"
+        >
+          <bm-info-window 
+            :show="marker.show"
+            @close="marker.show = false"
+            @open="marker.show = true"
+            class="custom-info-window"
+          >
+          <div class="info-content">
+              <div class="info-title">{{ marker.name }}
+                <span v-if="marker.age">{{ marker.age }}{{$t('customerManage.year')  }}</span>
+              </div>
+              <div class="info-item" v-if="marker.id_no">{{$t('customerManage.idCard')}}: {{ marker.id_no }}</div>
+              <div class="info-item" v-if="marker.card_no">{{$t('customerManage.cardNo')}}: {{ marker.card_no }}</div>
+              <div class="info-item" v-if="marker.in_date">{{$t('customerManage.inDate')}}: {{ marker.in_date }}</div>
+              <div class="info-item" v-if="marker.out_date">{{$t('customerManage.outDate')}}: {{ marker.out_date }}</div>
+            </div>
+          </bm-info-window>
+        </bm-marker>
+      </bml-marker-clusterer>
       <bm-geolocation anchor="BMAP_ANCHOR_BOTTOM_RIGHT" :showAddressBar="true" :autoLocation="true"></bm-geolocation>
     </baidu-map>
   </div>
@@ -17,58 +44,155 @@
 <script>
 
 import { getListByPartId } from '@/api/ncs_customer'
-
+import { BmlMarkerClusterer } from 'vue-baidu-map'
+import { unixToDate } from '@/utils/Foundation'
 export default {
 name: "allMap",
   data() {
     return {
       center: {lng: 0, lat: 0},
       zoom: 5,
-      myCenter: {
-        lng: 116.404,
-        lat: 39.915
-      },
-      isShow: false,
+      // myCenter: {
+      //   lng: 116.404,
+      //   lat: 39.915
+      // },
+      // isShow: false,
       location: '',
       keyword: '',
       markers: [],
+      clusterStyles: [
+        {
+          url: 'https://api.map.baidu.com/library/MarkerClusterer/1.2/examples/images/m1.png',
+          size: {
+            width: 54,
+            height: 54
+          },
+          minSize: 2,  // 添加最小聚合数
+          textSize: 16,
+        textColor: '#fff'
+        }
+      ],
+      map: null,
     }
   },
+  components: {
+    BmlMarkerClusterer
+  },
   computed: {
     tableHeight() {
       return  this.mainAreaHeight - 130
     },
+    inputStyle() {
+      const widthMap = {
+      'zh': '200px',    // 中文
+      'en': '260px',    // 英文
+      'ru': '400px',    // 俄语
+      'es': '400px',    // 西班牙语
+      'zh-TW': '200px'  // 繁体中文
+    }
+      return {
+        width:widthMap[this.$i18n.locale] || '200px',
+        marginLeft: '20px',
+        marginTop: '20px',
+        padding: '8px 12px',
+      border: '1px solid #c0c4cc',
+      borderRadius: '4px',
+  
+      }
+    }
   },
   mounted() {
     window.onresize = this.windowResize()
   },
   methods: {
+    unixToDate:unixToDate,
     windowResize() {
       this.$set(this, 'mainAreaHeight', Number(document.documentElement.clientHeight) - 84)
     },
     handler ({BMap, map}) {
-      this.zoom = 7
+      this.map = map
+      // console.log('百度地图实例',map)
       this.getList()
+
+    },
+    handleClusterReady() {
+        const BMap = window.BMap
+        const points = this.markers.map(marker => new BMap.Point(marker.lng, marker.lat))
+        this.map.setViewport(points,{
+          enableAnimation: true,
+          margins: [50, 50, 50, 50],
+          zoomFactor: -1  
+        })
+        // 强制触发聚合点刷新
+        setTimeout(() => {
+          this.map.zoomIn()
+        }, 100)
+      
     },
+    // getList() {
+    //   const _this = this
+    //   this.markers = []
+    //   let i = 0
+    //   getListByPartId(this.$store.getters.partId).then(res =>{
+    //     if (res) {
+    //       console.log('888',res)
+    //       res.forEach(item => {
+    //         if (item.lng_lat) {
+    //           const position = {lng: item.lng_lat.split(',')[0], lat: item.lng_lat.split(',')[1]}
+    //           if (i === 0) {
+    //             _this.center = position
+    //           }
+    //           ++ i
+    //           _this.markers.push(position)
+    //           console.log('999',_this.markers)
+    //         }
+    //       })
+    //     }
+    //   })
+    // },
     getList() {
       const _this = this
       this.markers = []
       let i = 0
-      getListByPartId(this.$store.getters.partId).then(res =>{
+     return getListByPartId(this.$store.getters.partId).then(res =>{
         if (res) {
           res.forEach(item => {
             if (item.lng_lat) {
-              const position = {lng: item.lng_lat.split(',')[0], lat: item.lng_lat.split(',')[1]}
+              const position = {
+                id: item.id,
+                lng: item.lng_lat.split(',')[0], 
+                lat: item.lng_lat.split(',')[1],
+                name: item.named || '',
+                age:item.age ? (item.age) : '',
+                id_no: item.id_no || '',
+                in_date:item.in_date ? this.unixToDate(item.in_date)  : '',
+                out_date:item.out_date? this.unixToDate(item.out_date) : '',
+                id_type:item.id_type? item.id_type : '',
+                card_no:item.card_no || '',
+                show: false
+              }
               if (i === 0) {
                 _this.center = position
               }
               ++ i
               _this.markers.push(position)
+              // console.log('111markers',_this.markers)
             }
           })
+             // 数据加载完成后触发聚合点刷新
+             if (this.map) {
+            this.handleClusterReady()
+          }
         }
+  
       })
     },
+
+    showMarkerInfo(marker) {
+      this.markers.forEach(item => {
+        item.show = item.id === marker.id
+      })
+    }
   }
 }
 </script>
@@ -78,4 +202,32 @@ name: "allMap",
   width: 100%;
   height: 1000px;
 }
+.custom-info-window {
+  /deep/ .BMap_bubble_title {
+    display: none;
+  }
+  /deep/ .BMap_bubble_content {
+    padding: 0;
+  }
+}
+
+.info-content {
+  padding: 10px;
+  min-width: 200px;
+}
+
+.info-title {
+  font-size: 16px;
+  font-weight: bold;
+  color: #333;
+  padding-bottom:2px ;
+}
+
+.info-item {
+  font-size: 14px;
+  color: #666;
+  margin: 5px 0;
+  line-height: 1.5;
+}
+ 
 </style>

+ 4 - 2
src/views/customer/components/patientManager.vue

@@ -45,7 +45,7 @@
     </ag-grid-layout>
 
     <!-- 用户信息 -->
-    <el-dialog :visible.sync="customerFormVisible" class="customer-dialog" width="60%">
+    <el-dialog :visible.sync="customerFormVisible" class="customer-dialog" width="60%" top="8vh" >
       <el-tabs v-model="activeName" type="border-card" tab-position="top" width="50%" @tab-click="handleClick">
         <el-tab-pane :label="this.$t('customerManage.baseInfo')" name="customerBaseInfo">
           <div>
@@ -1804,7 +1804,9 @@ export default {
 /deep/ .customer-dialog .el-dialog__body {
   padding: 1px;
 }
-
+/deep/ .el-dialog__header{
+  margin-bottom: 10px;
+}
 .el-input-group__append .el-select {
   width: 60px;
 }

+ 135 - 43
src/views/customer/myMapHtml.vue

@@ -13,14 +13,14 @@
               <el-button type="success" icon="el-icon-location-outline" :disabled="disable" circle plain
                          @click="changeType(0)"></el-button>
             </el-tooltip>
-            <el-tooltip class="item" effect="dark" content="刷新" placement="right-start">
+            <el-tooltip class="item" effect="dark" :content="$t('zy20240205.flushed')" placement="right-start">
               <el-button type="primary" icon="el-icon-refresh-left" :disabled="disable" circle plain
                          @click="changeType(1)"></el-button>
             </el-tooltip>
             <el-tooltip class="item" effect="dark" :content="$t('customerManage.footprint')" placement="right-start">
               <el-button type="warning" icon="el-icon-rank" circle plain @click="changeType(-1)"></el-button>
             </el-tooltip>
-            <el-tooltip class="item" effect="dark" content="安全围栏" placement="right-start">
+            <el-tooltip class="item" effect="dark" :content="$t('he20250514.safetyFence')" placement="right-start">
               <el-button type="danger" icon="el-icon-s-help" circle plain @click="changeType(2)"></el-button>
             </el-tooltip>
           </div>
@@ -36,23 +36,36 @@
         <div v-if="type === -1">
           <!-- 轨迹 -->
           <!-- 添加起点标记 -->
-          <bm-marker v-if="dataList.length > 1" :position="endPoint" @mouseover="clickMap">
-            <bm-label content="终点" :offset="{width: 0, height: 30}"/>
+          <bm-marker v-if="dataList.length > 1" :position="markers[0]" @mouseover="clickMap">
+            <bm-label :content="$t('he20250514.startPoint')" :offset="{width: 0, height: 30}"/>
             <bm-info-window v-if="myContent" :show="true" @close="infoWindowClose">
               <p>{{ unixDateFormatter(dataList[dataList.length - 1].position_time) }}</p>
               <p style="color: #4abe84">{{ myContent }}</p>
             </bm-info-window>
           </bm-marker>
           <!-- 添加终点标记 -->
-          <bm-marker :position="markers[0]">
-            <bm-label content="起点" :offset="{width: 0, height: 30}"/>
+          <bm-marker :position="endPoint">
+            <bm-label :content="$t('he20250514.endPoint')" :offset="{width: 0, height: 30}"/>
           </bm-marker>
           <!-- 折线 -->
           <bm-polyline :path="markers" stroke-color="#4aa4f3" :stroke-opacity="1" :stroke-weight="6"
                        stroke-style="dashed"
                        @lineupdate="updatePolylinePath"/>
           <!--      <bm-polyline :path="markers" stroke-color="red" :stroke-opacity="0.5" :stroke-weight="2"></bm-polyline>-->
-          <bml-lushu @stop="reset" :path="markers" :icon="icon" :play="play" :autoView="true" :speed="700"></bml-lushu>
+    <bm-control anchor="BMAP_ANCHOR_BOTTOM_RIGHT">
+    <div class="control-panel">
+      <el-slider
+        v-model="duration"
+        :min="1"
+        :max="10"
+        :step="1"
+        style="width: 160px;height: 36px;"
+        @change="handleDurationChange">
+      </el-slider>
+      <div style="font-size:14px;text-align: center;">{{this.$t('he20250514.speed')}}: {{ duration }}</div>
+    </div>
+  </bm-control>
+          <bml-lushu  @stop="reset"  :path="markers" :icon="icon" :play="play" :autoView="true" :speed="moveSpeed" ></bml-lushu>
           <!-- 轨迹 -->
         </div>
         <div v-else-if="type === 2">
@@ -66,10 +79,10 @@
                 <el-input v-model="securityFence.address"/>
               </p>
 
-              <div style="color: #4abe84">半径:{{ securityFence.radius }} 米
-                <el-button type="primary" icon="el-icon-edit" size="mini" plain @click="API_SaveFence">保存</el-button>
+              <div style="color: #4abe84">{{this.$t('he20250514.radius')}}:{{ securityFence.radius }} {{this.$t('he20250514.meter')}}
+                <el-button type="primary" icon="el-icon-edit" size="mini" plain @click="API_SaveFence">{{this.$t('action.save')}}</el-button>
                 <el-button v-if="securityFence.id" type="danger" icon="el-icon-delete" size="mini" plain
-                           @click="API_DelFence">删除
+                           @click="API_DelFence">{{ this.$t('action.delete') }}
                 </el-button>
               </div>
 
@@ -82,25 +95,37 @@
 <!--            <bm-label content="我爱北京天安门" :labelStyle="{color: 'red', fontSize : '24px'}" :offset="{width: -35, height: 30}"/>-->
 
             <bm-info-window v-if="myContent" :show="true" @close="infoWindowClose">
-              <p>{{ unixDateFormatter(dataList[0].position_time) }}</p>
+              <p v-if="dataList && dataList.length > 0">{{ unixDateFormatter(dataList[0].position_time) }}</p>
               <p style="color: #4abe84">{{ myContent }}</p>
             </bm-info-window>
-            <bm-label content="最后位置" :labelStyle="{color: 'red', fontSize : '14px'}" :offset="{width: -20, height: 30}"/>
+            <bm-label :content="$t('he20250514.lastLocation')" :labelStyle="{color: 'red', fontSize : '14px'}" :offset="{width: -20, height: 30}"/>
           </bm-marker>
         </div>
 <!--        <bm-geolocation anchor="BMAP_ANCHOR_BOTTOM_RIGHT" :showAddressBar="false" :autoLocation="true" @locationSuccess="locationSuccess"></bm-geolocation>-->
-      </baidu-map>
+<bm-control  anchor="BMAP_ANCHOR_TOP_RIGHT">
+  <div style="margin: 20px">
+    <el-select v-model="selectedDevice" :placeholder="$t('he20250514.pleaseSelectDevice')" style="width: 300px; margin-bottom: 10px" @change="handleDeviceChange" >
+      <el-option
+        v-for="device in deviceList"
+        :key="device.eth_mac"
+        :label="`${device.name} --- ${device.eth_mac}`"
+        :value="device.eth_mac">
+      </el-option>
+    </el-select>
+  </div>
+</bm-control>
+</baidu-map>
     </div>
   </div>
 </template>
 
 <script>
-import walkImg from '@/icons/images/walk.png'
+import walkImg from '@/icons/images/point.png'
 import {BmlLushu} from 'vue-baidu-map'
 import {getLocationList} from '@/api/initialize'
 import {unix2Date} from '@/utils/Foundation'
 import * as API_MemberLocation from '@/api/member_location'
-import {getDeviceList} from '@/api/ncs_device'
+import {getDevicePositionList} from '@/api/ncs_device'
 import {DEVICE_TYPE} from '@/utils/enum/DeviceTypeEnum'
 
 export default {
@@ -113,7 +138,7 @@ export default {
       mapUrl: null,
       uuid: null,
       frameId: null,
-      center: '北京',
+      center: '深圳',
       zoom: 16,
       isShow: false,
       markers: [{
@@ -123,8 +148,8 @@ export default {
       play: false, //是否行进
       icon: { // 覆盖物的图标
         url: walkImg,
-        size: {width: 220, height: 220},
-        opts: {anchor: {width: 0, height: 113}}
+        size: {width: 100, height: 100},
+        opts: {anchor: {width: 50, height: 100}}
       },
       boolCircle: false,
       type: 1,
@@ -140,12 +165,16 @@ export default {
       deviceList: [],
       circleCenter: {
         lng: 113.971151,
-        lat: 22.570388
+        lat: 22.570388,
       },
       securityFence: {
         radius: 200
       },
-      editing: false
+      editing: false,
+      selectedDevice: '',
+      moveSpeed: 0, 
+      duration: 5, // 默认5秒
+      map: null,
     }
   },
   created() {
@@ -164,12 +193,45 @@ export default {
     if (!this.mapUrl) {
       this.uuid = this.$route.query.uuid
       this.frameId = this.$route.query.frameId
-      this.params.fixedCondition = " member_union_id = '" + this.uuid + "'"
-      this.handleMapData()
+      // this.uuid = '6747d6a5ce12000007688915'
+      // this.params.fixedCondition = " member_union_id = '" + this.uuid + "' and imei = '" + this.selectedDevice + "'"
+      // this.handleMapData()
       this.API_GetDeviceList()
     }
   },
   methods: {
+calculatePathSpeed(){
+if( !this.markers || this.markers.length < 2)   return
+let totalDistance = 0
+for(let i = 0 ; i < this.markers.length - 1;i++){ 
+  const point1 =new BMap.Point(this.markers[i].lng,this.markers[i].lat)
+  const point2 =new BMap.Point(this.markers[i+1].lng,this.markers[i+1].lat)
+  totalDistance += this.map.getDistance(point1, point2);
+ 
+}
+this.moveSpeed = Math.ceil(totalDistance / this.duration)
+},
+
+handleDurationChange(value) {
+      this.duration = value
+      this.calculatePathSpeed();
+      const currentPath = [...this.markers]
+       this.reset();
+    this.$nextTick(() => {
+      this.markers = currentPath;
+      this.play = true;
+    });
+    },
+    handleDeviceChange(value) {
+  if(!value) return;
+  this.params.fixedCondition = ` member_union_id = '${this.uuid}' and imei = '${value}'`;
+  // 重置地图状态
+  this.type = 1;
+  this.isShow = false;
+  this.myContent = null;
+  this.disable = true;
+  this.handleMapData();
+},
     //处理数据
     handleMapData() {
       let markers = []
@@ -180,6 +242,7 @@ export default {
         this.dataList.sort(function (a, b) {
           return a.position_time - b.position_time
         })
+        // console.log('handleMapData', this.dataList)
         if (res.data.length > 0) {
           this.dataList.forEach(item => {
             markers.push({lng: item.longitude, lat: item.latitude})
@@ -197,9 +260,10 @@ export default {
           // })
           this.markers = markers
           this.center = this.markers[0]
-        }
-        this.play = true
-        this.isShow = true
+          this.calculatePathSpeed();
+          }
+      this.play = true
+      this.isShow = true
       })
     },
     locationSuccess({point, AddressComponent, marker}){
@@ -207,15 +271,25 @@ console.log('locationSuccess', point, AddressComponent, marker)
      this.center={...point}
     },
     handler({BMap, map}) {
+      this.map = map
       if (this.markers.length > 0 && this.type !== 2) {
         let view = map.getViewport(eval(this.markers))
         this.zoom = view.zoom
-        console.log('zoom===', this.zoom)
       }
     },
     async API_GetDeviceList() {
-      getDeviceList(this.frameId, DEVICE_TYPE.WATCH_IW).then(res => {
-        this.deviceList = res
+      getDevicePositionList(this.frameId, [DEVICE_TYPE.WATCH_IW,DEVICE_TYPE.WATCH_BSJ]).then(res => {
+    console.log('API_GetDeviceList设备列表',res)
+      this.deviceList = res
+    if(this.deviceList && this.deviceList.length > 0) {
+      this.selectedDevice = this.deviceList[0].eth_mac
+      this.params.fixedCondition = " member_union_id = '" + this.uuid + "' and imei = '" + this.selectedDevice + "'"
+      // this.params.fixedCondition = " member_union_id = '" + this.uuid + "'"
+      // 只有在有设备时才获取位置数据
+      this.handleMapData()
+    }else{
+      this.isShow = true
+    }
       })
     },
     // 停止本次移动
@@ -235,14 +309,15 @@ console.log('locationSuccess', point, AddressComponent, marker)
       if (this.myContent) {
         return
       }
+      //转化为百度地图坐标
+      const bPoint =new BMap.Point(this.markers[0].lng, this.markers[0].lat)
       const geoCoder = new BMap.Geocoder()
       // 利用坐标获取地址的详细信息
-      geoCoder.getLocation(e.point, (res) => {
-        console.log(res)
-        if (res.surroundingPois[0]) {
-          this.myContent = res.surroundingPois[0].title + '附近'
+      geoCoder.getLocation(bPoint, (res) => {
+        if ( res.surroundingPois && res.surroundingPois[0]) {
+          this.myContent = res.surroundingPois[0].title + this.$t('watch.nearby')
         } else {
-          this.myContent = res.address + '附近'
+          this.myContent = res.address +  this.$t('watch.nearby')
         }
         console.log('地址为' + this.myContent)
       })
@@ -262,9 +337,9 @@ console.log('locationSuccess', point, AddressComponent, marker)
       geoCoder.getLocation(point, (res) => {
         console.log(res)
         if (res.surroundingPois[0]) {
-          this.securityFence.address = res.surroundingPois[0].title + '附近'
+          this.securityFence.address = res.surroundingPois[0].title + this.$t('watch.nearby')
         } else {
-          this.securityFence.address = res.address + '附近'
+          this.securityFence.address = res.address + this.$t('watch.nearby')
         }
         this.boolCircle = true
         console.log('circleContent地址为' + this.securityFence.address)
@@ -283,7 +358,7 @@ console.log('locationSuccess', point, AddressComponent, marker)
           this.type = 0
           if (this.deviceList.length === 0) {
             this.$message({
-              message: '没有定位设备',
+              message:this.$t('he20250514.noLocationDevice'),
               type: 'info'
             })
             return
@@ -295,13 +370,15 @@ console.log('locationSuccess', point, AddressComponent, marker)
           this.type = 1
           this.disable = true
           this.params.page_size = 1
+          this.markers = []
+          this.zoom = 16  // 设置默认缩放级别
           this.isShow = false
           this.handleMapData()
           break
         case 2:
           if (this.deviceList.length === 0) {
             this.$message({
-              message: '没有定位设备',
+              message: this.$t('he20250514.noLocationDevice'),
               type: 'info'
             })
             return
@@ -312,6 +389,7 @@ console.log('locationSuccess', point, AddressComponent, marker)
           break
         default:
           this.type = -1
+          this.duration = 5
           this.params.page_size = 20
           this.isShow = false
           this.handleMapData()
@@ -328,7 +406,7 @@ console.log('locationSuccess', point, AddressComponent, marker)
       return unix2Date(time * 1000)
     },
     API_GetLocation() {
-      API_MemberLocation.getLocation(this.deviceList[0].eth_mac).then(res => {
+      API_MemberLocation.getLocation(this.selectedDevice).then(res => {
         if (res.success) {
 
           if (Array.isArray(res.data)) {
@@ -354,14 +432,14 @@ console.log('locationSuccess', point, AddressComponent, marker)
           const data = JSON.parse(res.data)
           if (data.Data.Code === 'iot.messagebroker.OFFLINE') {
             this.$message({
-              message: '设备离线,操作失败',
+              message: this.$t('he20250514.deviceOffline'),
               type: 'info'
             })
             this.disable = false
             return
           }
           this.$message({
-            message: '正在定位,请稍后',
+            message: this.$t('he20250514.locating'),
             type: 'info'
           })
           const _this = this
@@ -383,7 +461,8 @@ console.log('locationSuccess', point, AddressComponent, marker)
     API_GetFenceList() {
       const data = {
         unionId: this.uuid,
-        imei: this.deviceList[0].eth_mac,
+        // imei: this.deviceList[0].eth_mac,
+        imei: this.selectedDevice,
         type: 0
       }
       const _this = this
@@ -414,14 +493,14 @@ console.log('locationSuccess', point, AddressComponent, marker)
     API_SaveFence() {
       if (this.securityFence.radius < 200) {
         this.$message({
-          message: '围栏半径不能小于200',
+          message: this.$t('he20250514.radiusTooSmall'),
           type: 'info'
         })
         return
       }
       if (this.securityFence.radius > 2000) {
         this.$message({
-          message: '围栏半径不能超过2000米',
+          message: this.$t('he20250514.radiusTooLarge'),
           type: 'info'
         })
         return
@@ -462,4 +541,17 @@ console.log('locationSuccess', point, AddressComponent, marker)
   max-width: none!important;
   background: none!important;
 }
+.control-panel {
+  margin: 20px;
+  /* background: #e6f7ff; */
+  padding: 10px;
+  border-radius: 12px;
+  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
+  background: rgba(255, 255, 255, 0.7);
+  
+  &:hover {
+    background: rgba(255, 255, 255, 0.8);
+    transform: translateY(-2px);
+  }
+}
 </style>

+ 0 - 1
src/views/ncs-device/components/deviceManager.vue

@@ -1421,7 +1421,6 @@
                     this.getTypeFrame(shopId)
                 })
             },
-
             initWebSocket: function () {
                 var stockbase = DeviceUrl.replace('http', 'ws')
                 this.websock = new WebSocket(stockbase + '/deviceonline/' + this.$store.getters.uuid)

+ 1 - 1
src/views/ncs-device/user_watch.vue

@@ -53,7 +53,7 @@
 
 <!--    位置信息-->
     <el-dialog :title="this.$t('watch.placeInfo')" :visible.sync="locationShow" width="70%">
-      <watch-location :device-id="deviceId" :is-show="locationShow"></watch-location>
+      <watch-location :device-id="deviceId" :is-show="locationShow" ></watch-location>
     </el-dialog>
   </div>
 </template>

+ 146 - 132
src/views/ncs-device/watch_location.vue

@@ -1,85 +1,67 @@
 <template>
   <div class="app-container">
     <div class="block">
-      <el-date-picker
-          v-model="queryTime"
-          type="date"
-          :placeholder="this.$t('watch.dateKeywords')"
-          value-format="yyyy-MM-dd">
+      <el-date-picker v-model="queryTime" type="date" :placeholder="this.$t('watch.dateKeywords')"
+        value-format="yyyy-MM-dd">
       </el-date-picker>
-      <el-button type="primary" plain style="margin-left: 30px" v-loading.fullscreen.lock="fullscreenLoading" @click="getNewLocation">
+      <el-button type="primary" plain style="margin-left: 30px" v-loading.fullscreen.lock="fullscreenLoading"
+        @click="getNewLocation">
         {{ this.$t('watch.getNewPlace') }}</el-button>
       <el-tag v-if="myTitle" style="margin-left: 20px;" type="warning">{{ myTitle }}</el-tag>
+
+<div class="color-legend">
+  <div class="legend-item">
+    <span class="color-dot green"></span>
+    <span>{{ this.$t('he20250514.currentPosition') }}</span>
+  </div>
+  <div class="legend-item">
+    <span class="color-dot red"></span>
+    <span>{{ this.$t('he20250514.aiarmArea') }}</span>
+  </div>
+</div>
     </div>
     <div class="location-map">
       <div class="grid-background"></div>
-      
+      <!-- <svg class="path-overlay" :viewBox="viewBox"> -->
+        <!-- 背景虚线路径 -->
+        <!-- <path :d="pathData" class="movement-path-bg" stroke="#f7da98" stroke-width="3" stroke-dasharray="8,8"
+          fill="none" /> -->
+        <!-- 动态实线路径 -->
+        <!-- <path :d="pathData" class="movement-path" stroke="#409EFF" stroke-width="3" fill="none" /> -->
+        <!-- 移动点 -->
+        <!-- <circle v-if="pathData" class="moving-dot" r="6">
+          <animateMotion :path="pathData" dur="3s" repeatCount="indefinite" />
+        </circle>
+      </svg> -->
       <div class="beacon-grid">
         <el-card 
-          v-for="item in beaconDevices" 
-          :key="item.id"
-          :id="'myFrame'+item.id" 
-          class="beacon-point"
-          :class="{
-            'active': isBeaconActive(item.id),
-            'fence': item.device_type === 39
-          }"
-          :style="getBeaconStyle(item)">
-          <div :class="['beacon-content', {
-            'active': isBeaconActive(item.id),
-            'fence': item.device_type === 39
-          }]">
+        v-for="item in beaconDevices" 
+        :key="item.id" :id="'myFrame' + item.id" 
+        class="beacon-point" :class="{
+        'active': isBeaconActive(item.id),
+        'fence': item.device_type === 39
+      }" 
+      :style="getBeaconStyle(item)">
+          <div :class="['beacon-content', {'active': isBeaconActive(item.id), 'fence': item.device_type === 39 }]">
             <div class="beacon-icon">
-              <div :class="['radar-wave', {
-                'fence-wave': item.device_type === 39
-              }]" v-if="isBeaconActive(item.id)"></div>
-              <svg-icon 
-                :id="'myIcon' + item.id" 
-                icon-class="footmark" 
-                class="footmark-icon"
-                :style="{ 
-                  display: isBeaconActive(item.id) ? 'inline-block' : 'none',
-                  color: item.device_type === 39 ? '#fff' : ''
-                }" />
+              <div :class="['radar-wave', {'fence-wave': item.device_type === 39}]" v-if="isBeaconActive(item.id)"></div>
+              <svg-icon :id="'myIcon' + item.id" icon-class="footmark" class="footmark-icon" 
+              :style="{display: isBeaconActive(item.id) ? 'inline-block' : 'none',color: item.device_type === 39 ? '#fff' : '' }" />
             </div>
             <div class="beacon-name">{{ item.name }}</div>
-            <div :id="'myText' + item.id" class="sequence-number"></div>
+            <!-- <div :id="'myText' + item.id" class="sequence-number"></div> -->
           </div>
         </el-card>
       </div>
-      
-      <svg class="path-overlay" :viewBox="viewBox">
-        <path 
-          :d="pathData" 
-          class="movement-path-bg"
-          stroke="#E1E1E1"
-          stroke-width="3"
-          stroke-dasharray="5,5"
-          fill="none" />
-        <path 
-          :d="pathData" 
-          class="movement-path" 
-          fill="none" />
-        <circle 
-          v-if="pathData"
-          class="moving-dot"
-          r="6"
-          fill="#409EFF">
-          <animateMotion
-            :path="pathData"
-            dur="3s"
-            repeatCount="indefinite" />
-        </circle>
-      </svg>
+
+
     </div>
     <div v-if="locationList.length > 0" class="timeline-container">
       <el-timeline>
-        <el-timeline-item
-          v-for="(item, index) in locationList"
-          :key="index"
-          :timestamp="formatterCreateTime(item.create_time)"
-          :type="getTimelineItemType(index)">
-          <el-card class="timeline-card" :class="{'current-location': index === 0}">
+        <el-timeline-item v-for="(item, index) in locationList" 
+         :key="index"
+          :timestamp="formatterCreateTime(item.create_time)" :type="getTimelineItemType(index)">
+          <el-card class="timeline-card" :class="{ 'current-location': index === 0 }">
             <div class="location-info">
               <i class="el-icon-location-outline"></i>
               <span>{{ item.full_name }}</span>
@@ -89,17 +71,9 @@
         </el-timeline-item>
       </el-timeline>
     </div>
-    <el-pagination
-        v-if="pageData"
-        slot="pagination"
-        :current-page="pageData.page_no"
-        :page-sizes="[20, 50, 100, 200]"
-        :page-size="pageData.page_size"
-        layout="total, sizes, prev, pager, next, jumper"
-        :total="pageData.data_total"
-        @size-change="handlePageSizeChange"
-        @current-change="handlePageCurrentChange"
-    />
+    <el-pagination v-if="pageData" slot="pagination" :current-page="pageData.page_no" :page-sizes="[20, 50, 100, 200]"
+      :page-size="pageData.page_size" layout="total, sizes, prev, pager, next, jumper" :total="pageData.data_total"
+      @size-change="handlePageSizeChange" @current-change="handlePageCurrentChange" />
   </div>
 </template>
 
@@ -123,8 +97,7 @@ export default {
       default: false
     }
   },
-
-  data: function() {
+  data: function () {
     return {
       beaconDevices: [],
       locationList: [],
@@ -145,7 +118,8 @@ export default {
       myTitle: '',
       pathData: '',
       viewBox: '0 0 1000 1000',
-      gridPositions: null
+      gridPositions: null,
+
     }
   },
   watch: {
@@ -190,7 +164,7 @@ export default {
         }
         _this.locationList = []
         if (_this.tableData.length > 0) {
-          _this.changeStyle()
+              _this.changeStyle()
         }
       })
     },
@@ -202,7 +176,7 @@ export default {
         _this.myTitle = this.$t('watch.notCovered')
       }
       this.tableData.reverse()
-      for (let i = 0; i< _this.tableData.length; i++) {
+      for (let i = 0; i < _this.tableData.length; i++) {
         (function (t, data) {
           setTimeout(function () {
             if (myId) {
@@ -217,7 +191,7 @@ export default {
                 prevElement.style.color = prevBeacon?.device_type === 39 ? 'white' : '#333'
               }
             }
-            const currentElement = document.getElementById('myFrame'+data.beacon_device_id)
+            const currentElement = document.getElementById('myFrame' + data.beacon_device_id)
             if (currentElement) {
               const beacon = _this.beaconDevices.find(b => b.id === data.beacon_device_id)
               if (beacon && beacon.device_type === 39) {
@@ -228,7 +202,7 @@ export default {
                 currentElement.style.color = 'white'
               }
             }
-            document.getElementById('myIcon'+data.beacon_device_id).style.display='inline'
+            document.getElementById('myIcon' + data.beacon_device_id).style.display = 'inline'
             _this.$notify({
               title: data.full_name,
               message: unixToDate(data.create_time) + _this.$t('watch.in') + data.full_name + _this.$t('watch.nearby'),
@@ -238,19 +212,22 @@ export default {
             })
             myId = 'myFrame'+data.beacon_device_id
             _this.locationList.unshift(data)
-            let text = document.getElementById('myText'+data.beacon_device_id)
-            if (text.innerText) {
-              text.innerText = text.innerText + '、'+ (i+1)
-            } else {
-              text.innerText = (i+1)
-            }
+            // let text = document.getElementById('myText' + data.beacon_device_id)
+            // if (text.innerText) {
+            //   text.innerText = text.innerText + '、' + (i + 1)
+            // } else {
+            //   text.innerText = (i + 1)
+            // }
             if (_this.clearList.find(p => p === data.beacon_device_id) == null) {
               _this.clearList.push(data.beacon_device_id)
             }
+            // if (t === _this.tableData.length - 1) {
+            //   _this.updateMovementPath()
+            // }
           }, 250 * t)
         })(i, _this.tableData[i])
       }
-      this.updateMovementPath()
+      // this.updateMovementPath()
     },
     sx() {
       this.queryTime = null
@@ -289,8 +266,8 @@ export default {
         } else {
           element.style = ''
         }
-        document.getElementById('myIcon'+item).style.display='none'
-        document.getElementById('myText'+item).innerText = ''
+        document.getElementById('myIcon' + item).style.display = 'none'
+        document.getElementById('myText' + item).innerText = ''
       })
     },
     formatterCreateTime(data) {
@@ -305,7 +282,7 @@ export default {
       }, 30000)
       this.websock.send(this.deviceId)
     },
-    initWebSocket: function() {
+    initWebSocket: function () {
       const stockbase = DeviceUrl.replace('http', 'ws')
       this.websock = new WebSocket(stockbase + '/web-socket/device_location/' + this.deviceId)
       this.websock.onopen = this.websocketonopen
@@ -313,13 +290,13 @@ export default {
       this.websock.onmessage = this.websocketonmessage
       this.websock.onclose = this.websocketclose
     },
-    websocketonopen: function() {
+    websocketonopen: function () {
       console.log(this.$t('deviceManage.webSocketSuccess'))
     },
-    websocketonerror: function(e) {
+    websocketonerror: function (e) {
       console.log(this.$t('deviceManage.webSocketError'))
     },
-    websocketonmessage: function(e) {
+    websocketonmessage: function (e) {
       console.log(this.$t('action.getMsg'), e.data)
       const data = JSON.parse(e.data)
       if (data.beacon_device_id) {
@@ -327,24 +304,26 @@ export default {
         this.fullscreenLoading = false
         let size = this.tableData.length
         if (size > 0) {
-          document.getElementById('myFrame'+this.tableData[(size - 1)].beacon_device_id).style = 'background: #d3dce6'
+          document.getElementById('myFrame' + this.tableData[(size - 1)].beacon_device_id).style = 'background: #d3dce6'
         }
-        document.getElementById('myFrame'+data.beacon_device_id).style = 'color: white;background: #3DCB0A'
-        document.getElementById('myIcon'+data.beacon_device_id).style.display='inline'
+        document.getElementById('myFrame' + data.beacon_device_id).style = 'color: white;background: #3DCB0A'
+        document.getElementById('myIcon' + data.beacon_device_id).style.display = 'inline'
         this.$notify({
           title: data.full_name,
           message: unixToDate(data.create_time) + this.$t('watch.in') + data.full_name + this.$t('watch.nearby'),
           showClose: false,
           offset: 100
         })
-        let text = document.getElementById('myText'+data.beacon_device_id)
-        if (text.innerText) {
-          text.innerText = text.innerText + '、'+ (size+1)
-        } else {
-          text.innerText = (i+1)
-        }
+        
+        // let text = document.getElementById('myText' + data.beacon_device_id)
+        // if (text.innerText) {
+        //   text.innerText = text.innerText + '、' + (size + 1)
+        // } else {
+        //   text.innerText = (i + 1)
+        // }
         this.tableData.push(data)
         this.locationList.unshift(data)
+        console.log('websocketonmessage连接赋值', this.locationList)
         if (this.clearList.find(p => p === data.beacon_device_id) == null) {
           this.clearList.push(data.beacon_device_id)
         }
@@ -377,17 +356,16 @@ export default {
       }
     },
     isBeaconActive(beaconId) {
-      return this.locationList.length > 0 && 
+      return this.locationList.length > 0 &&
         this.locationList[0].beacon_device_id === beaconId
     },
     updateMovementPath() {
       if (this.locationList.length < 2 || !this.gridPositions) return
-      
+
       const points = this.locationList.map(location => {
         const pos = this.gridPositions[location.beacon_device_id]
         return `${pos.x * 100},${pos.y * 100}`
       })
-      
       this.pathData = `M ${points.join(' L ')}`
     },
     getTimelineItemType(index) {
@@ -403,20 +381,46 @@ export default {
 .location-map {
   position: relative;
   margin: 20px 0;
-  height: 600px;
+  height: 200px;
   background: #f8fafc;
   border-radius: 8px;
   padding: 20px;
   overflow: hidden;
 }
-
+.block {
+  display: flex;
+  align-items: center;
+}
+.color-legend {
+  margin-left: auto;
+  display: flex;
+  gap: 20px;
+}
+.legend-item {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+.color-dot {
+  width: 12px;
+  height: 12px;
+  border-radius: 50%;
+  
+  &.green {
+    background: #3DCB0A;
+  }
+  
+  &.red {
+    background: #F56C6C;
+  }
+}
 .grid-background {
   position: absolute;
   top: 0;
   left: 0;
   right: 0;
   bottom: 0;
-  background-image: 
+  background-image:
     linear-gradient(rgba(200, 200, 200, 0.1) 1px, transparent 1px),
     linear-gradient(90deg, rgba(200, 200, 200, 0.1) 1px, transparent 1px);
   background-size: 20px 20px;
@@ -424,80 +428,84 @@ export default {
 
 .beacon-grid {
   display: grid;
-  grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
+  grid-template-columns: repeat(auto-fit, minmax(75px, 1fr));
   gap: 20px;
-  height: 100%;
 }
 
 .beacon-point {
   position: relative;
   transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
-  background: white;
+  background: #cfdce0;
   border: none;
   box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
-  
   &:hover {
     transform: translateY(-2px);
     box-shadow: 0 8px 15px -3px rgba(0, 0, 0, 0.1);
   }
-  
+
   &.active {
     transform: scale(1.05);
     background: #3DCB0A;
     box-shadow: 0 0 25px rgba(61, 203, 10, 0.4);
-    
+
     .beacon-content {
       color: white;
-      
+
       .sequence-number {
         color: rgba(255, 255, 255, 0.8);
       }
     }
   }
-  
+
   &.fence {
     background: #F56C6C !important;
-    
+
     .beacon-content {
       color: white !important;
     }
-    
+
     &:hover {
       box-shadow: 0 8px 15px -3px rgba(245, 108, 108, 0.4);
     }
-    
+
     &.active {
       box-shadow: 0 0 25px rgba(245, 108, 108, 0.6);
     }
   }
 }
-
 .beacon-content {
-  text-align: center;
-  padding: 10px;
-  
+  position: relative;  // 添加相对
+  display: flex;
+  justify-content: center;  // 添加水平居中
+  align-items: center;
+
+  width: 100%;
+  height: 100%;
+
   &.active {
     color: white;
   }
-  
+
   &.fence {
     color: white;
   }
 }
 
 .beacon-name {
-  margin: 8px 0;
+  // margin: 12px 0;
+  text-align: center;  // 文字居中
   font-weight: bold;
 }
 
 .beacon-icon {
-  position: relative;
+  position: absolute; 
+  left: 8px;         
+  top: 50%;          
+  transform: translateY(-50%);  // 调整垂直居中位置
   width: 48px;
   height: 48px;
-  margin: 0 auto 12px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
+  display: inline-flex;
+  align-items: center; /* 垂直居中对齐雷达波和图标 */
 }
 
 .radar-wave {
@@ -507,7 +515,7 @@ export default {
   border-radius: 50%;
   background: rgba(61, 203, 10, 0.2);
   animation: radar-pulse 2s infinite;
-  
+
   &.fence-wave {
     background: rgba(245, 108, 108, 0.2);
   }
@@ -545,8 +553,14 @@ export default {
 
 .timeline-container {
   margin: 20px 0;
-  max-height: 300px;
+  max-height: 475px;
   overflow-y: auto;
+  ::v-deep .el-timeline-item__tail{
+    left:6px
+  }
+  ::v-deep .el-timeline-item__node--normal {
+  left:0 ;
+}
 }
 
 .timeline-card {

+ 6 - 6
src/views/ncs-orginazition/components/partInfoEdit.vue

@@ -94,8 +94,8 @@
               </el-form-item>
             </el-col>
             <el-col v-if="isShow" :span="8">
-              <el-form-item label="机构UI类型" prop="ui_version_type">
-                <el-select v-model="formmodel.ui_version_type" placeholder="请选择机构UI类型" clearable>
+              <el-form-item :label="this.$t('partInfo.institutionUiType')" prop="ui_version_type">
+                <el-select v-model="formmodel.ui_version_type" :placeholder="this.$t('partInfo.selectUiVersionType')" clearable>
                   <el-option v-for="(item, index) in versionTypeList" :key="index" :label="item.label" :value="item.value" />
                 </el-select>
               </el-form-item>
@@ -1007,12 +1007,12 @@ export default {
       ],
       voiceType: ['RTC', 'SIP'],
       versionTypeList: [
-        {label: '医院版', value: 1},
-        {label: '月子中心版', value: 2},
-        {label: '养老院版', value: 3}
+        {label: this.$t('he20250514.versionTypeHospital'), value: 1},
+        {label: this.$t('he20250514.versionTypeMaternity'), value: 2},
+        {label: this.$t('he20250514.versionTypeNursingHome'), value: 3}
       ]
     }
-  },
+  },   
   async mounted() {
     this.isShow = JSON.parse(Storage.getItem('calling_user')).username === 'superadmin'
     // this.nurseLevel0Config = await API_Nurse.getNurseConfigs(this.partId, 0)

+ 7 - 7
src/views/ncs-orginazition/index.vue

@@ -87,8 +87,8 @@
               </el-form-item>
             </el-col>
             <el-col :span="8">
-              <el-form-item label="机构UI类型" prop="ui_version_type">
-                <el-select v-model="formmodel.ui_version_type" placeholder="请选择机构UI类型" clearable>
+              <el-form-item :label="this.$t('partInfo.institutionUiType')" prop="ui_version_type">
+                <el-select v-model="formmodel.ui_version_type" :placeholder="this.$t('partInfo.selectUiVersionType')" clearable>
                   <el-option v-for="(item, index) in versionTypeList" :key="index" :label="item.label" :value="item.value" />
                 </el-select>
               </el-form-item>
@@ -145,7 +145,7 @@ export default {
           { required: true, message: this.$t('partInfo.shopMemberPasswordMsg'), trigger: 'blur' }
         ],
         ui_version_type: [
-            { required: true, message: '请选择机构UI类型', trigger: 'blur' }
+            { required: true, message: this.$t('partInfo.selectUiVersionType'), trigger: 'blur' }
         ]
       },
       /** 上级机构数组 **/
@@ -167,9 +167,9 @@ export default {
       frameworkComponents: null,
       isDisabled: false,
       versionTypeList: [
-        {label: '医院版', value: 1},
-        {label: '月子中心版', value: 2},
-        {label: '养老院版', value: 3}
+        {label: this.$t('he20250514.versionTypeHospital'), value: 1},
+        {label: this.$t('he20250514.versionTypeMaternity'), value: 2},
+        {label: this.$t('he20250514.versionTypeNursingHome'), value: 3}
       ]
     }
   },
@@ -210,7 +210,7 @@ export default {
         filterParams: {debounceMs: 200, newRowsAction: 'keep'},
         cellRenderer: this.shopTypeFormatter
       },
-      { headerName: 'UI类型', field: 'ui_version_type', sortable: true, filter: 'agTextColumnFilter', width: 110,
+      { headerName: this.$t('partInfo.uiVersionType'), field: 'ui_version_type', sortable: true, filter: 'agTextColumnFilter', width: 110,
         // flex: 1,
         filterParams: {debounceMs: 200, newRowsAction: 'keep'},
         cellRenderer: this.versionTypeTypeFormatter