Jelajahi Sumber

人员结构看板/首页钻取弹框支持全屏;代理地址注释调整

lihaiting 1 hari lalu
induk
melakukan
6f5fa382d8

+ 56 - 0
src/mixins/board-fullscreen.js

@@ -0,0 +1,56 @@
+/**
+ * 看板页全屏切换(首页 / 人员结构看板 / 人员结构分析看板共用)
+ *
+ * ★ 关键点:全屏对象是页面根元素(this.$el),不是 documentElement。
+ * Element-UI 的日期/下拉/弹窗默认挂到 body —— 全屏后 body 不在全屏渲染层里,
+ * 弹层会整个看不见。所以各页面必须把
+ *   el-date-picker / el-dialog 绑 :append-to-body="!isBoardFullscreen"
+ *   el-select        绑 :popper-append-to-body="!isBoardFullscreen"
+ * 全屏期间弹层挂回页面内部,选择器才能正常点开。
+ *
+ * 图表尺寸:全屏切换会改变根节点尺寸,页面已有的 ResizeObserver 会触发;
+ * mixin 里再兜一层,兼容复用各页面已有的 resizeCharts / handleResize 方法名。
+ */
+export default {
+  data() {
+    return {
+      isBoardFullscreen: false,
+    };
+  },
+  mounted() {
+    document.addEventListener("fullscreenchange", this.handleBoardFullscreenChange);
+    document.addEventListener("webkitfullscreenchange", this.handleBoardFullscreenChange);
+  },
+  beforeDestroy() {
+    document.removeEventListener("fullscreenchange", this.handleBoardFullscreenChange);
+    document.removeEventListener("webkitfullscreenchange", this.handleBoardFullscreenChange);
+  },
+  methods: {
+    getBoardFullscreenElement() {
+      return document.fullscreenElement || document.webkitFullscreenElement || null;
+    },
+    toggleBoardFullscreen() {
+      const doc = document;
+      if (this.getBoardFullscreenElement()) {
+        const exit = doc.exitFullscreen || doc.webkitExitFullscreen;
+        if (exit) exit.call(doc);
+        return;
+      }
+      const root = this.$el;
+      const request = root.requestFullscreen || root.webkitRequestFullscreen;
+      if (!request) {
+        this.$message.warning("当前浏览器不支持全屏展示");
+        return;
+      }
+      request.call(root);
+    },
+    handleBoardFullscreenChange() {
+      this.isBoardFullscreen = Boolean(this.getBoardFullscreenElement());
+      /* 下一帧重算图表:兼容三个页面已有的 resize 方法名 */
+      this.$nextTick(() => {
+        if (typeof this.resizeCharts === "function") this.resizeCharts();
+        if (typeof this.handleResize === "function") this.handleResize();
+      });
+    },
+  },
+};

+ 8 - 1
src/views/homeView/components/MetricDrillDownDialog.vue

@@ -1,5 +1,6 @@
 <template>
 <template>
-  <el-dialog :visible.sync="show" :title="config.title" custom-class="metric-drill-dialog" append-to-body width="1100px"
+  <el-dialog :visible.sync="show" :title="config.title" custom-class="metric-drill-dialog"
+    :append-to-body="appendToBody" width="1100px"
     top="6vh" :close-on-click-modal="false">
     top="6vh" :close-on-click-modal="false">
     <div v-if="config" class="drill-body" v-loading="loading">
     <div v-if="config" class="drill-body" v-loading="loading">
       <!-- 口径行:与卡片同源的统计口径 + 数量 -->
       <!-- 口径行:与卡片同源的统计口径 + 数量 -->
