|
@@ -0,0 +1,1971 @@
|
|
|
|
|
+<template>
|
|
|
|
|
+ <div class="expert-step-table">
|
|
|
|
|
+ <el-card>
|
|
|
|
|
+ <div class="search-layout">
|
|
|
|
|
+ <section class="filter-panel">
|
|
|
|
|
+ <div class="filter-fields">
|
|
|
|
|
+ <el-input v-model="queryParams.expertName" placeholder="姓名 / 专业 / 业绩" clearable />
|
|
|
|
|
+
|
|
|
|
|
+ <el-select v-model="queryParams.education" placeholder="请选择专家学历" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_EDUCATION)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="dict.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+
|
|
|
|
|
+ <el-select v-model="queryParams.echelon" placeholder="请选择专家梯队" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_ECHELON)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="dict.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="flex">
|
|
|
|
|
+ <el-button type="primary" @click="handleAdd"
|
|
|
|
|
+ ><Icon icon="ep:plus" class="mr-5px" />新增</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <el-button type="primary" @click="getList"
|
|
|
|
|
+ ><Icon icon="ep:search" class="mr-5px" />搜索</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+
|
|
|
|
|
+ <el-button type="primary" @click="handleReset"
|
|
|
|
|
+ ><Icon icon="ep:refresh" class="mr-5px" />重置</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <aside class="word-cloud-panel">
|
|
|
|
|
+ <div class="word-cloud">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="word in wordCloudWords"
|
|
|
|
|
+ :key="word.text"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="cloud-word"
|
|
|
|
|
+ :style="{
|
|
|
|
|
+ left: word.left,
|
|
|
|
|
+ top: word.top,
|
|
|
|
|
+ '--rotate': `${word.rotate}deg`,
|
|
|
|
|
+ fontSize: `${word.size}px`,
|
|
|
|
|
+ color: word.color,
|
|
|
|
|
+ transform: `rotate(${word.rotate}deg)`
|
|
|
|
|
+ }"
|
|
|
|
|
+ @click="handleWordClick(word.text)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ word.text }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </aside>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div
|
|
|
|
|
+ v-for="(row, rowIndex) in filteredTableData"
|
|
|
|
|
+ :key="row.teamName"
|
|
|
|
|
+ class="ladder-row"
|
|
|
|
|
+ :style="{ width: row.tableWidth, '--row-bg': row.backgroundColor }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <div class="team-cell">
|
|
|
|
|
+ <span class="team-index">0{{ rowIndex + 1 }}</span>
|
|
|
|
|
+ <span class="team-name">{{ row.teamName }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <table class="content-table" :aria-label="row.teamName">
|
|
|
|
|
+ <colgroup>
|
|
|
|
|
+ <col
|
|
|
|
|
+ v-for="(_, index) in row.columnWidths || []"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :style="{ width: `${getColumnPercent(row, index)}%` }"
|
|
|
|
|
+ />
|
|
|
|
|
+ </colgroup>
|
|
|
|
|
+
|
|
|
|
|
+ <tbody>
|
|
|
|
|
+ <tr>
|
|
|
|
|
+ <td
|
|
|
|
|
+ v-for="(cell, index) in row.cells || []"
|
|
|
|
|
+ :key="index"
|
|
|
|
|
+ :class="['step-cell', cell.tone === 'danger' ? 'notice-cell' : '']"
|
|
|
|
|
+ >
|
|
|
|
|
+ <span class="cell-bookmark">{{ row.bookmarks[index] }}</span>
|
|
|
|
|
+
|
|
|
|
|
+ <template v-if="cell.type === 'members'">
|
|
|
|
|
+ <button
|
|
|
|
|
+ v-for="member in cell.members"
|
|
|
|
|
+ :key="member.id"
|
|
|
|
|
+ type="button"
|
|
|
|
|
+ class="member-link"
|
|
|
|
|
+ @click="handleMemberClick(member)"
|
|
|
|
|
+ >
|
|
|
|
|
+ {{ member.expertName }}
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </template>
|
|
|
|
|
+
|
|
|
|
|
+ <span v-else :class="['cell-text', cell.tone === 'danger' ? 'is-danger' : '']">
|
|
|
|
|
+ {{ cell.text }}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ </td>
|
|
|
|
|
+ </tr>
|
|
|
|
|
+ </tbody>
|
|
|
|
|
+ </table>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-card>
|
|
|
|
|
+
|
|
|
|
|
+ <el-drawer
|
|
|
|
|
+ v-model="drawerVisible"
|
|
|
|
|
+ size="56%"
|
|
|
|
|
+ :with-header="false"
|
|
|
|
|
+ class="expert-drawer"
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ >
|
|
|
|
|
+ <template v-if="activeExpert">
|
|
|
|
|
+ <div class="drawer-shell" v-loading="drawerLoading">
|
|
|
|
|
+ <header class="drawer-header">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <h2>{{ activeExpert.name }} - 专家详情</h2>
|
|
|
|
|
+ <p>
|
|
|
|
|
+ {{ activeExpert.role }} / {{ activeExpert.team }} /
|
|
|
|
|
+ {{ activeExpert.direction.join('、') }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </header>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="drawer-profile">
|
|
|
|
|
+ <div class="profile-aside">
|
|
|
|
|
+ <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
|
|
|
|
|
+ }}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="profile-main">
|
|
|
|
|
+ <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.direction.join('、') || '-' }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <span class="info-label">专业软件技能</span>
|
|
|
|
|
+ <strong>{{ activeExpert.softwareSkills.join('、') || '-' }}</strong>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="info-block">
|
|
|
|
|
+ <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">
|
|
|
|
|
+ <el-button type="primary" size="large" @click="handleJoinTeam">加入团队</el-button>
|
|
|
|
|
+ <el-button size="large" type="primary" plain @click="handleEdit"
|
|
|
|
|
+ >维护档案</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-button size="large" plain>下载履历</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section class="record-panel">
|
|
|
|
|
+ <div class="record-header">
|
|
|
|
|
+ <h3>所在团队</h3>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ :data="currentExpertDetail?.teams || []"
|
|
|
|
|
+ class="record-table"
|
|
|
|
|
+ table-layout="fixed"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column prop="teamName" label="团队名称" min-width="120" align="center" />
|
|
|
|
|
+ <el-table-column label="团队所属公司" min-width="120" align="center">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ getDeptName(row.deptId) || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="团队 Logo" align="center" width="120">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-image
|
|
|
|
|
+ v-if="row.avatar"
|
|
|
|
|
+ :src="row.avatar"
|
|
|
|
|
+ fit="cover"
|
|
|
|
|
+ class="team-logo"
|
|
|
|
|
+ :preview-src-list="[row.avatar]"
|
|
|
|
|
+ preview-teleported
|
|
|
|
|
+ />
|
|
|
|
|
+ <span v-else>-</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column
|
|
|
|
|
+ prop="summary"
|
|
|
|
|
+ label="团队概述"
|
|
|
|
|
+ min-width="240"
|
|
|
|
|
+ align="center"
|
|
|
|
|
+ show-overflow-tooltip
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+ </section>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-drawer>
|
|
|
|
|
+
|
|
|
|
|
+ <Dialog
|
|
|
|
|
+ :title="dialogTitle"
|
|
|
|
|
+ v-model="dialogVisible"
|
|
|
|
|
+ :close-on-click-modal="false"
|
|
|
|
|
+ :close-on-press-escape="false"
|
|
|
|
|
+ destroy-on-close
|
|
|
|
|
+ @close="closeDialog"
|
|
|
|
|
+ width="60%"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-form
|
|
|
|
|
+ ref="formRef"
|
|
|
|
|
+ :model="formData"
|
|
|
|
|
+ :rules="formRules"
|
|
|
|
|
+ v-loading="formLoading"
|
|
|
|
|
+ label-width="auto"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="部门" prop="deptId">
|
|
|
|
|
+ <el-tree-select
|
|
|
|
|
+ clearable
|
|
|
|
|
+ v-model="formData.deptId"
|
|
|
|
|
+ :data="deptList2"
|
|
|
|
|
+ :props="defaultProps"
|
|
|
|
|
+ :check-strictly="false"
|
|
|
|
|
+ node-key="id"
|
|
|
|
|
+ filterable
|
|
|
|
|
+ placeholder="请选择所在部门"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="专家姓名" prop="expertName">
|
|
|
|
|
+ <el-input v-model="formData.expertName" placeholder="请输入专家姓名" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="专家序列" prop="expertRank">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ multiple
|
|
|
|
|
+ v-model="formData.expertRank"
|
|
|
|
|
+ placeholder="请选择专家序列"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_RANK)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="Number(dict.value)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="专家梯队" prop="echelon">
|
|
|
|
|
+ <el-select v-model="formData.echelon" placeholder="请选择专家梯队" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_ECHELON)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="dict.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="学历" prop="education">
|
|
|
|
|
+ <el-select v-model="formData.education" placeholder="请选择专家学历" clearable>
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_EDUCATION)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="dict.value"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="专业方向" prop="specialDirection">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="formData.specialDirection"
|
|
|
|
|
+ placeholder="请选择专业方向"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ multiple
|
|
|
|
|
+ filterable
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_TECHNICAL)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="Number(dict.value)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="专业软件技能" prop="softwareSkills">
|
|
|
|
|
+ <el-select
|
|
|
|
|
+ v-model="formData.softwareSkills"
|
|
|
|
|
+ placeholder="请选择专业软件技能"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ multiple
|
|
|
|
|
+ filterable
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-option
|
|
|
|
|
+ v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_SOFTWARE)"
|
|
|
|
|
+ :key="dict.value"
|
|
|
|
|
+ :label="dict.label"
|
|
|
|
|
+ :value="Number(dict.value)"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-select>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="所属团队" prop="teamIds">
|
|
|
|
|
+ <div class="team-select-field">
|
|
|
|
|
+ <el-input :model-value="selectedTeamNames" placeholder="请选择所属团队" readonly />
|
|
|
|
|
+ <el-button type="primary" plain @click="openTeamDialog">选择团队</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="毕业院校" prop="school">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="formData.school"
|
|
|
|
|
+ placeholder="请输入毕业院校"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="出生日期" prop="birthday">
|
|
|
|
|
+ <el-date-picker
|
|
|
|
|
+ v-model="formData.birthday"
|
|
|
|
|
+ type="date"
|
|
|
|
|
+ value-format="x"
|
|
|
|
|
+ placeholder="请选择出生日期"
|
|
|
|
|
+ style="width: 100%"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="头像" prop="avatar">
|
|
|
|
|
+ <UploadImg v-model="formData.avatar" height="50px" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="历任岗位" prop="formerPost">
|
|
|
|
|
+ <el-input v-model="formData.formerPost" placeholder="请输入历任岗位" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="岗位" prop="jobTitle">
|
|
|
|
|
+ <el-input v-model="formData.jobTitle" placeholder="请输入岗位" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+
|
|
|
|
|
+ <el-col :span="12">
|
|
|
|
|
+ <el-form-item label="职称" prop="professionalTitle">
|
|
|
|
|
+ <el-input v-model="formData.professionalTitle" placeholder="请输入职称" />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+
|
|
|
|
|
+ <el-row :gutter="20">
|
|
|
|
|
+ <el-col :span="24">
|
|
|
|
|
+ <el-form-item label="主要业绩" prop="achievement">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ type="textarea"
|
|
|
|
|
+ v-model="formData.achievement"
|
|
|
|
|
+ placeholder="请输入主要业绩"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-col>
|
|
|
|
|
+ </el-row>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <el-button @click="closeDialog">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="submitExpertForm" :loading="submitLoading"
|
|
|
|
|
+ >确 定</el-button
|
|
|
|
|
+ >
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+
|
|
|
|
|
+ <Dialog v-model="teamDialogVisible" title="选择所属团队" width="70%" class="team-picker-dialog">
|
|
|
|
|
+ <el-form :inline="true" :model="teamQueryParams" class="-mb-15px">
|
|
|
|
|
+ <el-form-item label="团队名称">
|
|
|
|
|
+ <el-input
|
|
|
|
|
+ v-model="teamQueryParams.teamName"
|
|
|
|
|
+ placeholder="请输入团队名称"
|
|
|
|
|
+ clearable
|
|
|
|
|
+ @keyup.enter="handleTeamQuery"
|
|
|
|
|
+ />
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ <el-form-item>
|
|
|
|
|
+ <el-button @click="handleTeamQuery">
|
|
|
|
|
+ <Icon icon="ep:search" class="mr-5px" />
|
|
|
|
|
+ 搜索
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ <el-button @click="resetTeamQuery">
|
|
|
|
|
+ <Icon icon="ep:refresh" class="mr-5px" />
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </el-button>
|
|
|
|
|
+ </el-form-item>
|
|
|
|
|
+ </el-form>
|
|
|
|
|
+
|
|
|
|
|
+ <div class="team-picker-table-wrap">
|
|
|
|
|
+ <el-table
|
|
|
|
|
+ ref="teamTableRef"
|
|
|
|
|
+ v-loading="teamLoading"
|
|
|
|
|
+ :data="teamList"
|
|
|
|
|
+ row-key="id"
|
|
|
|
|
+ height="calc(100vh - 400px)"
|
|
|
|
|
+ @selection-change="handleTeamSelectionChange"
|
|
|
|
|
+ >
|
|
|
|
|
+ <el-table-column type="selection" width="55" reserve-selection />
|
|
|
|
|
+ <el-table-column label="团队名称" prop="teamName" width="120" />
|
|
|
|
|
+ <el-table-column label="团队所属公司" align="center" min-width="120">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ {{ getDeptName(row.deptId) || '-' }}
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="团队 Logo" align="center" width="120">
|
|
|
|
|
+ <template #default="{ row }">
|
|
|
|
|
+ <el-image
|
|
|
|
|
+ v-if="row.avatar"
|
|
|
|
|
+ :src="row.avatar"
|
|
|
|
|
+ fit="cover"
|
|
|
|
|
+ class="team-logo"
|
|
|
|
|
+ :preview-src-list="[row.avatar]"
|
|
|
|
|
+ preview-teleported
|
|
|
|
|
+ />
|
|
|
|
|
+ <span v-else>-</span>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </el-table-column>
|
|
|
|
|
+ <el-table-column label="团队概述" prop="summary" min-width="240" show-overflow-tooltip />
|
|
|
|
|
+ </el-table>
|
|
|
|
|
+
|
|
|
|
|
+ <Pagination
|
|
|
|
|
+ v-model:page="teamQueryParams.pageNo"
|
|
|
|
|
+ v-model:limit="teamQueryParams.pageSize"
|
|
|
|
|
+ :total="teamTotal"
|
|
|
|
|
+ @pagination="getTeamOptionsList"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <template #footer>
|
|
|
|
|
+ <div class="team-picker-footer">
|
|
|
|
|
+ <el-button @click="teamDialogVisible = false">取 消</el-button>
|
|
|
|
|
+ <el-button type="primary" @click="confirmTeamSelection">确 定</el-button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </template>
|
|
|
|
|
+ </Dialog>
|
|
|
|
|
+ </div>
|
|
|
|
|
+</template>
|
|
|
|
|
+
|
|
|
|
|
+<script setup lang="ts">
|
|
|
|
|
+import { computed, nextTick, onMounted, reactive, ref } from 'vue'
|
|
|
|
|
+import {
|
|
|
|
|
+ createExpert,
|
|
|
|
|
+ getExpertDetail,
|
|
|
|
|
+ getExpertList,
|
|
|
|
|
+ getTeamList,
|
|
|
|
|
+ updateExpert
|
|
|
|
|
+} from '@/api/expert'
|
|
|
|
|
+import * as DeptApi from '@/api/system/dept'
|
|
|
|
|
+import { defaultProps } from '@/utils/tree'
|
|
|
|
|
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
|
|
|
|
|
+import { ElTable } from 'element-plus'
|
|
|
|
|
+
|
|
|
|
|
+const defaultFormData = () => ({
|
|
|
|
|
+ id: undefined,
|
|
|
|
|
+ expertName: '',
|
|
|
|
|
+ achievement: '',
|
|
|
|
|
+ echelon: '',
|
|
|
|
|
+ education: '',
|
|
|
|
|
+ specialDirection: [],
|
|
|
|
|
+ softwareSkills: [],
|
|
|
|
|
+ teamIds: [],
|
|
|
|
|
+ school: '',
|
|
|
|
|
+ birthday: null,
|
|
|
|
|
+ avatar: '',
|
|
|
|
|
+ deptId: '',
|
|
|
|
|
+ formerPost: '',
|
|
|
|
|
+ jobTitle: '',
|
|
|
|
|
+ professionalTitle: '',
|
|
|
|
|
+ expertRank: []
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const deptList2 = ref<Tree[]>([]) // 树形结构
|
|
|
|
|
+
|
|
|
|
|
+const formLoading = ref(false) // 表单加载中
|
|
|
|
|
+const submitLoading = ref(false) // 提交按钮加载中
|
|
|
|
|
+const teamLoading = ref(false)
|
|
|
|
|
+const drawerLoading = ref(false)
|
|
|
|
|
+
|
|
|
|
|
+// 对话框相关
|
|
|
|
|
+const dialogVisible = ref(false)
|
|
|
|
|
+const dialogTitle = ref('')
|
|
|
|
|
+const isEdit = ref(false)
|
|
|
|
|
+const teamDialogVisible = ref(false)
|
|
|
|
|
+const teamDialogMode = ref<'form' | 'drawer'>('form')
|
|
|
|
|
+
|
|
|
|
|
+// 表单相关
|
|
|
|
|
+const formRef = ref()
|
|
|
|
|
+const formData = ref(defaultFormData())
|
|
|
|
|
+const teamTableRef = ref<InstanceType<typeof ElTable>>()
|
|
|
|
|
+const teamList = ref<any[]>([])
|
|
|
|
|
+const teamTotal = ref(0)
|
|
|
|
|
+const tempSelectedTeams = ref<any[]>([])
|
|
|
|
|
+const selectedTeams = ref<any[]>([])
|
|
|
|
|
+const tempSelectedTeamMap = ref<Record<number, any>>({})
|
|
|
|
|
+const syncingTeamSelection = ref(false)
|
|
|
|
|
+const currentExpertDetail = ref<any>(null)
|
|
|
|
|
+
|
|
|
|
|
+const formRules = {
|
|
|
|
|
+ expertName: [{ required: true, message: '请输入专家名称', trigger: 'blur' }],
|
|
|
|
|
+ achievement: [{ required: true, message: '请输入成果', trigger: 'blur' }],
|
|
|
|
|
+ echelon: [{ required: true, message: '请选择专家层级', trigger: 'blur' }],
|
|
|
|
|
+ education: [{ required: true, message: '请选择专家学历', trigger: 'blur' }],
|
|
|
|
|
+ specialDirection: [{ required: true, message: '请选择专业方向', trigger: 'blur' }],
|
|
|
|
|
+ // softwareSkills: [{ required: true, message: '请选择专业软件技能', trigger: 'blur' }],
|
|
|
|
|
+ // teamIds: [{ required: true, message: '请选择所属团队', trigger: 'blur' }],
|
|
|
|
|
+ school: [{ required: true, message: '请输入毕业院校', trigger: 'blur' }],
|
|
|
|
|
+ birthday: [{ required: true, message: '请选择出生日期', trigger: 'blur' }],
|
|
|
|
|
+ avatar: [{ required: true, message: '请上传头像', trigger: 'blur' }],
|
|
|
|
|
+ deptId: [{ required: true, message: '请选择部门', trigger: 'blur' }],
|
|
|
|
|
+ // formerPost: [{ required: true, message: '请输入历任岗位', trigger: 'blur' }],
|
|
|
|
|
+ // jobTitle: [{ required: true, message: '请输入岗位', trigger: 'blur' }],
|
|
|
|
|
+ // professionalTitle: [{ required: true, message: '请输入职称', trigger: 'blur' }],
|
|
|
|
|
+ expertRank: [{ required: true, message: '请选择专家序列', trigger: 'blur' }]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 重置表单
|
|
|
|
|
+const resetForm = () => {
|
|
|
|
|
+ formData.value = defaultFormData()
|
|
|
|
|
+ selectedTeams.value = []
|
|
|
|
|
+ tempSelectedTeams.value = []
|
|
|
|
|
+ tempSelectedTeamMap.value = {}
|
|
|
|
|
+ formRef.value?.clearValidate()
|
|
|
|
|
+}
|
|
|
|
|
+// 关闭对话框
|
|
|
|
|
+const closeDialog = () => {
|
|
|
|
|
+ dialogVisible.value = false
|
|
|
|
|
+ resetForm()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const fillTeamSelection = (teams: any[] = []) => {
|
|
|
|
|
+ selectedTeams.value = [...teams]
|
|
|
|
|
+ tempSelectedTeams.value = [...teams]
|
|
|
|
|
+ tempSelectedTeamMap.value = Object.fromEntries(teams.map((team) => [team.id, team]))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const buildFormDataFromExpertDetail = (detail: any) => {
|
|
|
|
|
+ const teams = detail?.teams || []
|
|
|
|
|
+ fillTeamSelection(teams)
|
|
|
|
|
+ return {
|
|
|
|
|
+ id: detail?.id,
|
|
|
|
|
+ expertName: detail?.expertName || '',
|
|
|
|
|
+ achievement: detail?.achievement || '',
|
|
|
|
|
+ echelon: detail?.echelon || '',
|
|
|
|
|
+ education: detail?.education || '',
|
|
|
|
|
+ specialDirection: detail?.specialDirection || [],
|
|
|
|
|
+ softwareSkills: detail?.softwareSkills || [],
|
|
|
|
|
+ teamIds: teams.length ? teams.map((team) => team.id) : detail?.teamIds || [],
|
|
|
|
|
+ school: detail?.school || '',
|
|
|
|
|
+ birthday: detail?.birthday || null,
|
|
|
|
|
+ avatar: detail?.avatar || '',
|
|
|
|
|
+ deptId: detail?.deptId || '',
|
|
|
|
|
+ formerPost: detail?.formerPost || '',
|
|
|
|
|
+ jobTitle: detail?.jobTitle || '',
|
|
|
|
|
+ professionalTitle: detail?.professionalTitle || '',
|
|
|
|
|
+ expertRank: detail?.expertRank || []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type CellItem =
|
|
|
|
|
+ | {
|
|
|
|
|
+ type: 'members'
|
|
|
|
|
+ members: ExpertItem[]
|
|
|
|
|
+ }
|
|
|
|
|
+ | {
|
|
|
|
|
+ type: 'text'
|
|
|
|
|
+ text: string
|
|
|
|
|
+ tone?: 'normal' | 'danger'
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+type TableRow = {
|
|
|
|
|
+ teamName: string
|
|
|
|
|
+ education: string
|
|
|
|
|
+ searchText: string
|
|
|
|
|
+ bookmarks: string[]
|
|
|
|
|
+ tableWidth: string
|
|
|
|
|
+ backgroundColor: string
|
|
|
|
|
+ columnWidths: number[]
|
|
|
|
|
+ cells: CellItem[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ExpertRecord = {
|
|
|
|
|
+ date: string
|
|
|
|
|
+ event: string
|
|
|
|
|
+ project: string
|
|
|
|
|
+ status: string
|
|
|
|
|
+ statusType: 'success' | 'primary'
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ExpertProfile = {
|
|
|
|
|
+ id: number
|
|
|
|
|
+ name: string
|
|
|
|
|
+ avatar: string
|
|
|
|
|
+ role: string
|
|
|
|
|
+ team: string
|
|
|
|
|
+ company: string
|
|
|
|
|
+ age: number
|
|
|
|
|
+ education: string
|
|
|
|
|
+ direction: string[]
|
|
|
|
|
+ softwareSkills: string[]
|
|
|
|
|
+ teamNames: string[]
|
|
|
|
|
+ teamIds: number[]
|
|
|
|
|
+ school: string
|
|
|
|
|
+ birthdayText: string
|
|
|
|
|
+ formerPost: string
|
|
|
|
|
+ jobTitle: string
|
|
|
|
|
+ professionalTitle: string
|
|
|
|
|
+ tags: string[]
|
|
|
|
|
+ achievement: string
|
|
|
|
|
+ records: ExpertRecord[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+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[]
|
|
|
|
|
+ teams?: Array<{ id: number; teamName?: string }>
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+type ExpertGroup = {
|
|
|
|
|
+ echelonLabel: string
|
|
|
|
|
+ num: number
|
|
|
|
|
+ sort: number
|
|
|
|
|
+ experts: ExpertItem[]
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const selectedMember = ref('')
|
|
|
|
|
+const selectedEducation = ref('')
|
|
|
|
|
+const selectedTeam = ref('')
|
|
|
|
|
+const drawerVisible = ref(false)
|
|
|
|
|
+const activeExpert = ref<ExpertProfile | null>(null)
|
|
|
|
|
+
|
|
|
|
|
+const resetQueryParams = () => ({
|
|
|
|
|
+ deptId: undefined,
|
|
|
|
|
+ education: undefined,
|
|
|
|
|
+ echelon: undefined,
|
|
|
|
|
+ expertName: undefined
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const wordCloudWords = [
|
|
|
|
|
+ { text: '地质', size: 24, rotate: -8, color: '#7fcd72', left: '8%', top: '52%' },
|
|
|
|
|
+ { text: '油藏', size: 30, rotate: 6, color: '#8ea4d8', left: '26%', top: '38%' },
|
|
|
|
|
+ { text: '钻井', size: 27, rotate: -10, color: '#f0df62', left: '46%', top: '22%' },
|
|
|
|
|
+ { text: '采油工艺', size: 22, rotate: 8, color: '#b26c41', left: '60%', top: '14%' },
|
|
|
|
|
+ { text: '压裂', size: 21, rotate: -16, color: '#3d79a5', left: '74%', top: '48%' },
|
|
|
|
|
+ { text: '酸化', size: 18, rotate: 12, color: '#f1b477', left: '14%', top: '16%' },
|
|
|
|
|
+ { text: '修井', size: 19, rotate: -12, color: '#d26ea8', left: '36%', top: '66%' },
|
|
|
|
|
+ { text: '博士', size: 17, rotate: 5, color: '#7e8d64', left: '58%', top: '64%' },
|
|
|
|
|
+ { text: '研究生', size: 16, rotate: -5, color: '#7f85b5', left: '79%', top: '28%' }
|
|
|
|
|
+]
|
|
|
|
|
+
|
|
|
|
|
+const selectedTeamNames = computed(() =>
|
|
|
|
|
+ selectedTeams.value.map((team) => team.teamName).join('、')
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+const teamQueryParams = reactive({
|
|
|
|
|
+ pageNo: 1,
|
|
|
|
|
+ pageSize: 10,
|
|
|
|
|
+ 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 rankOptions = computed(() => getStrDictOptions(DICT_TYPE.RD_EXPERT_RANK))
|
|
|
|
|
+
|
|
|
|
|
+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 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[] = [], teams?: Array<{ id: number; teamName?: string }>) => {
|
|
|
|
|
+ if (teams?.length) {
|
|
|
|
|
+ return teams
|
|
|
|
|
+ .map((team) => team.teamName || teamNameMap.value.get(team.id))
|
|
|
|
|
+ .filter(Boolean) as string[]
|
|
|
|
|
+ }
|
|
|
|
|
+ return 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()
|
|
|
|
|
+ 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 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)
|
|
|
|
|
+ const softwareSkillLabels = getDictLabels(DICT_TYPE.RD_EXPERT_SOFTWARE, expert.softwareSkills)
|
|
|
|
|
+ const teamIds = expert.teams?.length ? expert.teams.map((team) => team.id) : expert.teamIds || []
|
|
|
|
|
+ const teamNames = getTeamNames(teamIds, expert.teams)
|
|
|
|
|
+ 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 : ['暂无'],
|
|
|
|
|
+ softwareSkills: softwareSkillLabels,
|
|
|
|
|
+ teamNames,
|
|
|
|
|
+ teamIds,
|
|
|
|
|
+ school: expert.school || '',
|
|
|
|
|
+ birthdayText: formatBirthday(expert.birthday),
|
|
|
|
|
+ formerPost: expert.formerPost || '',
|
|
|
|
|
+ jobTitle: expert.jobTitle || '',
|
|
|
|
|
+ professionalTitle: expert.professionalTitle || '',
|
|
|
|
|
+ tags: [expert.professionalTitle].filter(Boolean),
|
|
|
|
|
+ achievement: expert.achievement || '暂无',
|
|
|
|
|
+ records: []
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getColumnPercent = (row: TableRow, index: number) => {
|
|
|
|
|
+ if (!row.columnWidths || row.columnWidths.length === 0) {
|
|
|
|
|
+ return '0'
|
|
|
|
|
+ }
|
|
|
|
|
+ const total = row.columnWidths.reduce((sum, width) => sum + width, 0)
|
|
|
|
|
+ const value = row.columnWidths[index] ?? 0
|
|
|
|
|
+ if (total === 0) {
|
|
|
|
|
+ return '0'
|
|
|
|
|
+ }
|
|
|
|
|
+ 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 bookmarks = rankOptions.value.map((item) => item.label)
|
|
|
|
|
+ const columnWidths =
|
|
|
|
|
+ preset.columnWidths.length === rankOptions.value.length
|
|
|
|
|
+ ? preset.columnWidths
|
|
|
|
|
+ : Array(rankOptions.value.length).fill(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()
|
|
|
|
|
+ const cells: CellItem[] = rankOptions.value.map((option) => ({
|
|
|
|
|
+ type: 'members',
|
|
|
|
|
+ members: group.experts.filter((expert) =>
|
|
|
|
|
+ (expert.expertRank || []).some((rank) => String(rank) === String(option.value))
|
|
|
|
|
+ )
|
|
|
|
|
+ }))
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ teamName: group.echelonLabel,
|
|
|
|
|
+ education: educationLabels.join('、') || '-',
|
|
|
|
|
+ searchText,
|
|
|
|
|
+ bookmarks,
|
|
|
|
|
+ tableWidth: preset.tableWidth,
|
|
|
|
|
+ backgroundColor: preset.backgroundColor,
|
|
|
|
|
+ columnWidths,
|
|
|
|
|
+ cells
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const filteredTableData = computed(() => {
|
|
|
|
|
+ return tableData.value.filter((row) => {
|
|
|
|
|
+ const educationMatch = selectedEducation.value
|
|
|
|
|
+ ? row.education === selectedEducation.value
|
|
|
|
|
+ : true
|
|
|
|
|
+ const selectedTeamMatch = selectedTeam.value ? row.teamName === selectedTeam.value : true
|
|
|
|
|
+
|
|
|
|
|
+ if (!educationMatch || !selectedTeamMatch) {
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ return true
|
|
|
|
|
+ })
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const handleMemberClick = async (member: ExpertItem) => {
|
|
|
|
|
+ selectedMember.value = member.expertName
|
|
|
|
|
+ drawerVisible.value = true
|
|
|
|
|
+ drawerLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const detail = await getExpertDetail(member.id)
|
|
|
|
|
+ currentExpertDetail.value = detail
|
|
|
|
|
+ activeExpert.value = buildExpertProfile(detail)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ drawerLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleWordClick = (word: string) => {
|
|
|
|
|
+ queryParams.expertName = word
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleTeamSelectionChange = (rows: any[]) => {
|
|
|
|
|
+ if (syncingTeamSelection.value) return
|
|
|
|
|
+ const currentPageIds = teamList.value.map((item) => item.id)
|
|
|
|
|
+ const nextMap = { ...tempSelectedTeamMap.value }
|
|
|
|
|
+ currentPageIds.forEach((id) => {
|
|
|
|
|
+ delete nextMap[id]
|
|
|
|
|
+ })
|
|
|
|
|
+ rows.forEach((row) => {
|
|
|
|
|
+ nextMap[row.id] = row
|
|
|
|
|
+ })
|
|
|
|
|
+ tempSelectedTeamMap.value = nextMap
|
|
|
|
|
+ tempSelectedTeams.value = Object.values(nextMap)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const syncTeamSelection = async () => {
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ syncingTeamSelection.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ teamTableRef.value?.clearSelection()
|
|
|
|
|
+ const selectedTeamIds = new Set(Object.keys(tempSelectedTeamMap.value).map((id) => Number(id)))
|
|
|
|
|
+ teamList.value.forEach((row) => {
|
|
|
|
|
+ if (selectedTeamIds.has(row.id)) {
|
|
|
|
|
+ teamTableRef.value?.toggleRowSelection(row, true)
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ syncingTeamSelection.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getTeamOptionsList = async () => {
|
|
|
|
|
+ teamLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const res = await getTeamList(teamQueryParams)
|
|
|
|
|
+ teamList.value = res.list || []
|
|
|
|
|
+ teamTotal.value = res.total || 0
|
|
|
|
|
+ await syncTeamSelection()
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ teamLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleTeamQuery = async () => {
|
|
|
|
|
+ teamQueryParams.pageNo = 1
|
|
|
|
|
+ await getTeamOptionsList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const resetTeamQuery = async () => {
|
|
|
|
|
+ teamQueryParams.pageNo = 1
|
|
|
|
|
+ teamQueryParams.teamName = undefined
|
|
|
|
|
+ await getTeamOptionsList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openTeamDialog = async () => {
|
|
|
|
|
+ teamDialogMode.value = 'form'
|
|
|
|
|
+ teamDialogVisible.value = true
|
|
|
|
|
+ tempSelectedTeams.value = [...selectedTeams.value]
|
|
|
|
|
+ tempSelectedTeamMap.value = Object.fromEntries(selectedTeams.value.map((team) => [team.id, team]))
|
|
|
|
|
+ await getTeamOptionsList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const ensureSelectedTeams = async (teamIds: number[]) => {
|
|
|
|
|
+ if (!teamIds?.length) {
|
|
|
|
|
+ selectedTeams.value = []
|
|
|
|
|
+ tempSelectedTeams.value = []
|
|
|
|
|
+ tempSelectedTeamMap.value = {}
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const teamMap = new Map<number, any>()
|
|
|
|
|
+ ;[...selectedTeams.value, ...teamList.value].forEach((team) => {
|
|
|
|
|
+ teamMap.set(team.id, team)
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ const missingIds = teamIds.filter((id) => !teamMap.has(id))
|
|
|
|
|
+ if (missingIds.length) {
|
|
|
|
|
+ const res = await getTeamList({ pageNo: 1, pageSize: 10 })
|
|
|
|
|
+ ;(res.list || []).forEach((team) => {
|
|
|
|
|
+ teamMap.set(team.id, team)
|
|
|
|
|
+ })
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ selectedTeams.value = teamIds.map((id) => teamMap.get(id)).filter(Boolean)
|
|
|
|
|
+ tempSelectedTeams.value = [...selectedTeams.value]
|
|
|
|
|
+ tempSelectedTeamMap.value = Object.fromEntries(selectedTeams.value.map((team) => [team.id, team]))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleJoinTeam = async () => {
|
|
|
|
|
+ if (!activeExpert.value) return
|
|
|
|
|
+
|
|
|
|
|
+ teamDialogMode.value = 'drawer'
|
|
|
|
|
+ const currentTeams = currentExpertDetail.value?.teams || []
|
|
|
|
|
+ if (currentTeams.length) {
|
|
|
|
|
+ selectedTeams.value = [...currentTeams]
|
|
|
|
|
+ tempSelectedTeams.value = [...currentTeams]
|
|
|
|
|
+ tempSelectedTeamMap.value = Object.fromEntries(currentTeams.map((team) => [team.id, team]))
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const currentTeamIds = activeExpert.value.teamIds || []
|
|
|
|
|
+ await ensureSelectedTeams(currentTeamIds)
|
|
|
|
|
+ }
|
|
|
|
|
+ teamDialogVisible.value = true
|
|
|
|
|
+ await getTeamOptionsList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleEdit = () => {
|
|
|
|
|
+ if (!currentExpertDetail.value) return
|
|
|
|
|
+ isEdit.value = true
|
|
|
|
|
+ dialogTitle.value = '编辑专家'
|
|
|
|
|
+ formData.value = buildFormDataFromExpertDetail(currentExpertDetail.value)
|
|
|
|
|
+ dialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const confirmTeamSelection = async () => {
|
|
|
|
|
+ selectedTeams.value = [...tempSelectedTeams.value]
|
|
|
|
|
+ const teamIds = selectedTeams.value.map((team) => team.id)
|
|
|
|
|
+
|
|
|
|
|
+ if (teamDialogMode.value === 'drawer') {
|
|
|
|
|
+ if (!currentExpertDetail.value || !activeExpert.value) return
|
|
|
|
|
+
|
|
|
|
|
+ await updateExpert({
|
|
|
|
|
+ ...currentExpertDetail.value,
|
|
|
|
|
+ teams: selectedTeams.value,
|
|
|
|
|
+ teamIds
|
|
|
|
|
+ })
|
|
|
|
|
+
|
|
|
|
|
+ currentExpertDetail.value = {
|
|
|
|
|
+ ...currentExpertDetail.value,
|
|
|
|
|
+ teams: selectedTeams.value,
|
|
|
|
|
+ teamIds
|
|
|
|
|
+ }
|
|
|
|
|
+ activeExpert.value = buildExpertProfile(currentExpertDetail.value)
|
|
|
|
|
+ teamDialogVisible.value = false
|
|
|
|
|
+ ElMessage.success('加入团队成功')
|
|
|
|
|
+ await getList()
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ formData.value.teamIds = teamIds
|
|
|
|
|
+ teamDialogVisible.value = false
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// 显示新增对话框
|
|
|
|
|
+const handleAdd = () => {
|
|
|
|
|
+ isEdit.value = false
|
|
|
|
|
+ dialogTitle.value = '新增专家'
|
|
|
|
|
+ resetForm()
|
|
|
|
|
+ dialogVisible.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+let loading = ref(false)
|
|
|
|
|
+const list = ref([]) // 列表的数据
|
|
|
|
|
+const queryParams = reactive({
|
|
|
|
|
+ deptId: undefined,
|
|
|
|
|
+ education: undefined,
|
|
|
|
|
+ echelon: undefined,
|
|
|
|
|
+ expertName: undefined
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
|
|
+const handleReset = async () => {
|
|
|
|
|
+ Object.assign(queryParams, resetQueryParams())
|
|
|
|
|
+ await getList()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const submitExpertForm = async () => {
|
|
|
|
|
+ if (!formRef.value) return
|
|
|
|
|
+
|
|
|
|
|
+ const valid = await formRef.value.validate().catch(() => false)
|
|
|
|
|
+ if (!valid) return
|
|
|
|
|
+
|
|
|
|
|
+ submitLoading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const payload = {
|
|
|
|
|
+ ...formData.value,
|
|
|
|
|
+ avatar:
|
|
|
|
|
+ typeof formData.value.avatar === 'string'
|
|
|
|
|
+ ? formData.value.avatar
|
|
|
|
|
+ : formData.value.avatar?.path || ''
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (isEdit.value) {
|
|
|
|
|
+ await updateExpert(payload)
|
|
|
|
|
+ ElMessage.success('修改成功')
|
|
|
|
|
+ if (payload.id) {
|
|
|
|
|
+ const detail = await getExpertDetail(payload.id)
|
|
|
|
|
+ currentExpertDetail.value = detail
|
|
|
|
|
+ activeExpert.value = buildExpertProfile(detail)
|
|
|
|
|
+ }
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await createExpert(payload)
|
|
|
|
|
+ ElMessage.success('新增成功')
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ closeDialog()
|
|
|
|
|
+ await getList()
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(error)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ submitLoading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const getList = async () => {
|
|
|
|
|
+ loading.value = true
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await getExpertList({
|
|
|
|
|
+ ...queryParams,
|
|
|
|
|
+ expertName: queryParams.expertName?.trim() || undefined
|
|
|
|
|
+ })
|
|
|
|
|
+ list.value = data
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ loading.value = false
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+onMounted(async () => {
|
|
|
|
|
+ getList()
|
|
|
|
|
+
|
|
|
|
|
+ deptList2.value = await DeptApi.getSimpleDeptList()
|
|
|
|
|
+})
|
|
|
|
|
+</script>
|
|
|
|
|
+
|
|
|
|
|
+<style scoped lang="scss">
|
|
|
|
|
+.expert-step-table {
|
|
|
|
|
+ --ink: #182536;
|
|
|
|
|
+ --muted-ink: #718092;
|
|
|
|
|
+ --line: rgba(28, 44, 62, 0.18);
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: none;
|
|
|
|
|
+ margin: 0 auto;
|
|
|
|
|
+ // padding: 34px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.expert-step-table el-card {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-select-field {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-select-field :deep(.el-input) {
|
|
|
|
|
+ flex: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// .team-picker-table-wrap {
|
|
|
|
|
+// margin-top: 8px;
|
|
|
|
|
+// margin-bottom: 12px;
|
|
|
|
|
+// }
|
|
|
|
|
+
|
|
|
|
|
+.team-picker-pagination-wrap {
|
|
|
|
|
+ margin-top: 16px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.team-picker-dialog .el-dialog__footer) {
|
|
|
|
|
+ border-top: none;
|
|
|
|
|
+ padding-top: 8px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.team-picker-table) {
|
|
|
|
|
+ margin-bottom: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.team-picker-pagination) {
|
|
|
|
|
+ margin-top: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-picker-footer {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ justify-content: flex-end;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ padding-top: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.search-layout {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: minmax(220px, 0.34fr) minmax(0, 1fr);
|
|
|
|
|
+ gap: 18px;
|
|
|
|
|
+ margin-bottom: 15px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-panel,
|
|
|
|
|
+.word-cloud-panel {
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ padding: 12px;
|
|
|
|
|
+ border: 1px solid rgba(28, 44, 62, 0.1);
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-panel {
|
|
|
|
|
+ background: linear-gradient(160deg, rgba(237, 245, 249, 0.95), rgba(247, 249, 248, 0.9)), #f4f7f8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.word-cloud-panel {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: radial-gradient(circle at 84% 20%, rgba(177, 205, 210, 0.3), transparent 32%), #f8f7f2;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.word-cloud-panel::after {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ right: -70px;
|
|
|
|
|
+ bottom: -90px;
|
|
|
|
|
+ width: 220px;
|
|
|
|
|
+ height: 220px;
|
|
|
|
|
+ border: 1px solid rgba(161, 94, 63, 0.16);
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ content: '';
|
|
|
|
|
+ box-shadow:
|
|
|
|
|
+ 0 0 0 20px rgba(161, 94, 63, 0.04),
|
|
|
|
|
+ 0 0 0 42px rgba(161, 94, 63, 0.03);
|
|
|
|
|
+ pointer-events: none;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-kicker {
|
|
|
|
|
+ margin-bottom: 8px;
|
|
|
|
|
+ color: #8a98a5;
|
|
|
|
|
+ font-family: 'Courier New', monospace;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.14em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-panel h2,
|
|
|
|
|
+.word-cloud-panel h2 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ font-size: 20px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: -0.03em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.panel-description {
|
|
|
|
|
+ margin: 8px 0 18px;
|
|
|
|
|
+ color: var(--muted-ink);
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.65;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-fields {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-fields :deep(.el-input__wrapper),
|
|
|
|
|
+.filter-fields :deep(.el-select__wrapper) {
|
|
|
|
|
+ min-height: 38px;
|
|
|
|
|
+ border-radius: 7px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.78);
|
|
|
|
|
+ box-shadow: 0 0 0 1px rgba(28, 44, 62, 0.09) inset;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-fields :deep(.el-input__wrapper.is-focus),
|
|
|
|
|
+.filter-fields :deep(.el-select__wrapper.is-focused) {
|
|
|
|
|
+ box-shadow: 0 0 0 1px #467da4 inset;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.filter-status {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-top: 18px;
|
|
|
|
|
+ padding-top: 14px;
|
|
|
|
|
+ border-top: 1px dashed rgba(28, 44, 62, 0.15);
|
|
|
|
|
+ color: #718092;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.status-dot {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ height: 6px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #6f9878;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(111, 152, 120, 0.13);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cloud-heading {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cloud-hint {
|
|
|
|
|
+ padding-top: 18px;
|
|
|
|
|
+ color: #9aa6b3;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.word-cloud {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ z-index: 1;
|
|
|
|
|
+ min-height: 180px;
|
|
|
|
|
+ margin-top: 4px;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: radial-gradient(circle at 24% 20%, rgba(255, 255, 255, 0.72), transparent 28%),
|
|
|
|
|
+ linear-gradient(180deg, rgba(255, 255, 255, 0.55), rgba(255, 255, 255, 0.18));
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cloud-word {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ border: 0;
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ // font-weight: 600;
|
|
|
|
|
+ letter-spacing: -0.03em;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ transform-origin: center;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ transform 180ms ease,
|
|
|
|
|
+ opacity 180ms ease,
|
|
|
|
|
+ text-shadow 180ms ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cloud-word:hover {
|
|
|
|
|
+ opacity: 0.72;
|
|
|
|
|
+ text-shadow: 0 5px 12px rgba(49, 91, 115, 0.18);
|
|
|
|
|
+ transform: scale(1.06) rotate(var(--rotate, 0deg));
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cloud-word:focus-visible {
|
|
|
|
|
+ outline: 2px solid #a15e3f;
|
|
|
|
|
+ outline-offset: 5px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-divider {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ margin: 4px 2px 14px;
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.result-count {
|
|
|
|
|
+ color: #9aa6b3;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ letter-spacing: 0.1em;
|
|
|
|
|
+ text-transform: uppercase;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-intro {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: flex-end;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ gap: 32px;
|
|
|
|
|
+ margin-bottom: 28px;
|
|
|
|
|
+ padding: 0 4px 22px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(28, 44, 62, 0.16);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.eyebrow {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ margin-bottom: 12px;
|
|
|
|
|
+ color: #758496;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.18em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.eyebrow-dot,
|
|
|
|
|
+.legend-dot {
|
|
|
|
|
+ display: inline-block;
|
|
|
|
|
+ width: 7px;
|
|
|
|
|
+ height: 7px;
|
|
|
|
|
+ border-radius: 50%;
|
|
|
|
|
+ background: #b26d42;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(178, 109, 66, 0.13);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-intro h1 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ font-size: clamp(30px, 4vw, 46px);
|
|
|
|
|
+ font-weight: 500;
|
|
|
|
|
+ letter-spacing: -0.045em;
|
|
|
|
|
+ line-height: 1;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-intro p {
|
|
|
|
|
+ max-width: 420px;
|
|
|
|
|
+ margin: 12px 0 0;
|
|
|
|
|
+ color: var(--muted-ink);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ line-height: 1.7;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.intro-meta {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 20px;
|
|
|
|
|
+ padding-bottom: 4px;
|
|
|
|
|
+ color: #647285;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.08em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-item {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 9px;
|
|
|
|
|
+ letter-spacing: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.legend-dot {
|
|
|
|
|
+ width: 6px;
|
|
|
|
|
+ height: 6px;
|
|
|
|
|
+ background: #467da4;
|
|
|
|
|
+ box-shadow: 0 0 0 4px rgba(70, 125, 164, 0.13);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.tier-count {
|
|
|
|
|
+ color: #9aa6b3;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.table-list {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ladder-row {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ max-width: 100%;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ min-height: 78px;
|
|
|
|
|
+
|
|
|
|
|
+ transform: translateX(0);
|
|
|
|
|
+ animation: row-in 0.55s both;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ transform 220ms ease,
|
|
|
|
|
+ filter 220ms ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ladder-row:nth-child(2) {
|
|
|
|
|
+ animation-delay: 80ms;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ladder-row:nth-child(3) {
|
|
|
|
|
+ animation-delay: 160ms;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ladder-row:hover {
|
|
|
|
|
+ transform: translateX(5px);
|
|
|
|
|
+ filter: saturate(1.04);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.ladder-row + .ladder-row {
|
|
|
|
|
+ margin-top: -1px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-cell {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex: 0 0 104px;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: flex-start;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+ min-height: 78px;
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ background-color: var(--row-bg);
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-index {
|
|
|
|
|
+ color: rgba(24, 37, 54, 0.38);
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.08em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-name {
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-table {
|
|
|
|
|
+ flex: 1 1 auto;
|
|
|
|
|
+ width: 0;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ table-layout: fixed;
|
|
|
|
|
+ border-collapse: collapse;
|
|
|
|
|
+ border-spacing: 0;
|
|
|
|
|
+ background: transparent;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.content-table td {
|
|
|
|
|
+ height: 78px;
|
|
|
|
|
+ padding: 10px 14px 10px 26px;
|
|
|
|
|
+ border: 1px solid var(--line);
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ background-color: var(--row-bg);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.step-cell {
|
|
|
|
|
+ position: relative;
|
|
|
|
|
+ text-align: center;
|
|
|
|
|
+ vertical-align: middle;
|
|
|
|
|
+ overflow-wrap: anywhere;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cell-bookmark {
|
|
|
|
|
+ position: absolute;
|
|
|
|
|
+ top: 10px;
|
|
|
|
|
+ left: -1px;
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-width: 18px;
|
|
|
|
|
+ height: 48px;
|
|
|
|
|
+ padding: 4px 2px;
|
|
|
|
|
+ border: 1px solid rgba(70, 125, 164, 0.28);
|
|
|
|
|
+ border-left: 0;
|
|
|
|
|
+ border-radius: 0 6px 6px 0;
|
|
|
|
|
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(230, 238, 244, 0.92));
|
|
|
|
|
+ box-shadow: 0 6px 14px rgba(24, 37, 54, 0.08);
|
|
|
|
|
+ color: #47657e;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.08em;
|
|
|
|
|
+ line-height: 1.1;
|
|
|
|
|
+ writing-mode: vertical-rl;
|
|
|
|
|
+ text-orientation: upright;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.notice-cell {
|
|
|
|
|
+ background-image: linear-gradient(135deg, rgba(178, 109, 66, 0.09), transparent 60%);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.member-link {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ margin: 3px 2px;
|
|
|
|
|
+ padding: 4px 7px;
|
|
|
|
|
+ border: 1px solid rgba(70, 125, 164, 0.24);
|
|
|
|
|
+ border-radius: 4px;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.42);
|
|
|
|
|
+ color: #356783;
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+ line-height: 1.2;
|
|
|
|
|
+ transition:
|
|
|
|
|
+ transform 180ms ease,
|
|
|
|
|
+ border-color 180ms ease,
|
|
|
|
|
+ background-color 180ms ease,
|
|
|
|
|
+ color 180ms ease;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.member-link:hover {
|
|
|
|
|
+ border-color: #467da4;
|
|
|
|
|
+ background: #467da4;
|
|
|
|
|
+ color: #fff;
|
|
|
|
|
+ transform: translateY(-2px);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.member-link:focus-visible {
|
|
|
|
|
+ outline: 2px solid #b26d42;
|
|
|
|
|
+ outline-offset: 2px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.cell-text {
|
|
|
|
|
+ color: #687789;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ line-height: 1.65;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.is-danger {
|
|
|
|
|
+ color: #a95f3c;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.member-detail {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ gap: 12px;
|
|
|
|
|
+ margin-top: 22px;
|
|
|
|
|
+ padding: 12px 16px;
|
|
|
|
|
+ border: 1px solid rgba(70, 125, 164, 0.2);
|
|
|
|
|
+ border-left: 3px solid #467da4;
|
|
|
|
|
+ background: rgba(255, 255, 255, 0.62);
|
|
|
|
|
+ color: #637285;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.member-detail strong {
|
|
|
|
|
+ color: var(--ink);
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-label {
|
|
|
|
|
+ color: #467da4;
|
|
|
|
|
+ font-size: 10px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+ letter-spacing: 0.12em;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.detail-copy {
|
|
|
|
|
+ color: #8b97a5;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@keyframes row-in {
|
|
|
|
|
+ from {
|
|
|
|
|
+ opacity: 0;
|
|
|
|
|
+ transform: translateY(10px);
|
|
|
|
|
+ }
|
|
|
|
|
+ to {
|
|
|
|
|
+ opacity: 1;
|
|
|
|
|
+ transform: translateY(0);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 700px) {
|
|
|
|
|
+ .expert-step-table {
|
|
|
|
|
+ padding: 22px 14px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .table-intro {
|
|
|
|
|
+ align-items: flex-start;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 18px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .search-layout {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .word-cloud {
|
|
|
|
|
+ min-height: 160px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .intro-meta {
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ justify-content: space-between;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .team-cell {
|
|
|
|
|
+ flex-basis: 86px;
|
|
|
|
|
+ padding: 10px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .content-table td {
|
|
|
|
|
+ padding: 8px 6px 8px 22px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .member-link {
|
|
|
|
|
+ padding: 4px 5px;
|
|
|
|
|
+ font-size: 11px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .cell-bookmark {
|
|
|
|
|
+ top: 8px;
|
|
|
|
|
+ min-width: 16px;
|
|
|
|
|
+ height: 42px;
|
|
|
|
|
+ font-size: 9px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .member-detail {
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+ gap: 8px;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.expert-drawer) {
|
|
|
|
|
+ --el-drawer-padding-primary: 0;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.expert-drawer.el-drawer.rtl) {
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-drawer__wrapper) {
|
|
|
|
|
+ overflow: hidden !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.el-overlay) {
|
|
|
|
|
+ overflow: hidden !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+:deep(.expert-drawer .el-drawer__body) {
|
|
|
|
|
+ padding: 0;
|
|
|
|
|
+ background: #eef2ef;
|
|
|
|
|
+ overflow-x: hidden !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-shell {
|
|
|
|
|
+ min-height: 100%;
|
|
|
|
|
+ background: #eef2ef;
|
|
|
|
|
+ overflow-x: hidden !important;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-header {
|
|
|
|
|
+ padding: 16px 22px 14px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(24, 37, 54, 0.08);
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-header h2 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ color: #102234;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-header p {
|
|
|
|
|
+ margin: 6px 0 0;
|
|
|
|
|
+ color: #6f7f8e;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 600;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.drawer-profile {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: 150px minmax(0, 1fr);
|
|
|
|
|
+ gap: 16px;
|
|
|
|
|
+ margin: 18px;
|
|
|
|
|
+ padding: 16px;
|
|
|
|
|
+ border: 1px solid rgba(28, 44, 62, 0.08);
|
|
|
|
|
+ border-radius: 14px;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-aside {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-avatar {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-height: 150px;
|
|
|
|
|
+ max-height: 150px;
|
|
|
|
|
+ border-radius: 14px;
|
|
|
|
|
+ background: linear-gradient(145deg, #b7d3cd, #e8f0ee);
|
|
|
|
|
+ color: #0f5b58;
|
|
|
|
|
+ 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: 8px;
|
|
|
|
|
+ flex-wrap: wrap;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-tag {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ padding: 6px 10px;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ background: #f1f4f2;
|
|
|
|
|
+ color: #6e7d7a;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-main {
|
|
|
|
|
+ display: flex;
|
|
|
|
|
+ flex-direction: column;
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-block {
|
|
|
|
|
+ padding: 12px 14px;
|
|
|
|
|
+ border: 1px solid #dbe3df;
|
|
|
|
|
+ border-radius: 12px;
|
|
|
|
|
+ background: #fdfefe;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-label {
|
|
|
|
|
+ display: block;
|
|
|
|
|
+ margin-bottom: 5px;
|
|
|
|
|
+ color: #7a8a89;
|
|
|
|
|
+ font-size: 12px;
|
|
|
|
|
+ // font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.info-block strong {
|
|
|
|
|
+ color: #203130;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+ line-height: 1.55;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-actions {
|
|
|
|
|
+ display: grid;
|
|
|
|
|
+ grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
|
|
|
+ gap: 10px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-actions :deep(.el-button) {
|
|
|
|
|
+ height: 42px;
|
|
|
|
|
+ border-radius: 10px;
|
|
|
|
|
+ font-size: 14px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.profile-actions :deep(.el-button--success) {
|
|
|
|
|
+ background: #237b63;
|
|
|
|
|
+ border-color: #237b63;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-panel {
|
|
|
|
|
+ margin: 0 26px 26px;
|
|
|
|
|
+ border: 1px solid rgba(28, 44, 62, 0.08);
|
|
|
|
|
+ border-radius: 16px;
|
|
|
|
|
+ overflow: hidden;
|
|
|
|
|
+ background: #fff;
|
|
|
|
|
+ box-shadow: 0 8px 30px rgba(28, 44, 62, 0.05);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-header {
|
|
|
|
|
+ padding: 22px 24px 18px;
|
|
|
|
|
+ border-bottom: 1px solid rgba(28, 44, 62, 0.08);
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-header h3 {
|
|
|
|
|
+ margin: 0;
|
|
|
|
|
+ color: #1e2d3a;
|
|
|
|
|
+ font-size: 18px;
|
|
|
|
|
+ font-weight: 800;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-header p {
|
|
|
|
|
+ margin: 6px 0 0;
|
|
|
|
|
+ color: #8a97a3;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-table :deep(.el-table__header th) {
|
|
|
|
|
+ background: #f7faf8;
|
|
|
|
|
+ color: #556574;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-table :deep(.el-table__inner-wrapper) {
|
|
|
|
|
+ overflow-x: hidden;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-table :deep(.el-table__row td) {
|
|
|
|
|
+ padding-top: 14px;
|
|
|
|
|
+ padding-bottom: 14px;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-badge {
|
|
|
|
|
+ display: inline-flex;
|
|
|
|
|
+ align-items: center;
|
|
|
|
|
+ justify-content: center;
|
|
|
|
|
+ min-width: 88px;
|
|
|
|
|
+ padding: 7px 12px;
|
|
|
|
|
+ border-radius: 999px;
|
|
|
|
|
+ font-size: 13px;
|
|
|
|
|
+ font-weight: 700;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-badge.success {
|
|
|
|
|
+ background: #e4f4ec;
|
|
|
|
|
+ color: #1f8a61;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.record-badge.primary {
|
|
|
|
|
+ background: #e8f0fb;
|
|
|
|
|
+ color: #3d6ea8;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+@media (max-width: 900px) {
|
|
|
|
|
+ .drawer-profile {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .profile-avatar {
|
|
|
|
|
+ min-height: 180px;
|
|
|
|
|
+ font-size: 64px;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ .profile-actions {
|
|
|
|
|
+ grid-template-columns: 1fr;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.team-logo {
|
|
|
|
|
+ width: 52px;
|
|
|
|
|
+ height: 52px;
|
|
|
|
|
+ border-radius: 8px;
|
|
|
|
|
+ border: 1px solid #e5e7eb;
|
|
|
|
|
+}
|
|
|
|
|
+</style>
|