| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775 |
- <template>
- <Page @click="visible = false" v-loading="loading" element-loading-background="rgba(22, 22, 22, 0.8)">
- <div class="content">
- <div class="content-search">
- <SearchBar v-model:visible="visible" @search="searchHandler" :deptType="'user'" />
- </div>
-
- <div v-show="portrait.personName" class="content-info">
- <Card title="个人基本信息">
- <div class="userInfo">
- <div class="userInfo-name">
- <div class="avatar-area">
- <img v-if="portrait.avatar" :src="portrait.avatar" class="avatar" alt="头像" />
- <div v-else class="avatar">{{ portrait.personName?.charAt(0) }}</div>
- </div>
- <div class="basic-name">
- <div class="basic-label">姓名:</div>
- <div class="basic-value">{{ portrait.personName }}</div>
- </div>
- </div>
- <div class="userInfo-info">
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img01.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">所属部门及队室:</div>
- <div class="info-item-value">{{ portrait.deptPath || '-' }}</div>
- </div>
- </div>
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img04.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">学历:</div>
- <div class="info-item-value">{{ getSchooling(portrait.schooling) }}</div>
- </div>
- </div>
- </div>
- <div class="userInfo-info">
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img02.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">出生日期:</div>
- <div class="info-item-value">{{ portrait.birthday || '-' }}</div>
- </div>
- </div>
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img05.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">专业:</div>
- <div class="info-item-value">{{ portrait.major || '-' }}</div>
- </div>
- </div>
- </div>
- <div class="userInfo-info">
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img03.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">技能等级:</div>
- <div class="info-item-value">{{ portrait.qualificationLevelText || '-' }}</div>
- </div>
- </div>
- <div class="info-item">
- <div class="info-item-icon">
- <img src="/src/assets/dataBigScreen/img06.png" alt="" />
- </div>
- <div class="info-item-content">
- <div class="info-item-label">标签:</div>
- <div class="info-item-value">
- <span class="info-item-tag" v-if="portrait.roleNames">{{ portrait.roleNames }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="userInfo-score">
- <div class="score-progress">
- <el-progress type="circle" :width="160" :stroke-width="18" color="#5BE39E" :percentage="(portrait.totalScore || -2) + 2">
- <div class="percentage-content">
- <span class="percentage-value">{{ (portrait.totalScore || -2) + 2 }}</span>
- <span class="percentage-text">综合得分</span>
- </div>
- </el-progress>
- </div>
- <div class="score-box">
- <div class="score-row">
- <span class="score-col">评分:</span>
- <span class="score-col-2">{{ portrait.totalScore || 0 }}</span>
- </div>
- <div class="score-row">
- <span class="score-col">标签得分:</span>
- <span class="score-col-2">{{ portrait.totalScore || portrait.totalScore == 0 ? 2 : 0 }}</span>
- </div>
- </div>
- </div>
- </div>
- </Card>
- </div>
- <div v-show="portrait.personName" class="content-bottom">
- <div class="content-bottom-left">
- <Card title="工作履历">
- <div class="work-history">
- <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>
- </Card>
- <Card title="获奖记录">
- <div class="honor-item" v-for="(value, index) in portrait.awards" :key="index">
- <div :style="{'--indexBgColor': value.color}">
- <div :data-index="index + 1"></div>
- {{ value.level2Name }} {{ value.level4Name }}
- </div>
- <div>{{ value.score || '-' }}分</div>
- </div>
- </Card>
- </div>
- <div class="content-bottom-center">
- <Card title="个人能力">
- <div ref="abilityChart" class="chart-box" />
- </Card>
- </div>
- <div class="content-bottom-right">
- <Card title="补充信息">
- <div class="supp-item">
- <span class="s-lbl">政治面貌:</span>
- <span class="s-val">{{ portrait.politicalStatusText || '-' }}</span>
- </div>
- <div class="supp-item supp-item-2">
- <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.nativePlace || '-' }}</span>
- </div>
- <div class="supp-item supp-item-2">
- <span class="s-lbl">民族:</span>
- <span class="s-val">{{ portrait.nation || '-' }}</span>
- </div>
- <div class="supp-item">
- <span class="s-lbl">年龄:</span>
- <span class="s-val">{{ getAge(portrait.birthday) }}</span>
- </div>
- <div class="supp-item supp-item-2">
- <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.characterCharacteristics || '-' }}</span>
- </div>
- <div class="supp-item supp-item-2">
- <span class="s-lbl">工作风格:</span>
- <span class="s-val">{{ portrait.workingStyle || '-' }}</span>
- </div>
- <div :class="i % 2 ? 'supp-item' : 'supp-item supp-item-2'" v-for="(p, i) in (portrait.postNames || '').split('、')" :key="i">
- <span class="s-lbl">{{ i === 0 ? '业务岗位:' : ''}}</span>
- <span class="s-val">{{ p }}</span>
- </div>
- </Card>
- </div>
- </div>
- <div v-show="!portrait.personName" class="ep-empty">
- <el-empty description="搜索员工姓名以查看画像" />
- </div>
- </div>
- <div class="radar-tooltip" ref="tipRef">
- <Card>
- <!-- 标题 -->
- <div class="tooltip-title">{{ activeDimName }}</div>
- <!-- 加分区域 -->
- <div class="tooltip-section">
- <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 class="tooltip-section">
- <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>
- </Card>
- </div>
- </Page>
- </template>
- <script setup>
- import Page from '../components/page.vue'
- import Card from '../components/card.vue'
- import SearchBar from '../components/SearchBar.vue'
- import { getEmployeePortrait } from '@/api/score/index'
- import { onMounted, reactive, ref } from 'vue'
- import { useDict } from '@/utils/dict'
- import { useECharts } from '@/hooks/useEcharts'
- const { sys_user_schooling } = useDict('sys_user_schooling')
- const visible = ref(false)
- const loading = ref(false)
- const portrait = ref({ dimensions: [] })
- const abilityChart = ref(null)
- 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 getSchooling = (schooling) => {
- const result = (sys_user_schooling.value || []).find(item => item.value === schooling) || { label: schooling }
- return result.label || '-'
- }
- const getAge = (birthday) => {
- const birthDate = new Date(birthday);
- if (isNaN(birthDate.getTime())) {
- return '-'
- }
- const today = new Date();
- // 计算年份差
- let age = today.getFullYear() - birthDate.getFullYear();
- // 计算月份差
- const monthDiff = today.getMonth() - birthDate.getMonth();
- // 如果还没到今年生日,年龄减1
- if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
- age--;
- }
- return `${age}岁`;
- }
- const getRandomHexColor = () => {
- return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0');
- }
- 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 invokerEmployeePortrait = (query) => {
- loading.value = true
- return getEmployeePortrait(query).then(res => {
- portrait.value = res.data || { dimensions: [], awards: [] }
- portrait.value.awards.forEach(item => {
- item.color = getRandomHexColor()
- })
- permutationRadarDataHandler(res.data.dimensions)
- }).finally(() => {
- loading.value = false
- })
- }
- const searchHandler = (query) => {
- visible.value = false
- invokerEmployeePortrait(query)
- }
- const radarData = reactive({
- grounp: [],
- data: []
- })
- const permutationRadarDataHandler = (result = []) => {
- radarData.grounp = result.map(d => ({ name: d.name + '\n\n' + d.score, max: 100 })),
- radarData.data = [{
- name: '个人能力',
- value: result.map(attr => attr.score),
- symbolSize: 10,
- areaStyle: {
- show: false,
- opacity: 0,
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [{
- offset: 0, color: '#4DC8FE'
- }, {
- offset: 1, color: '#6C26F3'
- }],
- global: false
- },
- },
- lineStyle: {
- color: {
- type: 'linear',
- x: 0,
- y: 0,
- x2: 0,
- y2: 1,
- colorStops: [{
- offset: 0, color: '#4DC8FE'
- }, {
- offset: 1, color: '#6C26F3'
- }],
- global: false
- },
- width: 5
- },
- itemStyle: { color: '#fff', borderWidth: 1, borderColor: '#00C8DA', borderJoin: 'round' }
- }]
- }
- const setRadarOptions = computed(() => {
- return {
- radar: {
- indicator: radarData.grounp,
- center: ['50%', '52%'],
- radius: '65%',
- splitNumber: 8,
- axisLine: { lineStyle: { color: '#ccc' } },
- splitLine: { lineStyle: { color: ['#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#ccc', '#fe4322', '#8EC742', '#ccc'], width: 3 } },
- splitArea: { show: false },
- axisName: {
- color: '#fff',
- fontSize: 18
- },
- },
- series: [
- {
- type: 'radar',
- data: radarData.data,
- symbol: 'circle',
- symbolSize: 6,
- label: {
- show: false,
- formatter: (p) => p.value,
- color: '#fff',
- fontSize: 16,
- fontWeight: 'bold'
- }
- }
- ]
- }
- })
- let pisition = {
- x: 0,
- y: 0
- }
- const tipRef = ref(null)
- useECharts(abilityChart, setRadarOptions).bindEvent('mousemove', (e) => {
- if (!Array.isArray(portrait.value.dimensions) || !portrait.value.dimensions.length) return
- const rect = abilityChart.value.getBoundingClientRect()
- if (!tipRef.value) 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.35 // 对应 radius: 65%
- if (distance > radarRadius) {
- tipRef.value.style.display = 'none'
- return
- }
- // 角度计算
- let angle = Math.atan2(dx, -dy) * (180 / Math.PI)
- if (angle < 0) angle += 360
- const count = portrait.value.dimensions.length
- const step = 360 / count
- let idx = Math.abs(Math.round(angle / step) % count - count)
- if (idx < 0) idx += count
- if (idx >= count) idx = 0
- // 显示 tooltip
- tipRef.value.style.display = 'block'
- tipRef.value.style.left = pisition.x + 30 + 'px'
- tipRef.value.style.top = pisition.y + 'px'
- activeDimName.value = portrait.value.dimensions[idx].name
- }).bindEvent('mouseout', () => {
- if (tipRef.value) tipRef.value.style.display = 'none'
- })
- onMounted(() => {
- window.addEventListener('mousemove', (eve) => {
- pisition.x = eve.pageX
- pisition.y = eve.pageY
- })
- })
- </script>
- <style lang="scss" scoped>
- .content {
- height: calc(100vh - 90px);
- display: flex;
- flex-direction: column;
- row-gap: 15px;
- padding: 15px;
- box-sizing: border-box;
- overflow: hidden;
- .ep-empty {
- background: #fff;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- background: #21213a;
- opacity: 0.8;
- border-radius: 30px;
- }
- .content-search {}
- .content-info {
- & > * {
- height: 100%;
- }
- .userInfo {
- display: flex;
- width: 100%;
- height: 180px;
- box-sizing: border-box;
- column-gap: 5px;
- .userInfo-name {
- flex: 1;
- position: relative;
- display: flex;
- align-items: center;
- justify-content: center;
- column-gap: 30px;
- &::after {
- content: '';
- display: block;
- width: 1px;
- background:linear-gradient(180deg, transparent, #6f6c98, transparent);
- height: 100%;
- position: absolute;
- top: 0;
- right: -2px;
- }
- .avatar-area {
- height: 140px;
- width: 140px;
- background: #fff;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- .avatar {
- width: 100%;
- height: 100%;
- display: flex;
- align-items: center;
- justify-content: center;
- color: #6f6c98;
- font-size: 48px;
- font-weight: 900;
- }
- }
- .basic-name {
- display: flex;
- flex-direction: column;
- justify-content: center;
- row-gap: 10px;
- font-weight: 500;
- .basic-label {
- font-size: 18px;
- color: #8675AE;
- }
- .basic-value {
- font-size: 26px;
- color: #fff;
- }
- }
- }
- .userInfo-info {
- flex: 1;
- position: relative;
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- padding: 10px 15px;
- padding-right: 0px;
- box-sizing: border-box;
- &::after {
- content: '';
- display: block;
- width: 1px;
- background:linear-gradient(180deg, transparent, #6f6c98, transparent);
- height: 100%;
- position: absolute;
- top: 0;
- right: -2px;
- }
- .info-item {
- display: flex;
- column-gap: 15px;
- .info-item-content {
- display: flex;
- flex-direction: column;
- justify-content: space-between;
- font-weight: 500;
- .info-item-label {
- font-size: 18px;
- color: #8675AE;
- }
- .info-item-value {
- font-size: 18px;
- color: #fff;
- display: flex;
- align-items: center;
- flex-wrap: wrap;
- }
- .info-item-tag {
- font-size: 14px;
- display: block;
- padding: 3px 15px;
- background: linear-gradient(125deg, #2AB3E6, #9605FC);
- border-radius: 20px;
- }
- }
- }
- }
- .userInfo-score {
- :deep(.el-progress-circle) {
- width: 160px;
- height: 160px;
- --el-fill-color-light: #004970;
- transform: rotate(0.5turn);
- }
- flex: 1;
- display: flex;
- align-items: center;
- padding-left: 15px;
- column-gap: 25px;
- .score-progress {
- .percentage-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- row-gap: 6px;
- font-weight: 500;
- .percentage-value {
- font-size: 48px;
- color: #02E5CC;
- }
- .percentage-text {
- font-size: 16px;
- color: #fff;
- }
- }
- }
- .score-box {
- display: flex;
- flex-direction: column;
- font-weight: 500;
- row-gap: 15px;
- font-size: 18px;
- .score-col {
- display: inline-block;
- width: 5em;
- }
- .score-col-2 {
- font-size: 24px;
- }
- }
- }
- }
- }
- .content-bottom {
- flex: 1;
- display: flex;
- column-gap: 15px;
- .content-bottom-left {
- width: 380px;
- display: flex;
- flex-direction: column;
- row-gap: 15px;
- & > *:nth-child(1) {
- flex: 1;
- .work-history {
- font-size: 18px;
- }
- }
- & > *:nth-child(2) {
- flex: 1.6;
- .honor-item {
- height: 48px;
- padding-right: 15px;
- box-sizing: border-box;
- font-weight: 500;
- font-size: 18px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- position: relative;
- & > div:nth-child(1) {
- display: flex;
- align-items: center;
- column-gap: 15px;
- & > div {
- width: 26px;
- height: 20px;
- position: relative;
- font-size: 18px;
- text-align: center;
- line-height: 20px;
- color: #222;
- &::before {
- position: absolute;
- inset: 0;
- content: '';
- display: block;
- background: var(--indexBgColor);
- width: 100%;
- height: 100%;
- transform: skew(-25deg);
- }
- &::after {
- position: absolute;
- inset: 0;
- content: attr(data-index);
- display: block;
- width: 100%;
- height: 100%;
- z-index: 100;
- }
- }
- }
- & > div:nth-child(2) {
- color: #02E5CC;
- }
- &::after {
- content: '';
- display: block;
- position: absolute;
- bottom: 0;
- left: 0;
- height: 1px;
- width: 100%;
- background:linear-gradient(45deg, transparent 0%, transparent 30%, #6f6c98 100%);
- }
- }
- }
- }
- .content-bottom-center {
- flex: 1;
- & > * {
- height: 100%;
- }
- .chart-box {
- width: 100%;
- height: 100%;
- overflow: hidden;
- }
- }
- .content-bottom-right {
- width: 380px;
- & > * {
- height: 100%;
- }
- .supp-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- height: 42px;
- padding: 0 15px;
- font-weight: 500;
- font-size: 18px;
- .s-lbl {
- color: #8675AE;
- }
- .s-val {
- color: #fff;
- }
- }
- .supp-item-2 {
- background: #261C48;
- }
- }
- }
- }
- /* Tooltip 样式(全部放这里) */
- .radar-tooltip {
- position: fixed;
- min-width: 300px;
- z-index: 999;
- pointer-events: none;
- transform: translateY(-80%);
- display: none;
- .p-empty {
- text-align: center;
- margin-top: 5px;
- }
- .tooltip-title {
- font-size: 18px;
- font-weight: bold;
- color: #fff;
- padding: 0 10px;
- }
- .tooltip-section {
- padding: 0 10px;
- }
- .list-item {
- margin-top: 4px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- color: #fff;
- .deduct-text {
- color: #ff4d4f;
- }
- .add-text {
- color: #00b42a;
- }
- }
- .section-total {
- margin-top: 4px;
- display: flex;
- align-items: center;
- justify-content: space-between;
- font-weight: 500;
- font-size: 16px;
- &.deduct {
- color: #ff4d4f;
- }
- &.add {
- color: #00b42a;
- }
- }
- }
- </style>
|