Sfoglia il codice sorgente

新增客流统计系统接口查询

wangshuangpan 2 mesi fa
parent
commit
298f9951e6

+ 66 - 0
pm-admin/src/main/java/com/pm/web/controller/kltj/PeopleFlowController.java

@@ -0,0 +1,66 @@
+package com.pm.web.controller.kltj;
+
+import com.pm.common.core.controller.BaseController;
+import com.pm.common.core.domain.AjaxResult;
+import com.pm.common.core.page.TableDataInfo;
+import com.pm.kltj.service.IPeopleFlowService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import com.pm.common.utils.poi.ExcelUtilPageData;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/subsystem/peopleflow")
+public class PeopleFlowController extends BaseController {
+
+    @Autowired
+    private IPeopleFlowService peopleFlowService;
+
+    /**
+     * 查询客流设备列表
+     */
+    @GetMapping("/devices")
+    public AjaxResult listDevices() {
+        List<Map<String, Object>> list = peopleFlowService.selectDeviceList();
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 查询客流统计数据列表
+     */
+    @GetMapping("/list")
+    public TableDataInfo list() {
+        startPage();
+        List<Map<String, Object>> list = peopleFlowService.selectFlowDataList(getPageData());
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出客流统计数据
+     */
+    @PostMapping("/export")
+    public void export(HttpServletResponse response) {
+        List<Map<String, Object>> list = peopleFlowService.selectFlowDataList(getPageData());
+        String filename = "客流统计数据";
+        String[] titles = {
+            "时间,stat_time",
+            "设备名称,device_name",
+            "区域名称,area_name",
+            "楼栋名称,building_name",
+            "楼层名称,floor_name",
+            "进入人数,enter_count",
+            "离开人数,exit_count",
+            "当前人数,current_count",
+            "峰值人数,peak_count",
+            "峰值时间,peak_time",
+            "占用率(%),occupancy_rate",
+            "平均停留时间(分钟),avg_stay_time"
+        };
+        ExcelUtilPageData.exportXLSX(response, null, null, filename, titles, list);
+    }
+}

+ 11 - 0
pm-system/src/main/java/com/pm/kltj/mapper/PeopleFlowMapper.java

@@ -0,0 +1,11 @@
+package com.pm.kltj.mapper;
+
+import com.pm.common.config.PageData;
+
+import java.util.List;
+import java.util.Map;
+
+public interface PeopleFlowMapper {
+    List<Map<String, Object>> selectDeviceList();
+    List<Map<String, Object>> selectFlowDataList(PageData pd);
+}

+ 18 - 0
pm-system/src/main/java/com/pm/kltj/service/IPeopleFlowService.java

@@ -0,0 +1,18 @@
+package com.pm.kltj.service;
+
+import com.pm.common.config.PageData;
+
+import java.util.List;
+import java.util.Map;
+
+public interface IPeopleFlowService {
+    /**
+     * 查询客流设备列表
+     */
+    List<Map<String, Object>> selectDeviceList();
+
+    /**
+     * 查询客流统计数据列表
+     */
+    List<Map<String, Object>> selectFlowDataList(PageData pd);
+}

+ 27 - 0
pm-system/src/main/java/com/pm/kltj/service/impl/PeopleFlowServiceImpl.java

@@ -0,0 +1,27 @@
+package com.pm.kltj.service.impl;
+
+import com.pm.kltj.mapper.PeopleFlowMapper;
+import com.pm.kltj.service.IPeopleFlowService;
+import com.pm.common.config.PageData;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+import java.util.Map;
+
+@Service
+public class PeopleFlowServiceImpl implements IPeopleFlowService {
+
+    @Autowired
+    private PeopleFlowMapper peopleFlowMapper;
+
+    @Override
+    public List<Map<String, Object>> selectDeviceList() {
+        return peopleFlowMapper.selectDeviceList();
+    }
+
+    @Override
+    public List<Map<String, Object>> selectFlowDataList(PageData pd) {
+        return peopleFlowMapper.selectFlowDataList(pd);
+    }
+}

+ 61 - 0
pm-system/src/main/resources/mapper/subsystem/PeopleFlowMapper.xml

@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.pm.kltj.mapper.PeopleFlowMapper">
+
+    <!-- 查询客流设备列表 -->
+    <select id="selectDeviceList" resultType="java.util.HashMap">
+        SELECT
+            device_code,
+            device_name,
+            area_id,
+            area_name,
+            building_id,
+            building_name,
+            floor_id,
+            floor_name
+        FROM people_flow_device
+        WHERE is_online = 1
+    </select>
+
+    <!-- 查询客流统计数据 -->
+    <select id="selectFlowDataList" parameterType="pd" resultType="java.util.HashMap">
+        SELECT
+        d.device_name,
+        d.area_name,
+        d.building_name,
+        d.floor_name,
+        f.stat_time,
+        f.enter_count,
+        f.exit_count,
+        f.current_count,
+        f.peak_count,
+        f.peak_time,
+        f.occupancy_rate,
+        f.avg_stay_time
+        FROM people_flow_data f
+        LEFT JOIN people_flow_device d ON f.device_code = d.device_code
+        <where>
+            <if test="deviceCode != null and deviceCode != ''">
+                AND f.device_code = #{deviceCode}
+            </if>
+            <if test="areaId != null and areaId != ''">
+                AND d.area_id = #{areaId}
+            </if>
+            <if test="buildingId != null and buildingId != ''">
+                AND d.building_id = #{buildingId}
+            </if>
+            <if test="statType != null and statType != ''">
+                AND f.stat_type = #{statType}
+            </if>
+            <if test="beginTime != null and beginTime != ''">
+                AND f.stat_time &gt;= #{beginTime}
+            </if>
+            <if test="endTime != null and endTime != ''">
+                AND f.stat_time &lt;= #{endTime}
+            </if>
+        </where>
+        ORDER BY f.stat_time DESC
+    </select>
+</mapper>

+ 28 - 0
pm_ui/src/api/subsystem/peopleflow.js

@@ -0,0 +1,28 @@
+import request from '@/utils/request'
+
+// 查询客流设备列表
+export function listFlowDevices() {
+    return request({
+        url: '/subsystem/peopleflow/devices',
+        method: 'get'
+    })
+}
+
+// 查询客流统计数据列表
+export function listFlowData(query) {
+    return request({
+        url: '/subsystem/peopleflow/list',
+        method: 'get',
+        params: query
+    })
+}
+
+// 导出客流统计数据
+export function exportFlowData(query) {
+    return request({
+        url: '/subsystem/peopleflow/export',
+        method: 'post',
+        data: query,
+        responseType: 'blob'
+    })
+}

+ 406 - 0
pm_ui/src/views/device/active/index.vue

@@ -0,0 +1,406 @@
+<template>
+  <div class="device-monitor-container">
+ <!-- 实时数据页面 -->
+    <div v-show="activeTab === 'realtime'" class="tab-content">
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>实时数据</span>
+          <el-button style="float: right; padding: 3px 0" type="text" @click="refreshRealtimeData">
+            <i class="el-icon-refresh"></i> 刷新 ({{ realtimeCountdown }}s)
+          </el-button>
+        </div>
+
+        <!-- 数据分组展示 -->
+        <div class="realtime-groups">
+          <el-row :gutter="20">
+            <el-col :span="8" v-for="group in realtimeGroups" :key="group.groupName">
+              <el-card class="group-card">
+                <div slot="header">
+                  <span>{{ group.groupName }}</span>
+                  <el-badge :value="group.points.length" class="point-count"></el-badge>
+                </div>
+                <div class="point-list">
+                  <div v-for="point in group.points" :key="point.pointId" class="point-item">
+                    <div class="point-name">{{ point.pointName }}</div>
+                    <div class="point-value">
+                      <span :class="['value', getPointStatusClass(point.status)]">
+                        {{ point.value }} {{ point.unit }}
+                      </span>
+                      <el-tag :type="getPointStatusTag(point.status)" size="mini">
+                        {{ getPointStatusText(point.status) }}
+                      </el-tag>
+                    </div>
+                    <div class="point-time">{{ point.updateTime }}</div>
+                  </div>
+                </div>
+              </el-card>
+            </el-col>
+          </el-row>
+        </div>
+      </el-card>
+    </div>
+      </div>
+</template>
+<script>
+import * as echarts from 'echarts'
+export default {
+  name: 'DeviceMonitor',
+  data() {
+    return {
+      // 设备选择
+      deviceForm: {
+        deviceId: '',
+        deviceType: ''
+      },
+      deviceList: [],
+
+      // 当前活动页签
+      activeTab: 'control',
+
+
+      // 实时数据相关
+      realtimeGroups: [],
+      realtimeCountdown: 30,
+      realtimeTimer: null,
+
+      // 设备详情相关
+      deviceDetail: {},
+      deviceDocuments: [],
+      showUploadDialog: false,
+      fileList: [],
+      uploadUrl: '/api/device/upload',
+      uploadHeaders: {
+        'Authorization': 'Bearer ' + this.getToken()
+      }
+    }
+  },
+
+  mounted() {
+    this.loadDeviceList()
+    this.initRealtimeTimer()
+  },
+  methods: {
+    // 获取设备列表
+    async loadDeviceList() {
+      try {
+        const response = await this.$deviceApi.getDeviceList()
+        this.deviceList = response.data || []
+      } catch (error) {
+        this.$message.error('获取设备列表失败')
+      }
+    },
+
+    // 页签切换
+    handleTabClick(tab) {
+      this.loadCurrentTabData()
+    },
+
+    // 加载当前页签数据
+    loadCurrentTabData() {
+      if (!this.deviceForm.deviceId) {
+        this.$message.warning('请先选择设备')
+        return
+      }
+
+      switch (this.activeTab) {
+        case 'control':
+          this.loadControlRecords()
+          break
+        case 'realtime':
+          this.loadRealtimeData()
+          break
+        case 'history':
+          this.loadHistoryData()
+          this.loadAvailablePoints()
+          break
+        case 'events':
+          this.loadEventRecords()
+          break
+        case 'details':
+          this.loadDeviceDetail()
+          this.loadDeviceDocuments()
+          break
+      }
+    },
+
+    // ========== 实时数据相关方法 ==========
+    async loadRealtimeData() {
+      try {
+        const response = await this.$deviceApi.getRealtimeData({
+          deviceId: this.deviceForm.deviceId
+        })
+        this.realtimeGroups = response.data || []
+      } catch (error) {
+        this.$message.error('获取实时数据失败')
+      }
+    },
+
+    refreshRealtimeData() {
+      this.loadRealtimeData()
+      this.realtimeCountdown = 30
+    },
+
+    initRealtimeTimer() {
+      this.realtimeTimer = setInterval(() => {
+        this.realtimeCountdown--
+        if (this.realtimeCountdown <= 0) {
+          if (this.activeTab === 'realtime' && this.deviceForm.deviceId) {
+            this.refreshRealtimeData()
+          } else {
+            this.realtimeCountdown = 30
+          }
+        }
+      }, 1000)
+    },
+
+    getPointStatusClass(status) {
+      return status === 'normal' ? 'normal' : 'abnormal'
+    },
+
+    getPointStatusTag(status) {
+      return status === 'normal' ? 'success' : 'danger'
+    },
+
+    getPointStatusText(status) {
+      return status === 'normal' ? '正常' : '异常'
+    },
+
+
+    // ========== 设备详情相关方法 ==========
+    async loadDeviceDetail() {
+      try {
+        const response = await this.$deviceApi.getDeviceDetail({
+          deviceId: this.deviceForm.deviceId
+        })
+        this.deviceDetail = response.data || {}
+      } catch (error) {
+        this.$message.error('获取设备详情失败')
+      }
+    },
+
+    async loadDeviceDocuments() {
+      try {
+        const response = await this.$deviceApi.getDeviceDocuments({
+          deviceId: this.deviceForm.deviceId
+        })
+        this.deviceDocuments = response.data || []
+      } catch (error) {
+        this.$message.error('获取设备文档失败')
+      }
+    },
+
+    formatFileSize(bytes) {
+      if (bytes === 0) return '0 Bytes'
+      const k = 1024
+      const sizes = ['Bytes', 'KB', 'MB', 'GB']
+      const i = Math.floor(Math.log(bytes) / Math.log(k))
+      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
+    },
+
+    beforeUpload(file) {
+      const isLt10M = file.size / 1024 / 1024 < 10
+      if (!isLt10M) {
+        this.$message.error('上传文件大小不能超过 10MB!')
+      }
+      return isLt10M
+    },
+
+    handleUploadSuccess(response, file) {
+      this.$message.success('上传成功')
+      this.loadDeviceDocuments()
+    },
+
+    handleUploadError(error, file) {
+      this.$message.error('上传失败')
+    },
+
+    submitUpload() {
+      this.$refs.upload.submit()
+      this.showUploadDialog = false
+    },
+
+    async downloadFile(file) {
+      try {
+        const response = await this.$deviceApi.downloadFile({
+          fileId: file.id
+        })
+
+        const url = window.URL.createObjectURL(new Blob([response.data]))
+        const link = document.createElement('a')
+        link.href = url
+        link.setAttribute('download', file.fileName)
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+        window.URL.revokeObjectURL(url)
+      } catch (error) {
+        this.$message.error('下载失败')
+      }
+    },
+
+    deleteFile(file) {
+      this.$confirm('确认删除该文件?', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }).then(async () => {
+        try {
+          await this.$deviceApi.deleteDocument({
+            fileId: file.id
+          })
+          this.$message.success('删除成功')
+          this.loadDeviceDocuments()
+        } catch (error) {
+          this.$message.error('删除失败')
+        }
+      }).catch(() => {
+        this.$message.info('已取消删除')
+      })
+    },
+
+    // 获取认证token
+    getToken() {
+      return localStorage.getItem('token') || sessionStorage.getItem('token') || ''
+    }
+  }
+}
+</script>
+
+<style scoped>
+.device-monitor-container {
+  padding: 20px;
+}
+
+.device-selector {
+  margin-bottom: 20px;
+}
+
+.function-tabs {
+  margin-bottom: 20px;
+}
+
+.tab-content {
+  min-height: 500px;
+}
+
+/* 实时数据样式 */
+.realtime-groups {
+  margin-top: 20px;
+}
+
+.group-card {
+  margin-bottom: 20px;
+}
+
+.point-count {
+  margin-left: 10px;
+}
+
+.point-list {
+  max-height: 300px;
+  overflow-y: auto;
+}
+
+.point-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 0;
+  border-bottom: 1px solid #f0f0f0;
+}
+
+.point-item:last-child {
+  border-bottom: none;
+}
+
+.point-name {
+  font-weight: bold;
+  flex: 1;
+}
+
+.point-value {
+  flex: 2;
+  text-align: center;
+}
+
+.point-value .value {
+  font-size: 16px;
+  margin-right: 10px;
+}
+
+.point-value .value.normal {
+  color: #67C23A;
+}
+
+.point-value .value.abnormal {
+  color: #F56C6C;
+}
+
+.point-time {
+  flex: 1;
+  text-align: right;
+  font-size: 12px;
+  color: #999;
+}
+
+/* 图表容器 */
+.chart-container {
+  margin: 20px 0;
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+}
+
+/* 时间轴样式 */
+.timeline-container {
+  max-height: 600px;
+  overflow-y: auto;
+  padding: 20px 0;
+}
+
+.event-header {
+  display: flex;
+  align-items: center;
+  margin-bottom: 10px;
+}
+
+.event-title {
+  font-weight: bold;
+  margin-right: 10px;
+  flex: 1;
+}
+
+.event-content p {
+  margin: 5px 0;
+}
+
+/* 设备信息样式 */
+.device-info {
+  padding: 20px 0;
+}
+
+/* 上传对话框样式 */
+.dialog-footer {
+  text-align: right;
+}
+
+/* 响应式布局 */
+@media (max-width: 768px) {
+  .device-monitor-container {
+    padding: 10px;
+  }
+
+  .realtime-groups .el-col {
+    margin-bottom: 20px;
+  }
+
+  .point-item {
+    flex-direction: column;
+    align-items: flex-start;
+  }
+
+  .point-value {
+    text-align: left;
+    margin: 5px 0;
+  }
+}
+</style>

