Преглед изворни кода

修复 ss-objp ss-cc ppopup 区域显示问题,调整 cobjlist 返回多requestParentViewObject参数,调整 ssonffbutton 类样式

apple пре 3 дана
родитељ
комит
28912da0bc
3 измењених фајлова са 277 додато и 146 уклоњено
  1. 244 121
      js/vue/ss-components.js
  2. 28 21
      page/env/cobjList.jsp
  3. 5 4
      skin/easy/css/base.css

+ 244 - 121
js/vue/ss-components.js

@@ -785,16 +785,45 @@ import { EVEN_VAR } from "./EventBus.js";
       },
     },
     emits: ["update:modelValue", "input", "blur", "change"],
-    setup(props, { emit }) {
-      const canInput = props.inp;
-      const errMsg = Vue.ref(props.errTip);
-      const selectItem = Vue.ref({});
-      let inputText = Vue.ref(""); // 用于存储输入框的文本
-      const popupWinVisible = Vue.ref(false);
-
-      const filteredOptions = Vue.ref(props.opt);
-      const popupDirection = Vue.ref("bottom");
-      const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
+	    setup(props, { emit }) {
+	      const canInput = props.inp;
+	      const errMsg = Vue.ref(props.errTip);
+	      const selectItem = Vue.ref({});
+	      let inputText = Vue.ref(""); // 用于存储输入框的文本
+	      const popupWinVisible = Vue.ref(false);
+
+	      const filteredOptions = Vue.ref(props.opt);
+	      const popupDirection = Vue.ref("bottom");
+	      const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
+	      const popupContentAreaMaxHeight = Vue.computed(() => {
+	        if (!popupMaxHeight.value || popupMaxHeight.value === "none") return null;
+	        const maxHeightNum = Number.parseFloat(popupMaxHeight.value);
+	        if (!Number.isFinite(maxHeightNum)) return null;
+	        // 功能说明:滚动条统一落在 .content-area(CSS 默认如此),因此需要扣掉 popup-win padding-top(10) 与 popup-content padding(15*2) by xu 20260126
+	        const contentAreaMaxHeight = Math.max(60, maxHeightNum - 40);
+	        return `${contentAreaMaxHeight}px`;
+	      });
+	      // 修复表格内下拉弹层被 overflow 截断:popup 使用 Teleport 到 body + fixed 定位 by xu 20260126
+	      const selectContainerRef = Vue.ref(null);
+	      const popupRef = Vue.ref(null);
+	      const teleportRootStyle = Vue.ref({
+	        position: "fixed",
+	        left: "0",
+	        top: "0",
+	        width: "0",
+	        height: "0",
+	        zIndex: 9999,
+	        pointerEvents: "none",
+	      });
+	      const popupLayerStyle = Vue.ref({
+	        position: "fixed",
+	        left: "0",
+	        top: "0",
+	        bottom: "auto", // 功能说明:覆盖 .popup-win.top 的 bottom 定位,避免 fixed 场景被撑高/错位 by xu 20260126
+	        minWidth: "0",
+	        zIndex: 9999,
+	        pointerEvents: "auto",
+	      });
 
       // const showRequired = Vue.computed(() => {
       //   const hasValidationRule = window.ssVm?.validations?.has(props.name);
@@ -963,13 +992,13 @@ import { EVEN_VAR } from "./EventBus.js";
         }
       }
 
-      //可录入的objPicker,更新下拉菜单选项
-      async function updateOptionBYInputText(inpTxt) {
-        try {
-          let objectPickerParam;
-          let url = props.url;
+	      //可录入的objPicker,更新下拉菜单选项
+	      async function updateOptionBYInputText(inpTxt) {
+	        try {
+	          let objectPickerParam;
+	          let url = props.url;
 
-          if (props.url && props.cb) {
+	          if (props.url && props.cb) {
             //如果有定义过滤器
             if (props.filter) {
 
@@ -1009,11 +1038,11 @@ import { EVEN_VAR } from "./EventBus.js";
                   "Content-Type": "application/x-www-form-urlencoded", // 必须手动设置!
                 },
               })
-              .then((response) => {
-                if ("timeout" == response.data.statusText) {
-                  alert("网络超时!");
-                  return;
-                }
+	              .then((response) => {
+	                if ("timeout" == response.data.statusText) {
+	                  alert("网络超时!");
+	                  return;
+	                }
 
                 // 先清空选项 by xu 20251212
                 if (props.opt) {
@@ -1075,33 +1104,29 @@ import { EVEN_VAR } from "./EventBus.js";
                   console.log("[objp] 接口返回无result");
                 }
 
-                // 无论是否有数据,都显示popup by xu 20251212
-                if (!popupWinVisible.value) {
-                  popupWinVisible.value = true;
-                }
-              });
-          }
-        } catch (error) {
-          // callback(null, error.message); // 失败回调,传递错误
-        }
-      }
+	                // 无论是否有数据,都显示popup by xu 20251212
+	                openPopup(); // Teleport 场景下统一打开并重定位 by xu 20260126
+	              });
+	          }
+	        } catch (error) {
+	          // callback(null, error.message); // 失败回调,传递错误
+	        }
+	      }
 
