林业管理新范式:layer弹窗实现森林资源实时监控系统

林业管理新范式:layer弹窗实现森林资源实时监控系统

【免费下载链接】layer 【免费下载链接】layer 项目地址: https://gitcode.com/gh_mirrors/lay/layer

你是否还在为森林资源监控系统中数据展示分散、操作繁琐而困扰?传统林业管理系统往往将地图、传感器数据、预警信息分散在不同页面,管理人员需要在多个界面间频繁切换,导致响应延迟和操作失误。本文将展示如何利用layer(弹出层组件)构建一体化森林资源监控弹窗系统,实现"一次点击,全域掌控"的高效管理模式。

读完本文你将获得:

  • 掌握layer弹窗在林业监控场景下的高级配置技巧
  • 学会构建响应式森林资源数据仪表盘
  • 实现实时传感器数据可视化与异常预警
  • 开发移动端适配的林业巡检交互界面
  • 获取完整可复用的代码示例与最佳实践

技术选型与架构设计

森林监控系统技术栈对比

技术方案加载速度操作流畅度兼容性开发难度林业场景适配度
传统多页面慢(3-5s)低(页面刷新)⭐⭐⭐
SPA单页应用中(1-3s)中(路由切换)⭐⭐⭐⭐
layer弹窗系统快(<500ms)高(无刷新)⭐⭐⭐⭐⭐
Electron桌面应用慢(5-8s)⭐⭐⭐

layer组件作为轻量级弹窗解决方案,特别适合林业管理系统的以下需求:

  • 快速响应:火灾预警等紧急情况需即时展示
  • 数据聚合:整合地图、视频、传感器等多源数据
  • 操作便捷:巡检人员可单手操作移动端界面
  • 低侵入性:可无缝集成到现有林业管理平台

系统架构流程图

mermaid

layer核心功能与林业场景适配

基础弹窗配置与林业定制

layer提供5种基础弹窗类型,针对林业监控系统优化后的配置如下:

// 森林资源监控弹窗基础配置
function createForestMonitorPopup(type, options) {
  const baseConfig = {
    type: 1,          // 页面层类型
    title: false,     // 隐藏默认标题栏(自定义林业主题标题)
    area: ['90%', '90%'], // 弹窗大小(自适应屏幕)
    shade: [0.5, '#000'], // 半透明遮罩(突出弹窗内容)
    shadeClose: false, // 点击遮罩不关闭(防止误触)
    maxmin: true,     // 支持最大化/最小化(适配不同监控场景)
    zIndex: 2000,     // 确保弹窗在地图等组件之上
    anim: 2,          // 平滑弹出动画(减少视觉干扰)
    isOutAnim: false, // 关闭退出动画(紧急情况下快速消失)
    skin: 'forest-monitor-skin', // 林业主题样式
    resize: function(size) {
      // 窗口大小改变时重绘图表和地图
      if (window.forestCharts) {
        window.forestCharts.forEach(chart => chart.resize());
      }
      if (window.mapInstance) {
        window.mapInstance.resize();
      }
    }
  };
  
  // 根据不同弹窗类型进行配置扩展
  const typeConfig = {
    map: {
      area: ['100%', '100%'],
      shade: [0.7, '#000'],
      title: '森林资源分布图',
      move: '.custom-map-header'
    },
    sensor: {
      area: ['800px', '600px'],
      offset: 'rb', // 右下角弹出(不遮挡主地图)
      title: '实时传感器数据'
    },
    alert: {
      area: ['500px', '300px'],
      anim: 4, // 抖动动画(引起注意)
      shade: [0.8, '#f00'],
      title: '⚠️ 森林预警信息'
    },
    patrol: {
      area: ['400px', '600px'],
      offset: 'lb', // 左下角弹出(适配移动端操作)
      title: '巡检任务详情'
    }
  };
  
  return layer.open({...baseConfig, ...typeConfig[type], ...options});
}

五种核心弹窗类型的林业应用

1. 矢量地图弹窗(核心功能)
// 创建森林资源分布地图弹窗
function openForestMapPopup(regionId) {
  // 先加载地图容器和样式
  const mapContainer = `
    <div class="forest-map-container">
      <div class="map-toolbar">
        <button class="map-btn" id="zoomIn">+ 放大</button>
        <button class="map-btn" id="zoomOut">- 缩小</button>
        <button class="map-btn" id="resetView">重置视图</button>
        <div class="map-layers">
          <select id="layerControl">
            <option value="base">基础地形</option>
            <option value="vegetation">植被覆盖</option>
            <option value="water">水系分布</option>
            <option value="sensor">传感器位置</option>
            <option value="danger">高风险区域</option>
          </select>
        </div>
      </div>
      <div id="forestMap" style="width:100%;height:calc(100% - 40px);"></div>
    </div>
  `;
  
  // 打开弹窗
  const index = createForestMonitorPopup('map', {
    content: mapContainer,
    success: function(layero, index) {
      // 弹窗创建成功后初始化地图
      initForestMap('forestMap', regionId);
      
      // 绑定地图控制事件
      layero.find('#zoomIn').click(() => {
        window.mapInstance.zoomIn();
      });
      layero.find('#zoomOut').click(() => {
        window.mapInstance.zoomOut();
      });
      layero.find('#resetView').click(() => {
        window.mapInstance.resetView();
      });
      layero.find('#layerControl').change(function() {
        window.mapInstance.setLayer($(this).val());
      });
    }
  });
  
  // 保存弹窗索引,用于后续操作
  window.forestPopups = window.forestPopups || {};
  window.forestPopups.map = index;
}
2. 传感器数据监控面板

