Ben 6 өдөр өмнө
parent
commit
b37683cbbb

+ 213 - 61
js/vue/ss-components.js

@@ -178,6 +178,11 @@ import { EVEN_VAR } from "./EventBus.js";
         type: String,
         default: "",
       },
+      // 新增:是否允许回车换行,默认false禁止换行 by xu 20251212
+      multiline: {
+        type: Boolean,
+        default: false,
+      },
     },
     emits: ["update:modelValue", "input", "blur", "change"], // 允许更新 v-model 绑定的值
     setup(props, { emit }) {
@@ -294,17 +299,33 @@ import { EVEN_VAR } from "./EventBus.js";
       };
 
       // 检查是否应该显示浮动窗口(需要同时满足:有焦点 + 内容超出)
+      // 修复新增页面点击就出现floatdiv的问题 by xu 20251212
       const checkShouldShowFloatingDiv = () => {
         const textarea = inputRef.value;
         if (!textarea) return false;
 
-        // 判断内容是否超出:scrollHeight > clientHeight
-        // scrollHeight 是内容的完整高度(包括不可见部分)
-        // clientHeight 是可视区域高度(不包括滚动的部分)
-        const isOverflow = textarea.scrollHeight > textarea.clientHeight;
+        // 首先检查是否有内容,没有内容时不显示floatdiv by xu 20251212
+        if (!inputValue.value || inputValue.value.toString().trim() === '') {
+          console.log('[floatdiv] 内容为空,不显示floatdiv');
+          return false;
+        }
+
+        // 判断内容是否超出 by xu 20251212
+        // 同时检查横向和纵向溢出,任一方向溢出都应显示floatdiv
+        // 纵向溢出需要加容差值,避免padding/border导致的误判 by xu 20251212
+        const verticalTolerance = 5; // 容差值5px
+        const isHorizontalOverflow = textarea.scrollWidth > textarea.clientWidth;
+        const isVerticalOverflow = textarea.scrollHeight > textarea.clientHeight + verticalTolerance;
+        const isOverflow = isHorizontalOverflow || isVerticalOverflow;
+
+        console.log('[floatdiv] 溢出检测 - scrollWidth:', textarea.scrollWidth, 'clientWidth:', textarea.clientWidth, 'horizontalOverflow:', isHorizontalOverflow);
+        console.log('[floatdiv] 溢出检测 - scrollHeight:', textarea.scrollHeight, 'clientHeight:', textarea.clientHeight, 'tolerance:', verticalTolerance, 'verticalOverflow:', isVerticalOverflow);
+
+        const shouldShow = isFocused.value && isOverflow;
+        console.log('[floatdiv] 最终判断 - isFocused:', isFocused.value, 'isOverflow:', isOverflow, 'shouldShow:', shouldShow);
 
         // 需要同时满足:有焦点 + 内容超出
-        return isFocused.value && isOverflow;
+        return shouldShow;
       };
 
       // 定义事件处理函数
@@ -365,6 +386,14 @@ import { EVEN_VAR } from "./EventBus.js";
         // contentFloatingDiv.value = false
       };
 
+      // 处理键盘按下事件,禁止回车换行 by xu 20251212
+      const onKeydown = (event) => {
+        // 如果不允许多行且按下的是回车键,阻止默认行为
+        if (!props.multiline && event.key === 'Enter') {
+          event.preventDefault();
+        }
+      };
+
       // 附件按钮点击处理(从 SsEditor 搬运)
       const onAttachmentClick = (e) => {
         e.preventDefault();
@@ -428,6 +457,7 @@ import { EVEN_VAR } from "./EventBus.js";
         onChange,
         onMouseover,
         onMouseleave,
+        onKeydown, // 新增:键盘事件处理 by xu 20251212
         contentFloatingDiv,
         floatingDivPosition,
         getFloatingDivTop,
@@ -478,6 +508,7 @@ import { EVEN_VAR } from "./EventBus.js";
               onFocus: this.onFocus,
               onBlur: this.onBlur,
               onChange: this.onChange,
+              onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
               placeholder: this.placeholder,
               onMouseover: this.onMouseover, // 监听鼠标悬停
               onMouseleave: this.onMouseleave, // 监听鼠标离开
@@ -532,6 +563,7 @@ import { EVEN_VAR } from "./EventBus.js";
                   onInput: this.onInput,
                   onBlur: this.onBlur,
                   onFocus: this.onFocus,
+                  onKeydown: this.onKeydown, // 新增:禁止回车换行 by xu 20251212
                   onMouseover: this.onMouseover, // 监听鼠标悬停
                   onMouseleave: this.onMouseleave, // 监听鼠标离开
                   autocomplete: "off",
@@ -758,6 +790,7 @@ import { EVEN_VAR } from "./EventBus.js";
 
       const filteredOptions = Vue.ref(props.opt);
       const popupDirection = Vue.ref("bottom");
+      const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
 
       // const showRequired = Vue.computed(() => {
       //   const hasValidationRule = window.ssVm?.validations?.has(props.name);
@@ -958,15 +991,17 @@ import { EVEN_VAR } from "./EventBus.js";
                     return;
                   }
 
