Sfoglia il codice sorgente

feat: 应急管理-应急架构体系-队伍管理 功能完成

“fujiacheng” 9 mesi fa
parent
commit
54512ea5fd

+ 135 - 0
src/api/emergency-organization/teams.ts

@@ -0,0 +1,135 @@
+import { http } from '@/utils/http/axios';
+// import type { QueryPageRequest, QueryPageResponse } from '@/types/basic-query';
+import { LeaderTeamType, TeamAndPersonInfoType } from '@/views/emergency/organization/team-management/type';
+
+/**
+ * 查询应急架构体系
+ */
+export function queryLeaderTeam() {
+  return http.request<Omit<LeaderTeamType, 'level'>>({
+    url: '/emergencySystem/queryEmergencySystem',
+    method: 'get',
+  });
+}
+
+/**
+ * 查询队伍详细信息
+ */
+export function queryEmergencyTeamDetail(teamId: number) {
+  return http.request<TeamAndPersonInfoType>({
+    url: `/emergencySystem/queryEmergencyTeamDetail?teamId=${teamId}`,
+    method: 'get',
+  });
+}
+
+export type SetTeamInfoType = {
+  id: number;
+  memberCount?: number;
+  teamName?: string;
+  description?: string;
+};
+
+/**
+ * 保存队伍信息
+ */
+export function updateEmergencyTeamInfo(data: SetTeamInfoType) {
+  return http.request({
+    url: `/emergencySystem/updateEmergencyTeamInfo`,
+    method: 'post',
+    data,
+  });
+}
+
+export type CreateTeamInfoType = {
+  parentId: number;
+  teamName: string;
+};
+
+/**
+ * 创建队伍
+ */
+export function saveEmergencyTeamInfo(data: CreateTeamInfoType) {
+  return http.request({
+    url: `/emergencySystem/saveEmergencyTeamInfo`,
+    method: 'post',
+    data,
+  });
+}
+
+/**
+ * 删除队伍
+ */
+export function deleteEmergencyTeam(teamId: number) {
+  return http.request({
+    url: `/emergencySystem/deleteEmergencyTeam?teamId=${teamId}`,
+    method: 'post',
+  });
+}
+
+export type SetPositionType = {
+  teamId: number;
+  positionList: PositionType[];
+};
+
+export type PositionType = {
+  id?: number;
+  positionLevel: number;
+  title: string;
+};
+
+/**
+ * 获取队伍职位列表
+ */
+export function queryTeamPositionList(teamId: number) {
+  return http.request<PositionType[]>({
+    url: `/emergencySystem/queryTeamPositionList?teamId=${teamId}`,
+    method: 'get',
+  });
+}
+
+/**
+ * 设置队伍职位
+ */
+export function saveTeamPosition(data: SetPositionType) {
+  return http.request({
+    url: `/emergencySystem/saveTeamPosition`,
+    method: 'post',
+    data,
+  });
+}
+
+export type AddPersonType = {
+  teamId: number;
+} & PersonInfoType;
+
+export type PersonInfoType = {
+  id?: number;
+  userId: number;
+  positionId: number | string;
+  realname: string;
+  staffNo: string;
+  mobile: string;
+  department: string;
+  jobTitle: string;
+};
+
+/**
+ * 添加/编辑人员,编辑人员需要传id
+ */
+export function saveTeamPersonnel(data: AddPersonType) {
+  return http.request({
+    url: `/emergencySystem/saveTeamPersonnel`,
+    method: 'post',
+    data,
+  });
+}
+
+/**
+ * 从队伍中删除人员
+ */
+export function deleteTeamPersonnel(personnelId: number) {
+  return http.request({
+    url: `/emergencySystem/deleteTeamPersonnel?personnelId=${personnelId}`,
+    method: 'post',
+  });
+}

+ 28 - 0
src/api/user/user.ts

@@ -0,0 +1,28 @@
+import { http } from '@/utils/http/axios';
+
+export type UserType = {
+  id: number;
+  username: string;
+  password: string;
+  realname: string;
+  staffNo: string; // 工号
+  gender: 1 | 2; // 1-男,2-女
+  mobile: string;
+  remark: string;
+  email: string;
+  avatar: string; // 用户头像
+  roleType: 0 | 1 | 2; //  0-普通用户,1-超级管理员,2-租户管理员
+  deptId: number;
+  deptName: string;
+  jobName: string; // 职位
+};
+
+/**
+ *根据用户名查询用户列表
+ */
+export function queryUserListByUsername(username: string) {
+  return http.request<UserType[]>({
+    url: `/admin/user/queryUserListByUsername?username=${username}`,
+    method: 'post',
+  });
+}

+ 20 - 0
src/router/routers/emergency.ts

@@ -327,6 +327,26 @@ const emergencyManagementRoute = {
         title: '应急处置报表',
       },
     },
+    {
+      id: 2013,
+      parentId: 2000,
+      name: 'team-management',
+      path: 'team-management',
+      component: '/emergency/organization/team-management/PageTeamManagement',
+      redirect: '',
+      meta: {
+        activeMenu: '/emergency/organization/PageOrganization',
+        alwaysShow: false,
+        frameSrc: '',
+        hidden: false,
+        icon: '',
+        isFrame: 0,
+        isRoot: false,
+        noCache: false,
+        query: '',
+        title: '队伍管理',
+      },
+    },
   ],
 };
 

+ 4 - 2
src/views/emergency/components/OrgChart.vue

@@ -27,7 +27,7 @@
     (event: 'canvas-click'): void;
   }>();
 
-  const data = treeToGraphData(props.treeData); // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
+  let data = treeToGraphData(props.treeData); // 通过 treeToGraphData 方法,将树形结构数据转换为 G6 的标准数据结构
   const container = ref<HTMLDivElement | null>(null); // 图表容器引用
   let graph: any = null;
 
@@ -168,13 +168,15 @@
   watch(
     () => props.treeData,
     () => {
+      data = treeToGraphData(props.treeData);
       initGraph();
     },
-    { deep: true },
+    { deep: true, immediate: true },
   );
 
   // 生命周期钩子
   onMounted(() => {
+    data = treeToGraphData(props.treeData);
     initGraph();
   });
 

+ 45 - 5
src/views/emergency/organization/PageOrganization.vue

@@ -7,15 +7,26 @@
       <OrgChart :treeData="treeData" @node-click="handleNodeClick" @canvas-click="handleCanvasClick" />
     </div>
     <div class="safety-platform-container__footer">
-      <el-button> 编辑 </el-button>
+      <el-button @click="router.push('team-management')"> 编辑 </el-button>
     </div>
   </div>