-      // 计算弹出方向和最大高度的方法 by xu 20251212
-      // 当空间不足时限制popup高度并显示滚动条
-      const calculatePopupDirection = () => {
-        // 1. 获取select容器元素
-        const selectEl = document
-          .querySelector(`[name="${props.name}"]`)
-          ?.closest(".select-container");
-        if (!selectEl) return;
+	      // 计算弹出方向和最大高度的方法 by xu 20251212
+	      // 当空间不足时限制popup高度并显示滚动条
+	      const calculatePopupDirection = () => {
+	        const triggerEl =
+	          selectContainerRef.value?.querySelector(".input") ||
+	          selectContainerRef.value;
+	        if (!triggerEl) return;
 
-        // 2. 获取位置信息
-        const selectRect = selectEl.getBoundingClientRect();
-        const viewportHeight = window.innerHeight;
+	        const selectRect = triggerEl.getBoundingClientRect();
+	        const viewportHeight = window.innerHeight;
 
-        // 3. 计算上下可用空间
-        const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
-        const spaceAbove = selectRect.top - 10; // 减10px留边距
+	        // 3. 计算上下可用空间
+	        const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
+	        const spaceAbove = selectRect.top - 10; // 减10px留边距
 
         // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
         const estimatedPopupHeight = 300;
@@ -1132,21 +1157,101 @@ import { EVEN_VAR } from "./EventBus.js";
             console.log('[popup] 向上展开,空间不足,限制高度:', popupMaxHeight.value);
           }
         }
-      };
+	      };
 
-      //点击下拉菜单的文本区域时,会触发的方法
-      function togglePopup() {
-        // 可录入的 objPicker,更新下拉菜单选项
-        updateOptionBYInputText(inputText.value);
-        // popupWinVisible.value = !popupWinVisible.value;
-        Vue.nextTick(() => {
-          calculatePopupDirection();
-        });
-      }
+	      // Teleport popup 的定位(fixed) by xu 20260126
+	      const updatePopupPosition = () => {
+	        const triggerEl =
+	          selectContainerRef.value?.querySelector(".input") ||
+	          selectContainerRef.value;
+	        if (!triggerEl) return;
+
+	        const triggerRect = triggerEl.getBoundingClientRect();
+	        const margin = 10;
+	        const viewportWidth = window.innerWidth;
+	        const viewportHeight = window.innerHeight;
+	        const popupGap = 0; // 功能说明:定位不再做 -10 重叠,让 padding-top 自然形成间距 by xu 20260126
+
+	        // 先给一个初始位置,确保下一帧可以测量弹层尺寸 by xu 20260126
+	        popupLayerStyle.value = {
+	          position: "fixed",
+	          left: `${Math.max(margin, triggerRect.left)}px`,
+	          top: `${Math.max(margin, triggerRect.bottom + popupGap)}px`, // 功能说明:与输入框底部对齐 by xu 20260126
+	          bottom: "auto", // 功能说明:fixed 场景显式取消 bottom,避免与 top 同时生效 by xu 20260126
+	          minWidth: `${Math.max(0, triggerRect.width)}px`,
+	          zIndex: 9999,
+	          pointerEvents: "auto",
+	        };
+
+	        Vue.nextTick(() => {
+	          const popupEl = popupRef.value;
+	          if (!popupEl) return;
+
+	          const popupRect = popupEl.getBoundingClientRect();
+	          const maxLeft = viewportWidth - popupRect.width - margin;
+	          const left = Math.min(
+	            Math.max(margin, triggerRect.left),
+	            Math.max(margin, maxLeft)
+	          );
+
+	          let top;
+	          if (popupDirection.value === "top") {
+	            top = triggerRect.top - popupRect.height - popupGap; // 功能说明:向上展开时与输入框顶部对齐 by xu 20260126
+	            top = Math.max(margin, top);
+	          } else {
+	            top = triggerRect.bottom + popupGap; // 功能说明:向下展开时与输入框底部对齐 by xu 20260126
+	            if (top + popupRect.height > viewportHeight - margin) {
+	              top = Math.max(margin, viewportHeight - popupRect.height - margin);
+	            }
+	          }
+
+	          popupLayerStyle.value = {
+	            ...popupLayerStyle.value,
+	            left: `${left}px`,
+	            top: `${top}px`,
+	            bottom: "auto", // 功能说明:无论 top/bottom 展开都用 top 定位,禁用 bottom by xu 20260126
+	          };
+	        });
+	      };
 
