Просмотр исходного кода

feat(stationProfile): add station profile page and related features

1. 新增站画像页面路由配置
2. 更新测试环境API接口地址
3. 新增通用SectionTitle标题组件
4. 新增站画像页面全套业务组件:
   - 值班信息组件
   - 出勤状态组件
   - 团队成员表格组件
   - 不安全事件类型/物品/位置分布图表组件
   - 区域/物品客流分布图表组件
   - 安保测试图表组件
   - 人员基础/岗位资质分布组件
   - 查获信息/每日查获趋势图表组件
5. 新增站画像业务API接口
huoyi недель назад: 3
Родитель
Сommit
e62916cdbe

+ 101 - 0
src/api/portraitManagement/portraitManagement.js

@@ -0,0 +1,101 @@
1
+import request from '@/utils/request'
2
+
3
+// 安保测试物品分类
4
+export function securityTestItemClassification(data) {
5
+    return request({ url: '/ledger/securityTest/securityTestItemClassification', method: 'post', data })
6
+}
7
+
8
+// 安保测试通过情况
9
+export function securityTestPassingStatus(data) {
10
+    return request({ url: '/ledger/securityTest/securityTestPassingStatus', method: 'post', data })
11
+}
12
+
13
+// 安保测试区域情况
14
+export function securityTestRegion(data) {
15
+    return request({ url: '/ledger/securityTest/securityTestRegion', method: 'post', data })
16
+}
17
+
18
+// 实时质控拦截物品分布
19
+export function realtimeInterceptionItem(data) {
20
+    return request({ url: '/ledger/realtimeInterception/realtimeInterceptionItem', method: 'post', data })
21
+}
22
+
23
+// 各岗位监察问题分布
24
+export function supervisionProblemPosition(data) {
25
+    return request({ url: '/ledger/supervisionProblem/supervisionProblemPosition', method: 'post', data })
26
+}
27
+
28
+// 获取部门内所有成员列表
29
+export function getDeptMembers(data) {
30
+    return request({ url: '/score/dept-portrait/members', method: 'post', data })
31
+}
32
+
33
+// 获取部门成员基本情况分布
34
+export function getDeptMemberDistribution(data) {
35
+    return request({ url: '/score/dept-portrait/distribution', method: 'post', data })
36
+}
37
+
38
+// 获取部门成员职位情况分布
39
+export function getDeptPositionDistribution(data) {
40
+    return request({ url: '/score/dept-portrait/position-distribution', method: 'post', data })
41
+}
42
+
43
+// 维度得分一览
44
+export function getDimensionScoreOverview(data) {
45
+    return request({ url: '/score/dept-portrait/group-portrait', method: 'post', data })
46
+}
47
+
48
+// 1.查询每日通道过检率
49
+export function countLanePeakThroughput(data) {
50
+    return request({ url: '/ledger/operationLanePeakThroughput/countLanePeakThroughput', method: 'post', data })
51
+}
52
+// 2.查询当天开航每小时区域过检人数
53
+export function countStationHourlyThroughput(data) {
54
+    return request({ url: '/ledger/operationStationHourlyThroughput/countStationHourlyThroughput', method: 'get', data })
55
+}
56
+// 3.查获信息展示
57
+export function countSeizureInfoItem(data) {
58
+    return request({ url: '/ledger/seizureStats/countSeizureInfoItem', method: 'post', data })
59
+}
60
+// 4.查获物品分布 
61
+export function countSeizeSubjectCategoryQuantity(data) {
62
+    return request({ url: '/ledger/seizureStats/countSeizeSubjectItemQuantity', method: 'post', data })
63
+}
64
+// 5.近30天查获数量(总表)
65
+export function countSeizureTotalQuantity(data) {
66
+    return request({ url: '/ledger/seizureStats/countSeizureTotalQuantity', method: 'post', data })
67
+}
68
+// 6.近30天查获数量(员工/小组/班组/部门)
69
+export function countSeizureSingleQuantity(data) {
70
+    return request({ url: '/ledger/seizureStats/countSeizureSingleQuantity', method: 'post', data })
71
+}
72
+// 7.查获工作区域分布
73
+export function countSeizeAreaQuantity(data) {
74
+    return request({ url: '/ledger/seizureStats/countSeizeAreaQuantity', method: 'post', data })
75
+}
76
+// 8.不安全事件物品分布
77
+export function countSeizureStatsItem(data) {
78
+    return request({ url: '/ledger/unsafeEvent/countSeizureStatsItem', method: 'post', data })
79
+}
80
+// 9.不安全事件类型分布
81
+export function countSeizureStatsType(data) {
82
+    return request({ url: '/ledger/unsafeEvent/countSeizureStatsType', method: 'post', data })
83
+}
84
+// 10.不安全事件岗位分布
85
+export function countSeizureStatsPost(data) {
86
+    return request({ url: '/ledger/unsafeEvent/countSeizureStatsPost', method: 'post', data })
87
+}
88
+
89
+//获取站级别下所有部门的团队画像统计
90
+export function countStationTeamStats(data) {
91
+    return request({ url: '/score/dept-portrait/station-team-stats', method: 'post', data })
92
+}
93
+
94
+//获取部门的团队画像统计
95
+export function countDeptTeamStats(data) {
96
+    return request({ url: '/score/dept-portrait/team-stats', method: 'post', data })
97
+}
98
+//标签得分
99
+export function countTagScore(params) {
100
+    return request({ url: '/ledger/scoreEmployeeAdditional/getTotalScoreByEmployee', method: 'get', params: params })
101
+}

+ 55 - 0
src/components/SectionTitle.vue

@@ -0,0 +1,55 @@
1
+<template>
2
+  <div class="section-wrapper">
3
+    <div class="section-header">
4
+      <div class="section-title">{{ title }}</div>
5
+      <div class="section-header-right">
6
+        <slot name="header-right"></slot>
7
+      </div>
8
+    </div>
9
+    <div class="section-body">
10
+      <slot></slot>
11
+    </div>
12
+  </div>
13
+</template>
14
+
15
+<script>
16
+export default {
17
+  name: 'SectionTitle',
18
+  props: {
19
+    title: {
20
+      type: String,
21
+      default: ''
22
+    }
23
+  }
24
+}
25
+</script>
26
+
27
+<style lang="scss" scoped>
28
+.section-wrapper {
29
+  background: rgba(45, 42, 85, 0.9);
30
+  border-radius: 24rpx;
31
+  padding: 32rpx;
32
+  margin-bottom: 32rpx;
33
+  box-shadow: 0 8rpx 30rpx rgba(0, 0, 0, 0.2);
34
+}
35
+
36
+.section-header {
37
+  display: flex;
38
+  justify-content: space-between;
39
+  align-items: center;
40
+  margin-bottom: 24rpx;
41
+}
42
+
43
+.section-title {
44
+  font-size: 28rpx;
45
+  font-weight: 500;
46
+  color: #A78BFA;
47
+}
48
+
49
+
50
+
51
+.section-header-right {
52
+  font-size: 24rpx;
53
+  color: rgba(255, 255, 255, 0.5);
54
+}
55
+</style>

+ 1 - 1
src/config.js

@@ -4,7 +4,7 @@ module.exports = {
4 4
  // baseUrl: process.env.NODE_ENV === 'development' ? 'http://192.168.3.221:82/prod-api' : '/prod-api', //生产
5 5
   //  baseUrl: process.env.NODE_ENV === 'development' ? 'http://192.168.3.221:8080' : '/prod-api',
6 6
 // baseUrl: process.env.NODE_ENV === 'development' ? 'http://guangxi.qinghe.sundot.cn:8088' : 'http://guangxi.qinghe.sundot.cn:8088/prod-api',
7
-    baseUrl: 'http://airport-test.samsundot.com:9024/prod-api',//测试
7
+    baseUrl: 'http://airport-test.samsundot.com:9035/prod-api',//测试
8 8
 // baseUrl:'http://airport.samsundot.com:9021/prod-api',//生产
9 9
   // 应用信息
10 10
   appInfo: {

+ 6 - 0
src/pages.json

@@ -117,6 +117,12 @@
117 117
       }
118 118
     },