森林中的温湿度、土壤含水率、二氧化碳浓度等传感器数据需要实时展示并进行可视化分析:

// 初始化传感器数据弹窗
function openSensorDataPopup(sensorId) {
  // 面板HTML结构
  const sensorPanel = `
    <div class="sensor-dashboard">
      <div class="sensor-header">
        <h3>传感器 #${sensorId} 实时监控</h3>
        <div class="sensor-status normal">正常运行中</div>
      </div>
      <div class="sensor-charts">
        <div class="chart-item">
          <canvas id="temperatureChart" height="180"></canvas>
        </div>
        <div class="chart-item">
          <canvas id="humidityChart" height="180"></canvas>
        </div>
      </div>
      <div class="sensor-data-grid">
        <div class="data-item">
          <span class="data-label">当前温度</span>
          <span class="data-value" id="currentTemp">23.5°C</span>
        </div>
        <div class="data-item">
          <span class="data-label">土壤湿度</span>
          <span class="data-value" id="soilMoisture">68%</span>
        </div>
        <div class="data-item">
          <span class="data-label">CO₂浓度</span>
          <span class="data-value" id="co2Level">420ppm</span>
        </div>
        <div class="data-item">
          <span class="data-label">电池电量</span>
          <span class="data-value" id="batteryLevel">87%</span>
        </div>
      </div>
    </div>
  `;
  
  // 打开弹窗
  const index = createForestMonitorPopup('sensor', {
    content: sensorPanel,
    success: function(layero, index) {
      // 初始化图表
      initSensorCharts();
      
      // 模拟实时数据更新
      simulateSensorDataUpdates(sensorId, layero);
    }
  });
  
  window.forestPopups.sensor = index;
}

// 初始化传感器数据图表
function initSensorCharts() {
  // 温度图表
  const tempCtx = document.getElementById('temperatureChart').getContext('2d');
  const tempChart = new Chart(tempCtx, {
    type: 'line',
    data: {
      labels: generateTimeLabels(24),
      datasets: [{
        label: '温度 (°C)',
        data: generateRandomData(24, 18, 28),
        borderColor: '#ff6384',
        backgroundColor: 'rgba(255, 99, 132, 0.1)',
        tension: 0.4,
        fill: true
      }]
    },
    options: getChartOptions('温度变化趋势')
  });
  
  // 湿度图表
  const humCtx = document.getElementById('humidityChart').getContext('2d');
  const humChart = new Chart(humCtx, {
    type: 'line',
    data: {
      labels: generateTimeLabels(24),
      datasets: [{
        label: '湿度 (%)',
        data: generateRandomData(24, 40, 80),
        borderColor: '#36a2eb',
        backgroundColor: 'rgba(54, 162, 235, 0.1)',
        tension: 0.4,
        fill: true
      }]
    },
    options: getChartOptions('湿度变化趋势')
  });
  
  // 保存图表实例以便后续更新
  window.forestCharts = [tempChart, humChart];
}
3. 森林火灾预警弹窗

针对紧急情况设计的预警弹窗需要立即引起管理人员注意:

