|
|
@@ -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",
|