-      const hidePopup = () => {
-        popupWinVisible.value = false;
-      };
+	      const openPopup = () => {
+	        if (!popupWinVisible.value) popupWinVisible.value = true;
+	        Vue.nextTick(() => {
+	          calculatePopupDirection();
+	          updatePopupPosition();
+	        });
+	      };
+
+	      // Teleport 场景下:滚动/缩放重定位 by xu 20260126
+	      const handleViewportChange = () => {
+	        if (!popupWinVisible.value) return;
+	        calculatePopupDirection();
+	        updatePopupPosition();
+	      };
+
+	      // Teleport 场景下:点击外部关闭 by xu 20260126
+	      const onDocPointerDown = (event) => {
+	        if (!popupWinVisible.value) return;
+	        const target = event.target;
+	        if (selectContainerRef.value?.contains(target)) return;
+	        if (popupRef.value?.contains(target)) return;
+	        hidePopup();
+	      };
+
+	      //点击下拉菜单的文本区域时,会触发的方法
+	      function togglePopup() {
+	        // 可录入的 objPicker,更新下拉菜单选项
+	        updateOptionBYInputText(inputText.value);
+	        // popupWinVisible.value = !popupWinVisible.value;
+	        Vue.nextTick(() => {
+	          calculatePopupDirection();
+	          updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
+	        });
+	      }
+
+	      const hidePopup = () => {
+	        popupWinVisible.value = false;
+	      };
 
       //点击下拉菜单的三角形时,会触发的方法
       // 添加toggle逻辑,点击时切换显示/隐藏 by xu 20251212
@@ -1158,13 +1263,14 @@ import { EVEN_VAR } from "./EventBus.js";
           return;
         }
 
-        //可录入的objPicker,更新下拉菜单选项
-        updateOptionBYInputText("");
-        Vue.nextTick(() => {
-          calculatePopupDirection();
-        });
-        console.log("[objp] 点三角打开popup");
-      };
+	        //可录入的objPicker,更新下拉菜单选项
+	        updateOptionBYInputText("");
+	        Vue.nextTick(() => {
+	          calculatePopupDirection();
+	          updatePopupPosition(); // Teleport 场景下同步定位 by xu 20260126
+	        });
+	        console.log("[objp] 点三角打开popup");
+	      };
 
       //可录入的objPicker,录入项变化时,会触发
       async function handleInputChange(event) {
@@ -1183,38 +1289,50 @@ import { EVEN_VAR } from "./EventBus.js";
         //   popupWinVisible.value = true; // 确保下拉框在输入时打开
         // }
       }
-      Vue.onMounted(() => {
-        initDefaultValue();
-        window.addEventListener("resize", calculatePopupDirection);
-      });
-      Vue.onUnmounted(() => {
-        window.removeEventListener("resize", calculatePopupDirection);
-      });
+	      Vue.onMounted(() => {
+	        initDefaultValue();
+	        // Teleport 场景下:滚动/缩放需要重定位(scroll 用 capture 捕获容器滚动) by xu 20260126
+	        window.addEventListener("resize", handleViewportChange);
+	        window.addEventListener("scroll", handleViewportChange, true);
+
+	        // 点击外部关闭(原本依赖 mouseleave,Teleport 后会误关) by xu 20260126
+	        document.addEventListener("pointerdown", onDocPointerDown, true);
+	      });
+	      Vue.onUnmounted(() => {
+	        window.removeEventListener("resize", handleViewportChange);
+	        window.removeEventListener("scroll", handleViewportChange, true);
+	        document.removeEventListener("pointerdown", onDocPointerDown, true);
+	      });
 
