Ver código fonte

滚动问题调整

肖栋 3 dias atrás
pai
commit
2b47fda84f

+ 2 - 1
src/components/mcpBreadcrumb.vue

@@ -42,7 +42,8 @@ const goHome = () => {
   display: flex;
   align-items: center;
   gap: 8px;
-  margin-bottom: 16px;
+  margin: 0;
+  padding: 0;
   font-family: PingFang SC, PingFang SC;
   font-size: 13px;
   color: #94A3B8;

+ 73 - 18
src/components/mcpSiderBar.vue

@@ -40,8 +40,8 @@
           v-for="child in item.children"
           :key="child.key"
           class="mcp-sidebar-child-wrap"
-          @mouseenter="child.key === 'support' && (supportOpen = true)"
-          @mouseleave="child.key === 'support' && (supportOpen = false)"
+          @mouseenter="child.key === 'support' && openSupportPopover($event)"
+          @mouseleave="child.key === 'support' && scheduleCloseSupportPopover()"
         >
           <button
             type="button"
@@ -51,21 +51,30 @@
           >
             {{ child.label }}
           </button>
-          <div v-if="child.key === 'support' && supportOpen" class="mcp-sidebar-support-popover">
-            <img
-              src="@/assets/images/service_WeChat.png"
-              alt="云助手客服微信"
-              class="mcp-sidebar-support-popover-img"
-            />
-            <div class="mcp-sidebar-support-popover-sub">云助手客服微信</div>
-          </div>
         </div>
       </div>
     </div>
   </aside>
+
+  <Teleport to="body">
+    <div
+      v-if="supportOpen"
+      class="mcp-sidebar-support-popover"
+      :style="supportPopoverStyle"
+      @mouseenter="cancelCloseSupportPopover"
+      @mouseleave="closeSupportPopover"
+    >
+      <img
+        src="@/assets/images/service_WeChat.png"
+        alt="云助手客服微信"
+        class="mcp-sidebar-support-popover-img"
+      />
+      <div class="mcp-sidebar-support-popover-sub">云助手客服微信</div>
+    </div>
+  </Teleport>
 </template>
 <script setup>
-import { computed, onMounted, ref, watch } from "vue";
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { getMcpServerList } from "@/api/mcp";
 import globalStore from "@/store/modules/global";
@@ -76,6 +85,9 @@ const global = globalStore();
 
 const serviceChildren = ref([]);
 const supportOpen = ref(false);
+const supportPopoverStyle = ref({});
+const supportTriggerEl = ref(null);
+let supportCloseTimer = null;
 
 const menuList = computed(() => [
   {
@@ -144,8 +156,53 @@ const fetchServerList = async () => {
   }
 };
 
+const updateSupportPopoverPosition = () => {
+  const el = supportTriggerEl.value;
+  if (!el) return;
+  const rect = el.getBoundingClientRect();
+  supportPopoverStyle.value = {
+    top: `${rect.top}px`,
+    left: `${rect.right + 12}px`,
+  };
+};
+
+const cancelCloseSupportPopover = () => {
+  if (supportCloseTimer) {
+    clearTimeout(supportCloseTimer);
+    supportCloseTimer = null;
+  }
+};
+
+const closeSupportPopover = () => {
+  cancelCloseSupportPopover();
+  supportOpen.value = false;
+  supportTriggerEl.value = null;
+};
+
+const scheduleCloseSupportPopover = () => {
+  cancelCloseSupportPopover();
+  supportCloseTimer = setTimeout(() => {
+    closeSupportPopover();
+  }, 120);
+};
+
+const openSupportPopover = (event) => {
+  cancelCloseSupportPopover();
+  supportTriggerEl.value = event.currentTarget;
+  updateSupportPopoverPosition();
+  supportOpen.value = true;
+};
+
 onMounted(() => {
   fetchServerList();
+  window.addEventListener("scroll", updateSupportPopoverPosition, true);
+  window.addEventListener("resize", updateSupportPopoverPosition);
+});
+
+onBeforeUnmount(() => {
+  cancelCloseSupportPopover();
+  window.removeEventListener("scroll", updateSupportPopoverPosition, true);
+  window.removeEventListener("resize", updateSupportPopoverPosition);
 });
 
 const getItemIcon = (item) => {
@@ -175,7 +232,7 @@ const handleChildClick = (child) => {
   if (child?.key === "support") {
     return;
   }
-  supportOpen.value = false;
+  closeSupportPopover();
   if (child?.id) {
     navigateToServer(child);
     return;
@@ -204,8 +261,8 @@ const handleItemClick = (item) => {
   flex-shrink: 0;
   box-sizing: border-box;
   position: sticky;
-  top: 16px;
-  max-height: calc(100vh - 32px);
+  top: 46px;
+  max-height: calc(100vh - 64px - 46px - 16px);
   overflow-y: auto;
   padding: 10px 8px 20px;
   background: #FFFFFF;
@@ -345,10 +402,8 @@ const handleItemClick = (item) => {
 
 .mcp-sidebar-support-popover {
   box-sizing: border-box;
-  position: absolute;
-  top: 0;
-  left: calc(100% + 12px);
-  z-index: 40;
+  position: fixed;
+  z-index: 2000;
   display: flex;
   flex-direction: column;
   align-items: center;

+ 13 - 2
src/pages/home/mcpIndex.vue

@@ -1,6 +1,8 @@
 <template>
   <div class="mcp-index">
-    <mcp-breadcrumb />
+    <div class="mcp-index-top">
+      <mcp-breadcrumb />
+    </div>
     <div class="mcp-index-body">
       <mcp-sider-bar />
       <div class="mcp-index-content">
@@ -17,7 +19,15 @@ import McpBreadcrumb from "@/components/mcpBreadcrumb.vue";
 .mcp-index {
   box-sizing: border-box;
   min-height: calc(100vh - 64px);
-  padding: 16px 20px 24px;
+  padding: 0 0 24px;
+  background: #F7F9FC;
+}
+
+.mcp-index-top {
+  position: sticky;
+  top: 0;
+  z-index: 20;
+  padding: 16px 20px 12px;
   background: #F7F9FC;
 }
 
@@ -25,6 +35,7 @@ import McpBreadcrumb from "@/components/mcpBreadcrumb.vue";
   display: flex;
   align-items: flex-start;
   gap: 16px;
+  padding: 0 20px;
 }
 
 .mcp-index-content {

+ 40 - 22
src/pages/mcp/onlineTest.vue

@@ -1,15 +1,17 @@
 <template>
   <div class="otp-page">
-    <div class="otp-breadcrumb">
-      <button type="button" class="otp-breadcrumb-link" @click="goHome">
-        环评云助手MCP
-      </button>
-      <span class="otp-breadcrumb-sep">›</span>
-      <button type="button" class="otp-breadcrumb-link" @click="goBackToService">
-        {{ breadcrumbServiceName }}
-      </button>
-      <span class="otp-breadcrumb-sep">›</span>
-      <span class="otp-breadcrumb-current">在线测试</span>
+    <div class="otp-page-top">
+      <div class="otp-breadcrumb">
+        <button type="button" class="otp-breadcrumb-link" @click="goHome">
+          环评云助手MCP
+        </button>
+        <span class="otp-breadcrumb-sep">›</span>
+        <button type="button" class="otp-breadcrumb-link" @click="goBackToService">
+          {{ breadcrumbServiceName }}
+        </button>
+        <span class="otp-breadcrumb-sep">›</span>
+        <span class="otp-breadcrumb-current">在线测试</span>
+      </div>
     </div>
 
     <div class="otp-layout">
@@ -505,7 +507,7 @@ import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getMcpServerDetail, getMcpServerList, getMcpToolDetail, callMcpHandle, createMcpToken } from "@/api/mcp";
 import { requireUserLogin } from "@/utils/auth";
-import { applyMcpTokenData } from "@/utils/mcpToken";
+import { applyMcpTokenData, fetchMcpToken } from "@/utils/mcpToken";
 import globalStore from "@/store/modules/global";
 
 const route = useRoute();
@@ -692,6 +694,7 @@ watch(jsonMode, (enabled) => {
 });
 
 onMounted(() => {
+  fetchMcpToken();
   fetchServerList();
 });
 
@@ -1323,17 +1326,25 @@ const runTest = async () => {
 .otp-page {
   box-sizing: border-box;
   min-height: calc(100vh - 64px);
-  padding: 16px 20px 24px;
+  padding: 0 0 24px;
   background: #F7F9FC;
   min-width: 0;
   overflow-x: clip;
 }
 
+.otp-page-top {
+  position: sticky;
+  top: 0;
+  z-index: 20;
+  padding: 16px 20px 12px;
+  background: #F7F9FC;
+}
+
 .otp-breadcrumb {
   display: flex;
   align-items: center;
   gap: 8px;
-  margin-bottom: 16px;
+  margin: 0;
   font-family: PingFang SC, PingFang SC;
   font-size: 13px;
   color: #94A3B8;
@@ -1369,6 +1380,8 @@ const runTest = async () => {
   gap: 16px;
   width: 100%;
   min-width: 0;
+  padding: 0 20px;
+  box-sizing: border-box;
 }
 
 .otp-side {
@@ -1377,8 +1390,8 @@ const runTest = async () => {
   display: flex;
   flex-direction: column;
   position: sticky;
-  top: 16px;
-  max-height: calc(100vh - 32px);
+  top: 46px;
+  max-height: calc(100vh - 64px - 46px - 16px);
   overflow: hidden;
   box-sizing: border-box;
   padding: 16px;
@@ -2404,8 +2417,8 @@ const runTest = async () => {
   flex: 1.5 1.5 0;
   min-width: 0;
   position: sticky;
-  top: 16px;
-  max-height: calc(100vh - 32px);
+  top: 46px;
+  max-height: calc(100vh - 64px - 46px - 16px);
   overflow: hidden;
 }
 
@@ -2695,15 +2708,20 @@ const runTest = async () => {
 @media (max-width: 1200px) {
   .otp-layout {
     gap: 12px;
+    padding: 0 16px;
   }
 
   .otp-page {
-    padding: 12px 16px 20px;
+    padding: 0 0 20px;
+  }
+
+  .otp-page-top {
+    padding: 12px 16px 10px;
   }
 
   .otp-side {
-    top: 12px;
-    max-height: calc(100vh - 32px);
+    top: 40px;
+    max-height: calc(100vh - 64px - 40px - 16px);
     padding: 12px;
   }
 
@@ -2787,8 +2805,8 @@ const runTest = async () => {
   }
 
   .otp-result {
-    top: 12px;
-    max-height: calc(100vh - 32px);
+    top: 40px;
+    max-height: calc(100vh - 64px - 40px - 16px);
   }
 
   .otp-result-card {

+ 5 - 0
src/pages/mcp/serve.vue

@@ -342,6 +342,11 @@ const scrollToMatrix = () => {
 };
 
 const scrollToTop = () => {
+  const main = document.querySelector(".page-main");
+  if (main) {
+    main.scrollTo({ top: 0, behavior: "smooth" });
+    return;
+  }
   const shell = document.querySelector(".page-shell");
   if (shell) {
     shell.scrollTo({ top: 0, behavior: "smooth" });

+ 12 - 4
src/pages/mcp/serverDetail.vue

@@ -357,7 +357,7 @@ import { computed, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getMcpServerDetail, getMcpServerList, getMcpToolDetail, createMcpToken } from "@/api/mcp";
-import { requireUserLogin } from "@/utils/auth";
+import { isUserLoggedIn, requireUserLogin } from "@/utils/auth";
 import { applyMcpTokenData } from "@/utils/mcpToken";
 import globalStore from "@/store/modules/global";
 
@@ -781,12 +781,20 @@ const navigateTo = (key) => {
 };
 
 const goOnlineTest = () => {
-  if (!requireUserLogin("请先登录", { redirect: true })) return;
   const id = route.query.id || serverDetail.value?.id;
-  router.push({
+  const target = {
     name: "mcpOnlineTest",
     query: id ? { id: String(id) } : {},
-  });
+  };
+  if (!isUserLoggedIn()) {
+    ElMessage.warning("请先登录");
+    router.push({
+      name: "login",
+      query: { redirect: router.resolve(target).fullPath },
+    });
+    return;
+  }
+  router.push(target);
 };
 
 const createToken = async () => {

+ 21 - 7
src/pages/pc/index.vue

@@ -45,6 +45,10 @@ const showDesktopHeader = computed(() => isPC);
 
 const scrollPageToTop = () => {
   nextTick(() => {
+    const main = pageShellRef.value?.querySelector?.(".page-main");
+    if (main) {
+      main.scrollTop = 0;
+    }
     if (pageShellRef.value) {
       pageShellRef.value.scrollTop = 0;
     }
@@ -90,11 +94,27 @@ onBeforeMount(async () => {
   flex-direction: column;
   height: 100%;
   min-height: 0;
-  overflow-y: auto;
+  overflow: hidden;
   scrollbar-gutter: stable;
 
+  :deep(.desktop-header) {
+    flex-shrink: 0;
+  }
+
   &.is-hide-scrollbar {
     scrollbar-gutter: auto;
+  }
+}
+
+.page-main {
+  flex: 1 1 auto;
+  min-height: 0;
+  overflow-y: auto;
+  background-color: transparent;
+  scrollbar-gutter: stable;
+
+  .page-shell.is-hide-scrollbar & {
+    scrollbar-gutter: auto;
     scrollbar-width: none;
     -ms-overflow-style: none;
 
@@ -105,10 +125,4 @@ onBeforeMount(async () => {
     }
   }
 }
-
-.page-main {
-  flex: 0 0 auto;
-  min-height: 0;
-  background-color: transparent;
-}
 </style>

+ 2 - 4
src/router/index.js

@@ -5,7 +5,7 @@ import { isUserLoggedIn } from "@/utils/auth";
 const AUTH_WHITE_LIST = new Set(["login", "wechatLogin"]);
 
 /** 必须登录才可访问的路由 */
-const AUTH_REQUIRED_ROUTES = new Set(["mcpOnlineTest", "mcpAuth"]);
+const AUTH_REQUIRED_ROUTES = new Set(["mcpOnlineTest", "mcpAuth", "serve"]);
 
 const routes = [
   {
@@ -107,11 +107,9 @@ router.beforeEach((to, _from, next) => {
   }
 
   if (AUTH_REQUIRED_ROUTES.has(to.name) && !isLoggedIn()) {
-    // 在线测试未登录:登录后回首页;授权页仍回到原目标页
-    const redirect = to.name === "mcpOnlineTest" ? "/" : to.fullPath;
     next({
       name: "login",
-      query: { redirect },
+      query: { redirect: to.fullPath },
     });
     return;
   }