119 119
     {
120
+      "path": "pages/stationProfile/index",
121
+      "style": {
122
+        "navigationBarTitleText": "站画像"
123
+      }
124
+    },
125
+    {
120 126
       "path": "pages/attendanceStatistics/index",
121 127
       "style": {
122 128
         "navigationBarTitleText": "考勤统计"

+ 122 - 0
src/pages/stationProfile/components/AreaDistribution.vue

@@ -0,0 +1,122 @@
1
+<template>
2
+  <div class="area-distribution">
3
+    <div class="chart-container" ref="areaChart"></div>
4
+  </div>
5
+</template>
6
+
7
+<script>
8
+import * as echarts from 'echarts'
9
+
10
+export default {
11
+  name: 'AreaDistribution',
12
+  props: {
13
+    chartsData: {
14
+      type: Object,
15
+      default: () => ({
16
+        labels: ['T1-A区', 'T1-B区', 'T2-A区', 'T2-B区', 'T3-A区', 'T3-B区'],
17
+        data: [320, 280, 350, 380, 320, 332]
18
+      })
19
+    }
20
+  },
21
+  data() {
22
+    return {
23
+      chart: null
24
+    }
25
+  },
26
+  mounted() {
27
+    this.$nextTick(() => {
28
+      this.initChart()
29
+    })
30
+  },
31
+  watch: {
32
+    chartsData: {
33
+      deep: true,
34
+      handler() {
35
+        this.$nextTick(() => {
36
+          if (this.chart) this.chart.dispose()
37
+          this.initChart()
38
+        })
39
+      }
40
+    }
41
+  },
42
+  methods: {
43
+    initChart() {
44
+      if (!this.$refs.areaChart) return
45
+      this.chart = echarts.init(this.$refs.areaChart)
46
+      const option = {
47
+        responsive: true,
48
+        maintainAspectRatio: false,
49
+        grid: {
50
+          left: '15%',
51
+          right: '5%',
52
+          bottom: '15%',
53
+          top: '10%'
54
+        },
55
+        xAxis: {
56
+          type: 'category',
57
+          data: this.chartsData.labels,
58
+          axisLine: {
59
+            lineStyle: {
60
+              color: 'rgba(255, 255, 255, 0.1)'
61
+            }
62
+          },
63
+          axisLabel: {
64
+            color: 'rgba(255, 255, 255, 0.5)',
65
+            fontSize: 10,
66
+            // rotate: 30
67
+          }
68
+        },
69
+        yAxis: {
70
+          type: 'value',
71
+          axisLine: {
72
+            show: false
73
+          },
74
+          axisTick: {
75
+            show: false
76
+          },
77
+          splitLine: {
78
+            lineStyle: {
79
+              color: 'rgba(255, 255, 255, 0.05)'
80
+            }
81
+          },
82
+          axisLabel: {
83
+            color: 'rgba(255, 255, 255, 0.5)',
84
+            fontSize: 10
85
+          }
86
+        },
87
+        series: [{
88
+          type: 'bar',
89
+          data: this.chartsData.data,
90
+          itemStyle: {
91
+            color: {
92
+              type: 'linear',
93
+              x: 0,
94
+              y: 0,
95
+              x2: 0,
96
+              y2: 1,
97
+              colorStops: [{
98
+                offset: 0, color: '#A78BFA'
99
+              }, {
100
+                offset: 1, color: '#6366F1'
101
+              }]
102
+            }
103
+          },
104
+          barWidth: '60%'
105
+        }]
106
+      }
107
+      this.chart.setOption(option)
108
+    }
109
+  },
110
+  beforeDestroy() {
111
+    if (this.chart) this.chart.dispose()
112
+  }
113
+}
114
+</script>
115
+
116
+<style lang="scss" scoped>
117
+.area-distribution {
118
+  .chart-container {
119
+    height: 280rpx;
120
+  }
121
+}
122
+</style>

+ 52 - 0
src/pages/stationProfile/components/AttendanceStatus.vue

@@ -0,0 +1,52 @@
1
+<template>
2
+  <div class="attendance-grid">
3
+    <div v-for="(item, index) in attendanceData" :key="index" class="attendance-item">
4
+      <div class="attendance-number">{{ item.number }}</div>
5
+      <div class="attendance-label">{{ item.label }}</div>
6
+    </div>
7
+  </div>
8
+</template>
9
+
10
+<script>
11
+export default {
12
+  name: 'AttendanceStatus',
13
+  props: {
14
+    attendanceData: {
15
+      type: Array,
16
+      default: () => [
17
+        { number: '120', label: '主班在岗 | 旅检一部' },
18
+        { number: '130', label: '主班在岗 | 旅检二部' },
19
+        { number: '150', label: '主班在岗 | 旅检三部' },
20
+        { number: '80', label: '备勤在岗 | 旅检一部' },
21
+        { number: '70', label: '备勤在岗 | 旅检二部' },
22
+        { number: '65', label: '备勤在岗 | 旅检三部' }
23
+      ]
24
+    }
25
+  }
26
+}
27
+</script>
28
+
29
+<style lang="scss" scoped>
30
+.attendance-grid {
31
+  display: grid;
32
+  grid-template-columns: repeat(3, 1fr);
33
+  gap: 24rpx;
34
+}
35
+
36
+.attendance-item {
37
+  text-align: center;
38
+  
39
+  .attendance-number {
40
+    font-size: 48rpx;
41
+    font-weight: bold;
42
+    color: rgba(255, 255, 255, 0.95);
43
+  }
44
+  
45
+  .attendance-label {
46
+    margin-top: 8rpx;
47
+    font-size: 20rpx;
48
+    color: rgba(255, 255, 255, 0.5);
49
+    line-height: 1.4;
50
+  }
51
+}
52
+</style>

+ 233 - 0
src/pages/stationProfile/components/DailySeizureChart.vue

@@ -0,0 +1,233 @@
1
+<template>
2
+  <div class="daily-seizure-chart">
3
+    <div class="chart-section">
4
+      <div class="sub-title">总表</div>
5
+      <div class="chart-container" ref="totalChart"></div>
6
+    </div>
7
+    <div class="chart-section">
8
+      <div class="sub-title">部门对比</div>
9
+      <div class="chart-container" ref="deptChart"></div>
10
+    </div>
11
+  </div>
12
+</template>
13
+
14
+<script>
15
+import * as echarts from 'echarts'
16
+
17
+export default {
18
+  name: 'DailySeizureChart',
19
+  props: {
20
+    chartsData: {
21
+      type: Object,
22
+      default: () => ({
23
+        total: {
24
+          labels: ['5/20', '5/21', '5/22', '5/23', '5/24', '5/25', '5/26'],
25
+          data: [280, 320, 290, 350, 310, 280, 152]
26
+        },
27
+        dept: {
28
+          labels: ['5/20', '5/21', '5/22', '5/23', '5/24', '5/25', '5/26'],
29
+          deptData: [
30
+            { name: '旅检一部', data: [90, 100, 95, 110, 100, 95, 52] },
31
+            { name: '旅检二部', data: [95, 110, 100, 120, 105, 90, 50] },
32
+            { name: '旅检三部', data: [95, 110, 95, 120, 105, 95, 50] }
33
+          ]
34
+        }
35
+      })
36
+    }
37
+  },
38
+  data() {
39
+    return {
40
+      charts: {}
41
+    }
42
+  },
43
+  mounted() {
44
+    this.$nextTick(() => {
45
+      this.initCharts()
46
+    })
47
+  },
48
+  watch: {
49
+    chartsData: {
50
+      deep: true,
51
+      handler() {
52
+        this.$nextTick(() => {
53
+          this.disposeCharts()
54
+          this.initCharts()
55
+        })
56
+      }
57
+    }
58
+  },
59
+  methods: {
60
+    disposeCharts() {
61
+      Object.values(this.charts).forEach(chart => {
62
+        if (chart) chart.dispose()
63
+      })
64
+      this.charts = {}
65
+    },
66
+    initCharts() {
67
+      this.initTotalChart()
68
+      this.initDeptChart()
69
+    },
70
+    initTotalChart() {
71
+      if (!this.$refs.totalChart) return
72
+      this.charts.total = echarts.init(this.$refs.totalChart)
73
+      const option = {
74
+        responsive: true,
75
+        maintainAspectRatio: false,
76
+        grid: {
77
+          left: '10%',
78
+          right: '5%',
79
+          bottom: '14%',
80
+          top: '10%'
81
+        },
82
+        xAxis: {
83
+          type: 'category',
84
+          data: this.chartsData.total.labels,
85
+          axisLine: {
86
+            lineStyle: {
87
+              color: 'rgba(255, 255, 255, 0.1)'
88
+            }
89
+          },
90
+          axisLabel: {
91
+            color: 'rgba(255, 255, 255, 0.5)',
92
+            fontSize: 10
93
+          }
94
+        },
95
+        yAxis: {
96
+          type: 'value',
97
+          axisLine: {
98
+            show: false
99
+          },
100
+          axisTick: {
101
+            show: false
102
+          },
103
+          splitLine: {
104
+            lineStyle: {
105
+              color: 'rgba(255, 255, 255, 0.05)'
106
+            }
107
+          },
108
+          axisLabel: {
109
+            color: 'rgba(255, 255, 255, 0.5)',
110
+            fontSize: 10
111
+          }
112
+        },
113
+        series: [{
114
+          type: 'line',
115
+          data: this.chartsData.total.data,
116
+          smooth: true,
117
+          itemStyle: {
118
+            color: '#A78BFA'
119
+          },
120
+          areaStyle: {
121
+            color: {
122
+              type: 'linear',
123
+              x: 0,
124
+              y: 0,
125
+              x2: 0,
126
+              y2: 1,
127
+              colorStops: [{
128
+                offset: 0, color: '#A78BFA40'
129
+              }, {
130
+                offset: 1, color: '#A78BFA00'
131
+              }]
132
+            }
133
+          }
134
+        }]
135
+      }
136
+      this.charts.total.setOption(option)
137
+    },
138
+    initDeptChart() {
139
+      if (!this.$refs.deptChart) return
140
+      this.charts.dept = echarts.init(this.$refs.deptChart)
141
+      const colors = ['#60A5FA', '#A78BFA', '#34D399']
142
+      const series = (this.chartsData.dept.deptData || []).map((dept, index) => ({
143
+        name: dept.name,
144
+        type: 'line',
145
+        data: dept.data,
146
+        smooth: true,
147
+        itemStyle: {
148
+          color: colors[index % colors.length]
149
+        }
150
+      }))
151
+      
152
+      const option = {
153
+        responsive: true,
154
+        maintainAspectRatio: false,
155
+        legend: {
156
+          data: (this.chartsData.dept.deptData || []).map(d => d.name),
157
+          textStyle: {
158
+            color: 'rgba(255, 255, 255, 0.5)',
159
+            fontSize: 9
160
+          },
161
+          top: 0
162
+        },
163
+        grid: {
164
+          left: '10%',
165
+          right: '5%',
166
+          bottom: '14%',
167
+          top: '15%'
168
+        },
169
+        xAxis: {
170
+          type: 'category',
171
+          data: this.chartsData.dept.labels,
172
+          axisLine: {
173
+            lineStyle: {
174
+              color: 'rgba(255, 255, 255, 0.1)'
175
+            }
176
+          },
177
+          axisLabel: {
178
+            color: 'rgba(255, 255, 255, 0.5)',
179
+            fontSize: 10
180
+          }
181
+        },
182
+        yAxis: {
183
+          type: 'value',
184
+          axisLine: {
185
+            show: false
186
+          },
187
+          axisTick: {
188
+            show: false
189
+          },
190
+          splitLine: {
191
+            lineStyle: {
192
+              color: 'rgba(255, 255, 255, 0.05)'
193
+            }
194
+          },
195
+          axisLabel: {
196
+            color: 'rgba(255, 255, 255, 0.5)',
197
+            fontSize: 10
198
+          }
199
+        },
200
+        series: series
201
+      }
202
+      this.charts.dept.setOption(option)
203
+    }
204
+  },
205
+  beforeDestroy() {
206
+    Object.values(this.charts).forEach(chart => {
207
+      if (chart) chart.dispose()
208
+    })
209
+  }
210
+}
211
+</script>
212
+
213
+<style lang="scss" scoped>
214
+.daily-seizure-chart {
215
+  .chart-section {
216
+    margin-bottom: 32rpx;
217
+    
218
+    &:last-child {
219
+      margin-bottom: 0;
220
+    }
221
+  }
222
+  
223
+  .sub-title {
224
+    font-size: 24rpx;
225
+    color: rgba(255, 255, 255, 0.5);
226
+    margin-bottom: 16rpx;
227
+  }
228
+  
229
+  .chart-container {
230
+    height: 250rpx;
231
+  }
232
+}
233
+</style>

+ 49 - 0
src/pages/stationProfile/components/DutyInfo.vue

@@ -0,0 +1,49 @@
1
+<template>
2
+  <div class="duty-grid">
3
+    <div v-for="(item, index) in dutyData" :key="index" class="duty-item">
4
+      <div class="duty-label">{{ item.label }}</div>
5
+      <div class="duty-value">{{ item.value }}</div>
6
+    </div>
7
+  </div>
8
+</template>
9
+
10
+<script>
11
+export default {
12
+  name: 'DutyInfo',
13
+  props: {
14
+    dutyData: {
15
+      type: Array,
16
+      default: () => [
17
+        { label: '值班领导', value: '李凯' },
18
+        { label: '安全板块', value: '李凯' },
19
+        { label: '服务板块', value: '李凯' },
20
+        { label: '运行板块', value: '李凯' },
21
+        { label: '备勤值班', value: '李凯' }
22
+      ]
23
+    }
24
+  }
25
+}
26
+</script>
27
+
28
+<style lang="scss" scoped>
29
+.duty-grid {
30
+  display: grid;
31
+  grid-template-columns: repeat(2, 1fr);
32
+  gap: 24rpx;
33
+}
34
+
35
+.duty-item {
36
+  display: flex;
37
+  justify-content: space-between;
38
+  
39
+  .duty-label {
40
+    font-size: 24rpx;
41
+    color: rgba(255, 255, 255, 0.5);
42
+  }
43
+  
44
+  .duty-value {
45
+    font-size: 24rpx;
46
+    color: rgba(255, 255, 255, 0.9);
47
+  }
48
+}
49
+</style>

+ 120 - 0
src/pages/stationProfile/components/ItemDistribution.vue

@@ -0,0 +1,120 @@
1
+<template>
2
+  <div class="item-distribution">
3
+    <div class="chart-container" ref="itemChart"></div>
4
+    <div class="legend-grid">
5
+      <div v-for="(item, index) in chartsData.items" :key="index" class="legend-item">
6
+        <div class="legend-dot" :style="{ backgroundColor: item.color }"></div>
7
+        <div class="legend-text">{{ item.name }}: {{ item.value }}</div>
8
+      </div>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script>
14
+import * as echarts from 'echarts'
15
+
16
+export default {
17
+  name: 'ItemDistribution',
18
+  props: {
19
+    chartsData: {
20
+      type: Object,
21
+      default: () => ({
22
+        items: [
23
+          { name: '刀具', value: 836, color: '#60A5FA' },
24
+          { name: '易燃易爆', value: 718, color: '#34D399' },
25
+          { name: '枪支警械', value: 255, color: '#FBBF24' },
26
+          { name: '管制刀具', value: 1, color: '#EF4444' },
27
+          { name: '其他', value: 172, color: '#9CA3AF' }
28
+        ]
29
+      })
30
+    }
31
+  },
32
+  data() {
33
+    return {
34
+      chart: null
35
+    }
36
+  },
37
+  mounted() {
38
+    this.$nextTick(() => {
39
+      this.initChart()
40
+    })
41
+  },
42
+  watch: {
43
+    chartsData: {
44
+      deep: true,
45
+      handler() {
46
+        this.$nextTick(() => {
47
+          if (this.chart) this.chart.dispose()
48
+          this.initChart()
49
+        })
50
+      }
51
+    }
52
+  },
53
+  methods: {
54
+    initChart() {
55
+      if (!this.$refs.itemChart) return
56
+      this.chart = echarts.init(this.$refs.itemChart)
57
+      const option = {
58
+        responsive: true,
59
+        maintainAspectRatio: false,
60
+        series: [{
61
+          type: 'pie',
62
+          radius: ['50%', '70%'],
63
+          data: this.chartsData.items.map(item => ({
64
+            name: item.name,
65
+            value: item.value,
66
+            color: item.color
67
+          })),
68
+          itemStyle: {
69
+            color: function(params) {
70
+              return params.data.color || params.color
71
+            }
72
+          },
73
+          label: {
74
+            show: true,
75
+            color: 'rgba(255, 255, 255, 0.7)',
76
+            fontSize: 10,
77
+            formatter: '{b}\n{d}%'
78
+          }
79
+        }]
80
+      }
81
+      this.chart.setOption(option)
82
+    }
83
+  },
84
+  beforeDestroy() {
85
+    if (this.chart) this.chart.dispose()
86
+  }
87
+}
88
+</script>
89
+
90
+<style lang="scss" scoped>
91
+.item-distribution {
92
+  .chart-container {
93
+    height: 250rpx;
94
+  }
95
+  
96
+  .legend-grid {
97
+    display: grid;
98
+    grid-template-columns: repeat(2, 1fr);
99
+    gap: 8rpx;
100
+    margin-top: 16rpx;
101
+  }
102
+  
103
+  .legend-item {
104
+    display: flex;
105
+    align-items: center;
106
+    font-size: 24rpx;
107
+    
108
+    .legend-dot {
109
+      width: 12rpx;
110
+      height: 12rpx;
111
+      border-radius: 50%;
112
+      margin-right: 8rpx;
113
+    }
114
+    
115
+    .legend-text {
116
+      color: rgba(255, 255, 255, 0.7);
117
+    }
118
+  }
119
+}
120
+</style>

+ 190 - 0
src/pages/stationProfile/components/MemberBasicDistribution.vue

@@ -0,0 +1,190 @@
1
+<template>
2
+  <div class="member-basic-distribution">
3
+    <div class="chart-list">
4
+      <div class="chart-item">
5
+        <div class="chart-label">性别分布</div>
6
+        <div class="chart-container" ref="genderChart"></div>
7
+      </div>
8
+      <div class="chart-item">
9
+        <div class="chart-label">民族分布</div>
10
+        <div class="chart-container" ref="ethnicityChart"></div>
11
+      </div>
12
+      <div class="chart-item">
13
+        <div class="chart-label">政治面貌分布</div>
14
+        <div class="chart-container" ref="politicalChart"></div>
15
+      </div>
16
+    </div>
17
+  </div>
18
+</template>
19
+
20
+<script>
21
+import * as echarts from 'echarts'
22
+
23
+export default {
24
+  name: 'MemberBasicDistribution',
25
+  props: {
26
+    chartsData: {
27
+      type: Object,
28
+      default: () => ({
29
+        gender: { labels: ['女', '男'], data: [120, 198] },
30
+        ethnicity: { labels: ['汉族', '其他'], data: [280, 38] },
31
+        political: { labels: ['党员', '群众'], data: [150, 168] }
32
+      })
33
+    }
34
+  },
35
+  data() {
36
+    return {
37
+      charts: {}
38
+    }
39
+  },
40
+  mounted() {
41
+    this.$nextTick(() => {
42
+      this.initCharts()
43
+    })
44
+  },
45
+  watch: {
46
+    chartsData: {
47
+      deep: true,
48
+      handler() {
49
+        this.$nextTick(() => {
50
+          this.disposeCharts()
51
+          this.initCharts()
52
+        })
53
+      }
54
+    }
55
+  },
56
+  methods: {
57
+    disposeCharts() {
58
+      Object.values(this.charts).forEach(chart => {
59
+        if (chart) chart.dispose()
60
+      })
61
+      this.charts = {}
62
+    },
63
+    initCharts() {
64
+      this.initGenderChart()
65
+      this.initEthnicityChart()
66
+      this.initPoliticalChart()
67
+    },
68
+    initGenderChart() {
69
+      if (!this.$refs.genderChart) return
70
+      this.charts.gender = echarts.init(this.$refs.genderChart)
71
+      const option = {
72
+        responsive: true,
73
+        maintainAspectRatio: false,
74
+        series: [{
75
+          type: 'pie',
76
+          radius: ['40%', '60%'],
77
+          data: this.chartsData.gender.data.map((value, index) => ({
78
+            name: this.chartsData.gender.labels[index],
79
+            value: value
80
+          })),
81
+          itemStyle: {
82
+            color: function(params) {
83
+              const colors = ['#60A5FA', '#F472B6']
84
+              return colors[params.dataIndex]
85
+            }
86
+          },
87
+          label: {
88
+            show: true,
89
+            color: 'rgba(255, 255, 255, 0.7)',
90
+            fontSize: 10,
91
+            formatter: '{b}\n{d}%'
92
+          }
93
+        }]
94
+      }
95
+      this.charts.gender.setOption(option)
96
+    },
97
+    initEthnicityChart() {
98
+      if (!this.$refs.ethnicityChart) return
99
+      this.charts.ethnicity = echarts.init(this.$refs.ethnicityChart)
100
+      const option = {
101
+        responsive: true,
102
+        maintainAspectRatio: false,
103
+        series: [{
104
+          type: 'pie',
105
+          radius: ['40%', '60%'],
106
+          data: this.chartsData.ethnicity.data.map((value, index) => ({
107
+            name: this.chartsData.ethnicity.labels[index],
108
+            value: value
109
+          })),
110
+          itemStyle: {
111
+            color: function(params) {
112
+              const colors = ['#A78BFA', '#34D399']
113
+              return colors[params.dataIndex]
114
+            }
115
+          },
116
+          label: {
117
+            show: true,
118
+            color: 'rgba(255, 255, 255, 0.7)',
119
+            fontSize: 10,
120
+            formatter: '{b}\n{d}%'
121
+          }
122
+        }]
123
+      }
124
+      this.charts.ethnicity.setOption(option)
125
+    },
126
+    initPoliticalChart() {
127
+      if (!this.$refs.politicalChart) return
128
+      this.charts.political = echarts.init(this.$refs.politicalChart)
129
+      const option = {
130
+        responsive: true,
131
+        maintainAspectRatio: false,
132
+        series: [{
133
+          type: 'pie',
134
+          radius: ['40%', '60%'],
135
+          data: this.chartsData.political.data.map((value, index) => ({
136
+            name: this.chartsData.political.labels[index],
137
+            value: value
138
+          })),
139
+          itemStyle: {
140
+            color: function(params) {
141
+              const colors = ['#FBBF24', '#6366F1']
142
+              return colors[params.dataIndex]
143
+            }
144
+          },
145
+          label: {
146
+            show: true,
147
+            color: 'rgba(255, 255, 255, 0.7)',
148
+            fontSize: 10,
149
+            formatter: '{b}\n{d}%'
150
+          }
151
+        }]
152
+      }
153
+      this.charts.political.setOption(option)
154
+    }
155
+  },
156
+  beforeDestroy() {
157
+    Object.values(this.charts).forEach(chart => {
158
+      if (chart) chart.dispose()
159
+    })
160
+  }
161
+}
162
+</script>
163
+
164
+<style lang="scss" scoped>
165
+.member-basic-distribution {
166
+  .chart-list {
167
+    display: flex;
168
+    flex-direction: column;
169
+    gap: 32rpx;
170
+  }
171
+  
172
+  .chart-item {
173
+    display: flex;
174
+    align-items: center;
175
+    gap: 24rpx;
176
+    
177
+    .chart-label {
178
+      width: 160rpx;
179
+      flex-shrink: 0;
180
+      font-size: 24rpx;
181
+      color: rgba(255, 255, 255, 0.5);
182
+    }
183
+    
184
+    .chart-container {
185
+      flex: 1;
186
+      height:250rpx;
187
+    }
188
+  }
189
+}
190
+</style>

+ 306 - 0
src/pages/stationProfile/components/MemberPositionDistribution.vue

@@ -0,0 +1,306 @@
1
+<template>
2
+  <div class="member-position-distribution">
3
+    <div class="chart-section">
4
+      <div class="sub-title">职业资格等级分布</div>
5
+      <div class="chart-container" ref="qualificationChart"></div>
6
+    </div>
7
+    <div class="chart-section">
8
+      <div class="sub-title">开机年限分布</div>
9
+      <div class="chart-container" ref="experienceChart"></div>
10
+    </div>
11
+    <div class="chart-section">
12
+      <div class="sub-title">岗位资质分布</div>
13
+      <div class="chart-container" ref="positionChart"></div>
14
+    </div>
15
+  </div>
16
+</template>
17
+
18
+<script>
19
+import * as echarts from 'echarts'
20
+
21
+export default {
22
+  name: 'MemberPositionDistribution',
23
+  props: {
24
+    chartsData: {
25
+      type: Object,
26
+      default: () => ({
27
+        qualification: { labels: ['初级', '中级', '高级', '技师'], data: [50, 120, 80, 68] },
28
+        experience: { labels: ['1-3年', '3-5年', '5-10年', '10年以上'], data: [60, 80, 100, 78] },
29
+        position: { labels: ['开机员', '开包员', '引导员', '指挥员'], data: [100, 80, 60, 78] }
30
+      })
31
+    }
32
+  },
33
+  data() {
34
+    return {
35
+      charts: {}
36
+    }
37
+  },
38
+  mounted() {
39
+    this.$nextTick(() => {
40
+      this.initCharts()
41
+    })
42
+  },
43
+  watch: {
44
+    chartsData: {
45
+      deep: true,
46
+      handler() {
47
+        this.$nextTick(() => {
48
+          this.disposeCharts()
49
+          this.initCharts()
50
+        })
51
+      }
52
+    }
53
+  },
54
+  methods: {
55
+    disposeCharts() {
56
+      Object.values(this.charts).forEach(chart => {
57
+        if (chart) chart.dispose()
58
+      })
59
+      this.charts = {}
60
+    },
61
+    initCharts() {
62
+      this.initQualificationChart()
63
+      this.initExperienceChart()
64
+      this.initPositionChart()
65
+    },
66
+    initQualificationChart() {
67
+      if (!this.$refs.qualificationChart) return
68
+      this.charts.qualification = echarts.init(this.$refs.qualificationChart)
69
+      const option = {
70
+        responsive: true,
71
+        maintainAspectRatio: false,
72
+        grid: {
73
+          left: '10%',
74
+          right: '5%',
75
+          bottom: '14%',
76
+          top: '10%'
77
+        },
78
+        xAxis: {
79
+          type: 'category',
80
+          data: this.chartsData.qualification.labels,
81
+          axisLine: {
82
+            lineStyle: {
83
+              color: 'rgba(255, 255, 255, 0.1)'
84
+            }
85
+          },
86
+          axisLabel: {
87
+            color: 'rgba(255, 255, 255, 0.5)',
88
+            fontSize: 10
89
+          }
90
+        },
91
+        yAxis: {
92
+          type: 'value',
93
+          axisLine: {
94
+            show: false
95
+          },
96
+          axisTick: {
97
+            show: false
98
+          },
99
+          splitLine: {
100
+            lineStyle: {
101
+              color: 'rgba(255, 255, 255, 0.05)'
102
+            }
103
+          },
104
+          axisLabel: {
105
+            color: 'rgba(255, 255, 255, 0.5)',
106
+            fontSize: 10
107
+          }
108
+        },
109
+        series: [{
110
+          type: 'bar',
111
+          data: this.chartsData.qualification.data,
112
+          itemStyle: {
113
+            color: {
114
+              type: 'linear',
115
+              x: 0,
116
+              y: 0,
117
+              x2: 0,
118
+              y2: 1,
119
+              colorStops: [{
120
+                offset: 0, color: '#A78BFA'
121
+              }, {
122
+                offset: 1, color: '#6366F1'
123
+              }]
124
+            }
125
+          },
126
+          barWidth: '60%'
127
+        }]
128
+      }
129
+      this.charts.qualification.setOption(option)
130
+    },
131
+    initExperienceChart() {
132
+      if (!this.$refs.experienceChart) return
133
+      this.charts.experience = echarts.init(this.$refs.experienceChart)
134
+      const option = {
135
+        responsive: true,
136
+        maintainAspectRatio: false,
137
+        grid: {
138
+          left: '10%',
139
+          right: '5%',
140
+          bottom: '14%',
141
+          top: '10%'
142
+        },
143
+        xAxis: {
144
+          type: 'category',
145
+          data: this.chartsData.experience.labels,
146
+          axisLine: {
147
+            lineStyle: {
148
+              color: 'rgba(255, 255, 255, 0.1)'
149
+            }
150
+          },
151
+          axisLabel: {
152
+            color: 'rgba(255, 255, 255, 0.5)',
153
+            fontSize: 10
154
+          }
155
+        },
156
+        yAxis: {
157
+          type: 'value',
158
+          axisLine: {
159
+            show: false
160
+          },
161
+          axisTick: {
162
+            show: false
163
+          },
164
+          splitLine: {
165
+            lineStyle: {
166
+              color: 'rgba(255, 255, 255, 0.05)'
167
+            }
168
+          },
169
+          axisLabel: {
170
+            color: 'rgba(255, 255, 255, 0.5)',
171
+            fontSize: 10
172
+          }
173
+        },
174
+        series: [{
175
+          type: 'bar',
176
+          data: this.chartsData.experience.data,
177
+          itemStyle: {
178
+            color: {
179
+              type: 'linear',
180
+              x: 0,
181
+              y: 0,
182
+              x2: 0,
183
+              y2: 1,
184
+              colorStops: [{
185
+                offset: 0, color: '#34D399'
186
+              }, {
187
+                offset: 1, color: '#059669'
188
+              }]
189
+            }
190
+          },
191
+          barWidth: '60%'
192
+        }]
193
+      }
194
+      this.charts.experience.setOption(option)
195
+    },
196
+    initPositionChart() {
197
+      if (!this.$refs.positionChart) return
198
+      this.charts.position = echarts.init(this.$refs.positionChart)
199
+      const option = {
200
+        responsive: true,
201
+        maintainAspectRatio: false,
202
+        grid: {
203
+          left: '10%',
204
+          right: '5%',
205
+          bottom: '20%',
206
+          top: '10%'
207
+        },
208
+        xAxis: {
209
+          type: 'category',
210
+          data: this.chartsData.position.labels,
211
+          axisLine: {
212
+            lineStyle: {
213
+              color: 'rgba(255, 255, 255, 0.1)'
214
+            }
215
+          },
216
+          axisLabel: {
217
+            color: 'rgba(255, 255, 255, 0.5)',
218
+            fontSize: 10
219
+          }
220
+        },
221
+        yAxis: {
222
+          type: 'value',
223
+          axisLine: {
224
+            show: false
225
+          },
226
+          axisTick: {
227
+            show: false
228
+          },
229
+          splitLine: {
230
+            lineStyle: {
231
+              color: 'rgba(255, 255, 255, 0.05)'
232
+            }
233
+          },
234
+          axisLabel: {
235
+            color: 'rgba(255, 255, 255, 0.5)',
236
+            fontSize: 10
237
+          }
238
+        },
239
+        dataZoom: [{
240
+          type: 'slider',
241
+          show: true,
242
+          xAxisIndex: 0,
243
+          height: 20,
244
+          bottom: 0,
245
+          borderColor: 'rgba(255, 255, 255, 0.1)',
246
+          backgroundColor: 'rgba(45, 42, 85, 0.8)',
247
+          fillerColor: 'rgba(167, 139, 250, 0.3)',
248
+          handleStyle: {
249
+            color: '#A78BFA'
250
+          },
251
+          labelStyle: {
252
+            color: 'rgba(255, 255, 255, 0.5)'
253
+          }
254
+        }],
255
+        series: [{
256
+          type: 'bar',
257
+          data: this.chartsData.position.data,
258
+          itemStyle: {
259
+            color: {
260
+              type: 'linear',
261
+              x: 0,
262
+              y: 0,
263
+              x2: 0,
264
+              y2: 1,
265
+              colorStops: [{
266
+                offset: 0, color: '#FBBF24'
267
+              }, {
268
+                offset: 1, color: '#F59E0B'
269
+              }]
270
+            }
271
+          },
272
+          barWidth: '60%'
273
+        }]
274
+      }
275
+      this.charts.position.setOption(option)
276
+    }
277
+  },
278
+  beforeDestroy() {
279
+    Object.values(this.charts).forEach(chart => {
280
+      if (chart) chart.dispose()
281
+    })
282
+  }
283
+}
284
+</script>
285
+
286
+<style lang="scss" scoped>
287
+.member-position-distribution {
288
+  .chart-section {
289
+    margin-bottom: 32rpx;
290
+    
291
+    &:last-child {
292
+      margin-bottom: 0;
293
+    }
294
+  }
295
+  
296
+  .sub-title {
297
+    font-size: 24rpx;
298
+    color: rgba(255, 255, 255, 0.5);
299
+    margin-bottom: 16rpx;
300
+  }
301
+  
302
+  .chart-container {
303
+    height: 250rpx;
304
+  }
305
+}
306
+</style>

+ 161 - 0
src/pages/stationProfile/components/PassengerChart.vue

@@ -0,0 +1,161 @@
1
+<template>
2
+  <div class="passenger-chart">
3
+    <div class="chart-container" ref="passengerChart"></div>
4
+  </div>
5
+</template>
6
+
7
+<script>
8
+import * as echarts from 'echarts'
9
+
10
+export default {
11
+  name: 'PassengerChart',
12
+  props: {
13
+    chartsData: {
14
+      type: Object,
15
+      default: () => ({
16
+        labels: ['6:00', '8:00', '10:00', '12:00', '14:00', '16:00', '18:00', '20:00', '22:00'],
17
+        areas: {
18
+          'T1': [120, 280, 450, 380, 420, 500, 480, 320, 180],
19
+          'T2': [150, 320, 520, 450, 480, 580, 550, 380, 200],
20
+          'T3': [100, 250, 400, 350, 380, 450, 420, 280, 150]
21
+        },
22
+        totalFlow: [370, 850, 1370, 1180, 1280, 1530, 1450, 980, 530]
23
+      })
24
+    }
25
+  },
26
+  data() {
27
+    return {
28
+      chart: null
29
+    }
30
+  },
31
+  mounted() {
32
+    this.$nextTick(() => {
33
+      this.initChart()
34
+    })
35
+  },
36
+  watch: {
37
+    chartsData: {
38
+      deep: true,
39
+      handler() {
40
+        this.$nextTick(() => {
41
+          if (this.chart) this.chart.dispose()
42
+          this.initChart()
43
+        })
44
+      }
45
+    }
46
+  },
47
+  methods: {
48
+    initChart() {
49
+      if (!this.$refs.passengerChart) return
50
+      this.chart = echarts.init(this.$refs.passengerChart)
51
+
52
+      const colors = [
53
+        ['#4da6ff', '#0f46fa'],
54
+        ['#7eff7e', '#2ecc71'],
55
+        ['#bd03fb', '#8a06e8'],
56
+        ['#ffa500', '#ff6600'],
57
+        ['#00bfff', '#0077be']
58
+      ]
59
+
60
+      const areaNames = Object.keys(this.chartsData.areas)
61
+      const seriesBars = areaNames.map((areaName, idx) => {
62
+        const [c1, c2] = colors[idx % colors.length]
63
+        return {
64
+          name: areaName,
65
+          type: 'bar',
66
+          yAxisIndex: 0,
67
+          data: this.chartsData.areas[areaName],
68
+          itemStyle: {
69
+            color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
70
+              { offset: 0, color: c1 },
71
+              { offset: 1, color: c2 }
72
+            ])
73
+          },
74
+          barWidth: '20%'
75
+        }
76
+      })
77
+
78
+      const lineData = this.chartsData.totalFlow || []
79
+      const allBarValues = Object.values(this.chartsData.areas).flat()
80
+      const maxVal = Math.max(...allBarValues, ...lineData)
81
+
82
+      const option = {
83
+        tooltip: {
84
+          trigger: 'axis',
85
+          backgroundColor: 'rgba(30, 27, 75, 0.95)',
86
+          borderColor: '#A78BFA',
87
+          textStyle: { color: '#fff' }
88
+        },
89
+        legend: {
90
+          data: [...areaNames, '人流总数'],
91
+          textStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
92
+          top: 0
93
+        },
94
+        grid: {
95
+          left: '10%',
96
+          right: '8%',
97
+          bottom: '10%',
98
+          top: '22%'
99
+        },
100
+        xAxis: {
101
+          type: 'category',
102
+          data: this.chartsData.labels,
103
+          axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
104
+          axisLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.1)' } }
105
+        },
106
+        yAxis: [
107
+          {
108
+            type: 'value',
109
+            name: '人数',
110
+            nameTextStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
111
+            position: 'left',
112
+            max: Math.ceil(maxVal * 1.2),
113
+            axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
114
+            axisLine: { show: false },
115
+            splitLine: { lineStyle: { color: 'rgba(255, 255, 255, 0.05)' } }
116
+          },
117
+          {
118
+            type: 'value',
119
+            name: '人流总数',
120
+            nameTextStyle: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
121
+            position: 'right',
122
+            max: Math.ceil(maxVal * 1.2),
123
+            axisLabel: { color: 'rgba(255, 255, 255, 0.5)', fontSize: 10 },
124
+            axisLine: { show: false },
125
+            splitLine: { show: false }
126
+          }
127
+        ],
128
+        series: [
129
+          ...seriesBars,
130
+          {
131
+            name: '人流总数',
132
+            type: 'line',
133
+            yAxisIndex: 1,
134
+            data: lineData,
135
+            smooth: true,
136
+            itemStyle: { color: '#ffd93d' },
137
+            areaStyle: {
138
+              color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
139
+                { offset: 0, color: 'rgba(255,217,61,0.3)' },
140
+                { offset: 1, color: 'rgba(255,217,61,0.05)' }
141
+              ])
142
+            }
143
+          }
144
+        ]
145
+      }
146
+      this.chart.setOption(option)
147
+    }
148
+  },
149
+  beforeDestroy() {
150
+    if (this.chart) this.chart.dispose()
151
+  }
152
+}
153
+</script>
154
+
155
+<style lang="scss" scoped>
156
+.passenger-chart {
157
+  .chart-container {
158
+    height: 380rpx;
159
+  }
160
+}
161
+</style>