+  <TeamDetailDrawer ref="teamDetailDrawerRef" :selected-team-id="selectedTeamId" />
 </template>
 
 <script setup lang="ts">
+  import { ref, onMounted } from 'vue';
+  import { useRouter } from 'vue-router';
   import OrgChart from '../components/OrgChart.vue';
+  import TeamDetailDrawer from './components/TeamDetailDrawer.vue';
+  import useTeamStore from './team-management/store/userTeam';
 
-  const treeData = {
+  type OrganizationTreeType = {
+    id: string;
+    data: { name: string };
+    children?: OrganizationTreeType[];
+  };
+
+  const treeData = ref<OrganizationTreeType>({
     id: 'root',
     data: { name: '应急领导小组' },
     children: [
@@ -32,17 +43,46 @@
         data: { name: '应急支援小组' },
       },
     ],
-  };
+  });
+
+  const router = useRouter();
+
+  const { getLeaderTeams } = useTeamStore();
+
+  const teamDetailDrawerRef = ref<InstanceType<typeof TeamDetailDrawer>>();
+
+  const selectedTeamId = ref<number | null>(null);
 
   const handleNodeClick = (nodeData: any) => {
-    console.log('节点被点击:', nodeData);
+    // console.log('节点被点击:', nodeData);
     // 在这里处理节点点击事件
+
+    selectedTeamId.value = Number(nodeData.id);
+    if (selectedTeamId.value === Number(treeData.value.id)) return;
+
+    teamDetailDrawerRef.value?.drawerShow();
   };
 
   const handleCanvasClick = () => {
-    console.log('画布被点击');
+    // console.log('画布被点击');
     // 在这里处理画布点击事件
   };
+
+  function convertData(leaderTeams): OrganizationTreeType {
+    return {
+      id: leaderTeams.teamId.toString(),
+      data: {
+        name: leaderTeams.teamName,
+      },
+      children: leaderTeams.children?.map((child) => convertData(child)),
+    };
+  }
+
+  onMounted(async () => {
+    const res = await getLeaderTeams();
+
+    treeData.value = convertData(res);
+  });
 </script>
 
 <style lang="scss" scoped>

+ 138 - 0
src/views/emergency/organization/components/TeamDetailDrawer.vue

@@ -0,0 +1,138 @@
+<template>
+  <el-drawer v-model="showDrawer" direction="rtl" @close="clearData">
+    <div class="team-detail__info">
+      <div class="team-detail__info__team-name">{{ teamAndPersonInfo?.teamName }}</div>
+      <div class="team-detail__info__member-count">
+        共 <span>{{ teamAndPersonInfo?.memberCount || 0 }}</span> 人
+      </div>
+    </div>
+
+    <div class="team-detail__level" v-for="level in levelAndPersonList" :key="level.positionLevel">
+      <div class="team-detail__level__title">
+        {{ level.title }}
+      </div>
+      <div class="team-detail__level__content">
+        <div v-for="(person, index) in level.personList" :key="index">{{ person }}</div>
+      </div>
+    </div>
+
+    <div class="team-detail__description">
+      <div class="team-detail__description__title"> 队伍职责 </div>
+      <div class="team-detail__description__content"> {{ teamAndPersonInfo?.description }} </div>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { queryEmergencyTeamDetail } from '@/api/emergency-organization/teams';
+  import { TeamAndPersonInfoType } from '../team-management/type';
+
+  const props = defineProps<{ selectedTeamId: number | null }>();
+
+  type LevelAndPersonType = {
+    positionLevel: number;
+    title: string;
+    personList: string[];
+  };
+
+  const showDrawer = ref(false);
+  const teamAndPersonInfo = ref<TeamAndPersonInfoType>();
+  const levelAndPersonList = ref<LevelAndPersonType[]>([]);
+
+  function drawerShow() {
+    showDrawer.value = true;
+  }
+
+  function clearData() {
+    teamAndPersonInfo.value = undefined;
+    levelAndPersonList.value = [];
+  }
+
+  // 按照职位等级排序队伍人员
+  function setLevelAndPerson() {
+    const levelMap: { [key: number]: LevelAndPersonType } = {};
+
+    teamAndPersonInfo.value?.personnelList.map((person) => {
+      if (!(person.positionLevel in levelMap)) {
+        levelMap[person.positionLevel] = { positionLevel: person.positionLevel, title: person.title, personList: [] };
+      }
+
+      levelMap[person.positionLevel].personList.push(person.realname);
+    });
+
+    for (let level in levelMap) {
+      levelAndPersonList.value.push(levelMap[level]);
+    }
+  }
+
+  watch(
+    () => props.selectedTeamId,
+    async (teamId) => {
+      if (teamId) {
+        teamAndPersonInfo.value = await queryEmergencyTeamDetail(teamId);
+        setLevelAndPerson();
+      }
+    },
+  );
+
+  defineExpose({
+    drawerShow,
+  });
+</script>
+
+<style scoped lang="scss">
+  .team-detail__info {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+
+    &__team-name {
+      font-size: 20px;
+      font-weight: bold;
+    }
+    &__member-count {
+      span {
+        color: #1777ff;
+      }
+    }
+  }
+
+  .team-detail__level {
+    display: flex;
+    align-items: center;
+    margin-bottom: 10px;
+    padding: 10px 0;
+    border-left: 3px solid #1777ff;
+    background-color: #e5effe;
+
+    &__title {
+      flex: 1;
+      height: inherit;
+      color: #1777ff;
+      text-align: center;
+    }
+
+    &__content {
+      flex: 10;
+      display: flex;
+      flex-wrap: wrap;
+      padding: 0 15px;
+      gap: 15px;
+      border-left: 1px solid #c5c3c3;
+    }
+  }
+
+  .team-detail__description {
+    margin-top: 20px;
+    &__title {
+      margin-bottom: 10px;
+      font-size: 20px;
+      font-weight: bold;
+    }
+    &__content {
+      color: #aaaaaa;
+    }
+  }
+</style>

