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

feat(portraitManagement): add tag score display and optimize radar chart

1. 新增标签得分接口countTagScore并在员工画像页面展示该数据
2. 优化雷达图进度条计算逻辑与样式,新增分数颜色分级
3. 优化搜索组件的下拉选项展示,增加部门路径显示
4. 修复搜索选中项的参数传递与显示文本问题
huoyi преди 3 седмици
родител
ревизия
fac9e17976

+ 4 - 1
src/api/portraitManagement/portraitManagement.js

@@ -95,4 +95,7 @@ export function countStationTeamStats(data) {
95 95
 export function countDeptTeamStats(data) {
96 96
     return request({ url: '/score/dept-portrait/team-stats', method: 'post', data })
97 97
 }
98
-
98
+//标签得分
99
+export function countTagScore(params) {
100
+    return request({ url: '/ledger/scoreEmployeeAdditional/getTotalScoreByEmployee', method: 'get', params: params })
101
+}

+ 56 - 20
src/views/portraitManagement/components/ProfileRadar.vue

@@ -7,8 +7,9 @@
7 7
           <span class="item-label">{{ item.name }}</span>
8 8
           <div class="progress-row">
9 9
             <div class="progress-bar">
10
-              <div class="progress-fill" :style="{ width: (item.finalScore || 0) + '%', background: `linear-gradient(to right, transparent, ${item.color})` }">
11
-                <span class="progress-end"></span>
10
+              <div class="progress-fill"
11
+                :style="{ width: ((item.finalScore || 0) / (computedMaxScore || 1) * 100) + '%', background: '#2babeb' }">
12
+                <span class="progress-end" :style="{ background: '#2babeb' }"></span>
12 13
               </div>
13 14
             </div>
14 15
             <span class="item-value">{{ item.finalScore || 0 }}</span>
@@ -31,32 +32,44 @@ const props = defineProps({
31 32
   }
32 33
 })
33 34
 
35
+const getScoreColor = (score) => {
36
+  if (score < 75) return '#ff4d4f'
37
+  if (score >= 90) return '#52c41a'
38
+  return '#ffffff'
39
+}
40
+
34 41
 const freshColors = [
35 42
   '#00e5ff', '#36d399', '#fbbf24', '#f472b6', '#a78bfa',
36 43
   '#34d399', '#f97316', '#2dd4bf', '#e879f9', '#38bdf8'
37 44
 ]
38 45
 
39
-const defaultRadarData = [
40
-
41
-]
46
+const defaultRadarData = []
42 47
 
43 48
 const computedRadarData = computed(() => {
44 49
   const data = props.chartData.length > 0 ? props.chartData : defaultRadarData
45 50
   return data.map((item, index) => ({
46 51
     ...item,
47
-    color: item.color || freshColors[index % freshColors.length]
52
+    color: item.color || freshColors[index % freshColors.length],
53
+    itemColor: getScoreColor(item.finalScore || 0)
48 54
   }))
49 55
 })
50 56
 
51 57
 const computedIndicators = computed(() => {
52 58
   const data = computedRadarData.value
53
-  const maxValue = Math.max(...data.map(item => item.finalScore || 0), 100)
59
+  const allScores = data.map(item => item.finalScore || 0)
60
+  const maxValue = allScores.length > 0 ? Math.max(...allScores) : 100
54 61
   return data.map(item => ({
55 62
     name: item.name,
56
-    max: maxValue
63
+    max: Math.max(maxValue, 1)
57 64
   }))
58 65
 })
59 66
 
67
+const computedMaxScore = computed(() => {
68
+  const data = computedRadarData.value
69
+  const allScores = data.map(item => item.finalScore || 0)
70
+  return allScores.length > 0 ? Math.max(...allScores) : 100
71
+})
72
+
60 73
 const computedSeries = computed(() => {
61 74
   return computedRadarData.value.map(item => item.finalScore || 0)
62 75
 })
@@ -71,6 +84,11 @@ const updateRadarChart = () => {
71 84
     radarChart = echarts.init(radarChartRef.value)
72 85
   }
73 86
 