+ 183 - 0
src/pages/stationProfile/components/SecurityTestCharts.vue

@@ -0,0 +1,183 @@
1
+<template>
2
+  <div class="security-test-charts">
3
+    <SectionTitle title="安保测试物品分类">
4
+      <div class="chart-container" ref="itemsChart"></div>
5
+    </SectionTitle>
6
+    <SectionTitle title="安保测试通过结果">
7
+      <div class="chart-container" ref="resultsChart"></div>
8
+    </SectionTitle>
9
+    <SectionTitle title="安保测试区域情况">
10
+      <div class="chart-container" ref="areasChart"></div>
11
+    </SectionTitle>
12
+  </div>
13
+</template>
14
+
15
+<script>
16
+import * as echarts from 'echarts'
17
+import SectionTitle from '@/components/SectionTitle.vue'
18
+
19
+export default {
20
+  name: 'SecurityTestCharts',
21
+  components: {
22
+    SectionTitle
23
+  },
24
+  props: {
25
+    chartsData: {
26
+      type: Object,
27
+      default: () => ({
28
+        items: {
29
+          labels: ['违禁品', '限制品', '生活用品'],
30
+          data: [45, 35, 20]
31
+        },
32
+        results: {
33
+          labels: ['通过', '未通过'],
34
+          data: [85, 15]
35
+        },
36
+        areas: {
37
+          labels: ['T1', 'T2', 'T3'],
38
+          data: [35, 35, 30]
39
+        }
40
+      })
41
+    }
42
+  },
43
+  data() {
44
+    return {
45
+      charts: {}
46
+    }
47
+  },
48
+  mounted() {
49
+    this.$nextTick(() => {
50
+      this.initCharts()
51
+    })
52
+  },
53
+  watch: {
54
+    chartsData: {
55
+      deep: true,
56
+      handler() {
57
+        this.$nextTick(() => {
58
+          this.disposeCharts()
59
+          this.initCharts()
60
+        })
61
+      }
62
+    }
63
+  },
64
+  methods: {
65
+    disposeCharts() {
66
+      Object.values(this.charts).forEach(chart => {
67
+        if (chart) chart.dispose()
68
+      })
69
+      this.charts = {}
70
+    },
71
+    initCharts() {
72
+      this.initItemsChart()
73
+      this.initResultsChart()
74
+      this.initAreasChart()
75
+    },
76
+    initItemsChart() {
77
+      if (!this.$refs.itemsChart) return
78
+      this.charts.items = echarts.init(this.$refs.itemsChart)
79
+      const option = {
80
+        responsive: true,
81
+        maintainAspectRatio: false,
82
+        series: [{
83
+          type: 'pie',
84
+          radius: ['50%', '70%'],
85
+          data: this.chartsData.items.labels.map((label, index) => ({
86
+            name: label,
87
+            value: this.chartsData.items.data[index]
88
+          })),
89
+          itemStyle: {
90
+            color: function (params) {
91
+              const colors = ['#60A5FA', '#A78BFA', '#34D399', '#FBBF24', '#F472B6', '#EF4444', '#A78BFA', '#6366F1']
92
+              return colors[params.dataIndex % colors.length]
93
+            }
94
+          },
95
+          label: {
96
+            show: true,
97
+            color: 'rgba(255, 255, 255, 0.7)',
98
+            fontSize: 10,
99
+            formatter: '{b}\n{d}%'
100
+          }
101
+        }]
102
+      }
103
+      this.charts.items.setOption(option)
104
+    },
105
+    initResultsChart() {
106
+      if (!this.$refs.resultsChart) return
107
+      this.charts.results = echarts.init(this.$refs.resultsChart)
108
+      const option = {
109
+        responsive: true,
110
+        maintainAspectRatio: false,
111
+        series: [{
112
+          type: 'pie',
113
+          radius: ['50%', '70%'],
114
+          data: this.chartsData.results.labels.map((label, index) => ({
115
+            name: label,
116
+            value: this.chartsData.results.data[index]
117
+          })),
118
+          itemStyle: {
119
+            color: function (params) {
120
+              const colors = ['#60A5FA', '#A78BFA', '#34D399', '#FBBF24', '#F472B6', '#EF4444', '#A78BFA', '#6366F1']
121
+              return colors[params.dataIndex % colors.length]
122
+            }
123
+          },
124
+          label: {
125
+            show: true,
126
+            color: 'rgba(255, 255, 255, 0.7)',
127
+            fontSize: 10,
128
+            formatter: '{b}\n{d}%'
129
+          }
130
+        }]
131
+      }
132
+      this.charts.results.setOption(option)
133
+    },
134
+    initAreasChart() {
135
+      if (!this.$refs.areasChart) return
136
+      this.charts.areas = echarts.init(this.$refs.areasChart)
137
+      const option = {
138
+        responsive: true,
139
+        maintainAspectRatio: false,
140
+        series: [{
141
+          type: 'pie',
142
+          radius: ['50%', '70%'],
143
+          data: this.chartsData.areas.labels.map((label, index) => ({
144
+            name: label,
145
+            value: this.chartsData.areas.data[index]
146
+          })),
147
+          itemStyle: {
148
+            color: function (params) {
149
+              const colors = ['#60A5FA', '#A78BFA', '#34D399', '#FBBF24', '#F472B6', '#EF4444', '#A78BFA', '#6366F1']
150
+              return colors[params.dataIndex % colors.length]
151
+            }
152
+          },
153
+          label: {
154
+            show: true,
155
+            color: 'rgba(255, 255, 255, 0.7)',
156
+            fontSize: 10,
157
+            formatter: '{b}\n{d}%'
158
+          }
159
+        }]
160
+      }
161
+      this.charts.areas.setOption(option)
162
+    }
163
+  },
164
+  beforeDestroy() {
165
+    Object.values(this.charts).forEach(chart => {
166
+      if (chart) chart.dispose()
167
+    })
168
+  }
169
+}
170
+</script>
171
+
172
+<style lang="scss" scoped>
173
+.security-test-charts {
174
+  display: flex;
175
+
176
+
177
+  flex-direction: column;
178
+}
179
+.chart-container {
180
+  width: 100%;
181
+  height: 240px;
182
+}
183
+</style>

