|
|
@@ -100,12 +100,12 @@
|
|
|
<template v-if="cell.type === 'members'">
|
|
|
<button
|
|
|
v-for="member in cell.members"
|
|
|
- :key="member"
|
|
|
+ :key="member.id"
|
|
|
type="button"
|
|
|
class="member-link"
|
|
|
@click="handleMemberClick(member)"
|
|
|
>
|
|
|
- {{ member }}
|
|
|
+ {{ member.expertName }}
|
|
|
</button>
|
|
|
</template>
|
|
|
|
|
|
@@ -536,7 +536,7 @@ const closeDialog = () => {
|
|
|
type CellItem =
|
|
|
| {
|
|
|
type: 'members'
|
|
|
- members: string[]
|
|
|
+ members: ExpertItem[]
|
|
|
}
|
|
|
| {
|
|
|
type: 'text'
|
|
|
@@ -563,6 +563,7 @@ type ExpertRecord = {
|
|
|
}
|
|
|
|
|
|
type ExpertProfile = {
|
|
|
+ id: number
|
|
|
name: string
|
|
|
role: string
|
|
|
team: string
|
|
|
@@ -575,89 +576,31 @@ type ExpertProfile = {
|
|
|
records: ExpertRecord[]
|
|
|
}
|
|
|
|
|
|
-const tableData: TableRow[] = [
|
|
|
- {
|
|
|
- teamName: '第一梯队',
|
|
|
- education: '博士',
|
|
|
- searchText: '朱桂林 软件工程 50项成果',
|
|
|
- tableWidth: '64%',
|
|
|
- backgroundColor: '#eef6ff',
|
|
|
- columnWidths: [1.5, 1.5, 1.5, 1.5, 1.5],
|
|
|
- cells: [
|
|
|
- {
|
|
|
- type: 'members',
|
|
|
- members: [
|
|
|
- '朱桂林',
|
|
|
- '孙乐臻',
|
|
|
- '王军',
|
|
|
- '符云',
|
|
|
- '王伟',
|
|
|
- '王伟',
|
|
|
- '李四',
|
|
|
-
|
|
|
- '孙乐臻',
|
|
|
- '王军',
|
|
|
- '符云',
|
|
|
- '王伟',
|
|
|
- '王伟',
|
|
|
- '李四'
|
|
|
- ]
|
|
|
- },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '', tone: 'danger' },
|
|
|
- { type: 'text', text: '' }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- teamName: '第二梯队',
|
|
|
- education: '硕士',
|
|
|
- searchText: '孙乐臻 金融风险 30项业绩',
|
|
|
- tableWidth: '75%',
|
|
|
- backgroundColor: '#f2faef',
|
|
|
- columnWidths: [1.8, 1.8, 1.8, 1.8, 1.8],
|
|
|
- cells: [
|
|
|
- { type: 'members', members: ['朱桂林', '孙乐臻', '王军', '符云', '王伟', '王伟', '李四'] },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' }
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- teamName: '第三梯队',
|
|
|
- education: '本科',
|
|
|
- searchText: '王军 互联网产品 20项成果',
|
|
|
- tableWidth: '100%',
|
|
|
- backgroundColor: '#fff8e8',
|
|
|
- columnWidths: [2, 2, 2, 2, 2],
|
|
|
- cells: [
|
|
|
- {
|
|
|
- type: 'members',
|
|
|
- members: [
|
|
|
- '朱桂林',
|
|
|
- '孙乐臻',
|
|
|
- '王军',
|
|
|
- '符云',
|
|
|
- '王伟',
|
|
|
- '王伟',
|
|
|
- '李四',
|
|
|
- '朱桂林',
|
|
|
- '孙乐臻',
|
|
|
- '王军',
|
|
|
- '符云',
|
|
|
- '王伟',
|
|
|
- '王伟',
|
|
|
- '李四'
|
|
|
- ]
|
|
|
- },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' },
|
|
|
- { type: 'text', text: '' }
|
|
|
- ]
|
|
|
- }
|
|
|
-]
|
|
|
+type ExpertItem = {
|
|
|
+ id: number
|
|
|
+ deptId: number
|
|
|
+ expertName: string
|
|
|
+ echelon: string
|
|
|
+ expertRank: number[]
|
|
|
+ professionalTitle: string
|
|
|
+ jobTitle: string
|
|
|
+ formerPost: string
|
|
|
+ teamIds: number[]
|
|
|
+ avatar: string
|
|
|
+ birthday: number | null
|
|
|
+ school: string
|
|
|
+ education: string
|
|
|
+ specialDirection: number[]
|
|
|
+ achievement: string
|
|
|
+ softwareSkills: number[]
|
|
|
+}
|
|
|
+
|
|
|
+type ExpertGroup = {
|
|
|
+ echelonLabel: string
|
|
|
+ num: number
|
|
|
+ sort: number
|
|
|
+ experts: ExpertItem[]
|
|
|
+}
|
|
|
|
|
|
const selectedMember = ref('')
|
|
|
const searchQuery = ref('')
|
|
|
@@ -666,77 +609,6 @@ const selectedTeam = ref('')
|
|
|
const drawerVisible = ref(false)
|
|
|
const activeExpert = ref<ExpertProfile | null>(null)
|
|
|
|
|
|
-const expertProfileMap: Record<string, ExpertProfile> = {
|
|
|
- 朱桂林: {
|
|
|
- name: '朱桂林',
|
|
|
- role: '技术专家',
|
|
|
- team: '第一梯队',
|
|
|
- age: 52,
|
|
|
- education: '博士',
|
|
|
- direction: ['材料工程', '可靠性验证', '失效分析'],
|
|
|
- achievement:
|
|
|
- '高温井下材料可靠性验证、材料失效分析专家评审;参与 6 个科研项目评审,2 个项目验收专家组。',
|
|
|
- tasks: ['立项预审', '中期检查', '结题验收', '成果评审', '专项咨询'],
|
|
|
- tags: ['技术专家', '第一梯队', '启用'],
|
|
|
- records: [
|
|
|
- {
|
|
|
- date: '2026-08-01',
|
|
|
- event: '中期检查评审',
|
|
|
- project: '智能检测算法研发',
|
|
|
- status: '已完成',
|
|
|
- statusType: 'success'
|
|
|
- },
|
|
|
- {
|
|
|
- date: '2026-07-18',
|
|
|
- event: '立项预审',
|
|
|
- project: '平台数据治理标准',
|
|
|
- status: '已反馈意见',
|
|
|
- statusType: 'primary'
|
|
|
- }
|
|
|
- ]
|
|
|
- },
|
|
|
- 孙乐臻: {
|
|
|
- name: '孙乐臻',
|
|
|
- role: '经营专家',
|
|
|
- team: '第二梯队',
|
|
|
- age: 46,
|
|
|
- education: '硕士',
|
|
|
- direction: ['经营管理', '投资评估', '风险控制'],
|
|
|
- achievement: '参与多个经营分析专项评审,长期负责重点项目投资效益与风险识别。',
|
|
|
- tasks: ['立项预审', '预算审查', '经营诊断', '专项咨询'],
|
|
|
- tags: ['经营专家', '第二梯队', '启用'],
|
|
|
- records: [
|
|
|
- {
|
|
|
- date: '2026-08-05',
|
|
|
- event: '预算审查',
|
|
|
- project: '区域经营提升计划',
|
|
|
- status: '已完成',
|
|
|
- statusType: 'success'
|
|
|
- }
|
|
|
- ]
|
|
|
- },
|
|
|
- 王军: {
|
|
|
- name: '王军',
|
|
|
- role: '技术专家',
|
|
|
- team: '第三梯队',
|
|
|
- age: 41,
|
|
|
- education: '本科',
|
|
|
- direction: ['互联网产品', '数字化平台', '需求架构'],
|
|
|
- achievement: '参与多个数字平台项目方案论证和成果评审,具备丰富的产品落地经验。',
|
|
|
- tasks: ['方案评审', '中期检查', '成果评审'],
|
|
|
- tags: ['技术专家', '第三梯队', '启用'],
|
|
|
- records: [
|
|
|
- {
|
|
|
- date: '2026-07-26',
|
|
|
- event: '成果评审',
|
|
|
- project: '协同应用平台升级',
|
|
|
- status: '已完成',
|
|
|
- statusType: 'success'
|
|
|
- }
|
|
|
- ]
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
const wordCloudWords = [
|
|
|
{ text: '地质', size: 24, rotate: -8, color: '#7fcd72', left: '8%', top: '52%' },
|
|
|
{ text: '油藏', size: 30, rotate: 6, color: '#8ea4d8', left: '26%', top: '38%' },
|
|
|
@@ -760,6 +632,68 @@ const teamQueryParams = reactive({
|
|
|
teamName: undefined
|
|
|
})
|
|
|
|
|
|
+const rowStylePresets = [
|
|
|
+ { tableWidth: '64%', backgroundColor: '#eef6ff', columnWidths: [1.5, 1.5, 1.5, 1.5, 1.5] },
|
|
|
+ { tableWidth: '75%', backgroundColor: '#f2faef', columnWidths: [1.8, 1.8, 1.8, 1.8, 1.8] },
|
|
|
+ { tableWidth: '100%', backgroundColor: '#fff8e8', columnWidths: [2, 2, 2, 2, 2] }
|
|
|
+]
|
|
|
+
|
|
|
+const getDictLabel = (type: string, value: string | number | undefined | null) => {
|
|
|
+ if (value === undefined || value === null || value === '') return ''
|
|
|
+ return getStrDictOptions(type).find((item) => String(item.value) === String(value))?.label || ''
|
|
|
+}
|
|
|
+
|
|
|
+const getDictLabels = (type: string, values: Array<string | number> | undefined) => {
|
|
|
+ if (!values?.length) return []
|
|
|
+ return values.map((value) => getDictLabel(type, value)).filter(Boolean)
|
|
|
+}
|
|
|
+
|
|
|
+const teamNameMap = computed(() => {
|
|
|
+ const map = new Map<number, string>()
|
|
|
+ ;(teamList.value || []).forEach((team) => {
|
|
|
+ map.set(team.id, team.teamName)
|
|
|
+ })
|
|
|
+ ;(selectedTeams.value || []).forEach((team) => {
|
|
|
+ map.set(team.id, team.teamName)
|
|
|
+ })
|
|
|
+ return map
|
|
|
+})
|
|
|
+
|
|
|
+const getTeamNames = (teamIds: number[] = []) =>
|
|
|
+ teamIds.map((id) => teamNameMap.value.get(id)).filter(Boolean) as string[]
|
|
|
+
|
|
|
+const getExpertAge = (birthday: number | null) => {
|
|
|
+ if (!birthday) return 0
|
|
|
+ const today = new Date()
|
|
|
+ const birth = new Date(birthday)
|
|
|
+ let age = today.getFullYear() - birth.getFullYear()
|
|
|
+ const monthDiff = today.getMonth() - birth.getMonth()
|
|
|
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birth.getDate())) {
|
|
|
+ age--
|
|
|
+ }
|
|
|
+ return Math.max(age, 0)
|
|
|
+}
|
|
|
+
|
|
|
+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)
|
|
|
+ const softwareSkillLabels = getDictLabels(DICT_TYPE.RD_EXPERT_SOFTWARE, expert.softwareSkills)
|
|
|
+ const teamNames = getTeamNames(expert.teamIds)
|
|
|
+ return {
|
|
|
+ id: expert.id,
|
|
|
+ name: expert.expertName,
|
|
|
+ role: rankLabels.join('、') || '专家',
|
|
|
+ team: groupLabel || getDictLabel(DICT_TYPE.RD_EXPERT_ECHELON, expert.echelon) || '未分组',
|
|
|
+ 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 || '暂无'],
|
|
|
+ tags: [groupLabel, ...teamNames, expert.professionalTitle].filter(Boolean),
|
|
|
+ records: []
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const getColumnPercent = (row: TableRow, index: number) => {
|
|
|
if (!row.columnWidths || row.columnWidths.length === 0) {
|
|
|
return '0'
|
|
|
@@ -772,8 +706,50 @@ const getColumnPercent = (row: TableRow, index: number) => {
|
|
|
return ((value / total) * 100).toFixed(4)
|
|
|
}
|
|
|
|
|
|
+const tableData = computed<TableRow[]>(() => {
|
|
|
+ return (list.value as ExpertGroup[]).map((group, index) => {
|
|
|
+ const preset = rowStylePresets[index] || rowStylePresets[rowStylePresets.length - 1]
|
|
|
+ const educationLabels = Array.from(
|
|
|
+ new Set(
|
|
|
+ group.experts.map((expert) => getDictLabel(DICT_TYPE.RD_EXPERT_EDUCATION, expert.education))
|
|
|
+ )
|
|
|
+ ).filter(Boolean)
|
|
|
+ const searchText = group.experts
|
|
|
+ .map((expert) =>
|
|
|
+ [
|
|
|
+ expert.expertName,
|
|
|
+ expert.school,
|
|
|
+ expert.jobTitle,
|
|
|
+ expert.formerPost,
|
|
|
+ expert.achievement
|
|
|
+ ].join(' ')
|
|
|
+ )
|
|
|
+ .join(' ')
|
|
|
+ .toLowerCase()
|
|
|
+
|
|
|
+ return {
|
|
|
+ teamName: group.echelonLabel,
|
|
|
+ education: educationLabels.join('、') || '-',
|
|
|
+ searchText,
|
|
|
+ tableWidth: preset.tableWidth,
|
|
|
+ backgroundColor: preset.backgroundColor,
|
|
|
+ columnWidths: preset.columnWidths,
|
|
|
+ cells: [
|
|
|
+ {
|
|
|
+ type: 'members',
|
|
|
+ members: group.experts
|
|
|
+ },
|
|
|
+ { type: 'text', text: '' },
|
|
|
+ { type: 'text', text: '' },
|
|
|
+ { type: 'text', text: '', tone: 'danger' },
|
|
|
+ { type: 'text', text: '' }
|
|
|
+ ]
|
|
|
+ }
|
|
|
+ })
|
|
|
+})
|
|
|
+
|
|
|
const filteredTableData = computed(() => {
|
|
|
- return tableData.filter((row) => {
|
|
|
+ return tableData.value.filter((row) => {
|
|
|
const query = searchQuery.value.trim().toLowerCase()
|
|
|
const nameMatch = row.searchText.toLowerCase().includes(query)
|
|
|
const teamMatch = row.teamName.toLowerCase().includes(query)
|
|
|
@@ -792,20 +768,12 @@ const filteredTableData = computed(() => {
|
|
|
})
|
|
|
})
|
|
|
|
|
|
-const handleMemberClick = (member: string) => {
|
|
|
- selectedMember.value = member
|
|
|
- activeExpert.value = expertProfileMap[member] || {
|
|
|
- name: member,
|
|
|
- role: '专家',
|
|
|
- team: '待分配梯队',
|
|
|
- age: 45,
|
|
|
- education: '研究生',
|
|
|
- direction: ['领域研究', '项目评审'],
|
|
|
- achievement: '已录入专家基础信息,详细履历可继续补充。',
|
|
|
- tasks: ['立项预审', '专项咨询'],
|
|
|
- tags: ['专家', '待补充'],
|
|
|
- records: []
|
|
|
- }
|
|
|
+const handleMemberClick = (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
|
|
|
}
|
|
|
|
|
|
@@ -908,7 +876,7 @@ const getList = async () => {
|
|
|
try {
|
|
|
queryParams.expertName = searchQuery.value?.trim() || undefined
|
|
|
const data = await getExpertList(queryParams)
|
|
|
- list.value = data.list
|
|
|
+ list.value = data
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|