+ 58 - 0
src/views/emergency/organization/team-management/PageTeamManagement.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="safety-platform-container">
+    <header class="safety-platform-container__header">
+      <div class="breadcrumb-title"> 应急架构编辑 </div>
+    </header>
+
+    <main class="safety-platform-container__main">
+      <div class="safety-platform-container__main__team-bar" v-loading="loadingTeams">
+        <LeaderTeamBar />
+      </div>
+
+      <div v-if="curTeam" class="safety-platform-container__main__team-info" v-loading="loadingTeamInfo">
+        <TeamAndPersonInfo />
+      </div>
+      <div v-else class="safety-platform-container__main__team-info--empty"><div>请选择队伍</div></div>
+    </main>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { onUnmounted } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import LeaderTeamBar from './leader-teams/LeaderTeamBar.vue';
+  import TeamAndPersonInfo from './team-info/TeamAndPersonInfo.vue';
+  import useTeamStore from './store/userTeam';
+
+  const { curTeam, loadingTeams, loadingTeamInfo } = storeToRefs(useTeamStore());
+  const { clearData } = useTeamStore();
+
+  onUnmounted(() => {
+    {
+      clearData();
+    }
+  });
+</script>
+
+<style lang="scss" scoped>
+  @use '@/styles/page-details-layout.scss' as *;
+
+  .safety-platform-container__main {
+    width: 100%;
+    display: flex;
+    &__team-bar {
+    }
+    &__team-info {
+      flex: 1;
+      min-width: 0;
+      overflow: auto;
+    }
+
+    &__team-info--empty {
+      margin: 350cpx auto;
+      font-size: 20px;
+      font-weight: bold;
+      text-align: center;
+    }
+  }
+</style>

+ 308 - 0
src/views/emergency/organization/team-management/leader-teams/LeaderTeam.vue

@@ -0,0 +1,308 @@
+<template>
+  <div
+    class="leader-team"
+    :class="{
+      'leader-team--indent': props.teamData.level != 1,
+      'hide-arrow': props.teamData.level === 3,
+    }"
+  >
+    <el-collapse-item :name="props.teamData.teamId">
+      <template #title>
+        <div
+          class="leader-team__title"
+          :class="{
+            'leader-team--selected': props.teamData.teamId === curTeam?.teamId,
+          }"
+          @click.stop
+          @click="clickTeam"
+        >
+          <div class="leader-team__rename" v-show="showRenameTeamInput">
+            <el-input
+              v-model="inputNewTeamName"
+              class="leader-team__rename-input"
+              placeholder="为此队伍命名"
+              maxlength="15"
+              show-word-limit
+              ref="teamNameInputRef"
+              @blur="enterNewName"
+              @keyup.enter="$event.target.blur()"
+            />
+          </div>
+
+          <div v-show="!showRenameTeamInput" class="leader-team__team-name">
+            {{ props.teamData.teamName }}
+          </div>
+
+          <div class="leader-team__menu-icon" @click.stop>
+            <img src="@/assets/icons/nine-square-grid/more.png" @click="showTeamOperation = !showTeamOperation" />
+          </div>
+
+          <div
+            v-show="showTeamOperation"
+            class="leader-team__operation"
+            :class="{ 'adjust-menu-position': props.teamData.level !== 2 }"
+            @click.stop
+          >
+            <div v-if="props.teamData.level !== 3" @mousedown="showAddTeamDialog()" class="leader-team__operation-item">
+              新建队伍
+            </div>
+
+            <div v-if="props.teamData.level !== 1" @click="handleDelete()" class="leader-team__operation-item">
+              删除队伍
+            </div>
+          </div>
+        </div>
+      </template>
+
+      <div v-if="props.teamData.level <= 3">
+        <div v-for="team in props.teamData.children" :key="team.teamId">
+          <leaderTeam :team-data="team" :parent-id="props.teamData.teamId" @expand="emitExpandEvent" />
+        </div>
+      </div>
+    </el-collapse-item>
+  </div>
+  <AddTeam ref="addTeamRef" @emit-team-name="handleCreate" />
+</template>
+
+<script setup lang="ts">
+  import { ref, watch, nextTick } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import AddTeam from './components/AddTeam.vue';
+  import { LeaderTeamType } from '../type';
+  import useTeamStore from '../store/userTeam';
+  import { updateEmergencyTeamInfo } from '@/api/emergency-organization/teams';
+
+  const props = defineProps<{
+    teamData: LeaderTeamType;
+    parentId: number;
+  }>();
+
+  const emit = defineEmits<{ (e: 'expand', collapseId: number) }>();
+
+  const indentation = `${props.teamData.level * 10}px`;
+
+  const { leaderTeamTree, curTeam, loadingTeams } = storeToRefs(useTeamStore());
+  const { getLeaderTeams, createTeam, deleteTeam, getSelectTeamInfo } = useTeamStore();
+
+  const showTeamOperation = ref(false);
+  const showRenameTeamInput = ref(false);
+
+  const teamOriginName = ref(props.teamData.teamName);
+  const inputNewTeamName = ref('');
+  const teamNameInputRef = ref();
+
+  const addTeamRef = ref<InstanceType<typeof AddTeam>>();
+
+  function handleRenameTeam() {
+    showRenameTeamInput.value = true;
+    nextTick(() => {
+      teamNameInputRef.value.focus();
+      inputNewTeamName.value = teamOriginName.value;
+    });
+  }
+
+  async function enterNewName() {
+    if (inputNewTeamName.value === '' || inputNewTeamName.value === teamOriginName.value) {
+      showRenameTeamInput.value = false;
+      return;
+    }
+
+    loadingTeams.value = true;
+
+    const data = { id: props.teamData.teamId, teamName: inputNewTeamName.value };
+    await updateEmergencyTeamInfo(data);
+    await getLeaderTeams();
+    teamOriginName.value = inputNewTeamName.value;
+    showRenameTeamInput.value = false;
+
+    loadingTeams.value = false;
+  }
+
+  function showAddTeamDialog() {
+    addTeamRef.value?.dialogShow();
+  }
+
+  function handleCreate(teamName: string) {
+    createTeam(props.teamData, teamName);
+
+    // 添加完成后展开队伍
+    emitExpandEvent(props.teamData.teamId);
+  }
+
+  function handleDelete() {
+    deleteTeam(props.teamData);
+  }
+
+  let clickTimeout: any = null;
+  function clickTeam(event) {
+    // 单击事件
+    if (event.detail === 1) {
+      clickTimeout = setTimeout(() => {
+        if (props.teamData.teamId === leaderTeamTree.value.teamId) return;
+
+        getSelectTeamInfo(props.teamData);
+      }, 200);
+    }
+    // 双击事件
+    else if (event.detail === 2) {
+      clearTimeout(clickTimeout);
+      handleRenameTeam();
+    }
+  }
+
+  // 控制新建/删除菜单的隐藏
+  const closeTeamOperation = (event) => {
+    if (event.target.className !== 'leader-team__operation-item') showTeamOperation.value = false;
+    if (event.target.innerText === '新建队伍') {
+      showTeamOperation.value = false;
+    }
+  };
+
+  function emitExpandEvent(collapseId: number) {
+    emit('expand', collapseId);
+  }
+
+  watch(
+    () => showTeamOperation.value,
+    (newValue) => {
+      newValue
+        ? document.addEventListener('mousedown', closeTeamOperation)
+        : document.removeEventListener('mousedown', closeTeamOperation);
+    },
+  );
+</script>
+
+<style scoped lang="scss">
+  .leader-team {
+    .leader-team__title {
+      height: 38px;
+      width: 100%;
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      color: #333333;
+      border-radius: 4px;
+      position: relative;
+      cursor: default;
+
+      .leader-team__rename {
+        height: 32px;
+        width: 100%;
+        display: flex;
+        align-items: center;
+        background-color: #1777ff;
+        border-radius: 4px;
+        font-size: 14px;
+        line-height: 24px;
+        padding-right: 6px;
+
+        .leader-team__rename-input {
+          height: 24px;
+        }
+      }
+
+      .leader-team__team-name {
+        white-space: nowrap;
+      }
+
+      .leader-team__menu-icon {
+        visibility: hidden;
+        margin-left: 10px;
+        margin-right: 10px;
+
+        img {
+          height: 20px;
+          width: 20px;
+          &:hover {
+            background-color: rgba(255, 255, 255, 0.2);
+            border-radius: 4px;
+            cursor: pointer;
+          }
+        }
+      }
+
+      &:hover .leader-team__menu-icon {
+        visibility: visible;
+        display: flex;
+        align-items: center;
+      }
+
+      .leader-team__operation {
+        display: block;
+        width: 130px;
+        position: absolute;
+        bottom: -63px;
+        right: 5px;
+        box-shadow: 0px 0px 10px 2px rgb(0, 0, 0, 0.3);
+        border-radius: 4px;
+        background: #f4f7ff;
+        outline: 1px solid rgba(245, 248, 255, 0.25);
+        z-index: 9991;
+
+        .leader-team__operation-item {
+          height: 31.5px;
+          line-height: 31.5px;
+          border-radius: 4px;
+          &:hover {
+            color: #fff;
+            background-color: #1777ff;
+            cursor: pointer;
+          }
+        }
+      }
+
+      .adjust-menu-position {
+        bottom: -33px;
+      }
+    }
+
+    // 隐藏最后一个层级队伍的箭头icon
+    .hide-arrow {
+      .leader-team__team-name {
+        padding-left: 22px;
+      }
+      :deep(.el-collapse-item button) {
+        padding-left: 0;
+        cursor: auto;
+      }
+      :deep(.el-collapse-item__arrow) {
+        display: none;
+      }
+    }
+
+    :deep(.el-collapse-item__header) {
+      height: 40px;
+    }
+
+    // 防止最后一个队伍的操作面板不显示
+    :deep(.el-collapse-item__wrap) {
+      overflow: visible;
+    }
+
+    // 非选中队伍的名字的hover样式
+    .leader-team__title:not(.leader-team--selected):hover {
+      background-color: #cdd8ff;
+    }
+
+    // 非选中队伍的icon的hover样式
+    :deep(.el-collapse-item__header:not(.el-collapse-item__header:has(.leader-team--selected)):hover) {
+      border-radius: 4px;
+      background-color: #cdd8ff;
+    }
+
+    // 选中队伍的hover样式
+    :deep(.el-collapse-item__header:has(.leader-team--selected)) {
+      background-color: #1777ff;
+      border-radius: 4px;
+
+      .leader-team__team-name,
+      svg {
+        color: #fff;
+      }
+    }
+  }
+
+  .leader-team--indent {
+    margin-left: v-bind(indentation);
+  }
+</style>