+ 229 - 0
src/pages/stationProfile/components/SeizureInfo.vue

@@ -0,0 +1,229 @@
1
+<template>
2
+  <div class="seizure-info">
3
+    <div class="total-section">
4
+      <div class="donut-container">
5
+        <div class="chart-container" ref="seizureDonut"></div>
6
+      </div>
7
+      <div class="dept-bar-container">
8
+        <div class="chart-container" ref="deptSeizureBar"></div>
9
+      </div>
10
+    </div>
11
+  </div>
12
+</template>
13
+
14
+<script>
15
+import * as echarts from 'echarts'
16
+
17
+export default {
18
+  name: 'SeizureInfo',
19
+  props: {
20
+    chartsData: {
21
+      type: Object,
22
+      default: () => ({
23
+        total: 1982,
24
+        depts: {
25
+          labels: ['旅检一部', '旅检二部', '旅检三部'],
26
+          data: [620, 680, 682]
27
+        }
28
+      })
29
+    }
30
+  },
31
+  data() {
32
+    return {
33
+      charts: {}
34
+    }
35
+  },
36
+  mounted() {
37
+    this.$nextTick(() => {
38
+      this.initCharts()
39
+    })
40
+  },
41
+  watch: {
42
+    chartsData: {
43
+      deep: true,
44
+      handler() {
45
+        this.$nextTick(() => {
46
+          this.disposeCharts()
47
+          this.initCharts()
48
+        })
49
+      }
50
+    }
51
+  },
52
+  methods: {
53
+    disposeCharts() {
54
+      Object.values(this.charts).forEach(chart => {
55
+        if (chart) chart.dispose()
56
+      })
57
+      this.charts = {}
58
+    },
59
+    initCharts() {
60
+      this.initSeizureDonut()
61
+      this.initDeptSeizureBar()
62
+    },
63
+    initSeizureDonut() {
64
+      if (!this.$refs.seizureDonut) return
65
+      this.charts.seizureDonut = echarts.init(this.$refs.seizureDonut)
66
+      const option = {
67
+        responsive: true,
68
+        maintainAspectRatio: false,
69
+        series: [{
70
+          type: 'pie',
71
+          radius: ['60%', '80%'],
72
+          data: [
73
+            { value: this.chartsData.total, name: '查获总数' }
74
+          ],
75
+          itemStyle: {
76
+            color: {
77
+              type: 'linear',
78
+              x: 0,
79
+              y: 0,
80
+              x2: 1,
81
+              y2: 1,
82
+              colorStops: [{
83
+                offset: 0, color: '#A78BFA'
84
+              }, {
85
+                offset: 1, color: '#6366F1'
86
+              }]
87
+            }
88
+          },
89
+          label: {
90
+            show: false
91
+          }
92
+        }],
93
+        graphic: [
94
+          {
95
+            type: 'text',
96
+            left: 'center',
97
+            top: '38%',
98
+            style: {
99
+              text: String(this.chartsData.total),
100
+              fill: 'rgba(255, 255, 255, 0.9)',
101
+              fontSize: 14,
102
+              fontWeight: 'bold',
103
+              textAlign: 'center'
104
+            },
105
+            z: 100
106
+          },
107
+          {
108
+            type: 'text',
109
+            left: 'center',
110
+            top: '58%',
111
+            style: {
112
+              text: '查获总数',
113
+              fill: 'rgba(255, 255, 255, 0.5)',
114
+              fontSize: 15,
115
+              textAlign: 'center'
116
+            },
117
+            z: 100
118
+          }
119
+        ]
120
+      }
121
+      this.charts.seizureDonut.setOption(option)
122
+    },
123
+    initDeptSeizureBar() {
124
+      if (!this.$refs.deptSeizureBar) return
125
+      this.charts.deptSeizureBar = echarts.init(this.$refs.deptSeizureBar)
126
+      const option = {
127
+        responsive: true,
128
+        maintainAspectRatio: false,
129
+        grid: {
130
+          left: '15%',
131
+          right: '5%',
132
+          bottom: '15%',
133
+          top: '10%'
134
+        },
135
+        xAxis: {
136
+          type: 'category',
137
+          data: this.chartsData.depts.labels,
138
+          axisLine: {
139
+            lineStyle: {
140
+              color: 'rgba(255, 255, 255, 0.1)'
141
+            }
142
+          },
143
+          // axisLabel: {
144
+          //   color: 'rgba(255, 255, 255, 0.5)',
145
+          //   fontSize: 9,
146
+          //   rotate: 30
147
+          // }
148
+        },
149
+        yAxis: {
150
+          type: 'value',
151
+          axisLine: {
152
+            show: false
153
+          },
154
+          axisTick: {
155
+            show: false
156
+          },
157
+          splitLine: {
158
+            lineStyle: {
159
+              color: 'rgba(255, 255, 255, 0.05)'
160
+            }
161
+          },
162
+          axisLabel: {
163
+            color: 'rgba(255, 255, 255, 0.5)',
164
+            fontSize: 9
165
+          }
166
+        },
167
+        series: [{
168
+          type: 'bar',
169
+          data: this.chartsData.depts.data,
170
+          itemStyle: {
171
+            color: {
172
+              type: 'linear',
173
+              x: 0,
174
+              y: 0,
175
+              x2: 0,
176
+              y2: 1,
177
+              colorStops: [{
178
+                offset: 0, color: '#60A5FA'
179
+              }, {
180
+                offset: 1, color: '#3B82F6'
181
+              }]
182
+            }
183
+          },
184
+          barWidth: '50%'
185
+        }]
186
+      }
187
+      this.charts.deptSeizureBar.setOption(option)
188
+    }
189
+  },
190
+  beforeDestroy() {
191
+    Object.values(this.charts).forEach(chart => {
192
+      if (chart) chart.dispose()
193
+    })
194
+  }
195
+}
196
+</script>
197
+
198
+<style lang="scss" scoped>
199
+.seizure-info {
200
+  .total-section {
201
+    display: flex;
202
+    flex-direction: column;
203
+    align-items: center;
204
+  }
205
+  
206
+  .donut-container {
207
+    position: relative;
208
+    width: 240rpx;
209
+    height: 240rpx;
210
+    margin-bottom: 24rpx;
211
+    
212
+    .chart-container {
213
+      min-height: 240rpx;
214
+      width: 100%;
215
+      height: 100%;
216
+    }
217
+  }
218
+  
219
+  .dept-bar-container {
220
+    width: 100%;
221
+    height: 240rpx;
222
+
223
+    .chart-container {
224
+      width: 100%;
225
+      height: 100%;
226
+    }
227
+  }
228
+}
229
+</style>