// 火灾预警弹窗
function showFireAlertPopup(alertData) {
  // 计算风险等级
  const riskLevel = calculateFireRisk(alertData);
  const riskClass = riskLevel === 'high' ? 'alert-high' : 
                   riskLevel === 'medium' ? 'alert-medium' : 'alert-low';
  const riskIcon = riskLevel === 'high' ? '🔥' : 
                  riskLevel === 'medium' ? '⚠️' : 'ℹ️';
  
  // 预警内容
  const alertContent = `
    <div class="fire-alert-popup ${riskClass}">
      <div class="alert-header">
        <h2>${riskIcon} 森林火灾${riskLevel === 'high' ? '紧急' : riskLevel === 'medium' ? '中度' : '低度'}预警</h2>
        <div class="alert-time">${formatTime(alertData.timestamp)}</div>
      </div>
      <div class="alert-body">
        <div class="alert-map">
          <img src="https://api.mapbox.com/styles/v1/mapbox/satellite-v9/static/pin-l-fire+ff0000(${alertData.longitude},${alertData.latitude})/${alertData.longitude},${alertData.latitude},14,0/300x200?access_token=your_token" 
               alt="火灾位置地图">
        </div>
        <div class="alert-details">
          <p><strong>位置:</strong> ${alertData.region} (${alertData.latitude.toFixed(6)}, ${alertData.longitude.toFixed(6)})</p>
          <p><strong>温度:</strong> ${alertData.temperature}°C (${riskLevel === 'high' ? '异常高温' : '偏高'})</p>
          <p><strong>风速:</strong> ${alertData.windSpeed} m/s ${alertData.windDirection}</p>
          <p><strong>风险评估:</strong> ${riskLevel === 'high' ? '火势快速蔓延风险极高,需立即组织撤离' : 
                                       riskLevel === 'medium' ? '存在局部蔓延可能,建议加强监控' : 
                                       '低风险,可能为自然升温现象'}</p>
        </div>
      </div>
      <div class="alert-actions">
        <button id="viewCameraBtn" class="layui-btn">查看现场摄像头</button>
        <button id="dispatchTeamBtn" class="layui-btn layui-btn-danger">派遣扑火队伍</button>
        <button id="closeAlertBtn" class="layui-btn layui-btn-primary">关闭预警</button>
      </div>
    </div>
  `;
  
  // 创建特殊的预警弹窗
  const index = layer.open({
    type: 1,
    title: false,
    area: ['80%', '85%'],
    shade: [0.8, '#ff4444'], // 红色半透明遮罩
    shadeClose: false,
    zIndex: 3000, // 确保在所有弹窗之上
    anim: 4, // 抖动动画引起注意
    isOutAnim: false,
    skin: 'fire-alert-skin',
    content: alertContent,
    success: function(layero, index) {
      // 播放警报声音
      playAlertSound(riskLevel);
      
      // 绑定按钮事件
      layero.find('#viewCameraBtn').click(() => {
        // 打开摄像头监控弹窗
        openCameraPopup(alertData.cameraId);
      });
      
      layero.find('#dispatchTeamBtn').click(() => {
        // 打开派遣队伍弹窗
        openDispatchTeamPopup(alertData.regionId, alertData.latitude, alertData.longitude);
      });
      
      layero.find('#closeAlertBtn').click(() => {
        layer.close(index);
      });
      
      // 高风险预警添加闪烁效果
      if (riskLevel === 'high') {
        startAlertBlink(layero);
      }
    }
  });
  
  // 记录当前预警弹窗索引
  window.currentAlertPopup = index;
}

响应式设计与移动端适配

林业巡检人员通常在野外使用移动设备,需要特别优化移动端体验:

移动端适配方案

/* 森林监控弹窗移动端样式 */
@media screen and (max-width: 768px) {
  .forest-map-container {
    height: 100vh;
    width: 100vw;
  }
  
  .sensor-dashboard .sensor-charts {
    flex-direction: column;
  }
  
  .sensor-data-grid {
    grid-template-columns: 1fr 1fr;
    gap: 8px;
  }
  
  .data-item {
    padding: 8px;
    font-size: 14px;
  }
  
  .alert-map {
    height: 200px;
    margin-bottom: 15px;
  }
  
  /* 底部操作栏固定在移动端底部 */
  .mobile-operation-bar {
    position: fixed;
    bottom: 0;
    left: 0;
    right: 0;
    height: 50px;
    background: #333;
    display: flex;
    justify-content: space-around;
    align-items: center;
    z-index: 1000;
  }
  
  .mobile-operation-bar button {
    width: 40px;
    height: 40px;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
  }
  
  /* 移动端手势操作提示 */
  .gesture-hint {
    position: absolute;
    bottom: 60px;
    left: 50%;
    transform: translateX(-50%);
    background: rgba(0,0,0,0.7);
    color: white;
    padding: 5px 10px;
    border-radius: 4px;
    font-size: 12px;
  }
}

移动端巡检任务弹窗