+                  // 先清空选项 by xu 20251212
+                  if (props.opt) {
+                    props.opt.length = 0;
+                  } else {
+                    props.opt = [];
+                  }
+
                   if (response.data.result) {
                     const keys = Object.keys(response.data.result);
                     // console.log("params:"+params+"@@response.data:"+JSON.stringify(response.data));
                     if (keys.length > 0) {
-                      if (props.opt)
-                        props.opt.length = 0; //通过修改数组的length属性,直接清空数组元素,内存会被自动释放。这是性能最优的方式
-                      else {
-                        props.opt = [];
-                      }
                       for (let k in response.data.result) {
                         props.opt.push({
                           label: response.data.result[k],
@@ -987,6 +1022,15 @@ import { EVEN_VAR } from "./EventBus.js";
                                 .toLowerCase()
                                 .includes(inputText.value.toLowerCase())
                         );
+
+                        // 可录入的objPicker,当搜索结果只有一项时,自动选中这一项 by xu 20251212
+                        if (filteredOptions.value.length === 1) {
+                          const autoSelectItem = filteredOptions.value[0];
+                          console.log("[objp] 搜索结果只有一项,自动选中:", autoSelectItem);
+                          doSelectItem(autoSelectItem);
+                          return; // 自动选中后直接返回,不需要显示popup
+                        }
+
                         filteredOptions.value.unshift({ label: "", value: "" });
 
                         // console.log('###做了过滤:'+inputText.value.toLowerCase()+';');
@@ -995,12 +1039,21 @@ import { EVEN_VAR } from "./EventBus.js";
                         filteredOptions.value.unshift({ label: "", value: "" });
                       }
 
-                      if (!popupWinVisible.value) {
-                        popupWinVisible.value = true; // 确保下拉框在输入时打开
-                      }
-
                       console.log("props.opt11:" + JSON.stringify(props.opt));
+                    } else {
+                      // 没有数据时,清空过滤选项 by xu 20251212
+                      filteredOptions.value = [];
+                      console.log("[objp] 接口返回空数据");
                     }
+                  } else {
+                    // result不存在时,清空过滤选项 by xu 20251212
+                    filteredOptions.value = [];
+                    console.log("[objp] 接口返回无result");
+                  }
+
+                  // 无论是否有数据,都显示popup by xu 20251212
+                  if (!popupWinVisible.value) {
+                    popupWinVisible.value = true;
                   }
                 });
           }
@@ -1009,7 +1062,8 @@ import { EVEN_VAR } from "./EventBus.js";
         }
       }
 
-      // 计算弹出方向的方法
+      // 计算弹出方向和最大高度的方法 by xu 20251212
+      // 当空间不足时限制popup高度并显示滚动条
       const calculatePopupDirection = () => {
         // 1. 获取select容器元素
         const selectEl = document
@@ -1021,20 +1075,39 @@ import { EVEN_VAR } from "./EventBus.js";
         const selectRect = selectEl.getBoundingClientRect();
         const viewportHeight = window.innerHeight;
 
-        // 3. 简单判断:如果下方剩余空间小于300px就向上弹出
-        const spaceBelow = viewportHeight - selectRect.bottom;
-
-        // 调试信息
-        // console.log('空间判断:', {
-        //   elementBottom: selectRect.bottom,
-        //   viewportHeight,
-        //   spaceBelow,
-        //   willPopUp: spaceBelow < 300,
-        //   popupDirection: popupDirection.value
-        // });
-
-        // 4. 设置方向
-        popupDirection.value = spaceBelow < 300 ? "top" : "bottom";
+        // 3. 计算上下可用空间
+        const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
+        const spaceAbove = selectRect.top - 10; // 减10px留边距
+
+        // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
+        const estimatedPopupHeight = 300;
+        const minPopupHeight = 100; // 最小高度
+
+        console.log('[popup] 空间计算 - spaceAbove:', spaceAbove, 'spaceBelow:', spaceBelow, 'estimatedHeight:', estimatedPopupHeight);
+
+        // 5. 判断方向和最大高度 by xu 20251212
+        if (spaceBelow >= estimatedPopupHeight) {
+          // 下方空间足够,向下展开,不限制高度
+          popupDirection.value = "bottom";
+          popupMaxHeight.value = "none";
+          console.log('[popup] 向下展开,空间充足');
+        } else if (spaceAbove >= estimatedPopupHeight) {
+          // 上方空间足够,向上展开,不限制高度
+          popupDirection.value = "top";
+          popupMaxHeight.value = "none";
+          console.log('[popup] 向上展开,空间充足');
+        } else {
+          // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
+          if (spaceBelow >= spaceAbove) {
+            popupDirection.value = "bottom";
+            popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
+            console.log('[popup] 向下展开,空间不足,限制高度:', popupMaxHeight.value);
+          } else {
+            popupDirection.value = "top";
+            popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
+            console.log('[popup] 向上展开,空间不足,限制高度:', popupMaxHeight.value);
+          }
+        }
       };
 
       //点击下拉菜单的文本区域时,会触发的方法