+ 73 - 0
src/pages/stationProfile/components/TeamMemberTable.vue

@@ -0,0 +1,73 @@
1
+<template>
2
+  <div class="team-member-table">
3
+    <scroll-view scroll-x class="table-scroll">
4
+      <table class="data-table">
5
+        <thead>
6
+          <tr>
7
+            <th>部门</th>
8
+            <th>员工数量</th>
9
+            <th>党员数量</th>
10
+            <th>平均年龄</th>
11
+            <th>平均司龄</th>
12
+            <th>证书数量</th>
13
+            <th>开机年限</th>
14
+            <th>综合得分</th>
15
+          </tr>
16
+        </thead>
17
+        <tbody>
18
+          <tr v-for="(item, index) in chartsData" :key="index">
19
+            <td>{{ item.department }}</td>
20
+            <td>{{ item.employeeCount }}</td>
21
+            <td>{{ item.partyMemberCount }}</td>
22
+            <td>{{ item.avgAge }}</td>
23
+            <td>{{ item.avgTenure }}</td>
24
+            <td>{{ item.certificateCount }}</td>
25
+            <td>{{ item.machineYears }}</td>
26
+            <td>{{ item.comprehensiveScore }}</td>
27
+          </tr>
28
+        </tbody>
29
+      </table>
30
+    </scroll-view>
31
+  </div>
32
+</template>
33
+
34
+<script>
35
+export default {
36
+  name: 'TeamMemberTable',
37
+  props: {
38
+    chartsData: {
39
+      type: Array,
40
+      default: () => []
41
+    }
42
+  }
43
+}
44
+</script>
45
+
46
+<style lang="scss" scoped>
47
+.team-member-table {
48
+  .table-scroll {
49
+    width: 100%;
50
+  }
51
+  
52
+  .data-table {
53
+    width: 100%;
54
+    font-size: 24rpx;
55
+    border-collapse: collapse;
56
+    
57
+    th, td {
58
+      padding: 16rpx 8rpx;
59
+      text-align: left;
60
+      white-space: nowrap;
61
+    }
62
+    
63
+    th {
64
+      color: rgba(255, 255, 255, 0.5);
65
+      border-bottom: 1rpx solid rgba(255, 255, 255, 0.1);
66
+    }
67
+    
68
+    td {
69
+      color: rgba(255, 255, 255, 0.9);
70
+    }
71
+  }
72
+}
73
+</style>

+ 115 - 0
src/pages/stationProfile/components/UnsafeItemsChart.vue