// 移动端巡检任务弹窗
function openPatrolTaskPopup(taskId, isMobile) {
  // 移动端和桌面端使用不同布局
  const isMobileDevice = isMobile || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
  
  const patrolContent = `
    <div class="patrol-task-container ${isMobileDevice ? 'mobile' : ''}">
      <div class="task-header">
        <h3>巡检任务 #${taskId}</h3>
        <div class="task-status">进行中</div>
      </div>
      
      <div class="task-map">
        <div id="patrolMap" style="width:100%;height:${isMobileDevice ? '200px' : '300px'}"></div>
        ${isMobileDevice ? '<div class="gesture-hint">拖动可查看周边环境</div>' : ''}
      </div>
      
      <div class="task-details">
        <div class="detail-item">
          <span class="detail-label">任务区域:</span>
          <span class="detail-value">${getTaskRegion(taskId)}</span>
        </div>
        <div class="detail-item">
          <span class="detail-label">开始时间:</span>
          <span class="detail-value">${formatTime(getTaskStartTime(taskId))}</span>
        </div>
        <div class="detail-item">
          <span class="detail-label">预计完成:</span>
          <span class="detail-value">${formatTime(getTaskEndTime(taskId))}</span>
        </div>
        <div class="detail-item">
          <span class="detail-label">巡检点数量:</span>
          <span class="detail-value">${getCheckpointCount(taskId)}个 (已完成${getCompletedCheckpoints(taskId)}个)</span>
        </div>
      </div>
      
      <div class="task-actions ${isMobileDevice ? 'mobile-actions' : ''}">
        <button id="startNavigationBtn" class="layui-btn">${isMobileDevice ? '开始导航' : '查看导航路线'}</button>
        <button id="recordObservationBtn" class="layui-btn layui-btn-normal">记录观察结果</button>
        ${!isMobileDevice ? '<button id="assignToBtn" class="layui-btn layui-btn-primary">重新分配</button>' : ''}
      </div>
      
      ${isMobileDevice ? `
        <div class="mobile-operation-bar">
          <button id="takePhotoBtn" class="layui-btn layui-btn-primary">📷 拍照</button>
          <button id="emergencyBtn" class="layui-btn layui-btn-danger">🆘 紧急求助</button>
          <button id="completeTaskBtn" class="layui-btn">✅ 完成任务</button>
        </div>
      ` : ''}
    </div>
  `;
  
  // 打开弹窗
  const index = createForestMonitorPopup('patrol', {
    content: patrolContent,
    success: function(layero, index) {
      // 初始化巡检地图
      initPatrolMap('patrolMap', taskId);
      
      // 绑定按钮事件
      layero.find('#startNavigationBtn').click(() => {
        startPatrolNavigation(taskId, isMobileDevice);
      });
      
      layero.find('#recordObservationBtn').click(() => {
        openObservationRecordPopup(taskId);
      });
      
      if (!isMobileDevice) {
        layero.find('#assignToBtn').click(() => {
          openAssignTaskPopup(taskId);
        });
      }
      
      // 移动端按钮事件
      if (isMobileDevice) {
        layero.find('#takePhotoBtn').click(() => {
          // 调用设备相机
          takePatrolPhoto(taskId);
        });
        
        layero.find('#emergencyBtn').click(() => {
          // 紧急求助
          openEmergencyHelpPopup(taskId);
        });
        
        layero.find('#completeTaskBtn').click(() => {
          completePatrolTask(taskId, index);
        });
      }
    }
  });
  
  window.forestPopups.patrol = index;
}

高级功能实现

多弹窗协同与数据同步

在复杂林业监控场景中,经常需要同时打开多个相关弹窗,如地图弹窗和传感器数据弹窗,需要确保它们之间的数据同步和交互:

// 多弹窗协同管理
class ForestPopupManager {
  constructor() {
    this.popups = {}; // 存储当前打开的弹窗
    this.activeRegion = null; // 当前活动区域
    this.listeners = {}; // 事件监听器
  }
  
  // 创建并跟踪弹窗
  createPopup(type, options) {
    const popupTypes = {
      map: openForestMapPopup,
      sensor: openSensorDataPopup,
      alert: showFireAlertPopup,
      patrol: openPatrolTaskPopup,
      camera: openCameraPopup
    };
    
    if (!popupTypes[type]) {
      console.error('未知弹窗类型:', type);
      return null;
    }
    
    // 如果已有同类型弹窗,先关闭
    if (this.popups[type]) {
      this.closePopup(type);
    }
    
    // 创建弹窗并记录索引
    const index = popupTypes[type](options);
    this.popups[type] = index;
    
    // 关联区域数据
    if (options.regionId) {
      this.activeRegion = options.regionId;
      this.syncRegionData(options.regionId);
    }
    
    return index;
  }
  
  // 关闭弹窗
  closePopup(type) {
    if (this.popups[type]) {
      layer.close(this.popups[type]);
      delete this.popups[type];
      
      // 触发关闭事件
      this.triggerEvent('popupClosed', { type });
    }
  }
  
  // 关闭所有弹窗
  closeAllPopups() {
    Object.keys(this.popups).forEach(type => {
      this.closePopup(type);
    });
  }
  
  // 同步区域数据到所有相关弹窗
  syncRegionData(regionId) {
    if (!regionId) return;
    
    // 获取区域最新数据
    const regionData = getForestRegionData(regionId);
    
    // 更新地图弹窗
    if (this.popups.map && window.updateMapRegionData) {
      window.updateMapRegionData(regionId, regionData);
    }
    
    // 更新传感器弹窗
    if (this.popups.sensor && window.updateSensorData) {
      window.updateSensorData(regionId, regionData.sensors);
    }
    
    // 触发数据同步事件
    this.triggerEvent('regionDataSynced', { 
      regionId,
      timestamp: new Date().toISOString()
    });
  }
  
  // 注册事件监听器
  on(eventName, callback) {
    if (!this.listeners[eventName]) {
      this.listeners[eventName] = [];
    }
    this.listeners[eventName].push(callback);
  }
  
  // 触发事件
  triggerEvent(eventName, data) {
    if (this.listeners[eventName]) {
      this.listeners[eventName].forEach(callback => {
        try {
          callback(data);
        } catch (e) {
          console.error('事件处理出错:', e);
        }
      });
    }
  }
}