87
+  const radarData = computedRadarData.value
88
+  const indicators = computedIndicators.value
89
+  const pointColors = radarData.map(item => item.itemColor)
90
+  const pointValues = radarData.map(item => item.finalScore || 0)
91
+
74 92
   const option = {
75 93
     grid: {
76 94
       top: 40,
@@ -79,7 +97,7 @@ const updateRadarChart = () => {
79 97
       right: 50
80 98
     },
81 99
     radar: {
82
-      indicator: computedIndicators.value,
100
+      indicator: indicators,
83 101
       center: ['50%', '50%'],
84 102
       radius: '60%',
85 103
       splitNumber: 4,
@@ -90,7 +108,7 @@ const updateRadarChart = () => {
90 108
       },
91 109
       splitArea: {
92 110
         areaStyle: {
93
-          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)']
111
+          color: ['rgba(15, 70, 250, 0)', 'rgba(15, 70, 250, 0)', 'rgba(15, 70, 250, 0)', 'rgba(15, 70, 250, 0)']
94 112
         }
95 113
       },
96 114
       splitLine: {
@@ -108,25 +126,42 @@ const updateRadarChart = () => {
108 126
     series: [
109 127
       {
110 128
         type: 'radar',
129
+        symbol: 'circle',
130
+        symbolSize: 13,
111 131
         data: [
112 132
           {
113
-            value: computedSeries.value,
133
+            value: pointValues,
114 134
             name: '综合得分',
115 135
             areaStyle: {
116
-              color: 'rgba(189, 3, 251, 0.3)'
136
+              color: 'rgba(189, 3, 251, 0)'
117 137
             },
118 138
             lineStyle: {
119
-              color: '#bd03fb',
139
+              // color: '#bd03fb',
120 140
               width: 2
121 141
             },
122
-            itemStyle: {
123
-              color: '#bd03fb'
124
-            }
142
+            // itemStyle: {
143
+            //   color: 'transparent'
144
+            // }
125 145
           }
126
-        ],
127
-        symbol: 'circle',
128
-        symbolSize: 6
129
-      }
146
+        ]
147
+      },
148
+      // ...(pointValues.length > 0 ? [
149
+      //   {
150
+      //     type: 'scatter',
151
+      //     coordinateSystem: 'radar',
152
+      //     data: pointValues.map((_, i) => {
153
+      //       const arr = new Array(pointValues.length).fill(null)
154
+      //       arr[i] = pointValues[i]
155
+      //       return { value: arr }
156
+      //     }),
157
+      //     symbol: 'circle',
158
+      //     symbolSize: 22,
159
+      //     itemStyle: {
160
+      //       color: ({ dataIndex }) => pointColors[dataIndex]
161
+      //     },
162
+      //     z: 10
163
+      //   }
164
+      // ] : [])
130 165
     ],
131 166
     tooltip: {
132 167
       trigger: 'item',
@@ -205,6 +240,7 @@ onUnmounted(() => {
205 240
           border-radius: 0;
206 241
           overflow: visible;
207 242
 
243
+
208 244
           .progress-fill {
209 245
             height: 100%;
210 246
             border-radius: 0;

+ 74 - 18
src/views/portraitManagement/components/SearchBar.vue

@@ -12,26 +12,30 @@
12 12
             start-placeholder="开始" end-placeholder="结束" style="width:320px" @change="() => searchHandler(curQuery)" />
13 13
         </div>
14 14
       </div>
15
-      <el-popover v-if=" deptType !== 'STATION' " class="popover" title="" :visible="visible" placement="bottom-start" trigger="click" width="45vw">
15
+      <el-popover v-if="deptType !== 'STATION'" class="popover" title="" :visible="visible" placement="bottom-start"
16
+        trigger="click" width="45vw">
16 17
         <template #reference>
17 18
           <div class="primary" style="border-radius: 6px;" @click.stop="visible = !visible">{{ props.deptType === 'user'
18 19
             ?
19
-            '组织架构/模糊搜索' : deptType === 'BRIGADE' ?  '部门选择' : deptType === 'MANAGER' ? '班组选择' :   deptType === 'TEAMS' ? '小组选择' : '组织选择' }}</div>
20
+            '组织架构/模糊搜索' : deptType === 'BRIGADE' ? '部门选择' : deptType === 'MANAGER' ? '班组选择' : deptType === 'TEAMS' ?
21
+              '小组选择' : '组织选择' }}</div>
20 22
         </template>
21 23
 
22 24
         <div class="custom-el-style">
23 25
           <div>
24 26
             <el-autocomplete v-model="personName"
25 27
               :fetch-suggestions="props.deptType === 'user' ? queryUsers : queryDept"
26
-              :placeholder="props.deptType === 'user' ? '搜索员工/团队画像' : '搜索部门'" style="width:320px;" clearable
28
+              :placeholder="props.deptType === 'user' ? '搜索员工/团队画像' : '搜索部门'" style="width:400px;" clearable
27 29
               @select="handleSelect">
28 30
               <template #suffix><el-icon>
29 31
                   <Search />
30 32
                 </el-icon></template>
31 33
               <template #default="{ item }">
32
-                <span>{{ item.nickName || item.deptName }}</span>
33
-                <span v-if="item.deptName && item.nickName" style="font-size:12px;color:#999;margin-left:8px">{{
34
-                  item.deptName }}</span>
34
+                <div style="display:flex;flex-direction:column;gap:2px;">
35
+                  <span style="font-size:14px;color:#fff;">{{ item.nickName || item.deptName || item.name || item.label
36
+                    }}</span>
37
+                  <span v-if="item.path && item.path.length > 0" style="font-size:12px;color:#999;">{{ item.path.join('/ ') }}</span>
38
+                </div>
35 39
               </template>
36 40
             </el-autocomplete>
37 41
             <!-- <div class="primary" style="border-radius: 6px;" @click="() => searchHandler()">模糊搜索</div> -->
@@ -90,11 +94,40 @@ const findNodeById = (nodes, id) => {
90 94
   return null
91 95
 }
92 96
 
97
+const buildPathForNode = (nodes, targetId, path = []) => {
98
+  if (!nodes) return path
99
+  for (const node of nodes) {
100
+    if (node.id === targetId || node.deptId === targetId) {
101
+      // 如果是用户节点,不把用户名字加入 path
102
+      if (node.deptType === 'user' || node.nodeType === 'user') {
103
+        return path
104
+      }
105
+      const name = node.nickName || node.deptName || node.name || node.label
106
+      return [...path, name]
107
+    }
108
+    if (node.children) {
109
+      const name = node.deptName || node.name || node.label
110
+      const found = buildPathForNode(node.children, targetId, [...path, name])
111
+      if (found.length > path.length) {
112
+        return found
113
+      }
114
+    }
115
+  }
116
+  return path
117
+}
118
+
93 119
 const queryUsers = async (query, cb) => {
94 120
   if (!query?.trim()) { cb([]); return }
95 121
   try {
96 122
     const res = await searchPortraitUsers(query.trim())
97
-    cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
123
+    const mapped = (res.data || []).map(u => {
124
+      let path = []
125
+      if (u.deptId) {
126
+        path = buildPathForNode(departments.value, u.deptId)
127
+      }
128
+      return { ...u, value: u.nickName, path }
129
+    })
130
+    cb(mapped)
98 131
   } catch (_) { cb([]) }
99 132
 }
100 133
 
@@ -102,17 +135,30 @@ const queryDept = async (query, cb) => {
102 135
   if (!query?.trim()) { cb([]); return }
103 136
   try {
104 137
     const res = await listDept({ deptName: query.trim(), deptType: props.deptType })
105
-    cb((res.data || []).map(d => ({ ...d, value: d.deptName })))
138
+    const mapped = (res.data || []).map(d => {
139
+      let path = []
140
+      if (d.id || d.deptId) {
141
+        path = buildPathForNode(departments.value, d.deptId || d.id)
142
+      }
143
+      return { ...d, value: d.deptName, path }
144
+    })
145
+    cb(mapped)
106 146
   } catch (_) { cb([]) }
107 147
 }
108 148
 
109 149
 const handleSelect = (item) => {
150
+  
110 151
   if (props.deptType === 'user') {
111
-    personName.value = item.nickName
112
-    curQuery.value = { personName: item.nickName }
152
+    const path = item.path && item.path.length > 0 ? item.path : (item.deptId ? buildPathForNode(departments.value, item.deptId) : [])
153
+    const name = item.nickName
154
+
155
+    personName.value = path.length > 0 ? `${path.join(' / ')} / ${name}` : name
156
+    curQuery.value = { personName: item.nickName,id:item.id }
113 157
     searchHandler(curQuery.value)
114 158
   } else {
115
-    personName.value = item.deptName
159
+    const path = item.path && item.path.length > 0 ? item.path : ((item.id || item.deptId) ? buildPathForNode(departments.value, item.deptId || item.id) : [])
160
+    const name = item.deptName || item.name || item.label
161
+    personName.value = path.length > 0 ? path.join(' / ') : name
116 162
     if (props.deptType === 'BRIGADE' || props.deptType === 'STATION') {
117 163
       curQuery.value = { deptId: item.deptId || item.id }
118 164
     }
@@ -192,13 +238,19 @@ const searchHandler = (query = {}) => {
192 238
 const handleNodeClick = (node) => {
193 239
   if (props.deptType === 'user') {
194 240
     if (node.nodeType === 'user') {
195
-      personName.value = node.label
196
-      curQuery.value = { personName: node.label }
241
+   
242
+      const path = buildPathForNode(departments.value, node.deptId || node.id)
243
+      const name = node.label
244
+      
245
+      personName.value = path.length > 0 ? `${path.join(' / ')} / ${name}` : name
246
+      curQuery.value = { personName: node.label,id:node.id }
197 247
       searchHandler(curQuery.value)
198 248
     }
199 249
   } else {
200 250
     if (node.deptType === props.deptType) {
201
-      personName.value = node.deptName || node.name || node.label;
251
+      const path = buildPathForNode(departments.value, node.deptId || node.id)
252
+      const name = node.deptName || node.name || node.label;
253
+      personName.value = path.length > 0 ? path.join(' / ') : name
202 254
       if (props.deptType === 'BRIGADE' || props.deptType === 'STATION') {
203 255
         curQuery.value = { deptId: node.deptId || node.id }
204 256
       }
@@ -253,11 +305,15 @@ onMounted(async () => {
253 305
     if (defaultNode) {
254 306
       currentKey.value = defaultId
255 307
       if (props.deptType === 'user') {
256
-        personName.value = defaultNode.label
308
+        const path = buildPathForNode(departments.value, defaultNode.deptId || defaultNode.id)
309
+        const name = defaultNode.label
310
+        personName.value = path.length > 0 ? `${path.join(' / ')} / ${name}` : name
257 311
         curQuery.value = { personName: defaultNode.label }
258 312
         searchHandler(curQuery.value)
259 313
       } else {
260
-        personName.value = defaultNode.deptName || defaultNode.name || defaultNode.label
314
+        const path = buildPathForNode(departments.value, defaultNode.deptId || defaultNode.id)
315
+        const name = defaultNode.deptName || defaultNode.name || defaultNode.label
316
+        personName.value = path.length > 0 ? path.join(' / ') : name
261 317
         if (props.deptType === 'BRIGADE' || props.deptType === 'STATION') {
262 318
           curQuery.value = { deptId: defaultNode.deptId || defaultNode.id }
263 319
         }
@@ -281,10 +337,10 @@ onUnmounted(() => {
281 337
 
282 338
 defineExpose({
283 339
   getDefQuery() {
284
-   
340
+
285 341
     return {
286 342
       ...getTimeRange(),
287
-    
343
+
288 344
     }
289 345
   }
290 346
 })

+ 31 - 4
src/views/portraitManagement/employeeProfile/index.vue

@@ -96,7 +96,7 @@
96 96
                   </div>
97 97
                   <div class="score-row">
98 98
                     <span class="score-col">标签得分:</span>
99
-                    <span class="score-col-2">{{ portrait.totalScore || portrait.totalScore == 0 ? 2 : 0 }}</span>
99
+                    <span class="score-col-2">{{ tagScoreData != null ? (typeof tagScoreData === 'object' ? (tagScoreData.totalScore ?? tagScoreData.score ?? tagScoreData) : tagScoreData) : 0 }}</span>
100 100
                   </div>
101 101
                 </div>
102 102
               </div>
@@ -223,17 +223,21 @@ import Page from '../components/page.vue'
223 223
 import Card from '../components/card.vue'
224 224
 import SearchBar from '../components/SearchBar.vue'
225 225
 import { getEmployeePortrait } from '@/api/score/index'
226
-import { onMounted, reactive, ref } from 'vue'
226
+import { countTagScore } from '@/api/portraitManagement/portraitManagement'
227
+import { onMounted, reactive, ref, computed } from 'vue'
227 228
 import { useDict } from '@/utils/dict'
228 229
 import { useECharts } from '@/hooks/useEcharts'
230
+import useUserStore from '@/store/modules/user'
229 231
 
230 232
 
231 233
 const { sys_user_schooling } = useDict('sys_user_schooling')
234
+const userStore = useUserStore()
232 235
 const visible = ref(false)
233 236
 const loading = ref(false)
234 237
 const portrait = ref({ dimensions: [] })
235 238
 const abilityChart = ref(null)
236 239
 const activeDimName = ref(null)
240
+const tagScoreData = ref(null)
237 241
 
238 242
 const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
239 243
 
@@ -291,13 +295,36 @@ const formatWorkDate = (d) => {
291 295
 
292 296
 const invokerEmployeePortrait = (query) => {
293 297
   loading.value = true
294
-  return getEmployeePortrait(query).then(res => {
298
+  const userId = query.id
299
+  const queryData = {
300
+    ...query
301
+  }
302
+  delete queryData.id
303
+  const portraitPromise = getEmployeePortrait(queryData).then(res => {
295 304
     portrait.value = res.data || { dimensions: [], awards: [] }
296 305
     portrait.value.awards.forEach(item => {
297 306
       item.color = getRandomHexColor()
298 307
     })
299 308
     permutationRadarDataHandler(res.data.dimensions)
300
-  }).finally(() => {
309
+  })
310
+  const tagPromise = countTagScore(queryData).then(res => {
311
+    const data = res.data
312
+    if (Array.isArray(data)) {
313
+   
314
+   
315
+      const found = data.find(item => (item.userId == userId) )
316
+      
317
+      if (found) {
318
+        
319
+        tagScoreData.value = found.totalScore ?? found.score
320
+      } else {
321
+        tagScoreData.value = null
322
+      }
323
+    } else {
324
+      tagScoreData.value = data
325
+    }
326
+  })
327
+  return Promise.all([portraitPromise, tagPromise]).finally(() => {
301 328
     loading.value = false
302 329
   })
303 330
 }