+ 81 - 0
src/views/emergency/organization/team-management/leader-teams/LeaderTeamBar.vue

@@ -0,0 +1,81 @@
+<template>
+  <div class="leader-team-bar">
+    <div class="leader-team-bar__decoration"></div>
+
+    <div class="leader-team-bar__teams">
+      <el-scrollbar>
+        <el-collapse v-model="activeTeam">
+          <LeaderTeam :teamData="leaderTeamTree" :parentId="leaderTeamTree.teamId" @expand="expandCollapse" />
+        </el-collapse>
+      </el-scrollbar>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  import { ref, onMounted } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { ElScrollbar, ElCollapse } from 'element-plus';
+  import LeaderTeam from './LeaderTeam.vue';
+  import useTeamStore from '../store/userTeam';
+
+  const activeTeam = ref<number[]>([]);
+
+  const { leaderTeamTree } = storeToRefs(useTeamStore());
+  const { getLeaderTeams } = useTeamStore();
+
+  function expandCollapse(collapseId: number) {
+    activeTeam.value.push(collapseId);
+  }
+
+  onMounted(async () => {
+    await getLeaderTeams();
+
+    activeTeam.value.push(leaderTeamTree.value.teamId); // 默认展开一级队伍
+  });
+</script>
+
+<style lang="scss" scoped>
+  .leader-team-bar {
+    height: 100%;
+    background: #f4f7ff;
+
+    .leader-team-bar__decoration {
+      height: 5px;
+      margin-bottom: 20px;
+      border-radius: 8px 8px 0 0;
+      background-color: #1777ff;
+    }
+
+    .leader-team-bar__teams {
+      height: calc(100% - 25px);
+      color: #333333;
+      padding: 0px 16px;
+      padding-right: 4px;
+    }
+  }
+
+  :deep(.el-collapse) {
+    border: 0px;
+  }
+  :deep(.el-collapse-item__arrow) {
+    margin-right: 12px;
+    color: #333333;
+  }
+
+  :deep(.el-collapse-item__wrap) {
+    background: transparent;
+    border-bottom: transparent;
+  }
+  :deep(.el-collapse-item__content) {
+    padding: 0px;
+    margin-right: 10px;
+  }
+  :deep(.el-collapse-item__header) {
+    background: #f4f7ff;
+    border: 0px;
+    flex-direction: row-reverse;
+    padding-left: 12px;
+    margin-right: 10px;
+  }
+</style>

+ 56 - 0
src/views/emergency/organization/team-management/leader-teams/components/AddTeam.vue

@@ -0,0 +1,56 @@
+<template>
+  <el-dialog v-model="showDialog" title="添加队伍" width="300" class="add-team-dialog">
+    <el-form :model="formData" ref="ruleFormRef" :rules="rules" label-position="top">
+      <el-form-item prop="teamName" label="队伍名">
+        <el-input v-model="formData.teamName" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div>
+        <el-button type="primary" @click="submitForm"> 提交 </el-button>
+        <el-button @click="showDialog = false">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import type { FormInstance, FormRules } from 'element-plus';
+
+  const emit = defineEmits<{ (e: 'emit-team-name', teamName: string) }>();
+
+  const showDialog = ref(false);
+  const formData = ref<{ teamName: string }>({
+    teamName: '',
+  });
+  const ruleFormRef = ref<FormInstance>();
+  const rules = ref<FormRules<{ teamName: string }>>({
+    teamName: [{ required: true, message: '请输入队伍名', trigger: 'blur' }],
+  });
+
+  function dialogShow() {
+    showDialog.value = true;
+  }
+
+  async function submitForm() {
+    await ruleFormRef.value!.validate((valid) => {
+      if (valid) {
+        emit('emit-team-name', formData.value.teamName);
+        showDialog.value = false;
+      }
+    });
+  }
+
+  defineExpose({
+    dialogShow,
+  });
+</script>
+
+<style scoped lang="scss"></style>
+<style>
+  .add-team-dialog {
+    min-height: 0;
+  }
+</style>