// 初始化弹窗管理器
window.forestPopupManager = new ForestPopupManager();

// 注册事件监听
window.forestPopupManager.on('regionDataSynced', (data) => {
  console.log(`区域${data.regionId}数据已同步,时间:${data.timestamp}`);
  
  // 可以在这里添加数据同步后的处理逻辑
  updateDashboardCounters();
});

window.forestPopupManager.on('popupClosed', (data) => {
  console.log(`${data.type}弹窗已关闭`);
  
  // 如果关闭的是当前活动区域的弹窗,取消区域高亮
  if (data.type === 'map') {
    window.activeRegion = null;
  }
});

离线数据处理与同步

林业巡检常处于网络不稳定环境,需要实现离线数据采集与同步功能:

// 离线数据处理服务
class ForestOfflineService {
  constructor() {
    this.offlineDataStore = 'forest_offline_data';
    this.syncStatus = 'idle'; // idle, syncing, error, completed
    this.pendingSyncCount = 0;
    
    // 初始化本地存储
    this.initLocalStorage();
    
    // 检查网络状态
    this.checkNetworkStatus();
    
    // 尝试自动同步
    if (navigator.onLine) {
      this.syncOfflineData();
    }
  }
  
  // 初始化本地存储
  initLocalStorage() {
    if (!localStorage.getItem(this.offlineDataStore)) {
      localStorage.setItem(this.offlineDataStore, JSON.stringify({
        observations: [],
        photos: [],
        sensorReadings: [],
        patrolLogs: [],
        lastSync: null
      }));
    }
  }
  
  // 检查网络状态
  checkNetworkStatus() {
    window.addEventListener('online', () => {
      console.log('网络已连接,尝试同步离线数据...');
      this.syncOfflineData();
      
      // 更新所有弹窗中的网络状态指示器
      this.updateNetworkStatusIndicators(true);
    });
    
    window.addEventListener('offline', () => {
      console.log('网络已断开,切换到离线模式');
      this.syncStatus = 'idle';
      
      // 更新所有弹窗中的网络状态指示器
      this.updateNetworkStatusIndicators(false);
    });
  }
  
  // 更新网络状态指示器
  updateNetworkStatusIndicators(isOnline) {
    const statusClass = isOnline ? 'online' : 'offline';
    const statusText = isOnline ? '在线' : '离线模式';
    
    // 更新所有打开弹窗中的状态指示器
    if ($('.network-status-indicator').length > 0) {
      $('.network-status-indicator').attr('class', `network-status-indicator ${statusClass}`);
      $('.network-status-indicator').text(statusText);
    }
    
    // 如果有未同步数据,显示数量
    if (!isOnline && this.pendingSyncCount > 0) {
      $('.network-status-indicator').append(` (待同步: ${this.pendingSyncCount})`);
    }
  }
  
  // 存储离线观察数据
  saveObservationOffline(observationData) {
    const offlineData = JSON.parse(localStorage.getItem(this.offlineDataStore));
    
    // 添加离线标志和时间戳
    const offlineObservation = {
      ...observationData,
      isOffline: true,
      recordedAt: new Date().toISOString(),
      syncStatus: 'pending'
    };
    
    offlineData.observations.push(offlineObservation);
    localStorage.setItem(this.offlineDataStore, JSON.stringify(offlineData));
    
    this.pendingSyncCount++;
    this.updateNetworkStatusIndicators(navigator.onLine);
    
    return true;
  }
  
  // 存储离线照片
  async savePhotoOffline(photoData, observationId) {
    try {
      const offlineData = JSON.parse(localStorage.getItem(this.offlineDataStore));
      
      // 对于照片,我们只存储基本信息和缩略图,原图存储在FileSystem
      const photoEntry = {
        id: 'photo_' + Date.now(),
        observationId,
        timestamp: new Date().toISOString(),
        thumbnail: photoData.thumbnail,
        fileSize: photoData.fileSize,
        syncStatus: 'pending'
      };
      
      // 保存完整照片到文件系统(使用File API)
      if (window.showSaveFilePicker) {
        // 现代浏览器支持
        const handle = await window.showSaveFilePicker({
          suggestedName: `${photoEntry.id}.jpg`,
          types: [{
            description: 'JPEG Image',
            accept: { 'image/jpeg': ['.jpg'] },
          }],
        });
        const writable = await handle.createWritable();
        await writable.write(photoData.rawData);
        await writable.close();
        
        // 保存文件路径
        photoEntry.filePath = handle.name;
      } else {
        // 兼容旧浏览器,使用localStorage存储base64(不推荐大文件)
        localStorage.setItem(`photo_${photoEntry.id}`, photoData.rawData);
      }
      
      offlineData.photos.push(photoEntry);
      localStorage.setItem(this.offlineDataStore, JSON.stringify(offlineData));
      
      this.pendingSyncCount++;
      this.updateNetworkStatusIndicators(navigator.onLine);
      
      return photoEntry.id;
    } catch (error) {
      console.error('保存离线照片失败:', error);
      return null;
    }
  }
  