-      return {
-        errMsg,
-        selectItem,
-        inputText,
-        canInput,
-        filteredOptions,
-        popupWinVisible,
-        popupDirection,
-        popupMaxHeight, // 添加popup最大高度 by xu 20251212
-        suffixClick,
-        togglePopup,
-        hidePopup,
-        doSelectItem,
-        handleInputChange,
-      };
-    },
+	      return {
+	        errMsg,
+	        selectItem,
+	        inputText,
+	        canInput,
+	        filteredOptions,
+	        popupWinVisible,
+	        popupDirection,
+	        popupMaxHeight, // 添加popup最大高度 by xu 20251212
+	        popupContentAreaMaxHeight,
+	        selectContainerRef,
+	        popupRef,
+	        teleportRootStyle,
+	        popupLayerStyle,
+	        suffixClick,
+	        togglePopup,
+	        hidePopup,
+	        doSelectItem,
+	        handleInputChange,
+	      };
+	    },
 
-    template: `
-      <div class="input" style="position: relative" :style="{width: width}">
-        <div class="select-container" @mouseleave="hidePopup">
-          <div class="input" @click="togglePopup">
-            <input 
-              type="hidden"
-              :name="name"
+	    template: `
+	      <div class="input" style="position: relative" :style="{width: width}">
+	        <div class="select-container" ref="selectContainerRef">
+	          <div class="input" @click="togglePopup">
+	            <input 
+	              type="hidden"
+	              :name="name"
               :value="selectItem.value"
               .value="selectItem.value" 
             />
@@ -1236,32 +1354,37 @@ import { EVEN_VAR } from "./EventBus.js";
             />
            
             <div class="suffix" @click.stop="suffixClick">
-              <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
-              
-            </div>
-          </div>
-         
-            
-          <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
-          <div v-show="popupWinVisible" class="popup-win" :class="popupDirection" :style="{ maxHeight: popupMaxHeight, overflowY: popupMaxHeight !== 'none' ? 'auto' : 'visible' }">
-            <div v-if="opt && opt.length && filteredOptions.length > 0" class="popup-content">
-              <div class="content-area">
-                <div v-for="(item, index) in filteredOptions" :key="index" @click="doSelectItem(item)" :class="{ active: item.value === selectItem.value }">
-                  <span class="check-icon">
-                   
-                    <ss-form-icon class="form-icon-select-checked" />
-                  </span>
-                  <span>{{ item.label }}</span>
-                </div>
-              </div>
-            </div>
-            <div v-else class="popup-content"><div class="content-area"><div class="content-area"> <span>无选项</span></div></div></div>
-          </div>
-        </div>
-      </div>
-
-    `,
-  };
+	              <ss-form-icon :class="popupWinVisible ? 'form-icon-transform-select select' : 'form-icon-select'" />
+	              
+	            </div>
+	          </div>
+	         
+	            
+	          <!-- 修复表格内弹层被截断:popup Teleport 到 body by xu 20260126 -->
+	          <teleport to="body">
+	            <div v-show="popupWinVisible" class="select-container ss-objp-teleport-root" :style="teleportRootStyle">
+	              <!-- popup弹出层,添加maxHeight和overflowY支持空间不足时滚动 by xu 20251212 -->
+	              <div ref="popupRef" class="popup-win" :class="popupDirection" :style="[popupLayerStyle, { maxHeight: popupMaxHeight !== 'none' ? popupMaxHeight : 'none', overflowY: 'visible' }]">
+	                <div v-if="opt && opt.length && filteredOptions.length > 0" class="popup-content">
+	                  <div class="content-area" :style="popupContentAreaMaxHeight ? { maxHeight: popupContentAreaMaxHeight, overflowY: 'auto' } : null">
+	                    <div v-for="(item, index) in filteredOptions" :key="index" @click="doSelectItem(item)" :class="{ active: item.value === selectItem.value }">
+	                      <span class="check-icon">
+	                       
+	                        <ss-form-icon class="form-icon-select-checked" />
+	                      </span>
+	                      <span>{{ item.label }}</span>
+	                    </div>
+	                  </div>
+	                </div>
+	                <div v-else class="popup-content"><div class="content-area"><div class="content-area"> <span>无选项</span></div></div></div>
+	              </div>
+	            </div>
+	          </teleport>
+	        </div>
+	      </div>
+
+	    `,
+	  };
   // ss-hidden 隐藏字段组件
   const SsHidden = {
     name: "SsHidden",

+ 28 - 21
page/env/cobjList.jsp

@@ -1205,6 +1205,7 @@
         btnElemConfig: window.SS.dom.btnElemConfig,
         // 功能说明:二级对象查询服务名(首次/后续统一一个),用于初始化时调接口打印返回 by xu 20260115
         ssSearchCobjServName: "${ssSearchCobjServName}",
+        requestParentViewObject:"${requestParentViewObject}",
         // 功能说明:从 JSP 注入当前对象信息,供二级对象初始化接口拼参使用(key=ssObjName+'id') by xu 20260115
         ssObjName: "${ssObjName}",
         ssObjId: "${ssObjId}",
@@ -1347,27 +1348,33 @@
                         });
                     },
                     // 功能说明:二级对象页面初始化时调用 ssSearchCobjServName 并打印返回(仅联调用) by xu 20260115
