Explorar o código

feat: 完成人像管理模块的团队/部门画像页面开发

新增多个数据可视化组件,完善五边形组件样式,搭建部门和团队画像的完整页面框架,包含人员列表、各类统计图表和运行数据看板
huoyi hai 4 semanas
pai
achega
5dbb8faeb6
Modificáronse 21 ficheiros con 2642 adicións e 1839 borrados
  1. 1 1
      src/views/portraitManagement/components/PentagonGroup.vue
  2. 144 0
      src/views/portraitManagement/components/ProfileBasicDistribution.vue
  3. 184 0
      src/views/portraitManagement/components/ProfileDailySeizedArea.vue
  4. 103 0
      src/views/portraitManagement/components/ProfileDailySeizedBar.vue
  5. 101 0
      src/views/portraitManagement/components/ProfileDailySeizedLine.vue
  6. 43 0
      src/views/portraitManagement/components/ProfileMembers.vue
  7. 127 0
      src/views/portraitManagement/components/ProfilePassRate.vue
  8. 160 0
      src/views/portraitManagement/components/ProfilePositionDistribution.vue
  9. 253 0
      src/views/portraitManagement/components/ProfileRadar.vue
  10. 75 0
      src/views/portraitManagement/components/ProfileSeizedDistribution.vue
  11. 137 0
      src/views/portraitManagement/components/ProfileSeizedInfo.vue
  12. 209 0
      src/views/portraitManagement/deptProfile/component/profile.vue
  13. 143 0
      src/views/portraitManagement/deptProfile/component/runData.vue
  14. 173 2
      src/views/portraitManagement/deptProfile/index.vue
  15. 114 999
      src/views/portraitManagement/groupProfile/component/profile.vue
  16. 6 4
      src/views/portraitManagement/groupProfile/index.vue
  17. 42 318
      src/views/portraitManagement/stationProfile/component/profile.vue
  18. 101 514
      src/views/portraitManagement/stationProfile/component/runData.vue
  19. 209 0
      src/views/portraitManagement/teamProfile/component/profile.vue
  20. 143 0
      src/views/portraitManagement/teamProfile/component/runData.vue
  21. 174 1
      src/views/portraitManagement/teamProfile/index.vue

+ 1 - 1
src/views/portraitManagement/components/PentagonGroup.vue

@@ -28,7 +28,7 @@
28 28
         :class="{ 'vertical': item.vertical || vertical }"
29 29
         :style="item.cardContentStyle">
30 30
         <div class="icon-wrapper" :style="item.iconStyle">
31
-          <img v-if="item.iconPath" :src="item.iconPath" class="icon-image" :alt="item.label" />
31
+          <img v-if="item.iconPath" :src="item.iconPath" class="icon-image" :style="item.iconStyle" :alt="item.label" />
32 32
           <span v-else-if="item.icon" class="icon-emoji">{{ item.icon }}</span>
33 33
         </div>
34 34
         <div class="content-wrapper">

+ 144 - 0
src/views/portraitManagement/components/ProfileBasicDistribution.vue

@@ -0,0 +1,144 @@
1
+<template>
2
+  <InfoCard title="成员基本情况分布">
3
+    <div class="basic-distribution">
4
+      <div class="distribution-item">
5
+        <div ref="genderChartRef" class="distribution-chart"></div>
6
+        <div class="distribution-label">性别分布</div>
7
+      </div>
8
+      <div class="distribution-item">
9
+        <div ref="nationChartRef" class="distribution-chart"></div>
10
+        <div class="distribution-label">民族分布</div>
11
+      </div>
12
+      <div class="distribution-item">
13
+        <div ref="politicalChartRef" class="distribution-chart"></div>
14
+        <div class="distribution-label">政治面貌分布</div>
15
+      </div>
16
+    </div>
17
+  </InfoCard>
18
+</template>
19
+
20
+<script setup>
21
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
22
+import * as echarts from 'echarts'
23
+import InfoCard from './card.vue'
24
+
25
+const props = defineProps({
26
+  chartData1: {
27
+    type: Array,
28
+    default: () => []
29
+  },
30
+  chartData2: {
31
+    type: Array,
32
+    default: () => []
33
+  },
34
+  chartData3: {
35
+    type: Array,
36
+    default: () => []
37
+  }
38
+})
39
+
40
+const defaultGenderData = [
41
+  { value: 6, name: '女', itemStyle: { color: '#ff6b9d' } },
42
+  { value: 8, name: '男', itemStyle: { color: '#4da6ff' } }
43
+]
44
+
45
+const defaultNationData = [
46
+  { value: 1, name: '回族', itemStyle: { color: '#ff9f43' } },
47
+  { value: 8, name: '汉族', itemStyle: { color: '#4da6ff' } },
48
+  { value: 5, name: '其他', itemStyle: { color: '#a55eea' } }
49
+]
50
+
51
+const defaultPoliticalData = [
52
+  { value: 1, name: '党员', itemStyle: { color: '#ff6b6b' } },
53
+  { value: 2, name: '共青团员', itemStyle: { color: '#ffd93d' } },
54
+  { value: 5, name: '群众', itemStyle: { color: '#6bcb77' } },
55
+  { value: 6, name: '其他', itemStyle: { color: '#4d96ff' } }
56
+]
57
+
58
+const genderChartRef = ref(null)
59
+let genderChart = null
60
+const nationChartRef = ref(null)
61
+let nationChart = null
62
+const politicalChartRef = ref(null)
63
+let politicalChart = null
64
+
65
+const initPieChart = (ref, data) => {
66
+  if (!ref) return
67
+  const chart = echarts.init(ref)
68
+  const option = {
69
+    series: [{
70
+      type: 'pie',
71
+      radius: ['50%', '70%'],
72
+      data: data,
73
+      label: { show: true, color: '#fff', fontSize: 11 },
74
+      labelLine: { show: true }
75
+    }]
76
+  }
77
+  chart.setOption(option)
78
+  return chart
79
+}
80
+
81
+const initCharts = () => {
82
+  const genderData = props.chartData1.length > 0 ? props.chartData1 : defaultGenderData
83
+  const nationData = props.chartData2.length > 0 ? props.chartData2 : defaultNationData
84
+  const politicalData = props.chartData3.length > 0 ? props.chartData3 : defaultPoliticalData
85
+
86
+  genderChart = initPieChart(genderChartRef.value, genderData)
87
+  nationChart = initPieChart(nationChartRef.value, nationData)
88
+  politicalChart = initPieChart(politicalChartRef.value, politicalData)
89
+}
90
+
91
+const handleResize = () => {
92
+  if (genderChart) genderChart.resize()
93
+  if (nationChart) nationChart.resize()
94
+  if (politicalChart) politicalChart.resize()
95
+}
96
+
97
+onMounted(() => {
98
+  nextTick(() => {
99
+    setTimeout(() => {
100
+      initCharts()
101
+      window.addEventListener('resize', handleResize)
102
+    }, 100)
103
+  })
104
+})
105
+
106
+onUnmounted(() => {
107
+  window.removeEventListener('resize', handleResize)
108
+  if (genderChart) genderChart.dispose()
109
+  if (nationChart) nationChart.dispose()
110
+  if (politicalChart) politicalChart.dispose()
111
+})
112
+</script>
113
+
114
+<style lang="scss" scoped>
115
+.basic-distribution {
116
+  display: flex;
117
+  justify-content: space-around;
118
+  align-items: stretch;
119
+  min-height: 280px;
120
+  gap: 15px;
121
+  padding: 10px 0;
122
+
123
+  .distribution-item {
124
+    flex: 1;
125
+    display: flex;
126
+    flex-direction: column;
127
+    align-items: center;
128
+    justify-content: center;
129
+    min-width: 0;
130
+
131
+    .distribution-chart {
132
+      width: 100%;
133
+      height: 200px;
134
+    }
135
+
136
+    .distribution-label {
137
+      color: #a0c4ff;
138
+      font-size: 12px;
139
+      margin-top: 10px;
140
+      text-align: center;
141
+    }
142
+  }
143
+}
144
+</style>

+ 184 - 0
src/views/portraitManagement/components/ProfileDailySeizedArea.vue

@@ -0,0 +1,184 @@
1
+<template>
2
+  <InfoCard title="每日查获数量(个人对比)">
3
+    <div ref="dailySeizedAreaRef" class="daily-chart"></div>
4
+  </InfoCard>
5
+</template>
6
+
7
+<script setup>
8
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
9
+import * as echarts from 'echarts'
10
+import InfoCard from './card.vue'
11
+
12
+const props = defineProps({
13
+  chartData1: {
14
+    type: Array,
15
+    default: () => []
16
+  },
17
+  chartData2: {
18
+    type: Array,
19
+    default: () => []
20
+  },
21
+  chartData3: {
22
+    type: Array,
23
+    default: () => []
24
+  },
25
+  chartData4: {
26
+    type: Array,
27
+    default: () => []
28
+  },
29
+  chartData5: {
30
+    type: Array,
31
+    default: () => []
32
+  }
33
+})
34
+
35
+const defaultCategories = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
36
+const defaultPersonA = [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330]
37
+const defaultPersonB = [220, 182, 191, 234, 290, 330, 310, 123, 144, 210, 182, 191]
38
+const defaultPersonC = [150, 232, 201, 154, 190, 330, 410, 201, 154, 190, 330, 410]
39
+const defaultPersonD = [320, 332, 301, 334, 390, 330, 320, 332, 301, 334, 390, 330]
40
+const defaultPersonE = [820, 932, 901, 934, 1290, 1330, 1320, 820, 932, 901, 934, 1290]
41
+
42
+const dailySeizedAreaRef = ref(null)
43
+let dailySeizedArea = null
44
+
45
+const initChart = () => {
46
+  if (!dailySeizedAreaRef.value) return
47
+  dailySeizedArea = echarts.init(dailySeizedAreaRef.value)
48
+  const option = {
49
+    tooltip: {
50
+      trigger: 'axis',
51
+      backgroundColor: 'rgba(13,80,122,0.95)',
52
+      borderColor: '#70CFE7',
53
+      textStyle: { color: '#fff' }
54
+    },
55
+    legend: {
56
+      data: ['人员A', '人员B', '人员C', '人员D', '人员E'],
57
+      textStyle: { color: '#a0c4ff' },
58
+      top: 0
59
+    },
60
+    grid: {
61
+      left: '3%',
62
+      right: '4%',
63
+      bottom: '3%',
64
+      containLabel: true
65
+    },
66
+    xAxis: {
67
+      type: 'category',
68
+      boundaryGap: false,
69
+      data: defaultCategories,
70
+      axisLabel: { color: '#a0c4ff' },
71
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
72
+    },
73
+    yAxis: {
74
+      type: 'value',
75
+      axisLabel: { color: '#a0c4ff' },
76
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
77
+      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
78
+    },
79
+    series: [
80
+      {
81
+        name: '人员A',
82
+        type: 'line',
83
+        stack: 'Total',
84
+        smooth: true,
85
+        areaStyle: {
86
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
87
+            { offset: 0, color: 'rgba(77, 166, 255, 0.5)' },
88
+            { offset: 1, color: 'rgba(77, 166, 255, 0.1)' }
89
+          ])
90
+        },
91
+        itemStyle: { color: '#4da6ff' },
92
+        lineStyle: { color: '#4da6ff', width: 2 },
93
+        data: props.chartData1.length > 0 ? props.chartData1 : defaultPersonA
94
+      },
95
+      {
96
+        name: '人员B',
97
+        type: 'line',
98
+        stack: 'Total',
99
+        smooth: true,
100
+        areaStyle: {
101
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
102
+            { offset: 0, color: 'rgba(189, 3, 251, 0.5)' },
103
+            { offset: 1, color: 'rgba(189, 3, 251, 0.1)' }
104
+          ])
105
+        },
106
+        itemStyle: { color: '#bd03fb' },
107
+        lineStyle: { color: '#bd03fb', width: 2 },
108
+        data: props.chartData2.length > 0 ? props.chartData2 : defaultPersonB
109
+      },
110
+      {
111
+        name: '人员C',
112
+        type: 'line',
113
+        stack: 'Total',
114
+        smooth: true,
115
+        areaStyle: {
116
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
117
+            { offset: 0, color: 'rgba(126, 255, 126, 0.5)' },
118
+            { offset: 1, color: 'rgba(126, 255, 126, 0.1)' }
119
+          ])
120
+        },
121
+        itemStyle: { color: '#7eff7e' },
122
+        lineStyle: { color: '#7eff7e', width: 2 },
123
+        data: props.chartData3.length > 0 ? props.chartData3 : defaultPersonC
124
+      },
125
+      {
126
+        name: '人员D',
127
+        type: 'line',
128
+        stack: 'Total',
129
+        smooth: true,
130
+        areaStyle: {
131
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
132
+            { offset: 0, color: 'rgba(255, 217, 62, 0.5)' },
133
+            { offset: 1, color: 'rgba(255, 217, 62, 0.1)' }
134
+          ])
135
+        },
136
+        itemStyle: { color: '#ffd93e' },
137
+        lineStyle: { color: '#ffd93e', width: 2 },
138
+        data: props.chartData4.length > 0 ? props.chartData4 : defaultPersonD
139
+      },
140
+      {
141
+        name: '人员E',
142
+        type: 'line',
143
+        stack: 'Total',
144
+        smooth: true,
145
+        areaStyle: {
146
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
147
+            { offset: 0, color: 'rgba(126, 196, 255, 0.5)' },
148
+            { offset: 1, color: 'rgba(126, 196, 255, 0.1)' }
149
+          ])
150
+        },
151
+        itemStyle: { color: '#7ec4ff' },
152
+        lineStyle: { color: '#7ec4ff', width: 2 },
153
+        data: props.chartData5.length > 0 ? props.chartData5 : defaultPersonE
154
+      }
155
+    ]
156
+  }
157
+  dailySeizedArea.setOption(option)
158
+}
159
+
160
+const handleResize = () => {
161
+  if (dailySeizedArea) dailySeizedArea.resize()
162
+}
163
+
164
+onMounted(() => {
165
+  nextTick(() => {
166
+    setTimeout(() => {
167
+      initChart()
168
+      window.addEventListener('resize', handleResize)
169
+    }, 100)
170
+  })
171
+})
172
+
173
+onUnmounted(() => {
174
+  window.removeEventListener('resize', handleResize)
175
+  if (dailySeizedArea) dailySeizedArea.dispose()
176
+})
177
+</script>
178
+
179
+<style lang="scss" scoped>
180
+.daily-chart {
181
+  width: 100%;
182
+  height: 320px;
183
+}
184
+</style>

+ 103 - 0
src/views/portraitManagement/components/ProfileDailySeizedBar.vue

@@ -0,0 +1,103 @@
1
+<template>
2
+  <InfoCard title="每日查获数量(总表)">
3
+    <div ref="dailySeizedBarRef" class="daily-bar-chart"></div>
4
+  </InfoCard>
5
+</template>
6
+
7
+<script setup>
8
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
9
+import * as echarts from 'echarts'
10
+import InfoCard from './card.vue'
11
+
12
+const props = defineProps({
13
+  chartData1: {
14
+    type: Array,
15
+    default: () => []
16
+  }
17
+})
18
+
19
+const defaultCategories = ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', '旅检A区', 'T3要客服务区东', 'T3要客服务区西', 'T1要客服务区东', 'T2要客服务区东', '旅检B区', '重检C区']
20
+const defaultData = [120, 200, 150, 80, 70, 110, 130, 90, 160, 140, 180]
21
+
22
+const dailySeizedBarRef = ref(null)
23
+let dailySeizedBar = null
24
+
25
+const initChart = () => {
26
+  if (!dailySeizedBarRef.value) return
27
+  dailySeizedBar = echarts.init(dailySeizedBarRef.value)
28
+  const data = props.chartData1.length > 0 ? props.chartData1 : defaultData
29
+  const option = {
30
+    tooltip: {
31
+      trigger: 'axis',
32
+      backgroundColor: 'rgba(13,80,122,0.95)',
33
+      borderColor: '#70CFE7',
34
+      textStyle: { color: '#fff' }
35
+    },
36
+    legend: {
37
+      data: ['查获数量'],
38
+      textStyle: { color: '#a0c4ff' },
39
+      top: 0
40
+    },
41
+    grid: {
42
+      left: '3%',
43
+      right: '4%',
44
+      bottom: '15%',
45
+      containLabel: true
46
+    },
47
+    xAxis: {
48
+      type: 'category',
49
+      data: defaultCategories,
50
+      axisLabel: {
51
+        color: '#a0c4ff',
52
+        rotate: 30,
53
+        fontSize: 10
54
+      },
55
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
56
+    },
57
+    yAxis: {
58
+      type: 'value',
59
+      axisLabel: { color: '#a0c4ff' },
60
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
61
+      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
62
+    },
63
+    series: [{
64
+      name: '查获数量',
65
+      type: 'bar',
66
+      data: data,
67
+      itemStyle: {
68
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
69
+          { offset: 0, color: '#bd03fb' },
70
+          { offset: 1, color: '#0f46fa' }
71
+        ])
72
+      },
73
+      barWidth: '40%'
74
+    }]
75
+  }
76
+  dailySeizedBar.setOption(option)
77
+}
78
+
79
+const handleResize = () => {
80
+  if (dailySeizedBar) dailySeizedBar.resize()
81
+}
82
+
83
+onMounted(() => {
84
+  nextTick(() => {
85
+    setTimeout(() => {
86
+      initChart()
87
+      window.addEventListener('resize', handleResize)
88
+    }, 100)
89
+  })
90
+})
91
+
92
+onUnmounted(() => {
93
+  window.removeEventListener('resize', handleResize)
94
+  if (dailySeizedBar) dailySeizedBar.dispose()
95
+})
96
+</script>
97
+
98
+<style lang="scss" scoped>
99
+.daily-bar-chart {
100
+  width: 100%;
101
+  height: 380px;
102
+}
103
+</style>