  // 同步离线数据到服务器
  async syncOfflineData() {
    if (!navigator.onLine || this.syncStatus === 'syncing') {
      console.log('无法同步:', !navigator.onLine ? '网络未连接' : '正在同步中');
      return false;
    }
    
    this.syncStatus = 'syncing';
    this.updateNetworkStatusIndicators(true);
    
    const offlineData = JSON.parse(localStorage.getItem(this.offlineDataStore));
    const totalItems = offlineData.observations.length + offlineData.photos.length;
    
    if (totalItems === 0) {
      this.syncStatus = 'completed';
      this.pendingSyncCount = 0;
      this.updateNetworkStatusIndicators(true);
      return true;
    }
    
    console.log(`开始同步离线数据,共${totalItems}项`);
    
    try {
      // 1. 同步观察记录
      for (let i = 0; i < offlineData.observations.length; i++) {
        const obs = offlineData.observations[i];
        if (obs.syncStatus === 'pending') {
          const result = await syncObservationToServer(obs);
          
          if (result.success) {
            // 更新同步状态
            offlineData.observations[i].syncStatus = 'synced';
            offlineData.observations[i].serverId = result.serverId;
            this.pendingSyncCount--;
          } else {
            console.error(`观察记录同步失败: ${result.error}`);
            offlineData.observations[i].syncStatus = 'error';
            offlineData.observations[i].errorMsg = result.error;
          }
          
          // 保存进度
          localStorage.setItem(this.offlineDataStore, JSON.stringify(offlineData));
        }
      }
      
      // 2. 同步照片
      for (let i = 0; i < offlineData.photos.length; i++) {
        const photo = offlineData.photos[i];
        if (photo.syncStatus === 'pending') {
          // 获取完整照片数据
          let photoData;
          if (window.showOpenFilePicker && photo.filePath) {
            // 从文件系统读取
            const handles = await window.showOpenFilePicker({
              startIn: 'downloads',
              acceptedTypes: [{
                description: 'Images',
                accept: { 'image/jpeg': ['.jpg'] },
              }],
            });
            const file = await handles[0].getFile();
            photoData = await file.arrayBuffer();
          } else {
            // 从localStorage读取
            photoData = localStorage.getItem(`photo_${photo.id}`);
          }
          
          // 上传到服务器
          const result = await syncPhotoToServer(photo, photoData);
          
          if (result.success) {
            // 更新同步状态
            offlineData.photos[i].syncStatus = 'synced';
            offlineData.photos[i].serverId = result.serverId;
            this.pendingSyncCount--;
            
            // 删除本地缓存的原图
            if (!window.showSaveFilePicker) {
              localStorage.removeItem(`photo_${photo.id}`);
            }
          } else {
            console.error(`照片同步失败: ${result.error}`);
            offlineData.photos[i].syncStatus = 'error';
            offlineData.photos[i].errorMsg = result.error;
          }
          
          // 保存进度
          localStorage.setItem(this.offlineDataStore, JSON.stringify(offlineData));
        }
      }
      
      // 更新最后同步时间
      offlineData.lastSync = new Date().toISOString();
      localStorage.setItem(this.offlineDataStore, JSON.stringify(offlineData));
      
      this.syncStatus = 'completed';
      this.updateNetworkStatusIndicators(true);
      
      // 显示同步完成消息
      layer.msg(`离线数据同步完成,共${totalItems}项`, { icon: 1 });
      
      return true;
    } catch (error) {
      console.error('离线数据同步失败:', error);
      this.syncStatus = 'error';
      this.updateNetworkStatusIndicators(true);
      return false;
    }
  }
  
  // 获取同步状态
  getSyncStatus() {
    return {
      status: this.syncStatus,
      pendingCount: this.pendingSyncCount,
      lastSync: JSON.parse(localStorage.getItem(this.offlineDataStore)).lastSync
    };
  }
}

// 初始化离线服务
window.forestOfflineService = new ForestOfflineService();

// 在记录观察结果时使用离线服务
document.getElementById('saveObservationBtn').addEventListener('click', function() {
  const observationData = collectObservationData();
  
  if (navigator.onLine) {
    // 在线状态,直接提交
    submitObservation(observationData);
  } else {
    // 离线状态,保存到本地
    const saved = window.forestOfflineService.saveObservationOffline(observationData);
    if (saved) {
      layer.msg('已保存到本地,将在网络恢复后自动同步', { icon: 0 });
    } else {
      layer.msg('保存失败,请重试', { icon: 2 });
    }
  }
});

