|
@@ -127,7 +127,7 @@
|
|
|
destroy-on-close
|
|
destroy-on-close
|
|
|
>
|
|
>
|
|
|
<template v-if="activeExpert">
|
|
<template v-if="activeExpert">
|
|
|
- <div class="drawer-shell">
|
|
|
|
|
|
|
+ <div class="drawer-shell" v-loading="drawerLoading">
|
|
|
<header class="drawer-header">
|
|
<header class="drawer-header">
|
|
|
<div>
|
|
<div>
|
|
|
<h2>{{ activeExpert.name }} - 专家详情</h2>
|
|
<h2>{{ activeExpert.name }} - 专家详情</h2>
|
|
@@ -140,7 +140,10 @@
|
|
|
|
|
|
|
|
<section class="drawer-profile">
|
|
<section class="drawer-profile">
|
|
|
<div class="profile-aside">
|
|
<div class="profile-aside">
|
|
|
- <div class="profile-avatar">{{ activeExpert.name.slice(0, 1) }}</div>
|
|
|
|
|
|
|
+ <div class="profile-avatar">
|
|
|
|
|
+ <img :src="activeExpert.avatar" :alt="activeExpert.name" />
|
|
|
|
|
+ <!-- <span v-else>{{ activeExpert.name.slice(0, 1) }}</span> -->
|
|
|
|
|
+ </div>
|
|
|
<div class="profile-tags">
|
|
<div class="profile-tags">
|
|
|
<span v-for="tag in activeExpert.tags" :key="tag" class="profile-tag">{{
|
|
<span v-for="tag in activeExpert.tags" :key="tag" class="profile-tag">{{
|
|
|
tag
|
|
tag
|
|
@@ -150,23 +153,56 @@
|
|
|
|
|
|
|
|
<div class="profile-main">
|
|
<div class="profile-main">
|
|
|
<div class="info-block">
|
|
<div class="info-block">
|
|
|
- <span class="info-label">基础信息</span>
|
|
|
|
|
- <strong>年龄 {{ activeExpert.age }},学历 {{ activeExpert.education }}</strong>
|
|
|
|
|
|
|
+ <span class="info-label">团队所属公司</span>
|
|
|
|
|
+ <strong>{{ activeExpert.company }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">专家序列 / 梯队 / 学历</span>
|
|
|
|
|
+ <strong
|
|
|
|
|
+ >{{ activeExpert.role }} / {{ activeExpert.team }} /
|
|
|
|
|
+ {{ activeExpert.education }}</strong
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">所属团队</span>
|
|
|
|
|
+ <strong>{{ activeExpert.teamNames.join('、') || '-' }}</strong>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="info-block">
|
|
<div class="info-block">
|
|
|
<span class="info-label">专业方向</span>
|
|
<span class="info-label">专业方向</span>
|
|
|
- <strong>{{ activeExpert.direction.join('、') }}</strong>
|
|
|
|
|
|
|
+ <strong>{{ activeExpert.direction.join('、') || '-' }}</strong>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="info-block">
|
|
<div class="info-block">
|
|
|
- <span class="info-label">主要业绩</span>
|
|
|
|
|
- <strong>{{ activeExpert.achievement }}</strong>
|
|
|
|
|
|
|
+ <span class="info-label">专业软件技能</span>
|
|
|
|
|
+ <strong>{{ activeExpert.softwareSkills.join('、') || '-' }}</strong>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="info-block">
|
|
<div class="info-block">
|
|
|
- <span class="info-label">可参与事项</span>
|
|
|
|
|
- <strong>{{ activeExpert.tasks.join(' / ') }}</strong>
|
|
|
|
|
|
|
+ <span class="info-label">毕业院校 / 出生日期</span>
|
|
|
|
|
+ <strong
|
|
|
|
|
+ >{{ activeExpert.school || '-' }} / {{ activeExpert.birthdayText || '-' }}</strong
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">职称 / 岗位</span>
|
|
|
|
|
+ <strong
|
|
|
|
|
+ >{{ activeExpert.professionalTitle || '-' }} /
|
|
|
|
|
+ {{ activeExpert.jobTitle || '-' }}</strong
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">历任岗位</span>
|
|
|
|
|
+ <strong>{{ activeExpert.formerPost || '-' }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">主要业绩</span>
|
|
|
|
|
+ <strong>{{ activeExpert.achievement || '-' }}</strong>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="profile-actions">
|
|
<div class="profile-actions">
|
|
@@ -457,7 +493,7 @@
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
-import { createExpert, getExpertList, getTeamList } from '@/api/expert'
|
|
|
|
|
|
|
+import { createExpert, getExpertDetail, getExpertList, getTeamList } from '@/api/expert'
|
|
|
import * as DeptApi from '@/api/system/dept'
|
|
import * as DeptApi from '@/api/system/dept'
|
|
|
import { defaultProps } from '@/utils/tree'
|
|
import { defaultProps } from '@/utils/tree'
|
|
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
|
import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
|
@@ -486,6 +522,7 @@ const deptList2 = ref<Tree[]>([]) // 树形结构
|
|
|
const formLoading = ref(false) // 表单加载中
|
|
const formLoading = ref(false) // 表单加载中
|
|
|
const submitLoading = ref(false) // 提交按钮加载中
|
|
const submitLoading = ref(false) // 提交按钮加载中
|
|
|
const teamLoading = ref(false)
|
|
const teamLoading = ref(false)
|
|
|
|
|
+const drawerLoading = ref(false)
|
|
|
|
|
|
|
|
// 对话框相关
|
|
// 对话框相关
|
|
|
const dialogVisible = ref(false)
|
|
const dialogVisible = ref(false)
|
|
@@ -566,14 +603,22 @@ type ExpertRecord = {
|
|
|
type ExpertProfile = {
|
|
type ExpertProfile = {
|
|
|
id: number
|
|
id: number
|
|
|
name: string
|
|
name: string
|
|
|
|
|
+ avatar: string
|
|
|
role: string
|
|
role: string
|
|
|
team: string
|
|
team: string
|
|
|
|
|
+ company: string
|
|
|
age: number
|
|
age: number
|
|
|
education: string
|
|
education: string
|
|
|
direction: string[]
|
|
direction: string[]
|
|
|
- achievement: string
|
|
|
|
|
- tasks: string[]
|
|
|
|
|
|
|
+ softwareSkills: string[]
|
|
|
|
|
+ teamNames: string[]
|
|
|
|
|
+ school: string
|
|
|
|
|
+ birthdayText: string
|
|
|
|
|
+ formerPost: string
|
|
|
|
|
+ jobTitle: string
|
|
|
|
|
+ professionalTitle: string
|
|
|
tags: string[]
|
|
tags: string[]
|
|
|
|
|
+ achievement: string
|
|
|
records: ExpertRecord[]
|
|
records: ExpertRecord[]
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -660,9 +705,28 @@ const teamNameMap = computed(() => {
|
|
|
return map
|
|
return map
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const deptNameMap = computed(() => {
|
|
|
|
|
+ const map = new Map<number, string>()
|
|
|
|
|
+ const walk = (nodes: any[]) => {
|
|
|
|
|
+ nodes.forEach((node) => {
|
|
|
|
|
+ map.set(node.id, node.name)
|
|
|
|
|
+ if (node.children?.length) {
|
|
|
|
|
+ walk(node.children)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+ walk(deptList2.value || [])
|
|
|
|
|
+ return map
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
const getTeamNames = (teamIds: number[] = []) =>
|
|
const getTeamNames = (teamIds: number[] = []) =>
|
|
|
teamIds.map((id) => teamNameMap.value.get(id)).filter(Boolean) as string[]
|
|
teamIds.map((id) => teamNameMap.value.get(id)).filter(Boolean) as string[]
|
|
|
|
|
|
|
|
|
|
+const getDeptName = (deptId?: number) => {
|
|
|
|
|
+ if (deptId === undefined || deptId === null) return ''
|
|
|
|
|
+ return deptNameMap.value.get(deptId) || ''
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const getExpertAge = (birthday: number | null) => {
|
|
const getExpertAge = (birthday: number | null) => {
|
|
|
if (!birthday) return 0
|
|
if (!birthday) return 0
|
|
|
const today = new Date()
|
|
const today = new Date()
|
|
@@ -675,6 +739,16 @@ const getExpertAge = (birthday: number | null) => {
|
|
|
return Math.max(age, 0)
|
|
return Math.max(age, 0)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const formatBirthday = (birthday: number | null) => {
|
|
|
|
|
+ if (!birthday) return ''
|
|
|
|
|
+ const date = new Date(birthday)
|
|
|
|
|
+ if (Number.isNaN(date.getTime())) return ''
|
|
|
|
|
+ const year = date.getFullYear()
|
|
|
|
|
+ const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
|
|
|
+ const day = String(date.getDate()).padStart(2, '0')
|
|
|
|
|
+ return `${year}-${month}-${day}`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const buildExpertProfile = (expert: ExpertItem, groupLabel?: string): ExpertProfile => {
|
|
const buildExpertProfile = (expert: ExpertItem, groupLabel?: string): ExpertProfile => {
|
|
|
const rankLabels = getDictLabels(DICT_TYPE.RD_EXPERT_RANK, expert.expertRank)
|
|
const rankLabels = getDictLabels(DICT_TYPE.RD_EXPERT_RANK, expert.expertRank)
|
|
|
const directionLabels = getDictLabels(DICT_TYPE.RD_EXPERT_TECHNICAL, expert.specialDirection)
|
|
const directionLabels = getDictLabels(DICT_TYPE.RD_EXPERT_TECHNICAL, expert.specialDirection)
|
|
@@ -683,14 +757,22 @@ const buildExpertProfile = (expert: ExpertItem, groupLabel?: string): ExpertProf
|
|
|
return {
|
|
return {
|
|
|
id: expert.id,
|
|
id: expert.id,
|
|
|
name: expert.expertName,
|
|
name: expert.expertName,
|
|
|
|
|
+ avatar: expert.avatar || '',
|
|
|
role: rankLabels.join('、') || '专家',
|
|
role: rankLabels.join('、') || '专家',
|
|
|
team: groupLabel || getDictLabel(DICT_TYPE.RD_EXPERT_ECHELON, expert.echelon) || '未分组',
|
|
team: groupLabel || getDictLabel(DICT_TYPE.RD_EXPERT_ECHELON, expert.echelon) || '未分组',
|
|
|
|
|
+ company: getDeptName(expert.deptId) || '-',
|
|
|
age: getExpertAge(expert.birthday),
|
|
age: getExpertAge(expert.birthday),
|
|
|
education: getDictLabel(DICT_TYPE.RD_EXPERT_EDUCATION, expert.education) || '-',
|
|
education: getDictLabel(DICT_TYPE.RD_EXPERT_EDUCATION, expert.education) || '-',
|
|
|
direction: directionLabels.length ? directionLabels : ['暂无'],
|
|
direction: directionLabels.length ? directionLabels : ['暂无'],
|
|
|
- achievement: expert.achievement || '暂无',
|
|
|
|
|
- tasks: softwareSkillLabels.length ? softwareSkillLabels : [expert.jobTitle || '暂无'],
|
|
|
|
|
|
|
+ softwareSkills: softwareSkillLabels,
|
|
|
|
|
+ teamNames,
|
|
|
|
|
+ school: expert.school || '',
|
|
|
|
|
+ birthdayText: formatBirthday(expert.birthday),
|
|
|
|
|
+ formerPost: expert.formerPost || '',
|
|
|
|
|
+ jobTitle: expert.jobTitle || '',
|
|
|
|
|
+ professionalTitle: expert.professionalTitle || '',
|
|
|
tags: [groupLabel, ...teamNames, expert.professionalTitle].filter(Boolean),
|
|
tags: [groupLabel, ...teamNames, expert.professionalTitle].filter(Boolean),
|
|
|
|
|
+ achievement: expert.achievement || '暂无',
|
|
|
records: []
|
|
records: []
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
@@ -772,13 +854,16 @@ const filteredTableData = computed(() => {
|
|
|
})
|
|
})
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-const handleMemberClick = (member: ExpertItem) => {
|
|
|
|
|
|
|
+const handleMemberClick = async (member: ExpertItem) => {
|
|
|
selectedMember.value = member.expertName
|
|
selectedMember.value = member.expertName
|
|
|
- const group = (list.value as ExpertGroup[]).find((item) =>
|
|
|
|
|
- item.experts.some((expert) => expert.id === member.id)
|
|
|
|
|
- )
|
|
|
|
|
- activeExpert.value = buildExpertProfile(member, group?.echelonLabel)
|
|
|
|
|
drawerVisible.value = true
|
|
drawerVisible.value = true
|
|
|
|
|
+ drawerLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await getExpertDetail(member.id)
|
|
|
|
|
+ activeExpert.value = buildExpertProfile(detail)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ drawerLoading.value = false
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleWordClick = (word: string) => {
|
|
const handleWordClick = (word: string) => {
|
|
@@ -1484,7 +1569,7 @@ onMounted(async () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.drawer-header {
|
|
.drawer-header {
|
|
|
- padding: 20px 28px 18px;
|
|
|
|
|
|
|
+ padding: 16px 22px 14px;
|
|
|
border-bottom: 1px solid rgba(24, 37, 54, 0.08);
|
|
border-bottom: 1px solid rgba(24, 37, 54, 0.08);
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
}
|
|
}
|
|
@@ -1492,25 +1577,25 @@ onMounted(async () => {
|
|
|
.drawer-header h2 {
|
|
.drawer-header h2 {
|
|
|
margin: 0;
|
|
margin: 0;
|
|
|
color: #102234;
|
|
color: #102234;
|
|
|
- font-size: 20px;
|
|
|
|
|
|
|
+ font-size: 18px;
|
|
|
font-weight: 800;
|
|
font-weight: 800;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.drawer-header p {
|
|
.drawer-header p {
|
|
|
- margin: 8px 0 0;
|
|
|
|
|
|
|
+ margin: 6px 0 0;
|
|
|
color: #6f7f8e;
|
|
color: #6f7f8e;
|
|
|
- font-size: 14px;
|
|
|
|
|
|
|
+ font-size: 13px;
|
|
|
font-weight: 600;
|
|
font-weight: 600;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.drawer-profile {
|
|
.drawer-profile {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
- grid-template-columns: 230px minmax(0, 1fr);
|
|
|
|
|
- gap: 24px;
|
|
|
|
|
- margin: 26px;
|
|
|
|
|
- padding: 26px;
|
|
|
|
|
|
|
+ grid-template-columns: 150px minmax(0, 1fr);
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin: 18px;
|
|
|
|
|
+ padding: 16px;
|
|
|
border: 1px solid rgba(28, 44, 62, 0.08);
|
|
border: 1px solid rgba(28, 44, 62, 0.08);
|
|
|
- border-radius: 16px;
|
|
|
|
|
|
|
+ border-radius: 14px;
|
|
|
background: #fff;
|
|
background: #fff;
|
|
|
box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
|
|
box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
|
|
|
}
|
|
}
|
|
@@ -1518,77 +1603,86 @@ onMounted(async () => {
|
|
|
.profile-aside {
|
|
.profile-aside {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 16px;
|
|
|
|
|
|
|
+ gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-avatar {
|
|
.profile-avatar {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
justify-content: center;
|
|
|
- min-height: 260px;
|
|
|
|
|
- border-radius: 18px;
|
|
|
|
|
|
|
+ min-height: 150px;
|
|
|
|
|
+ max-height: 150px;
|
|
|
|
|
+ border-radius: 14px;
|
|
|
background: linear-gradient(145deg, #b7d3cd, #e8f0ee);
|
|
background: linear-gradient(145deg, #b7d3cd, #e8f0ee);
|
|
|
color: #0f5b58;
|
|
color: #0f5b58;
|
|
|
- font-size: 84px;
|
|
|
|
|
|
|
+ font-size: 46px;
|
|
|
font-weight: 800;
|
|
font-weight: 800;
|
|
|
letter-spacing: -0.08em;
|
|
letter-spacing: -0.08em;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-avatar img {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ height: 100%;
|
|
|
|
|
+ object-fit: cover;
|
|
|
|
|
+ display: block;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-tags {
|
|
.profile-tags {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
- gap: 10px;
|
|
|
|
|
|
|
+ gap: 8px;
|
|
|
flex-wrap: wrap;
|
|
flex-wrap: wrap;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-tag {
|
|
.profile-tag {
|
|
|
display: inline-flex;
|
|
display: inline-flex;
|
|
|
align-items: center;
|
|
align-items: center;
|
|
|
- padding: 8px 14px;
|
|
|
|
|
|
|
+ padding: 6px 10px;
|
|
|
border-radius: 999px;
|
|
border-radius: 999px;
|
|
|
background: #f1f4f2;
|
|
background: #f1f4f2;
|
|
|
color: #6e7d7a;
|
|
color: #6e7d7a;
|
|
|
- font-size: 13px;
|
|
|
|
|
|
|
+ font-size: 12px;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-main {
|
|
.profile-main {
|
|
|
display: flex;
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
flex-direction: column;
|
|
|
- gap: 16px;
|
|
|
|
|
|
|
+ gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.info-block {
|
|
.info-block {
|
|
|
- padding: 18px 18px 16px;
|
|
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
border: 1px solid #dbe3df;
|
|
border: 1px solid #dbe3df;
|
|
|
- border-radius: 14px;
|
|
|
|
|
|
|
+ border-radius: 12px;
|
|
|
background: #fdfefe;
|
|
background: #fdfefe;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.info-label {
|
|
.info-label {
|
|
|
display: block;
|
|
display: block;
|
|
|
- margin-bottom: 8px;
|
|
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
color: #7a8a89;
|
|
color: #7a8a89;
|
|
|
- font-size: 13px;
|
|
|
|
|
|
|
+ font-size: 12px;
|
|
|
// font-weight: 700;
|
|
// font-weight: 700;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.info-block strong {
|
|
.info-block strong {
|
|
|
color: #203130;
|
|
color: #203130;
|
|
|
- font-size: 14px;
|
|
|
|
|
|
|
+ font-size: 13px;
|
|
|
font-weight: 800;
|
|
font-weight: 800;
|
|
|
- line-height: 1.7;
|
|
|
|
|
|
|
+ line-height: 1.55;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-actions {
|
|
.profile-actions {
|
|
|
display: grid;
|
|
display: grid;
|
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
- gap: 14px;
|
|
|
|
|
|
|
+ gap: 10px;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.profile-actions :deep(.el-button) {
|
|
.profile-actions :deep(.el-button) {
|
|
|
- height: 54px;
|
|
|
|
|
- border-radius: 12px;
|
|
|
|
|
- font-size: 16px;
|
|
|
|
|
|
|
+ height: 42px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
font-weight: 700;
|
|
font-weight: 700;
|
|
|
}
|
|
}
|
|
|
|
|
|