+ 101 - 0
src/views/portraitManagement/components/ProfileDailySeizedLine.vue

@@ -0,0 +1,101 @@
1
+<template>
2
+  <InfoCard title="每日查获数量(总表)">
3
+    <div ref="dailySeizedLineRef" class="daily-chart"></div>
4
+  </InfoCard>
5
+</template>
6
+
7
+<script setup>
8
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
9
+import * as echarts from 'echarts'
10
+import InfoCard from './card.vue'
11
+
12
+const props = defineProps({
13
+  chartData1: {
14
+    type: Array,
15
+    default: () => []
16
+  }
17
+})
18
+
19
+const defaultData = [120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330]
20
+const defaultCategories = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']
21
+
22
+const dailySeizedLineRef = ref(null)
23
+let dailySeizedLine = null
24
+
25
+const initChart = () => {
26
+  if (!dailySeizedLineRef.value) return
27
+  dailySeizedLine = echarts.init(dailySeizedLineRef.value)
28
+  const data = props.chartData1.length > 0 ? props.chartData1 : defaultData
29
+  const option = {
30
+    tooltip: {
31
+      trigger: 'axis',
32
+      backgroundColor: 'rgba(13,80,122,0.95)',
33
+      borderColor: '#70CFE7',
34
+      textStyle: { color: '#fff' }
35
+    },
36
+    legend: {
37
+      data: ['查获数量'],
38
+      textStyle: { color: '#a0c4ff' },
39
+      top: 0
40
+    },
41
+    grid: {
42
+      left: '3%',
43
+      right: '4%',
44
+      bottom: '3%',
45
+      containLabel: true
46
+    },
47
+    xAxis: {
48
+      type: 'category',
49
+      boundaryGap: false,
50
+      data: defaultCategories,
51
+      axisLabel: { color: '#a0c4ff' },
52
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
53
+    },
54
+    yAxis: {
55
+      type: 'value',
56
+      axisLabel: { color: '#a0c4ff' },
57
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
58
+      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
59
+    },
60
+    series: [{
61
+      name: '查获数量',
62
+      type: 'line',
63
+      smooth: true,
64
+      data: data,
65
+      itemStyle: { color: '#bd03fb' },
66
+      areaStyle: {
67
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
68
+          { offset: 0, color: 'rgba(189,3,251,0.3)' },
69
+          { offset: 1, color: 'rgba(189,3,251,0.05)' }
70
+        ])
71
+      }
72
+    }]
73
+  }
74
+  dailySeizedLine.setOption(option)
75
+}
76
+
77
+const handleResize = () => {
78
+  if (dailySeizedLine) dailySeizedLine.resize()
79
+}
80
+
81
+onMounted(() => {
82
+  nextTick(() => {
83
+    setTimeout(() => {
84
+      initChart()
85
+      window.addEventListener('resize', handleResize)
86
+    }, 100)
87
+  })
88
+})
89
+
90
+onUnmounted(() => {
91
+  window.removeEventListener('resize', handleResize)
92
+  if (dailySeizedLine) dailySeizedLine.dispose()
93
+})
94
+</script>
95
+
96
+<style lang="scss" scoped>
97
+.daily-chart {
98
+  width: 100%;
99
+  height: 320px;
100
+}
101
+</style>

+ 43 - 0
src/views/portraitManagement/components/ProfileMembers.vue

@@ -0,0 +1,43 @@
1
+<template>
2
+  <InfoCard title="团队成员">
3
+    <div class="table-container">
4
+      <RollingTable :columns="columns" :data="data" :duration="20" />
5
+    </div>
6
+  </InfoCard>
7
+</template>
8
+
9
+<script setup>
10
+import InfoCard from './card.vue'
11
+import RollingTable from './rollingTable.vue'
12
+
13
+const props = defineProps({
14
+  columns: {
15
+    type: Array,
16
+    default: () => []
17
+  },
18
+  data: {
19
+    type: Array,
20
+    default: () => []
21
+  }
22
+})
23
+</script>
24
+
25
+<style lang="scss" scoped>
26
+.table-container {
27
+  max-height: 400px;
28
+  overflow-y: auto;
29
+
30
+  &::-webkit-scrollbar {
31
+    width: 6px;
32
+  }
33
+
34
+  &::-webkit-scrollbar-track {
35
+    background: rgba(15, 70, 250, 0.1);
36
+  }
37
+
38
+  &::-webkit-scrollbar-thumb {
39
+    background: rgba(15, 70, 250, 0.5);
40
+    border-radius: 3px;
41
+  }
42
+}
43
+</style>

+ 127 - 0
src/views/portraitManagement/components/ProfilePassRate.vue