+ 149 - 0
src/views/emergency/organization/team-management/store/userTeam.ts

@@ -0,0 +1,149 @@
+import { ref } from 'vue';
+import { defineStore } from 'pinia';
+import { LeaderTeamType, TeamAndPersonInfoType } from '../type';
+import { queryLeaderTeam, queryEmergencyTeamDetail } from '@/api/emergency-organization/teams';
+import { PositionType, saveTeamPosition } from '@/api/emergency-organization/teams';
+import {
+  SetTeamInfoType,
+  updateEmergencyTeamInfo,
+  deleteEmergencyTeam,
+  saveEmergencyTeamInfo,
+} from '@/api/emergency-organization/teams';
+
+const useTeamStore = defineStore('useTeam', () => {
+  // 左侧的领导队伍树
+  const leaderTeamTree = ref<LeaderTeamType>({
+    teamId: 0,
+    teamName: '',
+    level: 1,
+    memberCount: 0,
+    children: [],
+  });
+
+  const curTeam = ref<LeaderTeamType>();
+  const teamAndPersonInfo = ref<TeamAndPersonInfoType>(); // 右侧的队伍信息和人员信息表格数据
+
+  const loadingTeams = ref(false);
+  const loadingTeamInfo = ref(false);
+
+  async function getLeaderTeams() {
+    const res = await queryLeaderTeam();
+
+    leaderTeamTree.value = setLevel(res);
+    return leaderTeamTree.value;
+  }
+
+  function setLevel(emergencySystem, level = 1): LeaderTeamType {
+    return {
+      ...emergencySystem,
+      level,
+      children: emergencySystem.children.map((child) => setLevel(child, level + 1)),
+    };
+  }
+
+  async function createTeam(team: LeaderTeamType, teamName: string) {
+    loadingTeams.value = true;
+
+    const data = { parentId: team.teamId, teamName };
+    const id = await saveEmergencyTeamInfo(data);
+
+    team.children.push({
+      teamId: id,
+      teamName,
+      level: team.level + 1,
+      memberCount: 0,
+      children: [],
+    });
+
+    loadingTeams.value = false;
+  }
+
+  async function deleteTeam(team: LeaderTeamType) {
+    // 由于请求接口有延迟,先本地删除后再调用接口
+    leaderTeamTree.value = filterNode(leaderTeamTree.value, team.teamId)!;
+    deleteEmergencyTeam(team.teamId);
+
+    if (team.teamId === curTeam.value?.teamId) {
+      curTeam.value = undefined;
+      teamAndPersonInfo.value = undefined;
+    }
+  }
+
+  function filterNode(team: LeaderTeamType, id: number): LeaderTeamType | null {
+    return team.teamId === id
+      ? null
+      : {
+          ...team,
+          children: team.children.map((child) => filterNode(child, id)).filter(Boolean) as LeaderTeamType[],
+        };
+  }
+
+  function getTeamInfo(teamId: number) {
+    return queryEmergencyTeamDetail(teamId);
+  }
+
+  async function refreshCurTeamInfo() {
+    teamAndPersonInfo.value = await getTeamInfo(curTeam.value!.teamId);
+  }
+
+  async function getSelectTeamInfo(team: LeaderTeamType) {
+    loadingTeamInfo.value = true;
+
+    curTeam.value = team;
+    refreshCurTeamInfo();
+
+    loadingTeamInfo.value = false;
+  }
+
+  function setPositionInfo(positionList: PositionType[]) {
+    loadingTeamInfo.value = true;
+
+    const data = {
+      teamId: curTeam.value!.teamId!,
+      positionList: positionList,
+    };
+    saveTeamPosition(data);
+
+    loadingTeamInfo.value = false;
+  }
+
+  async function setTeamInfo(data: SetTeamInfoType) {
+    loadingTeamInfo.value = true;
+
+    await updateEmergencyTeamInfo(data);
+
+    refreshCurTeamInfo();
+
+    loadingTeamInfo.value = false;
+  }
+
+  function clearData() {
+    leaderTeamTree.value = {
+      teamId: 0,
+      teamName: '',
+      level: 1,
+      memberCount: 0,
+      children: [],
+    };
+    curTeam.value = undefined;
+    teamAndPersonInfo.value = undefined;
+  }
+
+  return {
+    leaderTeamTree,
+    curTeam,
+    teamAndPersonInfo,
+    loadingTeams,
+    loadingTeamInfo,
+    getLeaderTeams,
+    createTeam,
+    deleteTeam,
+    refreshCurTeamInfo,
+    getSelectTeamInfo,
+    setPositionInfo,
+    setTeamInfo,
+    clearData,
+  };
+});
+
+export default useTeamStore;

+ 29 - 0
src/views/emergency/organization/team-management/team-info/TeamAndPersonInfo.vue

@@ -0,0 +1,29 @@
+<template>
+  <div class="team-info-page">
+    <div class="team-info-page__team-info">
+      <TeamInfo />
+    </div>
+
+    <div class="team-info-page__personal-info">
+      <PersonalInfo />
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import TeamInfo from './components/TeamInfo.vue';
+  import PersonalInfo from './components/PersonalInfo.vue';
+</script>
+
+<style lang="scss" scoped>
+  .team-info-page {
+    padding: 15px;
+  }
+
+  .team-info-page--empty {
+    margin-top: 350cpx;
+    font-size: 20px;
+    font-weight: bold;
+    text-align: center;
+  }
+</style>

+ 191 - 0
src/views/emergency/organization/team-management/team-info/components/AddPerson.vue