+ 51 - 48
pm_ui/src/views/device/control/index.vue

@@ -17,15 +17,19 @@
             @keyup.enter="handleQuery"
         />
       </el-form-item>
-      <el-form-item label="控制结果(0-失败,1-成功)" prop="controlResult">
-        <el-input
-            v-model="queryParams.controlResult"
-            placeholder="请输入控制结果(0-失败,1-成功)"
-            clearable
-            @keyup.enter="handleQuery"
-        />
+      <el-form-item label="控制结果" prop="controlResult">
+        <el-select v-model="queryParams.controlResult" placeholder="请选择结果" style="width: 140px"
+                   @keyup.enter="handleQuery">
+          <el-option
+              v-for="dict in control_result"
+              :key="dict.value"
+              :label="dict.label"
+              :value="dict.value"
+          />
+        </el-select>
       </el-form-item>
-      <el-form-item label="操作人姓名" prop="operatorName">
+
+      <el-form-item label="操作人" prop="operatorName">
         <el-input
             v-model="queryParams.operatorName"
             placeholder="请输入操作人姓名"
@@ -33,6 +37,20 @@
             @keyup.enter="handleQuery"
         />
       </el-form-item>
+      <el-form-item label="操作类型" prop="controlType">
+          <el-select v-model="queryParams.controlType" placeholder="请选择操作类型" clearable style="width: 240px"
+                     @keyup.enter="handleQuery">
+            <el-option
+                v-for="dict in root_control_type"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+            />
+          </el-select>
+        </el-form-item>
+      <el-form-item label="操作时间" style="width: 308px">
+        <el-date-picker v-model="queryParams.operateTime" value-format="YYYY-MM-DD" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期"></el-date-picker>
+      </el-form-item>
       <el-form-item>
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
         <el-button icon="Refresh" @click="resetQuery">重置</el-button>