@@ -0,0 +1,115 @@
1
+<template>
2
+  <div class="unsafe-items-chart">
3
+    <div class="chart-container" ref="itemsChart"></div>
4
+    <div class="items-list">
5
+      <div v-for="(item, index) in chartsData.items" :key="index" class="list-item">
6
+        <div class="item-name">{{ item.name }}</div>
7
+        <div class="item-value">{{ item.value }}</div>
8
+      </div>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script>
14
+import * as echarts from 'echarts'
15
+
16
+export default {
17
+  name: 'UnsafeItemsChart',
18
+  props: {
19
+    chartsData: {
20
+      type: Object,
21
+      default: () => ({
22
+        items: [
23
+          { name: '打火机', value: 57 },
24
+          { name: '固体酒精', value: 55 },
25
+          { name: '手机', value: 56 },
26
+          { name: '不合格充电宝', value: 56 },
27
+          { name: '登山杖', value: 55 }
28
+        ]
29
+      })
30
+    }
31
+  },
32
+  data() {
33
+    return {
34
+      chart: null
35
+    }
36
+  },
37
+  mounted() {
38
+    this.$nextTick(() => {
39
+      this.initChart()
40
+    })
41
+  },
42
+  watch: {
43
+    chartsData: {
44
+      deep: true,
45
+      handler() {
46
+        this.$nextTick(() => {
47
+          if (this.chart) this.chart.dispose()
48
+          this.initChart()
49
+        })
50
+      }
51
+    }
52
+  },
53
+  methods: {
54
+    initChart() {
55
+      if (!this.$refs.itemsChart) return
56
+      this.chart = echarts.init(this.$refs.itemsChart)
57
+      const option = {
58
+        responsive: true,
59
+        maintainAspectRatio: false,
60
+        series: [{
61
+          type: 'pie',
62
+          radius: ['50%', '70%'],
63
+          data: this.chartsData.items.map(item => ({
64
+            name: item.name,
65
+            value: item.value
66
+          })),
67
+          itemStyle: {
68
+            color: function(params) {
69
+              const colors = ['#60A5FA', '#A78BFA', '#34D399', '#FBBF24', '#F472B6']
70
+              return colors[params.dataIndex]
71
+            }
72
+          },
73
+          label: {
74
+            show: true,
75
+            color: 'rgba(255, 255, 255, 0.7)',
76
+            fontSize: 10,
77
+            formatter: '{b}\n{d}%'
78
+          }
79
+        }]
80
+      }
81
+      this.chart.setOption(option)
82
+    }
83
+  },
84
+  beforeDestroy() {
85
+    if (this.chart) this.chart.dispose()
86
+  }
87
+}
88
+</script>
89
+
90
+<style lang="scss" scoped>
91
+.unsafe-items-chart {
92
+  .chart-container {
93
+    height: 220rpx;
94
+  }
95
+  
96
+  .items-list {
97
+    margin-top: 16rpx;
98
+  }
99
+  
100
+  .list-item {
101
+    display: flex;
102
+    justify-content: space-between;
103
+    font-size: 24rpx;
104
+    padding: 8rpx 0;
105
+    
106
+    .item-name {
107
+      color: rgba(255, 255, 255, 0.7);
108
+    }
109
+    
110
+    .item-value {
111
+      color: rgba(255, 255, 255, 0.9);
112
+    }
113
+  }
114
+}
115
+</style>

+ 138 - 0
src/pages/stationProfile/components/UnsafePositionChart.vue

@@ -0,0 +1,138 @@
1
+<template>
2
+  <div class="unsafe-position-chart">
3
+    <div class="chart-container" ref="positionChart"></div>
4
+    <div class="position-info">
5
+      <div class="info-text">X光机操作员: {{ chartsData.total }}</div>
6
+    </div>
7
+  </div>
8
+</template>
9
+
10
+<script>
11
+import * as echarts from 'echarts'
12
+
13
+export default {
14
+  name: 'UnsafePositionChart',
15
+  props: {
16
+    chartsData: {
17
+      type: Object,
18
+      default: () => ({
19
+        total: 279,
20
+        positions: {
21
+          labels: ['X光机', '开包台', '引导岗', '验证岗', '维序岗'],
22
+          data: [279, 0, 0, 0, 0]
23
+        }
24
+      })
25
+    }
26
+  },
27
+  data() {
28
+    return {
29
+      chart: null
30
+    }
31
+  },
32
+  mounted() {
33
+    this.$nextTick(() => {
34
+      this.initChart()
35
+    })
36
+  },
37
+  watch: {
38
+    chartsData: {
39
+      deep: true,
40
+      handler() {
41
+        this.$nextTick(() => {
42
+          if (this.chart) this.chart.dispose()
43
+          this.initChart()
44
+        })
45
+      }
46
+    }
47
+  },
48
+  methods: {
49
+    initChart() {
50
+      if (!this.$refs.positionChart) return
51
+      this.chart = echarts.init(this.$refs.positionChart)
52
+      const option = {
53
+        responsive: true,
54
+        maintainAspectRatio: false,
55
+        grid: {
56
+          left: '15%',
57
+          right: '5%',
58
+          bottom: '15%',
59
+          top: '10%'
60
+        },
61
+        xAxis: {
62
+          type: 'category',
63
+          data: this.chartsData.positions.labels,
64
+          axisLine: {
65
+            lineStyle: {
66
+              color: 'rgba(255, 255, 255, 0.1)'
67
+            }
68
+          },
69
+          axisLabel: {
70
+            color: 'rgba(255, 255, 255, 0.5)',
71
+            fontSize: 10,
72
+            // rotate: 30
73
+          }
74
+        },
75
+        yAxis: {
76
+          type: 'value',
77
+          axisLine: {
78
+            show: false
79
+          },
80
+          axisTick: {
81
+            show: false
82
+          },
83
+          splitLine: {
84
+            lineStyle: {
85
+              color: 'rgba(255, 255, 255, 0.05)'
86
+            }
87
+          },
88
+          axisLabel: {
89
+            color: 'rgba(255, 255, 255, 0.5)',
90
+            fontSize: 10
91
+          }
92
+        },
93
+        series: [{
94
+          type: 'bar',
95
+          data: this.chartsData.positions.data,
96
+          itemStyle: {
97
+            color: {
98
+              type: 'linear',
99
+              x: 0,
100
+              y: 0,
101
+              x2: 0,
102
+              y2: 1,
103
+              colorStops: [{
104
+                offset: 0, color: '#F472B6'
105
+              }, {
106
+                offset: 1, color: '#DB2777'
107
+              }]
108
+            }
109
+          },
110
+          barWidth: '60%'
111
+        }]
112
+      }
113
+      this.chart.setOption(option)
114
+    }
115
+  },
116
+  beforeDestroy() {
117
+    if (this.chart) this.chart.dispose()
118
+  }
119
+}
120
+</script>
121
+
122
+<style lang="scss" scoped>
123
+.unsafe-position-chart {
124
+  .chart-container {
125
+    height: 240rpx;
126
+  }
127
+  
128
+  .position-info {
129
+    margin-top: 16rpx;
130
+    text-align: center;
131
+    
132
+    .info-text {
133
+      font-size: 24rpx;
134
+      color: rgba(255, 255, 255, 0.5);
135
+    }
136
+  }
137
+}
138
+</style>

+ 115 - 0
src/pages/stationProfile/components/UnsafeTypesChart.vue

@@ -0,0 +1,115 @@
1
+<template>
2
+  <div class="unsafe-types-chart">
3
+    <div class="chart-container" ref="typesChart"></div>
4
+    <div class="types-list">
5
+      <div v-for="(item, index) in chartsData.types" :key="index" class="list-item">
6
+        <div class="item-name">{{ item.name }}</div>
7
+        <div class="item-value">{{ item.value }}</div>
8
+      </div>
9
+    </div>
10
+  </div>
11
+</template>
12
+
13
+<script>
14
+import * as echarts from 'echarts'
15
+
16
+export default {
17
+  name: 'UnsafeTypesChart',
18
+  props: {
19
+    chartsData: {
20
+      type: Object,
21
+      default: () => ({
22
+        types: [
23
+          { name: '一类', value: 56 },
24
+          { name: '二类', value: 57 },
25
+          { name: '三类', value: 56 },
26
+          { name: '四类', value: 55 },
27
+          { name: '五类', value: 55 }
28
+        ]
29
+      })
30
+    }
31
+  },
32
+  data() {
33
+    return {
34
+      chart: null
35
+    }
36
+  },
37
+  mounted() {
38
+    this.$nextTick(() => {
39
+      this.initChart()
40
+    })
41
+  },
42
+  watch: {
43
+    chartsData: {
44
+      deep: true,
45
+      handler() {
46
+        this.$nextTick(() => {
47
+          if (this.chart) this.chart.dispose()
48
+          this.initChart()
49
+        })
50
+      }
51
+    }
52
+  },
53
+  methods: {
54
+    initChart() {
55
+      if (!this.$refs.typesChart) return
56
+      this.chart = echarts.init(this.$refs.typesChart)
57
+      const option = {
58
+        responsive: true,
59
+        maintainAspectRatio: false,
60
+        series: [{
61
+          type: 'pie',
62
+          radius: ['50%', '70%'],
63
+          data: this.chartsData.types.map(item => ({
64
+            name: item.name,
65
+            value: item.value
66
+          })),
67
+          itemStyle: {
68
+            color: function(params) {
69
+              const colors = ['#A78BFA', '#6366F1', '#8B5CF6', '#7C3AED', '#6D28D9']
70
+              return colors[params.dataIndex]
71
+            }
72
+          },
73
+          label: {
74
+            show: true,
75
+            color: 'rgba(255, 255, 255, 0.7)',
76
+            fontSize: 10,
77
+            formatter: '{b}\n{d}%'
78
+          }
79
+        }]
80
+      }
81
+      this.chart.setOption(option)
82
+    }
83
+  },
84
+  beforeDestroy() {
85
+    if (this.chart) this.chart.dispose()
86
+  }
87
+}
88
+</script>
89
+
90
+<style lang="scss" scoped>
91
+.unsafe-types-chart {
92
+  .chart-container {
93
+    height: 220rpx;
94
+  }
95
+  
96
+  .types-list {
97
+    margin-top: 16rpx;
98
+  }
99
+  
100
+  .list-item {
101
+    display: flex;
102
+    justify-content: space-between;
103
+    font-size: 24rpx;
104
+    padding: 8rpx 0;
105
+    
106
+    .item-name {
107
+      color: rgba(255, 255, 255, 0.7);
108
+    }
109
+    
110
+    .item-value {
111
+      color: rgba(255, 255, 255, 0.9);
112
+    }
113
+  }
114
+}
115
+</style>

+ 732 - 0
src/pages/stationProfile/index.vue