@@ -0,0 +1,191 @@
+<template>
+  <el-dialog
+    v-model="showDialog"
+    class="add-person-dialog"
+    title="添加人员"
+    width="800"
+    destroy-on-close
+    @close="initForm()"
+  >
+    <el-form :model="formData" ref="ruleFormRef" :rules="rules" label-position="top" class="add-person-dialog__form">
+      <el-form-item prop="positionId" label="岗位职责">
+        <el-select v-model="formData.positionId" placeholder="请选择职位">
+          <el-option
+            v-for="position in props.positionOptions"
+            :key="position.positionLevel"
+            :label="position.title"
+            :value="position.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="realname" label="姓名">
+        <el-select
+          v-model="formData.realname"
+          filterable
+          remote
+          reserve-keyword
+          placeholder="输入名字搜索"
+          :remote-method="getUsers"
+          :loading="loading"
+        >
+          <el-option
+            v-for="user in userOptions"
+            :key="user.id"
+            :label="user.realname"
+            :value="user.realname"
+            @click="setUserInfo(user)"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="staffNo" label="员工号">
+        <el-input v-model="formData.staffNo" />
+      </el-form-item>
+      <el-form-item prop="mobile" label="联系电话">
+        <el-input v-model="formData.mobile" type="number" />
+      </el-form-item>
+      <el-form-item prop="deptName" label="部门">
+        <el-input v-model="formData.deptName" />
+      </el-form-item>
+      <el-form-item prop="jobName" label="职务">
+        <el-input v-model="formData.jobName" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitForm"> 提交 </el-button>
+        <el-button @click="showDialog = false">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import useTeamStore from '../../store/userTeam';
+  import { UserType, queryUserListByUsername } from '@/api/user/user';
+  import { PositionType, saveTeamPersonnel } from '@/api/emergency-organization/teams';
+
+  const props = defineProps<{ positionOptions: PositionType[] }>();
+
+  type PersonInfo = {
+    userId: number;
+    positionId: string;
+    realname: string;
+    staffNo: string;
+    mobile: string;
+    deptName: string;
+    jobName: string;
+  };
+
+  const showDialog = ref(false);
+  const loading = ref(false);
+  const userOptions = ref<UserType[]>([]);
+
+  const { curTeam, loadingTeamInfo } = storeToRefs(useTeamStore());
+  const { refreshCurTeamInfo } = useTeamStore();
+
+  const formData = ref<PersonInfo>({
+    userId: 0,
+    positionId: '',
+    realname: '',
+    staffNo: '',
+    mobile: '',
+    deptName: '',
+    jobName: '',
+  });
+  const ruleFormRef = ref<FormInstance>();
+  const rules = ref<FormRules<PersonInfo>>({
+    positionId: [{ required: true, message: '请选择岗位', trigger: 'blur' }],
+    realname: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+    staffNo: [{ required: true, message: '请输入员工号', trigger: 'blur' }],
+    mobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
+  });
+
+  function initForm() {
+    formData.value = {
+      userId: 0,
+      positionId: '',
+      realname: '',
+      staffNo: '',
+      mobile: '',
+      deptName: '',
+      jobName: '',
+    };
+  }
+
+  function dialogShow() {
+    showDialog.value = true;
+  }
+
+  async function getUsers(username: string) {
+    if (username) {
+      loading.value = true;
+      userOptions.value = await queryUserListByUsername(username);
+      loading.value = false;
+    } else {
+      userOptions.value = [];
+    }
+  }
+
+  function setUserInfo(user: UserType) {
+    formData.value.userId = user.id;
+    formData.value.staffNo = user.staffNo;
+    formData.value.mobile = user.mobile;
+    formData.value.deptName = user.deptName;
+    formData.value.jobName = user.jobName;
+  }
+
+  async function submitForm() {
+    await ruleFormRef.value!.validate(async (valid) => {
+      if (valid) {
+        loadingTeamInfo.value = true;
+        showDialog.value = false;
+
+        const data = {
+          teamId: curTeam.value!.teamId,
+          department: formData.value.deptName,
+          jobTitle: formData.value.jobName,
+          ...formData.value,
+        };
+        await saveTeamPersonnel(data);
+        await refreshCurTeamInfo();
+
+        loadingTeamInfo.value = false;
+      }
+    });
+  }
+
+  defineExpose({
+    dialogShow,
+  });
+</script>
+
+<style scoped lang="scss">
+  .add-person-dialog__header {
+  }
+  .add-person-dialog__form {
+    display: grid;
+    grid-template-columns: repeat(2, 45%);
+    justify-content: space-around;
+
+    // 去掉el input右侧的小箭头
+    :deep(input::-webkit-outer-spin-button),
+    :deep(input::-webkit-inner-spin-button) {
+      -webkit-appearance: none;
+    }
+  }
+
+  :deep(.el-dialog__header) {
+    text-align: left;
+    font-weight: bold;
+  }
+</style>
+
+<style>
+  .add-person-dialog {
+    min-height: 0;
+  }
+</style>

+ 143 - 0
src/views/emergency/organization/team-management/team-info/components/EditPerson.vue

@@ -0,0 +1,143 @@
+<template>
+  <el-dialog v-model="showDialog" class="edit-person-dialog" title="编辑人员" width="800" destroy-on-close>
+    <el-form :model="formData" ref="ruleFormRef" :rules="rules" label-position="top" class="edit-person-dialog__form">
+      <el-form-item prop="positionId" label="岗位职责">
+        <el-select v-model="formData.positionId" placeholder="请选择职位">
+          <el-option
+            v-for="position in props.positionOptions"
+            :key="position.positionLevel"
+            :label="position.title"
+            :value="position.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="realname" label="姓名">
+        <el-input v-model="formData.realname" />
+      </el-form-item>
+      <el-form-item prop="staffNo" label="员工号">
+        <el-input v-model="formData.staffNo" />
+      </el-form-item>
+      <el-form-item prop="mobile" label="联系电话">
+        <el-input v-model="formData.mobile" type="number" />
+      </el-form-item>
+      <el-form-item prop="department" label="部门">
+        <el-input v-model="formData.department" />
+      </el-form-item>
+      <el-form-item prop="jobTitle" label="职务">
+        <el-input v-model="formData.jobTitle" />
+      </el-form-item>
+    </el-form>
+
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="primary" @click="submitForm"> 提交 </el-button>
+        <el-button @click="showDialog = false">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import useTeamStore from '../../store/userTeam';
+  import { PersonInfoType, PositionType, saveTeamPersonnel } from '@/api/emergency-organization/teams';
+
+  const props = defineProps<{ positionOptions: PositionType[]; personInfo: PersonInfoType }>();
+
+  type PersonInfo = {
+    id: number;
+    positionId: string;
+    realname: string;
+    staffNo: string;
+    mobile: string;
+    department: string;
+    jobTitle: string;
+  };
+
+  const showDialog = ref(false);
+
+  const { curTeam, loadingTeamInfo } = storeToRefs(useTeamStore());
+  const { refreshCurTeamInfo } = useTeamStore();
+
+  const formData = ref<PersonInfoType>({
+    id: 0,
+    userId: 0,
+    positionId: '',
+    realname: '',
+    staffNo: '',
+    mobile: '',
+    department: '',
+    jobTitle: '',
+  });
+  const ruleFormRef = ref<FormInstance>();
+  const rules = ref<FormRules<PersonInfo>>({
+    positionId: [{ required: true, message: '请选择岗位', trigger: 'blur' }],
+    realname: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+    staffNo: [{ required: true, message: '请输入员工号', trigger: 'blur' }],
+    mobile: [{ required: true, message: '请输入联系电话', trigger: 'blur' }],
+  });
+
+  function dialogShow() {
+    showDialog.value = true;
+  }
+
+  async function submitForm() {
+    await ruleFormRef.value!.validate(async (valid) => {
+      if (valid) {
+        loadingTeamInfo.value = true;
+        showDialog.value = false;
+
+        const data = {
+          teamId: curTeam.value!.teamId,
+          ...formData.value,
+        };
+        await saveTeamPersonnel(data);
+        await refreshCurTeamInfo();
+
+        loadingTeamInfo.value = false;
+      }
+    });
+  }
+
+  watch(
+    () => showDialog.value,
+    () => {
+      if (showDialog.value) {
+        formData.value = props.personInfo;
+      }
+    },
+  );
+
+  defineExpose({
+    dialogShow,
+  });
+</script>
+
+<style scoped lang="scss">
+  .edit-person-dialog__header {
+  }
+  .edit-person-dialog__form {
+    display: grid;
+    grid-template-columns: repeat(2, 45%);
+    justify-content: space-around;
+
+    // 去掉el input右侧的小箭头
+    :deep(input::-webkit-outer-spin-button),
+    :deep(input::-webkit-inner-spin-button) {
+      -webkit-appearance: none;
+    }
+  }
+
+  :deep(.el-dialog__header) {
+    text-align: left;
+    font-weight: bold;
+  }
+</style>
+
+<style>
+  .edit-person-dialog {
+    min-height: 0;
+  }
+</style>

