index.vue 29 KB

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