index.vue 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024
  1. <template>
  2. <Page @click="visible = false" v-loading="loading" element-loading-background="rgba(22, 22, 22, 0.8)">
  3. <div class="content">
  4. <div class="content-search">
  5. <SearchBar v-model:visible="visible" @search="searchHandler" :deptType="'user'" />
  6. </div>
  7. <div v-show="portrait.personName" class="content-info">
  8. <Card title="个人基本信息">
  9. <div class="userInfo">
  10. <div class="userInfo-name">
  11. <div class="avatar-area">
  12. <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
  13. <div v-else class="avatar">{{ portrait.personName?.charAt(0) }}</div>
  14. </div>
  15. <div class="basic-name">
  16. <div class="basic-label">姓名:</div>
  17. <div class="basic-value">{{ portrait.personName }}</div>
  18. </div>
  19. </div>
  20. <div class="userInfo-info">
  21. <div class="info-item">
  22. <div class="info-item-icon">
  23. <img src="/src/assets/dataBigScreen/img01.png" alt="" />
  24. </div>
  25. <div class="info-item-content">
  26. <div class="info-item-label">所属部门及队室:</div>
  27. <div class="info-item-value">{{ portrait.deptPath || '-' }}</div>
  28. </div>
  29. </div>
  30. <div class="info-item">
  31. <div class="info-item-icon">
  32. <img src="/src/assets/dataBigScreen/img04.png" alt="" />
  33. </div>
  34. <div class="info-item-content">
  35. <div class="info-item-label">学历:</div>
  36. <div class="info-item-value">{{ getSchooling(portrait.schooling) }}</div>
  37. </div>
  38. </div>
  39. </div>
  40. <div class="userInfo-info">
  41. <div class="info-item">
  42. <div class="info-item-icon">
  43. <img src="/src/assets/dataBigScreen/img02.png" alt="" />
  44. </div>
  45. <div class="info-item-content">
  46. <div class="info-item-label">出生日期:</div>
  47. <div class="info-item-value">{{ portrait.birthday || '-' }}</div>
  48. </div>
  49. </div>
  50. <div class="info-item">
  51. <div class="info-item-icon">
  52. <img src="/src/assets/dataBigScreen/img05.png" alt="" />
  53. </div>
  54. <div class="info-item-content">
  55. <div class="info-item-label">专业:</div>
  56. <div class="info-item-value">{{ portrait.major || '-' }}</div>
  57. </div>
  58. </div>
  59. </div>
  60. <div class="userInfo-info">
  61. <div class="info-item">
  62. <div class="info-item-icon">
  63. <img src="/src/assets/dataBigScreen/img03.png" alt="" />
  64. </div>
  65. <div class="info-item-content">
  66. <div class="info-item-label">技能等级:</div>
  67. <div class="info-item-value">
  68. {{ portrait.qualificationLevelText || '-' }}
  69. <span class="info-item-tag" style="margin-left: 10px;margin-top: 2px;"
  70. v-if="!!portrait.xrayOperatorStarttime || portrait?.postNames?.includes('X射线安检仪操作岗位')">X射线安检仪操作岗位</span>
  71. </div>
  72. </div>
  73. </div>
  74. <div class="info-item">
  75. <div class="info-item-icon">
  76. <img src="/src/assets/dataBigScreen/img06.png" alt="" />
  77. </div>
  78. <div class="info-item-content">
  79. <div class="info-item-label">标签:</div>
  80. <div class="info-item-value" v-if="portrait.userTags">
  81. <span class="info-item-tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag }}</span>
  82. </div>
  83. </div>
  84. </div>
  85. </div>
  86. <div class="userInfo-score">
  87. <div class="score-progress">
  88. <el-progress type="circle" :width="160" :stroke-width="18" color="#5BE39E"
  89. :percentage="(portrait.totalScore || -2) + 2">
  90. <div class="percentage-content">
  91. <span class="percentage-value">{{ (portrait.totalScore || 0) }}</span>
  92. <span class="percentage-text">综合得分</span>
  93. </div>
  94. </el-progress>
  95. </div>
  96. <div class="score-box">
  97. <!-- <div class="score-row">
  98. <span class="score-col">评分:</span>
  99. <span class="score-col-2">{{ portrait.totalScore || 0 }}</span>
  100. </div> -->
  101. <div class="score-row">
  102. <span class="score-col">附加分:</span>
  103. <span class="score-col-2">{{ portrait?.userTags ? portrait?.userTags?.split(',').length || 0 : 0 }}分</span>
  104. <!-- <span class="score-col-2">{{ tagScoreData != null ? (typeof tagScoreData === 'object' ? (tagScoreData.totalScore ?? tagScoreData.score ?? tagScoreData) : tagScoreData) : 0 }}</span> -->
  105. </div>
  106. </div>
  107. </div>
  108. </div>
  109. </Card>
  110. </div>
  111. <div v-show="portrait.personName" class="content-bottom">
  112. <div class="content-bottom-left">
  113. <Card title="工作履历">
  114. <div class="work-history">
  115. <span v-if="portrait.entryDate">
  116. {{ formatWorkDate(portrait.entryDate) }}入职 | 司龄{{ portrait.companyYears != null ? portrait.companyYears
  117. : '-' }}年 | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-'
  118. }}年 | 现任职{{ portrait.roleNames || '-' }}
  119. </span>
  120. <span v-else>暂无数据</span>
  121. </div>
  122. </Card>
  123. <Card title="获奖记录">
  124. <div class="card-content">
  125. <div class="honor-item" v-for="(value, index) in portrait.awards" :key="index">
  126. <div :style="{ '--indexBgColor': value.color }">
  127. <div :data-index="index + 1"></div>
  128. {{ value.level2Name }} {{ value.level4Name }}
  129. </div>
  130. <div>{{ value.score || '-' }}分</div>
  131. </div>
  132. </div>
  133. </Card>
  134. <Card title="职业资格证书获取时间">
  135. <div class="cert-info">
  136. <div class="cert-item" v-for="level in displayQualificationLevels" :key="level.label">
  137. <span class="cert-name">{{ level.label }}:{{ level.time }}</span>
  138. </div>
  139. <span v-if="displayQualificationLevels.length === 0" class="no-data">暂无证书数据</span>
  140. </div>
  141. </Card>
  142. </div>
  143. <div class="content-bottom-center">
  144. <Card title="个人能力">
  145. <div class="chart-legend">
  146. <div class="legend-item legend-warning"><span></span>预警线(低于75分)</div>
  147. <!-- <div class="legend-item legend-normal"><span></span>正常线(75~90分)</div> -->
  148. <div class="legend-item legend-excellent"><span></span>优秀线(高于90分)</div>
  149. <div class="legend-item legend-current"><span></span>当前员工分值</div>
  150. </div>
  151. <div ref="abilityChart" class="chart-box" @click="goToWarningPage" />
  152. </Card>
  153. </div>
  154. <div class="content-bottom-right">
  155. <Card title="补充信息">
  156. <div class="card-content">
  157. <div class="supp-item">
  158. <span class="s-lbl">政治面貌:</span>
  159. <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
  160. </div>
  161. <div class="supp-item supp-item-2">
  162. <span class="s-lbl">性别:</span>
  163. <span class="s-val">{{ portrait.sexText || '-' }}</span>
  164. </div>
  165. <div class="supp-item">
  166. <span class="s-lbl">籍贯:</span>
  167. <span class="s-val">{{ portrait.nativePlace || '-' }}</span>
  168. </div>
  169. <div class="supp-item supp-item-2">
  170. <span class="s-lbl">民族:</span>
  171. <span class="s-val">{{ portrait.nation || '-' }}</span>
  172. </div>
  173. <div class="supp-item">
  174. <span class="s-lbl">年龄:</span>
  175. <span class="s-val">{{ getAge(portrait.birthday) }}</span>
  176. </div>
  177. <div class="supp-item supp-item-2">
  178. <span class="s-lbl">司龄:</span>
  179. <span class="s-val">{{ portrait.companyYears != null ? portrait.companyYears + '年' : '-' }}</span>
  180. </div>
  181. <div class="supp-item">
  182. <span class="s-lbl">性格特征:</span>
  183. <span class="s-val">{{ portrait.characterCharacteristics || '-' }}</span>
  184. </div>
  185. <div class="supp-item supp-item-2">
  186. <span class="s-lbl">工作风格:</span>
  187. <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
  188. </div>
  189. <div :class="i % 2 ? 'supp-item' : 'supp-item supp-item-2'"
  190. v-for="(p, i) in (portrait.postNames || '').split('、')" :key="i">
  191. <span class="s-lbl">{{ i === 0 ? '业务岗位:' : '' }}</span>
  192. <span class="s-val">{{ p }}</span>
  193. </div>
  194. </div>
  195. </Card>
  196. </div>
  197. </div>
  198. <div v-show="!portrait.personName" class="ep-empty">
  199. <el-empty description="搜索员工姓名以查看画像" />
  200. </div>
  201. </div>
  202. <div class="radar-tooltip" ref="tipRef">
  203. <Card>
  204. <!-- 标题 -->
  205. <div class="tooltip-title">{{ activeDimName }}</div>
  206. <!-- 加分区域 -->
  207. <div class="tooltip-section">
  208. <div class="section-list">
  209. <div v-if="addGroupList.length" class="list-item" v-for="(item, i) in addGroupList" :key="i">
  210. <span>{{ item.name }}:</span>
  211. <span class="add-text">+{{ item.total }}分</span>
  212. </div>
  213. <div v-else class="p-empty">暂无加分记录</div>
  214. </div>
  215. <div class="section-total add">
  216. <span>合计加分:</span>
  217. <span>{{ addTotal }}分</span>
  218. </div>
  219. </div>
  220. <!-- 扣分区域 -->
  221. <div class="tooltip-section">
  222. <div class="section-list">
  223. <div v-if="deductGroupList.length" class="list-item" v-for="(item, i) in deductGroupList" :key="i">
  224. <span>{{ item.name }}:</span>
  225. <span class="deduct-text">{{ item.total }}分</span>
  226. </div>
  227. <div v-if="!deductGroupList.length" class="p-empty">暂无扣分记录</div>
  228. </div>
  229. <div class="section-total deduct">
  230. <span>合计扣分:</span>
  231. <span>{{ deductTotal }}分</span>
  232. </div>
  233. </div>
  234. </Card>
  235. </div>
  236. </Page>
  237. </template>
  238. <script setup>
  239. import Page from '../components/page.vue'
  240. import Card from '../components/card.vue'
  241. import SearchBar from '../components/SearchBar.vue'
  242. import { getEmployeePortrait } from '@/api/score/index'
  243. import { countTagScore } from '@/api/portraitManagement/portraitManagement'
  244. import { onMounted, onUnmounted, reactive, ref, computed, watch } from 'vue'
  245. import { useDict } from '@/utils/dict'
  246. import { getQualificationLevelTime } from '@/api/ledger/index'
  247. import { useECharts } from '@/hooks/useEcharts'
  248. import useUserStore from '@/store/modules/user'
  249. import { useRouter } from 'vue-router'
  250. defineOptions({ name: 'ProfileEmployeeProfile' })
  251. const router = useRouter()
  252. let radarChartInstance = null
  253. const { sys_user_schooling } = useDict('sys_user_schooling')
  254. const userStore = useUserStore()
  255. const visible = ref(false)
  256. const loading = ref(false)
  257. const portrait = ref({ dimensions: [] })
  258. const abilityChart = ref(null)
  259. const activeDimName = ref(null)
  260. const tagScoreData = ref(null)
  261. const currentQuery = ref(null)
  262. const qualificationData = ref(null)
  263. const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
  264. const addList = computed(() => {
  265. const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) > 0)
  266. return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
  267. })
  268. const deductList = computed(() => {
  269. const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) < 0)
  270. return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
  271. })
  272. const addGroupList = computed(() => {
  273. const groups = {}
  274. addList.value.forEach(d => {
  275. const key = d.level2Name || '其他'
  276. groups[key] = (groups[key] || 0) + Number(d.totalScore)
  277. })
  278. return Object.entries(groups).map(([name, total]) => ({ name, total: total.toFixed(2) }))
  279. })
  280. const deductGroupList = computed(() => {
  281. const groups = {}
  282. deductList.value.forEach(d => {
  283. const key = d.level2Name || '其他'
  284. groups[key] = (groups[key] || 0) + Number(d.totalScore)
  285. })
  286. return Object.entries(groups).map(([name, total]) => ({ name, total: total.toFixed(2) }))
  287. })
  288. const addTotal = computed(() => {
  289. const s = addList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
  290. return (s > 0 ? '+' : '') + s.toFixed(2)
  291. })
  292. const deductTotal = computed(() => {
  293. const s = deductList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
  294. return s.toFixed(2)
  295. })
  296. // 职业资格等级枚举
  297. const qualificationLevelMap = [
  298. { label: '五级', field: 'levelFiveTime' },
  299. { label: '四级', field: 'levelFourTime' },
  300. { label: '三级', field: 'levelThreeTime' },
  301. { label: '二级', field: 'levelTwoTime' },
  302. { label: '一级', field: 'levelOneTime' },
  303. ]
  304. const displayQualificationLevels = computed(() => {
  305. if (!qualificationData.value) return []
  306. return qualificationLevelMap.map(item => ({
  307. label: item.label,
  308. time: qualificationData.value[item.field]
  309. })).filter(item => item.time)
  310. })
  311. const getSchooling = (schooling) => {
  312. const result = (sys_user_schooling.value || []).find(item => item.value === schooling) || { label: schooling }
  313. return result.label || '-'
  314. }
  315. const getAge = (birthday) => {
  316. const birthDate = new Date(birthday);
  317. if (isNaN(birthDate.getTime())) {
  318. return '-'
  319. }
  320. const today = new Date();
  321. // 计算年份差
  322. let age = today.getFullYear() - birthDate.getFullYear();
  323. // 计算月份差
  324. const monthDiff = today.getMonth() - birthDate.getMonth();
  325. // 如果还没到今年生日,年龄减1
  326. if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
  327. age--;
  328. }
  329. return `${age}岁`;
  330. }
  331. const getRandomHexColor = () => {
  332. return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0');
  333. }
  334. const formatWorkDate = (d) => {
  335. if (!d) return '-'
  336. const str = String(d)
  337. const parts = str.substring(0, 10).split('-')
  338. if (parts.length < 3) return str
  339. return `${parts[0]}.${Number(parts[1])}.${Number(parts[2])}`
  340. }
  341. const invokerEmployeePortrait = (query) => {
  342. loading.value = true
  343. const userId = query.id
  344. const queryData = {
  345. ...query
  346. }
  347. delete queryData.id
  348. const portraitPromise = getEmployeePortrait(queryData).then(res => {
  349. portrait.value = res.data || { dimensions: [], awards: [] }
  350. portrait.value.awards.forEach(item => {
  351. item.color = getRandomHexColor()
  352. })
  353. permutationRadarDataHandler(res.data.dimensions)
  354. })
  355. const tagPromise = countTagScore(queryData).then(res => {
  356. const data = res.data
  357. if (Array.isArray(data)) {
  358. const found = data.find(item => (item.userId == userId))
  359. if (found) {
  360. tagScoreData.value = found.totalScore ?? found.score
  361. } else {
  362. tagScoreData.value = null
  363. }
  364. } else {
  365. tagScoreData.value = data
  366. }
  367. })
  368. const qualificationPromise = getQualificationLevelTime({ userId }).then(res => {
  369. qualificationData.value = res.data || null
  370. }).catch(() => {
  371. qualificationData.value = null
  372. })
  373. return Promise.all([portraitPromise, tagPromise, qualificationPromise]).finally(() => {
  374. loading.value = false
  375. })
  376. }
  377. const searchHandler = (query) => {
  378. visible.value = false
  379. currentQuery.value = query
  380. invokerEmployeePortrait(query)
  381. }
  382. const goToWarningPage = () => {
  383. if (currentQuery.value) {
  384. router.push({
  385. path: '/score/event',
  386. query: { id: currentQuery.value.id, startDate: currentQuery.value.beginTime, endDate: currentQuery.value.endTime },
  387. })
  388. } else {
  389. router.push('/score/event')
  390. }
  391. }
  392. const radarData = reactive({
  393. grounp: [],
  394. data: []
  395. })
  396. const permutationRadarDataHandler = (result = []) => {
  397. radarData.grounp = result.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
  398. radarData.data = [{
  399. name: '个人能力',
  400. value: result.map(attr => attr.score),
  401. symbolSize: 10,
  402. areaStyle: {
  403. show: false,
  404. opacity: 0,
  405. // color: {
  406. // type: 'linear',
  407. // x: 0,
  408. // y: 0,
  409. // x2: 0,
  410. // y2: 1,
  411. // colorStops: [{
  412. // offset: 0, color: '#4DC8FE'
  413. // }, {
  414. // offset: 1, color: '#6C26F3'
  415. // }],
  416. // global: false
  417. // },
  418. },
  419. lineStyle: {
  420. color: '#4DC8FE',
  421. width: 2
  422. },
  423. // lineStyle: {
  424. // color: {
  425. // type: 'linear',
  426. // x: 0,
  427. // y: 0,
  428. // x2: 0,
  429. // y2: 1,
  430. // colorStops: [{
  431. // offset: 0, color: '#4DC8FE'
  432. // }, {
  433. // offset: 1, color: '#6C26F3'
  434. // }],
  435. // global: false
  436. // },
  437. // width: 5
  438. // },
  439. itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#00C8DA', borderJoin: 'round' }
  440. }]
  441. }
  442. const setRadarOptions = computed(() => {
  443. return {
  444. radar: {
  445. indicator: radarData.grounp,
  446. center: ['50%', '52%'],
  447. radius: '65%',
  448. splitNumber: 8,
  449. axisLine: { lineStyle: { color: '#ccc' } },
  450. splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 3 } },
  451. splitArea: { show: false },
  452. axisName: {
  453. color: '#fff',
  454. fontSize: 18
  455. },
  456. },
  457. series: [
  458. {
  459. type: 'radar',
  460. data: radarData.data,
  461. symbol: 'circle',
  462. symbolSize: 13,
  463. label: {
  464. show: false,
  465. formatter: (p) => p.value,
  466. color: '#fff',
  467. fontSize: 16,
  468. fontWeight: 'bold'
  469. }
  470. }
  471. ]
  472. }
  473. })
  474. let pisition = {
  475. x: 0,
  476. y: 0
  477. }
  478. const tipRef = ref(null)
  479. radarChartInstance = useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
  480. if (!Array.isArray(portrait.value.dimensions) || !portrait.value.dimensions.length) return
  481. const rect = abilityChart.value.getBoundingClientRect()
  482. if (!tipRef.value) return
  483. const cx = rect.width / 2
  484. const cy = rect.height / 2
  485. const dx = e.offsetX - cx
  486. const dy = e.offsetY - cy
  487. const distance = Math.sqrt(dx * dx + dy * dy)
  488. const radarRadius = rect.width * 0.35 // 对应 radius: 65%
  489. if (distance > radarRadius) {
  490. tipRef.value.style.display = 'none'
  491. return
  492. }
  493. // 角度计算
  494. let angle = Math.atan2(dx, -dy) * (180 / Math.PI)
  495. if (angle < 0) angle += 360
  496. const count = portrait.value.dimensions.length
  497. const step = 360 / count
  498. let idx = Math.abs(Math.round(angle / step) % count - count)
  499. if (idx < 0) idx += count
  500. if (idx >= count) idx = 0
  501. // 显示 tooltip
  502. tipRef.value.style.display = 'block'
  503. tipRef.value.style.left = pisition.x + 30 + 'px'
  504. tipRef.value.style.top = pisition.y + 'px'
  505. activeDimName.value = portrait.value.dimensions[idx].name
  506. }).bindEvent('mouseout', () => {
  507. if (tipRef.value) tipRef.value.style.display = 'none'
  508. })
  509. const handleMouseMove = (eve) => {
  510. pisition.x = eve.pageX
  511. pisition.y = eve.pageY
  512. }
  513. onMounted(() => {
  514. window.addEventListener('mousemove', handleMouseMove)
  515. })
  516. onUnmounted(() => {
  517. window.removeEventListener('mousemove', handleMouseMove)
  518. })
  519. </script>
  520. <style lang="scss" scoped>
  521. .content {
  522. height: calc(100vh - 90px);
  523. display: flex;
  524. flex-direction: column;
  525. row-gap: 15px;
  526. padding: 15px;
  527. box-sizing: border-box;
  528. overflow: hidden;
  529. .ep-empty {
  530. background: #fff;
  531. height: 100%;
  532. display: flex;
  533. align-items: center;
  534. justify-content: center;
  535. background: #21213a;
  536. opacity: 0.8;
  537. border-radius: 30px;
  538. }
  539. .content-search {}
  540. .content-info {
  541. &>* {
  542. height: 100%;
  543. }
  544. .userInfo {
  545. display: flex;
  546. width: 100%;
  547. height: 180px;
  548. box-sizing: border-box;
  549. column-gap: 5px;
  550. .userInfo-name {
  551. flex: 1;
  552. position: relative;
  553. display: flex;
  554. align-items: center;
  555. justify-content: center;
  556. column-gap: 30px;
  557. &::after {
  558. content: '';
  559. display: block;
  560. width: 1px;
  561. background: linear-gradient(180deg, transparent, #6f6c98, transparent);
  562. height: 100%;
  563. position: absolute;
  564. top: 0;
  565. right: -2px;
  566. }
  567. .avatar-area {
  568. height: 140px;
  569. width: 140px;
  570. background: #fff;
  571. border-radius: 50%;
  572. display: flex;
  573. align-items: center;
  574. justify-content: center;
  575. .avatar {
  576. width: 100%;
  577. height: 100%;
  578. display: flex;
  579. align-items: center;
  580. justify-content: center;
  581. color: #6f6c98;
  582. font-size: 48px;
  583. font-weight: 900;
  584. }
  585. }
  586. .basic-name {
  587. display: flex;
  588. flex-direction: column;
  589. justify-content: center;
  590. row-gap: 10px;
  591. font-weight: 500;
  592. .basic-label {
  593. font-size: 18px;
  594. color: #8675AE;
  595. }
  596. .basic-value {
  597. font-size: 26px;
  598. color: #fff;
  599. }
  600. }
  601. }
  602. .userInfo-info {
  603. flex: 1;
  604. position: relative;
  605. display: flex;
  606. flex-direction: column;
  607. justify-content: space-between;
  608. padding: 10px 15px;
  609. padding-right: 0px;
  610. box-sizing: border-box;
  611. &::after {
  612. content: '';
  613. display: block;
  614. width: 1px;
  615. background: linear-gradient(180deg, transparent, #6f6c98, transparent);
  616. height: 100%;
  617. position: absolute;
  618. top: 0;
  619. right: -2px;
  620. }
  621. .info-item {
  622. display: flex;
  623. column-gap: 15px;
  624. .info-item-content {
  625. display: flex;
  626. flex-direction: column;
  627. justify-content: space-between;
  628. font-weight: 500;
  629. .info-item-label {
  630. font-size: 18px;
  631. color: #8675AE;
  632. }
  633. .info-item-value {
  634. font-size: 18px;
  635. color: #fff;
  636. display: flex;
  637. align-items: center;
  638. flex-wrap: wrap;
  639. gap: 2px;
  640. }
  641. .info-item-tag {
  642. font-size: 14px;
  643. display: block;
  644. padding: 3px 15px;
  645. background: linear-gradient(125deg, #2AB3E6, #9605FC);
  646. border-radius: 20px;
  647. }
  648. }
  649. }
  650. }
  651. .userInfo-score {
  652. :deep(.el-progress-circle) {
  653. width: 160px;
  654. height: 160px;
  655. --el-fill-color-light: #004970;
  656. transform: rotate(0.5turn);
  657. }
  658. flex: 1;
  659. display: flex;
  660. align-items: center;
  661. padding-left: 15px;
  662. column-gap: 25px;
  663. .score-progress {
  664. .percentage-content {
  665. display: flex;
  666. flex-direction: column;
  667. align-items: center;
  668. row-gap: 6px;
  669. font-weight: 500;
  670. .percentage-value {
  671. font-size: 48px;
  672. color: #02E5CC;
  673. }
  674. .percentage-text {
  675. font-size: 16px;
  676. color: #fff;
  677. }
  678. }
  679. }
  680. .score-box {
  681. display: flex;
  682. flex-direction: column;
  683. font-weight: 500;
  684. row-gap: 15px;
  685. font-size: 18px;
  686. .score-col {
  687. display: inline-block;
  688. width: 5em;
  689. }
  690. .score-col-2 {
  691. font-size: 24px;
  692. }
  693. }
  694. }
  695. }
  696. }
  697. .content-bottom {
  698. flex: 1;
  699. display: flex;
  700. column-gap: 15px;
  701. overflow: hidden;
  702. .content-bottom-left {
  703. width: 380px;
  704. display: flex;
  705. flex-direction: column;
  706. row-gap: 15px;
  707. &>*:nth-child(1) {
  708. height: 160px;
  709. .work-history {
  710. font-size: 18px;
  711. }
  712. }
  713. &>*:nth-child(2) {
  714. flex: 1.6;
  715. .honor-item {
  716. height: 48px;
  717. padding-right: 15px;
  718. box-sizing: border-box;
  719. font-weight: 500;
  720. font-size: 18px;
  721. display: flex;
  722. align-items: center;
  723. justify-content: space-between;
  724. position: relative;
  725. &>div:nth-child(1) {
  726. display: flex;
  727. align-items: center;
  728. column-gap: 15px;
  729. margin-left: 10px;
  730. &>div {
  731. width: 26px;
  732. height: 20px;
  733. position: relative;
  734. font-size: 18px;
  735. text-align: center;
  736. line-height: 20px;
  737. color: #222;
  738. &::before {
  739. position: absolute;
  740. inset: 0;
  741. content: '';
  742. display: block;
  743. background: var(--indexBgColor);
  744. width: 100%;
  745. height: 100%;
  746. transform: skew(-25deg);
  747. }
  748. &::after {
  749. position: absolute;
  750. inset: 0;
  751. content: attr(data-index);
  752. display: block;
  753. width: 100%;
  754. height: 100%;
  755. z-index: 100;
  756. }
  757. }
  758. }
  759. &>div:nth-child(2) {
  760. color: #02E5CC;
  761. }
  762. &::after {
  763. content: '';
  764. display: block;
  765. position: absolute;
  766. bottom: 0;
  767. left: 0;
  768. height: 1px;
  769. width: 100%;
  770. background: linear-gradient(45deg, transparent 0%, transparent 30%, #6f6c98 100%);
  771. }
  772. }
  773. }
  774. }
  775. .content-bottom-center {
  776. flex: 1;
  777. &>* {
  778. height: 100%;
  779. }
  780. .chart-legend {
  781. display: flex;
  782. justify-content: center;
  783. align-items: center;
  784. flex-wrap: wrap;
  785. gap: 10px 18px;
  786. padding-top: 8px;
  787. color: #fff;
  788. font-size: 14px;
  789. }
  790. .legend-item {
  791. display: flex;
  792. align-items: center;
  793. gap: 6px;
  794. span {
  795. width: 24px;
  796. height: 10px;
  797. border: 2px solid currentColor;
  798. border-radius: 2px;
  799. }
  800. }
  801. .legend-warning {
  802. color: #fe4322;
  803. span {
  804. background-color: #fe4322;
  805. }
  806. }
  807. .legend-normal {
  808. color: #fff;
  809. span {
  810. background-color: #fff;
  811. }
  812. }
  813. .legend-excellent {
  814. color: #8EC742;
  815. span {
  816. background-color: #8EC742;
  817. }
  818. }
  819. .legend-current {
  820. color: #1890ff;
  821. span {
  822. background-color: #1890ff;
  823. }
  824. }
  825. .chart-box {
  826. width: 100%;
  827. height: calc(100% - 34px);
  828. overflow: hidden;
  829. }
  830. }
  831. .content-bottom-right {
  832. width: 380px;
  833. &>* {
  834. height: 100%;
  835. }
  836. .supp-item {
  837. display: flex;
  838. justify-content: space-between;
  839. align-items: center;
  840. height: 42px;
  841. padding: 0 15px;
  842. font-weight: 500;
  843. font-size: 18px;
  844. .s-lbl {
  845. color: #8675AE;
  846. }
  847. .s-val {
  848. color: #fff;
  849. }
  850. }
  851. .supp-item-2 {
  852. background: #261C48;
  853. }
  854. }
  855. .card-content {
  856. height: 100%;
  857. overflow-x: hidden;
  858. overflow-y: auto;
  859. &::-webkit-scrollbar-track {
  860. background: transparent;
  861. }
  862. &::-webkit-scrollbar-thumb {
  863. background: linear-gradient(225deg, #4355cb, #873dc3);
  864. border-radius: 4px;
  865. }
  866. }
  867. }
  868. }
  869. /* Tooltip 样式(全部放这里) */
  870. .radar-tooltip {
  871. position: fixed;
  872. min-width: 300px;
  873. z-index: 999;
  874. pointer-events: none;
  875. transform: translateY(-80%);
  876. display: none;
  877. .p-empty {
  878. text-align: center;
  879. margin-top: 5px;
  880. }
  881. .tooltip-title {
  882. font-size: 18px;
  883. font-weight: bold;
  884. color: #fff;
  885. padding: 0 10px;
  886. }
  887. .tooltip-section {
  888. padding: 0 10px;
  889. }
  890. .list-item {
  891. margin-top: 4px;
  892. display: flex;
  893. align-items: center;
  894. justify-content: space-between;
  895. color: #fff;
  896. .deduct-text {
  897. color: #ff4d4f;
  898. }
  899. .add-text {
  900. color: #00b42a;
  901. }
  902. }
  903. .section-total {
  904. margin-top: 4px;
  905. display: flex;
  906. align-items: center;
  907. justify-content: space-between;
  908. font-weight: 500;
  909. font-size: 16px;
  910. &.deduct {
  911. color: #ff4d4f;
  912. }
  913. &.add {
  914. color: #00b42a;
  915. }
  916. }
  917. }
  918. </style>