Explorar o código

Merge branch 'sso' of shuzhihua/randd-vue into master

yanghao hai 1 mes
pai
achega
805fa68253
Modificáronse 4 ficheiros con 1269 adicións e 213 borrados
  1. 49 0
      src/api/expert/index.ts
  2. 8 1
      src/utils/dict.ts
  3. 690 210
      src/views/expert/list/index.vue
  4. 522 2
      src/views/expert/team/index.vue

+ 49 - 0
src/api/expert/index.ts

@@ -0,0 +1,49 @@
+import request from '@/config/axios'
+
+// 获取专家列表
+export const getExpertList = (params) => {
+  return request.get({ url: '/rd/expert-pool/echelonExperts', params })
+}
+
+// 新建专家
+export const createExpert = (data: any) => {
+  return request.post({ url: '/rd/expert-pool/create', data })
+}
+
+// 更新专家
+export const updateExpert = (data: any) => {
+  return request.put({ url: '/rd/expert-pool/update', data })
+}
+
+// 专家详情
+export const getExpertDetail = (id: number) => {
+  return request.get({ url: `/rd/expert-pool/get?id=${id}` })
+}
+
+/**
+ * 团队管理
+ */
+
+export const getTeamList = (params) => {
+  return request.get({ url: '/rd/team/page', params })
+}
+
+export const createTeam = (data: any) => {
+  return request.post({ url: '/rd/team/create', data })
+}
+
+export const updateTeam = (data: any) => {
+  return request.put({ url: '/rd/team/update', data })
+}
+
+export const deleteTeam = (id: number) => {
+  return request.delete({ url: `/rd/team/delete?id=${id}` })
+}
+
+export const getTeamDetail = (id: number) => {
+  return request.get({ url: `/rd/team/get?id=${id}` })
+}
+
+export const getTeamMemberList = (id) => {
+  return request.get({ url: `/rd/team/get?id=${id}` })
+}

+ 8 - 1
src/utils/dict.ts

@@ -252,5 +252,12 @@ export enum DICT_TYPE {
   IOT_MODBUS_MODE = 'iot_modbus_mode', // IoT Modbus 工作模式
   IOT_MODBUS_FRAME_FORMAT = 'iot_modbus_frame_format', // IoT Modbus 帧格式
 
-  FLOW_TYPE = 'flow_type' // 流程类型
+  FLOW_TYPE = 'flow_type', // 流程类型
+
+  // ========== RD - 研发管理模块  ==========
+  RD_EXPERT_TECHNICAL = 'rd_expert_technical', //专业方向
+  RD_EXPERT_SOFTWARE = 'rd_expert_software', // 专业软件技能
+  RD_EXPERT_EDUCATION = 'rd_expert_education‌', // 学历
+  RD_EXPERT_RANK = 'rd_expert_rank', //专家序列
+  RD_EXPERT_ECHELON = 'rd_expert_echelon' //专家梯队
 }

A diferenza do arquivo foi suprimida porque é demasiado grande
+ 690 - 210
src/views/expert/list/index.vue


+ 522 - 2
src/views/expert/team/index.vue

@@ -1,5 +1,525 @@
 <template>