@@ -0,0 +1,127 @@
1
+<template>
2
+  <InfoCard title="每日通道过检率">
3
+    <div ref="passRateChartRef" class="pass-rate-chart"></div>
4
+  </InfoCard>
5
+</template>
6
+
7
+<script setup>
8
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
9
+import * as echarts from 'echarts'
10
+import InfoCard from './card.vue'
11
+
12
+const props = defineProps({
13
+  chartData1: {
14
+    type: Array,
15
+    default: () => []
16
+  },
17
+  chartData2: {
18
+    type: Array,
19
+    default: () => []
20
+  }
21
+})
22
+
23
+const defaultHours = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`)
24
+const defaultPassData = [370, 352, 330, 387, 426, 342, 325, 384, 445, 407, 382, 401, 380, 464, 342, 443, 305, 394, 334, 430, 364, 362, 400, 420]
25
+const defaultLineData = [4.5, 5, 4, 6, 7, 5, 4, 6, 8, 7, 6, 7, 6, 8, 5, 7, 4, 6, 5, 7, 6, 6, 6.5, 7]
26
+
27
+const passRateChartRef = ref(null)
28
+let passRateChart = null
29
+
30
+const initChart = () => {
31
+  if (!passRateChartRef.value) return
32
+  passRateChart = echarts.init(passRateChartRef.value)
33
+
34
+  const passData = props.chartData1.length > 0 ? props.chartData1 : defaultPassData
35
+  const lineData = props.chartData2.length > 0 ? props.chartData2 : defaultLineData
36
+
37
+  const option = {
38
+    xAxis: {
39
+      type: 'category',
40
+      data: defaultHours,
41
+      axisLabel: { color: '#a0c4ff', fontSize: 10 },
42
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
43
+    },
44
+    yAxis: [
45
+      {
46
+        type: 'value',
47
+        name: '过检率',
48
+        position: 'left',
49
+        max: 8,
50
+        axisLabel: { color: '#a0c4ff', fontSize: 10 },
51
+        axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
52
+        splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
53
+      },
54
+      {
55
+        type: 'value',
56
+        name: '数量',
57
+        position: 'right',
58
+        max: 5000,
59
+        axisLabel: { color: '#a0c4ff', fontSize: 10 },
60
+        axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
61
+        splitLine: { show: false }
62
+      }
63
+    ],
64
+    series: [
65
+      {
66
+        name: '过检人数',
67
+        type: 'bar',
68
+        yAxisIndex: 1,
69
+        data: passData,
70
+        itemStyle: {
71
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
72
+            { offset: 0, color: '#ffd93d' },
73
+            { offset: 1, color: '#ff9f43' }
74
+          ])
75
+        },
76
+        barWidth: '40%'
77
+      },
78
+      {
79
+        name: '过检率',
80
+        type: 'line',
81
+        yAxisIndex: 0,
82
+        data: lineData,
83
+        smooth: true,
84
+        itemStyle: { color: '#bd03fb' },
85
+        areaStyle: {
86
+          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
87
+            { offset: 0, color: 'rgba(189,3,251,0.3)' },
88
+            { offset: 1, color: 'rgba(189,3,251,0.05)' }
89
+          ])
90
+        }
91
+      }
92
+    ],
93
+    tooltip: {
94
+      trigger: 'axis',
95
+      backgroundColor: 'rgba(13,80,122,0.95)',
96
+      borderColor: '#70CFE7',
97
+      textStyle: { color: '#fff' }
98
+    }
99
+  }
100
+  passRateChart.setOption(option)
101
+}
102
+
103
+const handleResize = () => {
104
+  if (passRateChart) passRateChart.resize()
105
+}
106
+
107
+onMounted(() => {
108
+  nextTick(() => {
109
+    setTimeout(() => {
110
+      initChart()
111
+      window.addEventListener('resize', handleResize)
112
+    }, 100)
113
+  })
114
+})
115
+
116
+onUnmounted(() => {
117
+  window.removeEventListener('resize', handleResize)
118
+  if (passRateChart) passRateChart.dispose()
119
+})
120
+</script>
121
+
122
+<style lang="scss" scoped>
123
+.pass-rate-chart {
124
+  width: 100%;
125
+  height: 320px;
126
+}
127
+</style>

+ 160 - 0
src/views/portraitManagement/components/ProfilePositionDistribution.vue

@@ -0,0 +1,160 @@
1
+<template>
2
+  <InfoCard title="成员职位情况分布">
3
+    <div class="position-distribution">
4
+      <div class="distribution-item">
5
+        <div ref="skillChartRef" class="distribution-chart"></div>
6
+        <div class="distribution-label">职业资格等级分布</div>
7
+      </div>
8
+      <div class="distribution-item">
9
+        <div ref="operateChartRef" class="distribution-chart"></div>
10
+        <div class="distribution-label">开机年限分布</div>
11
+      </div>
12
+      <div class="distribution-item">
13
+        <div ref="postChartRef" class="distribution-chart"></div>
14
+        <div class="distribution-label">岗位资质分布</div>
15
+      </div>
16
+    </div>
17
+  </InfoCard>
18
+</template>
19
+
20
+<script setup>
21
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
22
+import * as echarts from 'echarts'
23
+import InfoCard from './card.vue'
24
+
25
+const props = defineProps({
26
+  chartData1: {
27
+    type: Array,
28
+    default: () => []
29
+  },
30
+  chartData2: {
31
+    type: Array,
32
+    default: () => []
33
+  },
34
+  chartData3: {
35
+    type: Array,
36
+    default: () => []
37
+  }
38
+})
39
+
40
+const defaultSkillData = {
41
+  categories: ['等级1', '等级2', '等级3', '等级4', '等级5'],
42
+  values: [3, 5, 7, 8, 4],
43
+  colors: ['#4da6ff', '#0f46fa']
44
+}
45
+
46
+const defaultOperateData = {
47
+  categories: ['0-3', '4-7', '8-11', '12-15', '15-18'],
48
+  values: [4, 2, 3, 5, 8],
49
+  colors: ['#6bcb77', '#2ecc71']
50
+}
51
+
52
+const defaultPostData = {
53
+  categories: ['前传', '人身', '验证', '开包', '开机'],
54
+  values: [4, 5, 6, 7, 8],
55
+  colors: ['#ff6b6b', '#ee5a24']
56
+}
57
+
58
+const skillChartRef = ref(null)
59
+let skillChart = null
60
+const operateChartRef = ref(null)
61
+let operateChart = null
62
+const postChartRef = ref(null)
63
+let postChart = null
64
+
65
+const initBarChart = (ref, chartData) => {
66
+  if (!ref) return
67
+  const chart = echarts.init(ref)
68
+  const option = {
69
+    xAxis: {
70
+      type: 'category',
71
+      data: chartData.categories,
72
+      axisLabel: { color: '#a0c4ff', fontSize: 10 },
73
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
74
+    },
75
+    yAxis: {
76
+      type: 'value',
77
+      axisLabel: { color: '#a0c4ff', fontSize: 10 },
78
+      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
79
+      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
80
+    },
81
+    series: [{
82
+      type: 'bar',
83
+      data: chartData.values,
84
+      itemStyle: {
85
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
86
+          { offset: 0, color: chartData.colors[0] },
87
+          { offset: 1, color: chartData.colors[1] }
88
+        ])
89
+      },
90
+      barWidth: '50%'
91
+    }]
92
+  }
93
+  chart.setOption(option)
94
+  return chart
95
+}
96
+
97
+const initCharts = () => {
98
+  const skillData = props.chartData1.length > 0 ? props.chartData1 : defaultSkillData
99
+  const operateData = props.chartData2.length > 0 ? props.chartData2 : defaultOperateData
100
+  const postData = props.chartData3.length > 0 ? props.chartData3 : defaultPostData
101
+
102
+  skillChart = initBarChart(skillChartRef.value, skillData)
103
+  operateChart = initBarChart(operateChartRef.value, operateData)
104
+  postChart = initBarChart(postChartRef.value, postData)
105
+}
106
+
107
+const handleResize = () => {
108
+  if (skillChart) skillChart.resize()
109
+  if (operateChart) operateChart.resize()
110
+  if (postChart) postChart.resize()
111
+}
112
+
113
+onMounted(() => {
114
+  nextTick(() => {
115
+    setTimeout(() => {
116
+      initCharts()
117
+      window.addEventListener('resize', handleResize)
118
+    }, 100)
119
+  })
120
+})
121
+
122
+onUnmounted(() => {
123
+  window.removeEventListener('resize', handleResize)
124
+  if (skillChart) skillChart.dispose()
125
+  if (operateChart) operateChart.dispose()
126
+  if (postChart) postChart.dispose()
127
+})
128
+</script>
129
+
130
+<style lang="scss" scoped>
131
+.position-distribution {
132
+  display: flex;
133
+  justify-content: space-around;
134
+  align-items: stretch;
135
+  min-height: 280px;
136
+  gap: 15px;
137
+  padding: 10px 0;
138
+
139
+  .distribution-item {
140
+    flex: 1;
141
+    display: flex;
142
+    flex-direction: column;
143
+    align-items: center;
144
+    justify-content: center;
145
+    min-width: 0;
146
+
147
+    .distribution-chart {
148
+      width: 100%;
149
+      height: 200px;
150
+    }
151
+
152
+    .distribution-label {
153
+      color: #a0c4ff;
154
+      font-size: 12px;
155
+      margin-top: 10px;
156
+      text-align: center;
157
+    }
158
+  }
159
+}
160
+</style>

+ 253 - 0
src/views/portraitManagement/components/ProfileRadar.vue

@@ -0,0 +1,253 @@
1
+<template>
2
+  <InfoCard title="七维得分一览">
3
+    <div class="radar-section">
4
+      <div ref="radarChartRef" class="radar-chart"></div>
5
+      <div class="radar-list">
6
+        <div class="radar-item" v-for="(item, index) in computedRadarData" :key="index">
7
+          <span class="item-label">{{ item.name }}</span>
8
+          <div class="progress-row">
9
+            <div class="progress-bar">
10
+              <div class="progress-fill" :style="{ width: item.value + '%', background: `linear-gradient(to right, transparent, ${item.color})` }">
11
+                <span class="progress-end"></span>
12
+              </div>
13
+            </div>
14
+            <span class="item-value">{{ item.value }}</span>
15
+          </div>
16
+        </div>
17
+      </div>
18
+    </div>
19
+  </InfoCard>
20
+</template>
21
+
22
+<script setup>
23
+import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
24
+import * as echarts from 'echarts'
25
+import InfoCard from './card.vue'
26
+
27
+const props = defineProps({
28
+  chartData1: {
29
+    type: Array,
30
+    default: () => []
31
+  },
32
+  chartData2: {
33
+    type: Array,
34
+    default: () => []
35
+  },
36
+  chartData3: {
37
+    type: Array,
38
+    default: () => []
39
+  },
40
+  chartData4: {
41
+    type: Array,
42
+    default: () => []
43
+  }
44
+})
45
+
46
+const defaultRadarData = [
47
+  { name: '通道安全防控力', value: 86, color: '#00e5ff' },
48
+  { name: '通道安全防控力', value: 90, color: '#00e5ff' },
49
+  { name: '通道安全防控力', value: 72, color: '#ff4757' },
50
+  { name: '通道安全防控力', value: 68, color: '#ff4757' },
51
+  { name: '通道安全防控力', value: 78, color: '#3742fa' },
52
+  { name: '通道安全防控力', value: 80, color: '#3742fa' },
53
+  { name: '通道协同作战能力', value: 72, color: '#ff4757' }
54
+]
55
+
56
+const defaultIndicators = [
57
+  { name: '通道安全防控力', max: 100 },
58
+  { name: '通道安全防控力', max: 100 },
59
+  { name: '通道安全防控力', max: 100 },
60
+  { name: '通道安全防控力', max: 100 },
61
+  { name: '通道安全防控力', max: 100 },
62
+  { name: '通道安全防控力', max: 100 },
63
+  { name: '通道协同作战能力', max: 100 }
64
+]
65
+
66
+const defaultSeries1 = [86, 90, 72, 68, 78, 80, 72]
67
+const defaultSeries2 = [80, 85, 65, 60, 72, 75, 68]
68
+
69
+const computedRadarData = computed(() => {
70
+  return props.chartData1.length > 0 ? props.chartData1 : defaultRadarData
71
+})
72
+
73
+const radarChartRef = ref(null)
74
+let radarChart = null
75
+
76
+const initRadarChart = () => {
77
+  if (!radarChartRef.value) return
78
+
79
+  radarChart = echarts.init(radarChartRef.value)
80
+
81
+  const indicators = props.chartData2.length > 0 ? props.chartData2 : defaultIndicators
82
+  const series1 = props.chartData3.length > 0 ? props.chartData3 : defaultSeries1
83
+  const series2 = props.chartData4.length > 0 ? props.chartData4 : defaultSeries2
84
+
85
+  const option = {
86
+    radar: {
87
+      indicator: indicators,
88
+      center: ['50%', '50%'],
89
+      radius: '70%',
90
+      splitNumber: 4,
91
+      axisLine: {
92
+        lineStyle: {
93
+          color: 'rgba(15, 70, 250, 0.3)'
94
+        }
95
+      },
96
+      splitArea: {
97
+        areaStyle: {
98
+          color: ['rgba(15, 70, 250, 0.05)', 'rgba(15, 70, 250, 0.1)', 'rgba(15, 70, 250, 0.15)', 'rgba(15, 70, 250, 0.2)']
99
+        }
100
+      },
101
+      splitLine: {
102
+        lineStyle: {
103
+          color: 'rgba(15, 70, 250, 0.2)'
104
+        }
105
+      },
106
+      name: {
107
+        textStyle: {
108
+          color: '#a0c4ff',
109
+          fontSize: 11
110
+        }
111
+      }
112
+    },
113
+    series: [
114
+      {
115
+        type: 'radar',
116
+        data: [
117
+          {
118
+            value: series1,
119
+            name: '综合得分',
120
+            areaStyle: {
121
+              color: 'rgba(189, 3, 251, 0.3)'
122
+            },
123
+            lineStyle: {
124
+              color: '#bd03fb',
125
+              width: 2
126
+            },
127
+            itemStyle: {
128
+              color: '#bd03fb'
129
+            }
130
+          },
131
+          {
132
+            value: series2,
133
+            name: '基准值',
134
+            areaStyle: {
135
+              color: 'rgba(15, 70, 250, 0.2)'
136
+            },
137
+            lineStyle: {
138
+              color: '#0f46fa',
139
+              width: 2
140
+            },
141
+            itemStyle: {
142
+              color: '#0f46fa'
143
+            }
144
+          }
145
+        ],
146
+        symbol: 'circle',
147
+        symbolSize: 6
148
+      }
149
+    ],
150
+    tooltip: {
151
+      trigger: 'item',
152
+      backgroundColor: 'rgba(13, 80, 122, 0.95)',
153
+      borderColor: '#70CFE7',
154
+      textStyle: {
155
+        color: '#fff'
156
+      }
157
+    }
158
+  }
159
+
160
+  radarChart.setOption(option)
161
+}
162
+
163
+const handleResize = () => {
164
+  if (radarChart) radarChart.resize()
165
+}
166
+
167
+onMounted(() => {
168
+  nextTick(() => {
169
+    setTimeout(() => {
170
+      initRadarChart()
171
+      window.addEventListener('resize', handleResize)
172
+    }, 100)
173
+  })
174
+})
175
+
176
+onUnmounted(() => {
177
+  window.removeEventListener('resize', handleResize)
178
+  if (radarChart) radarChart.dispose()
179
+})
180
+</script>
181
+
182
+<style lang="scss" scoped>
183
+.radar-section {
184
+  display: flex;
185
+  gap: 20px;
186
+  min-height: 400px;
187
+
188
+  .radar-chart {
189
+    flex: 1;
190
+    min-width: 300px;
191
+    height: 380px;
192
+  }
193
+
194
+  .radar-list {
195
+    flex: 1;
196
+    display: flex;
197
+    flex-direction: column;
198
+    gap: 16px;
199
+    padding: 10px 0;
200
+
201
+    .radar-item {
202
+      display: flex;
203
+      flex-direction: column;
204
+      gap: 6px;
205
+      font-size: 13px;
206
+
207
+      .item-label {
208
+        color: #a0c4ff;
209
+      }
210
+
211
+      .progress-row {
212
+        display: flex;
213
+        align-items: center;
214
+        gap: 10px;
215
+
216
+        .progress-bar {
217
+          flex: 1;
218
+          height: 5px;
219
+          background: rgba(255, 255, 255, 0.1);
220
+          border-radius: 0;
221
+          overflow: visible;
222
+
223
+          .progress-fill {
224
+            height: 100%;
225
+            border-radius: 0;
226
+            transition: width 0.3s ease;
227
+            position: relative;
228
+
229
+            .progress-end {
230
+              position: absolute;
231
+              right: -4px;
232
+              top: 50%;
233
+              transform: translateY(-50%);
234
+              width: 3px;
235
+              height: 9px;
236
+              background: #fff;
237
+              border-radius: 2px;
238
+            }
239
+          }
240
+        }
241
+
242
+        .item-value {
243
+          width: 45px;
244
+          text-align: right;
245
+          color: #fff;
246
+          font-weight: bold;
247
+          font-size: 13px;
248
+        }
249
+      }
250
+    }
251
+  }
252
+}
253
+</style>

+ 75 - 0
src/views/portraitManagement/components/ProfileSeizedDistribution.vue

@@ -0,0 +1,75 @@
1
+<template>
2
+  <InfoCard title="查获物品分布">
3
+    <div class="seized-distribution">
4
+      <div ref="seizedChartRef" class="seized-chart"></div>
5
+    </div>
6
+  </InfoCard>
7
+</template>
8
+
9
+<script setup>
10
+import { ref, onMounted, onUnmounted, nextTick } from 'vue'
11
+import * as echarts from 'echarts'
12
+import InfoCard from './card.vue'
13
+
14
+const props = defineProps({
15
+  chartData1: {
16
+    type: Array,
17
+    default: () => []
18
+  }
19
+})
20
+
21
+const defaultData = [
22
+  { value: 20, name: '打火机', itemStyle: { color: '#ff9f43' } },
23
+  { value: 30, name: '管制刀具', itemStyle: { color: '#ff6b6b' } },
24
+  { value: 50, name: '其他违禁品', itemStyle: { color: '#4da6ff' } }
25
+]
26
+
27
+const seizedChartRef = ref(null)
28
+let seizedChart = null
29
+
30
+const initChart = () => {
31
+  if (!seizedChartRef.value) return
32
+  seizedChart = echarts.init(seizedChartRef.value)
33
+  const data = props.chartData1.length > 0 ? props.chartData1 : defaultData
34
+  const option = {
35
+    series: [{
36
+      type: 'pie',
37
+      radius: ['40%', '70%'],
38
+      data: data,
39
+      label: { show: true, color: '#fff', fontSize: 11 },
40
+      labelLine: { show: true }
41
+    }]
42
+  }
43
+  seizedChart.setOption(option)
44
+}
45
+
46
+const handleResize = () => {
47
+  if (seizedChart) seizedChart.resize()
48
+}
49
+
50
+onMounted(() => {
51
+  nextTick(() => {
52
+    setTimeout(() => {
53
+      initChart()
54
+      window.addEventListener('resize', handleResize)
55
+    }, 100)
56
+  })
57
+})
58
+
59
+onUnmounted(() => {
60
+  window.removeEventListener('resize', handleResize)
61
+  if (seizedChart) seizedChart.dispose()
62
+})
63
+</script>
64
+
65
+<style lang="scss" scoped>
66
+.seized-distribution {
67
+  width: 100%;
68
+  height: 280px;
69
+
70
+  .seized-chart {
71
+    width: 100%;
72
+    height: 100%;
73
+  }
74
+}
75
+</style>

+ 137 - 0
src/views/portraitManagement/components/ProfileSeizedInfo.vue

@@ -0,0 +1,137 @@
1
+<template>
2
+  
3
+  <InfoCard title="查获信息展示">
4
+    <div class="seized-info">
5
+      <div class="seized-number">
6
+        <div class="number-circle">
7
+          <div class="number-value">{{ displayNumber }}</div>
8
+          <div class="number-label">查获总数(所有人员)</div>
9
+        </div>
10
+      </div>
11
+      <div class="seized-small-chart">
12
+        <div ref="seizedSmallChartRef" class="small-chart"></div>
13
+      </div>
14
+    </div>
15
+  </InfoCard>
16
+</template>
17
+
18
+<script setup>
19
+import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
20
+import * as echarts from 'echarts'
21
+import InfoCard from './card.vue'
22
+
23
+const props = defineProps({
24
+  chartData1: {
25
+    type: [Number, Array],
26
+    default: 1658
27
+  }
28
+})
29
+
30
+const displayNumber = computed(() => {
31
+  return typeof props.chartData1 === 'number' ? props.chartData1 : 1658
32
+})
33
+
34
+const seizedSmallChartRef = ref(null)
35
+let seizedSmallChart = null
36
+
37
+const initChart = () => {
38
+  if (!seizedSmallChartRef.value) return
39
+  seizedSmallChart = echarts.init(seizedSmallChartRef.value)
40
+  const option = {
41
+    xAxis: {
42
+      type: 'category',
43
+      data: ['等级1', '等级3', '等级5'],
44
+      axisLabel: { color: '#a0c4ff', fontSize: 9 },
45
+      axisLine: { show: false },
46
+      axisTick: { show: false }
47
+    },
48
+    yAxis: {
49
+      type: 'value',
50
+      axisLabel: { show: false },
51
+      axisLine: { show: false },
52
+      axisTick: { show: false },
53
+      splitLine: { show: false }
54
+    },
55
+    series: [{
56
+      type: 'bar',
57
+      data: [150, 400, 200],
58
+      itemStyle: {
59
+        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
60
+          { offset: 0, color: '#a55eea' },
61
+          { offset: 1, color: '#4da6ff' }
62
+        ])
63
+      },
64
+      barWidth: '30%'
65
+    }]
66
+  }
67
+  seizedSmallChart.setOption(option)
68
+}
69
+
70
+const handleResize = () => {
71
+  if (seizedSmallChart) seizedSmallChart.resize()
72
+}
73
+
74
+onMounted(() => {
75
+  nextTick(() => {
76
+    setTimeout(() => {
77
+      initChart()
78
+      window.addEventListener('resize', handleResize)
79
+    }, 100)
80
+  })
81
+})
82
+
83
+onUnmounted(() => {
84
+  window.removeEventListener('resize', handleResize)
85
+  if (seizedSmallChart) seizedSmallChart.dispose()
86
+})
87
+</script>
88
+
89
+<style lang="scss" scoped>
90
+.seized-info {
91
+  display: flex;
92
+  gap: 20px;
93
+  min-height: 280px;
94
+  align-items: center;
95
+
96
+  .seized-number {
97
+    flex: 1;
98
+    display: flex;
99
+    justify-content: center;
100
+
101
+    .number-circle {
102
+      width: 200px;
103
+      height: 200px;
104
+      border-radius: 50%;
105
+      background: linear-gradient(135deg, rgba(15, 70, 250, 0.3), rgba(189, 3, 251, 0.3));
106
+      display: flex;
107
+      flex-direction: column;
108
+      align-items: center;
109
+      justify-content: center;
110
+      border: 2px solid rgba(189, 3, 251, 0.5);
111
+
112
+      .number-value {
113
+        font-size: 48px;
114
+        font-weight: bold;
115
+        color: #fff;
116
+        text-shadow: 0 0 20px rgba(189, 3, 251, 0.8);
117
+      }
118
+
119
+      .number-label {
120
+        font-size: 14px;
121
+        color: #a0c4ff;
122
+        text-align: center;
123
+        margin-top: 10px;
124
+      }
125
+    }
126
+  }
127
+
128
+  .seized-small-chart {
129
+    flex: 1;
130
+
131
+    .small-chart {
132
+      width: 100%;
133
+      height: 200px;
134
+    }
135
+  }
136
+}
137
+</style>

+ 209 - 0
src/views/portraitManagement/deptProfile/component/profile.vue

@@ -0,0 +1,209 @@
1
+<template>
2
+  <div class="main-content-wrapper">
3
+    <div class="main-content">
4
+      <div class="content-row">
5
+        <ProfileRadar
6
+          :chartData1="radarData"
7
+          :chartData2="radarIndicators"
8
+          :chartData3="radarSeries1"
9
+          :chartData4="radarSeries2"
10
+        />
11
+        <ProfileMembers
12
+          :columns="memberColumns"
13
+          :data="teamMembers"
14
+        />
15
+      </div>
16
+
17
+      <div class="content-row">
18
+        <ProfileBasicDistribution
19
+          :chartData1="genderData"
20
+          :chartData2="nationData"
21
+          :chartData3="politicalData"
22
+        />
23
+        <ProfilePositionDistribution
24
+          :chartData1="skillData"
25
+          :chartData2="operateData"
26
+          :chartData3="postData"
27
+        />
28
+      </div>
29
+
30
+      <div class="content-row">
31
+        <ProfilePassRate
32
+          :chartData1="passRatePassData"
33
+          :chartData2="passRateLineData"
34
+        />
35
+      </div>
36
+
37
+      <div class="content-row">
38
+        <ProfileSeizedInfo :chartData1="seizedTotal" />
39
+        <ProfileSeizedDistribution :chartData1="seizedDistributionData" />
40
+      </div>
41
+
42
+      <div class="content-row">
43
+        <ProfileDailySeizedLine :chartData1="dailyLineData" />
44
+        <ProfileDailySeizedArea
45
+          :chartData1="dailyAreaPersonA"
46
+          :chartData2="dailyAreaPersonB"
47
+          :chartData3="dailyAreaPersonC"
48
+          :chartData4="dailyAreaPersonD"
49
+          :chartData5="dailyAreaPersonE"
50
+        />
51
+      </div>
52
+
53
+      <div class="content-row">
54
+        <ProfileDailySeizedBar :chartData1="dailyBarData" />
55
+      </div>
56
+    </div>
57
+  </div>
58
+</template>
59
+
60
+<script setup>
61
+import { ref, watch } from 'vue'
62
+import ProfileRadar from '../../components/ProfileRadar.vue'
63
+import ProfileMembers from '../../components/ProfileMembers.vue'
64
+import ProfileBasicDistribution from '../../components/ProfileBasicDistribution.vue'
65
+import ProfilePositionDistribution from '../../components/ProfilePositionDistribution.vue'
66
+import ProfilePassRate from '../../components/ProfilePassRate.vue'
67
+import ProfileSeizedInfo from '../../components/ProfileSeizedInfo.vue'
68
+import ProfileSeizedDistribution from '../../components/ProfileSeizedDistribution.vue'
69
+import ProfileDailySeizedLine from '../../components/ProfileDailySeizedLine.vue'
70
+import ProfileDailySeizedArea from '../../components/ProfileDailySeizedArea.vue'
71
+import ProfileDailySeizedBar from '../../components/ProfileDailySeizedBar.vue'
72
+
73
+const props = defineProps({
74
+  queryParams: {
75
+    type: Object,
76
+    default: () => ({})
77
+  }
78
+})
79
+
80
+const radarData = ref([
81
+  { name: '通道安全防控力', value: 86, color: '#00e5ff' },
82
+  { name: '通道安全防控力', value: 90, color: '#00e5ff' },
83
+  { name: '通道安全防控力', value: 72, color: '#ff4757' },
84
+  { name: '通道安全防控力', value: 68, color: '#ff4757' },
85
+  { name: '通道安全防控力', value: 78, color: '#3742fa' },
86
+  { name: '通道安全防控力', value: 80, color: '#3742fa' },
87
+  { name: '通道协同作战能力', value: 72, color: '#ff4757' }
88
+])
89
+
90
+const radarIndicators = ref([
91
+  { name: '通道安全防控力', max: 100 },
92
+  { name: '通道安全防控力', max: 100 },
93
+  { name: '通道安全防控力', max: 100 },
94
+  { name: '通道安全防控力', max: 100 },
95
+  { name: '通道安全防控力', max: 100 },
96
+  { name: '通道安全防控力', max: 100 },
97
+  { name: '通道协同作战能力', max: 100 }
98
+])
99
+
100
+const radarSeries1 = ref([86, 90, 72, 68, 78, 80, 72])
101
+const radarSeries2 = ref([80, 85, 65, 60, 72, 75, 68])
102
+
103
+const teamMembers = ref([
104
+  { name: '孙晓波', age: 25, seniority: 3, gender: '男', nation: '汉', political: '群众', position: '安检员', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 5, avgYears: 6, totalScore: 88 },
105
+  { name: '孙晓波', age: 28, seniority: 6, gender: '男', nation: '汉', political: '群众', position: '组长', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 5, avgYears: 5, totalScore: 85 },
106
+  { name: '孙晓波', age: 31, seniority: 2, gender: '女', nation: '汉', political: '党员', position: '组长', qualification: '前传、特检、人身', skillLevel: '四级', operateYears: 5, avgYears: 76, totalScore: 76 },
107
+  { name: '马力', age: 26, seniority: 3, gender: '男', nation: '汉', political: '群众', position: '组长', qualification: '前传、特检、人身', skillLevel: '五级', operateYears: 5, avgYears: 81, totalScore: 81 },
108
+  { name: '马建国', age: 24, seniority: 5, gender: '男', nation: '汉', political: '党员', position: '安检员', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 6, avgYears: 78, totalScore: 78 }
109
+])
110
+
111
+const memberColumns = ref([
112
+  { label: '姓名', prop: 'name' },
113
+  { label: '年龄', prop: 'age' },
114
+  { label: '司龄', prop: 'seniority' },
115
+  { label: '性别', prop: 'gender' },
116
+  { label: '民族', prop: 'nation' },
117
+  { label: '政治面貌', prop: 'political' },
118
+  { label: '职务', prop: 'position' },
119
+  { label: '岗位资质', prop: 'qualification' },
120
+  { label: '职业技能等级', prop: 'skillLevel' },
121
+  { label: '开机年限', prop: 'operateYears' },
122
+  { label: '平均年限', prop: 'avgYears' },
123
+  { label: '综合得分', prop: 'totalScore' }
124
+])
125
+
126
+const genderData = ref([
127
+  { value: 6, name: '女', itemStyle: { color: '#ff6b9d' } },
128
+  { value: 8, name: '男', itemStyle: { color: '#4da6ff' } }
129
+])
130
+
131
+const nationData = ref([
132
+  { value: 1, name: '回族', itemStyle: { color: '#ff9f43' } },
133
+  { value: 8, name: '汉族', itemStyle: { color: '#4da6ff' } },
134
+  { value: 5, name: '其他', itemStyle: { color: '#a55eea' } }
135
+])
136
+
137
+const politicalData = ref([
138
+  { value: 1, name: '党员', itemStyle: { color: '#ff6b6b' } },
139
+  { value: 2, name: '共青团员', itemStyle: { color: '#ffd93d' } },
140
+  { value: 5, name: '群众', itemStyle: { color: '#6bcb77' } },
141
+  { value: 6, name: '其他', itemStyle: { color: '#4d96ff' } }
142
+])
143
+
144
+const skillData = ref({
145
+  categories: ['等级1', '等级2', '等级3', '等级4', '等级5'],
146
+  values: [3, 5, 7, 8, 4],
147
+  colors: ['#4da6ff', '#0f46fa']
148
+})
149
+
150
+const operateData = ref({
151
+  categories: ['0-3', '4-7', '8-11', '12-15', '15-18'],
152
+  values: [4, 2, 3, 5, 8],
153
+  colors: ['#6bcb77', '#2ecc71']
154
+})
155
+
156
+const postData = ref({
157
+  categories: ['前传', '人身', '验证', '开包', '开机'],
158
+  values: [4, 5, 6, 7, 8],
159
+  colors: ['#ff6b6b', '#ee5a24']
160
+})
161
+
162
+const passRatePassData = ref([370, 352, 330, 387, 426, 342, 325, 384, 445, 407, 382, 401, 380, 464, 342, 443, 305, 394, 334, 430, 364, 362, 400, 420])
163
+const passRateLineData = ref([4.5, 5, 4, 6, 7, 5, 4, 6, 8, 7, 6, 7, 6, 8, 5, 7, 4, 6, 5, 7, 6, 6, 6.5, 7])
164
+
165
+const seizedTotal = ref(1658)
166
+
167
+const seizedDistributionData = ref([
168
+  { value: 20, name: '打火机', itemStyle: { color: '#ff9f43' } },
169
+  { value: 30, name: '管制刀具', itemStyle: { color: '#ff6b6b' } },
170
+  { value: 50, name: '其他违禁品', itemStyle: { color: '#4da6ff' } }
171
+])
172
+
173
+const dailyLineData = ref([120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330])
174
+const dailyAreaPersonA = ref([120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330])
175
+const dailyAreaPersonB = ref([220, 182, 191, 234, 290, 330, 310, 123, 144, 210, 182, 191])
176
+const dailyAreaPersonC = ref([150, 232, 201, 154, 190, 330, 410, 201, 154, 190, 330, 410])
177
+const dailyAreaPersonD = ref([320, 332, 301, 334, 390, 330, 320, 332, 301, 334, 390, 330])
178
+const dailyAreaPersonE = ref([820, 932, 901, 934, 1290, 1330, 1320, 820, 932, 901, 934, 1290])
179
+const dailyBarData = ref([120, 200, 150, 80, 70, 110, 130, 90, 160, 140, 180])
180
+
181
+watch(() => props.queryParams, (newParams) => {
182
+  fetchData(newParams)
183
+}, { deep: true })
184
+
185
+const fetchData = (params) => {
186
+  console.log('查询参数:', params)
187
+}
188
+</script>
189
+
190
+<style lang="scss" scoped>
191
+.main-content-wrapper {
192
+  .main-content {
193
+    display: flex;
194
+    flex-direction: column;
195
+    gap: 20px;
196
+    padding: 0 20px 40px;
197
+  }
198
+
199
+  .content-row {
200
+    display: flex;
201
+    gap: 20px;
202
+
203
+    > .info-card {
204
+      flex: 1;
205
+      min-width: 0;
206
+    }
207
+  }
208
+}
209
+</style>

+ 143 - 0
src/views/portraitManagement/deptProfile/component/runData.vue

@@ -0,0 +1,143 @@
1
+<template>
2
+  <div class="operation-data">
3
+    <div class="row">
4
+      <ChannelCheckChart :chartsData="chartsData" />
5
+    </div>
6
+    <div class="row">
7
+      <div class="col">
8
+        <SeizedInfo :chartsData="chartsData"/>
9
+      </div>
10
+      <div class="col">
11
+        <SeizedItems :chartsData="chartsData" />
12
+      </div>
13
+    </div>
14
+    <div class="row">
15
+      <div class="col">
16
+        <SeizedNumAll :chartsData="chartsData"/>
17
+      </div>
18
+      <div class="col">
19
+        <SeizedNum :chartsData="chartsData" />
20
+      </div>
21
+    </div>
22
+    <div class="row">
23
+      <SeizedWorkArea :chartsData="chartsData" />
24
+    </div>
25
+    <div class="row">
26
+      <div class="col">
27
+        <EventItems :chartsData="chartsData"  />
28
+      </div>
29
+      <div class="col">
30
+        <EventType :chartsData="chartsData"  />
31
+      </div>
32
+      <div class="col">
33
+        <EventWorkArea :chartsData="chartsData"  />
34
+      </div>
35
+    </div>
36
+    <div class="row">
37
+      <div class="col">
38
+        <TestItems :chartsData="chartsData"  />
39
+      </div>
40
+      <div class="col">
41
+        <TestResult :chartsData="chartsData"  />
42
+      </div>
43
+      <div class="col">
44
+        <TestArea :chartsData="chartsData"  />
45
+      </div>
46
+    </div>
47
+    <div class="row">
48
+      <div class="col">
49
+        <SupervisionDistribution :chartsData="chartsData"/>
50
+      </div>
51
+      <div class="col">
52
+        <InterceptionDistribution :chartsData="chartsData" />
53
+      </div>
54
+    </div>
55
+  </div>
56
+</template>
57
+
58
+<script setup>
59
+import ChannelCheckChart from '../../components/ChannelCheckChart.vue';
60
+import SeizedInfo from '../../components/SeizedInfo.vue';
61
+import SeizedItems from '../../components/SeizedItems.vue';
62
+import SeizedNumAll from '../../components/SeizedNumAll.vue';
63
+import SeizedNum from '../../components/SeizedNum.vue';
64
+import SeizedWorkArea from '../../components/SeizedWorkArea.vue';
65
+import EventItems from '../../components/EventItems.vue';
66
+import EventType from '../../components/EventType.vue';
67
+import EventWorkArea from '../../components/EventWorkArea.vue';
68
+import TestItems from '../../components/TestItems.vue';
69
+import TestResult from '../../components/TestResult.vue';
70
+import TestArea from '../../components/TestArea.vue';
71
+import SupervisionDistribution from '../../components/SupervisionDistribution.vue';
72
+import InterceptionDistribution from '../../components/InterceptionDistribution.vue';
73
+
74
+
75
+const chartsData = ref([
76
+  {
77
+    num: 370,
78
+    num1: 330,
79
+    num2: 120,
80
+    num3: 470,
81
+    num4: 570,
82
+    rate: 30,
83
+    name: '打火机'
84
+  },
85
+  {
86
+    num: 330,
87
+    num1: 430,
88
+    num2: 420,
89
+    num3: 430,
90
+    num4: 150,
91
+    rate: 60,
92
+    name: '火柴'
93
+  },
94
+  {
95
+    num: 410,
96
+    num1: 310,
97
+    num2: 220,
98
+    num3: 370,
99
+    num4: 510,
100
+    rate: 35,
101
+    name: '刀具'
102
+  },
103
+  {
104
+    num: 570,
105
+    num1: 440,
106
+    num2: 420,
107
+    num3: 410,
108
+    num4: 470,
109
+    rate: 80,
110
+    name: '充电宝'
111
+  },
112
+  {
113
+    num: 340,
114
+    num1: 330,
115
+    num2: 620,
116
+    num3: 370,
117
+    num4: 170,
118
+    rate: 60,
119
+    name: '烟火'
120
+  },
121
+])
122
+  
123
+
124
+</script>
125
+
126
+<style lang="scss" scoped>
127
+.operation-data {
128
+  padding: 20px;
129
+  .row {
130
+    display: flex;
131
+    column-gap: 15px;
132
+    height: 360px;
133
+    margin-bottom: 15px;
134
+    .col {
135
+      flex: 1;
136
+      height: 100%;
137
+      :deep(.info-card) {
138
+        height: 100%;
139
+      }
140
+    }
141
+  }
142
+}
143
+</style>

+ 173 - 2
src/views/portraitManagement/deptProfile/index.vue

@@ -1,3 +1,174 @@
1 1
 <template>
2
-    
3
-</template>
2
+  <div class="group-profile-page">
3
+    <Page title="安检人事管理可视化大屏" :tabs="['能力画像', '运行数据']" @tab-change="handleTabChange">
4
+      <SearchBar v-model:visible="searchVisible" @search="handleSearch" />
5
+      <PentagonGroup :items="pentagonItems" />
6
+      <Profile v-if="activeTab === 0" :query-params="queryParams" />
7
+      <RunData v-else />
8
+    </Page>
9
+
10
+  </div>
11
+</template>
12
+
13
+<script setup>
14
+import { ref } from 'vue'
15
+import Page from '../components/page.vue'
16
+import SearchBar from '../components/SearchBar.vue'
17
+import Profile from './component/profile.vue'
18
+import RunData from './component/runData.vue'
19
+import PentagonGroup from '../components/PentagonGroup.vue'
20
+import zongheIcon from '@/assets/icons/portrait/zonghe_icon.png'
21
+import renyuanIcon from '@/assets/icons/portrait/renyuan_icon.png'
22
+import zu02Icon from '@/assets/icons/portrait/zu02_icon.png'
23
+import nianlingIcon from '@/assets/icons/portrait/nianling_icon.png'
24
+import silingIcon from '@/assets/icons/portrait/siling_icon.png'
25
+
26
+const activeTab = ref(0)
27
+const searchVisible = ref(false)
28
+const queryParams = ref({})
29
+
30
+const pentagonItems = ref([
31
+  {
32
+    value: '88',
33
+    label: '综合得分',
34
+    iconPath: zongheIcon,
35
+    cornerRadius: 20,
36
+    vertices: {
37
+      topLeft: { x: 0, y: 0 },
38
+      topRight: { x: 300, y: 0 },
39
+      bottomRight: { x: 260, y: 200 },
40
+      bottomLeft: { x: 0, y: 200 }
41
+    },
42
+    cardContentStyle: {
43
+      gap: '100px'
44
+    },
45
+    edgeGradients: {
46
+      top: { start: '#0f46fa', end: 'transparent' },
47
+      right: { start: 'transparent', end: '#9903C1' },
48
+      bottom: { start: '#9903C1', end: 'transparent' },
49
+      left: { start: 'transparent', end: '#0f46fa' }
50
+    },
51
+    bgGradientStart: 'rgba(33,33,58,0.95)',
52
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
53
+  },
54
+  {
55
+    value: '10',
56
+    label: '人员数量',
57
+    iconPath: renyuanIcon,
58
+    cornerRadius: 20,
59
+    vertices: {
60
+      topLeft: { x: 10, y: 0 },
61
+      topRight: { x: 300, y: 0 },
62
+      bottomRight: { x: 260, y: 200 },
63
+      bottomLeft: { x: -30, y: 200 }
64
+    }, cardContentStyle: {
65
+      gap: '100px'
66
+    },
67
+    edgeGradients: {
68
+      top: { start: '#0f46fa', end: 'transparent' },
69
+      right: { start: 'transparent', end: '#9903C1' },
70
+      bottom: { start: '#9903C1', end: 'transparent' },
71
+      left: { start: 'transparent', end: '#0f46fa' }
72
+    },
73
+    bgGradientStart: 'rgba(33,33,58,0.95)',
74
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
75
+  },
76
+  {
77
+    value: '周游波小组',
78
+    label: '',
79
+    iconPath: zu02Icon,
80
+    iconStyle: {
81
+      width: '250px',
82
+      height: '250px'
83
+    },
84
+    valueStyle: {
85
+      fontSize: '48px',
86
+      bottom: '26px',
87
+      position: 'relative'
88
+    },
89
+    vertical: true,
90
+    cornerRadius: 20,
91
+    vertices: {
92
+      topLeft: { x: 10, y: 0 },
93
+      topRight: { x: 280, y: 0 },
94
+      bottomRight: { x: 320, y: 200 },
95
+      bottomLeft: { x: -30, y: 200 }
96
+    },
97
+    cardContentStyle: {
98
+      gap: '0px',
99
+      position: 'relative',
100
+      bottom: '65px'
101
+    },
102
+    edgeGradients: {
103
+      top: { start: '#0f46fa', end: 'transparent' },
104
+      right: { start: 'transparent', end: '#9903C1' },
105
+      bottom: { start: '#9903C1', end: 'transparent' },
106
+      left: { start: 'transparent', end: '#0f46fa' }
107
+    },
108
+    bgGradientStart: 'rgba(33,33,58,0.95)',
109
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
110
+  },
111
+  {
112
+    value: '28',
113
+    label: '平均年龄',
114
+    iconPath: nianlingIcon,
115
+    cornerRadius: 20,
116
+    vertices: {
117
+      topLeft: { x: -10, y: 0 },
118
+      topRight: { x: 290, y: 0 },
119
+      bottomRight: { x: 330, y: 200 },
120
+      bottomLeft: { x: 30, y: 200 }
121
+    },
122
+    cardContentStyle: {
123
+      gap: '100px'
124
+    },
125
+    edgeGradients: {
126
+      top: { start: '#0f46fa', end: 'transparent' },
127
+      right: { start: 'transparent', end: '#9903C1' },
128
+      bottom: { start: '#9903C1', end: 'transparent' },
129
+      left: { start: 'transparent', end: '#0f46fa' }
130
+    },
131
+    bgGradientStart: 'rgba(33,33,58,0.95)',
132
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
133
+  },
134
+  {
135
+    value: '3.5',
136
+    label: '平均司龄',
137
+    iconPath: silingIcon,
138
+    cornerRadius: 20,
139
+    vertices: {
140
+      topLeft: { x: 0, y: 0 },
141
+      topRight: { x: 300, y: 0 },
142
+      bottomRight: { x: 300, y: 200 },
143
+      bottomLeft: { x: 40, y: 200 }
144
+    },
145
+    cardContentStyle: {
146
+      gap: '100px'
147
+    },
148
+    edgeGradients: {
149
+      top: { start: '#0f46fa', end: 'transparent' },
150
+      right: { start: 'transparent', end: '#9903C1' },
151
+      bottom: { start: '#9903C1', end: 'transparent' },
152
+      left: { start: 'transparent', end: '#0f46fa' }
153
+    },
154
+    bgGradientStart: 'rgba(33,33,58,0.95)',
155
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
156
+  }
157
+])
158
+
159
+const handleSearch = (params) => {
160
+  queryParams.value = params
161
+}
162
+
163
+const handleTabChange = (index) => {
164
+  activeTab.value = index
165
+}
166
+</script>
167
+
168
+<style lang="scss" scoped>
169
+.group-profile-page {
170
+  width: 100%;
171
+  min-height: 100vh;
172
+  overflow-y: auto;
173
+}
174
+</style>

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 114 - 999
src/views/portraitManagement/groupProfile/component/profile.vue


+ 6 - 4
src/views/portraitManagement/groupProfile/index.vue

@@ -61,7 +61,7 @@ const pentagonItems = ref([
61 61
       topRight: { x: 300, y: 0 },
62 62
       bottomRight: { x: 260, y: 200 },
63 63
       bottomLeft: { x: -30, y: 200 }
64
-    },cardContentStyle: {
64
+    }, cardContentStyle: {
65 65
       gap: '100px'
66 66
     },
67 67
     edgeGradients: {
@@ -78,11 +78,13 @@ const pentagonItems = ref([
78 78
     label: '',
79 79
     iconPath: zu02Icon,
80 80
     iconStyle: {
81
-      width: '200px',
82
-      height: '200px'
81
+      width: '250px',
82
+      height: '250px'
83 83
     },
84 84
     valueStyle: {
85
-      fontSize: '48px'
85
+      fontSize: '48px',
86
+      bottom: '26px',
87
+      position: 'relative'
86 88
     },
87 89
     vertical: true,
88 90
     cornerRadius: 20,

+ 42 - 318
src/views/portraitManagement/stationProfile/component/profile.vue

@@ -3,62 +3,30 @@
3 3
   
4 4
 
5 5
     <div class="station-content">
6
-  
7
-
8 6
       <div class="content-row">
9
-        <InfoCard title="团队成员">
10
-          <RollingTable 
11
-            :columns="teamColumns"
12
-            :data="teamData"
13
-          />
14
-        </InfoCard>
15
-         <InfoCard title="成员基本情况分布">
16
-          <div class="member-distribution">
17
-            <div class="dist-item">
18
-              <h4>性别分布</h4>
19
-              <div ref="genderDistChartRef" class="dist-chart"></div>
20
-            </div>
21
-            <div class="dist-item">
22
-              <h4>民族分布</h4>
23
-              <div ref="nationDistChartRef" class="dist-chart"></div>
24
-            </div>
25
-            <div class="dist-item">
26
-              <h4>政治面貌分布</h4>
27
-              <div ref="politicalDistChartRef" class="dist-chart"></div>
28
-            </div>
29
-          </div>
30
-        </InfoCard>
7
+        <ProfileMembers :columns="teamColumns" :data="teamData" />
8
+        <ProfileBasicDistribution
9
+          :chartData1="genderData"
10
+          :chartData2="nationData"
11
+          :chartData3="politicalData"
12
+        />
31 13
       </div>
32
-
33
-      
34
-
35 14
       <div class="content-row" style="width: 50%;">
36
-        <InfoCard title="成员职位情况分布">
37
-          <div class="position-distribution">
38
-            <div class="dist-item">
39
-              <h4>职业资格等级分布</h4>
40
-              <div ref="qualificationLevelChartRef" class="dist-chart"></div>
41
-            </div>
42
-            <div class="dist-item">
43
-              <h4>开机年限分布</h4>
44
-              <div ref="experienceYearsChartRef" class="dist-chart"></div>
45
-            </div>
46
-            <div class="dist-item">
47
-              <h4>岗位资质分布</h4>
48
-              <div ref="positionQualificationChartRef" class="dist-chart"></div>
49
-            </div>
50
-          </div>
51
-        </InfoCard>
15
+        <ProfilePositionDistribution
16
+          :chartData1="skillData"
17
+          :chartData2="operateData"
18
+          :chartData3="postData"
19
+        />
52 20
       </div>
53 21
     </div>
54 22
   </div>
55 23
 </template>
56 24
 
57 25
 <script setup>
58
-import { ref, onMounted, onUnmounted, nextTick } from 'vue'
59
-import * as echarts from 'echarts'
60
-import InfoCard from '../../components/card.vue'
61
-import RollingTable from '../../components/rollingTable.vue'
26
+import { ref } from 'vue'
27
+import ProfileMembers from '../../components/ProfileMembers.vue'
28
+import ProfileBasicDistribution from '../../components/ProfileBasicDistribution.vue'
29
+import ProfilePositionDistribution from '../../components/ProfilePositionDistribution.vue'
62 30
 
63 31
 
64 32
 
@@ -86,255 +54,42 @@ const teamData = ref([
86 54
   { dept: '旅检三部', empCount: '708', partyCount: '7', avgAge: '25', avgWorkYears: '3', certLevel: '23', avgUpgradeAge: '3', totalScore: '86' }
87 55
 ])
88 56
 
89
-const genderDistChartRef = ref(null)
90
-let genderDistChart = null
91
-const nationDistChartRef = ref(null)
92
-let nationDistChart = null
93
-const politicalDistChartRef = ref(null)
94
-let politicalDistChart = null
95
-const qualificationLevelChartRef = ref(null)
96
-let qualificationLevelChart = null
97
-const experienceYearsChartRef = ref(null)
98
-let experienceYearsChart = null
99
-const positionQualificationChartRef = ref(null)
100
-let positionQualificationChart = null
101
-
102
-const initGenderDistChart = () => {
103
-  if (!genderDistChartRef.value) return
104
-  genderDistChart = echarts.init(genderDistChartRef.value)
105
-  const option = {
106
-    series: [{
107
-      type: 'pie',
108
-      radius: ['50%', '70%'],
109
-      data: [
110
-        { value: 650, name: '女', itemStyle: { color: '#ff9f9f' } },
111
-        { value: 750, name: '男', itemStyle: { color: '#7effc4' } }
112
-      ],
113
-      label: { show: true, color: '#fff', fontSize: 11 },
114
-      labelLine: { show: true }
115
-    }]
116
-  }
117
-  genderDistChart.setOption(option)
118
-}
57
+const genderData = [
58
+  { value: 650, name: '女', itemStyle: { color: '#ff9f9f' } },
59
+  { value: 750, name: '男', itemStyle: { color: '#7effc4' } }
60
+]
119 61
 
120
-const initNationDistChart = () => {
121
-  if (!nationDistChartRef.value) return
122
-  nationDistChart = echarts.init(nationDistChartRef.value)
123
-  const option = {
124
-    series: [{
125
-      type: 'pie',
126
-      radius: ['50%', '70%'],
127
-      data: [
128
-        { value: 615, name: '汉族', itemStyle: { color: '#7effc4' } },
129
-        { value: 484, name: '回族', itemStyle: { color: '#4da6ff' } },
130
-        { value: 156, name: '其他', itemStyle: { color: '#ffd93d' } }
131
-      ],
132
-      label: { show: true, color: '#fff', fontSize: 11 },
133
-      labelLine: { show: true }
134
-    }]
135
-  }
136
-  nationDistChart.setOption(option)
137
-}
62
+const nationData = [
63
+  { value: 615, name: '汉族', itemStyle: { color: '#7effc4' } },
64
+  { value: 484, name: '回族', itemStyle: { color: '#4da6ff' } },
65
+  { value: 156, name: '其他', itemStyle: { color: '#ffd93d' } }
66
+]
138 67
 
139
-const initPoliticalDistChart = () => {
140
-  if (!politicalDistChartRef.value) return
141
-  politicalDistChart = echarts.init(politicalDistChartRef.value)
142
-  const option = {
143
-    series: [{
144
-      type: 'pie',
145
-      radius: ['50%', '70%'],
146
-      data: [
147
-        { value: 410, name: '群众', itemStyle: { color: '#ffd93d' } },
148
-        { value: 540, name: '共青团员', itemStyle: { color: '#4da6ff' } },
149
-        { value: 270, name: '中共党员', itemStyle: { color: '#ff6b6b' } },
150
-        { value: 138, name: '预备党员', itemStyle: { color: '#7effc4' } }
151
-      ],
152
-      label: { show: true, color: '#fff', fontSize: 11 },
153
-      labelLine: { show: true }
154
-    }]
155
-  }
156
-  politicalDistChart.setOption(option)
157
-}
68
+const politicalData = [
69
+  { value: 410, name: '群众', itemStyle: { color: '#ffd93d' } },
70
+  { value: 540, name: '共青团员', itemStyle: { color: '#4da6ff' } },
71
+  { value: 270, name: '中共党员', itemStyle: { color: '#ff6b6b' } },
72
+  { value: 138, name: '预备党员', itemStyle: { color: '#7effc4' } }
73
+]
158 74
 
159
-const initQualificationLevelChart = () => {
160
-  if (!qualificationLevelChartRef.value) return
161
-  qualificationLevelChart = echarts.init(qualificationLevelChartRef.value)
162
-  const option = {
163
-    tooltip: {
164
-      trigger: 'axis',
165
-      backgroundColor: 'rgba(13,80,122,0.95)',
166
-      borderColor: '#70CFE7',
167
-      textStyle: { color: '#fff' }
168
-    },
169
-    legend: {
170
-      data: ['人数'],
171
-      textStyle: { color: '#a0c4ff' },
172
-      top: 0
173
-    },
174
-    grid: {
175
-      left: '10%',
176
-      right: '10%',
177
-      bottom: '15%',
178
-      containLabel: true
179
-    },
180
-    xAxis: {
181
-      type: 'category',
182
-      data: ['等级1', '等级2', '等级3', '等级4', '等级5'],
183
-      axisLabel: { color: '#a0c4ff', fontSize: 10 },
184
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
185
-    },
186
-    yAxis: {
187
-      type: 'value',
188
-      axisLabel: { color: '#a0c4ff' },
189
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
190
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
191
-    },
192
-    series: [{
193
-      name: '人数',
194
-      type: 'bar',
195
-      data: [450, 650, 900, 750, 550],
196
-      itemStyle: {
197
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
198
-          { offset: 0, color: '#ff6b9d' },
199
-          { offset: 1, color: '#ff6b6b' }
200
-        ])
201
-      },
202
-      barWidth: '50%'
203
-    }]
204
-  }
205
-  qualificationLevelChart.setOption(option)
75
+const skillData = {
76
+  categories: ['等级1', '等级2', '等级3', '等级4', '等级5'],
77
+  values: [450, 650, 900, 750, 550],
78
+  colors: ['#ff6b9d', '#ff6b6b']
206 79
 }
207 80
 
208
-const initExperienceYearsChart = () => {
209
-  if (!experienceYearsChartRef.value) return
210
-  experienceYearsChart = echarts.init(experienceYearsChartRef.value)
211
-  const option = {
212
-    tooltip: {
213
-      trigger: 'axis',
214
-      backgroundColor: 'rgba(13,80,122,0.95)',
215
-      borderColor: '#70CFE7',
216
-      textStyle: { color: '#fff' }
217
-    },
218
-    legend: {
219
-      data: ['人数'],
220
-      textStyle: { color: '#a0c4ff' },
221
-      top: 0
222
-    },
223
-    grid: {
224
-      left: '10%',
225
-      right: '10%',
226
-      bottom: '15%',
227
-      containLabel: true
228
-    },
229
-    xAxis: {
230
-      type: 'category',
231
-      data: ['0-3', '4-7', '8-11', '12-15', '16-19'],
232
-      axisLabel: { color: '#a0c4ff', fontSize: 10 },
233
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
234
-    },
235
-    yAxis: {
236
-      type: 'value',
237
-      axisLabel: { color: '#a0c4ff' },
238
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
239
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
240
-    },
241
-    series: [{
242
-      name: '人数',
243
-      type: 'bar',
244
-      data: [650, 500, 400, 300, 550],
245
-      itemStyle: {
246
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
247
-          { offset: 0, color: '#a55eea' },
248
-          { offset: 1, color: '#bd03fb' }
249
-        ])
250
-      },
251
-      barWidth: '50%'
252
-    }]
253
-  }
254
-  experienceYearsChart.setOption(option)
81
+const operateData = {
82
+  categories: ['0-3', '4-7', '8-11', '12-15', '16-19'],
83
+  values: [650, 500, 400, 300, 550],
84
+  colors: ['#a55eea', '#bd03fb']
255 85
 }
256 86
 
257
-const initPositionQualificationChart = () => {
258
-  if (!positionQualificationChartRef.value) return
259
-  positionQualificationChart = echarts.init(positionQualificationChartRef.value)
260
-  const option = {
261
-    tooltip: {
262
-      trigger: 'axis',
263
-      backgroundColor: 'rgba(13,80,122,0.95)',
264
-      borderColor: '#70CFE7',
265
-      textStyle: { color: '#fff' }
266
-    },
267
-    legend: {
268
-      data: ['人数'],
269
-      textStyle: { color: '#a0c4ff' },
270
-      top: 0
271
-    },
272
-    grid: {
273
-      left: '10%',
274
-      right: '10%',
275
-      bottom: '15%',
276
-      containLabel: true
277
-    },
278
-    xAxis: {
279
-      type: 'category',
280
-      data: ['仿伪', '炸探', '人身', '开包', '开检后', '开机'],
281
-      axisLabel: { color: '#a0c4ff', fontSize: 10 },
282
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
283
-    },
284
-    yAxis: {
285
-      type: 'value',
286
-      axisLabel: { color: '#a0c4ff' },
287
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
288
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
289
-    },
290
-    series: [{
291
-      name: '人数',
292
-      type: 'bar',
293
-      data: [450, 600, 180, 150, 650, 550],
294
-      itemStyle: {
295
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
296
-          { offset: 0, color: '#ffd93d' },
297
-          { offset: 1, color: '#ffd9b3' }
298
-        ])
299
-      },
300
-      barWidth: '50%'
301
-    }]
302
-  }
303
-  positionQualificationChart.setOption(option)
87
+const postData = {
88
+  categories: ['仿伪', '炸探', '人身', '开包', '开检后', '开机'],
89
+  values: [450, 600, 180, 150, 650, 550],
90
+  colors: ['#ffd93d', '#ffd9b3']
304 91
 }
305 92
 
306
-const handleResize = () => {
307
-  if (genderDistChart) genderDistChart.resize()
308
-  if (nationDistChart) nationDistChart.resize()
309
-  if (politicalDistChart) politicalDistChart.resize()
310
-  if (qualificationLevelChart) qualificationLevelChart.resize()
311
-  if (experienceYearsChart) experienceYearsChart.resize()
312
-  if (positionQualificationChart) positionQualificationChart.resize()
313
-}
314
-
315
-onMounted(() => {
316
-  nextTick(() => {
317
-    setTimeout(() => {
318
-      initGenderDistChart()
319
-      initNationDistChart()
320
-      initPoliticalDistChart()
321
-      initQualificationLevelChart()
322
-      initExperienceYearsChart()
323
-      initPositionQualificationChart()
324
-      window.addEventListener('resize', handleResize)
325
-    }, 100)
326
-  })
327
-})
328
-
329
-onUnmounted(() => {
330
-  window.removeEventListener('resize', handleResize)
331
-  if (genderDistChart) genderDistChart.dispose()
332
-  if (nationDistChart) nationDistChart.dispose()
333
-  if (politicalDistChart) politicalDistChart.dispose()
334
-  if (qualificationLevelChart) qualificationLevelChart.dispose()
335
-  if (experienceYearsChart) experienceYearsChart.dispose()
336
-  if (positionQualificationChart) positionQualificationChart.dispose()
337
-})
338 93
 </script>
339 94
 
340 95
 <style lang="scss" scoped>
@@ -355,36 +110,5 @@ onUnmounted(() => {
355 110
       min-width: 0;
356 111
     }
357 112
   }
358
-
359
-  .member-distribution,
360
-  .position-distribution {
361
-    display: flex;
362
-    gap: 30px;
363
-    justify-content: space-around;
364
-    flex-wrap: wrap;
365
-
366
-    .dist-item {
367
-      flex: 1;
368
-      min-width: 200px;
369
-      text-align: center;
370
-
371
-      h4 {
372
-        color: #a0c4ff;
373
-        margin-bottom: 15px;
374
-        font-size: 16px;
375
-      }
376
-
377
-      .dist-chart {
378
-        width: 100%;
379
-        height: 200px;
380
-      }
381
-    }
382
-  }
383
-
384
-  .position-distribution {
385
-    .dist-chart {
386
-      height: 280px;
387
-    }
388
-  }
389 113
 }
390 114
 </style>

+ 101 - 514
src/views/portraitManagement/stationProfile/component/runData.vue

@@ -42,30 +42,12 @@
42 42
     </div>
43 43
 
44 44
     <div class="content-row">
45
-      <InfoCard title="查获信息展示">
46
-        <div class="seized-info">
47
-          <div class="seized-number">
48
-            <div class="number-circle">
49
-              <div class="number-value">1651</div>
50
-              <div class="number-label">查获总数(所有部门)</div>
51
-            </div>
52
-          </div>
53
-          <div class="seized-small-chart">
54
-            <div ref="seizedSmallChartRef" class="small-chart"></div>
55
-          </div>
56
-        </div>
57
-      </InfoCard>
58
-      <InfoCard title="查获物品分布">
59
-        <div class="seized-distribution">
60
-          <div ref="seizedChartRef" class="seized-chart"></div>
61
-        </div>
62
-      </InfoCard>
45
+      <SeizedInfo :chartsData="seizedInfoData" />
46
+      <SeizedItems :chartsData="seizedItemsData" />
63 47
     </div>
64 48
 
65 49
     <div class="content-row">
66
-      <InfoCard title="每日查获数量(总表)">
67
-        <div ref="dailySeizedLineRef" class="daily-chart"></div>
68
-      </InfoCard>
50
+      <SeizedNumAll :chartsData="seizedNumAllData" />
69 51
       <InfoCard title="每日查获数量(部门对比)">
70 52
         <div ref="dailySeizedAreaRef" class="daily-chart"></div>
71 53
       </InfoCard>
@@ -74,33 +56,19 @@
74 56
 
75 57
 
76 58
     <div class="content-row">
77
-      <InfoCard title="查获工作区域分布">
78
-        <div ref="workAreaDistChartRef" class="work-area-dist-chart"></div>
79
-      </InfoCard>
59
+      <SeizedWorkArea :chartsData="workAreaDistData" />
80 60
     </div>
81 61
 
82 62
     <div class="content-row">
83
-      <InfoCard title="不安全事件物品分布">
84
-        <div ref="unsafeItemDistChartRef" class="unsafe-item-dist-chart"></div>
85
-      </InfoCard>
86
-      <InfoCard title="不安全事件类型分布">
87
-        <div ref="unsafeTypeDistChartRef" class="unsafe-type-dist-chart"></div>
88
-      </InfoCard>
89
-      <InfoCard title="不安全事件查获岗位分布">
90
-        <div ref="unsafePostDistChartRef" class="unsafe-post-dist-chart"></div>
91
-      </InfoCard>
63
+      <EventItems :chartsData="unsafeItemDistData" />
64
+      <EventType :chartsData="unsafeTypeDistData" />
65
+      <EventWorkArea :chartsData="unsafePostDistData" />
92 66
     </div>
93 67
 
94 68
     <div class="content-row">
95
-      <InfoCard title="安保测试物品分类">
96
-        <div ref="securityTestItemChartRef" class="security-test-item-chart"></div>
97
-      </InfoCard>
98
-      <InfoCard title="安保测试通过情况">
99
-        <div ref="securityTestPassChartRef" class="security-test-pass-chart"></div>
100
-      </InfoCard>
101
-      <InfoCard title="安保测试区域情况">
102
-        <div ref="securityTestAreaChartRef" class="security-test-area-chart"></div>
103
-      </InfoCard>
69
+      <TestItems :chartsData="securityTestItemData" />
70
+      <TestResult :chartsData="securityTestPassData" />
71
+      <TestArea :chartsData="securityTestAreaData" />
104 72
     </div>
105 73
   </div>
106 74
 </template>
@@ -109,6 +77,16 @@
109 77
 import { ref, onMounted, onUnmounted, nextTick } from 'vue'
110 78
 import * as echarts from 'echarts'
111 79
 import InfoCard from '../../components/card.vue'
80
+import SeizedInfo from '../../components/SeizedInfo.vue'
81
+import SeizedItems from '../../components/SeizedItems.vue'
82
+import SeizedNumAll from '../../components/SeizedNumAll.vue'
83
+import SeizedWorkArea from '../../components/SeizedWorkArea.vue'
84
+import EventItems from '../../components/EventItems.vue'
85
+import EventType from '../../components/EventType.vue'
86
+import EventWorkArea from '../../components/EventWorkArea.vue'
87
+import TestItems from '../../components/TestItems.vue'
88
+import TestResult from '../../components/TestResult.vue'
89
+import TestArea from '../../components/TestArea.vue'
112 90
 
113 91
 const props = defineProps({
114 92
   queryParams: {
@@ -156,32 +134,91 @@ const updateDateTime = () => {
156 134
   currentTime.value = `${hours}:${minutes}:${seconds}`
157 135
 }
158 136
 
137
+const seizedInfoData = [
138
+  { num: 550 },
139
+  { num: 620 },
140
+  { num: 481 }
141
+]
142
+
143
+const seizedItemsData = [
144
+  { num: 50, name: '打火机' },
145
+  { num: 30, name: '管制刀具' },
146
+  { num: 20, name: '其他' }
147
+]
148
+
149
+const seizedNumAllData = [
150
+  { num: 150 }, { num: 160 }, { num: 145 }, { num: 180 }, { num: 170 },
151
+  { num: 190 }, { num: 155 }, { num: 140 }, { num: 165 }, { num: 185 },
152
+  { num: 175 }, { num: 200 }, { num: 180 }, { num: 195 }, { num: 170 },
153
+  { num: 160 }, { num: 185 }, { num: 175 }, { num: 190 }
154
+]
155
+
156
+const workAreaDistData = [
157
+  { num: 287, name: 'T3国内出发(4层)' },
158
+  { num: 250, name: 'T3(8层出发)' },
159
+  { num: 300, name: 'T3国际出发' },
160
+  { num: 290, name: 'T3国际转国内' },
161
+  { num: 240, name: '旅检A区' },
162
+  { num: 250, name: 'T3要客服务区东' },
163
+  { num: 260, name: 'T3要客服务区西' },
164
+  { num: 255, name: 'T1要客服务区东' },
165
+  { num: 245, name: 'T2要客服务区东' },
166
+  { num: 260, name: '旅检B区' },
167
+  { num: 280, name: '重检C区' }
168
+]
169
+
170
+const unsafeItemDistData = [
171
+  { num: 66, name: '打火机' },
172
+  { num: 174, name: '火柴' },
173
+  { num: 65, name: '其他' },
174
+  { num: 106, name: '危险物品' },
175
+  { num: 173, name: '危险品' },
176
+  { num: 48, name: '其他/危险品' }
177
+]
178
+
179
+const unsafeTypeDistData = [
180
+  { num: 6, name: '一般' },
181
+  { num: 6, name: '二级' },
182
+  { num: 174, name: '三级' },
183
+  { num: 48, name: '四级' },
184
+  { num: 454, name: '五级' }
185
+]
186
+
187
+const unsafePostDistData = [
188
+  { num: 100, name: '境外/非通道' },
189
+  { num: 213, name: '值机柜台' },
190
+  { num: 150, name: '验证/通道' },
191
+  { num: 300, name: '人身检查' },
192
+  { num: 187, name: 'X光/行李/开检' },
193
+  { num: 387, name: '开箱/货检' }
194
+]
195
+
196
+const securityTestItemData = [
197
+  { num: 5, name: '烟花' },
198
+  { num: 8, name: '鞭炮' },
199
+  { num: 2, name: '打火机' },
200
+  { num: 3, name: '野生点火器' }
201
+]
202
+
203
+const securityTestPassData = [
204
+  { num: 44, name: '通过' },
205
+  { num: 4, name: '未通过' }
206
+]
207
+
208
+const securityTestAreaData = [
209
+  { num: 100, name: 'T3国内出发(4层)' },
210
+  { num: 246, name: 'T3(8层出发)' },
211
+  { num: 160, name: 'T3国际出发' },
212
+  { num: 300, name: 'T3国际转国内' },
213
+  { num: 100, name: 'T1要客服务区西' },
214
+  { num: 167, name: 'T1要客服务区东' }
215
+]
216
+
159 217
 const hourlyPassChartRef = ref(null)
160 218
 let hourlyPassChart = null
161
-const seizedSmallChartRef = ref(null)
162
-let seizedSmallChart = null
163
-const seizedChartRef = ref(null)
164
-let seizedChart = null
165
-const dailySeizedLineRef = ref(null)
166
-let dailySeizedLine = null
167 219
 const dailySeizedAreaRef = ref(null)
168 220
 let dailySeizedArea = null
169 221
 
170
-const workAreaDistChartRef = ref(null)
171
-let workAreaDistChart = null
172
-const unsafeItemDistChartRef = ref(null)
173
-let unsafeItemDistChart = null
174
-const unsafeTypeDistChartRef = ref(null)
175
-let unsafeTypeDistChart = null
176
-const unsafePostDistChartRef = ref(null)
177
-let unsafePostDistChart = null
178
-const securityTestItemChartRef = ref(null)
179
-let securityTestItemChart = null
180
-const securityTestPassChartRef = ref(null)
181
-let securityTestPassChart = null
182
-const securityTestAreaChartRef = ref(null)
183
-let securityTestAreaChart = null
184
-
185 222
 const initHourlyPassChart = () => {
186 223
   if (!hourlyPassChartRef.value) return
187 224
   hourlyPassChart = echarts.init(hourlyPassChartRef.value)
@@ -294,108 +331,6 @@ const initHourlyPassChart = () => {
294 331
   hourlyPassChart.setOption(option)
295 332
 }
296 333
 
297
-const initSeizedSmallChart = () => {
298
-  if (!seizedSmallChartRef.value) return
299
-  seizedSmallChart = echarts.init(seizedSmallChartRef.value)
300
-  const option = {
301
-    xAxis: {
302
-      type: 'category',
303
-      data: ['旅检一部', '旅检二部', '旅检三部'],
304
-      axisLabel: { color: '#a0c4ff', fontSize: 9 },
305
-      axisLine: { show: false },
306
-      axisTick: { show: false }
307
-    },
308
-    yAxis: {
309
-      type: 'value',
310
-      axisLabel: { show: false },
311
-      axisLine: { show: false },
312
-      axisTick: { show: false },
313
-      splitLine: { show: false }
314
-    },
315
-    series: [{
316
-      type: 'bar',
317
-      data: [550, 620, 481],
318
-      itemStyle: {
319
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
320
-          { offset: 0, color: '#bd03fb' },
321
-          { offset: 1, color: '#0f46fa' }
322
-        ])
323
-      },
324
-      barWidth: '30%'
325
-    }]
326
-  }
327
-  seizedSmallChart.setOption(option)
328
-}
329
-
330
-const initSeizedChart = () => {
331
-  if (!seizedChartRef.value) return
332
-  seizedChart = echarts.init(seizedChartRef.value)
333
-  const option = {
334
-    series: [{
335
-      type: 'pie',
336
-      radius: ['40%', '70%'],
337
-      data: [
338
-        { value: 50, name: '打火机', itemStyle: { color: '#ff9f43' } },
339
-        { value: 30, name: '管制刀具', itemStyle: { color: '#ff6b6b' } },
340
-        { value: 20, name: '其他', itemStyle: { color: '#4da6ff' } }
341
-      ],
342
-      label: { show: true, color: '#fff', fontSize: 11 },
343
-      labelLine: { show: true }
344
-    }]
345
-  }
346
-  seizedChart.setOption(option)
347
-}
348
-
349
-const initDailySeizedLine = () => {
350
-  if (!dailySeizedLineRef.value) return
351
-  dailySeizedLine = echarts.init(dailySeizedLineRef.value)
352
-  const option = {
353
-    tooltip: {
354
-      trigger: 'axis',
355
-      backgroundColor: 'rgba(13,80,122,0.95)',
356
-      borderColor: '#70CFE7',
357
-      textStyle: { color: '#fff' }
358
-    },
359
-    legend: {
360
-      data: ['查获数量'],
361
-      textStyle: { color: '#a0c4ff' },
362
-      top: 0
363
-    },
364
-    grid: {
365
-      left: '3%',
366
-      right: '4%',
367
-      bottom: '15%',
368
-      containLabel: true
369
-    },
370
-    xAxis: {
371
-      type: 'category',
372
-      data: ['1号', '2号', '3号', '4号', '5号', '6号', '7号', '8号', '9号', '10号', '11号', '12号', '13号', '14号', '15号', '16号', '17号', '18号', '19号'],
373
-      axisLabel: { color: '#a0c4ff', fontSize: 9 },
374
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
375
-    },
376
-    yAxis: {
377
-      type: 'value',
378
-      axisLabel: { color: '#a0c4ff' },
379
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
380
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
381
-    },
382
-    series: [{
383
-      name: '查获数量',
384
-      type: 'line',
385
-      smooth: true,
386
-      data: [150, 160, 145, 180, 170, 190, 155, 140, 165, 185, 175, 200, 180, 195, 170, 160, 185, 175, 190],
387
-      itemStyle: { color: '#bd03fb' },
388
-      areaStyle: {
389
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
390
-          { offset: 0, color: 'rgba(189,3,251,0.3)' },
391
-          { offset: 1, color: 'rgba(189,3,251,0.05)' }
392
-        ])
393
-      }
394
-    }]
395
-  }
396
-  dailySeizedLine.setOption(option)
397
-}
398
-
399 334
 const initDailySeizedArea = () => {
400 335
   if (!dailySeizedAreaRef.value) return
401 336
   dailySeizedArea = echarts.init(dailySeizedAreaRef.value)
@@ -477,262 +412,9 @@ const initDailySeizedArea = () => {
477 412
   dailySeizedArea.setOption(option)
478 413
 }
479 414
 
480
-
481
-
482
-const initWorkAreaDistChart = () => {
483
-  if (!workAreaDistChartRef.value) return
484
-  workAreaDistChart = echarts.init(workAreaDistChartRef.value)
485
-  const option = {
486
-    tooltip: {
487
-      trigger: 'axis',
488
-      backgroundColor: 'rgba(13,80,122,0.95)',
489
-      borderColor: '#70CFE7',
490
-      textStyle: { color: '#fff' }
491
-    },
492
-    legend: {
493
-      data: ['查获数量'],
494
-      textStyle: { color: '#a0c4ff' },
495
-      top: 0
496
-    },
497
-    grid: {
498
-      left: '3%',
499
-      right: '4%',
500
-      bottom: '15%',
501
-      containLabel: true
502
-    },
503
-    xAxis: {
504
-      type: 'category',
505
-      data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', '旅检A区', 'T3要客服务区东', 'T3要客服务区西', 'T1要客服务区东', 'T2要客服务区东', '旅检B区', '重检C区'],
506
-      axisLabel: {
507
-        color: '#a0c4ff',
508
-        rotate: 30,
509
-        fontSize: 10
510
-      },
511
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
512
-    },
513
-    yAxis: {
514
-      type: 'value',
515
-      axisLabel: { color: '#a0c4ff' },
516
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
517
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
518
-    },
519
-    series: [{
520
-      name: '查获数量',
521
-      type: 'bar',
522
-      data: [287, 250, 300, 290, 240, 250, 260, 255, 245, 260, 280],
523
-      itemStyle: {
524
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
525
-          { offset: 0, color: '#bd03fb' },
526
-          { offset: 1, color: '#0f46fa' }
527
-        ])
528
-      },
529
-      barWidth: '40%'
530
-    }]
531
-  }
532
-  workAreaDistChart.setOption(option)
533
-}
534
-
535
-const initUnsafeItemDistChart = () => {
536
-  if (!unsafeItemDistChartRef.value) return
537
-  unsafeItemDistChart = echarts.init(unsafeItemDistChartRef.value)
538
-  const option = {
539
-    series: [{
540
-      type: 'pie',
541
-      radius: ['40%', '70%'],
542
-      data: [
543
-        { value: 66, name: '打火机', itemStyle: { color: '#ff9f43' } },
544
-        { value: 174, name: '火柴', itemStyle: { color: '#4da6ff' } },
545
-        { value: 65, name: '其他', itemStyle: { color: '#7eff7e' } },
546
-        { value: 106, name: '危险物品', itemStyle: { color: '#ff6b6b' } },
547
-        { value: 173, name: '危险品', itemStyle: { color: '#ffd93d' } },
548
-        { value: 48, name: '其他/危险品', itemStyle: { color: '#a55eea' } }
549
-      ],
550
-      label: { show: true, color: '#fff', fontSize: 11 },
551
-      labelLine: { show: true }
552
-    }]
553
-  }
554
-  unsafeItemDistChart.setOption(option)
555
-}
556
-
557
-const initUnsafeTypeDistChart = () => {
558
-  if (!unsafeTypeDistChartRef.value) return
559
-  unsafeTypeDistChart = echarts.init(unsafeTypeDistChartRef.value)
560
-  const option = {
561
-    series: [{
562
-      type: 'pie',
563
-      radius: ['40%', '70%'],
564
-      data: [
565
-        { value: 6, name: '一般', itemStyle: { color: '#ff9f43' } },
566
-        { value: 6, name: '二级', itemStyle: { color: '#ff6b6b' } },
567
-        { value: 174, name: '三级', itemStyle: { color: '#7eff7e' } },
568
-        { value: 48, name: '四级', itemStyle: { color: '#ffd93d' } },
569
-        { value: 454, name: '五级', itemStyle: { color: '#4da6ff' } }
570
-      ],
571
-      label: { show: true, color: '#fff', fontSize: 11 },
572
-      labelLine: { show: true }
573
-    }]
574
-  }
575
-  unsafeTypeDistChart.setOption(option)
576
-}
577
-
578
-const initUnsafePostDistChart = () => {
579
-  if (!unsafePostDistChartRef.value) return
580
-  unsafePostDistChart = echarts.init(unsafePostDistChartRef.value)
581
-  const option = {
582
-    tooltip: {
583
-      trigger: 'axis',
584
-      backgroundColor: 'rgba(13,80,122,0.95)',
585
-      borderColor: '#70CFE7',
586
-      textStyle: { color: '#fff' }
587
-    },
588
-    legend: {
589
-      data: ['查获数量'],
590
-      textStyle: { color: '#a0c4ff' },
591
-      top: 0
592
-    },
593
-    grid: {
594
-      left: '3%',
595
-      right: '4%',
596
-      bottom: '15%',
597
-      containLabel: true
598
-    },
599
-    xAxis: {
600
-      type: 'category',
601
-      data: ['境外/非通道', '值机柜台', '验证/通道', '人身检查', 'X光/行李/开检', '开箱/货检'],
602
-      axisLabel: {
603
-        color: '#a0c4ff',
604
-        rotate: 30,
605
-        fontSize: 10
606
-      },
607
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
608
-    },
609
-    yAxis: {
610
-      type: 'value',
611
-      axisLabel: { color: '#a0c4ff' },
612
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
613
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
614
-    },
615
-    series: [{
616
-      name: '查获数量',
617
-      type: 'bar',
618
-      data: [100, 213, 150, 300, 187, 387],
619
-      itemStyle: {
620
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
621
-          { offset: 0, color: '#ff6b9d' },
622
-          { offset: 1, color: '#bd03fb' }
623
-        ])
624
-      },
625
-      barWidth: '40%'
626
-    }]
627
-  }
628
-  unsafePostDistChart.setOption(option)
629
-}
630
-
631
-const initSecurityTestItemChart = () => {
632
-  if (!securityTestItemChartRef.value) return
633
-  securityTestItemChart = echarts.init(securityTestItemChartRef.value)
634
-  const option = {
635
-    series: [{
636
-      type: 'pie',
637
-      radius: ['40%', '70%'],
638
-      data: [
639
-        { value: 5, name: '烟花', itemStyle: { color: '#ffd93d' } },
640
-        { value: 8, name: '鞭炮', itemStyle: { color: '#7eff7e' } },
641
-        { value: 2, name: '打火机', itemStyle: { color: '#a0c4ff' } },
642
-        { value: 3, name: '野生点火器', itemStyle: { color: '#4da6ff' } }
643
-      ],
644
-      label: { show: true, color: '#fff', fontSize: 11 },
645
-      labelLine: { show: true }
646
-    }]
647
-  }
648
-  securityTestItemChart.setOption(option)
649
-}
650
-
651
-const initSecurityTestPassChart = () => {
652
-  if (!securityTestPassChartRef.value) return
653
-  securityTestPassChart = echarts.init(securityTestPassChartRef.value)
654
-  const option = {
655
-    series: [{
656
-      type: 'pie',
657
-      radius: ['40%', '70%'],
658
-      data: [
659
-        { value: 44, name: '通过', itemStyle: { color: '#ffd9b3' } },
660
-        { value: 4, name: '未通过', itemStyle: { color: '#a55eea' } }
661
-      ],
662
-      label: { show: true, color: '#fff', fontSize: 11 },
663
-      labelLine: { show: true }
664
-    }]
665
-  }
666
-  securityTestPassChart.setOption(option)
667
-}
668
-
669
-const initSecurityTestAreaChart = () => {
670
-  if (!securityTestAreaChartRef.value) return
671
-  securityTestAreaChart = echarts.init(securityTestAreaChartRef.value)
672
-  const option = {
673
-    tooltip: {
674
-      trigger: 'axis',
675
-      backgroundColor: 'rgba(13,80,122,0.95)',
676
-      borderColor: '#70CFE7',
677
-      textStyle: { color: '#fff' }
678
-    },
679
-    legend: {
680
-      data: ['测试数量'],
681
-      textStyle: { color: '#a0c4ff' },
682
-      top: 0
683
-    },
684
-    grid: {
685
-      left: '3%',
686
-      right: '4%',
687
-      bottom: '15%',
688
-      containLabel: true
689
-    },
690
-    xAxis: {
691
-      type: 'category',
692
-      data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', 'T1要客服务区西', 'T1要客服务区东'],
693
-      axisLabel: {
694
-        color: '#a0c4ff',
695
-        rotate: 30,
696
-        fontSize: 10
697
-      },
698
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
699
-    },
700
-    yAxis: {
701
-      type: 'value',
702
-      axisLabel: { color: '#a0c4ff' },
703
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
704
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
705
-    },
706
-    series: [{
707
-      name: '测试数量',
708
-      type: 'bar',
709
-      data: [100, 246, 160, 300, 100, 167, 307],
710
-      itemStyle: {
711
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
712
-          { offset: 0, color: '#bd03fb' },
713
-          { offset: 1, color: '#ffd9b3' }
714
-        ])
715
-      },
716
-      barWidth: '40%'
717
-    }]
718
-  }
719
-  securityTestAreaChart.setOption(option)
720
-}
721
-
722 415
 const handleResize = () => {
723 416
   if (hourlyPassChart) hourlyPassChart.resize()
724
-  if (seizedSmallChart) seizedSmallChart.resize()
725
-  if (seizedChart) seizedChart.resize()
726
-  if (dailySeizedLine) dailySeizedLine.resize()
727 417
   if (dailySeizedArea) dailySeizedArea.resize()
728
-
729
-  if (workAreaDistChart) workAreaDistChart.resize()
730
-  if (unsafeItemDistChart) unsafeItemDistChart.resize()
731
-  if (unsafeTypeDistChart) unsafeTypeDistChart.resize()
732
-  if (unsafePostDistChart) unsafePostDistChart.resize()
733
-  if (securityTestItemChart) securityTestItemChart.resize()
734
-  if (securityTestPassChart) securityTestPassChart.resize()
735
-  if (securityTestAreaChart) securityTestAreaChart.resize()
736 418
 }
737 419
 
738 420
 onMounted(() => {
@@ -741,18 +423,7 @@ onMounted(() => {
741 423
   nextTick(() => {
742 424
     setTimeout(() => {
743 425
       initHourlyPassChart()
744
-      initSeizedSmallChart()
745
-      initSeizedChart()
746
-      initDailySeizedLine()
747 426
       initDailySeizedArea()
748
-
749
-      initWorkAreaDistChart()
750
-      initUnsafeItemDistChart()
751
-      initUnsafeTypeDistChart()
752
-      initUnsafePostDistChart()
753
-      initSecurityTestItemChart()
754
-      initSecurityTestPassChart()
755
-      initSecurityTestAreaChart()
756 427
       window.addEventListener('resize', handleResize)
757 428
     }, 100)
758 429
   })
@@ -762,18 +433,7 @@ onUnmounted(() => {
762 433
   clearInterval(dateTimer)
763 434
   window.removeEventListener('resize', handleResize)
764 435
   if (hourlyPassChart) hourlyPassChart.dispose()
765
-  if (seizedSmallChart) seizedSmallChart.dispose()
766
-  if (seizedChart) seizedChart.dispose()
767
-  if (dailySeizedLine) dailySeizedLine.dispose()
768 436
   if (dailySeizedArea) dailySeizedArea.dispose()
769
-
770
-  if (workAreaDistChart) workAreaDistChart.dispose()
771
-  if (unsafeItemDistChart) unsafeItemDistChart.dispose()
772
-  if (unsafeTypeDistChart) unsafeTypeDistChart.dispose()
773
-  if (unsafePostDistChart) unsafePostDistChart.dispose()
774
-  if (securityTestItemChart) securityTestItemChart.dispose()
775
-  if (securityTestPassChart) securityTestPassChart.dispose()
776
-  if (securityTestAreaChart) securityTestAreaChart.dispose()
777 437
 })
778 438
 </script>
779 439
 
@@ -784,6 +444,7 @@ onUnmounted(() => {
784 444
   gap: 20px;
785 445
 
786 446
   .content-row {
447
+  height: 400px;
787 448
     padding: 0 20px;
788 449
     display: flex;
789 450
     gap: 20px;
@@ -799,85 +460,11 @@ onUnmounted(() => {
799 460
     height: 350px;
800 461
   }
801 462
 
802
-  .seized-info {
803
-    display: flex;
804
-    gap: 20px;
805
-    min-height: 280px;
806
-    align-items: center;
807
-
808
-    .seized-number {
809
-      flex: 1;
810
-      display: flex;
811
-      justify-content: center;
812
-
813
-      .number-circle {
814
-        width: 200px;
815
-        height: 200px;
816
-        border-radius: 50%;
817
-        background: linear-gradient(135deg, rgba(15, 70, 250, 0.3), rgba(189, 3, 251, 0.3));
818
-        display: flex;
819
-        flex-direction: column;
820
-        align-items: center;
821
-        justify-content: center;
822
-        border: 2px solid rgba(189, 3, 251, 0.5);
823
-
824
-        .number-value {
825
-          font-size: 48px;
826
-          font-weight: bold;
827
-          color: #fff;
828
-          text-shadow: 0 0 20px rgba(189, 3, 251, 0.8);
829
-        }
830
-
831
-        .number-label {
832
-          font-size: 14px;
833
-          color: #a0c4ff;
834
-          text-align: center;
835
-          margin-top: 10px;
836
-        }
837
-      }
838
-    }
839
-
840
-    .seized-small-chart {
841
-      flex: 1;
842
-
843
-      .small-chart {
844
-        width: 100%;
845
-        height: 200px;
846
-      }
847
-    }
848
-  }
849
-
850
-  .seized-distribution {
851
-    width: 100%;
852
-    height: 280px;
853
-
854
-    .seized-chart {
855
-      width: 100%;
856
-      height: 100%;
857
-    }
858
-  }
859
-
860 463
   .daily-chart {
861 464
     width: 100%;
862 465
     height: 350px;
863 466
   }
864 467
 
865
-
866
-  .work-area-dist-chart {
867
-    width: 100%;
868
-    height: 400px;
869
-  }
870
-
871
-  .unsafe-item-dist-chart,
872
-  .unsafe-type-dist-chart,
873
-  .unsafe-post-dist-chart,
874
-  .security-test-item-chart,
875
-  .security-test-pass-chart,
876
-  .security-test-area-chart {
877
-    width: 100%;
878
-    height: 350px;
879
-  }
880
-
881 468
   .info-panel {
882 469
     width: 300px;
883 470
     min-width: 300px;

+ 209 - 0
src/views/portraitManagement/teamProfile/component/profile.vue

@@ -0,0 +1,209 @@
1
+<template>
2
+  <div class="main-content-wrapper">
3
+    <div class="main-content">
4
+      <div class="content-row">
5
+        <ProfileRadar
6
+          :chartData1="radarData"
7
+          :chartData2="radarIndicators"
8
+          :chartData3="radarSeries1"
9
+          :chartData4="radarSeries2"
10
+        />
11
+        <ProfileMembers
12
+          :columns="memberColumns"
13
+          :data="teamMembers"
14
+        />
15
+      </div>
16
+
17
+      <div class="content-row">
18
+        <ProfileBasicDistribution
19
+          :chartData1="genderData"
20
+          :chartData2="nationData"
21
+          :chartData3="politicalData"
22
+        />
23
+        <ProfilePositionDistribution
24
+          :chartData1="skillData"
25
+          :chartData2="operateData"
26
+          :chartData3="postData"
27
+        />
28
+      </div>
29
+
30
+      <div class="content-row">
31
+        <ProfilePassRate
32
+          :chartData1="passRatePassData"
33
+          :chartData2="passRateLineData"
34
+        />
35
+      </div>
36
+
37
+      <div class="content-row">
38
+        <ProfileSeizedInfo :chartData1="seizedTotal" />
39
+        <ProfileSeizedDistribution :chartData1="seizedDistributionData" />
40
+      </div>
41
+
42
+      <div class="content-row">
43
+        <ProfileDailySeizedLine :chartData1="dailyLineData" />
44
+        <ProfileDailySeizedArea
45
+          :chartData1="dailyAreaPersonA"
46
+          :chartData2="dailyAreaPersonB"
47
+          :chartData3="dailyAreaPersonC"
48
+          :chartData4="dailyAreaPersonD"
49
+          :chartData5="dailyAreaPersonE"
50
+        />
51
+      </div>
52
+
53
+      <div class="content-row">
54
+        <ProfileDailySeizedBar :chartData1="dailyBarData" />
55
+      </div>
56
+    </div>
57
+  </div>
58
+</template>
59
+
60
+<script setup>
61
+import { ref, watch } from 'vue'
62
+import ProfileRadar from '../../components/ProfileRadar.vue'
63
+import ProfileMembers from '../../components/ProfileMembers.vue'
64
+import ProfileBasicDistribution from '../../components/ProfileBasicDistribution.vue'
65
+import ProfilePositionDistribution from '../../components/ProfilePositionDistribution.vue'
66
+import ProfilePassRate from '../../components/ProfilePassRate.vue'
67
+import ProfileSeizedInfo from '../../components/ProfileSeizedInfo.vue'
68
+import ProfileSeizedDistribution from '../../components/ProfileSeizedDistribution.vue'
69
+import ProfileDailySeizedLine from '../../components/ProfileDailySeizedLine.vue'
70
+import ProfileDailySeizedArea from '../../components/ProfileDailySeizedArea.vue'
71
+import ProfileDailySeizedBar from '../../components/ProfileDailySeizedBar.vue'
72
+
73
+const props = defineProps({
74
+  queryParams: {
75
+    type: Object,
76
+    default: () => ({})
77
+  }
78
+})
79
+
80
+const radarData = ref([
81
+  { name: '通道安全防控力', value: 86, color: '#00e5ff' },
82
+  { name: '通道安全防控力', value: 90, color: '#00e5ff' },
83
+  { name: '通道安全防控力', value: 72, color: '#ff4757' },
84
+  { name: '通道安全防控力', value: 68, color: '#ff4757' },
85
+  { name: '通道安全防控力', value: 78, color: '#3742fa' },
86
+  { name: '通道安全防控力', value: 80, color: '#3742fa' },
87
+  { name: '通道协同作战能力', value: 72, color: '#ff4757' }
88
+])
89
+
90
+const radarIndicators = ref([
91
+  { name: '通道安全防控力', max: 100 },
92
+  { name: '通道安全防控力', max: 100 },
93
+  { name: '通道安全防控力', max: 100 },
94
+  { name: '通道安全防控力', max: 100 },
95
+  { name: '通道安全防控力', max: 100 },
96
+  { name: '通道安全防控力', max: 100 },
97
+  { name: '通道协同作战能力', max: 100 }
98
+])
99
+
100
+const radarSeries1 = ref([86, 90, 72, 68, 78, 80, 72])
101
+const radarSeries2 = ref([80, 85, 65, 60, 72, 75, 68])
102
+
103
+const teamMembers = ref([
104
+  { name: '孙晓波', age: 25, seniority: 3, gender: '男', nation: '汉', political: '群众', position: '安检员', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 5, avgYears: 6, totalScore: 88 },
105
+  { name: '孙晓波', age: 28, seniority: 6, gender: '男', nation: '汉', political: '群众', position: '组长', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 5, avgYears: 5, totalScore: 85 },
106
+  { name: '孙晓波', age: 31, seniority: 2, gender: '女', nation: '汉', political: '党员', position: '组长', qualification: '前传、特检、人身', skillLevel: '四级', operateYears: 5, avgYears: 76, totalScore: 76 },
107
+  { name: '马力', age: 26, seniority: 3, gender: '男', nation: '汉', political: '群众', position: '组长', qualification: '前传、特检、人身', skillLevel: '五级', operateYears: 5, avgYears: 81, totalScore: 81 },
108
+  { name: '马建国', age: 24, seniority: 5, gender: '男', nation: '汉', political: '党员', position: '安检员', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 6, avgYears: 78, totalScore: 78 }
109
+])
110
+
111
+const memberColumns = ref([
112
+  { label: '姓名', prop: 'name' },
113
+  { label: '年龄', prop: 'age' },
114
+  { label: '司龄', prop: 'seniority' },
115
+  { label: '性别', prop: 'gender' },
116
+  { label: '民族', prop: 'nation' },
117
+  { label: '政治面貌', prop: 'political' },
118
+  { label: '职务', prop: 'position' },
119
+  { label: '岗位资质', prop: 'qualification' },
120
+  { label: '职业技能等级', prop: 'skillLevel' },
121
+  { label: '开机年限', prop: 'operateYears' },
122
+  { label: '平均年限', prop: 'avgYears' },
123
+  { label: '综合得分', prop: 'totalScore' }
124
+])
125
+
126
+const genderData = ref([
127
+  { value: 6, name: '女', itemStyle: { color: '#ff6b9d' } },
128
+  { value: 8, name: '男', itemStyle: { color: '#4da6ff' } }
129
+])
130
+
131
+const nationData = ref([
132
+  { value: 1, name: '回族', itemStyle: { color: '#ff9f43' } },
133
+  { value: 8, name: '汉族', itemStyle: { color: '#4da6ff' } },
134
+  { value: 5, name: '其他', itemStyle: { color: '#a55eea' } }
135
+])
136
+
137
+const politicalData = ref([
138
+  { value: 1, name: '党员', itemStyle: { color: '#ff6b6b' } },
139
+  { value: 2, name: '共青团员', itemStyle: { color: '#ffd93d' } },
140
+  { value: 5, name: '群众', itemStyle: { color: '#6bcb77' } },
141
+  { value: 6, name: '其他', itemStyle: { color: '#4d96ff' } }
142
+])
143
+
144
+const skillData = ref({
145
+  categories: ['等级1', '等级2', '等级3', '等级4', '等级5'],
146
+  values: [3, 5, 7, 8, 4],
147
+  colors: ['#4da6ff', '#0f46fa']
148
+})
149
+
150
+const operateData = ref({
151
+  categories: ['0-3', '4-7', '8-11', '12-15', '15-18'],
152
+  values: [4, 2, 3, 5, 8],
153
+  colors: ['#6bcb77', '#2ecc71']
154
+})
155
+
156
+const postData = ref({
157
+  categories: ['前传', '人身', '验证', '开包', '开机'],
158
+  values: [4, 5, 6, 7, 8],
159
+  colors: ['#ff6b6b', '#ee5a24']
160
+})
161
+
162
+const passRatePassData = ref([370, 352, 330, 387, 426, 342, 325, 384, 445, 407, 382, 401, 380, 464, 342, 443, 305, 394, 334, 430, 364, 362, 400, 420])
163
+const passRateLineData = ref([4.5, 5, 4, 6, 7, 5, 4, 6, 8, 7, 6, 7, 6, 8, 5, 7, 4, 6, 5, 7, 6, 6, 6.5, 7])
164
+
165
+const seizedTotal = ref(1658)
166
+
167
+const seizedDistributionData = ref([
168
+  { value: 20, name: '打火机', itemStyle: { color: '#ff9f43' } },
169
+  { value: 30, name: '管制刀具', itemStyle: { color: '#ff6b6b' } },
170
+  { value: 50, name: '其他违禁品', itemStyle: { color: '#4da6ff' } }
171
+])
172
+
173
+const dailyLineData = ref([120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330])
174
+const dailyAreaPersonA = ref([120, 132, 101, 134, 90, 230, 210, 182, 191, 234, 290, 330])
175
+const dailyAreaPersonB = ref([220, 182, 191, 234, 290, 330, 310, 123, 144, 210, 182, 191])
176
+const dailyAreaPersonC = ref([150, 232, 201, 154, 190, 330, 410, 201, 154, 190, 330, 410])
177
+const dailyAreaPersonD = ref([320, 332, 301, 334, 390, 330, 320, 332, 301, 334, 390, 330])
178
+const dailyAreaPersonE = ref([820, 932, 901, 934, 1290, 1330, 1320, 820, 932, 901, 934, 1290])
179
+const dailyBarData = ref([120, 200, 150, 80, 70, 110, 130, 90, 160, 140, 180])
180
+
181
+watch(() => props.queryParams, (newParams) => {
182
+  fetchData(newParams)
183
+}, { deep: true })
184
+
185
+const fetchData = (params) => {
186
+  console.log('查询参数:', params)
187
+}
188
+</script>
189
+
190
+<style lang="scss" scoped>
191
+.main-content-wrapper {
192
+  .main-content {
193
+    display: flex;
194
+    flex-direction: column;
195
+    gap: 20px;
196
+    padding: 0 20px 40px;
197
+  }
198
+
199
+  .content-row {
200
+    display: flex;
201
+    gap: 20px;
202
+
203
+    > .info-card {
204
+      flex: 1;
205
+      min-width: 0;
206
+    }
207
+  }
208
+}
209
+</style>

+ 143 - 0
src/views/portraitManagement/teamProfile/component/runData.vue

@@ -0,0 +1,143 @@
1
+<template>
2
+  <div class="operation-data">
3
+    <div class="row">
4
+      <ChannelCheckChart :chartsData="chartsData" />
5
+    </div>
6
+    <div class="row">
7
+      <div class="col">
8
+        <SeizedInfo :chartsData="chartsData"/>
9
+      </div>
10
+      <div class="col">
11
+        <SeizedItems :chartsData="chartsData" />
12
+      </div>
13
+    </div>
14
+    <div class="row">
15
+      <div class="col">
16
+        <SeizedNumAll :chartsData="chartsData"/>
17
+      </div>
18
+      <div class="col">
19
+        <SeizedNum :chartsData="chartsData" />
20
+      </div>
21
+    </div>
22
+    <div class="row">
23
+      <SeizedWorkArea :chartsData="chartsData" />
24
+    </div>
25
+    <div class="row">
26
+      <div class="col">
27
+        <EventItems :chartsData="chartsData"  />
28
+      </div>
29
+      <div class="col">
30
+        <EventType :chartsData="chartsData"  />
31
+      </div>
32
+      <div class="col">
33
+        <EventWorkArea :chartsData="chartsData"  />
34
+      </div>
35
+    </div>
36
+    <div class="row">
37
+      <div class="col">
38
+        <TestItems :chartsData="chartsData"  />
39
+      </div>
40
+      <div class="col">
41
+        <TestResult :chartsData="chartsData"  />
42
+      </div>
43
+      <div class="col">
44
+        <TestArea :chartsData="chartsData"  />
45
+      </div>
46
+    </div>
47
+    <div class="row">
48
+      <div class="col">
49
+        <SupervisionDistribution :chartsData="chartsData"/>
50
+      </div>
51
+      <div class="col">
52
+        <InterceptionDistribution :chartsData="chartsData" />
53
+      </div>
54
+    </div>
55
+  </div>
56
+</template>
57
+
58
+<script setup>
59
+import ChannelCheckChart from '../../components/ChannelCheckChart.vue';
60
+import SeizedInfo from '../../components/SeizedInfo.vue';
61
+import SeizedItems from '../../components/SeizedItems.vue';
62
+import SeizedNumAll from '../../components/SeizedNumAll.vue';
63
+import SeizedNum from '../../components/SeizedNum.vue';
64
+import SeizedWorkArea from '../../components/SeizedWorkArea.vue';
65
+import EventItems from '../../components/EventItems.vue';
66
+import EventType from '../../components/EventType.vue';
67
+import EventWorkArea from '../../components/EventWorkArea.vue';
68
+import TestItems from '../../components/TestItems.vue';
69
+import TestResult from '../../components/TestResult.vue';
70
+import TestArea from '../../components/TestArea.vue';
71
+import SupervisionDistribution from '../../components/SupervisionDistribution.vue';
72
+import InterceptionDistribution from '../../components/InterceptionDistribution.vue';
73
+
74
+
75
+const chartsData = ref([
76
+  {
77
+    num: 370,
78
+    num1: 330,
79
+    num2: 120,
80
+    num3: 470,
81
+    num4: 570,
82
+    rate: 30,
83
+    name: '打火机'
84
+  },
85
+  {
86
+    num: 330,
87
+    num1: 430,
88
+    num2: 420,
89
+    num3: 430,
90
+    num4: 150,
91
+    rate: 60,
92
+    name: '火柴'
93
+  },
94
+  {
95
+    num: 410,
96
+    num1: 310,
97
+    num2: 220,
98
+    num3: 370,
99
+    num4: 510,
100
+    rate: 35,
101
+    name: '刀具'
102
+  },
103
+  {
104
+    num: 570,
105
+    num1: 440,
106
+    num2: 420,
107
+    num3: 410,
108
+    num4: 470,
109
+    rate: 80,
110
+    name: '充电宝'
111
+  },
112
+  {
113
+    num: 340,
114
+    num1: 330,
115
+    num2: 620,
116
+    num3: 370,
117
+    num4: 170,
118
+    rate: 60,
119
+    name: '烟火'
120
+  },
121
+])
122
+  
123
+
124
+</script>
125
+
126
+<style lang="scss" scoped>
127
+.operation-data {
128
+  padding: 20px;
129
+  .row {
130
+    display: flex;
131
+    column-gap: 15px;
132
+    height: 360px;
133
+    margin-bottom: 15px;
134
+    .col {
135
+      flex: 1;
136
+      height: 100%;
137
+      :deep(.info-card) {
138
+        height: 100%;
139
+      }
140
+    }
141
+  }
142
+}
143
+</style>

+ 174 - 1
src/views/portraitManagement/teamProfile/index.vue

@@ -1 +1,174 @@
1
-<template></template>
1
+<template>
2
+  <div class="group-profile-page">
3
+    <Page title="安检人事管理可视化大屏" :tabs="['能力画像', '运行数据']" @tab-change="handleTabChange">
4
+      <SearchBar v-model:visible="searchVisible" @search="handleSearch" />
5
+      <PentagonGroup :items="pentagonItems" />
6
+      <Profile v-if="activeTab === 0" :query-params="queryParams" />
7
+      <RunData v-else />
8
+    </Page>
9
+
10
+  </div>
11
+</template>
12
+
13
+<script setup>
14
+import { ref } from 'vue'
15
+import Page from '../components/page.vue'
16
+import SearchBar from '../components/SearchBar.vue'
17
+import Profile from './component/profile.vue'
18
+import RunData from './component/runData.vue'
19
+import PentagonGroup from '../components/PentagonGroup.vue'
20
+import zongheIcon from '@/assets/icons/portrait/zonghe_icon.png'
21
+import renyuanIcon from '@/assets/icons/portrait/renyuan_icon.png'
22
+import zu02Icon from '@/assets/icons/portrait/zu02_icon.png'
23
+import nianlingIcon from '@/assets/icons/portrait/nianling_icon.png'
24
+import silingIcon from '@/assets/icons/portrait/siling_icon.png'
25
+
26
+const activeTab = ref(0)
27
+const searchVisible = ref(false)
28
+const queryParams = ref({})
29
+
30
+const pentagonItems = ref([
31
+  {
32
+    value: '88',
33
+    label: '综合得分',
34
+    iconPath: zongheIcon,
35
+    cornerRadius: 20,
36
+    vertices: {
37
+      topLeft: { x: 0, y: 0 },
38
+      topRight: { x: 300, y: 0 },
39
+      bottomRight: { x: 260, y: 200 },
40
+      bottomLeft: { x: 0, y: 200 }
41
+    },
42
+    cardContentStyle: {
43
+      gap: '100px'
44
+    },
45
+    edgeGradients: {
46
+      top: { start: '#0f46fa', end: 'transparent' },
47
+      right: { start: 'transparent', end: '#9903C1' },
48
+      bottom: { start: '#9903C1', end: 'transparent' },
49
+      left: { start: 'transparent', end: '#0f46fa' }
50
+    },
51
+    bgGradientStart: 'rgba(33,33,58,0.95)',
52
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
53
+  },
54
+  {
55
+    value: '10',
56
+    label: '人员数量',
57
+    iconPath: renyuanIcon,
58
+    cornerRadius: 20,
59
+    vertices: {
60
+      topLeft: { x: 10, y: 0 },
61
+      topRight: { x: 300, y: 0 },
62
+      bottomRight: { x: 260, y: 200 },
63
+      bottomLeft: { x: -30, y: 200 }
64
+    }, cardContentStyle: {
65
+      gap: '100px'
66
+    },
67
+    edgeGradients: {
68
+      top: { start: '#0f46fa', end: 'transparent' },
69
+      right: { start: 'transparent', end: '#9903C1' },
70
+      bottom: { start: '#9903C1', end: 'transparent' },
71
+      left: { start: 'transparent', end: '#0f46fa' }
72
+    },
73
+    bgGradientStart: 'rgba(33,33,58,0.95)',
74
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
75
+  },
76
+  {
77
+    value: '周游波小组',
78
+    label: '',
79
+    iconPath: zu02Icon,
80
+    iconStyle: {
81
+      width: '250px',
82
+      height: '250px'
83
+    },
84
+    valueStyle: {
85
+      fontSize: '48px',
86
+      bottom: '26px',
87
+      position: 'relative'
88
+    },
89
+    vertical: true,
90
+    cornerRadius: 20,
91
+    vertices: {
92
+      topLeft: { x: 10, y: 0 },
93
+      topRight: { x: 280, y: 0 },
94
+      bottomRight: { x: 320, y: 200 },
95
+      bottomLeft: { x: -30, y: 200 }
96
+    },
97
+    cardContentStyle: {
98
+      gap: '0px',
99
+      position: 'relative',
100
+      bottom: '65px'
101
+    },
102
+    edgeGradients: {
103
+      top: { start: '#0f46fa', end: 'transparent' },
104
+      right: { start: 'transparent', end: '#9903C1' },
105
+      bottom: { start: '#9903C1', end: 'transparent' },
106
+      left: { start: 'transparent', end: '#0f46fa' }
107
+    },
108
+    bgGradientStart: 'rgba(33,33,58,0.95)',
109
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
110
+  },
111
+  {
112
+    value: '28',
113
+    label: '平均年龄',
114
+    iconPath: nianlingIcon,
115
+    cornerRadius: 20,
116
+    vertices: {
117
+      topLeft: { x: -10, y: 0 },
118
+      topRight: { x: 290, y: 0 },
119
+      bottomRight: { x: 330, y: 200 },
120
+      bottomLeft: { x: 30, y: 200 }
121
+    },
122
+    cardContentStyle: {
123
+      gap: '100px'
124
+    },
125
+    edgeGradients: {
126
+      top: { start: '#0f46fa', end: 'transparent' },
127
+      right: { start: 'transparent', end: '#9903C1' },
128
+      bottom: { start: '#9903C1', end: 'transparent' },
129
+      left: { start: 'transparent', end: '#0f46fa' }
130
+    },
131
+    bgGradientStart: 'rgba(33,33,58,0.95)',
132
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
133
+  },
134
+  {
135
+    value: '3.5',
136
+    label: '平均司龄',
137
+    iconPath: silingIcon,
138
+    cornerRadius: 20,
139
+    vertices: {
140
+      topLeft: { x: 0, y: 0 },
141
+      topRight: { x: 300, y: 0 },
142
+      bottomRight: { x: 300, y: 200 },
143
+      bottomLeft: { x: 40, y: 200 }
144
+    },
145
+    cardContentStyle: {
146
+      gap: '100px'
147
+    },
148
+    edgeGradients: {
149
+      top: { start: '#0f46fa', end: 'transparent' },
150
+      right: { start: 'transparent', end: '#9903C1' },
151
+      bottom: { start: '#9903C1', end: 'transparent' },
152
+      left: { start: 'transparent', end: '#0f46fa' }
153
+    },
154
+    bgGradientStart: 'rgba(33,33,58,0.95)',
155
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
156
+  }
157
+])
158
+
159
+const handleSearch = (params) => {
160
+  queryParams.value = params
161
+}
162
+
163
+const handleTabChange = (index) => {
164
+  activeTab.value = index
165
+}
166
+</script>
167
+
168
+<style lang="scss" scoped>
169
+.group-profile-page {
170
+  width: 100%;
171
+  min-height: 100vh;
172
+  overflow-y: auto;
173
+}
174
+</style>