Преглед на файлове

feat: 新增部门/班组/员工画像页面,重构公共组件

1.  新增部门、班组、小组、员工画像四个页面路由与基础文件
2.  将站画像页面的公共组件抽离到全局components目录
3.  添加员工画像相关的后端接口
4.  修复站画像页面的组件引用路径
huoyi преди 2 седмици
родител
ревизия
ce14e7fdf3
променени са 30 файла, в които са добавени 2652 реда и са изтрити 14 реда
  1. 7 0
      src/api/portraitManagement/portraitManagement.js
  2. 24 0
      src/pages.json
  3. 0 0
      src/pages/components/AreaDistribution.vue
  4. 0 0
      src/pages/components/AttendanceStatus.vue
  5. 0 0
      src/pages/components/DailySeizureChart.vue
  6. 109 0
      src/pages/components/DeptStats.vue
  7. 0 0
      src/pages/components/DutyInfo.vue
  8. 129 0
      src/pages/components/InterceptionDistribution.vue
  9. 0 0
      src/pages/components/ItemDistribution.vue
  10. 0 0
      src/pages/components/MemberBasicDistribution.vue
  11. 0 0
      src/pages/components/MemberPositionDistribution.vue
  12. 0 0
      src/pages/components/PassengerChart.vue
  13. 224 0
      src/pages/components/ProfileRadar.vue
  14. 0 0
      src/pages/components/SecurityTestCharts.vue
  15. 130 0
      src/pages/components/SeizedNumAll.vue
  16. 0 0
      src/pages/components/SeizureInfo.vue
  17. 127 0
      src/pages/components/SupervisionDistribution.vue
  18. 0 0
      src/pages/components/TeamMemberTable.vue
  19. 0 0
      src/pages/components/UnsafeItemsChart.vue
  20. 0 0
      src/pages/components/UnsafePositionChart.vue
  21. 0 0
      src/pages/components/UnsafeTypesChart.vue
  22. 758 0
      src/pages/deptProfile/index.vue
  23. 0 0
      src/pages/deptProfile/部门画像
  24. 1130 0
      src/pages/employeeProfile/index.vue
  25. 0 0
      src/pages/employeeProfile/员工画像
  26. 0 0
      src/pages/groupProfile/index.vue
  27. 0 0
      src/pages/groupProfile/小组画像
  28. 14 14
      src/pages/stationProfile/index.vue
  29. 0 0
      src/pages/teamProfile/index.vue
  30. 0 0
      src/pages/teamProfile/班组画像

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

@@ -98,4 +98,11 @@ export function countDeptTeamStats(data) {
98 98
 //标签得分
99 99
 export function countTagScore(params) {
100 100
     return request({ url: '/ledger/scoreEmployeeAdditional/getTotalScoreByEmployee', method: 'get', params: params })
101
+}
102
+// ===== 员工画像 =====
103
+export function getEmployeePortrait(params) {
104
+    return request({ url: '/score/portrait/employee', method: 'get', params })
105
+}
106
+export function searchPortraitUsers(keyword) {
107
+    return request({ url: '/score/portrait/searchUsers', method: 'get', params: { keyword } })
101 108
 }

+ 24 - 0
src/pages.json

@@ -123,6 +123,30 @@
123 123
       }
124 124
     },
