| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274 |
- <template>
- <div class="ep-root">
- <!-- 顶部工具栏 -->
- <div class="ep-topbar">
- <div class="time-btns">
- <el-button size="small" :type="currentTime==='week'?'primary':'default'" @click="selectTime('week')">近一周</el-button>
- <el-button size="small" :type="currentTime==='month'?'primary':'default'" @click="selectTime('month')">近一月</el-button>
- <el-button size="small" :type="currentTime==='quarter'?'primary':'default'" @click="selectTime('quarter')">近三月</el-button>
- <el-button size="small" :type="currentTime==='year'?'primary':'default'" @click="selectTime('year')">近一年</el-button>
- <el-button size="small" :type="currentTime==='custom'?'primary':'default'" @click="selectTime('custom')">自定义时间范围</el-button>
- <el-date-picker
- v-if="currentTime==='custom'"
- v-model="dateRange"
- type="daterange"
- range-separator="至"
- start-placeholder="开始"
- end-placeholder="结束"
- size="small"
- style="margin-left:8px;width:240px"
- @change="loadPortrait"
- />
- </div>
- <el-autocomplete
- v-model="personName"
- :fetch-suggestions="queryUsers"
- placeholder="搜索员工/团队画像"
- size="small"
- style="width:220px"
- @select="handlePersonSelect"
- @clear="portrait=null"
- clearable
- >
- <template #suffix><el-icon><Search /></el-icon></template>
- <template #default="{ item }">
- <span>{{ item.nickName }}</span>
- <span v-if="item.deptName" style="font-size:12px;color:#999;margin-left:8px">{{ item.deptName }}</span>
- </template>
- </el-autocomplete>
- </div>
- <!-- 空状态 -->
- <div v-if="!portrait" class="ep-empty">
- <el-empty description="请在右上角搜索员工姓名以查看画像" />
- </div>
- <!-- 主内容 -->
- <div v-else v-loading="loading" class="ep-body" ref="content">
- <!-- 左侧主区 -->
- <div class="ep-left">
- <!-- 第一行:基本信息卡 + 综合评价 -->
- <div class="row-flex gap">
- <!-- 基本信息 -->
- <div class="card basic-card">
- <div class="avatar-area">
- <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
- <div v-else class="avatar-fallback">{{ portrait.personName?.charAt(0) }}</div>
- </div>
- <div class="basic-detail">
- <div class="basic-detail-row">
- <div class="basic-detail-col basic-name">
- <div class="basic-label">姓名:</div>
- <div class="basic-value">{{ portrait.personName }}</div>
- </div>
- </div>
- <div class="basic-detail-row">
- <div class="basic-detail-col">
- <div class="basic-label">所属部门及队室:</div>
- <div class="basic-value">{{ portrait.deptPath || '-' }}</div>
- </div>
- <div class="basic-detail-col">
- <div class="basic-label">出生日期:</div>
- <div class="basic-value">{{ portrait.birthday || '-' }}</div>
- </div>
- </div>
- <div class="basic-detail-row">
- <div class="basic-detail-col">
- <div class="basic-label">技能等级:</div>
- <div class="basic-value">{{ portrait.qualificationLevelText || '-' }}</div>
- </div>
- </div>
- <div class="basic-detail-row">
- <div class="basic-detail-col">
- <div class="basic-label">学历:</div>
- <div class="basic-value">{{ portrait.schooling || '-' }}</div>
- </div>
- <div class="basic-detail-col">
- <div class="basic-label">专业:</div>
- <div class="basic-value">{{ portrait.major || '-' }}</div>
- </div>
- </div>
- <div class="basic-detail-row">
- <div class="basic-detail-col">
- <div class="basic-label">标签:</div>
- <el-tag size="small" type="primary" style="margin-right:6px" v-if="portrait.roleNames">{{ portrait.roleNames }}</el-tag>
- <el-tag size="small" type="warning" v-if="portrait.postNames">{{ portrait.postNames }}</el-tag>
- </div>
- </div>
- <!--
- <div class="basic-row2" v-if="portrait.startWorkingDate">
- <span><span class="lbl">入职日期:</span><span class="val">{{ portrait.startWorkingDate }}</span></span>
- <span v-if="portrait.phonenumber"><span class="lbl">电话:</span><span class="val">{{ portrait.phonenumber }}</span></span>
- </div>
- -->
- </div>
- </div>
- <!-- 综合评价 -->
- <div class="card score-card">
- <div class="score-label">综合评价</div>
- <div class="score-num">{{ portrait.totalScore }}</div>
- <!-- <div class="score-level-tag">{{ scoreLevel }}</div> -->
- </div>
- </div>
- <!-- 第二行:工作履历+获奖 / 个人能力雷达 -->
- <div class="row-flex gap" style="flex:1; overflow: hidden;">
- <!-- 工作履历 & 获奖记录 -->
- <div class="card left-mid-card">
- <div class="sec-title">工作履历</div>
- <div class="history-list">
- <div class="history-item">
- <span v-if="portrait.startWorkingDate">
- {{ formatWorkDate(portrait.startWorkingDate) }}入职
- | 司龄{{ portrait.workYears != null ? portrait.workYears : '-' }}年
- | 开机年限{{ portrait.securityCheckYears != null ? portrait.securityCheckYears : '-' }}年
- | 现任职{{ portrait.roleNames || '-' }}
- </span>
- <span v-else>暂无数据</span>
- </div>
- </div>
- <div class="sec-title">获奖记录</div>
- <div class="award-table-wrap">
- <div class="award-table" v-if="portrait.awards && portrait.awards.length">
- <div class="award-table-row level1" v-for="(aw, i) in portrait.awards" :key="i">
- <div class="award-table-col">{{ aw.level3Name || '-' }}</div>
- <div class="award-table-col">{{ aw.level2Name || '-' }}</div>
- <div class="award-table-col">{{ aw.level4Name || '-' }}</div>
- <div class="award-table-col">{{ aw.score }}</div>
- </div>
- </div>
- <div v-else class="empty-text mt8">暂无获奖记录</div>
- </div>
- </div>
- <!-- 个人能力雷达 -->
- <div class="card radar-card">
- <div class="sec-title">个人能力</div>
- <div class="radar-body">
- <!-- 扣分明细浮窗(点击维度后显示,绝对定位左侧) -->
- <!-- <transition name="panel-fade">
- <div class="score-panel deduct-panel" v-if="activeDimName">
- <div class="panel-header deduct-header">
- 扣分明细<span class="panel-dim-name">{{ activeDimName }}</span>
- <span class="panel-total">合计 {{ deductTotal }}</span>
- </div>
- <div class="panel-list">
- <div v-for="(item, i) in deductList" :key="i" class="panel-row">
- <span class="p-name">{{ item.level3Name || item.level2Name }}</span>
- <span class="p-score deduct-score">{{ item.totalScore }}</span>
- </div>
- <div v-if="!deductList.length" class="p-empty">暂无扣分记录</div>
- </div>
- </div>
- </transition> -->
- <!-- 雷达图 -->
- <div ref="abilityChart" class="chart-box">
- <!-- <div style="position: absolute;" v-if="activeDimName">
- <div style="padding: 12px; font-size: 14px; line-height: 1.6;">
- <div style="font-weight: bold; margin-bottom: 8px; font-size: 16px;">
- {{ activeDimName }}
- </div>
- <div style="margin-bottom: 10px;">
- <div style="color: #f56c6c; font-weight: bold;">扣分详情 (PENALTIES)</div>
- <ul style="margin: 6px 0; padding-left: 20px; color: #666;">
- <li>不安全事件(一类): -10.0分</li>
- <li>不安全事件(二类): -8.0分</li>
- <li>三/四/五类事件: -5/-3/-2分</li>
- </ul>
- <div style="text-align: right; color: #f56c6c; font-weight: bold;">合计扣分: 30.3分</div>
- </div>
- <div>
- <div style="color: #67c23a; font-weight: bold;">加分详情 (BONUSES)</div>
- <ul style="margin: 6px 0; padding-left: 20px; color: #666;">
- <li>一类查获/隐患上报: +1.0分</li>
- <li>建议采纳/二类查获: +1.0/+0.5分</li>
- <li>特殊物品查获: +0.15分</li>
- </ul>
- <div style="text-align: right; color: #67c23a; font-weight: bold;">合计加分: 4.3分</div>
- </div>
- </div>
- </div> -->
- </div>
-
- <!-- 加分明细浮窗(点击维度后显示,绝对定位右侧) -->
- <!-- <transition name="panel-fade">
- <div class="score-panel add-panel" v-if="activeDimName">
- <div class="panel-header add-header">
- 加分明细<span class="panel-dim-name">{{ activeDimName }}</span>
- <span class="panel-total">合计 {{ addTotal }}</span>
- </div>
- <div class="panel-list">
- <div v-for="(item, i) in addList" :key="i" class="panel-row">
- <span class="p-name">{{ item.level3Name || item.level2Name }}</span>
- <span class="p-score add-score">+{{ item.totalScore }}</span>
- </div>
- <div v-if="!addList.length" class="p-empty">暂无加分记录</div>
- </div>
- </div>
- </transition> -->
- </div>
- <div class="radar-hint" v-if="!activeDimName">
- {{ activeDimName }}
- <!-- 点击雷达图维度查看明细 -->
- </div>
- </div>
- </div>
- </div>
- <!-- 右侧边栏:一个大卡片 -->
- <div class="ep-right">
- <div class="side-big-card">
- <!-- 补充信息 -->
- <div class="side-section">
- <div class="side-title">补充信息</div>
- <div class="supp-grid supp-info-bg">
- <div class="supp-item">
- <span class="s-lbl">政治面貌</span>
- <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">性别</span>
- <span class="s-val">{{ portrait.sexText || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">籍贯</span>
- <span class="s-val">{{ portrait.sexText || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">民族</span>
- <span class="s-val">{{ portrait.sexText || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">年龄</span>
- <span class="s-val">{{ portrait.sexText || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">司龄</span>
- <span class="s-val">{{ portrait.workYears != null ? portrait.workYears+'年' : '-' }}</span>
- </div>
- <!-- <div class="supp-item">
- <span class="s-lbl">开机年限</span>
- <span class="s-val">{{ portrait.securityCheckYears != null ? portrait.securityCheckYears+'年' : '-' }}</span>
- </div> -->
- <div class="supp-item">
- <span class="s-lbl">性格特征</span>
- <span class="s-val">{{ portrait.characterCharacteristics || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">工作风格</span>
- <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
- </div>
- </div>
- </div>
- <!-- 业务岗位 -->
- <div class="side-section pos-section">
- <div class="side-title">业务岗位</div>
- <div class="supp-grid pos-info-bg">
- <div class="supp-item" v-for="(p, i) in positionList" :key="i">{{ p }}</div>
- <div v-if="!positionList.length" class="empty-text">暂无岗位信息</div>
- </div>
- </div>
- <!-- 培训情况 -->
- <div class="side-section">
- <div class="side-title">培训情况</div>
- <div class="supp-grid train-info-bg">
- <div class="train-info-table" v-if="portrait.theoryScore != null || portrait.imageScore != null">
- <div class="train-row train-header">
- <span>季度</span><span>理论成绩</span><span>图像成绩</span>
- </div>
- <div class="train-row">
- <span>{{ portrait.examPeriod || '-' }}</span>
- <span :style="examScoreStyle(portrait.theoryScore)">
- {{ portrait.theoryScore != null ? portrait.theoryScore+'分' : '-' }}
- </span>
- <span :style="examScoreStyle(portrait.imageScore)">
- {{ portrait.imageScore != null ? portrait.imageScore+'分' : '-' }}
- </span>
- </div>
- </div>
- <div v-else class="empty-text">暂无考试记录</div>
- </div>
- </div>
- <!-- 质控情况 -->
- <div class="side-section">
- <div class="side-title">质控情况</div>
- <div class="supp-grid qc-info-bg">
- <div class="supp-item">
- <span class="s-lbl">查获违规品次数</span>
- <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">监察问题记录</span>
- <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">三级质控巡查记录</span>
- <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">实时质控拦截记录</span>
- <span class="s-val">{{ portrait.qualityControlCount ?? '-' }}</span>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <!-- 自定义 Tooltip(放在 template 里) -->
- <div
- class="radar-tooltip"
- ref="tipRef"
- >
- <!-- 标题 -->
- <div class="tooltip-title">{{ activeDimName }}</div>
- <!-- 扣分区域 -->
- <div class="tooltip-section">
- <div class="section-title deduct">扣分明细</div>
- <div class="section-list">
- <div v-if="deductList.length" class="list-item" v-for="(item, i) in deductList" :key="i">
- <span>{{ item.level3Name }}:</span>
- <span class="deduct-text">{{ item.totalScore }}分</span>
- </div>
- <div v-if="!deductList.length" class="p-empty">暂无扣分记录</div>
- </div>
- <div class="section-total deduct">
- <span>合计扣分:</span>
- <span>{{ deductTotal }}分</span>
- </div>
- </div>
- <!-- 加分区域 -->
- <div class="tooltip-section">
- <div class="section-title add">加分明细</div>
- <div class="section-list">
- <div v-if="addList.length" class="list-item" v-for="(item, i) in addList" :key="i">
- <span>{{ item.level3Name }}:</span>
- <span class="add-text">{{ item.totalScore }}分</span>
- </div>
- <div v-else class="p-empty">暂无加分记录</div>
- </div>
- <div class="section-total add">
- <span>合计加分:</span>
- <span>{{ addTotal }}分</span>
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed, watch, onMounted, onBeforeUnmount, nextTick } from 'vue'
- import * as echarts from 'echarts'
- import { Search } from '@element-plus/icons-vue'
- import { useRoute } from 'vue-router'
- import { searchPortraitUsers, getEmployeePortrait } from '@/api/score/index'
- defineOptions({ name: 'EmployeeProfile' })
- const route = useRoute()
- const currentTime = ref('year')
- const dateRange = ref([])
- const personName = ref('')
- const portrait = ref(null)
- const loading = ref(false)
- const abilityChart = ref(null)
- let chart = null
- const formatDate = (d) => {
- const dt = new Date(d)
- return `${dt.getFullYear()}-${String(dt.getMonth()+1).padStart(2,'0')}-${String(dt.getDate()).padStart(2,'0')}`
- }
- const getTimeRange = () => {
- if (currentTime.value === 'custom') {
- if (dateRange.value?.length === 2) {
- return { beginTime: formatDate(dateRange.value[0]), endTime: formatDate(dateRange.value[1]) }
- }
- return {}
- }
- const end = new Date(), begin = new Date()
- if (currentTime.value === 'week') begin.setDate(end.getDate()-7)
- else if (currentTime.value === 'month') begin.setMonth(end.getMonth()-1)
- else if (currentTime.value === 'quarter') begin.setMonth(end.getMonth()-3)
- else begin.setFullYear(end.getFullYear()-1)
- return { beginTime: formatDate(begin), endTime: formatDate(end) }
- }
- const selectTime = (t) => {
- currentTime.value = t
- if (t !== 'custom') loadPortrait()
- }
- const queryUsers = async (query, cb) => {
- if (!query?.trim()) { cb([]); return }
- try {
- const res = await searchPortraitUsers(query.trim())
- cb((res.data || []).map(u => ({ ...u, value: u.nickName })))
- } catch (_) { cb([]) }
- }
- const handlePersonSelect = (item) => {
- personName.value = item.nickName
- loadPortrait()
- }
- const loadPortrait = async () => {
- if (!personName.value) return
- loading.value = true
- try {
- const res = await getEmployeePortrait({ personName: personName.value, ...getTimeRange() })
- portrait.value = res.data || null
- // portrait.value.awards = Array(30).fill(portrait.value.awards[0])
- } catch (_) {
- portrait.value = null
- } finally {
- loading.value = false
- }
- }
- const scoreColor = computed(() => {
- const s = Number(portrait.value?.totalScore)
- if (s < 75) return 'color:#f56c6c'
- if (s > 85) return 'color:#67c23a'
- return 'color:#ff9900'
- })
- const scoreLevel = computed(() => {
- const s = Number(portrait.value?.totalScore)
- if (s < 75) return '较差'
- if (s > 85) return '优秀'
- return '良好'
- })
- const positionList = computed(() => {
- const pos = portrait.value?.securityInspectionPosition
- if (!pos) return []
- return pos.split(/[,,、/]/).map(s => s.trim()).filter(Boolean)
- })
- const activeDimName = ref(null)
- const scoreDetails = computed(() => portrait.value?.scoreDetails || [])
- const addList = computed(() => {
- const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) > 0)
- return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
- })
- const deductList = computed(() => {
- const all = scoreDetails.value.filter(d => d.totalScore != null && Number(d.totalScore) < 0)
- return activeDimName.value ? all.filter(d => d.dimensionName === activeDimName.value) : all
- })
- const addTotal = computed(() => {
- const s = addList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
- return (s > 0 ? '+' : '') + s.toFixed(2)
- })
- const deductTotal = computed(() => {
- const s = deductList.value.reduce((acc, d) => acc + Number(d.totalScore), 0)
- return s.toFixed(2)
- })
- const formatWorkDate = (d) => {
- if (!d) return '-'
- const str = String(d)
- const parts = str.substring(0, 10).split('-')
- if (parts.length < 3) return str
- return `${parts[0]}.${Number(parts[1])}.${Number(parts[2])}`
- }
- const examScoreStyle = (score) => {
- const s = Number(score)
- if (s >= 90) return 'color:#67c23a;font-weight:600'
- if (s < 60) return 'color:#f56c6c;font-weight:600'
- return 'color:#409eff'
- }
- const radarColor = computed(() => {
- const total = Number(portrait.value?.totalScore ?? 0)
- if (total < 75) return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
- if (total >= 90) return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
- return { line: '#28ABE2', fill: '#5BBCE7', label: '#000' }
- })
- const tipRef = ref(null)
- const initChart = () => {
- if (!abilityChart.value || !portrait.value) return
- if (chart) { chart.dispose(); chart = null }
- chart = echarts.init(abilityChart.value)
- const dims = portrait.value.dimensions || []
- console.log(dims);
-
- const c = radarColor.value
- chart.setOption({
- radar: {
- indicator: dims.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
- center: ['50%', '52%'],
- radius: '65%',
- splitNumber: 4,
- axisLine: { lineStyle: { color: '#8EC742' } },
- splitLine: { lineStyle: { color: '#8EC742' } },
- splitArea: { show: false },
- axisName: {
- color: '#000',
- fontSize: 16
- },
- },
- series: [{
- type: 'radar',
- data: [{
- value: dims.map(d => Number(d.score)),
- name: '个人能力',
- areaStyle: { color: c.fill },
- lineStyle: { color: c.line, width: 3 },
- itemStyle: { color: c.line }
- }],
- symbol: 'circle',
- symbolSize: 6,
- label: {
- show: false,
- formatter: (p) => p.value,
- color: c.label,
- fontSize: 12,
- fontWeight: 'bold'
- }
- }],
- })
- const tip = tipRef.value
-
- chart.getZr().on('mousemove', (e) => {
- const rect = abilityChart.value.getBoundingClientRect()
- if (!tip) return
- const cx = rect.width / 2
- const cy = rect.height / 2
- const dx = e.offsetX - cx
- const dy = e.offsetY - cy
- const distance = Math.sqrt(dx * dx + dy * dy)
- const radarRadius = rect.width * 0.325 // 对应 radius: 65%
- if (distance > radarRadius) {
- tip.style.display = 'none'
- return
- }
- // 角度计算
- let angle = Math.atan2(dx, -dy) * (180 / Math.PI)
- if (angle < 0) angle += 360
- const count = dims.length
- const step = 360 / count
- let idx = Math.round(angle / step) % count
- if (idx < 0) idx += count
- // 显示 tooltip
- tip.style.display = 'block'
-
- // tip.style.left = rect.x + e.offsetX + 30 + 'px'
- // tip.style.top = rect.y + e.offsetY + 'px'
- tip.style.left = pisition.x + 30 + 'px'
- tip.style.top = pisition.y + 'px'
- activeDimName.value = dims[idx].name
- })
- chart.getZr().on('mouseout', () => {
- if (tip) tip.style.display = 'none'
- })
- }
- watch(portrait, (val) => {
- activeDimName.value = null
- if (val) nextTick(() => initChart())
- })
- const scaleValue = ref(1)
- const handleResize = () => {
- function setCSSZoom(scale) {
- scaleValue.value = scale
- const height = ((content.value.offsetHeight - (110 * scale)) / scale)
- content.value.style.setProperty('height', height + 'px')
- content.value.style.zoom = scale;
- }
- if (window.innerWidth < 1280) {
- setCSSZoom(0.5)
- }
- if (window.innerWidth > 1280 && window.innerWidth <= 1440) {
- setCSSZoom(0.65)
- }
- if (window.innerWidth > 1440 && window.innerWidth <= 1680) {
- setCSSZoom(0.7)
- }
- if (window.innerWidth > 1680 && window.innerWidth < 1920) {
- setCSSZoom(0.9)
- }
- if (window.innerWidth >= 1920) {
- setCSSZoom(1)
- }
- chart?.resize()
- }
- const content = ref(null)
- let pisition = {
- x: 0,
- y: 0
- }
- onMounted(() => {
- window.addEventListener('resize', handleResize)
- const n = route.query.personName
- if (n) { personName.value = n; loadPortrait() }
- window.addEventListener('mousemove', (eve) => {
- pisition.x = eve.pageX
- pisition.y = eve.pageY
- })
- })
- onBeforeUnmount(() => {
- window.removeEventListener('resize', handleResize)
- chart?.dispose(); chart = null
- })
- </script>
- <style scoped lang="scss">
- .ep-root {
- height: 100vh;
- background: linear-gradient(180deg, #C5DDFF 0%, #E4FBF0 100%);
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
- gap: 12px;
- color: rgba(16, 16, 16, 1);
- line-height: 28px;
- --cardBgColor: rgba(255, 255, 255, 0.7)
- }
- /* ── 顶部工具栏 ── */
- .ep-topbar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- background: rgba(255, 255, 255, 1);
- // border-radius: 20px;
- padding: 10px 16px;
- box-shadow: 0px 5px 10px 2px rgba(0, 0, 0, 0.35);
- flex-shrink: 0;
- height: 70px;
- gap: 12px;
- flex-wrap: wrap;
- .time-btns {
- display: flex;
- align-items: center;
- gap: 6px;
- flex-wrap: wrap;
- }
- }
- /* ── 空状态 ── */
- .ep-empty {
- background: #fff;
- border-radius: 8px;
- padding: 60px;
- text-align: center;
- }
- /* ── 主体两栏 ── */
- .ep-body {
- display: flex;
- gap: 20px;
- flex: 1;
- min-height: 0;
- padding: 15px;
- padding-top: 0px;
- }
- .ep-left {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- gap: 20px;
- }
- .ep-right {
- width: 366px;
- flex-shrink: 0;
- display: flex;
- flex-direction: column;
- gap: 20px;
- font-size: 20px;
- color: rgba(16, 16, 16, 1);
- line-height: 28px;
- }
- /* ── 通用卡片 ── */
- .card {
- background: var(--cardBgColor);
- border-radius: 20px;
- box-sizing: border-box;
- .sec-title {
- font-size: 48px;
- font-weight: bold;
- line-height: 68px;
- color: rgba(31, 135, 232, 1);
- text-align: left;
- font-style: italic;
- margin-bottom: 8px;
- text-indent: 25px;
- display: flex;
- align-items: center;
- &::before {
- content: '';
- display: block;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- transform: translateX(15px);
- background: rgba(31, 135, 232, 1);
- }
- }
- }
- .row-flex {
- display: flex;
- }
- .gap { gap: 20px; }
- /* ── 基本信息卡 ── */
- .basic-card {
- flex: 3;
- min-width: 0;
- display: flex;
- gap: 24px;
- align-items: flex-start;
- background: #eef7ff;
- padding: 20px 14px;
-
- .avatar-area {
- width: 260px;
- height: 260px;
- border-radius: 130px;
- background-color: #1677FF;
- display: flex;
- justify-content: center;
- align-items: center;
- .avatar {
- width: 250px;
- height: 250px;
- border-radius: 125px;
- object-fit: cover;
- }
- .avatar-fallback {
- width: 250px;
- height: 250px;
- border-radius: 125px;
- background-color: rgba(229, 229, 229, 1);
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 80px;
- font-weight: bold;
- color: #aaa;
- }
- }
- .basic-detail {
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- height: 100%;
- padding-top: 20px;
- box-sizing: border-box;
- .basic-detail-row {
- display: flex;
- color: rgba(16,16,16,1);
- font-size: 20px;
- text-align: left;
- font-weight: bold;
- width: 100%;
- .basic-detail-col {
- flex: 1;
- display: flex;
- align-items: center;
- line-height: 28px;
- .basic-label {
- }
- .basic-value {
- flex: 1;
- }
- }
- .basic-detail-col:nth-child(2) {
- max-width: 260px;
- }
- .basic-name {
- font-style: italic;
- line-height: 80px;
- .basic-label {
- font-size: 48px;
- width: 154px;
- color: rgba(31, 135, 232, 1);
- }
- .basic-value {
- font-size: 48px;
- color: rgba(31, 135, 232, 1);
- }
- }
- }
- }
-
- }
- /* ── 综合评价卡 ── */
- .score-card {
- width: 410px;
- height: 345px;
- display: flex;
- box-sizing: border-box;
- padding: 40px 30px 0px;
- flex-direction: column;
- align-items: center;
- background: var(--cardBgColor);
- font-size: 20px;
- text-align: center;
- .score-label {
- font-size: 48px;
- width: 100%;
- font-weight: bold;
- font-style: italic;
- line-height: 68px;
- color: rgba(31, 135, 232, 1);
- text-align: left;
- margin-bottom: 40px;
- }
- .score-num {
- width: 320px;
- height: 126px;
- border-radius: 30px;
- background-color: rgba(118, 220, 198, 1);
- color: #F58C78;
- font-size: 72px;
- font-weight: bold;
- font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- text-align: center;
- display: flex;
- align-items: center;
- justify-content: center;
- line-height: 1;
- }
- .score-level-tag {
- margin-top: 12px;
- font-size: 20px;
- font-weight: 600;
- color: rgba(16, 16, 16, 1);
- background: #f5f5f5;
- padding: 4px 20px;
- border-radius: 10px;
- }
- }
- /* ── 工作履历 + 获奖 ── */
- .left-mid-card {
- width: 604px;
- flex-shrink: 0;
- background: var(--cardBgColor);
- color: rgba(16, 16, 16, 1);
- font-size: 20px;
- line-height: 28px;
- box-sizing: border-box;
- padding: 30px 10px;
- text-align: center;
- height: 100%;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- .mt16 { margin-top: 16px; }
- .mt8 { margin-top: 8px; }
- .history-list {
- background-color: rgba(236, 220, 220, 1);
- border-radius: 20px;
- padding: 20px;
- .history-item {
- font-size: 20px;
- font-weight: bold;
- color: #101010;
- line-height: 28px;
- text-align: left;
- padding: 10px 0;
- display: flex;
- align-items: center;
- }
- }
- .award-table-wrap {
- background-color: rgba(169,209,250,1);
- border-radius: 20px;
- padding: 20px;
- flex: 1;
- overflow: hidden;
- .award-table {
- height: 100%;
- overflow-y: auto;
- overflow-x: hidden;
- .award-table-row {
- display: flex;
- height: 48px;
- .award-table-col {
- flex: 1;
- text-align: center;
- line-height: 48px;
- }
- }
- .level1 {
- background-color: rgba(239,172,169,1);
- }
- .level2 {
- background-color: rgba(184,245,216,1);
- }
- .level3 {
- background-color: rgba(85,164,244,1);
- }
- }
- }
- }
- .award-section {
- margin-top: 16px;
- background-color: rgba(169, 209, 250, 1);
- border-radius: 20px;
- padding: 12px;
- font-size: 20px;
- font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- font-weight: bold;
- .sec-title {
- font-size: 48px;
- font-weight: bold;
- line-height: 68px;
- color: rgba(31, 135, 232, 1);
- text-align: left;
- font-family: 'YSBiaoTiHei-bold', 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- margin-bottom: 10px;
- }
- }
- /* ── 雷达图卡 ── */
- .radar-card {
- flex: 1;
- min-width: 0;
- display: flex;
- flex-direction: column;
- background: rgb(242, 249, 253);
- color: rgba(16, 16, 16, 1);
- font-size: 20px;
- line-height: 28px;
- text-align: center;
- .chart-box {
- width: 100%;
- height: 100%;
- min-height: 400px;
- position: relative;
- }
- }
- /* ── 右侧大卡片 ── */
- .side-big-card {
- flex: 1;
- display: flex;
- flex-direction: column;
- row-gap: 8px;
- overflow: hidden;
- background: var(--cardBgColor);
- padding: 8px;
- border-radius: 20px;
- }
- .side-section {
- display: flex;
- flex-direction: column;
- .side-title {
- font-size: 32px;
- font-weight: 900;
- line-height: 50px;
- color: rgba(31, 135, 232, 1);
- text-indent: 20px;
- text-align: left;
- font-style: italic;
- font-family: 'YSBiaoTiHei-bold', 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
- }
- }
- .pos-section {
- flex: 1;
- overflow: hidden;
- }
- /* 补充信息 */
- .supp-grid {
- display: flex;
- flex: 1;
- flex-direction: column;
- border-radius: 30px;
- padding: 5px 20px;
- font-size: 20px;
- font-family: 'AlibabaPuHuiTi', 'PingFang SC', 'Microsoft YaHei', sans-serif;
-
- .supp-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- font-size: 20px;
- padding: 4px 0;
- line-height: 28px;
- color: rgba(0,0,0,1);
- font-weight: bold;
- .s-lbl {
- flex: 1;
- color: rgba(105,103,103,1);
- }
- .s-val {
- flex: 1;
- text-align: left;
- }
- }
- }
- /* 补充信息内容 */
- .supp-info-bg {
- background-color: rgba(254, 233, 232, 1);
- .supp-item {
- border-top: 1px dashed #fff;
- &:first-child {
- border: none;
- }
- }
- }
- /* 业务岗位内容 */
- .pos-info-bg {
- background-color: rgba(236,220,236,1);
- row-gap: 10px;
- overflow-y: auto;
- flex: content;
- }
- /* 培训情况 */
- .train-info-bg {
- background-color: rgba(206,248,228,1);
- padding: 10px 20px;
- .train-info-table {
- display: table;
- .train-row{
- display: table-row;
- & > span {
- display: table-cell;
- text-align: center;
- font-size: 16px;
- }
- }
- .train-header{
- display: table-row;
- font-weight: bold;
- & > span {
- display: table-cell;
- min-width: 5em;
- text-align: center;
- font-size: 20px;
- }
- }
- }
- }
- /* 质控情况内容背景 */
- .qc-info-bg {
- background-color: #D5E7FD;
- .s-val {
- max-width: 100px;
- }
- }
- /* Tooltip 样式(全部放这里) */
- .radar-tooltip {
- position: fixed;
- background: rgba(100, 100, 100, 0.9);
- border-radius: 16px;
- padding: 14px 8px;
- min-width: 320px;
- z-index: 999;
- pointer-events: none;
- font-size: 14px;
- line-height: 1.5;
- transform: translateY(-50%);
- display: none;
- .tooltip-title {
- font-size: 18px;
- font-weight: bold;
- color: #fff;
- margin-bottom: 12px;
- }
- .tooltip-section {
- margin-bottom: 12px;
- background: #fff;
- border-radius: 12px;
- padding: 20px;
- &:last-child {
- margin-bottom: 0;
- }
- }
- .section-title {
- font-weight: 500;
- margin-bottom: 6px;
- font-size: 18px;
- text-align: left;
- &.deduct {
- color: #ff4d4f;
- }
- &.add {
- color: #00b42a;
- }
- }
- .list-item {
- display: flex;
- align-items: center;
- row-gap: 15px;
- column-gap: 20px;
- padding: 0 20px;
- .deduct-text {
- text-align: left;
- color: #ff4d4f;
- }
- .add-text {
- color: #00b42a;
- }
- }
- .section-total {
- display: flex;
- align-items: center;
- justify-content: flex-end;
- margin-top: 6px;
- font-weight: 500;
- column-gap: 20px;
- font-size: 16px;
- &.deduct {
- color: #ff4d4f;
- }
- &.add {
- color: #00b42a;
- }
- }
- }
- /* ── 雷达图主体(含两侧浮窗) ── */
- .radar-body {
- flex: 1;
- position: relative;
- min-height: 0;
- }
- /* ── 加/扣分浮窗(绝对定位,叠在 chart 上方) ── */
- .score-panel {
- position: absolute;
- top: 0;
- width: 180px;
- max-height: 100%;
- display: flex;
- flex-direction: column;
- border-radius: 12px;
- overflow: hidden;
- border: 1px solid #e0e7f0;
- font-size: 12px;
- background: rgba(255,255,255,0.92);
- box-shadow: 0 2px 8px rgba(0,0,0,0.12);
- z-index: 10;
- }
- .deduct-panel { left: 0; }
- .add-panel { right: 0; }
- .panel-header {
- padding: 6px 10px;
- font-size: 13px;
- font-weight: bold;
- display: flex;
- align-items: center;
- flex-shrink: 0;
- }
- .deduct-header { background: rgba(254,233,232,1); color: #e03030; }
- .add-header { background: rgba(206,248,228,1); color: #1a9944; }
- .panel-dim-name {
- font-size: 11px;
- font-weight: normal;
- margin-left: 4px;
- opacity: 0.8;
- max-width: 60px;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- }
- .panel-total {
- font-size: 12px;
- font-weight: bold;
- margin-left: auto;
- }
- .radar-hint {
- text-align: center;
- font-size: 12px;
- color: #aaa;
- margin-top: 4px;
- flex-shrink: 0;
- }
- .panel-fade-enter-active, .panel-fade-leave-active { transition: opacity 0.2s; }
- .panel-fade-enter-from, .panel-fade-leave-to { opacity: 0; }
- .panel-list {
- flex: 1;
- overflow-y: auto;
- padding: 4px 0;
- }
- .panel-row {
- display: flex;
- align-items: center;
- padding: 4px 8px;
- gap: 4px;
- border-bottom: 1px dashed #f0f0f0;
- &:last-child { border-bottom: none; }
- }
- .p-dim { color: #999; font-size: 11px; flex-shrink: 0; min-width: 28px; }
- .p-name { flex: 1; color: #444; font-size: 11px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
- .p-score { font-size: 12px; font-weight: bold; flex-shrink: 0; }
- .deduct-score { color: #e03030; }
- .add-score { color: #1a9944; }
- .p-empty { color: #bbb; font-size: 11px; text-align: center; padding: 12px 0; }
- /* 通用 */
- .empty-text { color: #bbb; font-size: 20px; text-align: center; padding: 8px 0; }
- </style>
|