@@ -335,6 +336,12 @@ export default {
     statisticsDate: { type: String, default: "" },
     statisticsDate: { type: String, default: "" },
     /** 所属机构(首页机构选择联动) */
     /** 所属机构(首页机构选择联动) */
     organizationId: { type: String, default: "" },
     organizationId: { type: String, default: "" },
+    /**
+     * 弹窗是否挂到 body:默认挂 body(原行为不变)。
+     * 首页全屏(board-fullscreen mixin)时传 false —— 全屏对象是页面根元素,
+     * 挂 body 的弹窗在全屏渲染层外会整窗不可见。
+     */
+    appendToBody: { type: Boolean, default: true },
   },
   },
   data() {
   data() {
     return {
     return {

+ 45 - 2
src/views/homeView/index.vue

@@ -27,6 +27,12 @@
       <div class="overview-title__wing overview-title__wing--right">
       <div class="overview-title__wing overview-title__wing--right">
         <i></i><i></i><i></i>
         <i></i><i></i><i></i>
       </div>
       </div>
+      <!-- 右上角全屏(2026-11-03 用户要求,纯 icon;全屏对象是页面根元素,
+           Element 弹层默认挂 body 在全屏层外看不见 → 选择器全部条件挂载(见 board-fullscreen.js 注释) -->
+      <button class="board-fullscreen-btn" type="button" :aria-pressed="isBoardFullscreen"
+        :title="isBoardFullscreen ? '退出全屏' : '全屏'" @click="toggleBoardFullscreen">
+        <i :class="isBoardFullscreen ? 'el-icon-copy-document' : 'el-icon-full-screen'"></i>
+      </button>
       <p v-if="reportLabel" class="overview-title__meta">
       <p v-if="reportLabel" class="overview-title__meta">
         <span class="live-dot"></span>{{ reportLabel }}
         <span class="live-dot"></span>{{ reportLabel }}
       </p>
       </p>
@@ -95,7 +101,7 @@
         <div class="panel-header">
         <div class="panel-header">
           <h2><em></em>部门编制占比</h2>
           <h2><em></em>部门编制占比</h2>
           <el-select v-model="organizationId" size="mini" clearable filterable placeholder="所属机构选择" class="org-select"
           <el-select v-model="organizationId" size="mini" clearable filterable placeholder="所属机构选择" class="org-select"
-            popper-class="hr-org-popper" @change="loadOverview">
+            popper-class="hr-org-popper" :popper-append-to-body="!isBoardFullscreen" @change="loadOverview">
             <el-option v-for="item in organizationOptions" :key="item.value" :label="item.label" :value="item.value" />
             <el-option v-for="item in organizationOptions" :key="item.value" :label="item.label" :value="item.value" />
           </el-select>
           </el-select>
         </div>
         </div>
@@ -141,13 +147,14 @@
     </section>
     </section>
 
 
     <metric-drill-down-dialog :visible.sync="drillDownVisible" :code="drillDownCode" :report-month="reportMonth"
     <metric-drill-down-dialog :visible.sync="drillDownVisible" :code="drillDownCode" :report-month="reportMonth"
-      :statistics-date="statisticsDate" :organization-id="organizationId" />
+      :statistics-date="statisticsDate" :organization-id="organizationId" :append-to-body="!isBoardFullscreen" />
   </main>
   </main>
 </template>
 </template>
 
 
 <script>
 <script>
 import * as echarts from "echarts";
 import * as echarts from "echarts";
 import { getHrOverviewDataCenter } from "@/api/hr/overview";
 import { getHrOverviewDataCenter } from "@/api/hr/overview";
+import boardFullscreen from "@/mixins/board-fullscreen";
 /* 顶部卡片下钻弹窗(点击卡片开弹窗,不再跳路由页) */
 /* 顶部卡片下钻弹窗(点击卡片开弹窗,不再跳路由页) */
 import MetricDrillDownDialog, {
 import MetricDrillDownDialog, {
   DRILL_DOWN_CONFIGS,
   DRILL_DOWN_CONFIGS,
@@ -227,6 +234,7 @@ function toNullableNumber(value) {
 export default {
 export default {
   name: "HomeView",
   name: "HomeView",
   components: { MetricDrillDownDialog },
   components: { MetricDrillDownDialog },
+  mixins: [boardFullscreen],
   data() {
   data() {
     return {
     return {
       loading: false,
       loading: false,
@@ -773,6 +781,41 @@ export default {
 
 
 <style src="../../styles/views/HomeView.scss" scoped lang="scss"></style>
 <style src="../../styles/views/HomeView.scss" scoped lang="scss"></style>
 
 
+<!-- 右上角全屏按钮(纯 icon,scoped):与「统计月份」副标题同一行垂直居中,
+     副标题绝对定位 right:4px 会顶到最右,故左移 40px 给 icon 按钮让位 -->
+<style scoped lang="scss">
+.board-fullscreen-btn {
+  position: absolute;
+  top: 50%;
+  right: 4px;
+  z-index: 5;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 30px;
+  height: 30px;
+  padding: 0;
+  font-size: 16px;
+  color: #4fe3ff;
+  cursor: pointer;
+  background: linear-gradient(135deg, rgba(0, 60, 100, 0.55), rgba(0, 25, 50, 0.75));
+  border: 1px solid rgba(0, 210, 255, 0.4);
+  box-shadow: inset 0 0 10px rgba(0, 180, 255, 0.18);
+  transform: translateY(-50%);
+  transition: all 0.2s;
+
+  &:hover {
+    color: #fff;
+    border-color: rgba(0, 230, 255, 0.8);
+    box-shadow: 0 0 12px rgba(0, 200, 255, 0.4);
+  }
+}
+
+.overview-title__meta {
+  right: 42px;
+}
+</style>
+
 <!-- 机构选择器下拉弹框:el-select 的 popper 挂到 body,scoped 样式够不到,故用非 scoped 全局块(仅作用于 hr-org-popper) -->
 <!-- 机构选择器下拉弹框:el-select 的 popper 挂到 body,scoped 样式够不到,故用非 scoped 全局块(仅作用于 hr-org-popper) -->
 <style lang="scss">
 <style lang="scss">
 .hr-org-popper.el-popper,
 .hr-org-popper.el-popper,

+ 20 - 0
src/views/hrAnalysis/personnelStructureAnalysisBoard/index.vue

@@ -52,6 +52,11 @@
               :disabled="exporting" @click="exportBoard">
               :disabled="exporting" @click="exportBoard">
               <i class="el-icon-download"></i>{{ exporting ? "导出中" : "导出" }}
               <i class="el-icon-download"></i>{{ exporting ? "导出中" : "导出" }}
             </button>
             </button>
+            <!-- 右上角全屏(2026-11-03 用户要求);本页筛选用原生 select,全屏后原生渲染可正常选择 -->
+            <button class="header-button board-fullscreen-btn" type="button" :aria-pressed="isBoardFullscreen"
+              :title="isBoardFullscreen ? '退出全屏' : '全屏'" @click="toggleBoardFullscreen">
+              <i class="el-icon-full-screen"></i>{{ isBoardFullscreen ? "退出全屏" : "全屏" }}
+            </button>
           </div>
           </div>
         </header>
         </header>
 
 
@@ -260,6 +265,7 @@
 <script>
 <script>
 import * as echarts from "echarts";
 import * as echarts from "echarts";
 import BoardTitle from "@/components/board-title/index.vue";
 import BoardTitle from "@/components/board-title/index.vue";
+import boardFullscreen from "@/mixins/board-fullscreen";
 import { listOrganizations } from "@/api/organization";
 import { listOrganizations } from "@/api/organization";
 /* 机构选项取登录缓存(loginChangeGroupVOList);组织列表接口仍用于部门与名称映射 */
 /* 机构选项取登录缓存(loginChangeGroupVOList);组织列表接口仍用于部门与名称映射 */
 import { getOrgOptions, getCurrentOrgId } from "@/utils/current-org";
 import { getOrgOptions, getCurrentOrgId } from "@/utils/current-org";
@@ -425,6 +431,7 @@ const PERSONNEL_COLUMNS = [
 export default {
 export default {
   name: "PersonnelStructureAnalysisBoard",
   name: "PersonnelStructureAnalysisBoard",
   components: { PanelTitle, BoardTitle },
   components: { PanelTitle, BoardTitle },
+  mixins: [boardFullscreen],
   data() {
   data() {
     const now = new Date();
     const now = new Date();
     return {
     return {
@@ -1004,3 +1011,16 @@ export default {
 </script>
 </script>
 
 
 <style src="../../../styles/views/personnelStructureAnalysisBoard/index.css" scoped></style>
 <style src="../../../styles/views/personnelStructureAnalysisBoard/index.css" scoped></style>
+
+<!-- 右上角全屏按钮:沿用 header-button 基础样式,仅强调态微调(scoped) -->
+<style scoped>
+.board-fullscreen-btn.board-fullscreen-btn {
+  color: #4fe3ff;
+}
+
+.board-fullscreen-btn.board-fullscreen-btn:hover {
+  color: #fff;
+  border-color: rgba(0, 230, 255, 0.8);
+  box-shadow: 0 0 14px rgba(0, 200, 255, 0.4);
+}
+</style>

+ 44 - 7
src/views/hrAnalysis/personnelStructureDashboard/index.vue

@@ -9,32 +9,38 @@
           <div class="filter-item date-filter">
           <div class="filter-item date-filter">
             <span><i class="el-icon-date"></i> 统计月份</span>
             <span><i class="el-icon-date"></i> 统计月份</span>
             <el-date-picker v-model="filters.month" type="month" size="small" value-format="yyyy-MM"
             <el-date-picker v-model="filters.month" type="month" size="small" value-format="yyyy-MM"
-              placeholder="默认当前自然月" :clearable="false" popper-class="dark-popper" @change="reload" />
+              placeholder="默认当前自然月" :clearable="false" popper-class="dark-popper"
+              :append-to-body="!isBoardFullscreen" @change="reload" />
           </div>
           </div>
           <div class="filter-item organization-filter">
           <div class="filter-item organization-filter">
             <span><i class="el-icon-office-building"></i> 组织范围</span>
             <span><i class="el-icon-office-building"></i> 组织范围</span>
             <el-select v-model="filters.organizationId" size="small" clearable filterable popper-class="dark-popper"
             <el-select v-model="filters.organizationId" size="small" clearable filterable popper-class="dark-popper"
-              placeholder="全部授权组织" @change="handleOrganizationChange">
+              :popper-append-to-body="!isBoardFullscreen" placeholder="全部授权组织" @change="handleOrganizationChange">
               <el-option v-for="item in organizationList" :key="item.value" :label="item.label" :value="item.value" />
               <el-option v-for="item in organizationList" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
             </el-select>
           </div>
           </div>
           <div class="filter-item department-filter">
           <div class="filter-item department-filter">
             <span><i class="el-icon-s-grid"></i> 统计部门</span>
             <span><i class="el-icon-s-grid"></i> 统计部门</span>
             <el-select v-model="filters.departmentId" size="small" clearable filterable popper-class="dark-popper"
             <el-select v-model="filters.departmentId" size="small" clearable filterable popper-class="dark-popper"
-              placeholder="全部部门" @change="reload">
+              :popper-append-to-body="!isBoardFullscreen" placeholder="全部部门" @change="reload">
               <el-option v-for="item in departmentList" :key="item.value" :label="item.label" :value="item.value" />
               <el-option v-for="item in departmentList" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
             </el-select>
           </div>
           </div>
           <div class="filter-item contract-filter">
           <div class="filter-item contract-filter">
             <span><i class="el-icon-document"></i> 合同类型</span>
             <span><i class="el-icon-document"></i> 合同类型</span>
             <el-select v-model="filters.contractType" size="small" clearable popper-class="dark-popper"
             <el-select v-model="filters.contractType" size="small" clearable popper-class="dark-popper"
-              placeholder="全部类型" @change="reload">
+              :popper-append-to-body="!isBoardFullscreen" placeholder="全部类型" @change="reload">
               <el-option v-for="item in contractTypes" :key="item.value" :label="item.label" :value="item.value" />
               <el-option v-for="item in contractTypes" :key="item.value" :label="item.label" :value="item.value" />
             </el-select>
             </el-select>
           </div>
           </div>
           <el-button class="refresh-button" type="primary" size="small" :loading="loading" icon="el-icon-refresh"
           <el-button class="refresh-button" type="primary" size="small" :loading="loading" icon="el-icon-refresh"
             @click="reload">{{ loading ? "刷新中" : "刷新数据" }}</el-button>
             @click="reload">{{ loading ? "刷新中" : "刷新数据" }}</el-button>
         </div>
         </div>
+        <!-- 右上角全屏(2026-11-03 用户要求);全屏期间 Element 弹层挂回页面内部,选择器才能正常点开 -->
+        <button class="board-fullscreen-btn" type="button" :aria-pressed="isBoardFullscreen"
+          :title="isBoardFullscreen ? '退出全屏' : '全屏'" @click="toggleBoardFullscreen">
+          <i class="el-icon-full-screen"></i>{{ isBoardFullscreen ? "退出全屏" : "全屏" }}
+        </button>
       </div>
       </div>
     </header>
     </header>
 
 
@@ -217,8 +223,8 @@
       </div>
       </div>
     </section>
     </section>
 
 
-    <el-dialog :title="detailTitle" :visible.sync="detailVisible" width="90%" append-to-body
-      custom-class="personnel-detail-dialog" @open="loadDetail">
+    <el-dialog :title="detailTitle" :visible.sync="detailVisible" width="90%"
+      :append-to-body="!isBoardFullscreen" custom-class="personnel-detail-dialog" @open="loadDetail">
       <div class="detail-summary">
       <div class="detail-summary">
         <span><i class="el-icon-data-analysis"></i></span>
         <span><i class="el-icon-data-analysis"></i></span>
         <div>
         <div>
@@ -261,7 +267,7 @@
       <div class="detail-pager">
       <div class="detail-pager">
         <span class="detail-pager__total">共 {{ detailTotal }} 条</span>
         <span class="detail-pager__total">共 {{ detailTotal }} 条</span>
         <el-select v-model="detailSize" size="mini" class="detail-pager__size" popper-class="dark-popper"
         <el-select v-model="detailSize" size="mini" class="detail-pager__size" popper-class="dark-popper"
-          @change="reloadDetail">
+          :popper-append-to-body="!isBoardFullscreen" @change="reloadDetail">
           <el-option v-for="size in pageSizes" :key="size" :label="`${size} 条/页`" :value="size" />
           <el-option v-for="size in pageSizes" :key="size" :label="`${size} 条/页`" :value="size" />
         </el-select>
         </el-select>
         <el-button size="mini" :disabled="detailPage <= 1" @click="changeDetailPage(-1)">
         <el-button size="mini" :disabled="detailPage <= 1" @click="changeDetailPage(-1)">
@@ -282,6 +288,7 @@
 <script>
 <script>
 import * as echarts from "echarts";
 import * as echarts from "echarts";
 import BoardTitle from "@/components/board-title/index.vue";
 import BoardTitle from "@/components/board-title/index.vue";
+import boardFullscreen from "@/mixins/board-fullscreen";
 import { listOrganizations } from "@/api/organization";
 import { listOrganizations } from "@/api/organization";
 /* 机构选项取登录缓存(loginChangeGroupVOList);组织列表接口仍用于部门与名称映射 */
 /* 机构选项取登录缓存(loginChangeGroupVOList);组织列表接口仍用于部门与名称映射 */
 import { getOrgOptions, getCurrentOrgId } from "@/utils/current-org";
 import { getOrgOptions, getCurrentOrgId } from "@/utils/current-org";
@@ -452,6 +459,7 @@ function emptyTrend() {
 export default {
 export default {
   name: "PersonnelStructureDashboard",
   name: "PersonnelStructureDashboard",
   components: { BoardTitle },
   components: { BoardTitle },
+  mixins: [boardFullscreen],
   data() {
   data() {
     return {
     return {
       loading: false,
       loading: false,
@@ -1444,3 +1452,32 @@ export default {
 </script>
 </script>
 
 
 <style src="../../../styles/views/personnelStructureDashboard/index.scss" scoped lang="scss"></style>
 <style src="../../../styles/views/personnelStructureDashboard/index.scss" scoped lang="scss"></style>
+
+<!-- 右上角全屏按钮(scoped) -->
+<style scoped lang="scss">
+.board-fullscreen-btn {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  padding: 7px 14px;
+  font-size: 13px;
+  color: #bfe9ff;
+  cursor: pointer;
+  background: linear-gradient(135deg, rgba(0, 60, 100, 0.55), rgba(0, 25, 50, 0.75));
+  border: 1px solid rgba(0, 210, 255, 0.4);
+  box-shadow: inset 0 0 12px rgba(0, 180, 255, 0.18);
+  transition: all 0.2s;
+  white-space: nowrap;
+
+  i {
+    font-size: 14px;
+    color: #4fe3ff;
+  }
+
+  &:hover {
+    color: #fff;
+    border-color: rgba(0, 230, 255, 0.8);
+    box-shadow: 0 0 14px rgba(0, 200, 255, 0.4);
+  }
+}
+</style>

+ 3 - 2
vue.config.js

@@ -41,8 +41,9 @@ function stripProxyCacheHeaders(proxyRes) {
 
 
 const apiProxy = resolveApiProxyTarget(
 const apiProxy = resolveApiProxyTarget(
   process.env.VUE_APP_PROXY_TARGET ||
   process.env.VUE_APP_PROXY_TARGET ||
-    // "http://192.168.1.147:18086",
-    "http://192.168.1.113:18086",
+    // "http://192.168.1.10:18086",
+    "http://192.168.1.113:18086",   //郭丹
+    // "http://192.168.1.11:18086",    //谭zx
     // "http://aiot.zoomwin.com.cn:51001",
     // "http://aiot.zoomwin.com.cn:51001",
 );
 );