+ 127 - 0
src/views/emergency/organization/team-management/team-info/components/PersonalInfo.vue

@@ -0,0 +1,127 @@
+<template>
+  <div class="title"> 人员信息 </div>
+
+  <el-button type="primary" @click="showPositionSetting"> 职位设置 </el-button>
+  <el-button type="primary" @click="showAddPerson"> 添加人员 </el-button>
+
+  <div class="person-info-table">
+    <el-table :data="tableData" border max-height="700">
+      <el-table-column prop="date" label="序号">
+        <template #default="scope">
+          {{ scope.$index + 1 }}
+        </template>
+      </el-table-column>
+      <el-table-column prop="title" label="岗位职责" />
+      <el-table-column prop="realname" label="姓名" />
+      <el-table-column prop="staffNo" label="员工号" />
+      <el-table-column prop="mobile" label="联系方式" />
+      <el-table-column prop="department" label="部门" />
+      <el-table-column prop="jobTitle" label="职务" />
+      <el-table-column fixed="right" label="操作">
+        <template #default="scope">
+          <el-button link type="primary" size="small" @click.prevent="editRow(scope.row)"> 编辑 </el-button>
+          <el-button link type="primary" size="small" @click.prevent="deleteRow(scope.row)"> 删除 </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </div>
+  <PositionSetting ref="positionSettingRef" />
+  <AddPerson ref="addPersonRef" :position-options="positionOptions" />
+  <EditPerson ref="editPersonRef" :position-options="positionOptions" :person-info="editPersonInfo" />
+</template>
+
+<script lang="ts" setup>
+  import { ref, watch } from 'vue';
+  import { ElMessageBox, ElMessage } from 'element-plus';
+  import { storeToRefs } from 'pinia';
+  import { TeamPersonnelInfoType } from '../../type';
+  import PositionSetting from './PositionSetting.vue';
+  import useTeamStore from '../../store/userTeam';
+  import AddPerson from './AddPerson.vue';
+  import EditPerson from './EditPerson.vue';
+  import { PositionType, queryTeamPositionList, deleteTeamPersonnel } from '@/api/emergency-organization/teams';
+
+  const { teamAndPersonInfo, loadingTeamInfo } = storeToRefs(useTeamStore());
+  const { refreshCurTeamInfo } = useTeamStore();
+
+  const tableData = ref<TeamPersonnelInfoType[]>([]);
+  const positionSettingRef = ref<InstanceType<typeof PositionSetting>>();
+  const addPersonRef = ref<InstanceType<typeof AddPerson>>();
+  const editPersonRef = ref<InstanceType<typeof EditPerson>>();
+
+  const positionOptions = ref<PositionType[]>([]);
+  const editPersonInfo = ref({
+    id: 0,
+    userId: 0,
+    positionId: '',
+    realname: '',
+    staffNo: '',
+    mobile: '',
+    department: '',
+    jobTitle: '',
+  });
+
+  async function editRow(row) {
+    if (!(await positionIsSet())) return;
+
+    editPersonInfo.value = {
+      id: row.id,
+      userId: row.userId,
+      positionId: row.positionId,
+      realname: row.realname,
+      staffNo: row.staffNo,
+      mobile: row.mobile,
+      department: row.department,
+      jobTitle: row.jobTitle,
+    };
+
+    editPersonRef.value?.dialogShow();
+  }
+
+  function deleteRow(row) {
+    ElMessageBox.confirm('是否确认删除该人员').then(async () => {
+      loadingTeamInfo.value = true;
+      await deleteTeamPersonnel(row.id);
+      await refreshCurTeamInfo();
+      loadingTeamInfo.value = false;
+    });
+  }
+
+  function showPositionSetting() {
+    positionSettingRef.value?.dialogShow();
+  }
+
+  async function showAddPerson() {
+    if (!(await positionIsSet())) return;
+
+    addPersonRef.value?.dialogShow();
+  }
+
+  async function positionIsSet() {
+    loadingTeamInfo.value = true;
+    positionOptions.value = await queryTeamPositionList(teamAndPersonInfo.value!.teamId);
+    loadingTeamInfo.value = false;
+
+    if (positionOptions.value.length === 0) {
+      ElMessage({
+        message: '请先设置职位',
+        type: 'warning',
+      });
+      return false;
+    }
+
+    return true;
+  }
+
+  watch(
+    () => teamAndPersonInfo.value,
+    () => {
+      tableData.value = teamAndPersonInfo.value?.personnelList || [];
+    },
+  );
+</script>
+<style scoped lang="scss">
+  .person-info-table {
+    margin-top: 15px;
+  }
+</style>

+ 110 - 0
src/views/emergency/organization/team-management/team-info/components/PositionSetting.vue