@@ -1052,14 +1125,21 @@ import { EVEN_VAR } from "./EventBus.js";
       };
 
       //点击下拉菜单的三角形时,会触发的方法
+      // 添加toggle逻辑,点击时切换显示/隐藏 by xu 20251212
       const suffixClick = () => {
+        // 如果popup已显示,则关闭 by xu 20251212
+        if (popupWinVisible.value) {
+          hidePopup();
+          console.log("[objp] 点三角关闭popup");
+          return;
+        }
+
         //可录入的objPicker,更新下拉菜单选项
         updateOptionBYInputText("");
-        // popupWinVisible.value = !popupWinVisible.value;
         Vue.nextTick(() => {
           calculatePopupDirection();
         });
-        console.log("点三角");
+        console.log("[objp] 点三角打开popup");
       };
 
       //可录入的objPicker,录入项变化时,会触发
@@ -1095,6 +1175,7 @@ import { EVEN_VAR } from "./EventBus.js";
         filteredOptions,
         popupWinVisible,
         popupDirection,
+        popupMaxHeight, // 添加popup最大高度 by xu 20251212
         suffixClick,
         togglePopup,
         hidePopup,
@@ -1137,7 +1218,8 @@ import { EVEN_VAR } from "./EventBus.js";
           </div>
          
             
-          <div v-show="popupWinVisible" class="popup-win" :class="popupDirection">
+          <!-- 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 }">
@@ -1369,6 +1451,7 @@ import { EVEN_VAR } from "./EventBus.js";
       const isAutoEcho = Vue.ref(false); // 用于标记是否是自动回显
       const upperValue = Vue.ref(""); //上级下拉菜单当前值,在初始化下拉菜单默认值时,和上级下拉菜单的值变化时,修改此upperValue变量
       const popupDirection = Vue.ref("bottom");
+      const popupMaxHeight = Vue.ref("none"); // popup最大高度,用于空间不足时限制高度并出滚动条 by xu 20251212
 
       //有隐藏字段的下拉菜单,加载菜单项并展开事件
       // 被上级下拉菜单选中值后,触发本下拉菜单刷新菜单项并弹出显示
@@ -1853,6 +1936,8 @@ import { EVEN_VAR } from "./EventBus.js";
         }
       };
 
+      // 计算弹出方向和最大高度的方法 by xu 20251212
+      // 当空间不足时限制popup高度并显示滚动条
       const calculatePopupDirection = () => {
         // 1. 获取select容器元素
         const selectEl = document.querySelector(
@@ -1865,20 +1950,39 @@ import { EVEN_VAR } from "./EventBus.js";
         const selectRect = selectEl.getBoundingClientRect();
         const viewportHeight = window.innerHeight;
 
-        // 3. 简单判断:如果下方剩余空间小于300px就向上弹出
-        const spaceBelow = viewportHeight - selectRect.bottom;
-
-        // 调试信息
-        console.log("空间判断:", {
-          elementBottom: selectRect.bottom,
-          viewportHeight,
-          spaceBelow,
-          willPopUp: spaceBelow < 300,
-          popupDirection: popupDirection.value,
-        });
-
-        // 4. 设置方向
-        popupDirection.value = spaceBelow < 300 ? "top" : "bottom";
+        // 3. 计算上下可用空间 by xu 20251212
+        const spaceBelow = viewportHeight - selectRect.bottom - 10; // 减10px留边距
+        const spaceAbove = selectRect.top - 10; // 减10px留边距
+
+        // 4. popup预估高度(假设每项36px,最多显示8项 + padding)
+        const estimatedPopupHeight = 300;
+        const minPopupHeight = 100; // 最小高度
+
+        console.log('[popup] 空间计算 - spaceAbove:', spaceAbove, 'spaceBelow:', spaceBelow, 'estimatedHeight:', estimatedPopupHeight);
+
+        // 5. 判断方向和最大高度 by xu 20251212
+        if (spaceBelow >= estimatedPopupHeight) {
+          // 下方空间足够,向下展开,不限制高度
+          popupDirection.value = "bottom";
+          popupMaxHeight.value = "none";
+          console.log('[popup] 向下展开,空间充足');
+        } else if (spaceAbove >= estimatedPopupHeight) {
+          // 上方空间足够,向上展开,不限制高度
+          popupDirection.value = "top";
+          popupMaxHeight.value = "none";
+          console.log('[popup] 向上展开,空间充足');
+        } else {
+          // 上下空间都不足,选择空间大的方向,并限制高度出滚动条
+          if (spaceBelow >= spaceAbove) {
+            popupDirection.value = "bottom";
+            popupMaxHeight.value = Math.max(spaceBelow, minPopupHeight) + "px";
+            console.log('[popup] 向下展开,空间不足,限制高度:', popupMaxHeight.value);
+          } else {
+            popupDirection.value = "top";
+            popupMaxHeight.value = Math.max(spaceAbove, minPopupHeight) + "px";
+            console.log('[popup] 向上展开,空间不足,限制高度:', popupMaxHeight.value);
+          }
+        }
       };
       //级联菜单点击事件
       const togglePopup = () => {
@@ -1980,12 +2084,19 @@ import { EVEN_VAR } from "./EventBus.js";
                             });
                           }
 