@@ -40,35 +58,7 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-            type="primary"
-            plain
-            icon="Plus"
-            @click="handleAdd"
-            v-hasPermi="['device:control:add']"
-        >新增</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-            type="success"
-            plain
-            icon="Edit"
-            :disabled="single"
-            @click="handleUpdate"
-            v-hasPermi="['device:control:edit']"
-        >修改</el-button>
-      </el-col>
-      <el-col :span="1.5">
-        <el-button
-            type="danger"
-            plain
-            icon="Delete"
-            :disabled="multiple"
-            @click="handleDelete"
-            v-hasPermi="['device:control:remove']"
-        >删除</el-button>
-      </el-col>
+
       <el-col :span="1.5">
         <el-button
             type="warning"
@@ -86,10 +76,19 @@
       <el-table-column label="主键ID1" align="center" prop="id" />
       <el-table-column label="设备ID" align="center" prop="deviceId" />
       <el-table-column label="点位ID" align="center" prop="pointId" />
-      <el-table-column label="控制类型" align="center" prop="controlType" />
+      <el-table-column label="控制类型" align="center" prop="controlType">
+        <template #default="scope">
+          <dict-tag :options="root_control_type" :value="scope.row.controlType"/>
+        </template>
+
+      </el-table-column>
       <el-table-column label="原值" align="center" prop="oldValue" />
       <el-table-column label="新值" align="center" prop="newValue" />