125 125
     {
126
+      "path": "pages/deptProfile/index",
127
+      "style": {
128
+        "navigationBarTitleText": "部门画像"
129
+      }
130
+    },
131
+    {
132
+      "path": "pages/teamProfile/index",
133
+      "style": {
134
+        "navigationBarTitleText": "班组画像"
135
+      }
136
+    },
137
+        {
138
+      "path": "pages/groupProfile/index",
139
+      "style": {
140
+        "navigationBarTitleText": "小组画像"
141
+      }
142
+    },
143
+    {
144
+      "path": "pages/employeeProfile/index",
145
+      "style": {
146
+        "navigationBarTitleText": "员工画像"
147
+      }
148
+    },
149
+    {
126 150
       "path": "pages/attendanceStatistics/index",
127 151
       "style": {
128 152
         "navigationBarTitleText": "考勤统计"

src/pages/stationProfile/components/AreaDistribution.vue → src/pages/components/AreaDistribution.vue


src/pages/stationProfile/components/AttendanceStatus.vue → src/pages/components/AttendanceStatus.vue


src/pages/stationProfile/components/DailySeizureChart.vue → src/pages/components/DailySeizureChart.vue


+ 109 - 0
src/pages/components/DeptStats.vue

@@ -0,0 +1,109 @@
1
+<template>
2
+    <div class="dept-stats">
3
+        <div class="stats-grid">
4
+            <div class="stat-item">
5
+                <div class="stat-value">{{ statsData.totalScore || 0 }}</div>
6
+                <div class="stat-label">综合得分</div>
7
+            </div>
8
+            <div class="stat-item">
9
+                <div class="stat-value">{{ statsData.employeeCount || 0 }}</div>
10
+                <div class="stat-label">人员数量</div>
11
+            </div>
12
+            <div class="stat-item center">
13
+                <div class="dept-name">{{ statsData.deptName || '' }}</div>
14
+            </div>
15
+            <div class="stat-item">
16
+                <div class="stat-value">{{ statsData.avgAge || 0 }}</div>
17
+                <div class="stat-label">平均年龄</div>
18
+            </div>
19
+            <div class="stat-item">
20
+                <div class="stat-value">{{ statsData.avgWorkYears || 0 }}</div>
21
+                <div class="stat-label">平均司龄</div>
22
+            </div>
23
+        </div>
24
+    </div>
25
+</template>
26
+
27
+<script>
28
+export default {
29
+    name: 'DeptStats',
30
+    props: {
31
+        statsData: {
32
+            type: Object,
33
+            default: () => ({})
34
+        }
35
+    }
36
+}
37
+</script>
38
+
39
+<style lang="scss" scoped>
40
+.dept-stats {
41
+    width: 100%;
42
+    padding: 16rpx 0;
43
+}
44
+
45
+.stats-grid {
46
+    display: grid;
47
+    grid-template-columns: repeat(5, 1fr);
48
+    gap: 16rpx;
49
+    align-items: center;
50
+}
51
+
52
+.stat-item {
53
+    display: flex;
54
+    flex-direction: column;
55
+    align-items: center;
56
+    justify-content: center;
57
+    padding: 24rpx 16rpx;
58
+    background: linear-gradient(135deg, rgba(33, 33, 58, 0.95), rgba(15, 70, 250, 0.2));
59
+    border-radius: 16rpx;
60
+    border: 1rpx solid rgba(159, 3, 193, 0.3);
61
+    position: relative;
62
+    overflow: hidden;
63
+
64
+    &::before {
65
+        content: '';
66
+        position: absolute;
67
+        top: 0;
68
+        left: 0;
69
+        right: 0;
70
+        height: 2rpx;
71
+        background: linear-gradient(90deg, #0f46fa, transparent);
72
+    }
73
+
74
+    &::after {
75
+        content: '';
76
+        position: absolute;
77
+        bottom: 0;
78
+        right: 0;
79
+        left: 0;
80
+        height: 2rpx;
81
+        background: linear-gradient(90deg, transparent, #9903C1);
82
+    }
83
+}
84
+
85
+.stat-value {
86
+    font-size: 36rpx;
87
+    font-weight: bold;
88
+    color: #fff;
89
+    text-shadow: 0 0 12rpx rgba(15, 70, 250, 0.8);
90
+    line-height: 1;
91
+}
92
+
93
+.stat-label {
94
+    font-size: 20rpx;
95
+    color: #a0c4ff;
96
+    margin-top: 8rpx;
97
+}
98
+
99
+.stat-item.center {
100
+    padding: 32rpx 16rpx;
101
+
102
+    .dept-name {
103
+        font-size: 28rpx;
104
+        font-weight: bold;
105
+        color: #fff;
106
+        text-align: center;
107
+    }
108
+}
109
+</style>

src/pages/stationProfile/components/DutyInfo.vue → src/pages/components/DutyInfo.vue


+ 129 - 0
src/pages/components/InterceptionDistribution.vue

@@ -0,0 +1,129 @@
1
+<template>
2
+    <div class="interception-distribution">
3
+        <div class="chart-container" ref="chart"></div>
4
+    </div>
5
+</template>
6
+
7
+<script>
8
+import * as echarts from 'echarts'
9
+
10
+const countryColors = ['#6DA6FE', '#9963FB', '#FCE661', '#F801FA', '#95E530', '#22C2CC', '#F76723']
11
+
12
+export default {
13
+    name: 'InterceptionDistribution',
14
+    props: {
15
+        chartsData: {
16
+            type: Array,
17
+            default: () => []
18
+        }
19
+    },
20
+    data() {
21
+        return {
22
+            chart: null
23
+        }
24
+    },
25
+    mounted() {
26
+        this.$nextTick(() => {
27
+            this.initChart()
28
+        })
29
+    },
30
+    watch: {
31
+        chartsData: {
32
+            deep: true,
33
+            handler() {
34
+                this.$nextTick(() => {
35
+                    if (this.chart) this.chart.dispose()
36
+                    this.initChart()
37
+                })
38
+            }
39
+        }
40
+    },
41
+    methods: {
42
+        initChart() {
43
+            if (!this.$refs.chart) return
44
+            this.chart = echarts.init(this.$refs.chart)
45
+            const option = {
46
+                responsive: true,
47
+                maintainAspectRatio: false,
48
+                grid: {
49
+                    left: '25%',
50
+                    right: '10%',
51
+                    bottom: '10%',
52
+                    top: '10%'
53
+                },
54
+                xAxis: {
55
+                    type: 'value',
56
+                    axisLine: { show: false },
57
+                    axisTick: { show: false },
58
+                    splitLine: {
59
+                        lineStyle: {
60
+                            color: 'rgba(255, 255, 255, 0.05)'
61
+                        }
62
+                    },
63
+                    axisLabel: {
64
+                        color: 'rgba(255, 255, 255, 0.5)',
65
+                        fontSize: 9
66
+                    }
67
+                },
68
+                yAxis: {
69
+                    type: 'category',
70
+                    data: this.chartsData.map(item => item.name),
71
+                    axisLine: { show: false },
72
+                    axisTick: { show: false },
73
+                    splitLine: {
74
+                        lineStyle: {
75
+                            color: 'rgba(255, 255, 255, 0.05)'
76
+                        }
77
+                    },
78
+                    axisLabel: {
79
+                        color: 'rgba(255, 255, 255, 0.6)',
80
+                        fontSize: 9,
81
+                        formatter: function (value) {
82
+                            return value.length > 5 ? value.substring(0, 6) + '...' : value
83
+                        }
84
+                    }
85
+                },
86
+                series: [
87
+                    {
88
+                        name: '拦截数量',
89
+                        type: 'bar',
90
+                        data: this.chartsData.map(item => item.num),
91
+                        itemStyle: {
92
+                            borderRadius: [0, 5, 5, 0],
93
+                            color: function (param) {
94
+                                return countryColors[param.dataIndex % countryColors.length]
95
+                            }
96
+                        },
97
+                        label: {
98
+                            show: true,
99
+                            position: 'right',
100
+                            color: '#FFF',
101
+                            fontSize: 9,
102
+                            fontWeight: 500
103
+                        }
104
+                    }
105
+                ],
106
+                tooltip: {
107
+                    trigger: 'axis',
108
+                    backgroundColor: 'rgba(13,80,122,0.95)',
109
+                    borderColor: '#70CFE7',
110
+                    textStyle: { color: '#fff' }
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
+.interception-distribution {
124
+    .chart-container {
125
+        width: 100%;
126
+        height: 250rpx;
127
+    }
128
+}
129
+</style>

src/pages/stationProfile/components/ItemDistribution.vue → src/pages/components/ItemDistribution.vue


src/pages/stationProfile/components/MemberBasicDistribution.vue → src/pages/components/MemberBasicDistribution.vue


src/pages/stationProfile/components/MemberPositionDistribution.vue → src/pages/components/MemberPositionDistribution.vue


src/pages/stationProfile/components/PassengerChart.vue → src/pages/components/PassengerChart.vue


+ 224 - 0
src/pages/components/ProfileRadar.vue

@@ -0,0 +1,224 @@
1
+<template>
2
+    <div class="profile-radar">
3
+        <div class="chart-container" ref="radarChart"></div>
4
+        <div class="radar-list">
5
+            <div v-for="(item, index) in radarData" :key="index" class="radar-item">
6
+                <span class="item-label">{{ item.name }}</span>
7
+                <div class="progress-row">
8
+                    <div class="progress-bar">
9
+                        <div class="progress-fill"
10
+                            :style="{ width: ((item.finalScore || 0) / (maxScore || 1) * 100) + '%', background: `linear-gradient(90deg, ${item.color}33, ${item.color})` }">
11
+                            <span class="progress-end" :style="{ background: item.color }"></span>
12
+                        </div>
13
+                    </div>
14
+                    <span class="item-value">{{ item.finalScore || 0 }}</span>
15
+                </div>
16
+            </div>
17
+        </div>
18
+    </div>
19
+</template>
20
+
21
+<script>
22
+import * as echarts from 'echarts'
23
+
24
+const freshColors = [
25
+    '#00e5ff', '#36d399', '#fbbf24', '#f472b6', '#a78bfa',
26
+    '#34d399', '#f97316', '#2dd4bf', '#e879f9', '#38bdf8'
27
+]
28
+
29
+export default {
30
+    name: 'ProfileRadar',
31
+    props: {
32
+        chartsData: {
33
+            type: Array,
34
+            default: () => []
35
+        }
36
+    },
37
+    data() {
38
+        return {
39
+            chart: null
40
+        }
41
+    },
42
+    computed: {
43
+        radarData() {
44
+            const data = this.chartsData.length > 0 ? this.chartsData : []
45
+            return data.map((item, index) => ({
46
+                ...item,
47
+                color: item.color || freshColors[index % freshColors.length]
48
+            }))
49
+        },
50
+        maxScore() {
51
+            const allScores = this.radarData.map(item => item.finalScore || 0)
52
+            return allScores.length > 0 ? Math.max(...allScores) : 100
53
+        },
54
+        indicators() {
55
+            const data = this.radarData
56
+            return data.map(item => ({
57
+                name: item.name,
58
+                max: Math.max(this.maxScore, 1)
59
+            }))
60
+        }
61
+    },
62
+    mounted() {
63
+        this.$nextTick(() => {
64
+            this.initChart()
65
+        })
66
+    },
67
+    watch: {
68
+        chartsData: {
69
+            deep: true,
70
+            handler() {
71
+                this.$nextTick(() => {
72
+                    if (this.chart) this.chart.dispose()
73
+                    this.initChart()
74
+                })
75
+            }
76
+        }
77
+    },
78
+    methods: {
79
+        initChart() {
80
+            if (!this.$refs.radarChart) return
81
+            this.chart = echarts.init(this.$refs.radarChart)
82
+            const option = {
83
+                responsive: true,
84
+                maintainAspectRatio: false,
85
+                grid: {
86
+                    top: 20,
87
+                    bottom: 20,
88
+                    left: 30,
89
+                    right: 30
90
+                },
91
+                radar: {
92
+                    indicator: this.indicators,
93
+                    center: ['50%', '50%'],
94
+                    radius: '60%',
95
+                    splitNumber: 5,
96
+                    axisLine: {
97
+                        lineStyle: {
98
+                            color: 'rgba(255, 255, 255, 0.2)'
99
+                        }
100
+                    },
101
+                    splitLine: {
102
+                        lineStyle: {
103
+                            color: 'rgba(255, 255, 255, 0.1)'
104
+                        }
105
+                    },
106
+                    splitArea: { show: false },
107
+                    axisName: {
108
+                        color: 'rgba(255, 255, 255, 0.6)',
109
+                        fontSize: 10
110
+                    }
111
+                },
112
+                series: [
113
+                    {
114
+                        type: 'radar',
115
+                        symbol: 'circle',
116
+                        symbolSize: 8,
117
+                        data: [
118
+                            {
119
+                                value: this.radarData.map(item => item.finalScore || 0),
120
+                                name: '综合得分',
121
+                                lineStyle: {
122
+                                    color: '#4DC8FE',
123
+                                    width: 2
124
+                                },
125
+                                itemStyle: {
126
+                                    color: '#fff',
127
+                                    borderWidth: 1,
128
+                                    borderColor: '#00C8DA'
129
+                                },
130
+                                areaStyle: {
131
+                                    color: 'rgba(77, 200, 254, 0.2)'
132
+                                }
133
+                            }
134
+                        ]
135
+                    }
136
+                ],
137
+                tooltip: {
138
+                    trigger: 'item',
139
+                    backgroundColor: 'rgba(13, 80, 122, 0.95)',
140
+                    borderColor: '#70CFE7',
141
+                    textStyle: {
142
+                        color: '#fff'
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
+.profile-radar {
157
+    display: flex;
158
+    flex-direction: column;
159
+    gap: 20rpx;
160
+
161
+    .chart-container {
162
+        width: 100%;
163
+        height: 350rpx;
164
+    }
165
+
166
+    .radar-list {
167
+        display: flex;
168
+        flex-direction: column;
169
+        gap: 12rpx;
170
+        padding: 0 10rpx;
171
+
172
+        .radar-item {
173
+            display: flex;
174
+            flex-direction: column;
175
+            gap: 6rpx;
176
+            font-size: 24rpx;
177
+
178
+            .item-label {
179
+                color: rgba(160, 196, 255, 0.9);
180
+            }
181
+
182
+            .progress-row {
183
+                display: flex;
184
+                align-items: center;
185
+                gap: 10rpx;
186
+
187
+                .progress-bar {
188
+                    flex: 1;
189
+                    height: 10rpx;
190
+                    background: rgba(255, 255, 255, 0.1);
191
+                    border-radius: 0;
192
+                    overflow: visible;
193
+
194
+                    .progress-fill {
195
+                        height: 100%;
196
+                        border-radius: 0;
197
+                        transition: width 0.3s ease;
198
+                        position: relative;
199
+
200
+                        .progress-end {
201
+                            position: absolute;
202
+                            right: -4rpx;
203
+                            top: 50%;
204
+                            transform: translateY(-50%);
205
+                            width: 6rpx;
206
+                            height: 18rpx;
207
+                            background: #fff;
208
+                            border-radius: 4rpx;
209
+                        }
210
+                    }
211
+                }
212
+
213
+                .item-value {
214
+                    width: 80rpx;
215
+                    text-align: right;
216
+                    color: #fff;
217
+                    font-weight: bold;
218
+                    font-size: 24rpx;
219
+                }
220
+            }
221
+        }
222
+    }
223
+}
224
+</style>

src/pages/stationProfile/components/SecurityTestCharts.vue → src/pages/components/SecurityTestCharts.vue


+ 130 - 0
src/pages/components/SeizedNumAll.vue

@@ -0,0 +1,130 @@
1
+<template>
2
+    <div class="seized-num-all">
3
+        <div class="chart-container" ref="chart"></div>
4
+    </div>
5
+</template>
6
+
7
+<script>
8
+import * as echarts from 'echarts'
9
+
10
+export default {
11
+    name: 'SeizedNumAll',
12
+    props: {
13
+        chartsData: {
14
+            type: Array,
15
+            default: () => []
16
+        }
17
+    },
18
+    data() {
19
+        return {
20
+            chart: null
21
+        }
22
+    },
23
+    mounted() {
24
+        this.$nextTick(() => {
25
+            this.initChart()
26
+        })
27
+    },
28
+    watch: {
29
+        chartsData: {
30
+            deep: true,
31
+            handler() {
32
+                this.$nextTick(() => {
33
+                    if (this.chart) this.chart.dispose()
34
+                    this.initChart()
35
+                })
36
+            }
37
+        }
38
+    },
39
+    methods: {
40
+        initChart() {
41
+            if (!this.$refs.chart) return
42
+            this.chart = echarts.init(this.$refs.chart)
43
+            const option = {
44
+                responsive: true,
45
+                maintainAspectRatio: false,
46
+                grid: {
47
+                    left: '15%',
48
+                    right: '5%',
49
+                    bottom: '15%',
50
+                    top: '10%'
51
+                },
52
+                xAxis: {
53
+                    type: 'category',
54
+                    data: this.chartsData.map(item => item.recordDate),
55
+                    axisLine: {
56
+                        lineStyle: {
57
+                            color: 'rgba(255, 255, 255, 0.1)'
58
+                        }
59
+                    },
60
+                    axisLabel: {
61
+                        color: 'rgba(255, 255, 255, 0.5)',
62
+                        fontSize: 9,
63
+                        rotate: 30
64
+                    }
65
+                },
66
+                yAxis: {
67
+                    type: 'value',
68
+                    axisLine: { show: false },
69
+                    axisTick: { show: false },
70
+                    splitLine: {
71
+                        lineStyle: {
72
+                            color: 'rgba(255, 255, 255, 0.05)'
73
+                        }
74
+                    },
75
+                    axisLabel: {
76
+                        color: 'rgba(255, 255, 255, 0.5)',
77
+                        fontSize: 9
78
+                    }
79
+                },
80
+                series: [
81
+                    {
82
+                        name: '查获数',
83
+                        type: 'line',
84
+                        data: this.chartsData.map(item => item.seizeQuantity),
85
+                        smooth: true,
86
+                        itemStyle: {
87
+                            color: '#71B138'
88
+                        },
89
+                        lineStyle: {
90
+                            color: '#71B138'
91
+                        },
92
+                        areaStyle: {
93
+                            color: {
94
+                                type: 'linear',
95
+                                x: 0,
96
+                                y: 0,
97
+                                x2: 0,
98
+                                y2: 1,
99
+                                colorStops: [
100
+                                    { offset: 0, color: 'rgba(113, 177, 56, 0.3)' },
101
+                                    { offset: 1, color: 'rgba(113, 177, 56, 0)' }
102
+                                ]
103
+                            }
104
+                        }
105
+                    }
106
+                ],
107
+                tooltip: {
108
+                    trigger: 'axis',
109
+                    backgroundColor: 'rgba(13,80,122,0.95)',
110
+                    borderColor: '#70CFE7',
111
+                    textStyle: { color: '#fff' }
112
+                }
113
+            }
114
+            this.chart.setOption(option)
115
+        }
116
+    },
117
+    beforeDestroy() {
118
+        if (this.chart) this.chart.dispose()
119
+    }
120
+}
121
+</script>
122
+
123
+<style lang="scss" scoped>
124
+.seized-num-all {
125
+    .chart-container {
126
+        width: 100%;
127
+        height: 250rpx;
128
+    }
129
+}
130
+</style>

src/pages/stationProfile/components/SeizureInfo.vue → src/pages/components/SeizureInfo.vue


+ 127 - 0
src/pages/components/SupervisionDistribution.vue

@@ -0,0 +1,127 @@
1
+<template>
2
+    <div class="supervision-distribution">
3
+        <div class="chart-container" ref="chart"></div>
4
+    </div>
5
+</template>
6
+
7
+<script>
8
+import * as echarts from 'echarts'
9
+
10
+export default {
11
+    name: 'SupervisionDistribution',
12
+    props: {
13
+        chartsData: {
14
+            type: Array,
15
+            default: () => []
16
+        }
17
+    },
18
+    data() {
19
+        return {
20
+            chart: null
21
+        }
22
+    },
23
+    mounted() {
24
+        this.$nextTick(() => {
25
+            this.initChart()
26
+        })
27
+    },
28
+    watch: {
29
+        chartsData: {
30
+            deep: true,
31
+            handler() {
32
+                this.$nextTick(() => {
33
+                    if (this.chart) this.chart.dispose()
34
+                    this.initChart()
35
+                })
36
+            }
37
+        }
38
+    },
39
+    methods: {
40
+        initChart() {
41
+            if (!this.$refs.chart) return
42
+            this.chart = echarts.init(this.$refs.chart)
43
+            const option = {
44
+                responsive: true,
45
+                maintainAspectRatio: false,
46
+                grid: {
47
+                    left: '15%',
48
+                    right: '5%',
49
+                    bottom: '20%',
50
+                    top: '10%'
51
+                },
52
+                xAxis: {
53
+                    type: 'category',
54
+                    data: this.chartsData.map(item => item.name),
55
+                    axisLine: {
56
+                        lineStyle: {
57
+                            color: 'rgba(255, 255, 255, 0.1)'
58
+                        }
59
+                    },
60
+                    axisLabel: {
61
+                        color: 'rgba(255, 255, 255, 0.5)',
62
+                        fontSize: 9,
63
+                        rotate: 30
64
+                    }
65
+                },
66
+                yAxis: {
67
+                    type: 'value',
68
+                    name: '人数',
69
+                    nameTextStyle: {
70
+                        color: 'rgba(255, 255, 255, 0.5)',
71
+                        fontSize: 9
72
+                    },
73
+                    axisLine: { show: false },
74
+                    axisTick: { show: false },
75
+                    splitLine: {
76
+                        lineStyle: {
77
+                            color: 'rgba(255, 255, 255, 0.05)'
78
+                        }
79
+                    },
80
+                    axisLabel: {
81
+                        color: 'rgba(255, 255, 255, 0.5)',
82
+                        fontSize: 9
83
+                    }
84
+                },
85
+                series: [
86
+                    {
87
+                        name: '查获数量',
88
+                        type: 'bar',
89
+                        data: this.chartsData.map(item => item.num),
90
+                        itemStyle: {
91
+                            borderRadius: [5, 5, 0, 0],
92
+                            color: '#F462CC'
93
+                        },
94
+                        label: {
95
+                            show: true,
96
+                            position: 'top',
97
+                            color: '#F462CC',
98
+                            fontSize: 9,
99
+                            fontWeight: 500
100
+                        },
101
+                        barWidth: '40%'
102
+                    }
103
+                ],
104
+                tooltip: {
105
+                    trigger: 'axis',
106
+                    backgroundColor: 'rgba(13,80,122,0.95)',
107
+                    borderColor: '#70CFE7',
108
+                    textStyle: { color: '#fff' }
109
+                }
110
+            }
111
+            this.chart.setOption(option)
112
+        }
113
+    },
114
+    beforeDestroy() {
115
+        if (this.chart) this.chart.dispose()
116
+    }
117
+}
118
+</script>
119
+
120
+<style lang="scss" scoped>
121
+.supervision-distribution {
122
+    .chart-container {
123
+        width: 100%;
124
+        height: 250rpx;
125
+    }
126
+}
127
+</style>

src/pages/stationProfile/components/TeamMemberTable.vue → src/pages/components/TeamMemberTable.vue


src/pages/stationProfile/components/UnsafeItemsChart.vue → src/pages/components/UnsafeItemsChart.vue


src/pages/stationProfile/components/UnsafePositionChart.vue → src/pages/components/UnsafePositionChart.vue


src/pages/stationProfile/components/UnsafeTypesChart.vue → src/pages/components/UnsafeTypesChart.vue


+ 758 - 0
src/pages/deptProfile/index.vue

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

+ 0 - 0
src/pages/deptProfile/部门画像


Файловите разлики са ограничени, защото са твърде много
+ 1130 - 0
src/pages/employeeProfile/index.vue


+ 0 - 0
src/pages/employeeProfile/员工画像


+ 0 - 0
src/pages/groupProfile/index.vue


+ 0 - 0
src/pages/groupProfile/小组画像


+ 14 - 14
src/pages/stationProfile/index.vue

@@ -116,20 +116,20 @@
116 116
 
117 117
 <script>
118 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'
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 133
 import {
134 134
     countStationTeamStats,
135 135
     getDeptMemberDistribution,

+ 0 - 0
src/pages/teamProfile/index.vue


+ 0 - 0
src/pages/teamProfile/班组画像