yanghao 1 месяц назад
Родитель
Сommit
2480e6396f
1 измененных файлов с 140 добавлено и 46 удалено
  1. 140 46
      src/views/expert/list/index.vue

+ 140 - 46
src/views/expert/list/index.vue

@@ -127,7 +127,7 @@
       destroy-on-close
     >
       <template v-if="activeExpert">
-        <div class="drawer-shell">
+        <div class="drawer-shell" v-loading="drawerLoading">
           <header class="drawer-header">
             <div>
               <h2>{{ activeExpert.name }} - 专家详情</h2>
@@ -140,7 +140,10 @@
 
           <section class="drawer-profile">
             <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">
                 <span v-for="tag in activeExpert.tags" :key="tag" class="profile-tag">{{
                   tag
@@ -150,23 +153,56 @@
 
             <div class="profile-main">
               <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 class="info-block">
                 <span class="info-label">专业方向</span>
-                <strong>{{ activeExpert.direction.join('、') }}</strong>
+                <strong>{{ activeExpert.direction.join('、') || '-' }}</strong>
               </div>
 
               <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 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 class="profile-actions">
@@ -457,7 +493,7 @@
 
 <script setup lang="ts">
 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 { defaultProps } from '@/utils/tree'
 import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
@@ -486,6 +522,7 @@ const deptList2 = ref<Tree[]>([]) // 树形结构
 const formLoading = ref(false) // 表单加载中
 const submitLoading = ref(false) // 提交按钮加载中
 const teamLoading = ref(false)
+const drawerLoading = ref(false)
 
 // 对话框相关
 const dialogVisible = ref(false)
@@ -566,14 +603,22 @@ type ExpertRecord = {
 type ExpertProfile = {
   id: number
   name: string
+  avatar: string
   role: string
   team: string
+  company: string
   age: number
   education: string
   direction: string[]
-  achievement: string
-  tasks: string[]
+  softwareSkills: string[]
+  teamNames: string[]
+  school: string
+  birthdayText: string
+  formerPost: string
+  jobTitle: string
+  professionalTitle: string
   tags: string[]
+  achievement: string
   records: ExpertRecord[]
 }
 
@@ -660,9 +705,28 @@ const teamNameMap = computed(() => {
   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[] = []) =>
   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) => {
   if (!birthday) return 0
   const today = new Date()
@@ -675,6 +739,16 @@ const getExpertAge = (birthday: number | null) => {
   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 rankLabels = getDictLabels(DICT_TYPE.RD_EXPERT_RANK, expert.expertRank)
   const directionLabels = getDictLabels(DICT_TYPE.RD_EXPERT_TECHNICAL, expert.specialDirection)
@@ -683,14 +757,22 @@ const buildExpertProfile = (expert: ExpertItem, groupLabel?: string): ExpertProf
   return {
     id: expert.id,
     name: expert.expertName,
+    avatar: expert.avatar || '',
     role: rankLabels.join('、') || '专家',
     team: groupLabel || getDictLabel(DICT_TYPE.RD_EXPERT_ECHELON, expert.echelon) || '未分组',
+    company: getDeptName(expert.deptId) || '-',
     age: getExpertAge(expert.birthday),
     education: getDictLabel(DICT_TYPE.RD_EXPERT_EDUCATION, expert.education) || '-',
     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),
+    achievement: expert.achievement || '暂无',
     records: []
   }
 }
@@ -772,13 +854,16 @@ const filteredTableData = computed(() => {
   })
 })
 
-const handleMemberClick = (member: ExpertItem) => {
+const handleMemberClick = async (member: ExpertItem) => {
   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
+  drawerLoading.value = true
+  try {
+    const detail = await getExpertDetail(member.id)
+    activeExpert.value = buildExpertProfile(detail)
+  } finally {
+    drawerLoading.value = false
+  }
 }
 
 const handleWordClick = (word: string) => {
@@ -1484,7 +1569,7 @@ onMounted(async () => {
 }
 
 .drawer-header {
-  padding: 20px 28px 18px;
+  padding: 16px 22px 14px;
   border-bottom: 1px solid rgba(24, 37, 54, 0.08);
   background: #fff;
 }
@@ -1492,25 +1577,25 @@ onMounted(async () => {
 .drawer-header h2 {
   margin: 0;
   color: #102234;
-  font-size: 20px;
+  font-size: 18px;
   font-weight: 800;
 }
 
 .drawer-header p {
-  margin: 8px 0 0;
+  margin: 6px 0 0;
   color: #6f7f8e;
-  font-size: 14px;
+  font-size: 13px;
   font-weight: 600;
 }
 
 .drawer-profile {
   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-radius: 16px;
+  border-radius: 14px;
   background: #fff;
   box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
 }
@@ -1518,77 +1603,86 @@ onMounted(async () => {
 .profile-aside {
   display: flex;
   flex-direction: column;
-  gap: 16px;
+  gap: 10px;
 }
 
 .profile-avatar {
   display: flex;
   align-items: 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);
   color: #0f5b58;
-  font-size: 84px;
+  font-size: 46px;
   font-weight: 800;
   letter-spacing: -0.08em;
+  overflow: hidden;
+}
+
+.profile-avatar img {
+  width: 100%;
+  height: 100%;
+  object-fit: cover;
+  display: block;
 }
 
 .profile-tags {
   display: flex;
-  gap: 10px;
+  gap: 8px;
   flex-wrap: wrap;
 }
 
 .profile-tag {
   display: inline-flex;
   align-items: center;
-  padding: 8px 14px;
+  padding: 6px 10px;
   border-radius: 999px;
   background: #f1f4f2;
   color: #6e7d7a;
-  font-size: 13px;
+  font-size: 12px;
   font-weight: 700;
 }
 
 .profile-main {
   display: flex;
   flex-direction: column;
-  gap: 16px;
+  gap: 10px;
 }
 
 .info-block {
-  padding: 18px 18px 16px;
+  padding: 12px 14px;
   border: 1px solid #dbe3df;
-  border-radius: 14px;
+  border-radius: 12px;
   background: #fdfefe;
 }
 
 .info-label {
   display: block;
-  margin-bottom: 8px;
+  margin-bottom: 5px;
   color: #7a8a89;
-  font-size: 13px;
+  font-size: 12px;
   // font-weight: 700;
 }
 
 .info-block strong {
   color: #203130;
-  font-size: 14px;
+  font-size: 13px;
   font-weight: 800;
-  line-height: 1.7;
+  line-height: 1.55;
 }
 
 .profile-actions {
   display: grid;
   grid-template-columns: repeat(3, minmax(0, 1fr));
-  gap: 14px;
+  gap: 10px;
 }
 
 .profile-actions :deep(.el-button) {
-  height: 54px;
-  border-radius: 12px;
-  font-size: 16px;
+  height: 42px;
+  border-radius: 10px;
+  font-size: 14px;
   font-weight: 700;
 }