-      <el-table-column label="控制结果(0-失败,1-成功)" align="center" prop="controlResult" />
+      <el-table-column label="控制结果" align="center" prop="controlResult">
+        <template #default="scope">
+          <dict-tag :options="control_result" :value="scope.row.controlResult"/>
+        </template>
+      </el-table-column>
       <el-table-column label="操作人ID" align="center" prop="operatorId" />
       <el-table-column label="操作人姓名" align="center" prop="operatorName" />
       <el-table-column label="操作时间" align="center" prop="operateTime" width="180">
@@ -100,8 +99,7 @@
       <el-table-column label="备注" align="center" prop="remark" />
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template #default="scope">
-          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['device:control:edit']">修改</el-button>
-          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['device:control:remove']">删除</el-button>
+          <el-button link type="primary" icon="View" @click="handleView(scope.row, scope.index)" v-hasPermi="['device:control:query']">详情</el-button>
         </template>
       </el-table-column>
     </el-table>
@@ -115,8 +113,8 @@
     />
 
     <!-- 添加或修改设备控制记录对话框 -->
-    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
-      <el-form ref="controlRef" :model="form" :rules="rules" label-width="80px">
+    <el-dialog :title="title" v-model="open" width="800px" append-to-body>
+      <el-form ref="controlRef" :model="form" :rules="rules" label-width="100px" disabled>
         <el-form-item label="设备ID" prop="deviceId">
           <el-input v-model="form.deviceId" placeholder="请输入设备ID" />
         </el-form-item>