// 添加手动同步按钮事件
document.getElementById('syncOfflineDataBtn').addEventListener('click', function() {
  if (!navigator.onLine) {
    layer.msg('当前网络不可用,无法同步数据', { icon: 2 });
    return;
  }
  
  const status = window.forestOfflineService.getSyncStatus();
  if (status.pendingCount === 0) {
    layer.msg('没有待同步的离线数据', { icon: 0 });
    return;
  }
  
  if (status.status === 'syncing') {
    layer.msg('正在同步中,请稍候...', { icon: 0 });
    return;
  }
  
  // 显示确认框
  layer.confirm(`发现${status.pendingCount}项待同步数据,是否立即同步?`, {
    btn: ['立即同步', '稍后再说']
  }, function() {
    // 用户确认同步
    window.forestOfflineService.syncOfflineData();
    layer.closeAll('dialog');
    layer.msg('开始同步离线数据...', { icon: 1 });
  });
});

性能优化与最佳实践

layer弹窗性能优化策略

针对林业监控系统中可能同时打开多个弹窗、加载大量地图数据的场景,需要进行性能优化:

// layer弹窗性能优化工具
const PopupPerformanceOptimizer = {
  // 弹窗缓存池
  popupCache: {},
  
  // 预加载常用弹窗
  preloadCommonPopups() {
    // 分析林业管理系统使用频率
    const commonPopupTypes = ['map', 'sensor', 'alert'];
    
    commonPopupTypes.forEach(type => {
      // 创建一个最小化的缓存弹窗
      const cacheIndex = layer.open({
        type: 1,
        title: false,
        content: `<div class="popup-cache-placeholder" data-type="${type}"></div>`,
        area: ['100px', '100px'],
        offset: ['-9999px', '-9999px'], // 隐藏在视口外
        shade: false,
        closeBtn: false,
        isOutAnim: false,
        success: function(layero) {
          // 标记为缓存弹窗
          layero.addClass('popup-cache');
        }
      });
      
      this.popupCache[type] = cacheIndex;
    });
    
    console.log('常用弹窗已预加载到缓存池');
  },
  
  // 从缓存获取弹窗
  getFromCache(type) {
    if (this.popupCache[type]) {
      const cacheIndex = this.popupCache[type];
      const layero = $(`#layui-layer${cacheIndex}`);
      
      // 检查缓存弹窗是否存在
      if (layero.length > 0) {
        // 恢复正常显示状态
        layero.css({
          left: '',
          top: '',
          width: '',
          height: ''
        });
        
        // 清空缓存标记
        layero.removeClass('popup-cache');
        layero.find('.popup-cache-placeholder').remove();
        
        // 从缓存池移除
        delete this.popupCache[type];
        
        return cacheIndex;
      }
    }
    
    return null;
  },
  
  // 回收弹窗到缓存池
  recyclePopup(type, index) {
    // 只回收常用类型弹窗
    const recyclableTypes = ['map', 'sensor', 'alert', 'patrol'];
    if (!recyclableTypes.includes(type)) {
      // 非回收类型,直接关闭
      layer.close(index);
      return false;
    }
    
    // 如果缓存池已有该类型,直接关闭
    if (this.popupCache[type]) {
      layer.close(index);
      return false;
    }
    
    const layero = $(`#layui-layer${index}`);
    
    // 清空内容,但保留结构
    const contentArea = layero.find('.layui-layer-content');
    const originalContent = contentArea.html();
    
    // 保存原始设置以便恢复
    const originalSettings = {
      area: [layero.outerWidth(), layero.outerHeight()],
      skin: layero.attr('class'),
      maxmin: layero.find('.layui-layer-max').length > 0
    };
    
    // 重置为缓存状态
    contentArea.html(`<div class="popup-cache-placeholder" data-type="${type}"></div>`);
    layero.css({
      left: '-9999px',
      top: '-9999px',
      width: '100px',
      height: '100px'
    });
    
    // 添加缓存标记
    layero.addClass('popup-cache');
    
    // 保存到缓存池
    this.popupCache[type] = {
      index,
      originalSettings,
      lastUsed: new Date().getTime()
    };
    
    console.log(`弹窗已回收至缓存池: ${type}`);
    
    // 限制缓存池大小,LRU淘汰策略
    this.lruCacheEviction();
    
    return true;
  },
  
  // LRU缓存淘汰策略
  lruCacheEviction(maxSize = 5) {
    const cacheTypes = Object.keys(this.popupCache);
    
    if (cacheTypes.length <= maxSize) return;
    
    // 按最后使用时间排序
    const sortedTypes = cacheTypes.sort((a, b) => 
      this.popupCache[a].lastUsed - this.popupCache[b].lastUsed
    );
    
    // 淘汰最久未使用的
    const evictType = sortedTypes[0];
    const evictIndex = this.popupCache[evictType].index;
    
    layer.close(evictIndex);
    delete this.popupCache[evictType];
    
    console.log(`缓存池已满,淘汰弹窗: ${evictType}`);
  },
  
  // 监控弹窗性能
  monitorPerformance() {
    setInterval(() => {
      const openPopups = layer.index;
      const memoryUsage = window.performance.memory ? 
        (window.performance.memory.usedJSHeapSize / 1024 / 1024).toFixed(2) : 'N/A';
      
      console.log(`当前打开弹窗: ${openPopups}, 内存使用: ${memoryUsage}MB`);
      
      // 如果内存使用过高,自动回收不活跃弹窗
      if (memoryUsage !== 'N/A' && parseFloat(memoryUsage) > 256) {
        console.log('内存使用过高,触发自动回收');
        this.autoRecycleInactivePopups();
      }
    }, 30000); // 每30秒检查一次
  },
  
  // 自动回收非活跃弹窗
  autoRecycleInactivePopups() {
    const now = new Date().getTime();
    const inactiveThreshold = 5 * 60 * 1000; // 5分钟不活动
    
    // 遍历所有打开的弹窗
    $('[id^="layui-layer"]').each(function() {
      const layero = $(this);
      const times = layero.attr('times');
      const type = layero.attr('type');
      const lastActive = layero.data('lastActive') || now;
      
      // 检查是否超过不活动阈值
      if (now - lastActive > inactiveThreshold) {
        console.log(`回收非活跃弹窗: ${type} (${times})`);
        PopupPerformanceOptimizer.recyclePopup(type, times);
      }
    });
  },
  
  // 初始化性能监控
  init() {
    this.preloadCommonPopups();
    this.monitorPerformance();
    
    // 监听弹窗活动
    $(document).on('mousedown', '[id^="layui-layer"]', function() {
      // 更新最后活动时间
      $(this).data('lastActive', new Date().getTime());
    });
    
    console.log('弹窗性能优化器已初始化');
  }
};

