Explorar o código

feat: 应急管理-应急架构体系新增全部队伍信息查看

ai0187 hai 3 semanas
pai
achega
8db5e547fa

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

@@ -22,6 +22,16 @@ export function queryEmergencyTeamDetail(teamId: number) {
   });
 }
 
+/**
+ * 查询所有队伍详细信息
+ */
+export function queryAllEmergencyTeamDetail() {
+  return http.request<TeamAndPersonInfoType[]>({
+    url: '/emergencySystem/queryAllEmergencyTeamDetail',
+    method: 'get',
+  });
+}
+
 export type SetTeamInfoType = {
   id: number;
   memberCount?: number;

+ 1 - 1
src/views/emergency/components/OrgChart.vue

@@ -102,7 +102,7 @@
         animation: true, // 启用布局动画
       },
       autoFit: {
-        type: 'center', // 自适应类型:'view' 或 'center'
+        type: 'view', // 自适应类型:'view' 或 'center'
         // options: {
         //   // 仅适用于 'view' 类型
         //   when: 'always', // 何时适配:'overflow'(仅当内容溢出时) 或 'always'(总是适配)

+ 116 - 14
src/views/emergency/organization/PageOrganization.vue

@@ -3,25 +3,47 @@
     <div class="safety-platform-container__header">
       <div class="breadcrumb-title"> 应急架构体系 </div>
     </div>
-    <div class="safety-platform-container__main">
-      <OrgChart :treeData="treeData" @node-click="handleNodeClick" v-if="treeData.id !== '-1'" />
-      <div v-else class="no-data">暂无队伍</div>
+    <div class="safety-platform-container__main" :class="{ 'zoom-mode': isChartZoomed }">
+      <div class="chart-container" ref="chartContainerRef" v-loading="loadingTeams">
+        <OrgChart
+          :treeData="treeData"
+          @node-click="handleNodeClick"
+          @canvas-click="handleCanvasClick"
+          v-if="treeData.id !== '-1'"
+        />
+        <div class="no-data" v-else>暂无队伍</div>
+        <div class="chart-actions">
+          <el-button v-if="!isChartFullscreen" size="small" @click="toggleChartZoom">
+            {{ isChartZoomed ? '恢复布局' : '放大模式' }}
+          </el-button>
+          <el-button size="small" @click="toggleChartFullscreen">
+            {{ isChartFullscreen ? '退出全屏' : '全屏查看' }}
+          </el-button>
+        </div>
+      </div>
+      <div class="detail-container" v-loading="loadingTeamsDetail">
+        <TeamDetailList :selected-team-id="selectedTeamId" class="team-detail" />
+      </div>
     </div>
     <div class="safety-platform-container__footer" v-if="showOperationBar">
       <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 { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
   import { useRouter } from 'vue-router';
-  import OrgChart from '../components/OrgChart.vue';
-  import TeamDetailDrawer from './components/TeamDetailDrawer.vue';
+  import { storeToRefs } from 'pinia';
   import { useUserInfoHook } from '@/views/disaster/hooks';
   import useTeamStore from './team-management/store/userTeam';
   import { EMERGENCY_PERMISSIONS } from '@/views/emergency/constant';
+  import OrgChart from '../components/OrgChart.vue';
+  import TeamDetailList from './components/TeamDetailList.vue';
+
+  const { permissions } = useUserInfoHook();
+  const { loadingTeams, loadingTeamsDetail } = storeToRefs(useTeamStore());
+  const { getLeaderTeams } = useTeamStore();
 
   type OrganizationTreeType = {
     id: string;
@@ -51,20 +73,49 @@
   const router = useRouter();
 
   const showOperationBar = ref(false);
-
-  const { permissions } = useUserInfoHook();
-  const { getLeaderTeams } = useTeamStore();
-
-  const teamDetailDrawerRef = ref<InstanceType<typeof TeamDetailDrawer>>();
+  const chartContainerRef = ref<HTMLElement | null>(null);
+  const isChartFullscreen = ref(false);
+  const isChartZoomed = ref(false);
 
   const selectedTeamId = ref<number | null>(null);
 
   const handleNodeClick = (nodeData: any) => {
     selectedTeamId.value = Number(nodeData.id);
+  };
 
-    teamDetailDrawerRef.value?.drawerShow();
+  const handleCanvasClick = () => {
+    selectedTeamId.value = null;
   };
 
+  async function toggleChartFullscreen() {
+    const el = chartContainerRef.value;
+    if (!el) return;
+
+    if (document.fullscreenElement === el) {
+      await document.exitFullscreen();
+      await triggerChartResize();
+      return;
+    }
+
+    await el.requestFullscreen();
+    await triggerChartResize();
+  }
+
+  async function toggleChartZoom() {
+    isChartZoomed.value = !isChartZoomed.value;
+    await triggerChartResize();
+  }
+
+  function syncFullscreenState() {
+    isChartFullscreen.value = document.fullscreenElement === chartContainerRef.value;
+  }
+
+  async function triggerChartResize() {
+    await nextTick();
+    // G6 组件内部监听 window resize,这里主动触发一次确保尺寸立即更新
+    window.dispatchEvent(new Event('resize'));
+  }
+
   function convertData(leaderTeams): OrganizationTreeType {
     return {
       id: leaderTeams.teamId.toString(),
@@ -76,19 +127,70 @@
   }
 
   onMounted(async () => {
+    document.addEventListener('fullscreenchange', syncFullscreenState);
+
     showOperationBar.value = Boolean(
       permissions.find((item: { code: string }) => item.code === EMERGENCY_PERMISSIONS.ORGANIZATION_MANAGEMENT),
     );
 
     const res = await getLeaderTeams();
-
     treeData.value = convertData(res);
   });
+
+  onBeforeUnmount(() => {
+    document.removeEventListener('fullscreenchange', syncFullscreenState);
+  });
 </script>
 
 <style lang="scss" scoped>
   @use '@/styles/page-details-layout.scss' as *;
 
+  .chart-container {
+    position: relative;
+    width: 100%;
+    height: 40%;
+    box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
+    border-radius: 8px;
+  }
+
+  .chart-container:fullscreen {
+    height: 100%;
+    padding: 12px;
+    background: #ffffff;
+    border-radius: 0;
+  }
+
+  .chart-actions {
+    position: absolute;
+    display: flex;
+    gap: 8px;
+    right: 12px;
+    bottom: 12px;
+    z-index: 2;
+  }
+
+  .safety-platform-container__main.zoom-mode {
+    .chart-container {
+      height: 75%;
+    }
+
+    .detail-container {
+      height: 25%;
+    }
+  }
+
+  .detail-container {
+    width: 100%;
+    height: 60%;
+    padding-top: 20px;
+    overflow: auto;
+
+    .team-detail {
+      width: 60%;
+      margin: 0 auto;
+    }
+  }
+
   .no-data {
     width: 100%;
     height: 100%;

+ 213 - 0
src/views/emergency/organization/components/TeamDetailList.vue

@@ -0,0 +1,213 @@
+<template>
+  <div class="team-detail-list">
+    <template v-if="displayTeams.length">
+      <section v-for="team in displayTeams" :key="team.teamId" class="team-detail-list__section">
+        <header class="team-detail-list__header">
+          <h2 class="team-detail-list__title">{{ team.teamName }}</h2>
+          <span class="team-detail-list__count">共{{ team.memberCount ?? 0 }}人</span>
+        </header>
+
+        <div
+          v-for="row in roleRowsForTeam(team.personnelList)"
+          :key="`${team.teamId}-${row.positionLevel}`"
+          class="team-detail-list__role-row"
+        >
+          <span class="team-detail-list__role-pill">{{ row.title }}</span>
+          <div class="team-detail-list__names">
+            <el-tooltip v-for="person in row.members" :key="person.id" placement="top" effect="light" :show-after="200">
+              <template #content>
+                <div class="team-detail-list__tooltip">
+                  <div>部门:{{ person.department || '—' }}</div>
+                  <div>工号:{{ person.staffNo || '—' }}</div>
+                  <div>手机:{{ person.mobile || '—' }}</div>
+                </div>
+              </template>
+              <span class="team-detail-list__name">{{ person.realname }}</span>
+            </el-tooltip>
+          </div>
+        </div>
+
+        <div v-if="team.description?.trim()" class="team-detail-list__duty">
+          <h3 class="team-detail-list__duty-title">队伍职责</h3>
+          <div class="team-detail-list__duty-content">{{ team.description }}</div>
+        </div>
+      </section>
+    </template>
+    <div v-else class="team-detail-list__empty">暂无队伍详情</div>
+  </div>
+</template>
+
+<script setup lang="ts">
+  import { computed, ref, onMounted } from 'vue';
+  import useTeamStore from '../team-management/store/userTeam';
+  import type { TeamAndPersonInfoType, TeamPersonnelInfoType } from '../team-management/type';
+
+  const props = defineProps<{
+    selectedTeamId: number | null;
+  }>();
+
+  type RoleRow = {
+    positionLevel: number;
+    title: string;
+    members: TeamPersonnelInfoType[];
+  };
+
+  const teamStore = useTeamStore();
+  const teams = ref<TeamAndPersonInfoType[]>([]);
+  const displayTeams = computed(() => {
+    if (props.selectedTeamId === null) return teams.value;
+    return teams.value.filter((team) => team.teamId === props.selectedTeamId);
+  });
+
+  function roleRowsForTeam(personnelList: TeamPersonnelInfoType[] | undefined): RoleRow[] {
+    if (!personnelList?.length) return [];
+    const levelMap = new Map<number, RoleRow>();
+    for (const person of personnelList) {
+      const level = person.positionLevel;
+      if (!levelMap.has(level)) {
+        levelMap.set(level, {
+          positionLevel: level,
+          title: person.title || '成员',
+          members: [],
+        });
+      }
+      levelMap.get(level)!.members.push(person);
+    }
+    return [...levelMap.values()].sort((a, b) => a.positionLevel - b.positionLevel);
+  }
+
+  onMounted(async () => {
+    teams.value = await teamStore.getAllEmergencyTeamDetail();
+  });
+</script>
+
+<style scoped lang="scss">
+  $primary: #1777ff;
+  $text: #333333;
+  $text-muted: #666666;
+
+  .team-detail-list {
+    padding: 0 10px;
+  }
+
+  .team-detail-list__section {
+    padding: 18px 20px 20px;
+    background: #ffffff;
+    border: 1px solid #e8eef8;
+    border-left: 4px solid $primary;
+    border-radius: 10px;
+    box-shadow: 0 4px 14px rgba(23, 119, 255, 0.06);
+
+    & + & {
+      margin-top: 18px;
+    }
+  }
+
+  .team-detail-list__header {
+    display: flex;
+    align-items: baseline;
+    justify-content: space-between;
+    gap: 16px;
+    margin: -18px -20px 16px;
+    padding: 12px 20px;
+    background: linear-gradient(90deg, rgba(23, 119, 255, 0.08), rgba(23, 119, 255, 0.02));
+    border-bottom: 1px solid #edf2fb;
+    border-radius: 10px 10px 0 0;
+
+    .team-detail-list__title {
+      margin: 0;
+      font-size: 18px;
+      font-weight: 600;
+      color: $text;
+    }
+
+    .team-detail-list__count {
+      flex-shrink: 0;
+      font-size: 14px;
+      color: $primary;
+    }
+  }
+
+  .team-detail-list__role-row {
+    display: flex;
+    align-items: flex-start;
+    gap: 16px;
+    margin-bottom: 14px;
+  }
+
+  .team-detail-list__role-pill {
+    flex-shrink: 0;
+    min-width: 56px;
+    padding: 4px 14px;
+    font-size: 13px;
+    color: $primary;
+    text-align: center;
+    background: #ffffff;
+    border: 1px solid $primary;
+    border-radius: 999px;
+  }
+
+  .team-detail-list__names {
+    display: flex;
+    flex-wrap: wrap;
+    align-items: center;
+    gap: 12px 24px;
+    padding-top: 2px;
+    line-height: 1.5;
+  }
+
+  .team-detail-list__name {
+    cursor: default;
+    font-size: 14px;
+    color: $text-muted;
+    border-bottom: 1px dashed transparent;
+
+    &:hover {
+      color: $primary;
+      border-bottom-color: rgba($primary, 0.35);
+    }
+  }
+
+  .team-detail-list__tooltip {
+    padding: 2px 0;
+    font-size: 13px;
+    line-height: 1.6;
+    color: $text-muted;
+
+    div + div {
+      margin-top: 4px;
+    }
+  }
+
+  :deep(.el-tooltip__trigger) {
+    display: inline-block;
+  }
+
+  .team-detail-list__duty {
+    margin-top: 22px;
+    padding-top: 14px;
+    border-top: 1px dashed #dbe7fb;
+  }
+
+  .team-detail-list__duty-title {
+    margin: 0 0 10px;
+    font-size: 15px;
+    font-weight: 600;
+    color: $text;
+  }
+
+  .team-detail-list__duty-content {
+    margin: 0;
+    font-size: 14px;
+    line-height: 1.7;
+    color: $text-muted;
+    white-space: pre-wrap;
+  }
+
+  .team-detail-list__empty {
+    padding: 48px 0;
+    text-align: center;
+    font-size: 14px;
+    color: #999999;
+  }
+</style>

+ 16 - 1
src/views/emergency/organization/team-management/store/userTeam.ts

@@ -1,7 +1,11 @@
 import { ref } from 'vue';
 import { defineStore } from 'pinia';
 import { LeaderTeamType, TeamAndPersonInfoType } from '../type';
-import { queryLeaderTeam, queryEmergencyTeamDetail } from '@/api/emergency-organization/teams';
+import {
+  queryLeaderTeam,
+  queryEmergencyTeamDetail,
+  queryAllEmergencyTeamDetail,
+} from '@/api/emergency-organization/teams';
 import { PositionType, saveTeamPosition } from '@/api/emergency-organization/teams';
 import {
   SetTeamInfoType,
@@ -23,9 +27,18 @@ const useTeamStore = defineStore('useTeam', () => {
   const curTeam = ref<LeaderTeamType>();
   const teamAndPersonInfo = ref<TeamAndPersonInfoType>(); // 右侧的队伍信息和人员信息表格数据
 
+  const loadingTeamsDetail = ref(false);
   const loadingTeams = ref(false);
   const loadingTeamInfo = ref(false);
 
+  // 获取所有队伍详细信息
+  async function getAllEmergencyTeamDetail() {
+    loadingTeamsDetail.value = true;
+    const res = await queryAllEmergencyTeamDetail();
+    loadingTeamsDetail.value = false;
+    return res ?? [];
+  }
+
   async function getLeaderTeams() {
     loadingTeams.value = true;
 
@@ -152,8 +165,10 @@ const useTeamStore = defineStore('useTeam', () => {
     leaderTeamTree,
     curTeam,
     teamAndPersonInfo,
+    loadingTeamsDetail,
     loadingTeams,
     loadingTeamInfo,
+    getAllEmergencyTeamDetail,
     getLeaderTeams,
     createFirstLevelTeam,
     createTeam,

+ 4 - 3
src/views/emergency/organization/team-management/type.ts

@@ -18,14 +18,15 @@ export type TeamInfoType = {
 };
 
 export type TeamPersonnelInfoType = {
-  teamId: number;
+  id: number;
   userId: number;
   realname: string;
+  teamId: number;
   positionId: number; //	职位ID
-  positionLevel: number; //	职位等级
   jobTitle: string; //职务
-  title: string; //  岗位职责
   staffNo: string;
   department: string;
   mobile: string;
+  title: string; // 职位名称
+  positionLevel: number; //	职位等级
 };