@@ -129,8 +127,8 @@
         <el-form-item label="新值" prop="newValue">
           <el-input v-model="form.newValue" placeholder="请输入新值" />
         </el-form-item>
-        <el-form-item label="控制结果(0-失败,1-成功)" prop="controlResult">
-          <el-input v-model="form.controlResult" placeholder="请输入控制结果(0-失败,1-成功)" />
+        <el-form-item label="控制结果" prop="controlResult">
+          <el-input v-model="form.controlResult" placeholder="请输入控制结果" />
         </el-form-item>
         <el-form-item label="操作人ID" prop="operatorId">
           <el-input v-model="form.operatorId" placeholder="请输入操作人ID" />
@@ -152,7 +150,6 @@
       </el-form>
       <template #footer>
         <div class="dialog-footer">
-          <el-button type="primary" @click="submitForm">确 定</el-button>
           <el-button @click="cancel">取 消</el-button>
         </div>
       </template>
@@ -164,6 +161,8 @@
 import { listControl, getControl, delControl, addControl, updateControl } from "@/api/device/control";
 
 const { proxy } = getCurrentInstance();
+const {root_control_type,control_result} = proxy.useDict('root_control_type','control_result');
+
 
 const controlList = ref([]);
 const open = ref(false);
@@ -281,10 +280,14 @@ function handleUpdate(row) {
   getControl(_id).then(response => {
     form.value = response.data;
     open.value = true;
-    title.value = "修改设备控制记录";
+    title.value = "设备控制记录详情";
   });
 }