@@ -0,0 +1,110 @@
+<template>
+  <el-dialog v-model="showDialog" class="add-position-dialog" title="岗位职责设置" width="800" destroy-on-close>
+    <el-form :model="formData" ref="ruleFormRef" :rules="rules">
+      <el-table :data="formData" border v-loading="showLoading">
+        <el-table-column align="center" prop="positionLevel" width="200" label="等级" />
+
+        <el-table-column align="center" label="岗位职责名称">
+          <template #default="{ row, $index }">
+            <el-form-item :prop="`[${$index}].title`" :rules="rules.title">
+              <el-input v-model="row.title" />
+            </el-form-item>
+          </template>
+        </el-table-column>
+
+        <el-table-column align="center" width="200" label="操作">
+          <template #default="{ $index }">
+            <el-button v-if="$index + 1 === formData.length" @click="addRow">添加</el-button>
+            <el-button v-if="formData.length > 1" @click="deleteRow($index)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-form>
+
+    <template #footer>
+      <div v-if="!showLoading" class="dialog-footer">
+        <el-button type="primary" @click="submitForm"> 提交 </el-button>
+        <el-button @click="showDialog = false">取消</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import type { FormInstance, FormRules } from 'element-plus';
+  import useTeamStore from '../../store/userTeam';
+  import { PositionType, queryTeamPositionList } from '@/api/emergency-organization/teams';
+
+  const { curTeam } = storeToRefs(useTeamStore());
+  const { setPositionInfo } = useTeamStore();
+
+  const showDialog = ref(false);
+  const showLoading = ref(true);
+  const formData = ref<PositionType[]>([
+    {
+      positionLevel: 1,
+      title: '',
+    },
+  ]);
+  const ruleFormRef = ref<FormInstance>();
+  const rules = ref<FormRules<PositionType>>({
+    title: [{ required: true, message: '请输入岗位', trigger: 'blur' }],
+  });
+
+  function dialogShow() {
+    showDialog.value = true;
+  }
+
+  function addRow() {
+    formData.value.push({ positionLevel: formData.value.length + 1, title: '' });
+  }
+
+  function deleteRow(index: number) {
+    formData.value.splice(index, 1);
+    formData.value.forEach((row, index) => (row.positionLevel = index + 1));
+  }
+
+  async function submitForm() {
+    await ruleFormRef.value!.validate((valid) => {
+      if (valid) {
+        setPositionInfo(formData.value);
+        showDialog.value = false;
+      }
+    });
+  }
+
+  watch(
+    () => showDialog.value,
+    async () => {
+      if (showDialog.value && curTeam.value?.teamId) {
+        const res = await queryTeamPositionList(curTeam.value.teamId);
+        formData.value = res.map((position) => {
+          return { id: position.id, positionLevel: position.positionLevel, title: position.title };
+        });
+
+        if (!formData.value.length) {
+          formData.value.push({
+            positionLevel: 1,
+            title: '',
+          });
+        }
+
+        showLoading.value = false;
+      } else showLoading.value = true;
+    },
+  );
+
+  defineExpose({
+    dialogShow,
+  });
+</script>
+
+<style scoped lang="scss"></style>
+
+<style>
+  .add-position-dialog {
+    min-height: 0;
+  }
+</style>

+ 68 - 0
src/views/emergency/organization/team-management/team-info/components/TeamInfo.vue

@@ -0,0 +1,68 @@
+<template>
+  <div class="title"> 队伍信息 </div>
+  <el-form :model="formData" ref="ruleFormRef" :rules="rules" label-position="top">
+    <el-form-item prop="memberCount" label="队伍人数">
+      <el-input v-model="formData.memberCount" type="number" width="200" />
+    </el-form-item>
+    <el-form-item prop="description" label="队伍职责">
+      <el-input v-model="formData.description" type="textarea" />
+    </el-form-item>
+    <el-form-item>
+      <el-button type="primary" @click="submitForm">保存</el-button>
+    </el-form-item>
+  </el-form>
+</template>
+
+<script setup lang="ts">
+  import { ref, watch } from 'vue';
+  import { storeToRefs } from 'pinia';
+  import { FormInstance, FormRules, ElMessage } from 'element-plus';
+  import { TeamInfoType } from '../../type';
+  import useTeamStore from '../../store/userTeam';
+
+  const { teamAndPersonInfo } = storeToRefs(useTeamStore());
+  const { setTeamInfo } = useTeamStore();
+
+  const formData = ref<TeamInfoType>({
+    memberCount: 0,
+    description: '',
+  });
+  const ruleFormRef = ref<FormInstance>();
+  const rules = ref<FormRules<TeamInfoType>>({
+    memberCount: [{ required: true, message: '请输入人数', trigger: 'blur' }],
+    description: [{ required: true, message: '请输入职责', trigger: 'blur' }],
+  });
+
+  async function submitForm() {
+    await ruleFormRef.value!.validate(async (valid) => {
+      if (valid) {
+        const data = {
+          id: teamAndPersonInfo.value!.teamId,
+          memberCount: formData.value.memberCount,
+          description: formData.value.description,
+        };
+        await setTeamInfo(data);
+        ElMessage({
+          message: '保存成功',
+          type: 'success',
+        });
+      }
+    });
+  }
+
+  watch(
+    () => teamAndPersonInfo.value,
+    () => {
+      formData.value.memberCount = teamAndPersonInfo.value?.memberCount || 0;
+      formData.value.description = teamAndPersonInfo.value?.description || '';
+    },
+  );
+</script>
+
+<style scoped lang="scss">
+  // 去掉el input右侧的小箭头
+  :deep(input::-webkit-outer-spin-button),
+  :deep(input::-webkit-inner-spin-button) {
+    -webkit-appearance: none;
+  }
+</style>

+ 31 - 0
src/views/emergency/organization/team-management/type.ts

@@ -0,0 +1,31 @@
+export type LeaderTeamType = {
+  teamId: number;
+  teamName: string;
+  memberCount: number;
+  level: number;
+  children: LeaderTeamType[];
+};
+
+export type TeamAndPersonInfoType = {
+  teamId: number;
+  teamName: string;
+  personnelList: TeamPersonnelInfoType[];
+} & TeamInfoType;
+
+export type TeamInfoType = {
+  memberCount: number;
+  description: string;
+};
+
+export type TeamPersonnelInfoType = {
+  teamId: number;
+  userId: number;
+  realname: string;
+  positionId: number; //	职位ID
+  positionLevel: number; //	职位等级
+  jobTitle: string; //职务
+  title: string; //  岗位职责
+  staffNo: string;
+  department: string;
+  mobile: string;
+};