-                          if (!popupWinVisible.value) {
-                            popupWinVisible.value = true; // 确保下拉框打开
-                          }
-
                           console.log("props.opt11:" + JSON.stringify(props.opt));
+                        } else {
+                          // 没有数据时打印日志 by xu 20251212
+                          console.log("[ccp mode1] 接口返回空数据");
                         }
+                      } else {
+                        // result不存在时打印日志 by xu 20251212
+                        console.log("[ccp mode1] 接口返回无result");
+                      }
+
+                      // 无论是否有数据,都显示popup by xu 20251212
+                      if (!popupWinVisible.value) {
+                        popupWinVisible.value = true;
                       }
                     });
               }
@@ -2070,12 +2181,19 @@ import { EVEN_VAR } from "./EventBus.js";
                             });
                           }
 
-                          if (!popupWinVisible.value) {
-                            popupWinVisible.value = true; // 确保下拉框打开
-                          }
-
                           console.log("props.opt11:" + JSON.stringify(props.opt));
+                        } else {
+                          // 没有数据时打印日志 by xu 20251212
+                          console.log("[ccp mode2] 接口返回空数据");
                         }
+                      } else {
+                        // result不存在时打印日志 by xu 20251212
+                        console.log("[ccp mode2] 接口返回无result");
+                      }
+
+                      // 无论是否有数据,都显示popup by xu 20251212
+                      if (!popupWinVisible.value) {
+                        popupWinVisible.value = true;
                       }
                     });
               }
@@ -2172,6 +2290,7 @@ import { EVEN_VAR } from "./EventBus.js";
         selectItem,
         popupWinVisible,
         popupDirection,
+        popupMaxHeight, // 添加popup最大高度 by xu 20251212
         togglePopup,
         hidePopup,
         doSelectItem,
@@ -2199,7 +2318,8 @@ import { EVEN_VAR } from "./EventBus.js";
             </div>
           </div>
 
-          <div v-show="popupWinVisible" class="popup-win " :class="popupDirection">
+          <!-- 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 > 0" class="popup-content">
               <div class="content-area">
                 <div 
@@ -2363,10 +2483,16 @@ import { EVEN_VAR } from "./EventBus.js";
     },
   };
   // ss-icon 图标