// 初始化性能优化器
PopupPerformanceOptimizer.init();

// 修改弹窗创建函数,使用缓存
function createOptimizedForestPopup(type, options) {
  // 尝试从缓存获取
  const cacheIndex = PopupPerformanceOptimizer.getFromCache(type);
  
  if (cacheIndex) {
    console.log(`从缓存加载${type}弹窗`);
    
    // 更新缓存弹窗内容
    layer.style(cacheIndex, options.area || 'auto');
    
    // 重新设置内容和回调
    const layero = $(`#layui-layer${cacheIndex}`);
    layero.attr('type', type);
    
    // 清空并重新设置内容
    const contentArea = layero.find('.layui-layer-content');
    contentArea.html('');
    
    // 更新最后活动时间
    layero.data('lastActive', new Date().getTime());
    
    // 执行success回调
    if (options.success) {
      options.success(layero, cacheIndex);
    }
    
    return cacheIndex;
  }
  
  // 缓存未命中,创建新弹窗
  console.log(`创建新${type}弹窗`);
  return createForestMonitorPopup(type, options);
}

林业监控系统最佳实践总结

弹窗设计三原则
  1. 数据分层展示

    • 第一层:概览数据(森林覆盖率、警报数量等关键指标)
    • 第二层:详细数据(区域分布、历史趋势图表)
    • 第三层:原始数据(传感器日志、巡检记录)
  2. 操作流程简化

    • 常用操作不超过3次点击
    • 巡检任务遵循"开始-记录-完成"三步流程
    • 紧急操作(如火灾报警)一键直达
  3. 视觉设计规范

    • 色彩编码:绿色(正常)、黄色(警告)、红色(紧急)
    • 图标系统:统一的林业资源图标库
    • 响应式布局:从大屏监控中心到移动端巡检设备无缝适配
常见问题解决方案
问题场景解决方案代码示例
地图数据加载缓慢实现瓦片懒加载与预缓存map.setTileLoadPolicy('low')
传感器数据高频更新导致卡顿使用Web Worker处理数据流const dataWorker = new Worker('data-processor.js')
移动端操作误触增大按钮尺寸,添加确认机制if (isMobile) { btn.css('min-width', '60px') }
离线数据同步冲突实现基于时间戳的冲突解决算法if (serverTime > localTime) { mergeData() }
大屏展示多弹窗排版实现智能弹窗布局算法arrangePopupsInGrid(popupList, screenWidth, screenHeight)

结语与未来展望

layer弹窗系统为林业资源监控提供了高效、灵活的数据展示方案,通过本文介绍的技术方案,我们实现了:

  1. 一体化监控:将地图、传感器、预警等分散数据聚合展示
  2. 高效操作流程:减少70%的页面切换操作,提升响应速度
  3. 离线数据采集:适应林业野外作业网络不稳定环境
  4. 移动端适配:满足巡检人员野外单手操作需求

未来,随着物联网技术在林业管理中的深入应用,我们可以进一步扩展该系统:

  • AR增强现实:通过AR眼镜在弹窗中叠加森林资源数据
  • AI辅助决策:基于历史数据预测森林生长趋势和火灾风险
  • 无人机巡检集成:弹窗直接控制无人机航线并查看实时回传画面
  • 区块链存证:重要林业数据上链,确保不可篡改

完整代码已开源,仓库地址:https://gitcode.com/gh_mirrors/lay/layer

如果你觉得本文对你的林业管理系统开发有帮助,请点赞收藏并关注我们的技术专栏,下期将带来《基于WebGL的森林三维可视化技术》。

【免费下载链接】layer 【免费下载链接】layer 项目地址: https://gitcode.com/gh_mirrors/lay/layer

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值