index.vue 57 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749
  1. <template>
  2. <view class="employee-profile-page">
  3. <view class="page-header">
  4. <view class="header-title">
  5. <view class="title-main">员工综合信息展示</view>
  6. <view class="header-right">
  7. <view class="current-time">{{ currentTime }}</view>
  8. </view>
  9. </view>
  10. <view class="time-filter">
  11. <scroll-view scroll-x class="time-scroll">
  12. <view class="time-tags">
  13. <view v-for="(tag, index) in timeTags" :key="index"
  14. :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
  15. {{ tag }}
  16. </view>
  17. </view>
  18. </scroll-view>
  19. <view v-if="selectedTimeTag === 4" class="date-range-picker">
  20. <picker mode="date" :value="beginTime" @change="onBeginTimeChange">
  21. <view class="date-input" :class="{ filled: beginTime }">
  22. {{ beginTime || '开始日期' }}
  23. </view>
  24. </picker>
  25. <text class="date-separator">至</text>
  26. <picker mode="date" :value="endTime" @change="onEndTimeChange">
  27. <view class="date-input" :class="{ filled: endTime }">
  28. {{ endTime || '结束日期' }}
  29. </view>
  30. </picker>
  31. </view>
  32. </view>
  33. <view class="search-selector">
  34. <view class="search-select-trigger"
  35. :class="{ 'search-select-trigger-disabled': isSecurityCheck || routeUserId }"
  36. @click="!isSecurityCheck && !routeUserId && (showEmployeePicker = true)">
  37. <u-icon name="list" color="#A78BFA" size="16"></u-icon>
  38. <text class="search-name-text">{{ selectedEmployeeName || '请选择员工' }}</text>
  39. <u-icon v-if="!isSecurityCheck && !routeUserId" name="arrow-down" color="#A78BFA"
  40. size="14"></u-icon>
  41. </view>
  42. </view>
  43. <!-- <view class="tab-nav">
  44. <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
  45. 能力画像
  46. </view>
  47. <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
  48. 运行数据
  49. </view>
  50. </view> -->
  51. </view>
  52. <view class="page-content">
  53. <view v-if="!portrait.personName" class="empty-state">
  54. <view class="empty-icon">📋</view>
  55. <view class="empty-text">搜索员工姓名以查看画像</view>
  56. </view>
  57. <view v-if="portrait.personName">
  58. <SectionTitle title="个人基本信息">
  59. <view class="user-info">
  60. <view class="user-header">
  61. <view class="avatar-area">
  62. <image v-if="portrait.avatar" :src="portrait.avatar" class="avatar" mode="aspectFill">
  63. </image>
  64. <view v-else class="avatar-placeholder">{{ portrait.personName ?
  65. portrait.personName.charAt(0) : '' }}</view>
  66. </view>
  67. <view class="name-row">
  68. <view class="name-label">姓名:</view>
  69. <view class="name-value">{{ portrait.personName }}</view>
  70. </view>
  71. </view>
  72. <view class="info-grid">
  73. <view class="info-item">
  74. <view class="info-label">所属部门及队室:</view>
  75. <view class="info-value">{{ portrait.deptPath || '-' }}</view>
  76. </view>
  77. <view class="info-item">
  78. <view class="info-label">学历:</view>
  79. <view class="info-value">{{ schoolingText }}</view>
  80. </view>
  81. <view class="info-item">
  82. <view class="info-label">出生日期:</view>
  83. <view class="info-value">{{ portrait.birthday || '-' }}</view>
  84. </view>
  85. <view class="info-item">
  86. <view class="info-label">专业:</view>
  87. <view class="info-value">{{ portrait.major || '-' }}</view>
  88. </view>
  89. <view class="info-item">
  90. <view class="info-label">技能等级:</view>
  91. <view class="info-value">{{ portrait.qualificationLevelText || '-' }}</view>
  92. </view>
  93. <view class="info-item">
  94. <view class="info-label">标签:</view>
  95. <view class="info-value" v-if="portrait.userTags">
  96. <text class="tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag
  97. }}</text>
  98. </view>
  99. </view>
  100. </view>
  101. <view class="score-section">
  102. <div class="score-circle" ref="scoreCircle"></div>
  103. <view class="score-box">
  104. <!-- <view class="score-row">
  105. <text class="score-label">评分:</text>
  106. <text class="score-val">{{ portrait.totalScore || 0 }}</text>
  107. </view> -->
  108. <view class="score-row">
  109. <text class="score-label">附加分:</text>
  110. <text class="score-val">{{ tagScoreDisplay }}分</text>
  111. </view>
  112. </view>
  113. </view>
  114. </view>
  115. </SectionTitle>
  116. <SectionTitle v-if="activeTab === 'profile'" title="工作履历">
  117. <view class="work-history">
  118. <text v-if="portrait.entryDate">
  119. {{ formatWorkDate(portrait.entryDate) }}入职 | 司龄{{ portrait.companyYears != null ?
  120. portrait.companyYears : '-' }}年
  121. | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-' }}年
  122. | 现任职{{ portrait.roleNames || '-' }}
  123. </text>
  124. <text v-else>暂无数据</text>
  125. </view>
  126. </SectionTitle>
  127. <SectionTitle v-if="activeTab === 'profile'" title="获奖记录">
  128. <view class="honor-list">
  129. <view class="honor-item" v-for="(item, index) in portrait.awards" :key="index">
  130. <view class="honor-name">
  131. <text class="honor-dot"
  132. :style="{ background: item.color || honorColors[index % honorColors.length] }"></text>
  133. {{ item.level2Name }} {{ item.level4Name }}
  134. </view>
  135. <view class="honor-score">{{ item.score || '-' }}分</view>
  136. </view>
  137. <view v-if="!portrait.awards || portrait.awards.length === 0" class="no-data">暂无获奖记录</view>
  138. </view>
  139. </SectionTitle>
  140. <SectionTitle v-if="activeTab === 'profile'" title="职业资格证书获取时间">
  141. <view class="cert-info">
  142. <view class="cert-item" v-for="level in displayQualificationLevels" :key="level.field">
  143. <text class="cert-name">{{ level.label }}:{{ level.time || '-' }}</text>
  144. </view>
  145. <view v-if="!displayQualificationLevels.length" class="no-data">暂无数据</view>
  146. </view>
  147. </SectionTitle>
  148. <!-- <SectionTitle v-if="activeTab === 'profile' && displayQualificationLevels.length > 0"
  149. title="职业资格证书获取时间">
  150. <view class="cert-info">
  151. <view class="cert-item" v-for="level in displayQualificationLevels" :key="level.field">
  152. <text class="cert-name">{{ level.label }}:{{ level.time || '-' }}</text>
  153. </view>
  154. </view>
  155. </SectionTitle> -->
  156. <SectionTitle v-if="activeTab === 'profile'" title="个人能力">
  157. <div class="chart-legend">
  158. <div class="legend-item legend-warning"><span></span>预警线(低于75分)</div>
  159. <!-- <div class="legend-item legend-normal"><span></span>正常线(75~90分)</div> -->
  160. <div class="legend-item legend-excellent"><span></span>优秀线(高于90分)</div>
  161. <div class="legend-item legend-current"><span></span>当前分值</div>
  162. </div>
  163. <div class="chart-container" ref="radarChart"></div>
  164. </SectionTitle>
  165. <SectionTitle v-if="activeTab === 'profile'" title="补充信息">
  166. <view class="supp-grid">
  167. <view class="supp-item">
  168. <text class="supp-label">政治面貌:</text>
  169. <text class="supp-value">{{ portrait.politicalStatusText || '-' }}</text>
  170. </view>
  171. <view class="supp-item">
  172. <text class="supp-label">性别:</text>
  173. <text class="supp-value">{{ portrait.sexText || '-' }}</text>
  174. </view>
  175. <view class="supp-item">
  176. <text class="supp-label">籍贯:</text>
  177. <text class="supp-value">{{ portrait.nativePlace || '-' }}</text>
  178. </view>
  179. <view class="supp-item">
  180. <text class="supp-label">民族:</text>
  181. <text class="supp-value">{{ portrait.nation || '-' }}</text>
  182. </view>
  183. <view class="supp-item">
  184. <text class="supp-label">年龄:</text>
  185. <text class="supp-value">{{ ageText }}</text>
  186. </view>
  187. <view class="supp-item">
  188. <text class="supp-label">司龄:</text>
  189. <text class="supp-value">{{ portrait.companyYears != null ? portrait.companyYears + '年' :
  190. '-' }}</text>
  191. </view>
  192. <view class="supp-item">
  193. <text class="supp-label">性格特征:</text>
  194. <text class="supp-value">{{ portrait.characterCharacteristics || '-' }}</text>
  195. </view>
  196. <view class="supp-item">
  197. <text class="supp-label">工作风格:</text>
  198. <text class="supp-value">{{ portrait.workingStyle || '-' }}</text>
  199. </view>
  200. <view class="supp-item">
  201. <text class="supp-label">业务岗位:</text>
  202. <text class="supp-value">{{ portrait.postNames || '-' }}</text>
  203. </view>
  204. </view>
  205. </SectionTitle>
  206. <!-- <SectionTitle v-if="activeTab === 'data'" title="预警信息">
  207. <view class="warning-content" @click="showWarning = true">
  208. <view class="warning-tip">点击查看详细预警信息</view>
  209. </view>
  210. </SectionTitle> -->
  211. </view>
  212. </view>
  213. <view class="warning-overlay" v-if="showWarning" @click="showWarning = false">
  214. <view class="warning-panel" @click.stop>
  215. <view class="warning-header">
  216. <text class="warning-title">预警中枢</text>
  217. <text class="warning-close" @click="showWarning = false">✕</text>
  218. </view>
  219. <view class="warning-body">
  220. <view class="warning-desc">员工综合评估(<75分红色预警 | ≥90分优秀)</view>
  221. <view class="warning-score">
  222. <view class="warning-score-item">
  223. <text class="warning-score-label">综合得分</text>
  224. <text class="warning-score-value" :class="scoreLevelClass">{{ portrait.totalScore || 0
  225. }}</text>
  226. </view>
  227. </view>
  228. <view class="warning-detail" v-if="scoreDetails.length">
  229. <view class="warning-detail-title">评分明细</view>
  230. <view class="warning-detail-item" v-for="(item, index) in scoreDetails" :key="index">
  231. <view class="detail-left">
  232. <text class="detail-dim">{{ item.dimensionName }}</text>
  233. <text class="detail-name">{{ item.level3Name }}</text>
  234. </view>
  235. <view class="detail-score"
  236. :class="{ 'add-text': item.totalScore > 0, 'deduct-text': item.totalScore < 0 }">
  237. {{ item.totalScore > 0 ? '+' : '' }}{{ item.totalScore }}
  238. </view>
  239. </view>
  240. </view>
  241. </view>
  242. </view>
  243. </view>
  244. <u-popup :show="showEmployeePicker" mode="bottom" :round="16" :mask-close-able="true"
  245. :safe-area-inset-bottom="true" @close="onEmployeePickerClose">
  246. <view class="employee-picker">
  247. <view class="picker-header">
  248. <text class="picker-title">选择员工</text>
  249. </view>
  250. <view class="search-box">
  251. <u-input v-model="employeeSearchKeyword" placeholder="搜索员工" @confirm="onEmployeeSearch"
  252. @input="onEmployeeSearch"></u-input>
  253. </view>
  254. <scroll-view v-if="!employeeSearchKeyword.trim()" scroll-y class="tree-list">
  255. <template v-for="(node, index) in deptTreeData">
  256. <employee-tree-node :key="node.id" :node="node" :expanded-ids="expandedDeptIds"
  257. :selected-id="selectedEmployeeId" @toggle="toggleDeptExpand" @select="onEmployeeSelect" />
  258. </template>
  259. </scroll-view>
  260. <scroll-view v-else scroll-y class="employee-list">
  261. <view class="employee-item" v-for="item in filteredEmployeeList" :key="item.userId"
  262. @click="onEmployeeSelect(item)">
  263. <text class="employee-item-name">{{ item.nickName }}</text>
  264. <u-icon v-if="item.userId === selectedEmployeeId" name="checkmark" color="#34D399"
  265. size="18"></u-icon>
  266. </view>
  267. <view v-if="filteredEmployeeList.length === 0 && !employeeLoading" class="empty-state">
  268. <text>暂无数据</text>
  269. </view>
  270. <view v-if="employeeLoading" class="loading-state">
  271. <u-loading-icon text="加载中" textSize="14"></u-loading-icon>
  272. </view>
  273. </scroll-view>
  274. </view>
  275. </u-popup>
  276. <!-- 雷达图tooltip -->
  277. <u-popup :show="showRadarTooltipPopup" mode="center" :round="12" :mask-close-able="true"
  278. @close="closeRadarPopup">
  279. <div class="radar-tooltip-popup">
  280. <div class="tooltip-card">
  281. <!-- 标题 -->
  282. <view class="tooltip-header">
  283. <text class="tooltip-title">{{ activeDimName }}</text>
  284. <u-icon name="close" color="#666666" size="20" @click="closeRadarPopup" />
  285. </view>
  286. <!-- 加分区域 -->
  287. <div class="tooltip-section">
  288. <div class="section-list">
  289. <template v-if="addGroupList.length">
  290. <div class="list-item" v-for="(item, i) in addGroupList" :key="i">
  291. <span>{{ item.name }}:</span>
  292. <span class="add-text">+{{ item.total }}分</span>
  293. </div>
  294. </template>
  295. <div v-else class="p-empty">暂无加分记录</div>
  296. </div>
  297. <div class="section-total add">
  298. <span>合计加分:</span>
  299. <span>{{ addTotal }}分</span>
  300. </div>
  301. </div>
  302. <!-- 扣分区域 -->
  303. <div class="tooltip-section">
  304. <div class="section-list">
  305. <template v-if="deductGroupList.length">
  306. <div class="list-item" v-for="(item, i) in deductGroupList" :key="i">
  307. <span>{{ item.name }}:</span>
  308. <span class="deduct-text">{{ item.total }}分</span>
  309. </div>
  310. </template>
  311. <div v-else class="p-empty">暂无扣分记录</div>
  312. </div>
  313. <div class="section-total deduct">
  314. <span>合计扣分:</span>
  315. <span>{{ deductTotal }}分</span>
  316. </div>
  317. </div>
  318. </div>
  319. </div>
  320. </u-popup>
  321. </view>
  322. </template>
  323. <script>
  324. import * as echarts from 'echarts'
  325. import { getEmployeePortrait, countTagScore, getQualificationLevelTime } from '@/api/portraitManagement/portraitManagement'
  326. import { listAllUser, getDeptUserTree } from '@/api/system/user'
  327. import SectionTitle from '@/components/SectionTitle.vue'
  328. import EmployeeTreeNode from '@/pages/components/EmployeeTreeNode.vue'
  329. const honorColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#A78BFA', '#F472B6', '#6EE7B7', '#FB923C']
  330. const schoolingMap = {
  331. '1': '小学',
  332. '2': '初中',
  333. '3': '高中',
  334. '4': '中专',
  335. '5': '大专',
  336. '6': '本科',
  337. '7': '硕士',
  338. '8': '博士'
  339. }
  340. function getRandomHexColor() {
  341. return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
  342. }
  343. export default {
  344. name: 'EmployeeProfile',
  345. components: {
  346. SectionTitle,
  347. EmployeeTreeNode
  348. },
  349. data() {
  350. return {
  351. activeTab: 'profile',
  352. selectedTimeTag: 3,
  353. timeTags: ['近一周', '近一月', '近三月', '近一年', '自定义时间范围'],
  354. currentTime: '',
  355. timer: null,
  356. beginTime: '',
  357. endTime: '',
  358. searchKeyword: '',
  359. portrait: { dimensions: [], awards: [] },
  360. tagScoreData: null,
  361. qualificationData: null,
  362. scoreDetails: [],
  363. showWarning: false,
  364. radarChartInstance: null,
  365. scoreChartInstance: null,
  366. // 员工选择相关
  367. showEmployeePicker: false,
  368. selectedEmployeeId: null,
  369. selectedEmployeeName: '',
  370. deptTreeData: [],
  371. pickerTreeData: [],
  372. employeeList: [],
  373. employeeSearchKeyword: '',
  374. employeeLoading: false,
  375. expandedDeptIds: [],
  376. isSecurityCheck: false,
  377. userInfo: null,
  378. // 路由跳转参数
  379. routeUserId: null,
  380. routeUserName: '',
  381. // 雷达图tooltip相关
  382. activeDimName: null,
  383. radarTooltipPosition: { x: 0, y: 0 },
  384. // 控制雷达图 tooltip popup 显示
  385. showRadarTooltipPopup: false,
  386. // 评分明细数据
  387. allScoreDetails: [],
  388. qualificationLevelMap: [
  389. { label: '五级', field: 'levelFiveTime' },
  390. { label: '四级', field: 'levelFourTime' },
  391. { label: '三级', field: 'levelThreeTime' },
  392. { label: '二级', field: 'levelTwoTime' },
  393. { label: '一级', field: 'levelOneTime' },
  394. ]
  395. }
  396. },
  397. computed: {
  398. displayQualificationLevels() {
  399. if (!this.qualificationData) return []
  400. console.log(this.qualificationData)
  401. debugger
  402. return this.qualificationLevelMap.map(item => ({
  403. label: item.label,
  404. time: this.qualificationData[item.field] || 0
  405. })).filter(item => item.time)
  406. },
  407. schoolingText() {
  408. if (!this.portrait.schooling) return '-'
  409. return schoolingMap[this.portrait.schooling] || this.portrait.schooling
  410. },
  411. ageText() {
  412. if (!this.portrait.birthday) return '-'
  413. const birthDate = new Date(this.portrait.birthday)
  414. if (isNaN(birthDate.getTime())) return '-'
  415. const today = new Date()
  416. let age = today.getFullYear() - birthDate.getFullYear()
  417. const monthDiff = today.getMonth() - birthDate.getMonth()
  418. if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
  419. age--
  420. }
  421. return age + '岁'
  422. },
  423. tagScoreDisplay() {
  424. return this.portrait?.userTags?.split(',').length || 0
  425. },
  426. scoreLevelClass() {
  427. if ((this.portrait.totalScore || 0) < 75) return 'score-danger'
  428. if ((this.portrait.totalScore || 0) >= 90) return 'score-excellent'
  429. return ''
  430. },
  431. currentDate() {
  432. const now = new Date()
  433. const year = now.getFullYear()
  434. const month = String(now.getMonth() + 1).padStart(2, '0')
  435. const day = String(now.getDate()).padStart(2, '0')
  436. const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
  437. const weekDay = weekDays[now.getDay()]
  438. return `${year}年${month}月${day}日 ${weekDay}`
  439. },
  440. filteredEmployeeList() {
  441. const keyword = this.employeeSearchKeyword.trim().toLowerCase()
  442. if (!keyword) return this.employeeList
  443. return this.employeeList.filter(item =>
  444. (item.nickName || '').toLowerCase().includes(keyword)
  445. )
  446. },
  447. // 雷达图tooltip相关计算
  448. addList() {
  449. const all = (this.allScoreDetails || []).filter(d => d.totalScore != null && Number(d.totalScore) > 0)
  450. return this.activeDimName ? all.filter(d => d.dimensionName === this.activeDimName) : all
  451. },
  452. deductList() {
  453. const all = (this.allScoreDetails || []).filter(d => d.totalScore != null && Number(d.totalScore) < 0)
  454. return this.activeDimName ? all.filter(d => d.dimensionName === this.activeDimName) : all
  455. },
  456. addGroupList() {
  457. const groups = {}
  458. this.addList.forEach(d => {
  459. const key = d.level2Name || '其他'
  460. groups[key] = (groups[key] || 0) + Number(d.totalScore)
  461. })
  462. return Object.entries(groups).map(([name, total]) => ({ name, total: total.toFixed(2) }))
  463. },
  464. deductGroupList() {
  465. const groups = {}
  466. this.deductList.forEach(d => {
  467. const key = d.level2Name || '其他'
  468. groups[key] = (groups[key] || 0) + Number(d.totalScore)
  469. })
  470. return Object.entries(groups).map(([name, total]) => ({ name, total: total.toFixed(2) }))
  471. },
  472. addTotal() {
  473. const s = this.addList.reduce((acc, d) => acc + Number(d.totalScore), 0)
  474. return (s > 0 ? '+' : '') + s.toFixed(2)
  475. },
  476. deductTotal() {
  477. const s = this.deductList.reduce((acc, d) => acc + Number(d.totalScore), 0)
  478. return s.toFixed(2)
  479. },
  480. },
  481. // uni-app 页面生命周期:接收路由参数
  482. onLoad(options) {
  483. if (options.userId) {
  484. this.routeUserId = options.userId
  485. this.routeUserName = decodeURIComponent(options.userName || '')
  486. }
  487. },
  488. mounted() {
  489. this.updateTime()
  490. this.timer = setInterval(() => {
  491. this.updateTime()
  492. }, 1000)
  493. // 默认计算时间范围(近一年)
  494. this.onTimeTagClick(this.selectedTimeTag)
  495. if (this.routeUserId) {
  496. // 来自组织架构跳转,直接加载指定人员
  497. this.selectedEmployeeId = this.routeUserId
  498. this.selectedEmployeeName = this.routeUserName
  499. this.searchKeyword = this.routeUserName
  500. this.employeeLoading = false
  501. this.$nextTick(() => {
  502. this.fetchEmployeePortrait()
  503. })
  504. } else {
  505. this.fetchEmployeeList()
  506. }
  507. },
  508. beforeDestroy() {
  509. if (this.timer) {
  510. clearInterval(this.timer)
  511. }
  512. if (this.radarChartInstance) {
  513. this.radarChartInstance.dispose()
  514. }
  515. if (this.scoreChartInstance) {
  516. this.scoreChartInstance.dispose()
  517. }
  518. },
  519. methods: {
  520. updateTime() {
  521. const now = new Date()
  522. const hours = String(now.getHours()).padStart(2, '0')
  523. const minutes = String(now.getMinutes()).padStart(2, '0')
  524. const seconds = String(now.getSeconds()).padStart(2, '0')
  525. this.currentTime = `${hours}:${minutes}:${seconds}`
  526. },
  527. // 员工相关方法
  528. // 扁平化部门树,提取人员
  529. flattenDeptTree(tree) {
  530. const result = []
  531. const traverse = (nodes) => {
  532. nodes.forEach(node => {
  533. const hasChildren = node.children && node.children.length > 0
  534. if (!hasChildren && node.nodeType !== 'dept') {
  535. result.push({
  536. nodeType: node.nodeType || '',
  537. userId: node.userId || node.id,
  538. nickName: node.nickName || node.label || node.userName || ''
  539. })
  540. }
  541. if (hasChildren) {
  542. traverse(node.children)
  543. }
  544. })
  545. }
  546. traverse(tree)
  547. return result
  548. },
  549. // 从树中查找第一个用户节点
  550. findFirstUser(nodes) {
  551. for (const node of nodes) {
  552. const hasChildren = node.children && node.children.length > 0
  553. if (!hasChildren && node.nodeType !== 'dept') {
  554. return node
  555. }
  556. if (hasChildren) {
  557. const found = this.findFirstUser(node.children)
  558. if (found) return found
  559. }
  560. }
  561. return null
  562. },
  563. // 展开所有部门节点
  564. expandAllDepts(nodes) {
  565. this.expandedDeptIds = []
  566. const traverse = (list) => {
  567. list.forEach(node => {
  568. const hasChildren = node.children && node.children.length > 0
  569. if (hasChildren || node.nodeType === 'dept') {
  570. this.expandedDeptIds.push(node.id)
  571. if (hasChildren) {
  572. traverse(node.children)
  573. }
  574. }
  575. })
  576. }
  577. traverse(nodes)
  578. },
  579. fetchEmployeeList() {
  580. this.employeeLoading = true
  581. const user = this.$store.state.user?.userInfo || this.$store.state.user?.user
  582. this.userInfo = user
  583. // 检查角色是否包含 SecurityCheck
  584. const roles = this.$store.state.user?.roles || []
  585. this.isSecurityCheck = roles.some(role => role.includes('SecurityCheck'))
  586. if (this.isSecurityCheck && user) {
  587. // SecurityCheck 角色,默认显示登录人,不可编辑
  588. this.selectedEmployeeId = user.userId || user.id
  589. this.selectedEmployeeName = user.nickName || user.userName || ''
  590. this.searchKeyword = this.selectedEmployeeName
  591. this.deptTreeData = []
  592. this.pickerTreeData = []
  593. this.employeeLoading = false
  594. // 自动加载画像数据
  595. this.fetchEmployeePortrait()
  596. } else {
  597. // 其他角色,使用 getDeptUserTree 接口
  598. if (user && user.deptId) {
  599. getDeptUserTree({ deptId: user.deptId }).then(res => {
  600. if (res.code === 200) {
  601. this.deptTreeData = res.data || []
  602. const allUsers = this.flattenDeptTree(this.deptTreeData)
  603. this.employeeList = allUsers.filter(user => user.nodeType === 'user')
  604. // 默认展开所有部门
  605. this.expandAllDepts(this.deptTreeData)
  606. // 默认选中第一个用户
  607. const firstUser = this.findFirstUser(this.deptTreeData)
  608. if (firstUser) {
  609. this.selectedEmployeeId = firstUser.userId || firstUser.id
  610. this.selectedEmployeeName = firstUser.nickName || firstUser.label || firstUser.userName || ''
  611. this.searchKeyword = this.selectedEmployeeName
  612. this.fetchEmployeePortrait()
  613. }
  614. }
  615. }).catch(() => {
  616. }).finally(() => {
  617. this.employeeLoading = false
  618. })
  619. } else {
  620. this.employeeLoading = false
  621. }
  622. }
  623. },
  624. onEmployeeSearch() {
  625. // 搜索由 computed 自动处理
  626. },
  627. toggleDeptExpand(id) {
  628. const index = this.expandedDeptIds.indexOf(id)
  629. if (index > -1) {
  630. this.expandedDeptIds.splice(index, 1)
  631. } else {
  632. this.expandedDeptIds.push(id)
  633. }
  634. },
  635. onEmployeeSelect(item) {
  636. this.selectedEmployeeId = item.userId
  637. this.selectedEmployeeName = item.nickName
  638. this.searchKeyword = item.nickName
  639. this.showEmployeePicker = false
  640. this.fetchEmployeePortrait()
  641. },
  642. onEmployeePickerClose() {
  643. this.showEmployeePicker = false
  644. },
  645. buildTimeParams() {
  646. const now = new Date()
  647. const today = this.formatDate(now)
  648. if (this.selectedTimeTag === 4) {
  649. if (this.startDate && this.endDate) {
  650. return { startDate: this.startDate, endDate: this.endDate }
  651. }
  652. return {}
  653. }
  654. let startDate
  655. switch (this.selectedTimeTag) {
  656. case 0:
  657. startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
  658. break
  659. case 1:
  660. startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
  661. break
  662. case 2:
  663. startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
  664. break
  665. case 3:
  666. startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
  667. break
  668. default:
  669. return {}
  670. }
  671. return { startDate: this.formatDate(startDate), endDate: today }
  672. },
  673. formatDate(date) {
  674. const y = date.getFullYear()
  675. const m = String(date.getMonth() + 1).padStart(2, '0')
  676. const d = String(date.getDate()).padStart(2, '0')
  677. return `${y}-${m}-${d}`
  678. },
  679. onTimeTagClick(index) {
  680. this.selectedTimeTag = index
  681. if (index === 4) {
  682. this.beginTime = ''
  683. this.endTime = ''
  684. return
  685. }
  686. // 自动计算时间范围
  687. const now = new Date()
  688. // 设置结束时间为今天
  689. const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate())
  690. // 设置开始时间
  691. let beginTime
  692. switch (index) {
  693. case 0: // 近一周
  694. beginTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
  695. break
  696. case 1: // 近一月
  697. beginTime = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
  698. break
  699. case 2: // 近三月
  700. beginTime = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
  701. break
  702. case 3: // 近一年
  703. beginTime = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
  704. break
  705. default:
  706. beginTime = now
  707. }
  708. // 格式化日期为 YYYY-MM-DD
  709. this.beginTime = this.formatDate(beginTime)
  710. this.endTime = this.formatDate(endTime)
  711. // 选择时间标签后立即查询
  712. if (this.selectedEmployeeId) {
  713. this.fetchEmployeePortrait()
  714. }
  715. },
  716. onBeginTimeChange(e) {
  717. const dateStr = e.detail.value
  718. this.beginTime = dateStr
  719. // 自定义时间选择后立即查询
  720. if (this.selectedEmployeeId && this.endTime) {
  721. this.fetchEmployeePortrait()
  722. }
  723. },
  724. onEndTimeChange(e) {
  725. const dateStr = e.detail.value
  726. this.endTime = dateStr
  727. // 自定义时间选择后立即查询
  728. if (this.selectedEmployeeId && this.beginTime) {
  729. this.fetchEmployeePortrait()
  730. }
  731. },
  732. formatWorkDate(d) {
  733. if (!d) return '-'
  734. const str = String(d)
  735. const parts = str.substring(0, 10).split('-')
  736. if (parts.length < 3) return str
  737. return parts[0] + '.' + Number(parts[1]) + '.' + Number(parts[2])
  738. },
  739. onSearch() {
  740. if (!this.searchKeyword.trim()) return
  741. this.fetchEmployeePortrait()
  742. },
  743. fetchEmployeePortrait() {
  744. const params = {}
  745. if (this.searchKeyword.trim()) {
  746. params.personName = this.searchKeyword.trim()
  747. }
  748. // 添加时间参数
  749. if (this.beginTime) {
  750. params.beginTime = this.beginTime
  751. }
  752. if (this.endTime) {
  753. params.endTime = this.endTime
  754. }
  755. const portraitPromise = getEmployeePortrait(params).then(res => {
  756. if (res.code === 200 && res.data) {
  757. this.portrait = res.data || { dimensions: [], awards: [] }
  758. this.portrait.awards.forEach(item => {
  759. item.color = getRandomHexColor()
  760. })
  761. // 只取数组中最后一个元素的 scoreDetails
  762. const allScoreDetails = res.data.scoreDetails || []
  763. this.allScoreDetails = allScoreDetails
  764. this.scoreDetails = allScoreDetails.length > 0 ? allScoreDetails[allScoreDetails.length - 1] : []
  765. }
  766. }).catch(() => { })
  767. const tagPromise = countTagScore(params).then(res => {
  768. const data = res.data
  769. if (Array.isArray(data)) {
  770. const found = data.find(item => item.userId === this.selectedEmployeeId)
  771. this.tagScoreData = found ? (found.totalScore ?? found.score) : null
  772. } else {
  773. this.tagScoreData = data
  774. }
  775. }).catch(() => { })
  776. const qualificationPromise = getQualificationLevelTime({ userId: this.selectedEmployeeId }).then(res => {
  777. this.qualificationData = res.data || null
  778. }).catch(() => {
  779. this.qualificationData = null
  780. })
  781. Promise.all([portraitPromise, tagPromise, qualificationPromise]).finally(() => {
  782. this.$nextTick(() => {
  783. this.initRadarChart()
  784. this.initScoreChart()
  785. })
  786. })
  787. },
  788. initScoreChart() {
  789. if (!this.$refs.scoreCircle) return
  790. if (this.scoreChartInstance) {
  791. this.scoreChartInstance.dispose()
  792. }
  793. this.scoreChartInstance = echarts.init(this.$refs.scoreCircle)
  794. const score = this.portrait.totalScore || 0
  795. const option = {
  796. series: [
  797. {
  798. type: 'gauge',
  799. startAngle: 90,
  800. endAngle: -270,
  801. pointer: {
  802. show: false
  803. },
  804. progress: {
  805. show: true,
  806. overlap: false,
  807. roundCap: true,
  808. clip: false,
  809. itemStyle: {
  810. borderWidth: 1,
  811. borderColor: '#464646'
  812. }
  813. },
  814. axisLine: {
  815. lineStyle: {
  816. width: 5,
  817. color: [[1, '#34D399']]
  818. }
  819. },
  820. splitLine: {
  821. show: false,
  822. distance: 0,
  823. length: 10
  824. },
  825. axisTick: {
  826. show: false
  827. },
  828. axisLabel: {
  829. show: false,
  830. distance: 50
  831. },
  832. data: [
  833. {
  834. value: score,
  835. name: '综合得分'
  836. }
  837. ],
  838. title: {
  839. fontSize: 14
  840. },
  841. detail: {
  842. width: 50,
  843. height: 14,
  844. fontSize: 20,
  845. color: 'inherit',
  846. padding: [0, 0, 80, 0],
  847. borderColor: 'inherit',
  848. formatter: '{value}',
  849. rich: {
  850. value: {
  851. fontSize: 14,
  852. color: '#333',
  853. lineHeight: 35,
  854. }
  855. }
  856. }
  857. }
  858. ]
  859. }
  860. this.scoreChartInstance.setOption(option)
  861. },
  862. initRadarChart() {
  863. if (!this.$refs.radarChart) return
  864. if (this.radarChartInstance) {
  865. this.radarChartInstance.dispose()
  866. }
  867. this.radarChartInstance = echarts.init(this.$refs.radarChart)
  868. const dimensions = this.portrait.dimensions || []
  869. // 仿照 index copy.vue 的配置
  870. const radarData = {
  871. grounp: dimensions.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
  872. data: [{
  873. name: '个人能力',
  874. value: dimensions.map(attr => attr.score),
  875. symbolSize: 10,
  876. areaStyle: {
  877. show: false,
  878. opacity: 0
  879. },
  880. lineStyle: {
  881. color: '#4DC8FE',
  882. width: 1
  883. },
  884. itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#00C8DA', borderJoin: 'round' }
  885. }]
  886. }
  887. const option = {
  888. radar: {
  889. indicator: radarData.grounp,
  890. center: ['50%', '52%'],
  891. radius: '50%',
  892. splitNumber: 8,
  893. axisLine: { lineStyle: { color: '#ccc' } },
  894. splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 1 } },
  895. splitArea: { show: false },
  896. axisName: {
  897. color: '#333',
  898. fontSize: 12
  899. }
  900. },
  901. series: [
  902. {
  903. type: 'radar',
  904. data: radarData.data,
  905. symbol: 'circle',
  906. symbolSize: 13,
  907. label: {
  908. show: false,
  909. formatter: (p) => p.value,
  910. color: '#333',
  911. fontSize: 16,
  912. fontWeight: 'bold'
  913. }
  914. }
  915. ]
  916. }
  917. this.radarChartInstance.setOption(option)
  918. // 绑定鼠标事件
  919. this.bindRadarEvents()
  920. },
  921. bindRadarEvents() {
  922. if (!this.radarChartInstance || !this.$refs.radarChart) return
  923. this.radarChartInstance.off('click')
  924. this.radarChartInstance.on('click', (params) => {
  925. if (!Array.isArray(this.portrait.dimensions) || !this.portrait.dimensions.length) return
  926. const rect = this.$refs.radarChart.getBoundingClientRect()
  927. if (!rect) return
  928. const cx = rect.width / 2
  929. const cy = rect.height / 2
  930. const dx = params.event.offsetX - cx
  931. const dy = params.event.offsetY - cy
  932. const distance = Math.sqrt(dx * dx + dy * dy)
  933. const radarRadius = rect.width * 0.35 // 对应 radius: 65%
  934. if (distance > radarRadius) {
  935. return
  936. }
  937. // 角度计算
  938. let angle = Math.atan2(dx, -dy) * (180 / Math.PI)
  939. if (angle < 0) angle += 360
  940. const count = this.portrait.dimensions.length
  941. const step = 360 / count
  942. let idx = Math.abs(Math.round(angle / step) % count - count)
  943. if (idx < 0) idx += count
  944. if (idx >= count) idx = 0
  945. // 显示tooltip popup - 加上小延迟避免 u-popup 立即关闭的问题
  946. this.activeDimName = this.portrait.dimensions[idx].name
  947. setTimeout(() => {
  948. this.showRadarTooltipPopup = true
  949. }, 50)
  950. })
  951. },
  952. // 关闭雷达图 tooltip popup
  953. closeRadarPopup() {
  954. this.showRadarTooltipPopup = false
  955. }
  956. }
  957. }
  958. </script>
  959. <style lang="scss" scoped>
  960. .employee-profile-page {
  961. min-height: 100vh;
  962. background: #fff;
  963. padding-bottom: 40rpx;
  964. }
  965. .page-header {
  966. position: sticky;
  967. top: 0;
  968. z-index: 100;
  969. color: black;
  970. background: #fff;
  971. backdrop-filter: blur(10px);
  972. box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
  973. }
  974. .header-title {
  975. padding: 24rpx 32rpx;
  976. display: flex;
  977. justify-content: space-between;
  978. align-items: center;
  979. .title-main {
  980. font-size: 36rpx;
  981. font-weight: bold;
  982. color: black;
  983. }
  984. }
  985. .header-right {
  986. display: flex;
  987. align-items: center;
  988. gap: 16rpx;
  989. .current-time {
  990. font-size: 24rpx;
  991. color: #999;
  992. }
  993. }
  994. .time-filter {
  995. padding: 16rpx 32rpx;
  996. background: #fff;
  997. }
  998. .time-scroll {
  999. width: 100%;
  1000. }
  1001. .time-tags {
  1002. display: flex;
  1003. gap: 16rpx;
  1004. white-space: nowrap;
  1005. }
  1006. .time-tag {
  1007. padding: 8rpx 10rpx;
  1008. border-radius: 50rpx;
  1009. background: #f0f0f0;
  1010. font-size: 24rpx;
  1011. color: #666;
  1012. transition: all 0.3s;
  1013. &.active {
  1014. background: #60A5FA;
  1015. color: #fff;
  1016. font-weight: 500;
  1017. }
  1018. }
  1019. .date-range-picker {
  1020. display: flex;
  1021. align-items: center;
  1022. gap: 16rpx;
  1023. margin-top: 16rpx;
  1024. padding-top: 16rpx;
  1025. border-top: 1rpx solid #e0e0e0;
  1026. }
  1027. .date-input {
  1028. flex: 1;
  1029. padding: 12rpx 24rpx;
  1030. border-radius: 12rpx;
  1031. background: #f5f5f5;
  1032. font-size: 24rpx;
  1033. color: #999;
  1034. text-align: center;
  1035. &.filled {
  1036. color: #333;
  1037. }
  1038. }
  1039. .date-separator {
  1040. font-size: 24rpx;
  1041. color: #999;
  1042. flex-shrink: 0;
  1043. }
  1044. .search-selector {
  1045. padding: 16rpx 32rpx;
  1046. background: #fff;
  1047. }
  1048. .search-select-trigger {
  1049. display: flex;
  1050. align-items: center;
  1051. justify-content: center;
  1052. gap: 12rpx;
  1053. padding: 16rpx 24rpx;
  1054. background: #f5f5f5;
  1055. border: 1rpx solid #e0e0e0;
  1056. border-radius: 50rpx;
  1057. }
  1058. .search-select-trigger-disabled {
  1059. opacity: 0.5;
  1060. pointer-events: none;
  1061. }
  1062. .search-name-text {
  1063. font-size: 26rpx;
  1064. color: #333;
  1065. }
  1066. .employee-picker {
  1067. background: #fff;
  1068. display: flex;
  1069. flex-direction: column;
  1070. border-radius: 16rpx 16rpx 0 0;
  1071. width: 100%;
  1072. height: 70vh;
  1073. }
  1074. .picker-header {
  1075. padding: 24rpx 32rpx;
  1076. border-bottom: 1rpx solid #eee;
  1077. display: flex;
  1078. justify-content: center;
  1079. flex-shrink: 0;
  1080. }
  1081. .picker-title {
  1082. font-size: 32rpx;
  1083. font-weight: 600;
  1084. color: #333;
  1085. }
  1086. .search-box {
  1087. padding: 16rpx 32rpx;
  1088. flex-shrink: 0;
  1089. }
  1090. .tree-list {
  1091. flex: 1;
  1092. padding: 0;
  1093. height: 0;
  1094. overflow: hidden;
  1095. }
  1096. .employee-list {
  1097. flex: 1;
  1098. padding: 0 32rpx;
  1099. height: 0;
  1100. overflow: hidden;
  1101. }
  1102. .employee-item {
  1103. display: flex;
  1104. align-items: center;
  1105. justify-content: space-between;
  1106. padding: 24rpx 0;
  1107. border-bottom: 1rpx solid #f0f0f0;
  1108. }
  1109. .employee-item-name {
  1110. font-size: 28rpx;
  1111. color: #333;
  1112. }
  1113. .tab-nav {
  1114. padding: 16rpx 32rpx;
  1115. display: flex;
  1116. justify-content: center;
  1117. gap: 64rpx;
  1118. }
  1119. .tab-item {
  1120. font-size: 28rpx;
  1121. color: #999;
  1122. padding-bottom: 8rpx;
  1123. border-bottom: 2rpx solid transparent;
  1124. transition: all 0.3s;
  1125. &.active {
  1126. color: #333;
  1127. border-bottom-color: #333;
  1128. }
  1129. }
  1130. .page-content {
  1131. padding: 32rpx;
  1132. display: flex;
  1133. flex-direction: column;
  1134. }
  1135. .empty-state {
  1136. display: flex;
  1137. flex-direction: column;
  1138. align-items: center;
  1139. justify-content: center;
  1140. padding: 120rpx 0;
  1141. .empty-icon {
  1142. font-size: 80rpx;
  1143. margin-bottom: 24rpx;
  1144. }
  1145. .empty-text {
  1146. font-size: 28rpx;
  1147. color: #999;
  1148. }
  1149. }
  1150. .user-info {
  1151. .user-header {
  1152. display: flex;
  1153. align-items: center;
  1154. gap: 32rpx;
  1155. margin-bottom: 32rpx;
  1156. padding-bottom: 24rpx;
  1157. border-bottom: 1rpx solid #e0e0e0;
  1158. }
  1159. .avatar-area {
  1160. width: 120rpx;
  1161. height: 120rpx;
  1162. border-radius: 50%;
  1163. background: #f0f0f0;
  1164. display: flex;
  1165. align-items: center;
  1166. justify-content: center;
  1167. flex-shrink: 0;
  1168. .avatar {
  1169. width: 100%;
  1170. height: 100%;
  1171. border-radius: 50%;
  1172. object-fit: cover;
  1173. }
  1174. .avatar-placeholder {
  1175. font-size: 48rpx;
  1176. font-weight: bold;
  1177. color: #999;
  1178. }
  1179. }
  1180. .name-row {
  1181. display: flex;
  1182. align-items: baseline;
  1183. .name-label {
  1184. font-size: 26rpx;
  1185. color: #999;
  1186. }
  1187. .name-value {
  1188. font-size: 36rpx;
  1189. font-weight: bold;
  1190. color: #333;
  1191. }
  1192. }
  1193. }
  1194. .info-grid {
  1195. display: grid;
  1196. grid-template-columns: 1fr 1fr;
  1197. gap: 20rpx;
  1198. margin-bottom: 32rpx;
  1199. .info-item {
  1200. .info-label {
  1201. font-size: 22rpx;
  1202. color: #999;
  1203. margin-bottom: 4rpx;
  1204. }
  1205. .info-value {
  1206. font-size: 26rpx;
  1207. color: #333;
  1208. .tag {
  1209. display: inline-block;
  1210. padding: 4rpx 16rpx;
  1211. margin-left: 2rpx;
  1212. background: #f0f0f0;
  1213. border-radius: 8rpx;
  1214. color: #333;
  1215. font-size: 22rpx;
  1216. }
  1217. }
  1218. }
  1219. }
  1220. .score-section {
  1221. display: flex;
  1222. align-items: center;
  1223. justify-content: center;
  1224. gap: 48rpx;
  1225. padding-top: 24rpx;
  1226. border-top: 1rpx solid #e0e0e0;
  1227. .score-circle {
  1228. width: 280rpx;
  1229. height: 280rpx;
  1230. }
  1231. .score-box {
  1232. .score-row {
  1233. display: flex;
  1234. align-items: center;
  1235. margin-bottom: 16rpx;
  1236. .score-label {
  1237. font-size: 26rpx;
  1238. color: #999;
  1239. }
  1240. .score-val {
  1241. font-size: 32rpx;
  1242. font-weight: bold;
  1243. color: #333;
  1244. }
  1245. }
  1246. }
  1247. }
  1248. .work-history {
  1249. font-size: 26rpx;
  1250. color: #555;
  1251. line-height: 1.8;
  1252. }
  1253. .honor-list {
  1254. display: flex;
  1255. flex-direction: column;
  1256. gap: 16rpx;
  1257. .honor-item {
  1258. display: flex;
  1259. justify-content: space-between;
  1260. align-items: center;
  1261. padding: 16rpx;
  1262. background: #f5f5f5;
  1263. border-radius: 12rpx;
  1264. .honor-name {
  1265. display: flex;
  1266. align-items: center;
  1267. gap: 12rpx;
  1268. font-size: 26rpx;
  1269. color: #333;
  1270. flex: 1;
  1271. .honor-dot {
  1272. width: 12rpx;
  1273. height: 12rpx;
  1274. border-radius: 50%;
  1275. flex-shrink: 0;
  1276. }
  1277. }
  1278. .honor-score {
  1279. font-size: 26rpx;
  1280. color: #A78BFA;
  1281. font-weight: 500;
  1282. flex-shrink: 0;
  1283. }
  1284. }
  1285. .no-data {
  1286. font-size: 26rpx;
  1287. color: #999;
  1288. text-align: center;
  1289. padding: 24rpx 0;
  1290. }
  1291. }
  1292. .cert-info {
  1293. .cert-item {
  1294. padding: 16rpx 0;
  1295. border-bottom: 1rpx solid #f0f0f0;
  1296. &:last-child {
  1297. border-bottom: none;
  1298. }
  1299. .cert-name {
  1300. font-size: 26rpx;
  1301. color: #333;
  1302. }
  1303. }
  1304. .no-data {
  1305. font-size: 26rpx;
  1306. color: #999;
  1307. text-align: center;
  1308. padding: 24rpx 0;
  1309. }
  1310. }
  1311. .cert-info {
  1312. display: flex;
  1313. flex-direction: column;
  1314. gap: 16rpx;
  1315. }
  1316. .cert-item {
  1317. display: flex;
  1318. align-items: center;
  1319. padding: 12rpx 0;
  1320. border-bottom: 1rpx solid #f0f0f0;
  1321. &:last-child {
  1322. border-bottom: none;
  1323. }
  1324. }
  1325. .cert-name {
  1326. font-size: 28rpx;
  1327. color: #333;
  1328. }
  1329. .chart-container {
  1330. width: 100%;
  1331. height: 500rpx;
  1332. }
  1333. .chart-legend {
  1334. display: flex;
  1335. justify-content: center;
  1336. align-items: center;
  1337. flex-wrap: wrap;
  1338. gap: 10px 18px;
  1339. padding-top: 8px;
  1340. color: #fff;
  1341. font-size: 14px;
  1342. }
  1343. .legend-item {
  1344. display: flex;
  1345. align-items: center;
  1346. gap: 6rpx;
  1347. span {
  1348. width: 24rpx;
  1349. height: 10rpx;
  1350. border: 2rpx solid currentColor;
  1351. border-radius: 2rpx;
  1352. }
  1353. }
  1354. .legend-warning {
  1355. color: #fe4322;
  1356. span {
  1357. background-color: #fe4322;
  1358. }
  1359. }
  1360. .legend-normal {
  1361. color: black;
  1362. span {
  1363. background-color: gray;
  1364. }
  1365. }
  1366. .legend-excellent {
  1367. color: #8EC742;
  1368. span {
  1369. background-color: #8EC742;
  1370. }
  1371. }
  1372. .legend-current {
  1373. color: #1890ff;
  1374. span {
  1375. background-color: #1890ff;
  1376. }
  1377. }
  1378. .supp-grid {
  1379. display: grid;
  1380. grid-template-columns: 1fr 1fr;
  1381. gap: 20rpx;
  1382. .supp-item {
  1383. .supp-label {
  1384. font-size: 22rpx;
  1385. color: #666;
  1386. margin-bottom: 4rpx;
  1387. display: block;
  1388. }
  1389. .supp-value {
  1390. font-size: 26rpx;
  1391. color: #333;
  1392. }
  1393. }
  1394. }
  1395. .warning-content {
  1396. padding: 32rpx;
  1397. text-align: center;
  1398. cursor: pointer;
  1399. .warning-tip {
  1400. font-size: 26rpx;
  1401. color: #666;
  1402. }
  1403. }
  1404. .warning-overlay {
  1405. position: fixed;
  1406. top: 0;
  1407. left: 0;
  1408. right: 0;
  1409. bottom: 0;
  1410. background: rgba(0, 0, 0, 0.7);
  1411. z-index: 200;
  1412. display: flex;
  1413. align-items: center;
  1414. justify-content: center;
  1415. padding: 48rpx;
  1416. }
  1417. .warning-panel {
  1418. width: 100%;
  1419. max-height: 80vh;
  1420. background: #fff;
  1421. border-radius: 24rpx;
  1422. border: 1rpx solid #e0e0e0;
  1423. overflow: hidden;
  1424. display: flex;
  1425. flex-direction: column;
  1426. }
  1427. .warning-header {
  1428. display: flex;
  1429. justify-content: space-between;
  1430. align-items: center;
  1431. padding: 32rpx;
  1432. border-bottom: 1rpx solid #eee;
  1433. .warning-title {
  1434. font-size: 32rpx;
  1435. font-weight: bold;
  1436. color: #333;
  1437. }
  1438. .warning-close {
  1439. width: 48rpx;
  1440. height: 48rpx;
  1441. display: flex;
  1442. align-items: center;
  1443. justify-content: center;
  1444. font-size: 32rpx;
  1445. color: #999;
  1446. border-radius: 50%;
  1447. background: #f0f0f0;
  1448. }
  1449. }
  1450. .warning-body {
  1451. padding: 32rpx;
  1452. overflow-y: auto;
  1453. }
  1454. .warning-desc {
  1455. font-size: 24rpx;
  1456. color: #666;
  1457. margin-bottom: 32rpx;
  1458. }
  1459. .warning-score {
  1460. margin-bottom: 32rpx;
  1461. .warning-score-item {
  1462. display: flex;
  1463. justify-content: space-between;
  1464. align-items: center;
  1465. padding: 24rpx;
  1466. background: #f5f5f5;
  1467. border-radius: 16rpx;
  1468. .warning-score-label {
  1469. font-size: 28rpx;
  1470. color: #333;
  1471. }
  1472. .warning-score-value {
  1473. font-size: 40rpx;
  1474. font-weight: bold;
  1475. &.score-danger {
  1476. color: #EF4444;
  1477. }
  1478. &.score-excellent {
  1479. color: #34D399;
  1480. }
  1481. color: #60A5FA;
  1482. }
  1483. }
  1484. }
  1485. .warning-detail {
  1486. .warning-detail-title {
  1487. font-size: 28rpx;
  1488. color: #333;
  1489. margin-bottom: 20rpx;
  1490. font-weight: 500;
  1491. }
  1492. .warning-detail-item {
  1493. display: flex;
  1494. justify-content: space-between;
  1495. align-items: center;
  1496. padding: 16rpx 0;
  1497. border-bottom: 1rpx solid #f0f0f0;
  1498. .detail-left {
  1499. display: flex;
  1500. flex-direction: column;
  1501. gap: 4rpx;
  1502. .detail-dim {
  1503. font-size: 22rpx;
  1504. color: #999;
  1505. }
  1506. .detail-name {
  1507. font-size: 26rpx;
  1508. color: #333;
  1509. }
  1510. }
  1511. .detail-score {
  1512. font-size: 28rpx;
  1513. font-weight: 500;
  1514. &.add-text {
  1515. color: #34D399;
  1516. }
  1517. &.deduct-text {
  1518. color: #EF4444;
  1519. }
  1520. }
  1521. }
  1522. }
  1523. .radar-tooltip-popup {
  1524. padding: 20rpx;
  1525. min-width: 300px;
  1526. .p-empty {
  1527. text-align: center;
  1528. margin-top: 10rpx;
  1529. font-size: 24rpx;
  1530. color: #999;
  1531. }
  1532. .tooltip-header {
  1533. display: flex;
  1534. justify-content: space-between;
  1535. align-items: center;
  1536. padding: 0 10rpx 20rpx;
  1537. border-bottom: 1rpx solid #eee;
  1538. margin-bottom: 20rpx;
  1539. .tooltip-title {
  1540. font-size: 32rpx;
  1541. font-weight: bold;
  1542. color: #333;
  1543. }
  1544. }
  1545. .tooltip-section {
  1546. padding: 0 10rpx;
  1547. margin-bottom: 16rpx;
  1548. }
  1549. .list-item {
  1550. margin-top: 8rpx;
  1551. display: flex;
  1552. align-items: center;
  1553. justify-content: space-between;
  1554. font-size: 26rpx;
  1555. color: #333;
  1556. .deduct-text {
  1557. color: #ff4d4f;
  1558. }
  1559. .add-text {
  1560. color: #00b42a;
  1561. }
  1562. }
  1563. .section-total {
  1564. margin-top: 12rpx;
  1565. padding-top: 12rpx;
  1566. border-top: 1rpx solid #f0f0f0;
  1567. display: flex;
  1568. align-items: center;
  1569. justify-content: space-between;
  1570. font-weight: 500;
  1571. font-size: 28rpx;
  1572. &.deduct {
  1573. color: #ff4d4f;
  1574. }
  1575. &.add {
  1576. color: #00b42a;
  1577. }
  1578. }
  1579. }
  1580. </style>