+  // v3.0 增加 class 属性分支:有 class 走新逻辑,否则走 v1.0 逻辑 by xu 20251212
+  // v3.0 用法: <ss-icon class="icon-obj-ry menu-icon" />
+  // v1.0 用法: <ss-icon name="setting" size="20px" />
   const SsIcon = {
     name: "SsIcon",
     props: {
-      name: { type: String, required: true },
+      // v3.0: class 属性(图标类 + 组类)
+      class: { type: String },
+      // v1.0: 以下为旧属性
+      name: { type: String },
       size: { type: [Number, String], default: 16 },
       unit: { type: String, default: "px" },
       color: String,
@@ -2380,6 +2506,12 @@ import { EVEN_VAR } from "./EventBus.js";
     },
     emits: ["update:modelValue", "input", "blur", "change"],
     setup(props, { emit }) {
+      // v3.0 分支:有 class 属性时直接渲染 by xu 20251212
+      if (props.class) {
+        return () => h("i", { class: props.class });
+      }
+
+      // v1.0 分支:原有逻辑
       const useIconType = computed(() => {
         return [ssIcon, commonIcon].find(
             (iconConfig) => iconConfig.name === props.type
@@ -2505,6 +2637,7 @@ import { EVEN_VAR } from "./EventBus.js";
           });
     },
   };
+
   // 全局菜单图标组件
   const SsGolbalMenuIcon = {
     name: "SsGolbalMenuIcon",
@@ -2634,6 +2767,11 @@ import { EVEN_VAR } from "./EventBus.js";
         type: Boolean,
         default: false,
       },
+      // 是否允许一项都不选,默认true允许 by xu 20251212
+      null: {
+        type: Boolean,
+        default: true,
+      },
     },
     emits: ["update:modelValue"], // 允许更新 v-model 绑定的值
     setup(props, { emit }) {
@@ -2667,11 +2805,25 @@ import { EVEN_VAR } from "./EventBus.js";
           if (index === -1) {
             checkedValue.value = [...checkedValue.value, value];
           } else {
-            checkedValue.value = checkedValue.value.filter((v) => v !== value);
+            // 取消选中当前项
+            const newValue = checkedValue.value.filter((v) => v !== value);
+            // 如果不允许为空且取消后为空,则阻止取消操作 by xu 20251212
+            if (!props.null && newValue.length === 0) {
+              return; // 阻止取消最后一项
+            }
+            checkedValue.value = newValue;
           }
         } else {
           // 单选模式
-          checkedValue.value = value;
+          // 如果点击的是当前已选中的项,判断是否允许取消 by xu 20251212
+          if (checkedValue.value === value) {
+            if (!props.null) {
+              return; // 不允许为空时,阻止取消
+            }
+            checkedValue.value = ""; // 允许为空时,取消选中
+          } else {
+            checkedValue.value = value;
+          }
         }
 
         emit("update:modelValue", checkedValue.value);
@@ -3634,13 +3786,13 @@ import { EVEN_VAR } from "./EventBus.js";
     },
   };
   // ss-bottom-button 底部按钮 
-
+  // 修改支持更多按钮 by xu 20251211
   const SsBottomButton = {
     name: "SsBottomButton",
     props: {
       text: {
         type: String,
-        required: false,// 修改支持更多按钮 by xu 20251211
+        required: false,
       },
       type: {
         type: String,
@@ -3757,7 +3909,7 @@ import { EVEN_VAR } from "./EventBus.js";
                           class: props.iconClass,
                         }),
                       ]),
-                      h("span", null, buttonText.value),// 修改支持更多按钮 by xu 20251211
+                      h("span", null, buttonText.value),
                     ]
                 ),
                 // 渲染下拉菜单

+ 116 - 60
js/vue/ss-index-components.js