-
+/** 详细按钮操作 */
+function handleView(row) {
+  open.value = true;
+  form.value = row;
+}
 /** 提交按钮 */
 function submitForm() {
   proxy.$refs["controlRef"].validate(valid => {

+ 322 - 0
pm_ui/src/views/subsystem/index.vue

@@ -0,0 +1,322 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px">
+      <el-form-item label="设备" prop="deviceCode">
+        <el-select
+            v-model="queryParams.deviceCode"
+            placeholder="请选择设备"
+            clearable
+            filterable
+            style="width: 200px"
+        >
+          <el-option
+              v-for="device in deviceOptions"
+              :key="device.device_code"
+              :label="device.device_name"
+              :value="device.device_code"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="区域" prop="areaId">
+        <el-select
+            v-model="queryParams.areaId"
+            placeholder="请选择区域"
+            clearable
+            style="width: 150px"
+        >
+          <el-option
+              v-for="area in areaOptions"
+              :key="area.value"
+              :label="area.label"
+              :value="area.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="楼栋" prop="buildingId">
+        <el-select
+            v-model="queryParams.buildingId"
+            placeholder="请选择楼栋"
+            clearable
+            style="width: 150px"
+        >
+          <el-option
+              v-for="building in buildingOptions"
+              :key="building.value"
+              :label="building.label"
+              :value="building.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="timeRange">
+        <el-date-picker
+            v-model="queryParams.timeRange"
+            type="datetimerange"
+            range-separator="至"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
+            value-format="YYYY-MM-DD HH:mm:ss"
+            style="width: 350px"
+        />
+      </el-form-item>
+      <el-form-item label="统计类型" prop="statType">
+        <el-select v-model="queryParams.statType" placeholder="请选择" style="width: 120px">
+          <el-option label="实时" value="realtime" />
+          <el-option label="小时" value="hourly" />
+          <el-option label="日" value="daily" />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
+        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 数据表格 -->
+    <el-table v-loading="loading" :data="dataList" stripe border>
+      <el-table-column label="时间" align="center" prop="stat_time">
+        <template #default="scope">
+          <span>{{ parseTime(scope.row.stat_time, '{y}-{m}-{d} {h}:{i}') }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="设备名称" align="center" prop="device_name" />
+      <el-table-column label="区域" align="center" prop="area_name" />
+      <el-table-column label="楼栋" align="center" prop="building_name" />
+      <el-table-column label="楼层" align="center" prop="floor_name" />
+      <el-table-column label="进入人数" align="center" prop="enter_count" />
+      <el-table-column label="离开人数" align="center" prop="exit_count" />
+      <el-table-column label="当前人数" align="center" prop="current_count" />
+      <el-table-column label="峰值人数" align="center" prop="peak_count" />
+      <el-table-column label="占用率" align="center" prop="occupancy_rate">
+        <template #default="scope">
+          <span>{{ scope.row.occupancy_rate }}%</span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+        v-show="total > 0"
+        :total="total"
+        v-model:page="queryParams.pageNum"
+        v-model:limit="queryParams.pageSize"
+        @pagination="getList"
+    />
+
+    <!-- ECharts图表 -->
+    <div class="chart-container">
+      <div ref="flowChart" style="height:500px; margin-top:20px;"></div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted, nextTick } from 'vue'
+import * as echarts from 'echarts'
+import { listFlowData, exportFlowData, listFlowDevices } from '@/api/subsystem/peopleflow'
+
+const { proxy } = getCurrentInstance()
+const flowChart = ref(null)
+let chartInstance = null
+
+const loading = ref(false)
+const showSearch = ref(true)
+const total = ref(0)
+const dataList = ref([])
+const deviceOptions = ref([])
+const areaOptions = ref([
+  { value: 'A', label: 'A区' },
+  { value: 'B', label: 'B区' },
+  { value: 'C', label: 'C区' }
+])
+const buildingOptions = ref([
+  { value: 'B1', label: '研发楼' },
+  { value: 'B2', label: '办公楼' },
+  { value: 'B3', label: '实验楼' }
+])
+
+const queryParams = reactive({
+  pageNum: 1,
+  pageSize: 10,
+  deviceCode: null,
+  areaId: null,
+  buildingId: null,
+  timeRange: [],
+  statType: 'realtime'
+})
+
+/** 查询设备列表 */
+function getDeviceOptions() {
+  listFlowDevices().then(response => {
+    deviceOptions.value = response.data
+  })
+}
+
+/** 查询客流数据列表 */
+function getList() {
+  loading.value = true
+  // 处理时间范围参数
+  const params = {
+    ...queryParams,
+    pageNum: queryParams.pageNum,
+    pageSize: queryParams.pageSize
+  }
+
+  if (queryParams.timeRange && queryParams.timeRange.length === 2) {
+    params.beginTime = queryParams.timeRange[0]
+    params.endTime = queryParams.timeRange[1]
+  }
+
+  listFlowData(params).then(response => {
+    dataList.value = response.rows
+    total.value = response.total
+    loading.value = false
+
+    // 渲染图表
+    nextTick(() => {
+      renderChart()
+    })
+  })
+}
+
+/** 渲染图表 */
+function renderChart() {
+  if (!flowChart.value) return
+
+  if (!chartInstance) {
+    chartInstance = echarts.init(flowChart.value)
+  }
+
+  // 准备图表数据
+  const timeData = []
+  const enterData = []
+  const exitData = []
+  const currentData = []
+
+  dataList.value.forEach(item => {
+    timeData.push(item.stat_time)
+    enterData.push(item.enter_count)
+    exitData.push(item.exit_count)
+    currentData.push(item.current_count)
+  })
+
+  const option = {
+    title: {
+      text: '客流统计趋势',
+      left: 'center'
+    },
+    tooltip: {
+      trigger: 'axis'
+    },
+    legend: {
+      data: ['进入人数', '离开人数', '当前人数'],
+      bottom: 10
+    },
+    grid: {
+      left: '3%',
+      right: '4%',
+      bottom: '12%',
+      containLabel: true
+    },
+    xAxis: {
+      type: 'category',
+      boundaryGap: false,
+      data: timeData
+    },
+    yAxis: {
+      type: 'value',
+      name: '人数'
+    },
+    series: [
+      {
+        name: '进入人数',
+        type: 'line',
+        data: enterData,
+        smooth: true,
+        lineStyle: {
+          color: '#5470C6'
+        }
+      },
+      {
+        name: '离开人数',
+        type: 'line',
+        data: exitData,
+        smooth: true,
+        lineStyle: {
+          color: '#91CC75'
+        }
+      },
+      {
+        name: '当前人数',
+        type: 'line',
+        data: currentData,
+        smooth: true,
+        lineStyle: {
+          color: '#EE6666'
+        }
+      }
+    ]
+  }
+
+  chartInstance.setOption(option)
+  window.addEventListener('resize', () => chartInstance.resize())
+}
+
+/** 搜索按钮操作 */
+function handleQuery() {
+  queryParams.pageNum = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+function resetQuery() {
+  proxy.resetForm('queryRef')
+  queryParams.pageNum = 1
+  handleQuery()
+}
+
+/** 导出按钮操作 */
+function handleExport() {
+  const params = {
+    ...queryParams,
+    pageNum: undefined,
+    pageSize: undefined
+  }
+
+  if (queryParams.timeRange && queryParams.timeRange.length === 2) {
+    params.beginTime = queryParams.timeRange[0]
+    params.endTime = queryParams.timeRange[1]
+  }
+
+  proxy.$modal.confirm('确认导出客流统计数据吗?').then(() => {
+    exportFlowData(params).then(response => {
+      proxy.download(response.msg)
+    })
+  })
+}
+
+onMounted(() => {
+  // 设置默认时间范围为当天
+  const now = new Date()
+  const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
+  const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
+
+  queryParams.timeRange = [
+    // parseTime(start, '{y}-{m}-{d} 00:00:00'),
+    // parseTime(end, '{y}-{m}-{d} 23:59:59')
+  ]
+
+  getDeviceOptions()
+  getList()
+})
+</script>
+
+<style scoped>
+.chart-container {
+  position: relative;
+  width: 100%;
+  margin-top: 20px;
+  background: #fff;
+  padding: 20px;
+  border-radius: 4px;
+  box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
+}
+</style>