index.vue 37 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274
  1. <template>
  2. <div class="ep-root">
  3. <!-- 顶部工具栏 -->
  4. <div class="ep-topbar">
  5. <div class="time-btns">
  6. <el-button size="small" :type="currentTime==='week'?'primary':'default'" @click="selectTime('week')">近一周</el-button>
  7. <el-button size="small" :type="currentTime==='month'?'primary':'default'" @click="selectTime('month')">近一月</el-button>
  8. <el-button size="small" :type="currentTime==='quarter'?'primary':'default'" @click="selectTime('quarter')">近三月</el-button>
  9. <el-button size="small" :type="currentTime==='year'?'primary':'default'" @click="selectTime('year')">近一年</el-button>
  10. <el-button size="small" :type="currentTime==='custom'?'primary':'default'" @click="selectTime('custom')">自定义时间范围</el-button>
  11. <el-date-picker
  12. v-if="currentTime==='custom'"
  13. v-model="dateRange"
  14. type="daterange"
  15. range-separator="至"
  16. start-placeholder="开始"
  17. end-placeholder="结束"
  18. size="small"
  19. style="margin-left:8px;width:240px"
  20. @change="loadPortrait"
  21. />
  22. </div>
  23. <el-autocomplete
  24. v-model="personName"
  25. :fetch-suggestions="queryUsers"
  26. placeholder="搜索员工/团队画像"
  27. size="small"
  28. style="width:220px"
  29. @select="handlePersonSelect"
  30. @clear="portrait=null"
  31. clearable
  32. >
  33. <template #suffix><el-icon><Search /></el-icon></template>
  34. <template #default="{ item }">
  35. <span>{{ item.nickName }}</span>
  36. <span v-if="item.deptName" style="font-size:12px;color:#999;margin-left:8px">{{ item.deptName }}</span>
  37. </template>
  38. </el-autocomplete>
  39. </div>
  40. <!-- 空状态 -->
  41. <div v-if="!portrait" class="ep-empty">
  42. <el-empty description="请在右上角搜索员工姓名以查看画像" />
  43. </div>
  44. <!-- 主内容 -->
  45. <div v-else v-loading="loading" class="ep-body" ref="content">
  46. <!-- 左侧主区 -->
  47. <div class="ep-left">
  48. <!-- 第一行:基本信息卡 + 综合评价 -->
  49. <div class="row-flex gap">
  50. <!-- 基本信息 -->
  51. <div class="card basic-card">
  52. <div class="avatar-area">
  53. <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
  54. <div v-else class="avatar-fallback">{{ portrait.personName?.charAt(0) }}</div>
  55. </div>
  56. <div class="basic-detail">
  57. <div class="basic-detail-row">
  58. <div class="basic-detail-col basic-name">
  59. <div class="basic-label">姓名:</div>
  60. <div class="basic-value">{{ portrait.personName }}</div>
  61. </div>
  62. </div>
  63. <div class="basic-detail-row">
  64. <div class="basic-detail-col">
  65. <div class="basic-label">所属部门及队室:</div>
  66. <div class="basic-value">{{ portrait.deptPath || '-' }}</div>
  67. </div>
  68. <div class="basic-detail-col">
  69. <div class="basic-label">出生日期:</div>
  70. <div class="basic-value">{{ portrait.birthday || '-' }}</div>
  71. </div>
  72. </div>
  73. <div class="basic-detail-row">
  74. <div class="basic-detail-col">
  75. <div class="basic-label">技能等级:</div>
  76. <div class="basic-value">{{ portrait.qualificationLevelText || '-' }}</div>
  77. </div>
  78. </div>
  79. <div class="basic-detail-row">
  80. <div class="basic-detail-col">
  81. <div class="basic-label">学历:</div>
  82. <div class="basic-value">{{ portrait.schooling || '-' }}</div>
  83. </div>
  84. <div class="basic-detail-col">
  85. <div class="basic-label">专业:</div>
  86. <div class="basic-value">{{ portrait.major || '-' }}</div>
  87. </div>
  88. </div>
  89. <div class="basic-detail-row">
  90. <div class="basic-detail-col">
  91. <div class="basic-label">标签:</div>
  92. <el-tag size="small" type="primary" style="margin-right:6px" v-if="portrait.roleNames">{{ portrait.roleNames }}</el-tag>
  93. <el-tag size="small" type="warning" v-if="portrait.postNames">{{ portrait.postNames }}</el-tag>
  94. </div>
  95. </div>
  96. <!--
  97. <div class="basic-row2" v-if="portrait.startWorkingDate">
  98. <span><span class="lbl">入职日期:</span><span class="val">{{ portrait.startWorkingDate }}</span></span>
  99. <span v-if="portrait.phonenumber"><span class="lbl">电话:</span><span class="val">{{ portrait.phonenumber }}</span></span>
  100. </div>
  101. -->
  102. </div>
  103. </div>
  104. <!-- 综合评价 -->
  105. <div class="card score-card">
  106. <div class="score-label">综合评价</div>
  107. <div class="score-num">{{ portrait.totalScore }}</div>
  108. <!-- <div class="score-level-tag">{{ scoreLevel }}</div> -->
  109. </div>
  110. </div>
  111. <!-- 第二行:工作履历+获奖 / 个人能力雷达 -->
  112. <div class="row-flex gap" style="flex:1; overflow: hidden;">
  113. <!-- 工作履历 & 获奖记录 -->
  114. <div class="card left-mid-card">
  115. <div class="sec-title">工作履历</div>
  116. <div class="history-list">
  117. <div class="history-item">
  118. <span v-if="portrait.startWorkingDate">
  119. {{ formatWorkDate(portrait.startWorkingDate) }}入职
  120. | 司龄{{ portrait.workYears != null ? portrait.workYears : '-' }}年
  121. | 开机年限{{ portrait.securityCheckYears != null ? portrait.securityCheckYears : '-' }}年
  122. | 现任职{{ portrait.roleNames || '-' }}
  123. </span>
  124. <span v-else>暂无数据</span>
  125. </div>
  126. </div>
  127. <div class="sec-title">获奖记录</div>
  128. <div class="award-table-wrap">
  129. <div class="award-table" v-if="portrait.awards && portrait.awards.length">
  130. <div class="award-table-row level1" v-for="(aw, i) in portrait.awards" :key="i">
  131. <div class="award-table-col">{{ aw.level3Name || '-' }}</div>
  132. <div class="award-table-col">{{ aw.level2Name || '-' }}</div>
  133. <div class="award-table-col">{{ aw.level4Name || '-' }}</div>
  134. <div class="award-table-col">{{ aw.score }}</div>
  135. </div>
  136. </div>
  137. <div v-else class="empty-text mt8">暂无获奖记录</div>
  138. </div>
  139. </div>
  140. <!-- 个人能力雷达 -->
  141. <div class="card radar-card">
  142. <div class="sec-title">个人能力</div>
  143. <div class="radar-body">
  144. <!-- 扣分明细浮窗(点击维度后显示,绝对定位左侧) -->
  145. <!-- <transition name="panel-fade">
  146. <div class="score-panel deduct-panel" v-if="activeDimName">
  147. <div class="panel-header deduct-header">
  148. 扣分明细<span class="panel-dim-name">{{ activeDimName }}</span>
  149. <span class="panel-total">合计 {{ deductTotal }}</span>
  150. </div>
  151. <div class="panel-list">
  152. <div v-for="(item, i) in deductList" :key="i" class="panel-row">
  153. <span class="p-name">{{ item.level3Name || item.level2Name }}</span>
  154. <span class="p-score deduct-score">{{ item.totalScore }}</span>
  155. </div>
  156. <div v-if="!deductList.length" class="p-empty">暂无扣分记录</div>
  157. </div>
  158. </div>
  159. </transition> -->
  160. <!-- 雷达图 -->
  161. <div ref="abilityChart" class="chart-box">
  162. <!-- <div style="position: absolute;" v-if="activeDimName">
  163. <div style="padding: 12px; font-size: 14px; line-height: 1.6;">
  164. <div style="font-weight: bold; margin-bottom: 8px; font-size: 16px;">
  165. {{ activeDimName }}
  166. </div>
  167. <div style="margin-bottom: 10px;">
  168. <div style="color: #f56c6c; font-weight: bold;">扣分详情 (PENALTIES)</div>
  169. <ul style="margin: 6px 0; padding-left: 20px; color: #666;">
  170. <li>不安全事件(一类): -10.0分</li>
  171. <li>不安全事件(二类): -8.0分</li>
  172. <li>三/四/五类事件: -5/-3/-2分</li>
  173. </ul>
  174. <div style="text-align: right; color: #f56c6c; font-weight: bold;">合计扣分: 30.3分</div>
  175. </div>
  176. <div>
  177. <div style="color: #67c23a; font-weight: bold;">加分详情 (BONUSES)</div>
  178. <ul style="margin: 6px 0; padding-left: 20px; color: #666;">
  179. <li>一类查获/隐患上报: +1.0分</li>
  180. <li>建议采纳/二类查获: +1.0/+0.5分</li>
  181. <li>特殊物品查获: +0.15分</li>
  182. </ul>
  183. <div style="text-align: right; color: #67c23a; font-weight: bold;">合计加分: 4.3分</div>
  184. </div>
  185. </div>
  186. </div> -->
  187. </div>
  188. <!-- 加分明细浮窗(点击维度后显示,绝对定位右侧) -->
  189. <!-- <transition name="panel-fade">
  190. <div class="score-panel add-panel" v-if="activeDimName">
  191. <div class="panel-header add-header">
  192. 加分明细<span class="panel-dim-name">{{ activeDimName }}</span>
  193. <span class="panel-total">合计 {{ addTotal }}</span>
  194. </div>
  195. <div class="panel-list">
  196. <div v-for="(item, i) in addList" :key="i" class="panel-row">
  197. <span class="p-name">{{ item.level3Name || item.level2Name }}</span>
  198. <span class="p-score add-score">+{{ item.totalScore }}</span>
  199. </div>
  200. <div v-if="!addList.length" class="p-empty">暂无加分记录</div>
  201. </div>
  202. </div>
  203. </transition> -->
  204. </div>
  205. <div class="radar-hint" v-if="!activeDimName">
  206. {{ activeDimName }}
  207. <!-- 点击雷达图维度查看明细 -->
  208. </div>
  209. </div>
  210. </div>
  211. </div>
  212. <!-- 右侧边栏:一个大卡片 -->
  213. <div class="ep-right">
  214. <div class="side-big-card">
  215. <!-- 补充信息 -->
  216. <div class="side-section">
  217. <div class="side-title">补充信息</div>
  218. <div class="supp-grid supp-info-bg">
  219. <div class="supp-item">
  220. <span class="s-lbl">政治面貌</span>
  221. <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
  222. </div>
  223. <div class="supp-item">
  224. <span class="s-lbl">性别</span>
  225. <span class="s-val">{{ portrait.sexText || '-' }}</span>
  226. </div>
  227. <div class="supp-item">
  228. <span class="s-lbl">籍贯</span>
  229. <span class="s-val">{{ portrait.sexText || '-' }}</span>
  230. </div>
  231. <div class="supp-item">
  232. <span class="s-lbl">民族</span>
  233. <span class="s-val">{{ portrait.sexText || '-' }}</span>
  234. </div>
  235. <div class="supp-item">
  236. <span class="s-lbl">年龄</span>
  237. <span class="s-val">{{ portrait.sexText || '-' }}</span>
  238. </div>
  239. <div class="supp-item">
  240. <span class="s-lbl">司龄</span>
  241. <span class="s-val">{{ portrait.workYears != null ? portrait.workYears+'年' : '-' }}</span>
  242. </div>
  243. <!-- <div class="supp-item">
  244. <span class="s-lbl">开机年限</span>
  245. <span class="s-val">{{ portrait.securityCheckYears != null ? portrait.securityCheckYears+'年' : '-' }}</span>
  246. </div> -->
  247. <div class="supp-item">
  248. <span class="s-lbl">性格特征</span>
  249. <span class="s-val">{{ portrait.characterCharacteristics || '-' }}</span>
  250. </div>
  251. <div class="supp-item">
  252. <span class="s-lbl">工作风格</span>
  253. <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
  254. </div>
  255. </div>
  256. </div>
  257. <!-- 业务岗位 -->
  258. <div class="side-section pos-section">
  259. <div class="side-title">业务岗位</div>
  260. <div class="supp-grid pos-info-bg">
  261. <div class="supp-item" v-for="(p, i) in positionList" :key="i">{{ p }}</div>
  262. <div v-if="!positionList.length" class="empty-text">暂无岗位信息</div>
  263. </div>
  264. </div>
  265. <!-- 培训情况 -->
  266. <div class="side-section">
  267. <div class="side-title">培训情况</div>
  268. <div class="supp-grid train-info-bg">
  269. <div class="train-info-table" v-if="portrait.theoryScore != null || portrait.imageScore != null">
  270. <div class="train-row train-header">
  271. <span>季度</span><span>理论成绩</span><span>图像成绩</span>
  272. </div>
  273. <div class="train-row">
  274. <span>{{ portrait.examPeriod || '-' }}</span>
  275. <span :style="examScoreStyle(portrait.theoryScore)">
  276. {{ portrait.theoryScore != null ? portrait.theoryScore+'分' : '-' }}
  277. </span>
  278. <span :style="examScoreStyle(portrait.imageScore)">
  279. {{ portrait.imageScore != null ? portrait.imageScore+'分' : '-' }}
  280. </span>
  281. </div>
  282. </div>
  283. <div v-else class="empty-text">暂无考试记录</div>
  284. </div>
  285. </div>
  286. <!-- 质控情况 -->
  287. <div class="side-section">
  288. <div class="side-title">质控情况</div>
  289. <div class="supp-grid qc-info-bg">
  290. <div class="supp-item">
  291. <span class="s-lbl">查获违规品次数</span>
  292. <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
  293. </div>
  294. <div class="supp-item">
  295. <span class="s-lbl">监察问题记录</span>
  296. <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
  297. </div>
  298. <div class="supp-item">
  299. <span class="s-lbl">三级质控巡查记录</span>
  300. <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
  301. </div>
  302. <div class="supp-item">
  303. <span class="s-lbl">实时质控拦截记录</span>
  304. <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
  305. </div>
  306. </div>
  307. </div>
  308. </div>
  309. </div>
  310. </div>
  311. <!-- 自定义 Tooltip(放在 template 里) -->
  312. <div
  313. class="radar-tooltip"
  314. ref="tipRef"
  315. >
  316. <!-- 标题 -->
  317. <div class="tooltip-title">{{ activeDimName }}</div>
  318. <!-- 扣分区域 -->
  319. <div class="tooltip-section">
  320. <div class="section-title deduct">扣分明细</div>
  321. <div class="section-list">
  322. <div v-if="deductList.length" class="list-item" v-for="(item, i) in deductList" :key="i">
  323. <span>{{ item.level3Name }}:</span>
  324. <span class="deduct-text">{{ item.totalScore }}分</span>
  325. </div>
  326. <div v-if="!deductList.length" class="p-empty">暂无扣分记录</div>
  327. </div>
  328. <div class="section-total deduct">
  329. <span>合计扣分:</span>
  330. <span>{{ deductTotal }}分</span>
  331. </div>
  332. </div>
  333. <!-- 加分区域 -->
  334. <div class="tooltip-section">
  335. <div class="section-title add">加分明细</div>
  336. <div class="section-list">
  337. <div v-if="addList.length" class="list-item" v-for="(item, i) in addList" :key="i">
  338. <span>{{ item.level3Name }}:</span>
  339. <span class="add-text">{{ item.totalScore }}分</span>
  340. </div>
  341. <div v-else class="p-empty">暂无加分记录</div>
  342. </div>
  343. <div class="section-total add">
  344. <span>合计加分:</span>
  345. <span>{{ addTotal }}分</span>
  346. </div>
  347. </div>
  348. </div>
  349. </div>
  350. </template>
  351. <script setup>
  352. import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
  353. import * as echarts from 'echarts'
  354. import { Search } from '@element-plus/icons-vue'
  355. import { useRoute } from 'vue-router'
  356. import { searchPortraitUsers, getEmployeePortrait } from '@/api/score/index'
  357. defineOptions({ name: 'EmployeeProfile' })
  358. const route = useRoute()
  359. const currentTime = ref('year')
  360. const dateRange = ref([])
  361. const personName = ref('')
  362. const portrait = ref(null)
  363. const loading = ref(false)
  364. const abilityChart = ref(null)
  365. let chart = null
  366. const formatDate = (d) => {
  367. const dt = new Date(d)
  368. return `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`
  369. }
  370. const getTimeRange = () => {
  371. if (currentTime.value === 'custom') {
  372. if (dateRange.value?.length === 2) {
  373. return { beginTime: formatDate(dateRange.value[0]), endTime: formatDate(dateRange.value[1]) }
  374. }
  375. return {}
  376. }
  377. const end = new Date(), begin = new Date()
  378. if (currentTime.value === 'week') begin.setDate(end.getDate()-7)
  379. else if (currentTime.value === 'month') begin.setMonth(end.getMonth()-1)
  380. else if (currentTime.value === 'quarter') begin.setMonth(end.getMonth()-3)
  381. else begin.setFullYear(end.getFullYear()-1)
  382. return { beginTime: formatDate(begin), endTime: formatDate(end) }
  383. }
  384. const selectTime = (t) => {
  385. currentTime.value = t
  386. if (t !== 'custom') loadPortrait()
  387. }
  388. const queryUsers = async (query, cb) => {
  389. if (!query?.trim()) { cb([]); return }
  390. try {
  391. const res = await searchPortraitUsers(query.trim())
  392. cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
  393. } catch (_) { cb([]) }
  394. }
  395. const handlePersonSelect = (item) => {
  396. personName.value = item.nickName
  397. loadPortrait()
  398. }
  399. const loadPortrait = async () => {
  400. if (!personName.value) return
  401. loading.value = true
  402. try {
  403. const res = await getEmployeePortrait({ personName: personName.value, ...getTimeRange() })
  404. portrait.value = res.data || null
  405. // portrait.value.awards = Array(30).fill(portrait.value.awards[0])
  406. } catch (_) {
  407. portrait.value = null
  408. } finally {
  409. loading.value = false
  410. }
  411. }
  412. const scoreColor = computed(() => {
  413. const s = Number(portrait.value?.totalScore)
  414. if (s < 75) return 'color:#f56c6c'
  415. if (s > 85) return 'color:#67c23a'
  416. return 'color:#ff9900'
  417. })
  418. const scoreLevel = computed(() => {
  419. const s = Number(portrait.value?.totalScore)
  420. if (s < 75) return '较差'
  421. if (s > 85) return '优秀'
  422. return '良好'
  423. })
  424. const positionList = computed(() => {
  425. const pos = portrait.value?.securityInspectionPosition
  426. if (!pos) return []
  427. return pos.split(/[,,、/]/).map(s => s.trim()).filter(Boolean)
  428. })
  429. const activeDimName = ref(null)
  430. const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
  431. const addList = computed(() => {
  432. const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) > 0)
  433. return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
  434. })
  435. const deductList = computed(() => {
  436. const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) < 0)
  437. return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
  438. })
  439. const addTotal = computed(() => {
  440. const s = addList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
  441. return (s > 0 ? '+' : '') + s.toFixed(2)
  442. })
  443. const deductTotal = computed(() => {
  444. const s = deductList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
  445. return s.toFixed(2)
  446. })
  447. const formatWorkDate = (d) => {
  448. if (!d) return '-'
  449. const str = String(d)
  450. const parts = str.substring(0, 10).split('-')
  451. if (parts.length < 3) return str
  452. return `${parts[0]}.${Number(parts[1])}.${Number(parts[2])}`
  453. }
  454. const examScoreStyle = (score) => {
  455. const s = Number(score)
  456. if (s >= 90) return 'color:#67c23a;font-weight:600'
  457. if (s < 60) return 'color:#f56c6c;font-weight:600'
  458. return 'color:#409eff'
  459. }
  460. const radarColor = computed(() => {
  461. const total = Number(portrait.value?.totalScore ?? 0)
  462. if (total < 75) return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
  463. if (total >= 90) return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
  464. return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
  465. })
  466. const tipRef = ref(null)
  467. const initChart = () => {
  468. if (!abilityChart.value || !portrait.value) return
  469. if (chart) { chart.dispose(); chart = null }
  470. chart = echarts.init(abilityChart.value)
  471. const dims = portrait.value.dimensions || []
  472. console.log(dims);
  473. const c = radarColor.value
  474. chart.setOption({
  475. radar: {
  476. indicator: dims.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
  477. center: ['50%', '52%'],
  478. radius: '65%',
  479. splitNumber: 4,
  480. axisLine: { lineStyle: { color: '#8EC742' } },
  481. splitLine: { lineStyle: { color: '#8EC742' } },
  482. splitArea: { show: false },
  483. axisName: {
  484. color: '#000',
  485. fontSize: 16
  486. },
  487. },
  488. series: [{
  489. type: 'radar',
  490. data: [{
  491. value: dims.map(d => Number(d.score)),
  492. name: '个人能力',
  493. areaStyle: { color: c.fill },
  494. lineStyle: { color: c.line, width: 3 },
  495. itemStyle: { color: c.line }
  496. }],
  497. symbol: 'circle',
  498. symbolSize: 6,
  499. label: {
  500. show: false,
  501. formatter: (p) => p.value,
  502. color: c.label,
  503. fontSize: 12,
  504. fontWeight: 'bold'
  505. }
  506. }],
  507. })
  508. const tip = tipRef.value
  509. chart.getZr().on('mousemove', (e) => {
  510. const rect = abilityChart.value.getBoundingClientRect()
  511. if (!tip) return
  512. const cx = rect.width / 2
  513. const cy = rect.height / 2
  514. const dx = e.offsetX - cx
  515. const dy = e.offsetY - cy
  516. const distance = Math.sqrt(dx * dx + dy * dy)
  517. const radarRadius = rect.width * 0.325 // 对应 radius: 65%
  518. if (distance > radarRadius) {
  519. tip.style.display = 'none'
  520. return
  521. }
  522. // 角度计算
  523. let angle = Math.atan2(dx, -dy) * (180 / Math.PI)
  524. if (angle < 0) angle += 360
  525. const count = dims.length
  526. const step = 360 / count
  527. let idx = Math.round(angle / step) % count
  528. if (idx < 0) idx += count
  529. // 显示 tooltip
  530. tip.style.display = 'block'
  531. // tip.style.left = rect.x + e.offsetX + 30 + 'px'
  532. // tip.style.top = rect.y + e.offsetY + 'px'
  533. tip.style.left = pisition.x + 30 + 'px'
  534. tip.style.top = pisition.y + 'px'
  535. activeDimName.value = dims[idx].name
  536. })
  537. chart.getZr().on('mouseout', () => {
  538. if (tip) tip.style.display = 'none'
  539. })
  540. }
  541. watch(portrait, (val) => {
  542. activeDimName.value = null
  543. if (val) nextTick(() => initChart())
  544. })
  545. const scaleValue = ref(1)
  546. const handleResize = () => {
  547. function setCSSZoom(scale) {
  548. scaleValue.value = scale
  549. const height = ((content.value.offsetHeight - (110 * scale)) / scale)
  550. content.value.style.setProperty('height', height + 'px')
  551. content.value.style.zoom = scale;
  552. }
  553. if (window.innerWidth < 1280) {
  554. setCSSZoom(0.5)
  555. }
  556. if (window.innerWidth > 1280 && window.innerWidth <= 1440) {
  557. setCSSZoom(0.65)
  558. }
  559. if (window.innerWidth > 1440 && window.innerWidth <= 1680) {
  560. setCSSZoom(0.7)
  561. }
  562. if (window.innerWidth > 1680 && window.innerWidth < 1920) {
  563. setCSSZoom(0.9)
  564. }
  565. if (window.innerWidth >= 1920) {
  566. setCSSZoom(1)
  567. }
  568. chart?.resize()
  569. }
  570. const content = ref(null)
  571. let pisition = {
  572. x: 0,
  573. y: 0
  574. }
  575. onMounted(() => {
  576. window.addEventListener('resize', handleResize)
  577. const n = route.query.personName
  578. if (n) { personName.value = n; loadPortrait() }
  579. window.addEventListener('mousemove', (eve) => {
  580. pisition.x = eve.pageX
  581. pisition.y = eve.pageY
  582. })
  583. })
  584. onBeforeUnmount(() => {
  585. window.removeEventListener('resize', handleResize)
  586. chart?.dispose(); chart = null
  587. })
  588. </script>
  589. <style scoped lang="scss">
  590. .ep-root {
  591. height: 100vh;
  592. background: linear-gradient(180deg, #C5DDFF 0%, #E4FBF0 100%);
  593. display: flex;
  594. flex-direction: column;
  595. box-sizing: border-box;
  596. gap: 12px;
  597. color: rgba(16, 16, 16, 1);
  598. line-height: 28px;
  599. --cardBgColor: rgba(255, 255, 255, 0.7)
  600. }
  601. /* ── 顶部工具栏 ── */
  602. .ep-topbar {
  603. display: flex;
  604. align-items: center;
  605. justify-content: space-between;
  606. background: rgba(255, 255, 255, 1);
  607. // border-radius: 20px;
  608. padding: 10px 16px;
  609. box-shadow: 0px 5px 10px 2px rgba(0, 0, 0, 0.35);
  610. flex-shrink: 0;
  611. height: 70px;
  612. gap: 12px;
  613. flex-wrap: wrap;
  614. .time-btns {
  615. display: flex;
  616. align-items: center;
  617. gap: 6px;
  618. flex-wrap: wrap;
  619. }
  620. }
  621. /* ── 空状态 ── */
  622. .ep-empty {
  623. background: #fff;
  624. border-radius: 8px;
  625. padding: 60px;
  626. text-align: center;
  627. }
  628. /* ── 主体两栏 ── */
  629. .ep-body {
  630. display: flex;
  631. gap: 20px;
  632. flex: 1;
  633. min-height: 0;
  634. padding: 15px;
  635. padding-top: 0px;
  636. }
  637. .ep-left {
  638. flex: 1;
  639. min-width: 0;
  640. display: flex;
  641. flex-direction: column;
  642. gap: 20px;
  643. }
  644. .ep-right {
  645. width: 366px;
  646. flex-shrink: 0;
  647. display: flex;
  648. flex-direction: column;
  649. gap: 20px;
  650. font-size: 20px;
  651. color: rgba(16, 16, 16, 1);
  652. line-height: 28px;
  653. }
  654. /* ── 通用卡片 ── */
  655. .card {
  656. background: var(--cardBgColor);
  657. border-radius: 20px;
  658. box-sizing: border-box;
  659. .sec-title {
  660. font-size: 48px;
  661. font-weight: bold;
  662. line-height: 68px;
  663. color: rgba(31, 135, 232, 1);
  664. text-align: left;
  665. font-style: italic;
  666. margin-bottom: 8px;
  667. text-indent: 25px;
  668. display: flex;
  669. align-items: center;
  670. &::before {
  671. content: '';
  672. display: block;
  673. width: 20px;
  674. height: 20px;
  675. border-radius: 50%;
  676. transform: translateX(15px);
  677. background: rgba(31, 135, 232, 1);
  678. }
  679. }
  680. }
  681. .row-flex {
  682. display: flex;
  683. }
  684. .gap { gap: 20px; }
  685. /* ── 基本信息卡 ── */
  686. .basic-card {
  687. flex: 3;
  688. min-width: 0;
  689. display: flex;
  690. gap: 24px;
  691. align-items: flex-start;
  692. background: #eef7ff;
  693. padding: 20px 14px;
  694. .avatar-area {
  695. width: 260px;
  696. height: 260px;
  697. border-radius: 130px;
  698. background-color: #1677FF;
  699. display: flex;
  700. justify-content: center;
  701. align-items: center;
  702. .avatar {
  703. width: 250px;
  704. height: 250px;
  705. border-radius: 125px;
  706. object-fit: cover;
  707. }
  708. .avatar-fallback {
  709. width: 250px;
  710. height: 250px;
  711. border-radius: 125px;
  712. background-color: rgba(229, 229, 229, 1);
  713. display: flex;
  714. align-items: center;
  715. justify-content: center;
  716. font-size: 80px;
  717. font-weight: bold;
  718. color: #aaa;
  719. }
  720. }
  721. .basic-detail {
  722. flex: 1;
  723. display: flex;
  724. flex-direction: column;
  725. justify-content: space-between;
  726. height: 100%;
  727. padding-top: 20px;
  728. box-sizing: border-box;
  729. .basic-detail-row {
  730. display: flex;
  731. color: rgba(16,16,16,1);
  732. font-size: 20px;
  733. text-align: left;
  734. font-weight: bold;
  735. width: 100%;
  736. .basic-detail-col {
  737. flex: 1;
  738. display: flex;
  739. align-items: center;
  740. line-height: 28px;
  741. .basic-label {
  742. }
  743. .basic-value {
  744. flex: 1;
  745. }
  746. }
  747. .basic-detail-col:nth-child(2) {
  748. max-width: 260px;
  749. }
  750. .basic-name {
  751. font-style: italic;
  752. line-height: 80px;
  753. .basic-label {
  754. font-size: 48px;
  755. width: 154px;
  756. color: rgba(31, 135, 232, 1);
  757. }
  758. .basic-value {
  759. font-size: 48px;
  760. color: rgba(31, 135, 232, 1);
  761. }
  762. }
  763. }
  764. }
  765. }
  766. /* ── 综合评价卡 ── */
  767. .score-card {
  768. width: 410px;
  769. height: 345px;
  770. display: flex;
  771. box-sizing: border-box;
  772. padding: 40px 30px 0px;
  773. flex-direction: column;
  774. align-items: center;
  775. background: var(--cardBgColor);
  776. font-size: 20px;
  777. text-align: center;
  778. .score-label {
  779. font-size: 48px;
  780. width: 100%;
  781. font-weight: bold;
  782. font-style: italic;
  783. line-height: 68px;
  784. color: rgba(31, 135, 232, 1);
  785. text-align: left;
  786. margin-bottom: 40px;
  787. }
  788. .score-num {
  789. width: 320px;
  790. height: 126px;
  791. border-radius: 30px;
  792. background-color: rgba(118, 220, 198, 1);
  793. color: #F58C78;
  794. font-size: 72px;
  795. font-weight: bold;
  796. font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  797. text-align: center;
  798. display: flex;
  799. align-items: center;
  800. justify-content: center;
  801. line-height: 1;
  802. }
  803. .score-level-tag {
  804. margin-top: 12px;
  805. font-size: 20px;
  806. font-weight: 600;
  807. color: rgba(16, 16, 16, 1);
  808. background: #f5f5f5;
  809. padding: 4px 20px;
  810. border-radius: 10px;
  811. }
  812. }
  813. /* ── 工作履历 + 获奖 ── */
  814. .left-mid-card {
  815. width: 604px;
  816. flex-shrink: 0;
  817. background: var(--cardBgColor);
  818. color: rgba(16, 16, 16, 1);
  819. font-size: 20px;
  820. line-height: 28px;
  821. box-sizing: border-box;
  822. padding: 30px 10px;
  823. text-align: center;
  824. height: 100%;
  825. overflow: hidden;
  826. display: flex;
  827. flex-direction: column;
  828. .mt16 { margin-top: 16px; }
  829. .mt8 { margin-top: 8px; }
  830. .history-list {
  831. background-color: rgba(236, 220, 220, 1);
  832. border-radius: 20px;
  833. padding: 20px;
  834. .history-item {
  835. font-size: 20px;
  836. font-weight: bold;
  837. color: #101010;
  838. line-height: 28px;
  839. text-align: left;
  840. padding: 10px 0;
  841. display: flex;
  842. align-items: center;
  843. }
  844. }
  845. .award-table-wrap {
  846. background-color: rgba(169,209,250,1);
  847. border-radius: 20px;
  848. padding: 20px;
  849. flex: 1;
  850. overflow: hidden;
  851. .award-table {
  852. height: 100%;
  853. overflow-y: auto;
  854. overflow-x: hidden;
  855. .award-table-row {
  856. display: flex;
  857. height: 48px;
  858. .award-table-col {
  859. flex: 1;
  860. text-align: center;
  861. line-height: 48px;
  862. }
  863. }
  864. .level1 {
  865. background-color: rgba(239,172,169,1);
  866. }
  867. .level2 {
  868. background-color: rgba(184,245,216,1);
  869. }
  870. .level3 {
  871. background-color: rgba(85,164,244,1);
  872. }
  873. }
  874. }
  875. }
  876. .award-section {
  877. margin-top: 16px;
  878. background-color: rgba(169, 209, 250, 1);
  879. border-radius: 20px;
  880. padding: 12px;
  881. font-size: 20px;
  882. font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  883. font-weight: bold;
  884. .sec-title {
  885. font-size: 48px;
  886. font-weight: bold;
  887. line-height: 68px;
  888. color: rgba(31, 135, 232, 1);
  889. text-align: left;
  890. font-family: 'YSBiaoTiHei-bold', 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  891. margin-bottom: 10px;
  892. }
  893. }
  894. /* ── 雷达图卡 ── */
  895. .radar-card {
  896. flex: 1;
  897. min-width: 0;
  898. display: flex;
  899. flex-direction: column;
  900. background: rgb(242, 249, 253);
  901. color: rgba(16, 16, 16, 1);
  902. font-size: 20px;
  903. line-height: 28px;
  904. text-align: center;
  905. .chart-box {
  906. width: 100%;
  907. height: 100%;
  908. min-height: 400px;
  909. position: relative;
  910. }
  911. }
  912. /* ── 右侧大卡片 ── */
  913. .side-big-card {
  914. flex: 1;
  915. display: flex;
  916. flex-direction: column;
  917. row-gap: 8px;
  918. overflow: hidden;
  919. background: var(--cardBgColor);
  920. padding: 8px;
  921. border-radius: 20px;
  922. }
  923. .side-section {
  924. display: flex;
  925. flex-direction: column;
  926. .side-title {
  927. font-size: 32px;
  928. font-weight: 900;
  929. line-height: 50px;
  930. color: rgba(31, 135, 232, 1);
  931. text-indent: 20px;
  932. text-align: left;
  933. font-style: italic;
  934. font-family: 'YSBiaoTiHei-bold', 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  935. }
  936. }
  937. .pos-section {
  938. flex: 1;
  939. overflow: hidden;
  940. }
  941. /* 补充信息 */
  942. .supp-grid {
  943. display: flex;
  944. flex: 1;
  945. flex-direction: column;
  946. border-radius: 30px;
  947. padding: 5px 20px;
  948. font-size: 20px;
  949. font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
  950. .supp-item {
  951. display: flex;
  952. justify-content: space-between;
  953. align-items: center;
  954. font-size: 20px;
  955. padding: 4px 0;
  956. line-height: 28px;
  957. color: rgba(0,0,0,1);
  958. font-weight: bold;
  959. .s-lbl {
  960. flex: 1;
  961. color: rgba(105,103,103,1);
  962. }
  963. .s-val {
  964. flex: 1;
  965. text-align: left;
  966. }
  967. }
  968. }
  969. /* 补充信息内容 */
  970. .supp-info-bg {
  971. background-color: rgba(254, 233, 232, 1);
  972. .supp-item {
  973. border-top: 1px dashed #fff;
  974. &:first-child {
  975. border: none;
  976. }
  977. }
  978. }
  979. /* 业务岗位内容 */
  980. .pos-info-bg {
  981. background-color: rgba(236,220,236,1);
  982. row-gap: 10px;
  983. overflow-y: auto;
  984. flex: content;
  985. }
  986. /* 培训情况 */
  987. .train-info-bg {
  988. background-color: rgba(206,248,228,1);
  989. padding: 10px 20px;
  990. .train-info-table {
  991. display: table;
  992. .train-row{
  993. display: table-row;
  994. & > span {
  995. display: table-cell;
  996. text-align: center;
  997. font-size: 16px;
  998. }
  999. }
  1000. .train-header{
  1001. display: table-row;
  1002. font-weight: bold;
  1003. & > span {
  1004. display: table-cell;
  1005. min-width: 5em;
  1006. text-align: center;
  1007. font-size: 20px;
  1008. }
  1009. }
  1010. }
  1011. }
  1012. /* 质控情况内容背景 */
  1013. .qc-info-bg {
  1014. background-color: #D5E7FD;
  1015. .s-val {
  1016. max-width: 100px;
  1017. }
  1018. }
  1019. /* Tooltip 样式(全部放这里) */
  1020. .radar-tooltip {
  1021. position: fixed;
  1022. background: rgba(100, 100, 100, 0.9);
  1023. border-radius: 16px;
  1024. padding: 14px 8px;
  1025. min-width: 320px;
  1026. z-index: 999;
  1027. pointer-events: none;
  1028. font-size: 14px;
  1029. line-height: 1.5;
  1030. transform: translateY(-50%);
  1031. display: none;
  1032. .tooltip-title {
  1033. font-size: 18px;
  1034. font-weight: bold;
  1035. color: #fff;
  1036. margin-bottom: 12px;
  1037. }
  1038. .tooltip-section {
  1039. margin-bottom: 12px;
  1040. background: #fff;
  1041. border-radius: 12px;
  1042. padding: 20px;
  1043. &:last-child {
  1044. margin-bottom: 0;
  1045. }
  1046. }
  1047. .section-title {
  1048. font-weight: 500;
  1049. margin-bottom: 6px;
  1050. font-size: 18px;
  1051. text-align: left;
  1052. &.deduct {
  1053. color: #ff4d4f;
  1054. }
  1055. &.add {
  1056. color: #00b42a;
  1057. }
  1058. }
  1059. .list-item {
  1060. display: flex;
  1061. align-items: center;
  1062. row-gap: 15px;
  1063. column-gap: 20px;
  1064. padding: 0 20px;
  1065. .deduct-text {
  1066. text-align: left;
  1067. color: #ff4d4f;
  1068. }
  1069. .add-text {
  1070. color: #00b42a;
  1071. }
  1072. }
  1073. .section-total {
  1074. display: flex;
  1075. align-items: center;
  1076. justify-content: flex-end;
  1077. margin-top: 6px;
  1078. font-weight: 500;
  1079. column-gap: 20px;
  1080. font-size: 16px;
  1081. &.deduct {
  1082. color: #ff4d4f;
  1083. }
  1084. &.add {
  1085. color: #00b42a;
  1086. }
  1087. }
  1088. }
  1089. /* ── 雷达图主体(含两侧浮窗) ── */
  1090. .radar-body {
  1091. flex: 1;
  1092. position: relative;
  1093. min-height: 0;
  1094. }
  1095. /* ── 加/扣分浮窗(绝对定位,叠在 chart 上方) ── */
  1096. .score-panel {
  1097. position: absolute;
  1098. top: 0;
  1099. width: 180px;
  1100. max-height: 100%;
  1101. display: flex;
  1102. flex-direction: column;
  1103. border-radius: 12px;
  1104. overflow: hidden;
  1105. border: 1px solid #e0e7f0;
  1106. font-size: 12px;
  1107. background: rgba(255,255,255,0.92);
  1108. box-shadow: 0 2px 8px rgba(0,0,0,0.12);
  1109. z-index: 10;
  1110. }
  1111. .deduct-panel { left: 0; }
  1112. .add-panel { right: 0; }
  1113. .panel-header {
  1114. padding: 6px 10px;
  1115. font-size: 13px;
  1116. font-weight: bold;
  1117. display: flex;
  1118. align-items: center;
  1119. flex-shrink: 0;
  1120. }
  1121. .deduct-header { background: rgba(254,233,232,1); color: #e03030; }
  1122. .add-header { background: rgba(206,248,228,1); color: #1a9944; }
  1123. .panel-dim-name {
  1124. font-size: 11px;
  1125. font-weight: normal;
  1126. margin-left: 4px;
  1127. opacity: 0.8;
  1128. max-width: 60px;
  1129. overflow: hidden;
  1130. text-overflow: ellipsis;
  1131. white-space: nowrap;
  1132. }
  1133. .panel-total {
  1134. font-size: 12px;
  1135. font-weight: bold;
  1136. margin-left: auto;
  1137. }
  1138. .radar-hint {
  1139. text-align: center;
  1140. font-size: 12px;
  1141. color: #aaa;
  1142. margin-top: 4px;
  1143. flex-shrink: 0;
  1144. }
  1145. .panel-fade-enter-active, .panel-fade-leave-active { transition: opacity 0.2s; }
  1146. .panel-fade-enter-from, .panel-fade-leave-to { opacity: 0; }
  1147. .panel-list {
  1148. flex: 1;
  1149. overflow-y: auto;
  1150. padding: 4px 0;
  1151. }
  1152. .panel-row {
  1153. display: flex;
  1154. align-items: center;
  1155. padding: 4px 8px;
  1156. gap: 4px;
  1157. border-bottom: 1px dashed #f0f0f0;
  1158. &:last-child { border-bottom: none; }
  1159. }
  1160. .p-dim { color: #999; font-size: 11px; flex-shrink: 0; min-width: 28px; }
  1161. .p-name { flex: 1; color: #444; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
  1162. .p-score { font-size: 12px; font-weight: bold; flex-shrink: 0; }
  1163. .deduct-score { color: #e03030; }
  1164. .add-score { color: #1a9944; }
  1165. .p-empty { color: #bbb; font-size: 11px; text-align: center; padding: 12px 0; }
  1166. /* 通用 */
  1167. .empty-text { color: #bbb; font-size: 20px; text-align: center; padding: 8px 0; }
  1168. </style>