@@ -229,14 +229,22 @@ export const GlobalMenu = {
          Vue.watchEffect(() => {
             if (props.menuItems && props.menuItems.length) {
                 menuItemsNew.value = convertMenuDataToTree(props.menuItems);
-                activeItem.value = menuItemsNew.value[0]?.name || '';
-                console.log('Menu items updated:', menuItemsNew.value);
-                if(menuItemsNew.value[0]?.component){
-                    eventBus.publish(EVEN_VAR.currentPage, menuItemsNew.value[0]?.component);
-                }else{
-                    eventBus.publish(EVEN_VAR.currentPage, menuItemsNew.value[0]?.children[0]?.component);
+
+                // 初始化 activeItem
+                const firstItem = menuItemsNew.value[0];
+                if(firstItem?.component){
+                    // 首页有URL,直接设置为首页
+                    activeItem.value = firstItem.name;
+                    eventBus.publish(EVEN_VAR.currentPage, firstItem.component);
+                } else if(firstItem?.children && firstItem.children.length > 0){
+                    // 首页没有URL,但有子菜单,设置为第一个子菜单
+                    activeItem.value = firstItem.children[0].name;
+                    eventBus.publish(EVEN_VAR.currentPage, firstItem.children[0].component);
+                } else {
+                    activeItem.value = '';
                 }
 
+                console.log('Menu items updated:', menuItemsNew.value);
             }
         });
         const SsIcon = Vue.resolveComponent('ss-icon');
@@ -252,6 +260,45 @@ export const GlobalMenu = {
         
         // 控制子菜单展开状态
         const expandedMenus = Vue.ref(new Set());
+        // 点击子菜单不高亮父菜单 by xu 20251211
+        const shouldHighlightMenu = (item) => {
+            if (!item) {
+                return false;
+            }
+            const hasChildren = Array.isArray(item.children) && item.children.length > 0;
+            return !hasChildren && activeItem.value === item.name;
+        };
+
+        // ===== 三种菜单模式管理 =====
+        const menuModeDict = {
+            collapse: { key: "collapse", label: "收起", width: "60px" },
+            fixed: { key: "fixed", label: "固定", width: "60px" },
+            expand: { key: "expand", label: "展开", width: "230px" }
+        };
+
+        // 当前菜单模式(默认收起)
+        const currentMenuMode = Vue.ref(menuModeDict.collapse);
+
+        // 切换菜单模式(循环:收起 → 固定 → 展开 → 收起)
+        const toggleMenuMode = () => {
+            const modeOrder = ['collapse', 'fixed', 'expand'];
+            const currentIndex = modeOrder.indexOf(currentMenuMode.value.key);
+            const nextIndex = (currentIndex + 1) % modeOrder.length;
+            const nextMode = menuModeDict[modeOrder[nextIndex]];
+
+            currentMenuMode.value = nextMode;
+
+            // 更新 CSS 变量
+            updateLayoutWidth(nextMode.width);
+        };
+
+        // 更新布局宽度
+        const updateLayoutWidth = (width) => {
+            const layoutContainer = document.querySelector('.layout-container');
+            if (layoutContainer) {
+                layoutContainer.style.setProperty('--left-side-width', width);
+            }
+        };
 
         const onMenuClick = (item) => {
             console.log(item)
@@ -270,82 +317,93 @@ export const GlobalMenu = {
         };
 
         const doChangeLeftSideType2Max = () => {
-            if (leftSideType.value.key === 'min') {
+            // 只有在收起模式下才响应鼠标悬停
+            if (currentMenuMode.value.key === 'collapse' && leftSideType.value.key === 'min') {
+                // 即刻展开,不需要延迟
                 clearTimeout(waitLeftSideChangeTimer);
-                waitLeftSideChangeTimer = setTimeout(() => {
-                    leftSideType.value = leftSideTypeDict.max;
-                }, 1000);
+                leftSideType.value = leftSideTypeDict.max;
             }
         };
 
         const onLeaveLeftMenuArea = () => {
-            if (waitLeftSideChangeTimer) {
-                clearTimeout(waitLeftSideChangeTimer);
-                waitLeftSideChangeTimer = null;
+            // 只有在收起模式下才响应鼠标离开
+            if (currentMenuMode.value.key === 'collapse') {
+                if (waitLeftSideChangeTimer) {
+                    clearTimeout(waitLeftSideChangeTimer);
+                    waitLeftSideChangeTimer = null;
+                }
                 leftSideType.value = leftSideTypeDict.min;
             }
         };
 
         return () => Vue.h('div', { class: 'left-side' }, [
-            Vue.h('div', { 
+            Vue.h('div', {
                 class: 'left-side-container',
+                'data-mode': currentMenuMode.value.key,  // 添加模式标识
                 size: leftSideType.value.key
             }, [
-                // 收缩按钮
-                Vue.h('div', { 
-                    class: 'btn-size',
-                    onClick: toggleLeftSideType
-                }, [
-                    Vue.h(SsIcon, {
-                        name: leftSideType.value.icon,
-                        type: 'common',
-                        size: '12px'
-                    })
+                // ===== 固定区域:首页 =====
+                Vue.h('div', { class: 'fixed-top' }, [
+                    menuItemsNew.value.length > 0 ? Vue.h('div', {
+                        class: ['menu-item', 'level-1', { active: shouldHighlightMenu(menuItemsNew.value[0]) }],
+                        onClick: () => {
+                            const firstItem = menuItemsNew.value[0];
+                            activeItem.value = firstItem.name;
+                            onMenuClick(firstItem);
+                        }
+                    }, [
+                        Vue.h('div', { class: 'menu-item-content' }, [
+                            Vue.h(SsNavIcon, { class: menuItemsNew.value[0]?.class || 'nav-icon-home' }),
+                            Vue.h('div', { class: 'menu-item-label' }, menuItemsNew.value[0]?.name || '首页')
+                        ])
+                    ]) : null
                 ]),
-                // 左侧内容
+
+                // ===== 可滚动区域:其他菜单项 =====
                 Vue.h('div', {
-                    class: 'left-side-content',
+                    class: 'scrollable-content',
                     onMouseenter: doChangeLeftSideType2Max,
                     onMousemove: doChangeLeftSideType2Max,
                     onMouseleave: onLeaveLeftMenuArea
                 }, [
-                    // 菜单项列表
-                    ...menuItemsNew.value.map(icon => [
+                    // 菜单项列表(跳过首页)
+                    ...menuItemsNew.value.slice(1).map(icon => [
                         // 父菜单项
-                        Vue.h('div', { 
-                            class:['menu-item',{active:activeItem.value === icon.name}],
+                        Vue.h('div', {
+                            class: ['menu-item', 'level-1', { active: shouldHighlightMenu(icon) }],
                             onClick: () => {
-                                activeItem.value = icon.name;
-                                if (icon.children.length > 0) {
+                                if (icon.children && icon.children.length > 0) {
+                                    // 有子菜单:仅切换展开/收起,不设置 activeItem(不高亮)
                                     if (expandedMenus.value.has(icon.name)) {
                                         expandedMenus.value.delete(icon.name);
                                     } else {
                                         expandedMenus.value.add(icon.name);
                                     }
                                 } else {
+                                    // 无子菜单:设置 activeItem 并导航
+                                    activeItem.value = icon.name;
                                     onMenuClick(icon);
-
                                 }
                             }
                         }, [
                             Vue.h('div', { class: 'menu-item-content' }, [
-                                Vue.h(SsNavIcon, { 
+                                Vue.h(SsNavIcon, {
                                     // 如果是文件夹类型(type == 1),根据展开状态设置不同的class
-                                    class: icon.itemType == 1 ? 
-                                        (expandedMenus.value.has(icon.name) ? 'nav-icon-folder-open' : 'nav-icon-folder-close') 
+                                    class: icon.itemType == 1 ?
+                                        (expandedMenus.value.has(icon.name) ? 'nav-icon-folder-open' : 'nav-icon-folder-close')
                                         : (icon.class || '')
                                 }),
                                 Vue.h('div', { class: 'menu-item-label' }, icon.name || ''),
-                                Vue.h('div', { class: 'edit-mark' }, [
-                                    Vue.h(SsIcon, { name: 'close-mark-fill', size: '36px' })
-                                ])
+                                // 有子菜单时显示小圆点(在一级菜单右上角)
+                                icon.children && icon.children.length > 0 ?
+                                    Vue.h('div', { class: 'has-children-dot' }) : null
                             ])
                         ]),
                         // 子菜单项(与父菜单项同级)
-                        ...(icon.children && expandedMenus.value.has(icon.name) ? 
-                            icon.children.map(child => 
-                                Vue.h('div', { 
-                                    class:['menu-item',{active:activeItem.value === child.name}],
+                        ...(icon.children && expandedMenus.value.has(icon.name) ?
+                            icon.children.map(child =>
+                                Vue.h('div', {
+                                    class: ['menu-item', 'level-2', { active: activeItem.value === child.name }],
                                     onClick: (e) => {
                                         activeItem.value = child.name;
                                         e.stopPropagation();
@@ -353,26 +411,24 @@ export const GlobalMenu = {
                                     }
                                 }, [
                                     Vue.h('div', { class: 'menu-item-content' }, [
-                                        Vue.h('div', { class: 'icon-container' }, [  // 添加图标容器
-                                            Vue.h(SsNavIcon, { 
-                                                class: child.class || ''
-                                            }),
-                                            Vue.h('div', { class: 'menu-item-dot' })  // 小圆点放在图标容器内
-                                        ]),
-                                        Vue.h('div', { class: 'menu-item-label' }, child.name),
-                                        Vue.h('div', { class: 'edit-mark' }, [
-                                            Vue.h(SsIcon, { name: 'close-mark-fill', size: '36px' })
-                                        ])
+                                        Vue.h(SsNavIcon, {
+                                            class: child.class || ''
+                                        }),
+                                        Vue.h('div', { class: 'menu-item-label' }, child.name)
                                     ])
                                 ])
                             ) : []
                         )
-                    ]).flat(),
-                    // 添加按钮
-                    Vue.h('div', { class: 'menu-item add-menu-btn' }, [
-                        Vue.h('div', { class: 'add' }, [
-                            Vue.h(SsNavIcon, { class: 'nav-icon-add' })
-                        ])
+                    ]).flat()
+                ]),
+
+                // ===== 固定区域:菜单模式按钮 =====
+                Vue.h('div', { class: 'fixed-bottom' }, [
+                    Vue.h('div', {
+                        class: 'menu-mode-btn',
+                        onClick: toggleMenuMode
+                    }, [
+                        Vue.h('div', { class: 'mode-label' }, currentMenuMode.value.label)
                     ])
                 ])
             ])
@@ -1153,4 +1209,4 @@ export const Statistics = {
         ]);
     },
 
-};
+};

+ 3 - 3
page/env/objList.jsp

@@ -860,8 +860,7 @@ wd.display.initGrowHigh('lmms${item2.bgmbid}','80px',{},null,false);
 					<ss:equal val='${empty item.service.btnList}' val2='false'>
 						item.buttons=[];
 						<ss:rpt name='${item.service.btnList}' id='btn'>
-
-							<%-- 改为不区分是否第1个变动按钮,都输出相同json数据 --%>
+							
 							item.buttons.push(
 									{
 										id:"${btn.btnID}",
@@ -876,11 +875,12 @@ wd.display.initGrowHigh('lmms${item2.bgmbid}','80px',{},null,false);
 										}
 									}
 							);
+<%--
 							<ss:equal val='${index}' val2='0'>
 							</ss:equal>
 							<ss:notEqual val='${index}' val2='0'>
 							</ss:notEqual>
-
+改为不区分是否第1个变动按钮,都输出相同json数据 --%>
 						</ss:rpt>
 
 					</ss:equal>

+ 13 - 7
page/home.jsp

@@ -549,13 +549,13 @@
 					// 	url:'<ss:serv name='bm_cx' dest='1objList' parm='{dwid:"101122",ssObjId:"101122",ssObjName:"dw",dataType:"change",ParentViewObject:"dw"}'/>'
 					// });
 
-					that.menuData["1"].unshift({
-						desc: "人员",
-						icon: "nav-icon-person",
-						pid:"00001",
-						type:"2",
-						url:'<ss:serv name='ry_cx' dest='1ryList' parm='{}'/>'
-					});
+					// that.menuData["1"].unshift({
+					// 	desc: "人员",
+					// 	icon: "nav-icon-person",
+					// 	pid:"00001",
+					// 	type:"2",
+					// 	url:'<ss:serv name='ry_cx' dest='1ryList' parm='{}'/>'
+					// });
 					<%-- 测试新UI临时写死 end --%>
 
 					that.menuData["1"].unshift({
@@ -580,6 +580,12 @@
 					});
 				},
 			});
+
+			// 初始化菜单模式为收起(设置CSS变量)
+			const layoutContainer = document.querySelector('.layout-container');
+			if (layoutContainer) {
+				layoutContainer.style.setProperty('--left-side-width', '60px');
+			}
           },
           unmounted() {
 			// 清理定时器和事件监听

+ 25 - 6
skin/easy/css/base.css

@@ -1,4 +1,7 @@
 @import url(var.css);
+/* v3.0 图标库引入 by xu 20251212 */
+@import url(icon-base/iconfont.css);
+@import url(icon-biz/iconfont.css);
 
 /* 证件照 */
 .id-photo{
@@ -51,6 +54,20 @@
   border-radius: 50%;
 }
 /* 图标相关开始 */
+/* v3.0 图标组类 - 定义 font-family 及样式 by xu 20251212 */
+/* 菜单图标组(左侧导航菜单)- 使用业务图标库 */
+.menu-icon {
+  font-family: "icon-biz" !important;
+  font-size: 22px;
+  font-style: normal;
+  color: #565d6d;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+.menu-icon:hover {
+  color: #333;
+}
+
 /* 通用icon */
 .common-icon{
   font-size: 22px;
@@ -749,7 +766,7 @@ input::placeholder ,textarea::placeholder{
   transition: width 0.2s;
   display: flex;  /* 添加 flex 布局 */
   flex-direction: column;  /* 垂直排列 */
-  border-right: 2px solid #dadee2;  /* 右边框 */
+  /* 移除容器边框,改为菜单项边框 by xu 20251212 */
 }
 
 .layout-container .left-side-container[size="max"] {
@@ -901,7 +918,6 @@ input::placeholder ,textarea::placeholder{
   flex-shrink: 0;
   border-top: none;  /* 移除上边框 */
   background: #edf1f5;  /* 背景色 */
-  width: 60px;
   height: 40px;
 }
 
@@ -949,11 +965,11 @@ input::placeholder ,textarea::placeholder{
   background: #eef0f5;
 }
 
-/* 有子菜单的小圆点(在一级菜单右上角) */
+/* 有子菜单的小圆点(固定在icon区域右上角)by xu 20251212 */
 .layout-container .has-children-dot {
   position: absolute;
   top: 8px;
-  right: 8px;
+  left: 45px;  /* 固定在icon区域右上角,不随菜单展开移动 by xu 20251212 */
   width: 6px;
   height: 6px;
   background: #999999;  /* 灰色 */
@@ -1269,6 +1285,8 @@ input::placeholder ,textarea::placeholder{
   align-items: center;
   position: relative;
   --edit-mark-display: none;
+  /* 菜单项右边框分割线 by xu 20251212 */
+  border-right: 2px solid #dadee2;
 }
 
 .left-side-container .menu-item-content:hover {
@@ -1317,9 +1335,10 @@ input::placeholder ,textarea::placeholder{
   align-items: center;
   flex-shrink: 0;
 }
-.left-side-container .active{
+/* 选中菜单项隐藏右边框 by xu 20251212 */
+.left-side-container .menu-item.active .menu-item-content{
   background: #edf1f5;
-  border-right: 1px solid #edf1f5;
+  border-right-color: #edf1f5; /* 边框与背景同色,视觉上消失 */
 }
 /* 全局左部边框结束 */