-  <div class="list"> team </div>
+  <ContentWrap>
+    <el-form
+      ref="queryFormRef"
+      :inline="true"
+      :model="queryParams"
+      class="-mb-15px"
+      label-width="96px"
+    >
+      <el-form-item label="团队所属公司" prop="deptId">
+        <el-tree-select
+          v-model="queryParams.deptId"
+          :data="deptList"
+          :props="defaultProps"
+          check-strictly
+          clearable
+          filterable
+          node-key="id"
+          placeholder="请选择团队所属公司"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item label="团队名称" prop="teamName">
+        <el-input
+          v-model="queryParams.teamName"
+          clearable
+          placeholder="请输入团队名称"
+          class="!w-240px"
+          @keyup.enter="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery">
+          <Icon icon="ep:search" class="mr-5px" />
+          搜索
+        </el-button>
+        <el-button @click="resetQuery">
+          <Icon icon="ep:refresh" class="mr-5px" />
+          重置
+        </el-button>
+        <el-button type="primary" plain @click="openForm('create')">
+          <Icon icon="ep:plus" class="mr-5px" />
+          新增
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" row-key="id" @row-click="handleRowClick">
+      <el-table-column label="团队所属公司" align="center" min-width="180">
+        <template #default="{ row }">
+          {{ getDeptName(row.deptId) || '-' }}
+        </template>
+      </el-table-column>
+      <el-table-column label="团队名称" align="center" prop="teamName" min-width="180" />
+      <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
+            @click.stop
+          />
+          <span v-else>-</span>
+        </template>
+      </el-table-column>
+      <el-table-column
+        label="团队概述"
+        align="center"
+        prop="summary"
+        min-width="260"
+        show-overflow-tooltip
+      />
+      <el-table-column label="创建时间" align="center" prop="createTime" min-width="180">
+        <template #default="{ row }">
+          {{ formatDate(row.createTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" width="160" fixed="right">
+        <template #default="{ row }">
+          <el-button link type="primary" @click.stop="openForm('update', row.id)">编辑</el-button>
+          <el-button link type="danger" @click.stop="handleDelete(row.id)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <Dialog
+    v-model="dialogVisible"
+    :title="dialogTitle"
+    width="640px"
+    destroy-on-close
+    @close="closeDialog"
+  >
+    <el-form
+      ref="formRef"
+      v-loading="formLoading"
+      :model="formData"
+      :rules="formRules"
+      label-width="110px"
+    >
+      <el-form-item label="团队所属公司" prop="deptId">
+        <el-tree-select
+          v-model="formData.deptId"
+          :data="deptList"
+          :props="defaultProps"
+          check-strictly
+          clearable
+          filterable
+          node-key="id"
+          placeholder="请选择团队所属公司"
+        />
+      </el-form-item>
+      <el-form-item label="团队名称" prop="teamName">
+        <el-input v-model="formData.teamName" placeholder="请输入团队名称" />
+      </el-form-item>
+      <el-form-item label="团队 Logo" prop="avatar">
+        <UploadImg v-model="formData.avatar" height="80px" width="80px" />
+      </el-form-item>
+      <el-form-item label="团队概述" prop="summary">
+        <el-input
+          v-model="formData.summary"
+          type="textarea"
+          :rows="4"
+          maxlength="500"
+          show-word-limit
+          placeholder="请输入团队概述"
+        />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="closeDialog">取 消</el-button>
+      <el-button type="primary" :loading="submitLoading" @click="submitForm">确 定</el-button>
+    </template>
+  </Dialog>
+
+  <el-drawer
+    v-model="memberDrawerVisible"
+    size="52%"
+    :with-header="false"
+    destroy-on-close
+    class="team-member-drawer"
+  >
+    <div class="member-drawer-shell" v-loading="memberLoading">
+      <div class="member-drawer-header">
+        <div>
+          <h3>{{ currentTeamName || '团队专家' }}</h3>
+        </div>
+      </div>
+
+      <el-table
+        :data="teamMembers"
+        class="member-table"
+        style="margin-top: 20px"
+        table-layout="fixed"
+      >
+        <el-table-column prop="expertName" label="专家姓名" min-width="100" fixed="left" />
+        <el-table-column label="头像" align="center" width="120">
+          <template #default="{ row }">
+            <el-image
+              :src="row.avatar"
+              fit="cover"
+              class="team-logo"
+              :preview-src-list="[row.avatar]"
+              preview-teleported
+              @click.stop
+            />
+          </template>
+        </el-table-column>
+        <el-table-column prop="echelonLabel" label="梯队" align="center" min-width="100" />
+        <el-table-column
+          prop="expertRankLabel"
+          show-overflow-tooltip
+          label="序列"
+          align="center"
+          min-width="100"
+        />
+        <el-table-column label="所属公司" align="center" min-width="120">
+          <template #default="{ row }">
+            {{ getDeptName(row.deptId) || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="professionalTitle"
+          label="职称"
+          align="center"
+          min-width="120"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          prop="jobTitle"
+          label="岗位"
+          min-width="120"
+          show-overflow-tooltip
+          align="center"
+        />
+        <el-table-column
+          prop="formerPost"
+          align="center"
+          label="历任岗位"
+          min-width="120"
+          show-overflow-tooltip
+        />
+
+        <el-table-column
+          prop="school"
+          align="center"
+          label="出生日期"
+          min-width="150"
+          show-overflow-tooltip
+        >
+          <template #default="{ row }">
+            {{ formatDate(row.birthday).substring(0, 10) || '-' }}
+          </template>
+        </el-table-column>
+        <el-table-column
+          prop="school"
+          align="center"
+          label="毕业院校"
+          min-width="150"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          prop="educationLabel"
+          align="center"
+          label="学历"
+          min-width="120"
+          show-overflow-tooltip
+        />
+        <el-table-column
+          prop="specialDirection"
+          label="专业方向"
+          min-width="150"
+          align="center"
+          show-overflow-tooltip
+        >
+          <template #default="scope">
+            {{ getSpecialDirectionLabels(scope.row.specialDirection) }}
+          </template>
+        </el-table-column>
+
+        <el-table-column
+          prop="achievement"
+          align="center"
+          label="主要业绩"
+          min-width="120"
+          show-overflow-tooltip
+        />
+      </el-table>
+    </div>
+  </el-drawer>
 </template>
 
-<script setup></script>
+<script setup lang="ts">
+import { computed, onMounted, reactive, ref } from 'vue'
+import { ElMessage, ElMessageBox, FormRules } from 'element-plus'
+import {
+  getTeamList,
+  createTeam,
+  updateTeam,
+  deleteTeam,
+  getTeamDetail,
+  getTeamMemberList
+} from '@/api/expert'
+import * as DeptApi from '@/api/system/dept'
+import { defaultProps } from '@/utils/tree'
+import { formatDate } from '@/utils/formatTime'
+import { DICT_TYPE, getIntDictOptions } from '@/utils/dict'
+
+type TeamFormData = {
+  id?: number
+  deptId?: number
+  teamName: string
+  avatar: string
+  summary: string
+}
+
+type TeamMember = {
+  id: number
+  expertName: string
+  deptId?: number
+  professionalTitle?: string
+  jobTitle?: string
+  school?: string
+  specialDirection?: number[]
+}
+
+const defaultFormData = (): TeamFormData => ({
+  id: undefined,
+  deptId: undefined,
+  teamName: '',
+  avatar: '',
+  summary: ''
+})
+
+const loading = ref(false)
+const total = ref(0)
+const list = ref<any[]>([])
+const deptList = ref<Tree[]>([])
+const dialogVisible = ref(false)
+const dialogTitle = ref('')
+const formLoading = ref(false)
+const submitLoading = ref(false)
+const memberLoading = ref(false)
+const queryFormRef = ref()
+const formRef = ref()
+const formType = ref<'create' | 'update'>('create')
+const formData = ref<TeamFormData>(defaultFormData())
+const memberDrawerVisible = ref(false)
+const currentTeamName = ref('')
+const teamMembers = ref<TeamMember[]>([])
+
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  deptId: undefined as number | undefined,
+  teamName: undefined as string | undefined
+})
+
+const formRules = reactive<FormRules>({
+  deptId: [{ required: true, message: '请选择团队所属公司', trigger: 'change' }],
+  teamName: [{ required: true, message: '请输入团队名称', trigger: 'blur' }],
+  avatar: [{ required: true, message: '请上传团队 Logo', trigger: 'change' }],
+  summary: [{ required: true, message: '请输入团队概述', trigger: 'blur' }]
+})
+
+const deptMap = 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(deptList.value || [])
+  return map
+})
+
+const getDeptName = (deptId?: number) => {
+  if (deptId === undefined || deptId === null) return ''
+  return deptMap.value.get(deptId) || ''
+}
+
+const getDeptOptions = async () => {
+  deptList.value = await DeptApi.getSimpleDeptList()
+}
+
+const getSpecialDirectionLabels = (specialDirection?: number[]) => {
+  if (!Array.isArray(specialDirection) || !specialDirection.length) return '-'
+  return (
+    specialDirection
+      .map(
+        (value) =>
+          getIntDictOptions(DICT_TYPE.RD_EXPERT_TECHNICAL).find((dict) => dict.value === value)
+            ?.label
+      )
+      .filter(Boolean)
+      .join('、') || '-'
+  )
+}
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const res = await getTeamList(queryParams)
+    list.value = res.list || []
+    total.value = res.total || 0
+  } finally {
+    loading.value = false
+  }
+}
+
+const handleQuery = async () => {
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const resetQuery = async () => {
+  queryFormRef.value?.resetFields()
+  queryParams.pageNo = 1
+  await getList()
+}
+
+const resetForm = () => {
+  formData.value = defaultFormData()
+  formRef.value?.clearValidate()
+}
+
+const closeDialog = () => {
+  dialogVisible.value = false
+  resetForm()
+}
+
+const normalizeDetail = (data: any): TeamFormData => ({
+  id: data.id,
+  deptId: data.deptId,
+  teamName: data.teamName || '',
+  avatar: typeof data.avatar === 'string' ? data.avatar : data.avatar?.path || '',
+  summary: data.summary || ''
+})
+
+const normalizeTeamMembers = (data: any): TeamMember[] => {
+  const members = data?.experts || data?.expertList || data?.members || data?.memberList || []
+  if (!Array.isArray(members)) return []
+  return members.map((item: any) => ({
+    id: item.id,
+    expertName: item.expertName || '-',
+    deptId: item.deptId,
+    professionalTitle: item.professionalTitle || '',
+    jobTitle: item.jobTitle || '',
+    school: item.school || '',
+    educationLabel: item.educationLabel || '',
+    specialDirection: Array.isArray(item.specialDirection) ? item.specialDirection : [],
+    avatar: item.avatar || '',
+    formerPost: item.formerPost || '',
+    echelonLabel: item.echelonLabel || '',
+    expertRankLabel: item.expertRankLabel || '',
+    achievement: item.achievement || '',
+    birthday: item.birthday || ''
+  }))
+}
+
+const handleRowClick = async (row: any) => {
+  memberDrawerVisible.value = true
+  currentTeamName.value = row.teamName || ''
+  memberLoading.value = true
+  try {
+    const res = await getTeamMemberList(row.id)
+    teamMembers.value = normalizeTeamMembers(res)
+  } finally {
+    memberLoading.value = false
+  }
+}
+
+const openForm = async (type: 'create' | 'update', id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = type === 'create' ? '新增团队' : '编辑团队'
+  formType.value = type
+  resetForm()
+
+  if (type === 'update' && id) {
+    formLoading.value = true
+    try {
+      const detail = await getTeamDetail(id)
+      formData.value = normalizeDetail(detail)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+
+const submitForm = 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 as any)?.path || ''
+    }
+
+    if (formType.value === 'create') {
+      await createTeam(payload)
+      ElMessage.success('新增成功')
+    } else {
+      await updateTeam(payload)
+      ElMessage.success('修改成功')
+    }
+
+    closeDialog()
+    await getList()
+  } finally {
+    submitLoading.value = false
+  }
+}
+
+const handleDelete = async (id: number) => {
+  try {
+    await ElMessageBox.confirm('确定要删除该团队吗?', '提示', {
+      type: 'warning',
+      confirmButtonText: '确定',
+      cancelButtonText: '取消'
+    })
+    await deleteTeam(id)
+    ElMessage.success('删除成功')
+    await getList()
+  } catch (error) {
+    // ignore
+  }
+}
+
+onMounted(async () => {
+  await getDeptOptions()
+  await getList()
+})
+</script>
+
+<style scoped lang="scss">
+.team-logo {
+  width: 52px;
+  height: 52px;
+  border-radius: 8px;
+  border: 1px solid #e5e7eb;
+}
+</style>

Algúns arquivos non se mostraron porque demasiados arquivos cambiaron neste cambio