Bladeren bron

refactor: 重构多个页面和组件,优化界面与功能

1. 重构部门人员查询接口,支持传入参数
2. 重构指标树接口,统一参数传递方式
3. 优化肖像管理页面布局与样式
4. 新增滚动表格组件替换旧静态表格
5. 完善配分管理页面的层级筛选功能
6. 优化事件管理页面的表单与数据联动
7. 重构运行数据页面,移除冗余图表并新增值班信息面板
huoyi 4 weken geleden
bovenliggende
commit
820ca7e84c

+ 2 - 2
src/api/score/index.js

@@ -27,8 +27,8 @@ export function exportDimension(params) {
27 27
 export function listIndicator(params) {
28 28
   return request({ url: '/score/indicator/list', method: 'get', params })
29 29
 }
30
-export function treeIndicator(dimensionId) {
31
-  return request({ url: '/score/indicator/tree', method: 'get', params: { dimensionId } })
30
+export function treeIndicator(params) {
31
+  return request({ url: '/score/indicator/tree', method: 'get', params })
32 32
 }
33 33
 export function getIndicator(id) {
34 34
   return request({ url: `/score/indicator/${id}`, method: 'get' })

+ 3 - 2
src/api/system/user.js

@@ -137,10 +137,11 @@ export function updateAuthRole(data) {
137 137
 }
138 138
 
139 139
 // 查询部门下拉树结构
140
-export function deptTreeSelect() {
140
+export function deptTreeSelect(params) {
141 141
   return request({
142 142
     url: '/system/user/deptTree',
143
-    method: 'get'
143
+    method: 'get',
144
+    params: params
144 145
   })
145 146
 }
146 147
 //获取所有部门和班组下人员

+ 9 - 3
src/views/portraitManagement/components/page.vue

@@ -75,8 +75,9 @@ const handleTabClick = (index) => {
75 75
     align-items: center;
76 76
     height: 88px;
77 77
     background: #2b394d86;
78
-    position: relative;
79 78
     margin-bottom: 2px;
79
+    position: relative;
80
+    justify-content: center;
80 81
     &::before {
81 82
       content: '';
82 83
       position: absolute;
@@ -91,8 +92,13 @@ const handleTabClick = (index) => {
91 92
     }
92 93
   }
93 94
   .left-tab {
95
+    position: absolute;
96
+    left: 0;
97
+    top: 0;
94 98
     display: flex;
95 99
     padding: 0 20px;
100
+    z-index: 1;
101
+    height: 88px;
96 102
     .tab-item {
97 103
       padding: 0 20px;
98 104
       height: 88px;
@@ -101,7 +107,7 @@ const handleTabClick = (index) => {
101 107
       font-size: 16px;
102 108
       cursor: pointer;
103 109
       transition: all 0.3s;
104
-     
110
+
105 111
       &:hover {
106 112
         background: rgba(15, 70, 250, 0.1);
107 113
         color: #fff;
@@ -114,13 +120,13 @@ const handleTabClick = (index) => {
114 120
     }
115 121
   }
116 122
   .title {
117
-    flex: 1;
118 123
     height: 88px;
119 124
     line-height: 88px;
120 125
     text-align: center;
121 126
     color: #fff;
122 127
     font-size: 48px;
123 128
     font-weight: bold;
129
+    white-space: nowrap;
124 130
   }
125 131
   .content {
126 132
     flex: 1;

+ 157 - 0
src/views/portraitManagement/components/rollingTable.vue

@@ -0,0 +1,157 @@
1
+<template>
2
+  <div class="rolling-table">
3
+    <div class="table-header">
4
+      <div class="header-row">
5
+        <div class="header-cell" v-for="(col, index) in columns" :key="index">{{ col.label }}</div>
6
+      </div>
7
+    </div>
8
+    <div class="table-body">
9
+      <div class="scroll-content" ref="scrollContentRef">
10
+        <div class="body-row" v-for="(row, rowIndex) in scrollData" :key="rowIndex">
11
+          <div class="body-cell" v-for="(col, cellIndex) in columns" :key="cellIndex">{{ row[col.prop] }}</div>
12
+        </div>
13
+      </div>
14
+    </div>
15
+  </div>
16
+</template>
17
+
18
+<script setup>
19
+import { ref, computed, onMounted, onUnmounted, nextTick } from 'vue'
20
+
21
+const props = defineProps({
22
+  columns: {
23
+    type: Array,
24
+    default: () => []
25
+  },
26
+  data: {
27
+    type: Array,
28
+    default: () => []
29
+  },
30
+  duration: {
31
+    type: Number,
32
+    default: 20
33
+  }
34
+})
35
+
36
+const scrollContentRef = ref(null)
37
+let animationId = null
38
+
39
+const scrollData = computed(() => {
40
+  if (!props.data || props.data.length === 0) return []
41
+  return [...props.data, ...props.data]
42
+})
43
+
44
+const startScroll = () => {
45
+  const content = scrollContentRef.value
46
+  if (!content || props.data.length <= 0) return
47
+
48
+  let lastTime = performance.now()
49
+  let currentScrollY = 0
50
+  content.style.transform = 'translateY(0px)'
51
+
52
+  const scroll = (currentTime) => {
53
+    if (!content) {
54
+      animationId = requestAnimationFrame(scroll)
55
+      return
56
+    }
57
+
58
+    const halfHeight = content.scrollHeight / 2
59
+    if (halfHeight <= 0) {
60
+      animationId = requestAnimationFrame(scroll)
61
+      return
62
+    }
63
+
64
+    const step = halfHeight / (props.duration * 60)
65
+    const delta = currentTime - lastTime
66
+    lastTime = currentTime
67
+
68
+    currentScrollY += step * (delta / 16.67)
69
+
70
+    if (currentScrollY >= halfHeight) {
71
+      currentScrollY = 0
72
+    }
73
+
74
+    content.style.transform = `translateY(${-currentScrollY}px)`
75
+
76
+    animationId = requestAnimationFrame(scroll)
77
+  }
78
+
79
+  animationId = requestAnimationFrame(scroll)
80
+}
81
+
82
+const stopScroll = () => {
83
+  if (animationId) {
84
+    cancelAnimationFrame(animationId)
85
+    animationId = null
86
+  }
87
+}
88
+
89
+onMounted(() => {
90
+  if (props.data.length > 0) {
91
+    nextTick(() => {
92
+      startScroll()
93
+    })
94
+  }
95
+})
96
+
97
+onUnmounted(() => {
98
+  stopScroll()
99
+})
100
+</script>
101
+
102
+<style lang="scss" scoped>
103
+.rolling-table {
104
+  width: 100%;
105
+  overflow: hidden;
106
+}
107
+
108
+.table-header {
109
+  .header-row {
110
+    display: flex;
111
+    background: linear-gradient(90deg, #1a2058, #1c2a6e);
112
+    border-radius: 6px;
113
+    margin-bottom: 4px;
114
+  }
115
+  .header-cell {
116
+    flex: 1;
117
+    padding: 10px 6px;
118
+    text-align: center;
119
+    color: #70CFE7;
120
+    font-size: 13px;
121
+    font-weight: bold;
122
+    white-space: nowrap;
123
+  }
124
+}
125
+
126
+.table-body {
127
+  overflow: hidden;
128
+  max-height: 300px;
129
+}
130
+
131
+.scroll-content {
132
+  display: flex;
133
+  flex-direction: column;
134
+  gap: 2px;
135
+  will-change: transform;
136
+}
137
+
138
+.body-row {
139
+  display: flex;
140
+  background: linear-gradient(90deg, #1E2553, #242D66, #381C4F);
141
+  border-radius: 4px;
142
+  margin-bottom: 2px;
143
+
144
+  &:last-child {
145
+    margin-bottom: 0;
146
+  }
147
+}
148
+
149
+.body-cell {
150
+  flex: 1;
151
+  padding: 8px 6px;
152
+  text-align: center;
153
+  color: #fff;
154
+  font-size: 13px;
155
+  white-space: nowrap;
156
+}
157
+</style>

+ 70 - 67
src/views/portraitManagement/groupProfile/component/profile.vue

@@ -10,50 +10,21 @@
10 10
             <div class="radar-list">
11 11
               <div class="radar-item" v-for="(item, index) in radarData" :key="index">
12 12
                 <span class="item-label">{{ item.name }}</span>
13
-                <div class="progress-bar">
14
-                  <div class="progress-fill" :style="{ width: item.value + '%' }"></div>
13
+                <div class="progress-row">
14
+                  <div class="progress-bar">
15
+                    <div class="progress-fill" :style="{ width: item.value + '%', background: `linear-gradient(to right, transparent, ${item.color})` }">
16
+                      <span class="progress-end"></span>
17
+                    </div>
18
+                  </div>
19
+                  <span class="item-value">{{ item.value }}</span>
15 20
                 </div>
16
-                <span class="item-value">{{ item.value }}</span>
17 21
               </div>
18 22
             </div>
19 23
           </div>
20 24
         </InfoCard>
21 25
         <InfoCard title="团队成员">
22 26
           <div class="table-container">
23
-            <table class="member-table">
24
-              <thead>
25
-                <tr>
26
-                  <th>姓名</th>
27
-                  <th>年龄</th>
28
-                  <th>司龄</th>
29
-                  <th>性别</th>
30
-                  <th>民族</th>
31
-                  <th>政治面貌</th>
32
-                  <th>职务</th>
33
-                  <th>岗位资质</th>
34
-                  <th>职业技能等级</th>
35
-                  <th>开机年限</th>
36
-                  <th>平均年限</th>
37
-                  <th>综合得分</th>
38
-                </tr>
39
-              </thead>
40
-              <tbody>
41
-                <tr v-for="(member, index) in teamMembers" :key="index">
42
-                  <td>{{ member.name }}</td>
43
-                  <td>{{ member.age }}</td>
44
-                  <td>{{ member.seniority }}</td>
45
-                  <td>{{ member.gender }}</td>
46
-                  <td>{{ member.nation }}</td>
47
-                  <td>{{ member.political }}</td>
48
-                  <td>{{ member.position }}</td>
49
-                  <td>{{ member.qualification }}</td>
50
-                  <td>{{ member.skillLevel }}</td>
51
-                  <td>{{ member.operateYears }}</td>
52
-                  <td>{{ member.avgYears }}</td>
53
-                  <td>{{ member.totalScore }}</td>
54
-                </tr>
55
-              </tbody>
56
-            </table>
27
+            <RollingTable :columns="memberColumns" :data="teamMembers" :duration="20" />
57 28
           </div>
58 29
         </InfoCard>
59 30
       </div>
@@ -145,6 +116,7 @@
145 116
 import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue'
146 117
 import * as echarts from 'echarts'
147 118
 import InfoCard from '../../components/card.vue'
119
+import RollingTable from '../../components/rollingTable.vue'
148 120
 
149 121
 
150 122
 const props = defineProps({
@@ -157,13 +129,13 @@ const props = defineProps({
157 129
 
158 130
 
159 131
 const radarData = ref([
160
-  { name: '通道安全防控力', value: 86 },
161
-  { name: '通道安全防控力', value: 90 },
162
-  { name: '通道安全防控力', value: 72 },
163
-  { name: '通道安全防控力', value: 68 },
164
-  { name: '通道安全防控力', value: 78 },
165
-  { name: '通道安全防控力', value: 80 },
166
-  { name: '通道协同作战能力', value: 72 }
132
+  { name: '通道安全防控力', value: 86, color: '#00e5ff' },
133
+  { name: '通道安全防控力', value: 90, color: '#00e5ff' },
134
+  { name: '通道安全防控力', value: 72, color: '#ff4757' },
135
+  { name: '通道安全防控力', value: 68, color: '#ff4757' },
136
+  { name: '通道安全防控力', value: 78, color: '#3742fa' },
137
+  { name: '通道安全防控力', value: 80, color: '#3742fa' },
138
+  { name: '通道协同作战能力', value: 72, color: '#ff4757' }
167 139
 ])
168 140
 
169 141
 const teamMembers = ref([
@@ -174,6 +146,21 @@ const teamMembers = ref([
174 146
   { name: '马建国', age: 24, seniority: 5, gender: '男', nation: '汉', political: '党员', position: '安检员', qualification: '前传、特检、人身', skillLevel: '三级', operateYears: 6, avgYears: 78, totalScore: 78 }
175 147
 ])
176 148
 
149
+const memberColumns = [
150
+  { label: '姓名', prop: 'name' },
151
+  { label: '年龄', prop: 'age' },
152
+  { label: '司龄', prop: 'seniority' },
153
+  { label: '性别', prop: 'gender' },
154
+  { label: '民族', prop: 'nation' },
155
+  { label: '政治面貌', prop: 'political' },
156
+  { label: '职务', prop: 'position' },
157
+  { label: '岗位资质', prop: 'qualification' },
158
+  { label: '职业技能等级', prop: 'skillLevel' },
159
+  { label: '开机年限', prop: 'operateYears' },
160
+  { label: '平均年限', prop: 'avgYears' },
161
+  { label: '综合得分', prop: 'totalScore' }
162
+]
163
+
177 164
 // 监听查询参数变化,重新请求数据
178 165
 watch(() => props.queryParams, (newParams) => {
179 166
   fetchData(newParams)
@@ -888,41 +875,57 @@ onUnmounted(() => {
888 875
       flex: 1;
889 876
       display: flex;
890 877
       flex-direction: column;
891
-      gap: 12px;
878
+      gap: 16px;
892 879
       padding: 10px 0;
893 880
       
894 881
       .radar-item {
895 882
         display: flex;
896
-        align-items: center;
897
-        gap: 10px;
883
+        flex-direction: column;
884
+        gap: 6px;
898 885
         font-size: 13px;
899 886
         
900 887
         .item-label {
901
-          width: 140px;
902 888
           color: #a0c4ff;
903
-          flex-shrink: 0;
904 889
         }
905 890
         
906
-        .progress-bar {
907
-          flex: 1;
908
-          height: 10px;
909
-          background: rgba(15, 70, 250, 0.2);
910
-          border-radius: 4px;
911
-          overflow: hidden;
891
+        .progress-row {
892
+          display: flex;
893
+          align-items: center;
894
+          gap: 10px;
912 895
           
913
-          .progress-fill {
914
-            height: 100%;
915
-            background: linear-gradient(90deg, #0f46fa, #bd03fb);
916
-            border-radius: 4px;
917
-            transition: width 0.3s ease;
896
+          .progress-bar {
897
+            flex: 1;
898
+            height: 5px;
899
+            background: rgba(255, 255, 255, 0.1);
900
+            border-radius: 0;
901
+            overflow: visible;
902
+            
903
+            .progress-fill {
904
+              height: 100%;
905
+              border-radius: 0;
906
+              transition: width 0.3s ease;
907
+              position: relative;
908
+              
909
+              .progress-end {
910
+                position: absolute;
911
+                right: -4px;
912
+                top: 50%;
913
+                transform: translateY(-50%);
914
+                width: 3px;
915
+                height: 9px;
916
+                background: #fff;
917
+                border-radius: 2px;
918
+              }
919
+            }
920
+          }
921
+          
922
+          .item-value {
923
+            width: 45px;
924
+            text-align: right;
925
+            color: #fff;
926
+            font-weight: bold;
927
+            font-size: 13px;
918 928
           }
919
-        }
920
-        
921
-        .item-value {
922
-          width: 45px;
923
-          text-align: right;
924
-          color: #fff;
925
-          font-weight: bold;
926 929
         }
927 930
       }
928 931
     }

+ 25 - 84
src/views/portraitManagement/stationProfile/component/profile.vue

@@ -7,59 +7,12 @@
7 7
 
8 8
       <div class="content-row">
9 9
         <InfoCard title="团队成员">
10
-          <div class="team-members-table">
11
-            <table>
12
-              <thead>
13
-                <tr>
14
-                  <th>部门</th>
15
-                  <th>员工数量</th>
16
-                  <th>党员数量</th>
17
-                  <th>平均年龄</th>
18
-                  <th>平均工龄</th>
19
-                  <th>职业资格证书等级</th>
20
-                  <th>平均升级年龄</th>
21
-                  <th>综合得分</th>
22
-                </tr>
23
-              </thead>
24
-              <tbody>
25
-                <tr>
26
-                  <td>旅检一部</td>
27
-                  <td>800</td>
28
-                  <td>7</td>
29
-                  <td>25</td>
30
-                  <td>3</td>
31
-                  <td>16</td>
32
-                  <td>3</td>
33
-                  <td>90</td>
34
-                </tr>
35
-                <tr>
36
-                  <td>旅检二部</td>
37
-                  <td>756</td>
38
-                  <td>2</td>
39
-                  <td>28</td>
40
-                  <td>8</td>
41
-                  <td>18</td>
42
-                  <td>5</td>
43
-                  <td>88</td>
44
-                </tr>
45
-                <tr>
46
-                  <td>旅检三部</td>
47
-                  <td>708</td>
48
-                  <td>7</td>
49
-                  <td>25</td>
50
-                  <td>3</td>
51
-                  <td>23</td>
52
-                  <td>3</td>
53
-                  <td>86</td>
54
-                </tr>
55
-              </tbody>
56
-            </table>
57
-          </div>
10
+          <RollingTable 
11
+            :columns="teamColumns"
12
+            :data="teamData"
13
+          />
58 14
         </InfoCard>
59
-      </div>
60
-
61
-      <div class="content-row">
62
-        <InfoCard title="成员基本情况分布">
15
+         <InfoCard title="成员基本情况分布">
63 16
           <div class="member-distribution">
64 17
             <div class="dist-item">
65 18
               <h4>性别分布</h4>
@@ -77,7 +30,9 @@
77 30
         </InfoCard>
78 31
       </div>
79 32
 
80
-      <div class="content-row">
33
+      
34
+
35
+      <div class="content-row" style="width: 50%;">
81 36
         <InfoCard title="成员职位情况分布">
82 37
           <div class="position-distribution">
83 38
             <div class="dist-item">
@@ -103,6 +58,7 @@
103 58
 import { ref, onMounted, onUnmounted, nextTick } from 'vue'
104 59
 import * as echarts from 'echarts'
105 60
 import InfoCard from '../../components/card.vue'
61
+import RollingTable from '../../components/rollingTable.vue'
106 62
 
107 63
 
108 64
 
@@ -113,6 +69,22 @@ const props = defineProps({
113 69
   }
114 70
 })
115 71
 
72
+const teamColumns = [
73
+  { label: '部门', prop: 'dept' },
74
+  { label: '员工数量', prop: 'empCount' },
75
+  { label: '党员数量', prop: 'partyCount' },
76
+  { label: '平均年龄', prop: 'avgAge' },
77
+  { label: '平均工龄', prop: 'avgWorkYears' },
78
+  { label: '职业资格证书等级', prop: 'certLevel' },
79
+  { label: '平均升级年龄', prop: 'avgUpgradeAge' },
80
+  { label: '综合得分', prop: 'totalScore' }
81
+]
82
+
83
+const teamData = ref([
84
+  { dept: '旅检一部', empCount: '800', partyCount: '7', avgAge: '25', avgWorkYears: '3', certLevel: '16', avgUpgradeAge: '3', totalScore: '90' },
85
+  { dept: '旅检二部', empCount: '756', partyCount: '2', avgAge: '28', avgWorkYears: '8', certLevel: '18', avgUpgradeAge: '5', totalScore: '88' },
86
+  { dept: '旅检三部', empCount: '708', partyCount: '7', avgAge: '25', avgWorkYears: '3', certLevel: '23', avgUpgradeAge: '3', totalScore: '86' }
87
+])
116 88
 
117 89
 const genderDistChartRef = ref(null)
118 90
 let genderDistChart = null
@@ -384,37 +356,6 @@ onUnmounted(() => {
384 356
     }
385 357
   }
386 358
 
387
-  .team-members-table {
388
-    width: 100%;
389
-    overflow-x: auto;
390
-
391
-    table {
392
-      width: 100%;
393
-      border-collapse: collapse;
394
-      color: #fff;
395
-
396
-      th, td {
397
-        padding: 12px 15px;
398
-        text-align: center;
399
-        border: 1px solid rgba(15, 70, 250, 0.3);
400
-      }
401
-
402
-      th {
403
-        background: rgba(15, 70, 250, 0.2);
404
-        color: #a0c4ff;
405
-        font-weight: 600;
406
-      }
407
-
408
-      td {
409
-        background: rgba(33, 33, 58, 0.5);
410
-      }
411
-
412
-      tbody tr:hover td {
413
-        background: rgba(15, 70, 250, 0.3);
414
-      }
415
-    }
416
-  }
417
-
418 359
   .member-distribution,
419 360
   .position-distribution {
420 361
     display: flex;

+ 170 - 447
src/views/portraitManagement/stationProfile/component/runData.vue

@@ -4,35 +4,41 @@
4 4
       <InfoCard title="当天开航每小时各区过检人数组合图">
5 5
         <div ref="hourlyPassChartRef" class="hourly-pass-chart"></div>
6 6
       </InfoCard>
7
-    </div>
7
+      <div class="info-panel">
8
+   
9
+        <div class="panel-body">
10
+          <div class="datetime-row">
11
+            <span class="info-date">{{ currentDate }}</span>
8 12
 
9
-    <div class="content-row">
10
-      <InfoCard title="每日各时段查获物品趋势图">
11
-        <div ref="dailyTrendChartRef" class="daily-trend-chart"></div>
12
-      </InfoCard>
13
-      <InfoCard title="各区每日查获物品数量对比">
14
-        <div ref="areaCompareChartRef" class="area-compare-chart"></div>
15
-      </InfoCard>
16
-    </div>
13
+          </div>
14
+          <div class="date-row">
15
+            <span class="info-weekday">{{ currentWeekday }}</span>
16
+            <span class="time-row">{{ currentTime }}</span>
17
+          </div>
17 18
 
18
-    <div class="content-row">
19
-      <InfoCard title="各区当日查获物品分布">
20
-        <div class="area-distribution">
21
-          <div class="dist-item" v-for="(item, index) in areaDistData" :key="index">
22
-            <div :ref="el => areaDistChartRefs[index] = el" class="dist-chart"></div>
23
-            <div class="dist-label">{{ item.name }}</div>
19
+       
20
+          <div class="duty-item">
21
+            <span class="duty-label">值班领导:</span>
22
+            <span class="duty-value">{{ dutyLeader }}</span>
23
+          </div>
24
+          <div class="duty-item">
25
+            <span class="duty-label">安全板块:</span>
26
+            <span class="duty-value">{{ safetySection }}</span>
27
+          </div>
28
+          <div class="duty-item">
29
+            <span class="duty-label">服务板块:</span>
30
+            <span class="duty-value">{{ serviceSection }}</span>
31
+          </div>
32
+          <div class="duty-item">
33
+            <span class="duty-label">运行板块:</span>
34
+            <span class="duty-value">{{ operationSection }}</span>
35
+          </div>
36
+          <div class="duty-item">
37
+            <span class="duty-label">备勤值班:</span>
38
+            <span class="duty-value">{{ standbyDuty }}</span>
24 39
           </div>
25 40
         </div>
26
-      </InfoCard>
27
-    </div>
28
-
29
-    <div class="content-row">
30
-      <InfoCard title="近一周各区查获总数趋势">
31
-        <div ref="weeklyTrendChartRef" class="weekly-trend-chart"></div>
32
-      </InfoCard>
33
-      <InfoCard title="当月各类型物品查获对比">
34
-        <div ref="monthlyTypeChartRef" class="monthly-type-chart"></div>
35
-      </InfoCard>
41
+      </div>
36 42
     </div>
37 43
 
38 44
     <div class="content-row">
@@ -65,11 +71,7 @@
65 71
       </InfoCard>
66 72
     </div>
67 73
 
68
-    <div class="content-row">
69
-      <InfoCard title="每日查获数量(总表)">
70
-        <div ref="dailySeizedBarRef" class="daily-bar-chart"></div>
71
-      </InfoCard>
72
-    </div>
74
+
73 75
 
74 76
     <div class="content-row">
75 77
       <InfoCard title="查获工作区域分布">
@@ -112,30 +114,50 @@ const props = defineProps({
112 114
   queryParams: {
113 115
     type: Object,
114 116
     default: () => ({})
117
+  },
118
+  dutyLeader: {
119
+    type: String,
120
+    default: '张某某'
121
+  },
122
+  safetySection: {
123
+    type: String,
124
+    default: '李某某'
125
+  },
126
+  serviceSection: {
127
+    type: String,
128
+    default: '王某某'
129
+  },
130
+  operationSection: {
131
+    type: String,
132
+    default: '赵某某'
133
+  },
134
+  standbyDuty: {
135
+    type: String,
136
+    default: '刘某某'
115 137
   }
116 138
 })
117 139
 
118
-const areaDistData = ref([
119
-  { name: 'T3国内出发(4层)' },
120
-  { name: 'T3(8层出发)' },
121
-  { name: 'T3国际出发' },
122
-  { name: 'T3国际转国内' },
123
-  { name: '旅检A区' }
124
-])
125
-
126
-const areaDistChartRefs = ref([])
127
-const areaDistCharts = ref([])
140
+const currentDate = ref('')
141
+const currentWeekday = ref('')
142
+const currentTime = ref('')
143
+const weekdays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
144
+let dateTimer = null
145
+
146
+const updateDateTime = () => {
147
+  const now = new Date()
148
+  const year = now.getFullYear()
149
+  const month = String(now.getMonth() + 1).padStart(2, '0')
150
+  const day = String(now.getDate()).padStart(2, '0')
151
+  currentDate.value = `${year}年${month}月${day}日`
152
+  currentWeekday.value = weekdays[now.getDay()]
153
+  const hours = String(now.getHours()).padStart(2, '0')
154
+  const minutes = String(now.getMinutes()).padStart(2, '0')
155
+  const seconds = String(now.getSeconds()).padStart(2, '0')
156
+  currentTime.value = `${hours}:${minutes}:${seconds}`
157
+}
128 158
 
129 159
 const hourlyPassChartRef = ref(null)
130 160
 let hourlyPassChart = null
131
-const dailyTrendChartRef = ref(null)
132
-let dailyTrendChart = null
133
-const areaCompareChartRef = ref(null)
134
-let areaCompareChart = null
135
-const weeklyTrendChartRef = ref(null)
136
-let weeklyTrendChart = null
137
-const monthlyTypeChartRef = ref(null)
138
-let monthlyTypeChart = null
139 161
 const seizedSmallChartRef = ref(null)
140 162
 let seizedSmallChart = null
141 163
 const seizedChartRef = ref(null)
@@ -144,8 +166,7 @@ const dailySeizedLineRef = ref(null)
144 166
 let dailySeizedLine = null
145 167
 const dailySeizedAreaRef = ref(null)
146 168
 let dailySeizedArea = null
147
-const dailySeizedBarRef = ref(null)
148
-let dailySeizedBar = null
169
+
149 170
 const workAreaDistChartRef = ref(null)
150 171
 let workAreaDistChart = null
151 172
 const unsafeItemDistChartRef = ref(null)
@@ -164,12 +185,12 @@ let securityTestAreaChart = null
164 185
 const initHourlyPassChart = () => {
165 186
   if (!hourlyPassChartRef.value) return
166 187
   hourlyPassChart = echarts.init(hourlyPassChartRef.value)
167
-  const hours = Array.from({length: 24}, (_, i) => `${i.toString().padStart(2, '0')}:00`)
188
+  const hours = Array.from({ length: 24 }, (_, i) => `${i.toString().padStart(2, '0')}:00`)
168 189
   const data1 = [450, 420, 380, 390, 410, 430, 460, 480, 520, 560, 600, 620, 650, 680, 700, 680, 650, 620, 580, 540, 500, 480, 450, 420]
169 190
   const data2 = [320, 310, 290, 300, 310, 330, 350, 370, 400, 430, 460, 480, 500, 520, 530, 510, 490, 470, 440, 410, 380, 360, 340, 320]
170 191
   const data3 = [250, 240, 220, 230, 240, 260, 280, 300, 330, 360, 390, 410, 430, 450, 460, 440, 420, 400, 370, 340, 310, 290, 270, 250]
171 192
   const lineData = [5.2, 4.8, 4.5, 4.7, 5.0, 5.3, 5.6, 5.8, 6.2, 6.5, 6.8, 7.0, 7.2, 7.5, 7.3, 7.1, 6.8, 6.5, 6.2, 5.8, 5.5, 5.2, 5.0, 4.8]
172
-  
193
+
173 194
   const option = {
174 195
     tooltip: {
175 196
       trigger: 'axis',
@@ -273,291 +294,6 @@ const initHourlyPassChart = () => {
273 294
   hourlyPassChart.setOption(option)
274 295
 }
275 296
 
276
-const initDailyTrendChart = () => {
277
-  if (!dailyTrendChartRef.value) return
278
-  dailyTrendChart = echarts.init(dailyTrendChartRef.value)
279
-  const option = {
280
-    tooltip: {
281
-      trigger: 'axis',
282
-      backgroundColor: 'rgba(13,80,122,0.95)',
283
-      borderColor: '#70CFE7',
284
-      textStyle: { color: '#fff' }
285
-    },
286
-    legend: {
287
-      data: ['打火机', '管制刀具', '其他违禁品'],
288
-      textStyle: { color: '#a0c4ff' },
289
-      top: 0
290
-    },
291
-    grid: {
292
-      left: '3%',
293
-      right: '4%',
294
-      bottom: '3%',
295
-      containLabel: true
296
-    },
297
-    xAxis: {
298
-      type: 'category',
299
-      data: ['00:00', '04:00', '08:00', '12:00', '16:00', '20:00', '24:00'],
300
-      axisLabel: { color: '#a0c4ff' },
301
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
302
-    },
303
-    yAxis: {
304
-      type: 'value',
305
-      axisLabel: { color: '#a0c4ff' },
306
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
307
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
308
-    },
309
-    series: [
310
-      {
311
-        name: '打火机',
312
-        type: 'line',
313
-        smooth: true,
314
-        data: [12, 8, 25, 45, 50, 35, 20],
315
-        itemStyle: { color: '#ff9f43' },
316
-        areaStyle: {
317
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
318
-            { offset: 0, color: 'rgba(255,159,67,0.3)' },
319
-            { offset: 1, color: 'rgba(255,159,67,0.05)' }
320
-          ])
321
-        }
322
-      },
323
-      {
324
-        name: '管制刀具',
325
-        type: 'line',
326
-        smooth: true,
327
-        data: [5, 3, 15, 25, 28, 18, 10],
328
-        itemStyle: { color: '#ff6b6b' },
329
-        areaStyle: {
330
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
331
-            { offset: 0, color: 'rgba(255,107,107,0.3)' },
332
-            { offset: 1, color: 'rgba(255,107,107,0.05)' }
333
-          ])
334
-        }
335
-      },
336
-      {
337
-        name: '其他违禁品',
338
-        type: 'line',
339
-        smooth: true,
340
-        data: [20, 15, 40, 60, 70, 45, 30],
341
-        itemStyle: { color: '#4da6ff' },
342
-        areaStyle: {
343
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
344
-            { offset: 0, color: 'rgba(77,166,255,0.3)' },
345
-            { offset: 1, color: 'rgba(77,166,255,0.05)' }
346
-          ])
347
-        }
348
-      }
349
-    ]
350
-  }
351
-  dailyTrendChart.setOption(option)
352
-}
353
-
354
-const initAreaCompareChart = () => {
355
-  if (!areaCompareChartRef.value) return
356
-  areaCompareChart = echarts.init(areaCompareChartRef.value)
357
-  const option = {
358
-    tooltip: {
359
-      trigger: 'axis',
360
-      backgroundColor: 'rgba(13,80,122,0.95)',
361
-      borderColor: '#70CFE7',
362
-      textStyle: { color: '#fff' }
363
-    },
364
-    legend: {
365
-      data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', '旅检A区'],
366
-      textStyle: { color: '#a0c4ff' },
367
-      top: 0
368
-    },
369
-    grid: {
370
-      left: '3%',
371
-      right: '4%',
372
-      bottom: '15%',
373
-      containLabel: true
374
-    },
375
-    xAxis: {
376
-      type: 'category',
377
-      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
378
-      axisLabel: { color: '#a0c4ff' },
379
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
380
-    },
381
-    yAxis: {
382
-      type: 'value',
383
-      axisLabel: { color: '#a0c4ff' },
384
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
385
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
386
-    },
387
-    series: [
388
-      {
389
-        name: 'T3国内出发(4层)',
390
-        type: 'bar',
391
-        data: [120, 132, 140, 125, 150, 160, 145],
392
-        itemStyle: { color: '#4da6ff' },
393
-        barWidth: '15%'
394
-      },
395
-      {
396
-        name: 'T3(8层出发)',
397
-        type: 'bar',
398
-        data: [90, 100, 110, 95, 120, 130, 115],
399
-        itemStyle: { color: '#7eff7e' },
400
-        barWidth: '15%'
401
-      },
402
-      {
403
-        name: 'T3国际出发',
404
-        type: 'bar',
405
-        data: [70, 80, 85, 75, 90, 100, 88],
406
-        itemStyle: { color: '#bd03fb' },
407
-        barWidth: '15%'
408
-      },
409
-      {
410
-        name: 'T3国际转国内',
411
-        type: 'bar',
412
-        data: [45, 55, 60, 50, 65, 70, 62],
413
-        itemStyle: { color: '#ff9f43' },
414
-        barWidth: '15%'
415
-      },
416
-      {
417
-        name: '旅检A区',
418
-        type: 'bar',
419
-        data: [55, 65, 70, 60, 75, 80, 72],
420
-        itemStyle: { color: '#ffd93d' },
421
-        barWidth: '15%'
422
-      }
423
-    ]
424
-  }
425
-  areaCompareChart.setOption(option)
426
-}
427
-
428
-const initAreaDistCharts = () => {
429
-  const colors = [
430
-    ['#4da6ff', '#0f46fa'],
431
-    ['#7eff7e', '#2ecc71'],
432
-    ['#bd03fb', '#8a06e8'],
433
-    ['#ff9f43', '#ee5a24'],
434
-    ['#ffd93d', '#ff9f43']
435
-  ]
436
-  areaDistData.value.forEach((item, index) => {
437
-    if (!areaDistChartRefs.value[index]) return
438
-    const chart = echarts.init(areaDistChartRefs.value[index])
439
-    areaDistCharts.value.push(chart)
440
-    const option = {
441
-      series: [{
442
-        type: 'pie',
443
-        radius: ['40%', '70%'],
444
-        data: [
445
-          { value: 40, name: '打火机', itemStyle: { color: colors[index][0] } },
446
-          { value: 30, name: '管制刀具', itemStyle: { color: colors[index][1] } },
447
-          { value: 30, name: '其他违禁品', itemStyle: { color: '#a0c4ff' } }
448
-        ],
449
-        label: { show: true, color: '#fff', fontSize: 10 },
450
-        labelLine: { show: true }
451
-      }]
452
-    }
453
-    chart.setOption(option)
454
-  })
455
-}
456
-
457
-const initWeeklyTrendChart = () => {
458
-  if (!weeklyTrendChartRef.value) return
459
-  weeklyTrendChart = echarts.init(weeklyTrendChartRef.value)
460
-  const option = {
461
-    tooltip: {
462
-      trigger: 'axis',
463
-      backgroundColor: 'rgba(13,80,122,0.95)',
464
-      borderColor: '#70CFE7',
465
-      textStyle: { color: '#fff' }
466
-    },
467
-    legend: {
468
-      data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发'],
469
-      textStyle: { color: '#a0c4ff' },
470
-      top: 0
471
-    },
472
-    grid: {
473
-      left: '3%',
474
-      right: '4%',
475
-      bottom: '15%',
476
-      containLabel: true
477
-    },
478
-    xAxis: {
479
-      type: 'category',
480
-      data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
481
-      axisLabel: { color: '#a0c4ff' },
482
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
483
-    },
484
-    yAxis: {
485
-      type: 'value',
486
-      axisLabel: { color: '#a0c4ff' },
487
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
488
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
489
-    },
490
-    series: [
491
-      {
492
-        name: 'T3国内出发(4层)',
493
-        type: 'line',
494
-        smooth: true,
495
-        data: [450, 480, 500, 420, 520, 580, 550],
496
-        itemStyle: { color: '#4da6ff' },
497
-        areaStyle: {
498
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
499
-            { offset: 0, color: 'rgba(77,166,255,0.3)' },
500
-            { offset: 1, color: 'rgba(77,166,255,0.05)' }
501
-          ])
502
-        }
503
-      },
504
-      {
505
-        name: 'T3(8层出发)',
506
-        type: 'line',
507
-        smooth: true,
508
-        data: [350, 380, 400, 320, 420, 480, 450],
509
-        itemStyle: { color: '#7eff7e' },
510
-        areaStyle: {
511
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
512
-            { offset: 0, color: 'rgba(126,255,126,0.3)' },
513
-            { offset: 1, color: 'rgba(126,255,126,0.05)' }
514
-          ])
515
-        }
516
-      },
517
-      {
518
-        name: 'T3国际出发',
519
-        type: 'line',
520
-        smooth: true,
521
-        data: [280, 300, 320, 260, 340, 380, 360],
522
-        itemStyle: { color: '#bd03fb' },
523
-        areaStyle: {
524
-          color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
525
-            { offset: 0, color: 'rgba(189,3,251,0.3)' },
526
-            { offset: 1, color: 'rgba(189,3,251,0.05)' }
527
-          ])
528
-        }
529
-      }
530
-    ]
531
-  }
532
-  weeklyTrendChart.setOption(option)
533
-}
534
-
535
-const initMonthlyTypeChart = () => {
536
-  if (!monthlyTypeChartRef.value) return
537
-  monthlyTypeChart = echarts.init(monthlyTypeChartRef.value)
538
-  const option = {
539
-    tooltip: {
540
-      trigger: 'item',
541
-      backgroundColor: 'rgba(13,80,122,0.95)',
542
-      borderColor: '#70CFE7',
543
-      textStyle: { color: '#fff' }
544
-    },
545
-    series: [{
546
-      type: 'pie',
547
-      radius: ['40%', '70%'],
548
-      data: [
549
-        { value: 35, name: '打火机', itemStyle: { color: '#ff9f43' } },
550
-        { value: 25, name: '管制刀具', itemStyle: { color: '#ff6b6b' } },
551
-        { value: 20, name: '易燃易爆', itemStyle: { color: '#ffd93d' } },
552
-        { value: 20, name: '其他', itemStyle: { color: '#a0c4ff' } }
553
-      ],
554
-      label: { show: true, color: '#fff', fontSize: 11 },
555
-      labelLine: { show: true }
556
-    }]
557
-  }
558
-  monthlyTypeChart.setOption(option)
559
-}
560
-
561 297
 const initSeizedSmallChart = () => {
562 298
   if (!seizedSmallChartRef.value) return
563 299
   seizedSmallChart = echarts.init(seizedSmallChartRef.value)
@@ -741,58 +477,7 @@ const initDailySeizedArea = () => {
741 477
   dailySeizedArea.setOption(option)
742 478
 }
743 479
 
744
-const initDailySeizedBar = () => {
745
-  if (!dailySeizedBarRef.value) return
746
-  dailySeizedBar = echarts.init(dailySeizedBarRef.value)
747
-  const option = {
748
-    tooltip: {
749
-      trigger: 'axis',
750
-      backgroundColor: 'rgba(13,80,122,0.95)',
751
-      borderColor: '#70CFE7',
752
-      textStyle: { color: '#fff' }
753
-    },
754
-    legend: {
755
-      data: ['查获数量'],
756
-      textStyle: { color: '#a0c4ff' },
757
-      top: 0
758
-    },
759
-    grid: {
760
-      left: '3%',
761
-      right: '4%',
762
-      bottom: '15%',
763
-      containLabel: true
764
-    },
765
-    xAxis: {
766
-      type: 'category',
767
-      data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', '旅检A区', 'T3要客服务区东', 'T3要客服务区西', 'T1要客服务区东', 'T2要客服务区东', '旅检B区', '重检C区'],
768
-      axisLabel: { 
769
-        color: '#a0c4ff',
770
-        rotate: 30,
771
-        fontSize: 10
772
-      },
773
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } }
774
-    },
775
-    yAxis: {
776
-      type: 'value',
777
-      axisLabel: { color: '#a0c4ff' },
778
-      axisLine: { lineStyle: { color: 'rgba(15,70,250,0.3)' } },
779
-      splitLine: { lineStyle: { color: 'rgba(15,70,250,0.2)' } }
780
-    },
781
-    series: [{
782
-      name: '查获数量',
783
-      type: 'bar',
784
-      data: [172, 200, 182, 82, 79, 149, 169, 92, 163, 144, 180],
785
-      itemStyle: {
786
-        color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
787
-          { offset: 0, color: '#bd03fb' },
788
-          { offset: 1, color: '#0f46fa' }
789
-        ])
790
-      },
791
-      barWidth: '40%'
792
-    }]
793
-  }
794
-  dailySeizedBar.setOption(option)
795
-}
480
+
796 481
 
797 482
 const initWorkAreaDistChart = () => {
798 483
   if (!workAreaDistChartRef.value) return
@@ -818,7 +503,7 @@ const initWorkAreaDistChart = () => {
818 503
     xAxis: {
819 504
       type: 'category',
820 505
       data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', '旅检A区', 'T3要客服务区东', 'T3要客服务区西', 'T1要客服务区东', 'T2要客服务区东', '旅检B区', '重检C区'],
821
-      axisLabel: { 
506
+      axisLabel: {
822 507
         color: '#a0c4ff',
823 508
         rotate: 30,
824 509
         fontSize: 10
@@ -914,7 +599,7 @@ const initUnsafePostDistChart = () => {
914 599
     xAxis: {
915 600
       type: 'category',
916 601
       data: ['境外/非通道', '值机柜台', '验证/通道', '人身检查', 'X光/行李/开检', '开箱/货检'],
917
-      axisLabel: { 
602
+      axisLabel: {
918 603
         color: '#a0c4ff',
919 604
         rotate: 30,
920 605
         fontSize: 10
@@ -1005,7 +690,7 @@ const initSecurityTestAreaChart = () => {
1005 690
     xAxis: {
1006 691
       type: 'category',
1007 692
       data: ['T3国内出发(4层)', 'T3(8层出发)', 'T3国际出发', 'T3国际转国内', 'T1要客服务区西', 'T1要客服务区东'],
1008
-      axisLabel: { 
693
+      axisLabel: {
1009 694
         color: '#a0c4ff',
1010 695
         rotate: 30,
1011 696
         fontSize: 10
@@ -1036,15 +721,11 @@ const initSecurityTestAreaChart = () => {
1036 721
 
1037 722
 const handleResize = () => {
1038 723
   if (hourlyPassChart) hourlyPassChart.resize()
1039
-  if (dailyTrendChart) dailyTrendChart.resize()
1040
-  if (areaCompareChart) areaCompareChart.resize()
1041
-  if (weeklyTrendChart) weeklyTrendChart.resize()
1042
-  if (monthlyTypeChart) monthlyTypeChart.resize()
1043 724
   if (seizedSmallChart) seizedSmallChart.resize()
1044 725
   if (seizedChart) seizedChart.resize()
1045 726
   if (dailySeizedLine) dailySeizedLine.resize()
1046 727
   if (dailySeizedArea) dailySeizedArea.resize()
1047
-  if (dailySeizedBar) dailySeizedBar.resize()
728
+
1048 729
   if (workAreaDistChart) workAreaDistChart.resize()
1049 730
   if (unsafeItemDistChart) unsafeItemDistChart.resize()
1050 731
   if (unsafeTypeDistChart) unsafeTypeDistChart.resize()
@@ -1052,23 +733,19 @@ const handleResize = () => {
1052 733
   if (securityTestItemChart) securityTestItemChart.resize()
1053 734
   if (securityTestPassChart) securityTestPassChart.resize()
1054 735
   if (securityTestAreaChart) securityTestAreaChart.resize()
1055
-  areaDistCharts.value.forEach(chart => chart && chart.resize())
1056 736
 }
1057 737
 
1058 738
 onMounted(() => {
739
+  updateDateTime()
740
+  dateTimer = setInterval(updateDateTime, 1000)
1059 741
   nextTick(() => {
1060 742
     setTimeout(() => {
1061 743
       initHourlyPassChart()
1062
-      initDailyTrendChart()
1063
-      initAreaCompareChart()
1064
-      initAreaDistCharts()
1065
-      initWeeklyTrendChart()
1066
-      initMonthlyTypeChart()
1067 744
       initSeizedSmallChart()
1068 745
       initSeizedChart()
1069 746
       initDailySeizedLine()
1070 747
       initDailySeizedArea()
1071
-      initDailySeizedBar()
748
+
1072 749
       initWorkAreaDistChart()
1073 750
       initUnsafeItemDistChart()
1074 751
       initUnsafeTypeDistChart()
@@ -1082,17 +759,14 @@ onMounted(() => {
1082 759
 })
1083 760
 
1084 761
 onUnmounted(() => {
762
+  clearInterval(dateTimer)
1085 763
   window.removeEventListener('resize', handleResize)
1086 764
   if (hourlyPassChart) hourlyPassChart.dispose()
1087
-  if (dailyTrendChart) dailyTrendChart.dispose()
1088
-  if (areaCompareChart) areaCompareChart.dispose()
1089
-  if (weeklyTrendChart) weeklyTrendChart.dispose()
1090
-  if (monthlyTypeChart) monthlyTypeChart.dispose()
1091 765
   if (seizedSmallChart) seizedSmallChart.dispose()
1092 766
   if (seizedChart) seizedChart.dispose()
1093 767
   if (dailySeizedLine) dailySeizedLine.dispose()
1094 768
   if (dailySeizedArea) dailySeizedArea.dispose()
1095
-  if (dailySeizedBar) dailySeizedBar.dispose()
769
+
1096 770
   if (workAreaDistChart) workAreaDistChart.dispose()
1097 771
   if (unsafeItemDistChart) unsafeItemDistChart.dispose()
1098 772
   if (unsafeTypeDistChart) unsafeTypeDistChart.dispose()
@@ -1100,7 +774,6 @@ onUnmounted(() => {
1100 774
   if (securityTestItemChart) securityTestItemChart.dispose()
1101 775
   if (securityTestPassChart) securityTestPassChart.dispose()
1102 776
   if (securityTestAreaChart) securityTestAreaChart.dispose()
1103
-  areaDistCharts.value.forEach(chart => chart && chart.dispose())
1104 777
 })
1105 778
 </script>
1106 779
 
@@ -1111,49 +784,17 @@ onUnmounted(() => {
1111 784
   gap: 20px;
1112 785
 
1113 786
   .content-row {
787
+    padding: 0 20px;
1114 788
     display: flex;
1115 789
     gap: 20px;
1116
-    
1117
-    > .info-card {
1118
-      flex: 1;
1119
-      min-width: 0;
1120
-    }
1121
-  }
1122
-
1123
-  .hourly-pass-chart,
1124
-  .daily-trend-chart,
1125
-  .area-compare-chart {
1126
-    width: 100%;
1127
-    height: 350px;
1128
-  }
1129 790
 
1130
-  .area-distribution {
1131
-    display: flex;
1132
-    gap: 20px;
1133
-    justify-content: space-around;
1134
-    flex-wrap: wrap;
1135
-
1136
-    .dist-item {
791
+    >.info-card {
1137 792
       flex: 1;
1138
-      min-width: 150px;
1139
-      text-align: center;
1140
-
1141
-      .dist-chart {
1142
-        width: 100%;
1143
-        height: 200px;
1144
-      }
1145
-
1146
-      .dist-label {
1147
-        color: #a0c4ff;
1148
-        font-size: 12px;
1149
-        margin-top: 10px;
1150
-        text-align: center;
1151
-      }
793
+      min-width: 0;
1152 794
     }
1153 795
   }
1154 796
 
1155
-  .weekly-trend-chart,
1156
-  .monthly-type-chart {
797
+  .hourly-pass-chart {
1157 798
     width: 100%;
1158 799
     height: 350px;
1159 800
   }
@@ -1221,7 +862,7 @@ onUnmounted(() => {
1221 862
     height: 350px;
1222 863
   }
1223 864
 
1224
-  .daily-bar-chart,
865
+
1225 866
   .work-area-dist-chart {
1226 867
     width: 100%;
1227 868
     height: 400px;
@@ -1236,5 +877,87 @@ onUnmounted(() => {
1236 877
     width: 100%;
1237 878
     height: 350px;
1238 879
   }
880
+
881
+  .info-panel {
882
+    width: 300px;
883
+    min-width: 300px;
884
+    background: linear-gradient(135deg, rgba(15, 70, 250, 0.15), rgba(189, 3, 251, 0.15));
885
+    border: 1px solid rgba(15, 70, 250, 0.3);
886
+    border-radius: 20px;
887
+    display: flex;
888
+    flex-direction: column;
889
+    overflow: hidden;
890
+
891
+
892
+    .panel-body {
893
+      padding: 15px 20px;
894
+      flex: 1;
895
+      display: flex;
896
+      flex-direction: column;
897
+      gap: 10px;
898
+      justify-content: space-between;
899
+    }
900
+
901
+    .datetime-row {
902
+      display: flex;
903
+      align-items: baseline;
904
+      gap: 12px;
905
+
906
+      .info-date {
907
+        font-size: 22px;
908
+        font-weight: bold;
909
+        color: #fff;
910
+        letter-spacing: 2px;
911
+      }
912
+    }
913
+
914
+    .date-row {
915
+      display: flex;
916
+      align-items: center;
917
+      .info-date {
918
+        font-size: 22px;
919
+        font-weight: bold;
920
+        color: #fff;
921
+        letter-spacing: 2px;
922
+      }
923
+
924
+      .info-weekday {
925
+        font-size: 16px;
926
+        color: #a0c4ff;
927
+      }
928
+    }
929
+
930
+    .time-row {
931
+      font-size: 36px;
932
+      font-weight: bold;
933
+      color: #00f5ff;
934
+      letter-spacing: 4px;
935
+      text-shadow: 0 0 20px rgba(0, 245, 255, 0.5);
936
+    }
937
+
938
+    .divider {
939
+      height: 1px;
940
+      background: linear-gradient(90deg, transparent, rgba(15, 70, 250, 0.5), rgba(189, 3, 251, 0.5), transparent);
941
+      margin: 4px 0;
942
+    }
943
+
944
+    .duty-item {
945
+      display: flex;
946
+      align-items: center;
947
+      line-height: 28px;
948
+
949
+      .duty-label {
950
+        font-size: 16px;
951
+        color: #a0c4ff;
952
+        white-space: nowrap;
953
+      }
954
+
955
+      .duty-value {
956
+        font-size: 16px;
957
+        color: #fff;
958
+        margin-left: 4px;
959
+      }
960
+    }
961
+  }
1239 962
 }
1240 963
 </style>

+ 16 - 16
src/views/portraitManagement/stationProfile/index.vue

@@ -69,7 +69,7 @@ const pentagonItems = ref([
69 69
       left: { start: 'transparent', end: '#0f46fa' }
70 70
     },
71 71
     bgGradientStart: 'rgba(33,33,58,0.95)',
72
-    bgGradientEnd: 'rgba(189,3,251,0.15)'
72
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
73 73
   },
74 74
   {
75 75
     value: '150',
@@ -79,8 +79,8 @@ const pentagonItems = ref([
79 79
     cornerRadius: 20,
80 80
     vertices: {
81 81
       topLeft: { x: 10, y: 0 },
82
-      topRight: { x: 270, y: 0 },
83
-      bottomRight: { x: 270, y: 200 },
82
+      topRight: { x: 290, y: 0 },
83
+      bottomRight: { x: 290, y: 200 },
84 84
       bottomLeft: { x: -30, y: 200 }
85 85
     },
86 86
     cardContentStyle: {
@@ -92,8 +92,8 @@ const pentagonItems = ref([
92 92
       bottom: { start: '#9903C1', end: 'transparent' },
93 93
       left: { start: 'transparent', end: '#0f46fa' }
94 94
     },
95
-    bgGradientStart: 'rgba(33,33,58,0.98)',
96
-    bgGradientEnd: 'rgba(189,3,251,0.25)'
95
+    bgGradientStart: 'rgba(33,33,58,0.95)',
96
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
97 97
   },
98 98
   {
99 99
     value: '80',
@@ -102,8 +102,8 @@ const pentagonItems = ref([
102 102
     vertical: true,
103 103
     cornerRadius: 20,
104 104
     vertices: {
105
-      topLeft: { x: -20, y: 0 },
106
-      topRight: { x: 300, y: 0 },
105
+      topLeft: { x: 0, y: 0 },
106
+      topRight: { x: 290, y: 0 },
107 107
       bottomRight: { x: 320, y: 200 },
108 108
       bottomLeft: { x: 0, y: 200 }
109 109
     },
@@ -111,13 +111,13 @@ const pentagonItems = ref([
111 111
       gap: '10px'
112 112
     },
113 113
     edgeGradients: {
114
-      top: { start: '#7eff7e', end: 'transparent' },
114
+      top: { start: '#0f46fa', end: 'transparent' },
115 115
       right: { start: 'transparent', end: '#9903C1' },
116 116
       bottom: { start: '#9903C1', end: 'transparent' },
117
-      left: { start: 'transparent', end: '#7eff7e' }
117
+      left: { start: 'transparent', end: '#0f46fa' }
118 118
     },
119 119
     bgGradientStart: 'rgba(33,33,58,0.95)',
120
-    bgGradientEnd: 'rgba(126,255,126,0.15)'
120
+    bgGradientEnd: 'rgba(15,70,250,0.2)'
121 121
   },
122 122
   {
123 123
     value: '70',
@@ -126,10 +126,10 @@ const pentagonItems = ref([
126 126
     vertical: true,
127 127
     cornerRadius: 20,
128 128
     vertices: {
129
-      topLeft: { x: 20, y: 0 },
130
-      topRight: { x: 300, y: 0 },
131
-      bottomRight: { x: 300, y: 200 },
132
-      bottomLeft: { x: 40, y: 200 }
129
+      topLeft: { x: 0, y: 0 },
130
+      topRight: { x: 280, y: 0 },
131
+      bottomRight: { x: 310, y: 200 },
132
+      bottomLeft: { x: 30, y: 200 }
133 133
     },
134 134
     cardContentStyle: {
135 135
       gap: '10px'
@@ -150,10 +150,10 @@ const pentagonItems = ref([
150 150
     vertical: true,
151 151
     cornerRadius: 20,
152 152
     vertices: {
153
-      topLeft: { x: 0, y: 0 },
153
+      topLeft: { x: -10, y: 0 },
154 154
       topRight: { x: 300, y: 0 },
155 155
       bottomRight: { x: 300, y: 200 },
156
-      bottomLeft: { x: 0, y: 200 }
156
+      bottomLeft: { x:20, y: 200 }
157 157
     },
158 158
     cardContentStyle: {
159 159
       gap: '10px'

+ 99 - 37
src/views/score/dimension/index.vue

@@ -3,24 +3,37 @@
3 3
     <el-row :gutter="16" style="height:100%">
4 4
       <!-- 左侧:维度列表 -->
5 5
       <el-col :span="7">
6
+        <el-card shadow="never">
7
+          <template #header>
8
+            <span>配分层级</span>
9
+          </template>
10
+          <div>
11
+            <el-select v-model="scoreLevel" placeholder="请选择层级" clearable style="width:100%" @change="loadTree">
12
+              <el-option v-for="dict in score_level" :key="dict.value" :label="dict.label" :value="dict.value" />
13
+            </el-select>
14
+          </div>
15
+        </el-card>
6 16
         <el-card class="dim-card" shadow="never">
7 17
           <template #header>
8 18
             <div class="card-header">
9 19
               <span>评分维度(6个)</span>
10
-              <el-button type="primary" size="small" icon="Plus" @click="handleAddDim" v-hasPermi="['score:dimension:add']">新增</el-button>
20
+              <el-button type="primary" size="small" icon="Plus" @click="handleAddDim"
21
+                v-hasPermi="['score:dimension:add']">新增</el-button>
11 22
             </div>
12 23
           </template>
13 24
           <div v-loading="dimLoading">
14 25
             <div v-for="dim in dimList" :key="dim.id"
15
-                 :class="['dim-item', { active: selectedDim && selectedDim.id === dim.id }]"
16
-                 @click="selectDimension(dim)">
26
+              :class="['dim-item', { active: selectedDim && selectedDim.id === dim.id }]" @click="selectDimension(dim)">
17 27
               <div class="dim-name">
18
-                <el-tag :type="dim.status === '0' ? '' : 'info'" size="small" style="margin-right:6px">{{ dim.name }}</el-tag>
28
+                <el-tag :type="dim.status === '0' ? '' : 'info'" size="small" style="margin-right:6px">{{ dim.name
29
+                  }}</el-tag>
19 30
                 <span class="dim-weight">{{ dim.weight }}%</span>
20 31
               </div>
21 32
               <div class="dim-actions">
22
-                <el-button link size="small" icon="Edit" @click.stop="handleEditDim(dim)" v-hasPermi="['score:dimension:edit']" />
23
-                <el-button link size="small" icon="Delete" type="danger" @click.stop="handleDeleteDim(dim)" v-hasPermi="['score:dimension:remove']" />
33
+                <el-button link size="small" icon="Edit" @click.stop="handleEditDim(dim)"
34
+                  v-hasPermi="['score:dimension:edit']" />
35
+                <el-button link size="small" icon="Delete" type="danger" @click.stop="handleDeleteDim(dim)"
36
+                  v-hasPermi="['score:dimension:remove']" />
24 37
               </div>
25 38
             </div>
26 39
           </div>
@@ -32,20 +45,21 @@
32 45
 
33 46
       <!-- 右侧:指标树 -->
34 47
       <el-col :span="17">
35
-        <el-card shadow="never">
48
+        <el-card shadow="never" >
36 49
           <template #header>
37 50
             <div class="card-header">
38 51
               <span>{{ selectedDim ? selectedDim.name + ' — 指标树' : '请选择左侧维度' }}</span>
39 52
               <div v-if="selectedDim">
40
-                <el-button size="small" icon="Plus" type="primary" @click="handleAddIndicator(null)" v-hasPermi="['score:indicator:add']">新增二级指标</el-button>
53
+                <el-button size="small" icon="Plus" type="primary" @click="handleAddIndicator(null)"
54
+                  v-hasPermi="['score:indicator:add']">新增二级指标</el-button>
41 55
                 <el-button size="small" icon="Refresh" @click="loadTree" />
42 56
               </div>
43 57
             </div>
44 58
           </template>
45 59
 
60
+
46 61
           <el-table v-loading="treeLoading" :data="indicatorTree" row-key="id"
47
-                    :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
48
-                    border default-expand-all>
62
+            :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" border default-expand-all>
49 63
             <el-table-column label="指标名称" prop="name" min-width="220" />
50 64
             <el-table-column label="层级" align="center" width="70">
51 65
               <template #default="{ row }">
@@ -56,7 +70,8 @@
56 70
             </el-table-column>
57 71
             <el-table-column label="类型" align="center" width="80">
58 72
               <template #default="{ row }">
59
-                <el-tag v-if="row.type" size="small" :type="row.type === '1' ? 'success' : row.type === '2' ? 'danger' : 'info'">
73
+                <el-tag v-if="row.type" size="small"
74
+                  :type="row.type === '1' ? 'success' : row.type === '2' ? 'danger' : 'info'">
60 75
                   {{ row.type === '1' ? '加分' : row.type === '2' ? '扣分' : '记录' }}
61 76
                 </el-tag>
62 77
               </template>
@@ -64,7 +79,7 @@
64 79
             <el-table-column label="分值" align="center" width="90">
65 80
               <template #default="{ row }">
66 81
                 <span v-if="row.scoreValue != null"
67
-                      :style="{ color: row.scoreValue > 0 ? '#67c23a' : row.scoreValue < 0 ? '#f56c6c' : '' }">
82
+                  :style="{ color: row.scoreValue > 0 ? '#67c23a' : row.scoreValue < 0 ? '#f56c6c' : '' }">
68 83
                   {{ row.scoreValue > 0 ? '+' : '' }}{{ row.scoreValue }}
69 84
                 </span>
70 85
               </template>
@@ -72,21 +87,22 @@
72 87
             <el-table-column label="叠加规则" prop="cascadeRule" min-width="180" show-overflow-tooltip />
73 88
             <el-table-column label="状态" align="center" width="80">
74 89
               <template #default="{ row }">
75
-                <el-switch v-model="row.status" active-value="0" inactive-value="1"
76
-                           @change="handleStatusChange(row)" v-hasPermi="['score:indicator:edit']" />
90
+                <el-switch v-model="row.status" active-value="0" inactive-value="1" @change="handleStatusChange(row)"
91
+                  v-hasPermi="['score:indicator:edit']" />
77 92
               </template>
78 93
             </el-table-column>
79 94
             <el-table-column label="操作" align="center" width="160">
80 95
               <template #default="{ row }">
81 96
                 <el-button v-if="row.level < 4" link type="primary" size="small" icon="Plus"
82
-                           @click="handleAddIndicator(row)" v-hasPermi="['score:indicator:add']">子级</el-button>
83
-                <el-button link type="primary" size="small" icon="Edit"
84
-                           @click="handleEditIndicator(row)" v-hasPermi="['score:indicator:edit']">修改</el-button>
85
-                <el-button link type="danger" size="small" icon="Delete"
86
-                           @click="handleDeleteIndicator(row)" v-hasPermi="['score:indicator:remove']">删除</el-button>
97
+                  @click="handleAddIndicator(row)" v-hasPermi="['score:indicator:add']">子级</el-button>
98
+                <el-button link type="primary" size="small" icon="Edit" @click="handleEditIndicator(row)"
99
+                  v-hasPermi="['score:indicator:edit']">修改</el-button>
100
+                <el-button link type="danger" size="small" icon="Delete" @click="handleDeleteIndicator(row)"
101
+                  v-hasPermi="['score:indicator:remove']">删除</el-button>
87 102
               </template>
88 103
             </el-table-column>
89 104
           </el-table>
105
+
90 106
         </el-card>
91 107
       </el-col>
92 108
     </el-row>
@@ -145,12 +161,10 @@
145 161
           </el-radio-group>
146 162
         </el-form-item>
147 163
         <el-form-item label="分值" prop="scoreValue">
148
-          <el-input-number v-model="indForm.scoreValue" :precision="2" style="width:100%"
149
-                           placeholder="正数=加分,负数=扣分" />
164
+          <el-input-number v-model="indForm.scoreValue" :precision="2" style="width:100%" placeholder="正数=加分,负数=扣分" />
150 165
         </el-form-item>
151 166
         <el-form-item label="叠加规则">
152
-          <el-input v-model="indForm.cascadeRule" type="textarea" :rows="2"
153
-                    placeholder="如:返航/二次清舱-10、航班延误-8..." />
167
+          <el-input v-model="indForm.cascadeRule" type="textarea" :rows="2" placeholder="如:返航/二次清舱-10、航班延误-8..." />
154 168
         </el-form-item>
155 169
         <el-form-item label="排序">
156 170
           <el-input-number v-model="indForm.sortOrder" :min="0" style="width:100%" />
@@ -174,7 +188,7 @@
174 188
 </template>
175 189
 
176 190
 <script setup>
177
-import { ref, reactive, computed, onMounted } from 'vue'
191
+import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
178 192
 import { ElMessage, ElMessageBox } from 'element-plus'
179 193
 import {
180 194
   listDimension, addDimension, updateDimension, delDimension,
@@ -183,6 +197,11 @@ import {
183 197
 
184 198
 defineOptions({ name: 'ScoreDimension' })
185 199
 
200
+const { proxy } = getCurrentInstance()
201
+const { score_level } = proxy.useDict('score_level')
202
+
203
+const scoreLevel = ref('')
204
+
186 205
 // ===== 维度 =====
187 206
 const dimLoading = ref(false)
188 207
 const dimList = ref([])
@@ -251,7 +270,7 @@ const parentIndicatorName = ref('')
251 270
 async function loadTree() {
252 271
   if (!selectedDim.value) return
253 272
   treeLoading.value = true
254
-  const r = await treeIndicator(selectedDim.value.id).finally(() => treeLoading.value = false)
273
+  const r = await treeIndicator({ dimensionId: selectedDim.value.id, scoreLevel: scoreLevel.value }).finally(() => treeLoading.value = false)
255 274
   indicatorTree.value = r.data || []
256 275
 }
257 276
 
@@ -292,18 +311,61 @@ onMounted(loadDimensions)
292 311
 </script>
293 312
 
294 313
 <style scoped>
295
-.score-dimension-page { height: calc(100vh - 130px); }
296
-.dim-card { height: 100%; }
297
-.card-header { display: flex; justify-content: space-between; align-items: center; }
314
+.score-dimension-page {
315
+  /* height: calc(100vh - 130px); */
316
+}
317
+
318
+.dim-card {
319
+  height: 100%;
320
+}
321
+
322
+.card-header {
323
+  display: flex;
324
+  justify-content: space-between;
325
+  align-items: center;
326
+}
327
+
298 328
 .dim-item {
299
-  display: flex; justify-content: space-between; align-items: center;
300
-  padding: 10px 12px; border-radius: 6px; cursor: pointer;
301
-  margin-bottom: 6px; border: 1px solid #ebeef5; transition: all .2s;
329
+  display: flex;
330
+  justify-content: space-between;
331
+  align-items: center;
332
+  padding: 10px 12px;
333
+  border-radius: 6px;
334
+  cursor: pointer;
335
+  margin-bottom: 6px;
336
+  border: 1px solid #ebeef5;
337
+  transition: all .2s;
338
+}
339
+
340
+.dim-item:hover {
341
+  border-color: #409eff;
342
+  background: #f0f7ff;
343
+}
344
+
345
+.dim-item.active {
346
+  border-color: #409eff;
347
+  background: #ecf5ff;
348
+}
349
+
350
+.dim-name {
351
+  display: flex;
352
+  align-items: center;
353
+}
354
+
355
+.dim-weight {
356
+  font-size: 12px;
357
+  color: #909399;
358
+}
359
+
360
+.dim-actions {
361
+  display: flex;
362
+  gap: 4px;
363
+}
364
+
365
+.weight-total {
366
+  text-align: right;
367
+  margin-top: 12px;
368
+  font-size: 13px;
369
+  color: #606266;
302 370
 }
303
-.dim-item:hover { border-color: #409eff; background: #f0f7ff; }
304
-.dim-item.active { border-color: #409eff; background: #ecf5ff; }
305
-.dim-name { display: flex; align-items: center; }
306
-.dim-weight { font-size: 12px; color: #909399; }
307
-.dim-actions { display: flex; gap: 4px; }
308
-.weight-total { text-align: right; margin-top: 12px; font-size: 13px; color: #606266; }
309 371
 </style>

+ 159 - 47
src/views/score/event/index.vue

@@ -10,6 +10,9 @@
10 10
       <el-form-item label="队室/班组" prop="teamName">
11 11
         <el-input v-model="queryParams.teamName" placeholder="请输入队室/班组" clearable @keyup.enter="handleQuery" />
12 12
       </el-form-item>
13
+      <el-form-item label="通道/小组" prop="groupName">
14
+        <el-input v-model="queryParams.groupName" placeholder="请输入通道/小组" clearable @keyup.enter="handleQuery" />
15
+      </el-form-item>
13 16
       <el-form-item label="维度" prop="dimensionId">
14 17
         <el-select v-model="queryParams.dimensionId" placeholder="全部维度" clearable style="width:150px">
15 18
           <el-option v-for="d in dimensionOptions" :key="d.id" :label="d.name" :value="d.id" />
@@ -22,8 +25,8 @@
22 25
         </el-select>
23 26
       </el-form-item>
24 27
       <el-form-item label="事件时间">
25
-        <el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD"
26
-          range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" clearable />
28
+        <el-date-picker v-model="dateRange" type="daterange" value-format="YYYY-MM-DD" range-separator="-"
29
+          start-placeholder="开始日期" end-placeholder="结束日期" clearable />
27 30
       </el-form-item>
28 31
       <el-form-item>
29 32
         <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
@@ -36,13 +39,16 @@
36 39
         <el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['score:event:add']">新增</el-button>
37 40
       </el-col>
38 41
       <el-col :span="1.5">
39
-        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate" v-hasPermi="['score:event:edit']">修改</el-button>
42
+        <el-button type="success" plain icon="Edit" :disabled="single" @click="handleUpdate"
43
+          v-hasPermi="['score:event:edit']">修改</el-button>
40 44
       </el-col>
41 45
       <el-col :span="1.5">
42
-        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete" v-hasPermi="['score:event:remove']">删除</el-button>
46
+        <el-button type="danger" plain icon="Delete" :disabled="multiple" @click="handleDelete"
47
+          v-hasPermi="['score:event:remove']">删除</el-button>
43 48
       </el-col>
44 49
       <el-col :span="1.5">
45
-        <el-button type="warning" plain icon="Download" @click="handleExport" v-hasPermi="['score:event:export']">导出</el-button>
50
+        <el-button type="warning" plain icon="Download" @click="handleExport"
51
+          v-hasPermi="['score:event:export']">导出</el-button>
46 52
       </el-col>
47 53
       <el-col :span="1.5">
48 54
         <el-upload :show-file-list="false" accept=".xlsx,.xls" :auto-upload="false" :on-change="handleImportFile">
@@ -93,20 +99,30 @@
93 99
         </template>
94 100
       </el-table-column>
95 101
       <el-table-column label="事件描述" align="center" prop="eventDesc" show-overflow-tooltip />
96
-      <el-table-column label="操作" align="center" width="120">
102
+      <el-table-column label="操作" align="center" width="180">
97 103
         <template #default="{ row }">
98
-          <el-button link type="primary" icon="Edit" @click="handleUpdate(row)" v-hasPermi="['score:event:edit']">修改</el-button>
99
-          <el-button link type="danger" icon="Delete" @click="handleDelete(row)" v-hasPermi="['score:event:remove']">删除</el-button>
104
+          <el-button link type="primary" icon="Edit" @click="handleUpdate(row)"
105
+            v-hasPermi="['score:event:edit']">修改</el-button>
106
+          <el-button link type="danger" icon="Delete" @click="handleDelete(row)"
107
+            v-hasPermi="['score:event:remove']">删除</el-button>
100 108
         </template>
101 109
       </el-table-column>
102 110
     </el-table>
103
-    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
111
+    <pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum"
112
+      v-model:limit="queryParams.pageSize" @pagination="getList" />
104 113
 
105 114
     <!-- 新增/修改对话框 -->
106 115
     <el-dialog :title="dialogTitle" v-model="dialogVisible" width="680px" append-to-body>
107 116
       <el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
108 117
         <el-row :gutter="16">
109 118
           <el-col :span="12">
119
+            <el-form-item label="配分层级" prop="org">
120
+              <el-select v-model="form.org" placeholder="请选择层级" style="width:100%">
121
+                <el-option v-for="dict in score_level" :key="dict.value" :label="dict.label" :value="dict.value" />
122
+              </el-select>
123
+            </el-form-item>
124
+          </el-col>
125
+          <el-col :span="12">
110 126
             <el-form-item label="维度" prop="dimensionId">
111 127
               <el-select v-model="form.dimensionId" placeholder="请选择维度" style="width:100%" @change="onDimensionChange">
112 128
                 <el-option v-for="d in dimensionOptions" :key="d.id" :label="d.name" :value="d.id" />
@@ -114,23 +130,25 @@
114 130
             </el-form-item>
115 131
           </el-col>
116 132
           <el-col :span="12">
117
-            <el-form-item label="二级指标" prop="level2Name">
118
-              <el-select v-model="form.level2Name" placeholder="请选择" style="width:100%" clearable @change="onLevel2Change">
119
-                <el-option v-for="n in level2Options" :key="n" :label="n" :value="n" />
133
+            <el-form-item label="二级指标" prop="level2Id">
134
+              <el-select v-model="form.level2Id" placeholder="请选择" style="width:100%" clearable
135
+                @change="onLevel2Change">
136
+                <el-option v-for="n in level2Options" :key="n.id" :label="n.name" :value="n.id" />
120 137
               </el-select>
121 138
             </el-form-item>
122 139
           </el-col>
123 140
           <el-col :span="12">
124
-            <el-form-item label="三级指标" prop="level3Name">
125
-              <el-select v-model="form.level3Name" placeholder="请选择" style="width:100%" clearable @change="onLevel3Change">
126
-                <el-option v-for="n in level3Options" :key="n" :label="n" :value="n" />
141
+            <el-form-item label="三级指标" prop="level3Id">
142
+              <el-select v-model="form.level3Id" placeholder="请选择" style="width:100%" clearable
143
+                @change="onLevel3Change">
144
+                <el-option v-for="n in level3Options" :key="n.id" :label="n.name" :value="n.id" />
127 145
               </el-select>
128 146
             </el-form-item>
129 147
           </el-col>
130 148
           <el-col :span="12">
131 149
             <el-form-item label="四级指标">
132
-              <el-select v-model="form.level4Name" placeholder="请选择" style="width:100%" clearable>
133
-                <el-option v-for="n in level4Options" :key="n" :label="n" :value="n" />
150
+              <el-select v-model="form.level4Id" placeholder="请选择" style="width:100%" clearable @change="onLevel4Change">
151
+                <el-option v-for="n in level4Options" :key="n.id" :label="n.name" :value="n.id" />
134 152
               </el-select>
135 153
             </el-form-item>
136 154
           </el-col>
@@ -146,35 +164,45 @@
146 164
             </el-form-item>
147 165
           </el-col>
148 166
           <el-col :span="12">
149
-            <el-form-item label="责任人" prop="personName">
150
-              <el-input v-model="form.personName" placeholder="请输入责任人姓名" />
167
+            <el-form-item label="部门名称">
168
+              <el-select v-model="form.deptId" placeholder="请选择部门" style="width:100%" clearable filterable
169
+                @change="handleDeptChange">
170
+                <el-option v-for="d in deptOptions" :key="d.deptId" :label="d.deptName" :value="d.deptId" />
171
+              </el-select>
151 172
             </el-form-item>
152 173
           </el-col>
153 174
           <el-col :span="12">
154
-            <el-form-item label="部门名称">
155
-              <el-input v-model="form.deptName" placeholder="请输入部门名称" />
175
+            <el-form-item label="队室/班组">
176
+              <el-select v-model="form.teamId" placeholder="请选择队室/班组" style="width:100%" clearable
177
+                :disabled="form.org < 2" @change="handleTeamChange">
178
+                <el-option v-for="t in teamOptions" :key="t.id" :label="t.label" :value="t.id" />
179
+              </el-select>
156 180
             </el-form-item>
157 181
           </el-col>
158 182
           <el-col :span="12">
159
-            <el-form-item label="队室/班组">
160
-              <el-input v-model="form.teamName" placeholder="请输入队室/班组" />
183
+            <el-form-item label="通道/小组">
184
+              <el-select v-model="form.groupId" placeholder="请选择通道/小组" style="width:100%" clearable
185
+                :disabled="form.org < 3" @change="handleGroupChange">
186
+                <el-option v-for="g in groupOptions" :key="g.id" :label="g.label" :value="g.id" />
187
+              </el-select>
161 188
             </el-form-item>
162 189
           </el-col>
163 190
           <el-col :span="12">
164
-            <el-form-item label="小组">
165
-              <el-input v-model="form.groupName" placeholder="请输入小组" />
191
+            <el-form-item label="责任人">
192
+              <el-select v-model="form.personId" placeholder="请选择责任人" style="width:100%" clearable
193
+                :disabled="form.org < 4">
194
+                <el-option v-for="p in personOptions" :key="p.userId" :label="p.nickName" :value="p.userId" />
195
+              </el-select>
166 196
             </el-form-item>
167 197
           </el-col>
168 198
           <el-col :span="12">
169 199
             <el-form-item label="基础分值" prop="scoreValue">
170
-              <el-input-number v-model="form.scoreValue" :precision="2" style="width:100%"
171
-                               placeholder="正数=加分 负数=扣分" />
200
+              <el-input-number v-model="form.scoreValue" :precision="2" style="width:100%" placeholder="正数=加分 负数=扣分" />
172 201
             </el-form-item>
173 202
           </el-col>
174 203
           <el-col :span="12">
175 204
             <el-form-item label="叠加分值">
176
-              <el-input-number v-model="form.cascadeScore" :precision="2" style="width:100%"
177
-                               placeholder="叠加后果产生的分值" />
205
+              <el-input-number v-model="form.cascadeScore" :precision="2" style="width:100%" placeholder="叠加后果产生的分值" />
178 206
             </el-form-item>
179 207
           </el-col>
180 208
           <el-col :span="24">
@@ -198,17 +226,23 @@
198 226
 </template>
199 227
 
200 228
 <script setup>
201
-import { ref, reactive, onMounted } from 'vue'
229
+import { ref, reactive, computed, onMounted, getCurrentInstance } from 'vue'
202 230
 import { ElMessage, ElMessageBox } from 'element-plus'
231
+import { listUser } from '@/api/system/user'
203 232
 import {
204 233
   listScoreEvent, addScoreEvent, updateScoreEvent, delScoreEvent,
205 234
   exportScoreEvent, importScoreEvent
206 235
 } from '@/api/score/index'
207 236
 import { allDimension, treeIndicator } from '@/api/score/index'
237
+import { listDept } from '@/api/system/dept'
238
+import { deptTreeSelect } from '@/api/system/user'
208 239
 import { parseTime } from '@/utils/ruoyi'
209 240
 
210 241
 defineOptions({ name: 'ScoreEvent' })
211 242
 
243
+const { proxy } = getCurrentInstance()
244
+const { score_level } = proxy.useDict('score_level')
245
+
212 246
 const loading = ref(false), list = ref([]), total = ref(0), showSearch = ref(true)
213 247
 const dateRange = ref([]), queryRef = ref(null), formRef = ref(null)
214 248
 const dialogVisible = ref(false), dialogTitle = ref('')
@@ -218,9 +252,13 @@ const indicatorTree = ref([])  // 当前维度的指标树(扁平+嵌套)
218 252
 const level2Options = ref([])
219 253
 const level3Options = ref([])
220 254
 const level4Options = ref([])
255
+const deptOptions = ref([])
256
+const teamOptions = ref([])
257
+const groupOptions = ref([])
258
+const personOptions = ref([])
221 259
 
222
-const queryParams = reactive({ pageNum: 1, pageSize: 10, personName: '', deptName: '', teamName: '', dimensionId: null, sourceType: '' })
223
-const form = reactive({ id: null, dimensionId: null, dimensionName: '', indicatorId: null, level2Name: '', level3Name: '', level4Name: '', eventTime: '', location: '', personName: '', deptName: '', teamName: '', groupName: '', scoreValue: 0, cascadeScore: 0, eventDesc: '', remark: '' })
260
+const queryParams = reactive({ pageNum: 1, pageSize: 10, personName: '', deptName: '', teamName: '', dimensionId: null, sourceType: '', org: '' })
261
+const form = reactive({ id: null, dimensionId: null, dimensionName: '', indicatorId: null, level2Id: null, level2Name: '', level3Id: null, level3Name: '', level4Id: null, level4Name: '', eventTime: '', location: '', personId: null, deptName: '', deptId: null, teamId: null, groupId: null, scoreValue: 0, cascadeScore: 0, eventDesc: '', remark: '', org: '' })
224 262
 const rules = {
225 263
   dimensionId: [{ required: true, message: '请选择维度', trigger: 'change' }],
226 264
   personName: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
@@ -228,11 +266,62 @@ const rules = {
228 266
   scoreValue: [{ required: true, message: '请输入分值', trigger: 'blur' }]
229 267
 }
230 268
 
269
+
270
+
231 271
 async function loadDimensions() {
232 272
   const r = await allDimension()
233 273
   dimensionOptions.value = r.data || []
234 274
 }
235 275
 
276
+async function loadDepts() {
277
+  const r = await listDept()
278
+  deptOptions.value = (r.data || []).filter(d => d.deptType === 'BRIGADE')
279
+}
280
+
281
+async function handleDeptChange(deptId, preserve) {
282
+  if (!preserve) {
283
+    form.teamId = null; form.groupId = null; form.personId = null
284
+    groupOptions.value = []; personOptions.value = []
285
+  }
286
+  if (deptId) {
287
+    const r = await deptTreeSelect({ parentId: deptId })
288
+    teamOptions.value = r.data || []
289
+    if (preserve && form.teamId) {
290
+      await handleTeamChange(form.teamId, true)
291
+    }
292
+  } else {
293
+    teamOptions.value = []
294
+  }
295
+}
296
+
297
+async function handleTeamChange(val, preserve) {
298
+  if (!preserve) {
299
+    form.groupId = null; form.personId = null
300
+    personOptions.value = []
301
+  }
302
+  const team = teamOptions.value.find(t => t.id === val)
303
+  if (team) {
304
+    const r = await deptTreeSelect({ parentId: team.id })
305
+    groupOptions.value = r.data || []
306
+    if (preserve && form.groupId) {
307
+      await handleGroupChange(form.groupId, true)
308
+    }
309
+  } else {
310
+    groupOptions.value = []
311
+  }
312
+}
313
+
314
+async function handleGroupChange(val, preserve) {
315
+  if (!preserve) form.personId = null
316
+  const group = groupOptions.value.find(g => g.id === val)
317
+  if (group) {
318
+    const r = await listUser({ deptId: group.id })
319
+    personOptions.value = r.rows || []
320
+  } else {
321
+    personOptions.value = []
322
+  }
323
+}
324
+
236 325
 function getList() {
237 326
   loading.value = true
238 327
   const p = { ...queryParams }
@@ -245,8 +334,9 @@ function resetQuery() { dateRange.value = []; queryRef.value?.resetFields(); han
245 334
 function handleSelectionChange(sel) { ids.value = sel.map(s => s.id); single.value = sel.length !== 1; multiple.value = !sel.length }
246 335
 
247 336
 function resetForm() {
248
-  Object.assign(form, { id: null, dimensionId: null, dimensionName: '', indicatorId: null, level2Name: '', level3Name: '', level4Name: '', eventTime: '', location: '', personName: '', deptName: '', teamName: '', groupName: '', scoreValue: 0, cascadeScore: 0, eventDesc: '', remark: '' })
337
+  Object.assign(form, { id: null, dimensionId: null, dimensionName: '', indicatorId: null, level2Id: null, level2Name: '', level3Id: null, level3Name: '', level4Id: null, level4Name: '', eventTime: '', location: '', personId: null, deptName: '', deptId: null, teamId: null, groupId: null, scoreValue: 0, cascadeScore: 0, eventDesc: '', remark: '', org: '' })
249 338
   level2Options.value = []; level3Options.value = []; level4Options.value = []
339
+  teamOptions.value = []; groupOptions.value = []; personOptions.value = []
250 340
 }
251 341
 
252 342
 function handleAdd() { resetForm(); dialogTitle.value = '新增配分事项'; dialogVisible.value = true }
@@ -256,6 +346,7 @@ function handleUpdate(row) {
256 346
   if (record) {
257 347
     Object.assign(form, record)
258 348
     if (record.dimensionId) onDimensionChange(record.dimensionId, true)
349
+    if (record.deptId) handleDeptChange(record.deptId, true)
259 350
   }
260 351
   dialogTitle.value = '修改配分事项'; dialogVisible.value = true
261 352
 }
@@ -263,27 +354,45 @@ function handleUpdate(row) {
263 354
 async function onDimensionChange(dimId, preserveSelection) {
264 355
   const dim = dimensionOptions.value.find(d => d.id === dimId)
265 356
   if (dim) form.dimensionName = dim.name
266
-  const r = await treeIndicator(dimId)
357
+  const r = await treeIndicator({ dimensionId: dimId })
267 358
   indicatorTree.value = r.data || []
268
-  level2Options.value = indicatorTree.value.map(n => n.name)
269
-  if (!preserveSelection) { form.level2Name = ''; form.level3Name = ''; form.level4Name = '' }
359
+  level2Options.value = indicatorTree.value.map(n => ({name: n.name,id: n.id, scoreValue: n.scoreValue}))
360
+  if (!preserveSelection) { form.level2Id = null; form.level2Name = ''; form.level3Id = null; form.level3Name = ''; form.level4Id = null; form.level4Name = ''; form.scoreValue = 0 }
270 361
   level3Options.value = []; level4Options.value = []
271
-  if (preserveSelection && form.level2Name) onLevel2Change(form.level2Name, true)
362
+  if (preserveSelection && form.level2Id) onLevel2Change(form.level2Id, true)
272 363
 }
273 364
 
274 365
 function onLevel2Change(val, preserveSelection) {
275
-  const node = indicatorTree.value.find(n => n.name === val)
276
-  level3Options.value = node ? (node.children || []).map(n => n.name) : []
277
-  if (!preserveSelection) { form.level3Name = ''; form.level4Name = '' }
366
+  const node = indicatorTree.value.find(n => n.id === val)
367
+  if (node) {
368
+    form.level2Name = node.name
369
+    if (node.scoreValue !== undefined) form.scoreValue = node.scoreValue
370
+  }
371
+  level3Options.value = node ? (node.children || []).map(n => ({name: n.name,id: n.id, scoreValue: n.scoreValue})) : []
372
+  if (!preserveSelection) { form.level3Id = null; form.level3Name = ''; form.level4Id = null; form.level4Name = ''; if (node && node.scoreValue === undefined) form.scoreValue = 0 }
278 373
   level4Options.value = []
279
-  if (preserveSelection && form.level3Name) onLevel3Change(form.level3Name, true)
374
+  if (preserveSelection && form.level3Id) onLevel3Change(form.level3Id, true)
280 375
 }
281 376
 
282 377
 function onLevel3Change(val, preserveSelection) {
283
-  const level2Node = indicatorTree.value.find(n => n.name === form.level2Name)
284
-  const node = level2Node ? (level2Node.children || []).find(n => n.name === val) : null
285
-  level4Options.value = node ? (node.children || []).map(n => n.name) : []
286
-  if (!preserveSelection) form.level4Name = ''
378
+  const level2Node = indicatorTree.value.find(n => n.id === form.level2Id)
379
+  const node = level2Node ? (level2Node.children || []).find(n => n.id === val) : null
380
+  if (node) {
381
+    form.level3Name = node.name
382
+    if (node.scoreValue !== undefined) form.scoreValue = node.scoreValue
383
+  }
384
+  level4Options.value = node ? (node.children || []).map(n => ({name: n.name,id: n.id, scoreValue: n.scoreValue})) : []
385
+  if (!preserveSelection) { form.level4Id = null; form.level4Name = ''; if (node && node.scoreValue === undefined) form.scoreValue = 0 }
386
+}
387
+
388
+function onLevel4Change(val) {
389
+  const level2Node = indicatorTree.value.find(n => n.id === form.level2Id)
390
+  const level3Node = level2Node ? (level2Node.children || []).find(n => n.id === form.level3Id) : null
391
+  const node = level3Node ? (level3Node.children || []).find(n => n.id === val) : null
392
+  if (node) {
393
+    form.level4Name = node.name
394
+    if (node.scoreValue !== undefined) form.scoreValue = node.scoreValue
395
+  }
287 396
 }
288 397
 
289 398
 async function submitForm() {
@@ -303,7 +412,10 @@ function handleDelete(row) {
303 412
 function handleExport() {
304 413
   const p = { ...queryParams }
305 414
   if (dateRange.value?.length === 2) { p['params[beginTime]'] = dateRange.value[0]; p['params[endTime]'] = dateRange.value[1] }
306
-  exportScoreEvent(p)
415
+  // exportScoreEvent(p)
416
+  proxy.download('score/event/export', {
417
+    ...p
418
+  }, `配分事项_${new Date().getTime()}.xlsx`)
307 419
 }
308 420
 
309 421
 async function handleImportFile(file) {
@@ -316,5 +428,5 @@ async function handleImportFile(file) {
316 428
   }
317 429
 }
318 430
 
319
-onMounted(() => { loadDimensions(); getList() })
431
+onMounted(() => { loadDimensions(); loadDepts(); getList() })
320 432
 </script>