| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381 |
- <template>
- <view class="employee-profile-page">
- <view class="page-header">
- <view class="header-title">
- <view class="title-main">员工综合信息展示</view>
- <view class="header-right">
- <view class="current-time">{{ currentTime }}</view>
- </view>
- </view>
- <view class="time-filter">
- <scroll-view scroll-x class="time-scroll">
- <view class="time-tags">
- <view v-for="(tag, index) in timeTags" :key="index"
- :class="['time-tag', { active: selectedTimeTag === index }]" @click="onTimeTagClick(index)">
- {{ tag }}
- </view>
- </view>
- </scroll-view>
- <view v-if="selectedTimeTag === 4" class="date-range-picker">
- <picker mode="date" :value="beginTime" @change="onBeginTimeChange">
- <view class="date-input" :class="{ filled: beginTime }">
- {{ (beginTime || '').substring(0, 10) || '开始日期' }}
- </view>
- </picker>
- <text class="date-separator">至</text>
- <picker mode="date" :value="endTime" @change="onEndTimeChange">
- <view class="date-input" :class="{ filled: endTime }">
- {{ (endTime || '').substring(0, 10) || '结束日期' }}
- </view>
- </picker>
- </view>
- </view>
- <view class="search-selector">
- <view class="search-select-trigger" :class="{ 'search-select-trigger-disabled': isSecurityCheck }"
- @click="!isSecurityCheck && (showEmployeePicker = true)">
- <u-icon name="list" color="#A78BFA" size="16"></u-icon>
- <text class="search-name-text">{{ selectedEmployeeName || '请选择员工' }}</text>
- <u-icon v-if="!isSecurityCheck" name="arrow-down" color="#A78BFA" size="14"></u-icon>
- </view>
- </view>
- <!-- <view class="tab-nav">
- <view class="tab-item" :class="{ active: activeTab === 'profile' }" @click="activeTab = 'profile'">
- 能力画像
- </view>
- <view class="tab-item" :class="{ active: activeTab === 'data' }" @click="activeTab = 'data'">
- 运行数据
- </view>
- </view> -->
- </view>
- <view class="page-content">
- <view v-if="!portrait.personName" class="empty-state">
- <view class="empty-icon">📋</view>
- <view class="empty-text">搜索员工姓名以查看画像</view>
- </view>
- <view v-if="portrait.personName">
- <SectionTitle title="个人基本信息">
- <view class="user-info">
- <view class="user-header">
- <view class="avatar-area">
- <image v-if="portrait.avatar" :src="portrait.avatar" class="avatar" mode="aspectFill">
- </image>
- <view v-else class="avatar-placeholder">{{ portrait.personName ?
- portrait.personName.charAt(0) : '' }}</view>
- </view>
- <view class="name-row">
- <view class="name-label">姓名:</view>
- <view class="name-value">{{ portrait.personName }}</view>
- </view>
- </view>
- <view class="info-grid">
- <view class="info-item">
- <view class="info-label">所属部门及队室:</view>
- <view class="info-value">{{ portrait.deptPath || '-' }}</view>
- </view>
- <view class="info-item">
- <view class="info-label">学历:</view>
- <view class="info-value">{{ schoolingText }}</view>
- </view>
- <view class="info-item">
- <view class="info-label">出生日期:</view>
- <view class="info-value">{{ portrait.birthday || '-' }}</view>
- </view>
- <view class="info-item">
- <view class="info-label">专业:</view>
- <view class="info-value">{{ portrait.major || '-' }}</view>
- </view>
- <view class="info-item">
- <view class="info-label">技能等级:</view>
- <view class="info-value">{{ portrait.qualificationLevelText || '-' }}</view>
- </view>
- <view class="info-item">
- <view class="info-label">标签:</view>
- <view class="info-value" v-if="portrait.userTags">
- <text class="tag" v-for="tag in portrait.userTags.split(',')" :key="tag">{{ tag }}</text>
- </view>
- </view>
- </view>
- <view class="score-section">
- <div class="score-circle" ref="scoreCircle"></div>
- <view class="score-box">
- <!-- <view class="score-row">
- <text class="score-label">评分:</text>
- <text class="score-val">{{ portrait.totalScore || 0 }}</text>
- </view> -->
- <view class="score-row">
- <text class="score-label">附加分:</text>
- <text class="score-val">{{ tagScoreDisplay }}分</text>
- </view>
- </view>
- </view>
- </view>
- </SectionTitle>
- <SectionTitle v-if="activeTab === 'profile'" title="工作履历">
- <view class="work-history">
- <text v-if="portrait.entryDate">
- {{ formatWorkDate(portrait.entryDate) }}入职 | 司龄{{ portrait.companyYears != null ?
- portrait.companyYears : '-' }}年
- | 开机年限{{ portrait.xrayOperatorYears != null ? portrait.xrayOperatorYears : '-' }}年
- | 现任职{{ portrait.roleNames || '-' }}
- </text>
- <text v-else>暂无数据</text>
- </view>
- </SectionTitle>
- <SectionTitle v-if="activeTab === 'profile'" title="获奖记录">
- <view class="honor-list">
- <view class="honor-item" v-for="(item, index) in portrait.awards" :key="index">
- <view class="honor-name">
- <text class="honor-dot"
- :style="{ background: item.color || honorColors[index % honorColors.length] }"></text>
- {{ item.level2Name }} {{ item.level4Name }}
- </view>
- <view class="honor-score">{{ item.score || '-' }}分</view>
- </view>
- <view v-if="!portrait.awards || portrait.awards.length === 0" class="no-data">暂无获奖记录</view>
- </view>
- </SectionTitle>
- <SectionTitle v-if="activeTab === 'profile'" title="个人能力">
- <div class="chart-container" ref="radarChart"></div>
- </SectionTitle>
- <SectionTitle v-if="activeTab === 'profile'" title="补充信息">
- <view class="supp-grid">
- <view class="supp-item">
- <text class="supp-label">政治面貌:</text>
- <text class="supp-value">{{ portrait.politicalStatusText || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">性别:</text>
- <text class="supp-value">{{ portrait.sexText || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">籍贯:</text>
- <text class="supp-value">{{ portrait.nativePlace || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">民族:</text>
- <text class="supp-value">{{ portrait.nation || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">年龄:</text>
- <text class="supp-value">{{ ageText }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">司龄:</text>
- <text class="supp-value">{{ portrait.companyYears != null ? portrait.companyYears + '年' :
- '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">性格特征:</text>
- <text class="supp-value">{{ portrait.characterCharacteristics || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">工作风格:</text>
- <text class="supp-value">{{ portrait.workingStyle || '-' }}</text>
- </view>
- <view class="supp-item">
- <text class="supp-label">业务岗位:</text>
- <text class="supp-value">{{ portrait.postNames || '-' }}</text>
- </view>
- </view>
- </SectionTitle>
- <!-- <SectionTitle v-if="activeTab === 'data'" title="预警信息">
- <view class="warning-content" @click="showWarning = true">
- <view class="warning-tip">点击查看详细预警信息</view>
- </view>
- </SectionTitle> -->
- </view>
- </view>
- <view class="warning-overlay" v-if="showWarning" @click="showWarning = false">
- <view class="warning-panel" @click.stop>
- <view class="warning-header">
- <text class="warning-title">预警中枢</text>
- <text class="warning-close" @click="showWarning = false">✕</text>
- </view>
- <view class="warning-body">
- <view class="warning-desc">员工综合评估(<75分红色预警 | ≥90分优秀)</view>
- <view class="warning-score">
- <view class="warning-score-item">
- <text class="warning-score-label">综合得分</text>
- <text class="warning-score-value" :class="scoreLevelClass">{{ portrait.totalScore || 0
- }}</text>
- </view>
- </view>
- <view class="warning-detail" v-if="scoreDetails.length">
- <view class="warning-detail-title">评分明细</view>
- <view class="warning-detail-item" v-for="(item, index) in scoreDetails" :key="index">
- <view class="detail-left">
- <text class="detail-dim">{{ item.dimensionName }}</text>
- <text class="detail-name">{{ item.level3Name }}</text>
- </view>
- <view class="detail-score"
- :class="{ 'add-text': item.totalScore > 0, 'deduct-text': item.totalScore < 0 }">
- {{ item.totalScore > 0 ? '+' : '' }}{{ item.totalScore }}
- </view>
- </view>
- </view>
- </view>
- </view>
- </view>
- <u-popup :show="showEmployeePicker" mode="bottom" :round="16" :mask-close-able="true"
- :safe-area-inset-bottom="true" @close="onEmployeePickerClose">
- <view class="employee-picker">
- <view class="picker-header">
- <text class="picker-title">选择员工</text>
- </view>
- <view class="search-box">
- <u-input v-model="employeeSearchKeyword" placeholder="搜索员工" @confirm="onEmployeeSearch"
- @input="onEmployeeSearch"></u-input>
- </view>
- <scroll-view v-if="!employeeSearchKeyword.trim()" scroll-y class="tree-list">
- <template v-for="(node, index) in deptTreeData">
- <employee-tree-node
- :key="node.id"
- :node="node"
- :expanded-ids="expandedDeptIds"
- :selected-id="selectedEmployeeId"
- @toggle="toggleDeptExpand"
- @select="onEmployeeSelect"
- />
- </template>
- </scroll-view>
- <scroll-view v-else scroll-y class="employee-list">
- <view class="employee-item" v-for="item in filteredEmployeeList" :key="item.userId"
- @click="onEmployeeSelect(item)">
- <text class="employee-item-name">{{ item.nickName }}</text>
- <u-icon v-if="item.userId === selectedEmployeeId" name="checkmark" color="#34D399"
- size="18"></u-icon>
- </view>
- <view v-if="filteredEmployeeList.length === 0 && !employeeLoading" class="empty-state">
- <text>暂无数据</text>
- </view>
- <view v-if="employeeLoading" class="loading-state">
- <u-loading-icon text="加载中" textSize="14"></u-loading-icon>
- </view>
- </scroll-view>
- </view>
- </u-popup>
- </view>
- </template>
- <script>
- import * as echarts from 'echarts'
- import { getEmployeePortrait, countTagScore } from '@/api/portraitManagement/portraitManagement'
- import { listAllUser, getDeptUserTree } from '@/api/system/user'
- import SectionTitle from '@/components/SectionTitle.vue'
- import EmployeeTreeNode from '@/pages/components/EmployeeTreeNode.vue'
- const honorColors = ['#60A5FA', '#34D399', '#FBBF24', '#EF4444', '#A78BFA', '#F472B6', '#6EE7B7', '#FB923C']
- const schoolingMap = {
- '1': '小学',
- '2': '初中',
- '3': '高中',
- '4': '中专',
- '5': '大专',
- '6': '本科',
- '7': '硕士',
- '8': '博士'
- }
- function getRandomHexColor() {
- return '#' + Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0')
- }
- export default {
- name: 'EmployeeProfile',
- components: {
- SectionTitle,
- EmployeeTreeNode
- },
- data() {
- return {
- activeTab: 'profile',
- selectedTimeTag: 3,
- timeTags: ['近一周', '近一月', '近三月', '近一年', '自定义时间范围'],
- currentTime: '',
- timer: null,
- beginTime: '',
- endTime: '',
- searchKeyword: '',
- portrait: { dimensions: [], awards: [] },
- tagScoreData: null,
- scoreDetails: [],
- showWarning: false,
- radarChartInstance: null,
- scoreChartInstance: null,
- // 员工选择相关
- showEmployeePicker: false,
- selectedEmployeeId: null,
- selectedEmployeeName: '',
- deptTreeData: [],
- pickerTreeData: [],
- employeeList: [],
- employeeSearchKeyword: '',
- employeeLoading: false,
- expandedDeptIds: [],
- isSecurityCheck: false,
- userInfo: null
- }
- },
- computed: {
- schoolingText() {
- if (!this.portrait.schooling) return '-'
- return schoolingMap[this.portrait.schooling] || this.portrait.schooling
- },
- ageText() {
- if (!this.portrait.birthday) return '-'
- const birthDate = new Date(this.portrait.birthday)
- if (isNaN(birthDate.getTime())) return '-'
- const today = new Date()
- let age = today.getFullYear() - birthDate.getFullYear()
- const monthDiff = today.getMonth() - birthDate.getMonth()
- if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
- age--
- }
- return age + '岁'
- },
- tagScoreDisplay() {
-
- // if (this.tagScoreData == null) return '0'
- // if (typeof this.tagScoreData === 'object') {
- // return this.tagScoreData.totalScore ?? this.tagScoreData.score ?? this.tagScoreData ?? '0'
- // }
- return this.portrait?.userTags?.split(',').length || 0
- },
- scoreLevelClass() {
- if ((this.portrait.totalScore || 0) < 75) return 'score-danger'
- if ((this.portrait.totalScore || 0) >= 90) return 'score-excellent'
- return ''
- },
- currentDate() {
- const now = new Date()
- const year = now.getFullYear()
- const month = String(now.getMonth() + 1).padStart(2, '0')
- const day = String(now.getDate()).padStart(2, '0')
- const weekDays = ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六']
- const weekDay = weekDays[now.getDay()]
- return `${year}年${month}月${day}日 ${weekDay}`
- },
- filteredEmployeeList() {
- const keyword = this.employeeSearchKeyword.trim().toLowerCase()
- if (!keyword) return this.employeeList
- return this.employeeList.filter(item =>
- (item.nickName || '').toLowerCase().includes(keyword)
- )
- }
- },
- mounted() {
- this.updateTime()
- this.timer = setInterval(() => {
- this.updateTime()
- }, 1000)
- // 默认计算时间范围(近一年)
- this.onTimeTagClick(this.selectedTimeTag)
- this.fetchEmployeeList()
- },
- beforeDestroy() {
- if (this.timer) {
- clearInterval(this.timer)
- }
- if (this.radarChartInstance) {
- this.radarChartInstance.dispose()
- }
- if (this.scoreChartInstance) {
- this.scoreChartInstance.dispose()
- }
- },
- methods: {
- updateTime() {
- const now = new Date()
- const hours = String(now.getHours()).padStart(2, '0')
- const minutes = String(now.getMinutes()).padStart(2, '0')
- const seconds = String(now.getSeconds()).padStart(2, '0')
- this.currentTime = `${hours}:${minutes}:${seconds}`
- },
- // 员工相关方法
- // 扁平化部门树,提取人员
- flattenDeptTree(tree) {
- const result = []
- const traverse = (nodes) => {
- nodes.forEach(node => {
- const hasChildren = node.children && node.children.length > 0
- if (!hasChildren && node.nodeType !== 'dept') {
- result.push({
- nodeType: node.nodeType || '',
- userId: node.userId || node.id,
- nickName: node.nickName || node.label || node.userName || ''
- })
- }
- if (hasChildren) {
- traverse(node.children)
- }
- })
- }
- traverse(tree)
- return result
- },
- // 从树中查找第一个用户节点
- findFirstUser(nodes) {
- for (const node of nodes) {
- const hasChildren = node.children && node.children.length > 0
- if (!hasChildren && node.nodeType !== 'dept') {
- return node
- }
- if (hasChildren) {
- const found = this.findFirstUser(node.children)
- if (found) return found
- }
- }
- return null
- },
- // 展开所有部门节点
- expandAllDepts(nodes) {
- this.expandedDeptIds = []
- const traverse = (list) => {
- list.forEach(node => {
- const hasChildren = node.children && node.children.length > 0
- if (hasChildren || node.nodeType === 'dept') {
- this.expandedDeptIds.push(node.id)
- if (hasChildren) {
- traverse(node.children)
- }
- }
- })
- }
- traverse(nodes)
- },
- fetchEmployeeList() {
- this.employeeLoading = true
- const user = this.$store.state.user?.userInfo || this.$store.state.user?.user
- this.userInfo = user
- // 检查角色是否包含 SecurityCheck
- const roles = this.$store.state.user?.roles || []
- this.isSecurityCheck = roles.some(role => role.includes('SecurityCheck'))
- if (this.isSecurityCheck && user) {
- // SecurityCheck 角色,默认显示登录人,不可编辑
- this.selectedEmployeeId = user.userId || user.id
- this.selectedEmployeeName = user.nickName || user.userName || ''
- this.searchKeyword = this.selectedEmployeeName
- this.deptTreeData = []
- this.pickerTreeData = []
- this.employeeLoading = false
- // 自动加载画像数据
- this.fetchEmployeePortrait()
- } else {
- // 其他角色,使用 getDeptUserTree 接口
- if (user && user.deptId) {
- getDeptUserTree({ deptId: user.deptId }).then(res => {
- if (res.code === 200) {
- this.deptTreeData = res.data || []
- const allUsers = this.flattenDeptTree(this.deptTreeData)
- this.employeeList = allUsers.filter(user => user.nodeType === 'user')
- // 默认展开所有部门
- this.expandAllDepts(this.deptTreeData)
- // 默认选中第一个用户
- const firstUser = this.findFirstUser(this.deptTreeData)
- if (firstUser) {
- this.selectedEmployeeId = firstUser.userId || firstUser.id
- this.selectedEmployeeName = firstUser.nickName || firstUser.label || firstUser.userName || ''
- this.searchKeyword = this.selectedEmployeeName
- this.fetchEmployeePortrait()
- }
- }
- }).catch(() => {
- }).finally(() => {
- this.employeeLoading = false
- })
- } else {
- this.employeeLoading = false
- }
- }
- },
- onEmployeeSearch() {
- // 搜索由 computed 自动处理
- },
- toggleDeptExpand(id) {
- const index = this.expandedDeptIds.indexOf(id)
- if (index > -1) {
- this.expandedDeptIds.splice(index, 1)
- } else {
- this.expandedDeptIds.push(id)
- }
- },
- onEmployeeSelect(item) {
- this.selectedEmployeeId = item.userId
- this.selectedEmployeeName = item.nickName
- this.searchKeyword = item.nickName
- this.showEmployeePicker = false
- this.fetchEmployeePortrait()
- },
- onEmployeePickerClose() {
- this.showEmployeePicker = false
- },
- buildTimeParams() {
- const now = new Date()
- const today = this.formatDate(now)
- if (this.selectedTimeTag === 4) {
- if (this.startDate && this.endDate) {
- return { startDate: this.startDate, endDate: this.endDate }
- }
- return {}
- }
- let startDate
- switch (this.selectedTimeTag) {
- case 0:
- startDate = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
- break
- case 1:
- startDate = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
- break
- case 2:
- startDate = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
- break
- case 3:
- startDate = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
- break
- default:
- return {}
- }
- return { startDate: this.formatDate(startDate), endDate: today }
- },
- formatDate(date) {
- const y = date.getFullYear()
- const m = String(date.getMonth() + 1).padStart(2, '0')
- const d = String(date.getDate()).padStart(2, '0')
- return `${y}-${m}-${d}`
- },
- onTimeTagClick(index) {
- this.selectedTimeTag = index
- if (index === 4) {
- this.beginTime = ''
- this.endTime = ''
- return
- }
- // 自动计算时间范围
- const now = new Date()
- // 设置结束时间为今天的 23:59:59
- const endTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59)
- // 设置开始时间
- let beginTime
- switch(index) {
- case 0: // 近一周
- beginTime = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000)
- break
- case 1: // 近一月
- beginTime = new Date(now.getFullYear(), now.getMonth() - 1, now.getDate())
- break
- case 2: // 近三月
- beginTime = new Date(now.getFullYear(), now.getMonth() - 3, now.getDate())
- break
- case 3: // 近一年
- beginTime = new Date(now.getFullYear() - 1, now.getMonth(), now.getDate())
- break
- default:
- beginTime = now
- }
- // 格式化日期为 YYYY-MM-DD HH:mm:ss
- this.beginTime = this.formatFullDate(beginTime)
- this.endTime = this.formatFullDate(endTime)
- // 选择时间标签后立即查询
- if (this.selectedEmployeeId) {
- this.fetchEmployeePortrait()
- }
- },
- formatFullDate(d) {
- const y = d.getFullYear()
- const m = String(d.getMonth() + 1).padStart(2, '0')
- const day = String(d.getDate()).padStart(2, '0')
- const h = String(d.getHours()).padStart(2, '0')
- const min = String(d.getMinutes()).padStart(2, '0')
- const s = String(d.getSeconds()).padStart(2, '0')
- return `${y}-${m}-${day} ${h}:${min}:${s}`
- },
- onBeginTimeChange(e) {
- const dateStr = e.detail.value
- this.beginTime = dateStr + ' 00:00:00'
- // 自定义时间选择后立即查询
- if (this.selectedEmployeeId && this.endTime) {
- this.fetchEmployeePortrait()
- }
- },
- onEndTimeChange(e) {
- const dateStr = e.detail.value
- this.endTime = dateStr + ' 23:59:59'
- // 自定义时间选择后立即查询
- if (this.selectedEmployeeId && this.beginTime) {
- this.fetchEmployeePortrait()
- }
- },
- 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])
- },
- onSearch() {
- if (!this.searchKeyword.trim()) return
- this.fetchEmployeePortrait()
- },
- fetchEmployeePortrait() {
- const params = {}
- if (this.searchKeyword.trim()) {
- params.personName = this.searchKeyword.trim()
- }
- // 添加时间参数
- if (this.beginTime) {
- params.beginTime = this.beginTime
- }
- if (this.endTime) {
- params.endTime = this.endTime
- }
- const portraitPromise = getEmployeePortrait(params).then(res => {
- if (res.code === 200 && res.data) {
- this.portrait = res.data || { dimensions: [], awards: [] }
- this.portrait.awards.forEach(item => {
- item.color = getRandomHexColor()
- })
- // 只取数组中最后一个元素的 scoreDetails
- const allScoreDetails = res.data.scoreDetails || []
- this.scoreDetails = allScoreDetails.length > 0 ? allScoreDetails[allScoreDetails.length - 1] : []
- }
- }).catch(() => { })
- const tagPromise = countTagScore(params).then(res => {
-
- const data = res.data
- if (Array.isArray(data)) {
- const found = data.find(item => item.userId === this.selectedEmployeeId)
- this.tagScoreData = found ? (found.totalScore ?? found.score) : null
- } else {
- this.tagScoreData = data
- }
- }).catch(() => { })
- Promise.all([portraitPromise, tagPromise]).finally(() => {
- this.$nextTick(() => {
-
- this.initRadarChart()
- this.initScoreChart()
- })
- })
- },
- initScoreChart() {
-
- if (!this.$refs.scoreCircle) return
- if (this.scoreChartInstance) {
- this.scoreChartInstance.dispose()
- }
- this.scoreChartInstance = echarts.init(this.$refs.scoreCircle)
- const score = this.portrait.totalScore || 0
-
- const option = {
- series: [
- {
- type: 'gauge',
- startAngle: 90,
- endAngle: -270,
- pointer: {
- show: false
- },
- progress: {
- show: true,
- overlap: false,
- roundCap: true,
- clip: false,
- itemStyle: {
- borderWidth: 1,
- borderColor: '#464646'
- }
- },
- axisLine: {
- lineStyle: {
- width: 5,
- color: [[1, '#34D399']]
- }
- },
- splitLine: {
- show: false,
- distance: 0,
- length: 10
- },
- axisTick: {
- show: false
- },
- axisLabel: {
- show: false,
- distance: 50
- },
- data: [
- {
- value: score,
- name: '综合得分'
- }
- ],
- title: {
- fontSize: 14
- },
- detail: {
- width: 50,
- height: 14,
- fontSize: 20,
- color: 'inherit',
- padding: [0, 0, 80, 0],
- borderColor: 'inherit',
- formatter: '{value}',
- rich: {
- value: {
- fontSize: 14,
- color: '#333',
- lineHeight: 35,
- }
- }
- }
- }
- ]
- }
- this.scoreChartInstance.setOption(option)
- },
- initRadarChart() {
-
- if (!this.$refs.radarChart) return
- if (this.radarChartInstance) {
- this.radarChartInstance.dispose()
- }
- this.radarChartInstance = echarts.init(this.$refs.radarChart)
- const dimensions = this.portrait.dimensions || []
- const maxScore = dimensions.length > 0 ? Math.max(...dimensions.map(d => d.score || 0)) : 100
- const indicator = dimensions.map(d => ({
- name: d.name + '\n' + d.score,
- max: maxScore
- }))
- const option = {
- radar: {
- indicator: indicator,
- center: ['50%', '52%'],
- radius: '60%',
- splitNumber: 8,
- axisLine: {
- lineStyle: { color: 'rgba(0,0,0,0.15)' }
- },
- splitLine: {
- lineStyle: {
- color: ['rgba(0,0,0,0.1)', 'rgba(0,0,0,0.1)', 'rgba(0,0,0,0.1)', 'rgba(0,0,0,0.1)', 'rgba(0,0,0,0.1)', 'rgba(0,0,0,0.1)', '#fe4322', '#8EC742', 'rgba(0,0,0,0.1)']
- }
- },
- splitArea: { show: false },
- axisName: {
- color: '#333',
- fontSize: 12
- }
- },
- series: [{
- type: 'radar',
- data: [{
- name: '个人能力',
- value: dimensions.map(d => d.score),
- areaStyle: { color: 'rgba(77, 200, 254, 0.2)' },
- lineStyle: { color: '#4DC8FE', width: 2 },
- itemStyle: {
- color: '#333',
- borderWidth: 1,
- borderColor: '#00C8DA'
- },
- symbol: 'circle',
- symbolSize: 8
- }]
- }]
- }
- this.radarChartInstance.setOption(option)
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- .employee-profile-page {
- min-height: 100vh;
- background: #fff;
- padding-bottom: 40rpx;
- }
- .page-header {
- position: sticky;
- top: 0;
- z-index: 100;
- color: black;
- background: #fff;
- backdrop-filter: blur(10px);
- box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.2);
- }
- .header-title {
- padding: 24rpx 32rpx;
- display: flex;
- justify-content: space-between;
- align-items: center;
- .title-main {
- font-size: 36rpx;
- font-weight: bold;
- color: black;
- }
- }
- .header-right {
- display: flex;
- align-items: center;
- gap: 16rpx;
- .current-time {
- font-size: 24rpx;
- color: #999;
- }
- }
- .time-filter {
- padding: 16rpx 32rpx;
- background: #fff;
- }
- .time-scroll {
- width: 100%;
- }
- .time-tags {
- display: flex;
- gap: 16rpx;
- white-space: nowrap;
- }
- .time-tag {
- padding: 8rpx 10rpx;
- border-radius: 50rpx;
- background: #f0f0f0;
- font-size: 24rpx;
- color: #666;
- transition: all 0.3s;
- &.active {
- background: #60A5FA;
- color: #fff;
- font-weight: 500;
- }
- }
- .date-range-picker {
- display: flex;
- align-items: center;
- gap: 16rpx;
- margin-top: 16rpx;
- padding-top: 16rpx;
- border-top: 1rpx solid #e0e0e0;
- }
- .date-input {
- flex: 1;
- padding: 12rpx 24rpx;
- border-radius: 12rpx;
- background: #f5f5f5;
- font-size: 24rpx;
- color: #999;
- text-align: center;
- &.filled {
- color: #333;
- }
- }
- .date-separator {
- font-size: 24rpx;
- color: #999;
- flex-shrink: 0;
- }
- .search-selector {
- padding: 16rpx 32rpx;
- background: #fff;
- }
- .search-select-trigger {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 12rpx;
- padding: 16rpx 24rpx;
- background: #f5f5f5;
- border: 1rpx solid #e0e0e0;
- border-radius: 50rpx;
- }
- .search-select-trigger-disabled {
- opacity: 0.5;
- pointer-events: none;
- }
- .search-name-text {
- font-size: 26rpx;
- color: #333;
- }
- .employee-picker {
- background: #fff;
- display: flex;
- flex-direction: column;
- border-radius: 16rpx 16rpx 0 0;
- width: 100%;
- height: 70vh;
- }
- .picker-header {
- padding: 24rpx 32rpx;
- border-bottom: 1rpx solid #eee;
- display: flex;
- justify-content: center;
- flex-shrink: 0;
- }
- .picker-title {
- font-size: 32rpx;
- font-weight: 600;
- color: #333;
- }
- .search-box {
- padding: 16rpx 32rpx;
- flex-shrink: 0;
- }
- .tree-list {
- flex: 1;
- padding: 0;
- height: 0;
- overflow: hidden;
- }
- .employee-list {
- flex: 1;
- padding: 0 32rpx;
- height: 0;
- overflow: hidden;
- }
- .employee-item {
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 24rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .employee-item-name {
- font-size: 28rpx;
- color: #333;
- }
- .tab-nav {
- padding: 16rpx 32rpx;
- display: flex;
- justify-content: center;
- gap: 64rpx;
- }
- .tab-item {
- font-size: 28rpx;
- color: #999;
- padding-bottom: 8rpx;
- border-bottom: 2rpx solid transparent;
- transition: all 0.3s;
- &.active {
- color: #333;
- border-bottom-color: #333;
- }
- }
- .page-content {
- padding: 32rpx;
- display: flex;
- flex-direction: column;
- }
- .empty-state {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 120rpx 0;
- .empty-icon {
- font-size: 80rpx;
- margin-bottom: 24rpx;
- }
- .empty-text {
- font-size: 28rpx;
- color: #999;
- }
- }
- .user-info {
- .user-header {
- display: flex;
- align-items: center;
- gap: 32rpx;
- margin-bottom: 32rpx;
- padding-bottom: 24rpx;
- border-bottom: 1rpx solid #e0e0e0;
- }
- .avatar-area {
- width: 120rpx;
- height: 120rpx;
- border-radius: 50%;
- background: #f0f0f0;
- display: flex;
- align-items: center;
- justify-content: center;
- flex-shrink: 0;
- .avatar {
- width: 100%;
- height: 100%;
- border-radius: 50%;
- object-fit: cover;
- }
- .avatar-placeholder {
- font-size: 48rpx;
- font-weight: bold;
- color: #999;
- }
- }
- .name-row {
- display: flex;
- align-items: baseline;
- .name-label {
- font-size: 26rpx;
- color: #999;
- }
- .name-value {
- font-size: 36rpx;
- font-weight: bold;
- color: #333;
- }
- }
- }
- .info-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20rpx;
- margin-bottom: 32rpx;
- .info-item {
- .info-label {
- font-size: 22rpx;
- color: #999;
- margin-bottom: 4rpx;
- }
- .info-value {
- font-size: 26rpx;
- color: #333;
- .tag {
- display: inline-block;
- padding: 4rpx 16rpx;
- margin-left: 2rpx;
- background: #f0f0f0;
- border-radius: 8rpx;
- color: #333;
- font-size: 22rpx;
- }
- }
- }
- }
- .score-section {
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 48rpx;
- padding-top: 24rpx;
- border-top: 1rpx solid #e0e0e0;
- .score-circle {
- width: 280rpx;
- height: 280rpx;
- }
- .score-box {
- .score-row {
- display: flex;
- align-items: center;
- margin-bottom: 16rpx;
- .score-label {
- font-size: 26rpx;
- color: #999;
- }
- .score-val {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- }
- }
- }
- }
- .work-history {
- font-size: 26rpx;
- color: #555;
- line-height: 1.8;
- }
- .honor-list {
- display: flex;
- flex-direction: column;
- gap: 16rpx;
- .honor-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16rpx;
- background: #f5f5f5;
- border-radius: 12rpx;
- .honor-name {
- display: flex;
- align-items: center;
- gap: 12rpx;
- font-size: 26rpx;
- color: #333;
- flex: 1;
- .honor-dot {
- width: 12rpx;
- height: 12rpx;
- border-radius: 50%;
- flex-shrink: 0;
- }
- }
- .honor-score {
- font-size: 26rpx;
- color: #A78BFA;
- font-weight: 500;
- flex-shrink: 0;
- }
- }
- .no-data {
- font-size: 26rpx;
- color: #999;
- text-align: center;
- padding: 24rpx 0;
- }
- }
- .chart-container {
- width: 100%;
- height: 500rpx;
- }
- .supp-grid {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 20rpx;
- .supp-item {
- .supp-label {
- font-size: 22rpx;
- color: #666;
- margin-bottom: 4rpx;
- display: block;
- }
- .supp-value {
- font-size: 26rpx;
- color: #333;
- }
- }
- }
- .warning-content {
- padding: 32rpx;
- text-align: center;
- cursor: pointer;
- .warning-tip {
- font-size: 26rpx;
- color: #666;
- }
- }
- .warning-overlay {
- position: fixed;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(0, 0, 0, 0.7);
- z-index: 200;
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 48rpx;
- }
- .warning-panel {
- width: 100%;
- max-height: 80vh;
- background: #fff;
- border-radius: 24rpx;
- border: 1rpx solid #e0e0e0;
- overflow: hidden;
- display: flex;
- flex-direction: column;
- }
- .warning-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 32rpx;
- border-bottom: 1rpx solid #eee;
- .warning-title {
- font-size: 32rpx;
- font-weight: bold;
- color: #333;
- }
- .warning-close {
- width: 48rpx;
- height: 48rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 32rpx;
- color: #999;
- border-radius: 50%;
- background: #f0f0f0;
- }
- }
- .warning-body {
- padding: 32rpx;
- overflow-y: auto;
- }
- .warning-desc {
- font-size: 24rpx;
- color: #666;
- margin-bottom: 32rpx;
- }
- .warning-score {
- margin-bottom: 32rpx;
- .warning-score-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 24rpx;
- background: #f5f5f5;
- border-radius: 16rpx;
- .warning-score-label {
- font-size: 28rpx;
- color: #333;
- }
- .warning-score-value {
- font-size: 40rpx;
- font-weight: bold;
- &.score-danger {
- color: #EF4444;
- }
- &.score-excellent {
- color: #34D399;
- }
- color: #60A5FA;
- }
- }
- }
- .warning-detail {
- .warning-detail-title {
- font-size: 28rpx;
- color: #333;
- margin-bottom: 20rpx;
- font-weight: 500;
- }
- .warning-detail-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 16rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- .detail-left {
- display: flex;
- flex-direction: column;
- gap: 4rpx;
- .detail-dim {
- font-size: 22rpx;
- color: #999;
- }
- .detail-name {
- font-size: 26rpx;
- color: #333;
- }
- }
- .detail-score {
- font-size: 28rpx;
- font-weight: 500;
- &.add-text {
- color: #34D399;
- }
- &.deduct-text {
- color: #EF4444;
- }
- }
- }
- }
- </style>
|