-                    loadCobjInit() {
-                        var self = this;
-                        var ssServ = this.ssSearchCobjServName;
-                        if (!ssServ) return;
-                        // 功能说明:进入 loading,并递增请求序号(只处理最后一次返回) by xu 20260115
-                        var reqId = (this.cobjReqId = Number(this.cobjReqId || 0) + 1);
-                        var startedAt = Date.now();
-                        this.loadingCobj = true;
-                        var params = this.getSearchFormParams();
-                        // 功能说明:二级对象查询额外携带“当前对象id”(key=ssObjName+'id',value=ssObjId),由 JSP 注入 by xu 20260115
-                        try {
-                            var objName = String(this.ssObjName || "").trim();
-                            var objId = String(this.ssObjId || "").trim();
-                            if (objName && objId) params[objName + "id"] = objId;
-                        } catch (e) {
-                        }
-                        // 功能说明:打印最终请求参数,避免不清楚拼接了什么 by xu 20260115
-                        try {
-                            console.log("[cobj] init params", params);
-                            console.log("[cobj] init qs", $.param(Object.assign({ssServ: ssServ}, params)));
-                        } catch (e) {
+	                    loadCobjInit() {
+	                        var self = this;
+	                        var ssServ = this.ssSearchCobjServName;
+	                        if (!ssServ) return;
+	                        // 功能说明:进入 loading,并递增请求序号(只处理最后一次返回) by xu 20260115
+	                        var reqId = (this.cobjReqId = Number(this.cobjReqId || 0) + 1);
+	                        var startedAt = Date.now();
+	                        this.loadingCobj = true;
+	                        var params = this.getSearchFormParams();
+	                        // 功能说明:二级对象查询额外携带“当前对象id”(key=ssObjName+'id',value=ssObjId),由 JSP 注入 by xu 20260115
+	                        try {
+	                            var objName = String(this.ssObjName || "").trim();
+	                            var objId = String(this.ssObjId || "").trim();
+	                            if (objName && objId) params[objName + "id"] = objId;
+	                        } catch (e) {
+	                        }
+	                        // 功能说明:二级对象查询额外携带 requestParentViewObject(由 JSP 注入),用于后端识别父视图对象请求场景 by xu 20260126
+	                        try {
+	                            var rpv = String(this.requestParentViewObject || "").trim();
+	                            if (rpv) params.requestParentViewObject = rpv;
+	                        } catch (e) {
+	                        }
+	                        // 功能说明:打印最终请求参数,避免不清楚拼接了什么 by xu 20260115
+	                        try {
+	                            console.log("[cobj] init params", params);
+	                            console.log("[cobj] init qs", $.param(Object.assign({ssServ: ssServ}, params)));
+	                        } catch (e) {
                         }
                         this.callSsService(ssServ, params)
                             .then(function (res) {

+ 5 - 4
skin/easy/css/base.css

@@ -15,7 +15,7 @@
 }
 .id-photo::before {
   content: "\e605";
-  font-size: 100px;
+  font-size: 40px;
   font-family: "iconfont";
   color: #ccc;
   position: absolute;
@@ -43,10 +43,11 @@
   cursor: pointer;
   position: relative;
   float: left;
+  margin-top: 5px !important;
 }
 .life-photo::before {
   content: "\e605";
-  font-size: 80px;
+  font-size: 40px;
   font-family: "iconfont";
   color: #ccc;
   position: absolute;
@@ -586,10 +587,10 @@
   content: "\e68d";
 }
 
-.form-icon-onoffbutton-checked::before{
+.form-icon-onoff-checked::before{
   content: "\e62d";
 }
-.form-icon-onoffbutton-unchecked::before{
+.form-icon-onoff-unchecked::before{
   content: "\e62b";
 }
 .form-icon-time::before{