肖栋 3 дней назад
Родитель
Сommit
ef10662803

BIN
src/assets/images/WorkBuddy_img.png


+ 8 - 1
src/components/mcpSiderBar.vue

@@ -257,7 +257,7 @@ const handleItemClick = (item) => {
 </script>
 <style scoped lang="less">
 .mcp-sidebar {
-  width: 220px;
+  width: 260px;
   flex-shrink: 0;
   box-sizing: border-box;
   position: sticky;
@@ -320,7 +320,10 @@ const handleItemClick = (item) => {
   font-weight: 600;
   font-size: 14px;
   line-height: 20px;
+  min-width: 0;
+  overflow: hidden;
   white-space: nowrap;
+  text-overflow: ellipsis;
 }
 
 .mcp-sidebar-arrow {
@@ -373,6 +376,7 @@ const handleItemClick = (item) => {
   display: block;
   box-sizing: border-box;
   width: 100%;
+  min-width: 0;
   min-height: auto;
   padding: 8px 12px;
   border-radius: 8px 8px 8px 8px;
@@ -382,6 +386,9 @@ const handleItemClick = (item) => {
   color: #4B5563;
   line-height: 20px;
   text-align: left;
+  overflow: hidden;
+  white-space: nowrap;
+  text-overflow: ellipsis;
 
   &.is-active {
     background: #EEF4FF;

+ 294 - 133
src/pages/guide/index.vue

@@ -148,51 +148,69 @@
                       </ul>
                     </div>
                   </div>
-                  <div class="home-guide-code">
-                    <div class="home-guide-code-bar">
-                      <div class="home-guide-code-dots">
-                        <span></span>
-                        <span></span>
-                        <span></span>
+                  <div class="home-guide-workbuddy-right">
+                    <div class="home-guide-workbuddy-carousel">
+                      <div
+                        class="home-guide-workbuddy-track"
+                        :style="{ transform: `translateX(-${workbuddySlideIndex * 100}%)` }"
+                      >
+                        <div class="home-guide-workbuddy-slide home-guide-workbuddy-slide--code">
+                          <div class="home-guide-code">
+                            <div class="home-guide-code-bar">
+                              <div class="home-guide-code-dots">
+                                <span></span>
+                                <span></span>
+                                <span></span>
+                              </div>
+                              <div class="home-guide-code-title">mcp.prompt</div>
+                              <button type="button" class="home-guide-code-copy" @click="copyGuideMcpJson">
+                                <icon-svg name="copy_icon_white" size="14px" class="home-guide-code-copy-icon" />
+                                <span>复制</span>
+                              </button>
+                            </div>
+                            <div class="home-guide-code-body">
+                              <div class="home-guide-code-lines">
+                                <span v-for="line in guideWorkbuddyMcpLines" :key="line">{{ line }}</span>
+                              </div>
+                              <pre class="home-guide-code-pre" v-html="guideWorkbuddyMcpJsonHtml"></pre>
+                            </div>
+                          </div>
+                        </div>
+                        <div class="home-guide-workbuddy-slide home-guide-workbuddy-slide--img">
+                          <img
+                            src="@/assets/images/WorkBuddy_img.png"
+                            alt="WorkBuddy 配置示意"
+                            class="home-guide-workbuddy-img"
+                          />
+                        </div>
                       </div>
-                      <div class="home-guide-code-title">mcp.json</div>
-                      <button type="button" class="home-guide-code-copy" @click="copyGuideMcpJson">
-                        <icon-svg name="copy_icon_white" size="14px" class="home-guide-code-copy-icon" />
-                        <span>复制</span>
+                      <button
+                        type="button"
+                        class="home-guide-workbuddy-arrow is-prev"
+                        aria-label="上一张"
+                        @click="setWorkbuddySlide(workbuddySlideIndex - 1)"
+                      >
+                        ‹
                       </button>
-                    </div>
-                    <div class="home-guide-code-body">
-                      <div class="home-guide-code-lines">
-                        <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
+                      <button
+                        type="button"
+                        class="home-guide-workbuddy-arrow is-next"
+                        aria-label="下一张"
+                        @click="setWorkbuddySlide(workbuddySlideIndex + 1)"
+                      >
+                        ›
+                      </button>
+                      <div class="home-guide-workbuddy-indicators">
+                        <button
+                          v-for="(_, index) in 2"
+                          :key="index"
+                          type="button"
+                          class="home-guide-workbuddy-dot"
+                          :class="{ 'is-active': workbuddySlideIndex === index }"
+                          :aria-label="`切换到第 ${index + 1} 张`"
+                          @click="setWorkbuddySlide(index)"
+                        />
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
                     </div>
                   </div>
                 </div>
@@ -385,34 +403,7 @@
                       <div class="home-guide-code-lines">
                         <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
+                      <pre class="home-guide-code-pre" v-html="guideMcpJsonHtml"></pre>
                     </div>
                   </div>
                 </div>
@@ -478,34 +469,7 @@
                       <div class="home-guide-code-lines">
                         <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
+                      <pre class="home-guide-code-pre" v-html="guideMcpJsonHtml"></pre>
                     </div>
                   </div>
                 </div>
@@ -646,7 +610,7 @@
   </div>
 </template>
 <script setup>
-import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { ElMessage } from "element-plus";
 import { getMcpToken, createMcpToken, getMcpServerList } from "@/api/mcp";
 import { requireUserLogin } from "@/utils/auth";
@@ -712,9 +676,45 @@ onMounted(() => {
 });
 onBeforeUnmount(() => {
   document.removeEventListener("click", closeDoubaoSelect);
+  stopWorkbuddyAutoScroll();
 });
 const guideAccessTab = ref("mcp");
 const guideToolTab = ref("qianwen");
+const workbuddySlideIndex = ref(0);
+let workbuddyAutoScrollTimer = null;
+
+const setWorkbuddySlide = (index) => {
+  const total = 2;
+  workbuddySlideIndex.value = ((index % total) + total) % total;
+};
+
+const stopWorkbuddyAutoScroll = () => {
+  if (workbuddyAutoScrollTimer != null) {
+    clearTimeout(workbuddyAutoScrollTimer);
+    workbuddyAutoScrollTimer = null;
+  }
+};
+
+const startWorkbuddyAutoScroll = () => {
+  stopWorkbuddyAutoScroll();
+  workbuddySlideIndex.value = 0;
+  workbuddyAutoScrollTimer = setTimeout(() => {
+    workbuddyAutoScrollTimer = null;
+    workbuddySlideIndex.value = 1;
+  }, 3000);
+};
+
+watch(
+  [guideAccessTab, guideToolTab],
+  ([accessTab, toolTab]) => {
+    if (accessTab === "mcp" && toolTab === "workbuddy") {
+      startWorkbuddyAutoScroll();
+      return;
+    }
+    stopWorkbuddyAutoScroll();
+  },
+  { immediate: true },
+);
 const guideTools = [
   { key: "qianwen", label: "千问办公", iconName: "qianwen_icon" },
   { key: "workbuddy", label: "WorkBuddy", iconName: "WorkBuddy_icon" },
@@ -740,7 +740,7 @@ const guideDoubaoContent = {
     },
   ],
   notes: [
-    "请参照右图依次入参数,每个服务对应一个URL",
+    "请参照右图依次入参数,每个服务对应一个URL",
     "请在豆包-工作-连接器中选择对应的MCP进行检索",
     "自定义 Headers 内需要填写页面「获取 Token」生成的真实 Token",
   ],
@@ -834,40 +834,72 @@ const guideWpsContent = {
 const guideAccessToken = computed(
   () => guideToken.value || "YOUR_ACCESS_TOKEN",
 );
+
+const buildGuideServerKey = (code) => {
+  const value = String(code || "mcp-server");
+  return value.startsWith("hpyzs-") ? value : `hpyzs-${value}`;
+};
+
 const guideMcpJson = computed(() => {
   const token = guideAccessToken.value;
-  return `{
-  "mcpServers": {
-    "hpyzs-law-keyword": {
-      "url": "https://mcp.eiacloud.com/law-keyword",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-law-semantic": {
-      "url": "https://mcp.eiacloud.com/law-semantic",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-qa": {
-      "url": "https://mcp.eiacloud.com/qa",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-emission": {
-      "url": "https://mcp.eiacloud.com/emission",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
+  const list = Array.isArray(global.mcpServerList) ? global.mcpServerList : [];
+  const mcpServers = {};
+  list.forEach((item) => {
+    const serverKey = buildGuideServerKey(item?.code);
+    mcpServers[serverKey] = {
+      url: item?.url || "",
+      headers: {
+        Authorization: `Bearer ${token}`,
+      },
+    };
+  });
+  return JSON.stringify({ mcpServers }, null, 2);
+});
+
+const escapeGuideHtml = (str) =>
+  String(str)
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+
+const highlightGuideJson = (jsonText) => {
+  const text = String(jsonText ?? "");
+  const re =
+    /("(?:\\.|[^"\\])*")\s*:|"((?:\\.|[^"\\])*)"|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],:])/g;
+  let last = 0;
+  let html = "";
+  let match;
+  while ((match = re.exec(text)) !== null) {
+    if (match.index > last) {
+      html += `<span class="code-punct">${escapeGuideHtml(text.slice(last, match.index))}</span>`;
+    }
+    if (match[1] !== undefined) {
+      html += `<span class="code-key">${escapeGuideHtml(match[1])}</span><span class="code-punct">:</span>`;
+    } else if (match[2] !== undefined) {
+      html += `<span class="code-string">"${escapeGuideHtml(match[2])}"</span>`;
+    } else if (match[3] !== undefined || match[4] !== undefined) {
+      html += `<span class="code-string">${escapeGuideHtml(match[0])}</span>`;
+    } else {
+      html += `<span class="code-punct">${escapeGuideHtml(match[0])}</span>`;
     }
+    last = re.lastIndex;
   }
-}`;
-});
+  if (last < text.length) {
+    html += `<span class="code-punct">${escapeGuideHtml(text.slice(last))}</span>`;
+  }
+  return html;
+};
+
+const guideMcpJsonHtml = computed(() => highlightGuideJson(guideMcpJson.value));
+const guideWorkbuddyMcpJsonHtml = computed(
+  () =>
+    `<span class="code-string">这是环评云助手的MCP服务JSON,帮我配置自定义的mcp</span>\n${guideMcpJsonHtml.value}`,
+);
 const guideMcpLines = computed(() =>
   Array.from({ length: guideMcpJson.value.split("\n").length }, (_, i) => i + 1),
 );
+const guideWorkbuddyMcpLines = computed(() => guideMcpLines.value.length + 1);
 const copyText = async (text) => {
   if (!text) {
     ElMessage.warning("暂无内容可复制");
@@ -2272,9 +2304,134 @@ const guideVerifyCards = [
 }
 
 .home-guide-tool-steps,
-.home-guide-code {
+.home-guide-code,
+.home-guide-workbuddy-right {
+  flex: 1;
+  min-width: 0;
+}
+
+.home-guide-workbuddy-right {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.home-guide-workbuddy-carousel {
+  position: relative;
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+  overflow: hidden;
+  border-radius: 12px;
+}
+
+.home-guide-workbuddy-track {
+  display: flex;
+  height: 100%;
+  transition: transform 0.35s ease;
+}
+
+.home-guide-workbuddy-slide {
+  flex: 0 0 100%;
+  width: 100%;
+  min-width: 100%;
+  min-height: 0;
+}
+
+.home-guide-workbuddy-slide--code {
+  display: flex;
+}
+
+.home-guide-workbuddy-slide--code .home-guide-code {
   flex: 1;
   min-width: 0;
+  min-height: 0;
+  height: 100%;
+}
+
+.home-guide-workbuddy-slide--img {
+  display: flex;
+  align-items: stretch;
+  overflow: hidden;
+  background: #FFFFFF;
+  border: 1px solid #E2E8F0;
+  border-radius: 12px;
+  box-sizing: border-box;
+}
+
+.home-guide-workbuddy-img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  object-position: center;
+  background: #F8FAFC;
+}
+
+.home-guide-workbuddy-arrow {
+  position: absolute;
+  top: 50%;
+  z-index: 2;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  margin: 0;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(15, 23, 42, 0.55);
+  color: #FFFFFF;
+  font-size: 20px;
+  line-height: 1;
+  cursor: pointer;
+  opacity: 0;
+  pointer-events: none;
+  transform: translateY(-50%);
+  transition: opacity 0.2s ease;
+}
+
+.home-guide-workbuddy-arrow.is-prev {
+  left: 10px;
+}
+
+.home-guide-workbuddy-arrow.is-next {
+  right: 10px;
+}
+
+.home-guide-workbuddy-carousel:hover .home-guide-workbuddy-arrow {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+.home-guide-workbuddy-indicators {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 10px;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  pointer-events: none;
+}
+
+.home-guide-workbuddy-dot {
+  width: 7px;
+  height: 7px;
+  margin: 0;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(148, 163, 184, 0.7);
+  cursor: pointer;
+  pointer-events: auto;
+}
+
+.home-guide-workbuddy-dot.is-active {
+  background: #1678FF;
 }
 
 .home-guide-tool-steps {
@@ -2508,20 +2665,19 @@ const guideVerifyCards = [
   font-family: Consolas, Monaco, monospace;
   font-size: 13px;
   line-height: 22px;
-  white-space: pre-wrap;
-  word-break: break-all;
+  white-space: pre;
   color: #E2E8F0;
 }
 
-.home-guide-code-pre .code-key {
+.home-guide-code-pre :deep(.code-key) {
   color: #7DD3FC;
 }
 
-.home-guide-code-pre .code-string {
+.home-guide-code-pre :deep(.code-string) {
   color: #86EFAC;
 }
 
-.home-guide-code-pre .code-punct {
+.home-guide-code-pre :deep(.code-punct) {
   color: #CBD5E1;
 }
 
@@ -2749,6 +2905,11 @@ const guideVerifyCards = [
     width: 100%;
   }
 
+  .home-guide-tool-body:not(.home-guide-ima-body) .home-guide-workbuddy-right {
+    flex: none;
+    width: 100%;
+  }
+
   .home-guide-tool-body:not(.home-guide-ima-body) .home-guide-code {
     flex: none;
     width: 100%;

+ 296 - 135
src/pages/home/index.vue

@@ -21,7 +21,7 @@
           </button>
           <button type="button" class="home-hero-btn home-hero-btn-secondary" @click="openRouteInNewTab('serve')">
             <icon-svg name="service_center" size="16px" />
-            <span>服务中心</span>
+            <span>MCP服务中心</span>
           </button>
           <button type="button" class="home-hero-btn home-hero-btn-secondary"
             @click="openRouteInNewTab('mcpOnlineTest')">
@@ -314,51 +314,69 @@
                       </ul>
                     </div>
                   </div>
-                  <div class="home-guide-code">
-                    <div class="home-guide-code-bar">
-                      <div class="home-guide-code-dots">
-                        <span></span>
-                        <span></span>
-                        <span></span>
+                  <div class="home-guide-workbuddy-right">
+                    <div class="home-guide-workbuddy-carousel">
+                      <div
+                        class="home-guide-workbuddy-track"
+                        :style="{ transform: `translateX(-${workbuddySlideIndex * 100}%)` }"
+                      >
+                        <div class="home-guide-workbuddy-slide home-guide-workbuddy-slide--code">
+                          <div class="home-guide-code">
+                            <div class="home-guide-code-bar">
+                              <div class="home-guide-code-dots">
+                                <span></span>
+                                <span></span>
+                                <span></span>
+                              </div>
+                              <div class="home-guide-code-title">mcp.prompt</div>
+                              <button type="button" class="home-guide-code-copy" @click="copyGuideMcpJson">
+                                <icon-svg name="copy_icon_white" size="14px" class="home-guide-code-copy-icon" />
+                                <span>复制</span>
+                              </button>
+                            </div>
+                            <div class="home-guide-code-body">
+                              <div class="home-guide-code-lines">
+                                <span v-for="line in guideWorkbuddyMcpLines" :key="line">{{ line }}</span>
+                              </div>
+                              <pre class="home-guide-code-pre" v-html="guideWorkbuddyMcpJsonHtml"></pre>
+                            </div>
+                          </div>
+                        </div>
+                        <div class="home-guide-workbuddy-slide home-guide-workbuddy-slide--img">
+                          <img
+                            src="@/assets/images/WorkBuddy_img.png"
+                            alt="WorkBuddy 配置示意"
+                            class="home-guide-workbuddy-img"
+                          />
+                        </div>
                       </div>
-                      <div class="home-guide-code-title">mcp.json</div>
-                      <button type="button" class="home-guide-code-copy" @click="copyGuideMcpJson">
-                        <icon-svg name="copy_icon_white" size="14px" class="home-guide-code-copy-icon" />
-                        <span>复制</span>
+                      <button
+                        type="button"
+                        class="home-guide-workbuddy-arrow is-prev"
+                        aria-label="上一张"
+                        @click="setWorkbuddySlide(workbuddySlideIndex - 1)"
+                      >
+                        ‹
                       </button>
-                    </div>
-                    <div class="home-guide-code-body">
-                      <div class="home-guide-code-lines">
-                        <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
+                      <button
+                        type="button"
+                        class="home-guide-workbuddy-arrow is-next"
+                        aria-label="下一张"
+                        @click="setWorkbuddySlide(workbuddySlideIndex + 1)"
+                      >
+                        ›
+                      </button>
+                      <div class="home-guide-workbuddy-indicators">
+                        <button
+                          v-for="(_, index) in 2"
+                          :key="index"
+                          type="button"
+                          class="home-guide-workbuddy-dot"
+                          :class="{ 'is-active': workbuddySlideIndex === index }"
+                          :aria-label="`切换到第 ${index + 1} 张`"
+                          @click="setWorkbuddySlide(index)"
+                        />
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
                     </div>
                   </div>
                 </div>
@@ -551,34 +569,7 @@
                       <div class="home-guide-code-lines">
                         <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
+                      <pre class="home-guide-code-pre" v-html="guideMcpJsonHtml"></pre>
                     </div>
                   </div>
                 </div>
@@ -644,34 +635,7 @@
                       <div class="home-guide-code-lines">
                         <span v-for="line in guideMcpLines" :key="line">{{ line }}</span>
                       </div>
-                      <pre class="home-guide-code-pre"><span class="code-punct">{</span>
-  <span class="code-key">"mcpServers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-    <span class="code-key">"hpyzs-law-keyword"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-keyword"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-law-semantic"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/law-semantic"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-qa"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/qa"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">},</span>
-    <span class="code-key">"hpyzs-emission"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-      <span class="code-key">"url"</span><span class="code-punct">:</span> <span class="code-string">"https://mcp.eiacloud.com/emission"</span><span class="code-punct">,</span>
-      <span class="code-key">"headers"</span><span class="code-punct">:</span> <span class="code-punct">{</span>
-        <span class="code-key">"Authorization"</span><span class="code-punct">:</span> <span class="code-string">"Bearer {{ guideAccessToken }}"</span>
-      <span class="code-punct">}</span>
-    <span class="code-punct">}</span>
-  <span class="code-punct">}</span>
-<span class="code-punct">}</span></pre>
+                      <pre class="home-guide-code-pre" v-html="guideMcpJsonHtml"></pre>
                     </div>
                   </div>
                 </div>
@@ -860,7 +824,7 @@
 </template>
 
 <script setup>
-import { computed, onBeforeUnmount, onMounted, ref } from "vue";
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
 import { getMcpToken, createMcpToken, getMcpServerHotList, getMcpServerList } from "@/api/mcp";
@@ -982,9 +946,45 @@ onMounted(() => {
 });
 onBeforeUnmount(() => {
   document.removeEventListener("click", closeDoubaoSelect);
+  stopWorkbuddyAutoScroll();
 });
 const guideAccessTab = ref("mcp");
 const guideToolTab = ref("qianwen");
+const workbuddySlideIndex = ref(0);
+let workbuddyAutoScrollTimer = null;
+
+const setWorkbuddySlide = (index) => {
+  const total = 2;
+  workbuddySlideIndex.value = ((index % total) + total) % total;
+};
+
+const stopWorkbuddyAutoScroll = () => {
+  if (workbuddyAutoScrollTimer != null) {
+    clearTimeout(workbuddyAutoScrollTimer);
+    workbuddyAutoScrollTimer = null;
+  }
+};
+
+const startWorkbuddyAutoScroll = () => {
+  stopWorkbuddyAutoScroll();
+  workbuddySlideIndex.value = 0;
+  workbuddyAutoScrollTimer = setTimeout(() => {
+    workbuddyAutoScrollTimer = null;
+    workbuddySlideIndex.value = 1;
+  }, 3000);
+};
+
+watch(
+  [guideAccessTab, guideToolTab],
+  ([accessTab, toolTab]) => {
+    if (accessTab === "mcp" && toolTab === "workbuddy") {
+      startWorkbuddyAutoScroll();
+      return;
+    }
+    stopWorkbuddyAutoScroll();
+  },
+  { immediate: true },
+);
 const guideTools = [
   { key: "qianwen", label: "千问办公", iconName: "qianwen_icon" },
   { key: "workbuddy", label: "WorkBuddy", iconName: "WorkBuddy_icon" },
@@ -1010,7 +1010,7 @@ const guideDoubaoContent = {
     },
   ],
   notes: [
-    "请参照右图依次入参数,每个服务对应一个URL",
+    "请参照右图依次入参数,每个服务对应一个URL",
     "请在豆包-工作-连接器中选择对应的MCP进行检索",
     "自定义 Headers 内需要填写页面「获取 Token」生成的真实 Token",
   ],
@@ -1104,40 +1104,72 @@ const guideWpsContent = {
 const guideAccessToken = computed(
   () => guideToken.value || "YOUR_ACCESS_TOKEN",
 );
+
+const buildGuideServerKey = (code) => {
+  const value = String(code || "mcp-server");
+  return value.startsWith("hpyzs-") ? value : `hpyzs-${value}`;
+};
+
 const guideMcpJson = computed(() => {
   const token = guideAccessToken.value;
-  return `{
-  "mcpServers": {
-    "hpyzs-law-keyword": {
-      "url": "https://mcp.eiacloud.com/law-keyword",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-law-semantic": {
-      "url": "https://mcp.eiacloud.com/law-semantic",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-qa": {
-      "url": "https://mcp.eiacloud.com/qa",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
-    },
-    "hpyzs-emission": {
-      "url": "https://mcp.eiacloud.com/emission",
-      "headers": {
-        "Authorization": "Bearer ${token}"
-      }
+  const list = Array.isArray(global.mcpServerList) ? global.mcpServerList : [];
+  const mcpServers = {};
+  list.forEach((item) => {
+    const serverKey = buildGuideServerKey(item?.code);
+    mcpServers[serverKey] = {
+      url: item?.url || "",
+      headers: {
+        Authorization: `Bearer ${token}`,
+      },
+    };
+  });
+  return JSON.stringify({ mcpServers }, null, 2);
+});
+
+const escapeGuideHtml = (str) =>
+  String(str)
+    .replace(/&/g, "&amp;")
+    .replace(/</g, "&lt;")
+    .replace(/>/g, "&gt;")
+    .replace(/"/g, "&quot;");
+
+const highlightGuideJson = (jsonText) => {
+  const text = String(jsonText ?? "");
+  const re =
+    /("(?:\\.|[^"\\])*")\s*:|"((?:\\.|[^"\\])*)"|(-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false|null)\b|([{}[\],:])/g;
+  let last = 0;
+  let html = "";
+  let match;
+  while ((match = re.exec(text)) !== null) {
+    if (match.index > last) {
+      html += `<span class="code-punct">${escapeGuideHtml(text.slice(last, match.index))}</span>`;
     }
+    if (match[1] !== undefined) {
+      html += `<span class="code-key">${escapeGuideHtml(match[1])}</span><span class="code-punct">:</span>`;
+    } else if (match[2] !== undefined) {
+      html += `<span class="code-string">"${escapeGuideHtml(match[2])}"</span>`;
+    } else if (match[3] !== undefined || match[4] !== undefined) {
+      html += `<span class="code-string">${escapeGuideHtml(match[0])}</span>`;
+    } else {
+      html += `<span class="code-punct">${escapeGuideHtml(match[0])}</span>`;
+    }
+    last = re.lastIndex;
   }
-}`;
-});
+  if (last < text.length) {
+    html += `<span class="code-punct">${escapeGuideHtml(text.slice(last))}</span>`;
+  }
+  return html;
+};
+
+const guideMcpJsonHtml = computed(() => highlightGuideJson(guideMcpJson.value));
+const guideWorkbuddyMcpJsonHtml = computed(
+  () =>
+    `<span class="code-string">这是环评云助手的MCP服务JSON,帮我配置自定义的mcp</span>\n${guideMcpJsonHtml.value}`,
+);
 const guideMcpLines = computed(() =>
   Array.from({ length: guideMcpJson.value.split("\n").length }, (_, i) => i + 1),
 );
+const guideWorkbuddyMcpLines = computed(() => guideMcpLines.value.length + 1);
 const copyText = async (text) => {
   if (!text) {
     ElMessage.warning("暂无内容可复制");
@@ -1371,7 +1403,7 @@ const guideVerifyCards = [
 .home-hero-btn {
   display: inline-flex;
   align-items: center;
-  gap: 3px;
+  gap: 10px;
   margin: 0;
   cursor: pointer;
   box-sizing: border-box;
@@ -3224,9 +3256,134 @@ const guideVerifyCards = [
 }
 
 .home-guide-tool-steps,
-.home-guide-code {
+.home-guide-code,
+.home-guide-workbuddy-right {
+  flex: 1;
+  min-width: 0;
+}
+
+.home-guide-workbuddy-right {
+  display: flex;
+  flex-direction: column;
+  min-height: 0;
+}
+
+.home-guide-workbuddy-carousel {
+  position: relative;
+  flex: 1;
+  min-width: 0;
+  min-height: 0;
+  overflow: hidden;
+  border-radius: 12px;
+}
+
+.home-guide-workbuddy-track {
+  display: flex;
+  height: 100%;
+  transition: transform 0.35s ease;
+}
+
+.home-guide-workbuddy-slide {
+  flex: 0 0 100%;
+  width: 100%;
+  min-width: 100%;
+  min-height: 0;
+}
+
+.home-guide-workbuddy-slide--code {
+  display: flex;
+}
+
+.home-guide-workbuddy-slide--code .home-guide-code {
   flex: 1;
   min-width: 0;
+  min-height: 0;
+  height: 100%;
+}
+
+.home-guide-workbuddy-slide--img {
+  display: flex;
+  align-items: stretch;
+  overflow: hidden;
+  background: #FFFFFF;
+  border: 1px solid #E2E8F0;
+  border-radius: 12px;
+  box-sizing: border-box;
+}
+
+.home-guide-workbuddy-img {
+  display: block;
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+  object-position: center;
+  background: #F8FAFC;
+}
+
+.home-guide-workbuddy-arrow {
+  position: absolute;
+  top: 50%;
+  z-index: 2;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 28px;
+  height: 28px;
+  margin: 0;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(15, 23, 42, 0.55);
+  color: #FFFFFF;
+  font-size: 20px;
+  line-height: 1;
+  cursor: pointer;
+  opacity: 0;
+  pointer-events: none;
+  transform: translateY(-50%);
+  transition: opacity 0.2s ease;
+}
+
+.home-guide-workbuddy-arrow.is-prev {
+  left: 10px;
+}
+
+.home-guide-workbuddy-arrow.is-next {
+  right: 10px;
+}
+
+.home-guide-workbuddy-carousel:hover .home-guide-workbuddy-arrow {
+  opacity: 1;
+  pointer-events: auto;
+}
+
+.home-guide-workbuddy-indicators {
+  position: absolute;
+  left: 0;
+  right: 0;
+  bottom: 10px;
+  z-index: 2;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  gap: 6px;
+  pointer-events: none;
+}
+
+.home-guide-workbuddy-dot {
+  width: 7px;
+  height: 7px;
+  margin: 0;
+  padding: 0;
+  border: none;
+  border-radius: 50%;
+  background: rgba(148, 163, 184, 0.7);
+  cursor: pointer;
+  pointer-events: auto;
+}
+
+.home-guide-workbuddy-dot.is-active {
+  background: #1678FF;
 }
 
 .home-guide-tool-steps {
@@ -3460,20 +3617,19 @@ const guideVerifyCards = [
   font-family: Consolas, Monaco, monospace;
   font-size: 13px;
   line-height: 22px;
-  white-space: pre-wrap;
-  word-break: break-all;
+  white-space: pre;
   color: #E2E8F0;
 }
 
-.home-guide-code-pre .code-key {
+.home-guide-code-pre :deep(.code-key) {
   color: #7DD3FC;
 }
 
-.home-guide-code-pre .code-string {
+.home-guide-code-pre :deep(.code-string) {
   color: #86EFAC;
 }
 
-.home-guide-code-pre .code-punct {
+.home-guide-code-pre :deep(.code-punct) {
   color: #CBD5E1;
 }
 
@@ -3908,6 +4064,11 @@ const guideVerifyCards = [
     width: 100%;
   }
 
+  .home-guide-tool-body:not(.home-guide-ima-body) .home-guide-workbuddy-right {
+    flex: none;
+    width: 100%;
+  }
+
   .home-guide-tool-body:not(.home-guide-ima-body) .home-guide-code {
     flex: none;
     width: 100%;

+ 42 - 20
src/pages/mcp/onlineTest.vue

@@ -476,7 +476,7 @@
             </div>
             <span class="otp-detail-filename">response.json</span>
           </div>
-          <div class="otp-detail-code-wrap">
+          <div ref="detailCodeWrapRef" class="otp-detail-code-wrap">
             <div class="otp-detail-lines" aria-hidden="true">
               <span v-for="n in detailLineCount" :key="n">{{ n }}</span>
             </div>
@@ -1088,9 +1088,23 @@ const hasTestResult = computed(() => searchResult.value != null);
 
 const resultDetailVisible = ref(false);
 const resultSearch = ref("");
+const detailCodeWrapRef = ref(null);
 
 const detailSearchKeyword = computed(() => resultSearch.value.trim());
 
+watch(detailSearchKeyword, async (kw) => {
+  await nextTick();
+  const wrap = detailCodeWrapRef.value;
+  if (!wrap || !kw) return;
+  const firstHit = wrap.querySelector(".otp-json-search-hit");
+  if (!firstHit) return;
+  const wrapRect = wrap.getBoundingClientRect();
+  const hitRect = firstHit.getBoundingClientRect();
+  const nextTop =
+    wrap.scrollTop + hitRect.top - wrapRect.top - (wrap.clientHeight - hitRect.height) / 2;
+  wrap.scrollTo({ top: nextTop, behavior: "smooth" });
+});
+
 const detailByteSize = computed(() =>
   new TextEncoder().encode(responseJson.value || "").length,
 );
@@ -1320,6 +1334,12 @@ const runTest = async () => {
     resultTab.value = "业务数据";
   } catch (error) {
     console.error(error);
+    const isTimeout =
+      error?.code === "ECONNABORTED" ||
+      /timeout/i.test(error?.message || "");
+    if (isTimeout) {
+      ElMessage.warning("请求超时,请稍后重试");
+    }
     searchResult.value = null;
     resultMeta.value = {
       code: error?.response?.status ?? null,
@@ -1336,16 +1356,18 @@ const runTest = async () => {
 <style scoped lang="less">
 .otp-page {
   box-sizing: border-box;
-  min-height: calc(100vh - 64px);
-  padding: 0 0 24px;
+  height: 100%;
+  min-height: 0;
+  display: flex;
+  flex-direction: column;
+  padding: 0;
   background: #F7F9FC;
   min-width: 0;
-  overflow-x: clip;
+  overflow: hidden;
 }
 
 .otp-page-top {
-  position: sticky;
-  top: 0;
+  flex-shrink: 0;
   z-index: 20;
   padding: 16px 20px 12px;
   background: #F7F9FC;
@@ -1391,7 +1413,11 @@ const runTest = async () => {
   gap: 16px;
   width: 100%;
   min-width: 0;
-  padding: 0 20px;
+  flex: 1;
+  min-height: 0;
+  overflow-x: clip;
+  overflow-y: auto;
+  padding: 0 20px 24px;
   box-sizing: border-box;
 }
 
@@ -1401,8 +1427,8 @@ const runTest = async () => {
   display: flex;
   flex-direction: column;
   position: sticky;
-  top: 46px;
-  max-height: calc(100vh - 64px - 46px - 16px);
+  top: 0;
+  max-height: 100%;
   overflow: hidden;
   box-sizing: border-box;
   padding: 16px;
@@ -2428,8 +2454,8 @@ const runTest = async () => {
   flex: 1.5 1.5 0;
   min-width: 0;
   position: sticky;
-  top: 46px;
-  max-height: calc(100vh - 64px - 46px - 16px);
+  top: 0;
+  max-height: 100%;
   overflow: hidden;
 }
 
@@ -2719,11 +2745,7 @@ const runTest = async () => {
 @media (max-width: 1200px) {
   .otp-layout {
     gap: 12px;
-    padding: 0 16px;
-  }
-
-  .otp-page {
-    padding: 0 0 20px;
+    padding: 0 16px 20px;
   }
 
   .otp-page-top {
@@ -2731,8 +2753,8 @@ const runTest = async () => {
   }
 
   .otp-side {
-    top: 40px;
-    max-height: calc(100vh - 64px - 40px - 16px);
+    top: 0;
+    max-height: 100%;
     padding: 12px;
   }
 
@@ -2816,8 +2838,8 @@ const runTest = async () => {
   }
 
   .otp-result {
-    top: 40px;
-    max-height: calc(100vh - 64px - 40px - 16px);
+    top: 0;
+    max-height: 100%;
   }
 
   .otp-result-card {

+ 20 - 3
src/pages/mcp/serve.vue

@@ -581,12 +581,13 @@ const onWindowResize = () => {
   resizeTimer = setTimeout(updateColumnCount, 160);
 };
 
-onMounted(() => {
+onMounted(async () => {
   nextTick(updateColumnCount);
   window.addEventListener("resize", onWindowResize);
   window.addEventListener("keydown", onGiftModalKeydown);
   fetchCenterList();
-  fetchReceiveYdStatus();
+  await fetchReceiveYdStatus();
+  await resumeClaimAfterLogin();
 });
 
 onBeforeUnmount(() => {
@@ -635,12 +636,29 @@ const openClaimSuccessModal = async () => {
 
 const goLoginToClaim = () => {
   needLoginVisible.value = false;
+  try {
+    sessionStorage.setItem("mcp_claim_gift_pending", "1");
+  } catch (_) {
+    /* ignore */
+  }
   router.push({
     name: "login",
     query: { redirect: router.currentRoute.value.fullPath || "/serve" },
   });
 };
 
+const resumeClaimAfterLogin = async () => {
+  let pending = false;
+  try {
+    pending = sessionStorage.getItem("mcp_claim_gift_pending") === "1";
+    if (pending) sessionStorage.removeItem("mcp_claim_gift_pending");
+  } catch (_) {
+    /* ignore */
+  }
+  if (!pending || !isUserLoggedIn()) return;
+  await claimGift();
+};
+
 const claimGift = async () => {
   if (!isUserLoggedIn()) {
     needLoginVisible.value = true;
@@ -1462,7 +1480,6 @@ const claimGift = async () => {
   flex-wrap: wrap;
   justify-content: flex-end;
   padding-top: 13px;
-  border-top: 1px dashed var(--line);
 }
 
 .btn-ico {