@@ -0,0 +1,732 @@
1
+<template>
2
+    <view class="station-profile-page">
3
+        <div class="page-header">
4
+            <div class="header-title">
5
+                <div class="title-main">旅检部综合信息展示</div>
6
+                <div class="header-right">
7
+                    <div class="current-time">{{ currentTime }}</div>
8
+                    <!-- <div class="menu-icon">
9
+            <uni-icons type="bars" size="20" color="#A78BFA"></uni-icons>
10
+          </div> -->
11
+                </div>
12
+            </div>
13
+
14
+            <div class="time-filter">
15
+                <scroll-view scroll-x class="time-scroll">
16
+                    <div class="time-tags">
17
+                        <div v-for="(tag, index) in timeTags" :key="index"
18
+                            :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
19
+                            {{ tag }}
20
+                        </div>
21
+                    </div>
22
+                </scroll-view>
23
+                <div v-if="selectedTimeTag === 4" class="date-range-picker">
24
+                    <picker mode="date" :value="startDate" @change="onStartDateChange">
25
+                        <div class="date-input" :class="{ filled: startDate }">
26
+                            {{ startDate || '开始日期' }}
27
+                        </div>
28
+                    </picker>
29
+                    <span class="date-separator">至</span>
30
+                    <picker mode="date" :value="endDate" @change="onEndDateChange">
31
+                        <div class="date-input" :class="{ filled: endDate }">
32
+                            {{ endDate || '结束日期' }}
33
+                        </div>
34
+                    </picker>
35
+                </div>
36
+            </div>
37
+
38
+            <div class="tab-nav">
39
+                <div class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
40
+                    站点画像
41
+                </div>
42
+                <div class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
43
+                    运行数据
44
+                </div>
45
+            </div>
46
+        </div>
47
+
48
+        <div class="page-content">
49
+            <SectionTitle title="值班信息">
50
+                <template #header-right>
51
+                    <div class="date-text">{{ currentDate }}</div>
52
+                </template>
53
+                <DutyInfo :dutyData="dutyData" />
54
+            </SectionTitle>
55
+
56
+            <SectionTitle title="人员在岗情况">
57
+                <AttendanceStatus :attendanceData="attendanceData" />
58
+            </SectionTitle>
59
+
60
+            <div v-if="activeTab === 'profile'">
61
+                <SectionTitle title="团队成员">
62
+                    <TeamMemberTable :chartsData="teamMemberData" />
63
+                </SectionTitle>
64
+
65
+                <SectionTitle title="成员基本情况分布">
66
+                    <MemberBasicDistribution :chartsData="memberBasicData" />
67
+                </SectionTitle>
68
+
69
+                <SectionTitle title="成员职位情况分布">
70
+                    <MemberPositionDistribution :chartsData="memberPositionData" />
71
+                </SectionTitle>
72
+            </div>
73
+
74
+            <div v-if="activeTab === 'data'">
75
+                <SectionTitle title="当天开航每小时各区过检人数组合图">
76
+                    <PassengerChart :chartsData="passengerData" />
77
+                </SectionTitle>
78
+
79
+
80
+                <SectionTitle title="查获信息展示">
81
+                    <SeizureInfo :chartsData="seizureInfoData" />
82
+                </SectionTitle>
83
+
84
+                <SectionTitle title="查获物品分布">
85
+                    <ItemDistribution :chartsData="itemDistributionData" />
86
+                </SectionTitle>
87
+
88
+
89
+                <SectionTitle title="每日查获数量">
90
+                    <DailySeizureChart :chartsData="dailySeizureData" />
91
+                </SectionTitle>
92
+
93
+                <SectionTitle title="查获工作区域分布">
94
+                    <AreaDistribution :chartsData="areaDistributionData" />
95
+                </SectionTitle>
96
+
97
+
98
+                <SectionTitle title="不安全事件物品分布">
99
+                    <UnsafeItemsChart :chartsData="unsafeItemsData" />
100
+                </SectionTitle>
101
+
102
+                <SectionTitle title="不安全事件类型分布">
103
+                    <UnsafeTypesChart :chartsData="unsafeTypesData" />
104
+                </SectionTitle>
105
+
106
+
107
+                <SectionTitle title="不安全事件查获岗位分布">
108
+                    <UnsafePositionChart :chartsData="unsafePositionData" />
109
+                </SectionTitle>
110
+
111
+                <SecurityTestCharts :chartsData="securityTestData" />
112
+            </div>
113
+        </div>
114
+    </view>
115
+</template>
116
+
117
+<script>
118
+import SectionTitle from '@/components/SectionTitle.vue'
119
+import TeamMemberTable from './components/TeamMemberTable.vue'
120
+import MemberBasicDistribution from './components/MemberBasicDistribution.vue'
121
+import MemberPositionDistribution from './components/MemberPositionDistribution.vue'
122
+import PassengerChart from './components/PassengerChart.vue'
123
+import SeizureInfo from './components/SeizureInfo.vue'
124
+import ItemDistribution from './components/ItemDistribution.vue'
125
+import DailySeizureChart from './components/DailySeizureChart.vue'
126
+import AreaDistribution from './components/AreaDistribution.vue'
127
+import UnsafeItemsChart from './components/UnsafeItemsChart.vue'
128
+import UnsafeTypesChart from './components/UnsafeTypesChart.vue'
129
+import UnsafePositionChart from './components/UnsafePositionChart.vue'
130
+import SecurityTestCharts from './components/SecurityTestCharts.vue'
131
+import DutyInfo from './components/DutyInfo.vue'
132
+import AttendanceStatus from './components/AttendanceStatus.vue'
133
+import {
134
+    countStationTeamStats,
135
+    getDeptMemberDistribution,
136
+    getDeptPositionDistribution,
137
+    countStationHourlyThroughput,
138
+    countSeizureInfoItem,
139
+    countSeizeSubjectCategoryQuantity,
140
+    countSeizureTotalQuantity,
141
+    countSeizureSingleQuantity,
142
+    countSeizeAreaQuantity,
143
+    countSeizureStatsItem,
144
+    countSeizureStatsType,
145
+    countSeizureStatsPost,
146
+    securityTestItemClassification,
147
+    securityTestPassingStatus,
148
+    securityTestRegion
149
+} from '@/api/portraitManagement/portraitManagement'
150
+
151
+const itemColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#9CA3AF', '#A78BFA', '#F472B6', '#6EE7B7']
152
+
153
+export default {
154
+    components: {
155
+        SectionTitle,
156
+        TeamMemberTable,
157
+        MemberBasicDistribution,
158
+        MemberPositionDistribution,
159
+        PassengerChart,
160
+        SeizureInfo,
161
+        ItemDistribution,
162
+        DailySeizureChart,
163
+        AreaDistribution,
164
+        UnsafeItemsChart,
165
+        UnsafeTypesChart,
166
+        UnsafePositionChart,
167
+        SecurityTestCharts,
168
+        DutyInfo,
169
+        AttendanceStatus
170
+    },
171
+    data() {
172
+        return {
173
+            activeTab: 'profile',
174
+            selectedTimeTag: 3,
175
+            timeTags: ['近一周', '近一月', '近三月', '近一年', '自定义时间范围'],
176
+            currentTime: '',
177
+            timer: null,
178
+            startDate: '',
179
+            endDate: '',
180
+            dutyData: [
181
+                { label: '值班领导', value: '李凯' },
182
+                { label: '安全板块', value: '李凯' },
183
+                { label: '服务板块', value: '李凯' },
184
+                { label: '运行板块', value: '李凯' },
185
+                { label: '备勤值班', value: '李凯' }
186
+            ],
187
+            attendanceData: [
188
+                { number: '120', label: '主班在岗 | 旅检一部' },
189
+                { number: '130', label: '主班在岗 | 旅检二部' },
190
+                { number: '150', label: '主班在岗 | 旅检三部' },
191
+                { number: '80', label: '备勤在岗 | 旅检一部' },
192
+                { number: '70', label: '备勤在岗 | 旅检二部' },
193
+                { number: '65', label: '备勤在岗 | 旅检三部' }
194
+            ],
195
+            teamMemberData: [],
196
+            memberBasicData: {
197
+                gender: { labels: [], data: [] },
198
+                ethnicity: { labels: [], data: [] },
199
+                political: { labels: [], data: [] }
200
+            },
201
+            memberPositionData: {
202
+                qualification: { labels: [], data: [] },
203
+                experience: { labels: [], data: [] },
204
+                position: { labels: [], data: [] }
205
+            },
206
+            passengerData: {
207
+                labels: [],
208
+                areas: {}
209
+            },
210
+            seizureInfoData: {
211
+                total: 0,
212
+                depts: {
213
+                    labels: [],
214
+                    data: []
215
+                }
216
+            },
217
+            itemDistributionData: {
218
+                items: []
219
+            },
220
+            dailySeizureData: {
221
+                total: {
222
+                    labels: [],
223
+                    data: []
224
+                },
225
+                dept: {
226
+                    labels: [],
227
+                    data: {}
228
+                }
229
+            },
230
+            areaDistributionData: {
231
+                labels: [],
232
+                data: []
233
+            },
234
+            unsafeItemsData: {
235
+                items: []
236
+            },
237
+            unsafeTypesData: {
238
+                types: []
239
+            },
240
+            unsafePositionData: {
241
+                total: 0,
242
+                positions: {
243
+                    labels: [],
244
+                    data: []
245
+                }
246
+            },
247
+            securityTestData: {
248
+                items: {
249
+                    labels: [],
250
+                    data: []
251
+                },
252
+                results: {
253
+                    labels: [],
254
+                    data: []
255
+                },
256
+                areas: {
257
+                    labels: [],
258
+                    data: []
259
+                }
260
+            }
261
+        }
262
+    },
263
+    computed: {
264
+        currentDate() {
265
+            const now = new Date()
266
+            const year = now.getFullYear()
267
+            const month = String(now.getMonth() + 1).padStart(2, '0')
268
+            const day = String(now.getDate()).padStart(2, '0')
269
+            const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
270
+            const weekDay = weekDays[now.getDay()]
271
+            return `${year}年${month}月${day}日 ${weekDay}`
272
+        }
273
+    },
274
+    mounted() {
275
+        this.updateTime()
276
+        this.timer = setInterval(() => {
277
+            this.updateTime()
278
+        }, 1000)
279
+        this.onTimeTagClick(3)
280
+    },
281
+    beforeDestroy() {
282
+        if (this.timer) {
283
+            clearInterval(this.timer)
284
+        }
285
+    },
286
+    methods: {
287
+        updateTime() {
288
+            const now = new Date()
289
+            const hours = String(now.getHours()).padStart(2, '0')
290
+            const minutes = String(now.getMinutes()).padStart(2, '0')
291
+            const seconds = String(now.getSeconds()).padStart(2, '0')
292
+            this.currentTime = `${hours}:${minutes}:${seconds}`
293
+        },
294
+        fetchAllData(params) {
295
+            this.fetchTeamData(params)
296
+            this.fetchMemberDistribution(params)
297
+            this.fetchPositionDistribution(params)
298
+            const copyParams = { startDate:params.startDate, endDate:params.endDate }
299
+            this.fetchPassengerData(copyParams)
300
+            this.fetchSeizureInfo(copyParams)
301
+            this.fetchItemDistribution(copyParams)
302
+            this.fetchDailySeizure(copyParams)
303
+            this.fetchAreaDistribution(copyParams)
304
+            this.fetchUnsafeItems(copyParams)
305
+            this.fetchUnsafeTypes(copyParams)
306
+            this.fetchUnsafePosition(copyParams)
307
+            this.fetchSecurityTestData(copyParams)
308
+        },
309
+        buildTimeParams() {
310
+            const now = new Date()
311
+            const today = this.formatDate(now)
312
+            if (this.selectedTimeTag === 4) {
313
+                if (this.startDate && this.endDate) {
314
+                    return { startDate: this.startDate, endDate: this.endDate, deptId: '100' }
315
+                }
316
+                return { deptId: '100' }
317
+            }
318
+            let startDate
319
+            switch (this.selectedTimeTag) {
320
+                case 0:
321
+                    startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
322
+                    break
323
+                case 1:
324
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
325
+                    break
326
+                case 2:
327
+                    startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
328
+                    break
329
+                case 3:
330
+                    startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
331
+                    break
332
+                default:
333
+                    return { deptId: '100' }
334
+            }
335
+            return { startDate: this.formatDate(startDate), endDate: today, deptId: '100' }
336
+        },
337
+        formatDate(date) {
338
+            const y = date.getFullYear()
339
+            const m = String(date.getMonth() + 1).padStart(2, '0')
340
+            const d = String(date.getDate()).padStart(2, '0')
341
+            return `${y}-${m}-${d}`
342
+        },
343
+        onTimeTagClick(index) {
344
+            this.selectedTimeTag = index
345
+            if (index === 4) {
346
+                this.startDate = ''
347
+                this.endDate = ''
348
+                return
349
+            }
350
+            const params = this.buildTimeParams()
351
+            this.fetchAllData(params)
352
+        },
353
+        onStartDateChange(e) {
354
+            this.startDate = e.detail.value
355
+            if (this.startDate && this.endDate) {
356
+                this.fetchAllData(this.buildTimeParams())
357
+            }
358
+        },
359
+        onEndDateChange(e) {
360
+            this.endDate = e.detail.value
361
+            if (this.startDate && this.endDate) {
362
+                this.fetchAllData(this.buildTimeParams())
363
+            }
364
+        },
365
+        fetchTeamData(params) {
366
+            countStationTeamStats(params).then(res => {
367
+                if (res.code === 200 && res.data) {
368
+                    this.teamMemberData = (res.data || []).map(item => ({
369
+                        department: item.deptName,
370
+                        employeeCount: item.employeeCount,
371
+                        partyMemberCount: item.partyMemberCount,
372
+                        avgAge: item.avgAge,
373
+                        avgTenure: item.avgWorkYears,
374
+                        certificateCount: item.qualificationLevel,
375
+                        machineYears: item.avgXrayOperatorYears,
376
+                        comprehensiveScore: item.totalScore
377
+                    }))
378
+                }
379
+            }).catch(() => { })
380
+        },
381
+        fetchMemberDistribution(params) {
382
+            getDeptMemberDistribution(params).then(res => {
383
+                if (res.code === 200 && res.data) {
384
+                    this.memberBasicData = {
385
+                        gender: {
386
+                            labels: (res.data.sexDistribution || []).map(item => item.name),
387
+                            data: (res.data.sexDistribution || []).map(item => item.count)
388
+                        },
389
+                        ethnicity: {
390
+                            labels: (res.data.nationDistribution || []).map(item => item.name),
391
+                            data: (res.data.nationDistribution || []).map(item => item.count)
392
+                        },
393
+                        political: {
394
+                            labels: (res.data.politicalDistribution || []).map(item => item.name),
395
+                            data: (res.data.politicalDistribution || []).map(item => item.count)
396
+                        }
397
+                    }
398
+                }
399
+
400
+            }).catch(() => { })
401
+        },
402
+        fetchPositionDistribution(params) {
403
+            getDeptPositionDistribution(params).then(res => {
404
+                if (res.code === 200 && res.data) {
405
+                    this.memberPositionData = {
406
+                        qualification: {
407
+                            labels: (res.data.qualificationDistribution || []).map(item => item.name),
408
+                            data: (res.data.qualificationDistribution || []).map(item => item.count)
409
+                        },
410
+                        experience: {
411
+                            labels: (res.data.xrayYearDistribution || []).map(item => item.name),
412
+                            data: (res.data.xrayYearDistribution || []).map(item => item.count)
413
+                        },
414
+                        position: {
415
+                            labels: (res.data.positionDistribution || []).map(item => item.name),
416
+                            data: (res.data.positionDistribution || []).map(item => item.count)
417
+                        }
418
+                    }
419
+                }
420
+            }).catch(() => { })
421
+        },
422
+        fetchPassengerData(params) {
423
+            countStationHourlyThroughput(params).then(res => {
424
+                if (res.code === 200 && res.data) {
425
+                    const rawData = res.data || []
426
+                    const hours = rawData.map(item => item.hour)
427
+                    const areaMap = {}
428
+                    rawData.forEach(item => {
429
+                        (item.areaList || []).forEach(area => {
430
+                            if (!areaMap[area.areaName]) {
431
+                                areaMap[area.areaName] = []
432
+                            }
433
+                            areaMap[area.areaName].push(area.areaFlow)
434
+                        })
435
+                    })
436
+                    this.passengerData = {
437
+                        labels: hours,
438
+                        areas: areaMap,
439
+                        totalFlow: rawData.map(item => item.totalHourFlow)
440
+                    }
441
+                }
442
+            }).catch(() => { })
443
+        },
444
+        fetchSeizureInfo(params) {
445
+            countSeizureInfoItem(params).then(res => {
446
+                if (res.code === 200 && res.data) {
447
+                    const itemList = res.data.itemList || []
448
+                    this.seizureInfoData = {
449
+                        total: res.data.totalSeizeNum || 0,
450
+                        depts: {
451
+                            labels: itemList.map(item => item.name),
452
+                            data: itemList.map(item => item.seizeNum)
453
+                        }
454
+                    }
455
+                }
456
+            }).catch(() => { })
457
+        },
458
+        fetchItemDistribution(params) {
459
+            countSeizeSubjectCategoryQuantity(params).then(res => {
460
+                if (res.code === 200 && res.data) {
461
+                    const items = (res.data || []).map((item, index) => ({
462
+                        name: item.itemName,
463
+                        value: item.itemNum,
464
+                        color: itemColors[index % itemColors.length]
465
+                    }))
466
+                    this.itemDistributionData = { items }
467
+                }
468
+            }).catch(() => { })
469
+        },
470
+        fetchDailySeizure(params) {
471
+            countSeizureTotalQuantity(params).then(res => {
472
+                if (res.code === 200 && res.data) {
473
+                    const data = res.data || []
474
+                    this.dailySeizureData.total = {
475
+                        labels: data.map(item => item.recordDate),
476
+                        data: data.map(item => item.seizeQuantity)
477
+                    }
478
+                }
479
+            }).catch(() => { })
480
+            countSeizureSingleQuantity(params).then(res => {
481
+                if (res.code === 200 && res.data) {
482
+                    const rawData = res.data || []
483
+                    const dates = rawData.map(item => item.recordDate)
484
+                    const deptMap = {}
485
+                    rawData.forEach(dayItem => {
486
+                        (dayItem.items || []).forEach(subItem => {
487
+                            if (!deptMap[subItem.groupName]) {
488
+                                deptMap[subItem.groupName] = []
489
+                            }
490
+                            deptMap[subItem.groupName].push(subItem.seizeQuantity)
491
+                        })
492
+                    })
493
+                    this.dailySeizureData.dept = {
494
+                        labels: dates,
495
+                        deptData: Object.keys(deptMap).map(name => ({
496
+                            name,
497
+                            data: deptMap[name]
498
+                        }))
499
+                    }
500
+                }
501
+            }).catch(() => { })
502
+        },
503
+        fetchAreaDistribution(params) {
504
+            countSeizeAreaQuantity(params).then(res => {
505
+                if (res.code === 200 && res.data) {
506
+                    const data = res.data || []
507
+                    this.areaDistributionData = {
508
+                        labels: data.map(item => item.workArea),
509
+                        data: data.map(item => item.areaSeizeNum)
510
+                    }
511
+                }
512
+            }).catch(() => { })
513
+        },
514
+        fetchUnsafeItems(params) {
515
+            countSeizureStatsItem(params).then(res => {
516
+                if (res.code === 200 && res.data) {
517
+                    const data = res.data || []
518
+                    this.unsafeItemsData = {
519
+                        items: data.map(item => ({
520
+                            name: item.itemName,
521
+                            value: item.itemNum
522
+                        }))
523
+                    }
524
+                }
525
+            }).catch(() => { })
526
+        },
527
+        fetchUnsafeTypes(params) {
528
+            countSeizureStatsType(params).then(res => {
529
+                if (res.code === 200 && res.data) {
530
+                    const data = res.data || []
531
+                    this.unsafeTypesData = {
532
+                        types: data.map(item => ({
533
+                            name: item.eventType,
534
+                            value: item.eventTypeNum
535
+                        }))
536
+                    }
537
+                }
538
+            }).catch(() => { })
539
+        },
540
+        fetchUnsafePosition(params) {
541
+            countSeizureStatsPost(params).then(res => {
542
+                if (res.code === 200 && res.data) {
543
+                    const data = res.data || []
544
+                    const total = data.reduce((sum, item) => sum + (item.count || 0), 0)
545
+                    this.unsafePositionData = {
546
+                        total: total,
547
+                        positions: {
548
+                            labels: data.map(item => item.positionName),
549
+                            data: data.map(item => item.positionNum)
550
+                        }
551
+                    }
552
+                }
553
+            }).catch(() => { })
554
+        },
555
+        fetchSecurityTestData(params) {
556
+            securityTestItemClassification(params).then(res => {
557
+                if (res.code === 200 && res.data) {
558
+                    const data = res.data || []
559
+                    this.securityTestData.items = {
560
+                        labels: data.map(item => item.name),
561
+                        data: data.map(item => item.total)
562
+                    }
563
+                }
564
+            }).catch(() => { })
565
+            securityTestPassingStatus(params).then(res => {
566
+                if (res.code === 200 && res.data) {
567
+                    const data = res.data || []
568
+                    this.securityTestData.results = {
569
+                        labels: data.map(item => item.name),
570
+                        data: data.map(item => item.total)
571
+                    }
572
+                }
573
+            }).catch(() => { })
574
+            securityTestRegion(params).then(res => {
575
+                if (res.code === 200 && res.data) {
576
+                    const data = res.data || []
577
+                    this.securityTestData.areas = {
578
+                        labels: data.map(item => item.name),
579
+                        data: data.map(item => item.total)
580
+                    }
581
+                }
582
+            }).catch(() => { })
583
+        }
584
+    }
585
+}
586
+</script>
587
+
588
+<style lang="scss" scoped>
589
+.station-profile-page {
590
+    min-height: 100vh;
591
+    background: linear-gradient(135deg, #1E1B4B 0%, #312E81 100%);
592
+    padding-bottom: 40rpx;
593
+}
594
+
595
+.page-header {
596
+    position: sticky;
597
+    top: 0;
598
+    z-index: 100;
599
+    background: rgba(30, 27, 75, 0.9);
600
+    backdrop-filter: blur(10px);
601
+    box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
602
+}
603
+
604
+.header-title {
605
+    padding: 24rpx 32rpx;
606
+    display: flex;
607
+    justify-content: space-between;
608
+    align-items: center;
609
+
610
+    .title-main {
611
+        font-size: 36rpx;
612
+        font-weight: bold;
613
+        background: linear-gradient(90deg, #A78BFA, #60A5FA);
614
+        -webkit-background-clip: text;
615
+        -webkit-text-fill-color: transparent;
616
+        background-clip: text;
617
+    }
618
+}
619
+
620
+.header-right {
621
+    display: flex;
622
+    align-items: center;
623
+    gap: 16rpx;
624
+
625
+    .current-time {
626
+        font-size: 24rpx;
627
+        color: rgba(255, 255, 255, 0.6);
628
+    }
629
+
630
+    .menu-icon {
631
+        display: flex;
632
+        align-items: center;
633
+    }
634
+}
635
+
636
+.time-filter {
637
+    padding: 16rpx 32rpx;
638
+    background: rgba(30, 27, 75, 0.8);
639
+}
640
+
641
+.time-scroll {
642
+    width: 100%;
643
+}
644
+
645
+.time-tags {
646
+    display: flex;
647
+    gap: 16rpx;
648
+    white-space: nowrap;
649
+}
650
+
651
+.time-tag {
652
+    padding: 8rpx 10rpx;
653
+    border-radius: 50rpx;
654
+    background: rgba(45, 42, 85, 0.8);
655
+    font-size: 24rpx;
656
+    color: rgba(255, 255, 255, 0.6);
657
+    transition: all 0.3s;
658
+
659
+    &.active {
660
+        background: #A78BFA;
661
+        color: #1E1B4B;
662
+        font-weight: 500;
663
+    }
664
+}
665
+
666
+.date-range-picker {
667
+    display: flex;
668
+    align-items: center;
669
+    gap: 16rpx;
670
+    margin-top: 16rpx;
671
+    padding-top: 16rpx;
672
+    border-top: 1rpx solid rgba(255, 255, 255, 0.1);
673
+}
674
+
675
+.date-input {
676
+    flex: 1;
677
+    padding: 12rpx 24rpx;
678
+    border-radius: 12rpx;
679
+    background: rgba(45, 42, 85, 0.8);
680
+    font-size: 24rpx;
681
+    color: rgba(255, 255, 255, 0.4);
682
+    text-align: center;
683
+
684
+    &.filled {
685
+        color: rgba(255, 255, 255, 0.9);
686
+    }
687
+}
688
+
689
+.date-separator {
690
+    font-size: 24rpx;
691
+    color: rgba(255, 255, 255, 0.5);
692
+    flex-shrink: 0;
693
+}
694
+
695
+.tab-nav {
696
+    padding: 16rpx 32rpx;
697
+    display: flex;
698
+    justify-content: center;
699
+    gap: 64rpx;
700
+}
701
+
702
+.tab-item {
703
+    font-size: 28rpx;
704
+    color: rgba(255, 255, 255, 0.5);
705
+    padding-bottom: 8rpx;
706
+    border-bottom: 2rpx solid transparent;
707
+    transition: all 0.3s;
708
+
709
+    &.active {
710
+        color: #A78BFA;
711
+        border-bottom-color: #A78BFA;
712
+    }
713
+}
714
+
715
+.page-content {
716
+    padding: 32rpx;
717
+    display: flex;
718
+    flex-direction: column;
719
+    /* gap: 32rpx; */
720
+}
721
+
722
+.two-col-grid {
723
+    display: grid;
724
+    grid-template-columns: repeat(2, 1fr);
725
+    gap: 24rpx;
726
+}
727
+
728
+.date-text {
729
+    font-size: 24rpx;
730
+    color: rgba(255, 255, 255, 0.5);
731
+}
732
+</style>