소스 검색

文档问题调整

肖栋 2 주 전
부모
커밋
d54d7fa600

+ 5 - 0
src/api/mcp.js

@@ -15,6 +15,11 @@ export function getMcpServerList() {
   return get("/mcp/server/list");
 }
 
+/** 获取热门 MCP Server 列表 */
+export function getMcpServerHotList() {
+  return get("/mcp/server/hotList");
+}
+
 /** 获取 MCP Server 详情 */
 export function getMcpServerDetail(id) {
   return get(`/mcp/server/${id}`);

+ 28 - 1
src/components/login.vue

@@ -144,7 +144,6 @@ const props = defineProps({
 const global = globalStore();
 const isPC = IsPC();
 const loginVisable = computed(() => global.loginVisable);
-const triggerLoginVisible = computed(() => global.triggerLoginVisible);
 const emit = defineEmits(["update:modelValue", "saveUser", "closed"]);
 const dialogVisible = computed(() => props.asPage || loginVisable.value);
 
@@ -803,14 +802,42 @@ onBeforeUnmount(() => {
                         margin-bottom: 0;
                         padding: 0;
 
+                        :deep(.el-checkbox) {
+                            height: 20px;
+                            margin: 0;
+                        }
+
+                        :deep(.el-checkbox__inner) {
+                            width: 20px;
+                            height: 20px;
+                        }
+
+                        :deep(.el-checkbox__inner::after) {
+                            width: 4px;
+                            height: 10px;
+                            left: 6px;
+                            top: 2px;
+                        }
+
                         .way-checkbox-password {
                             cursor: pointer;
+                            font-family: PingFang SC, PingFang SC;
+                            font-weight: 400;
+                            font-size: 12px;
+                            color: #888888;
+                            line-height: 14px;
+                            text-align: left;
                         }
 
                         .way-checkbox-text {
                             cursor: pointer;
                             white-space: nowrap;
                             flex-shrink: 0;
+                            font-family: PingFang SC, PingFang SC;
+                            font-weight: 400;
+                            font-size: 12px;
+                            line-height: 14px;
+                            text-align: left;
 
                             >span {
                                 color: #1678FF;

+ 74 - 0
src/components/mcpBreadcrumb.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="mcp-breadcrumb">
+    <button type="button" class="mcp-breadcrumb-link" @click="goHome">
+      环评云助手MCP
+    </button>
+    <span class="mcp-breadcrumb-sep">›</span>
+    <span class="mcp-breadcrumb-current">{{ currentLabel }}</span>
+  </div>
+</template>
+
+<script setup>
+import { computed } from "vue";
+import { useRoute, useRouter } from "vue-router";
+import globalStore from "@/store/modules/global";
+
+const route = useRoute();
+const router = useRouter();
+const global = globalStore();
+
+const ROUTE_LABELS = {
+  guide: "MCP服务接入指南",
+  faq: "常见问题",
+  support: "技术支持",
+};
+
+const currentLabel = computed(() => {
+  if (route.name === "mcpServer") {
+    const id = route.query.id ? String(route.query.id) : "";
+    const matched = global.mcpServerList.find((item) => String(item.id) === id);
+    return matched?.name || "MCP 服务";
+  }
+  return ROUTE_LABELS[route.name] || "";
+});
+
+const goHome = () => {
+  router.push({ name: "mcp" });
+};
+</script>
+
+<style scoped lang="less">
+.mcp-breadcrumb {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  margin-bottom: 16px;
+  font-family: PingFang SC, PingFang SC;
+  font-size: 13px;
+  color: #94A3B8;
+  line-height: 18px;
+}
+
+.mcp-breadcrumb-link {
+  margin: 0;
+  padding: 0;
+  border: none;
+  background: transparent;
+  color: #94A3B8;
+  font: inherit;
+  line-height: inherit;
+  cursor: pointer;
+}
+
+.mcp-breadcrumb-link:hover {
+  color: #1678FF;
+}
+
+.mcp-breadcrumb-sep {
+  color: #CBD5E1;
+}
+
+.mcp-breadcrumb-current {
+  color: #1E293B;
+}
+</style>

+ 82 - 8
src/components/mcpSiderBar.vue

@@ -36,22 +36,36 @@
         v-if="item.children?.length && expandedKeys.includes(item.key)"
         class="mcp-sidebar-children"
       >
-        <button
+        <div
           v-for="child in item.children"
           :key="child.key"
-          type="button"
-          class="mcp-sidebar-child"
-          :class="{ 'is-active': child.key === activeKey }"
-          @click="handleChildClick(child)"
+          class="mcp-sidebar-child-wrap"
+          :ref="(el) => setSupportRef(child, el)"
+          @click.stop
         >
-          {{ child.label }}
-        </button>
+          <button
+            type="button"
+            class="mcp-sidebar-child"
+            :class="{ 'is-active': child.key === activeKey }"
+            @click="handleChildClick(child)"
+          >
+            {{ 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>
 </template>
 <script setup>
-import { computed, onMounted, ref, watch } from "vue";
+import { computed, onMounted, onUnmounted, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { getMcpServerList } from "@/api/mcp";
 import globalStore from "@/store/modules/global";
@@ -61,6 +75,14 @@ const router = useRouter();
 const global = globalStore();
 
 const serviceChildren = ref([]);
+const supportOpen = ref(false);
+const supportRef = ref(null);
+
+const setSupportRef = (child, el) => {
+  if (child?.key === "support") {
+    supportRef.value = el;
+  }
+};
 
 const menuList = computed(() => [
   {
@@ -131,6 +153,11 @@ const fetchServerList = async () => {
 
 onMounted(() => {
   fetchServerList();
+  document.addEventListener("click", handleDocumentClick);
+});
+
+onUnmounted(() => {
+  document.removeEventListener("click", handleDocumentClick);
 });
 
 const getItemIcon = (item) => {
@@ -157,6 +184,11 @@ const navigateToServer = (child) => {
 };
 
 const handleChildClick = (child) => {
+  if (child?.key === "support") {
+    supportOpen.value = !supportOpen.value;
+    return;
+  }
+  supportOpen.value = false;
   if (child?.id) {
     navigateToServer(child);
     return;
@@ -166,6 +198,12 @@ const handleChildClick = (child) => {
   }
 };
 
+const handleDocumentClick = (event) => {
+  if (!supportOpen.value) return;
+  if (supportRef.value?.contains?.(event.target)) return;
+  supportOpen.value = false;
+};
+
 const handleItemClick = (item) => {
   if (item.children?.length) {
     const index = expandedKeys.value.indexOf(item.key);
@@ -315,4 +353,40 @@ const handleItemClick = (item) => {
     text-align: left;
   }
 }
+
+.mcp-sidebar-child-wrap {
+  position: relative;
+}
+
+.mcp-sidebar-support-popover {
+  box-sizing: border-box;
+  position: absolute;
+  top: 0;
+  left: calc(100% + 12px);
+  z-index: 40;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 4px;
+  width: 140px;
+  padding: 10px;
+  background: #FFFFFF;
+  box-shadow: 0px 0px 6px 0px rgba(0, 0, 0, 0.1);
+  border-radius: 8px;
+}
+
+.mcp-sidebar-support-popover-img {
+  display: block;
+  width: 100%;
+  height: auto;
+}
+
+.mcp-sidebar-support-popover-sub {
+  font-family: PingFang SC, PingFang SC;
+  font-weight: 400;
+  font-size: 12px;
+  color: #666666;
+  line-height: 16px;
+  text-align: left;
+}
 </style>

+ 1 - 2
src/components/pcHeader.vue

@@ -359,7 +359,7 @@ const handleUploadMaterialClick = (shareType) => {
 
 /**
  * 退出当前登录账号。
- * 成功后清理全局登录状态,并跳转登录页
+ * 成功后清理全局登录状态,停留在当前页面
  */
 const logoutHandle = async () => {
     if (isLoggingOut.value) {
@@ -374,7 +374,6 @@ const logoutHandle = async () => {
         if (response.code === 200) {
             ElMessage.success("退出登录成功!");
             await global.logout();
-            await router.replace({ name: "login" });
             return;
         }
 

+ 6 - 19
src/pages/guide/index.vue

@@ -114,15 +114,6 @@
                     <div class="home-guide-tool-step">
                       <div class="home-guide-tool-step-num">1</div>
                       <div class="home-guide-tool-step-copy">
-                        <div class="home-guide-tool-step-title">内置连接器一键接入</div>
-                        <div class="home-guide-tool-step-desc">
-                          打开 WorkBuddy → 连接第三方应用 → 管理连接器 → 找到「环评云助手」→ 一键连接并完成授权
-                        </div>
-                      </div>
-                    </div>
-                    <div class="home-guide-tool-step">
-                      <div class="home-guide-tool-step-num">2</div>
-                      <div class="home-guide-tool-step-copy">
                         <div class="home-guide-tool-step-title">自定义 MCP 配置</div>
                         <div class="home-guide-tool-step-desc">
                           复制右侧 JSON → 对话框发送「这是环评云助手的 MCP 服务 JSON,帮我配置自定义的 mcp」并粘贴
@@ -130,7 +121,7 @@
                       </div>
                     </div>
                     <div class="home-guide-tool-step">
-                      <div class="home-guide-tool-step-num">3</div>
+                      <div class="home-guide-tool-step-num">2</div>
                       <div class="home-guide-tool-step-copy">
                         <div class="home-guide-tool-step-title">验证接入是否成功</div>
                         <div class="home-guide-tool-step-desc">
@@ -467,6 +458,7 @@
 import { computed, onMounted, ref } from "vue";
 import { ElMessage } from "element-plus";
 import { getMcpToken, createMcpToken } from "@/api/mcp";
+import { requireUserLogin } from "@/utils/auth";
 import globalStore from "@/store/modules/global";
 
 const global = globalStore();
@@ -479,7 +471,7 @@ const guideTokenMasked = computed(() => {
   }
   return {
     head: token.slice(0, 9),
-    middle: "".repeat(token.length - 15),
+    middle: "*".repeat(token.length - 15),
     tail: token.slice(-6),
   };
 });
@@ -509,6 +501,7 @@ const fetchMcpToken = async () => {
 };
 
 const createToken = async () => {
+  if (!requireUserLogin("请先登录", { redirect: true })) return;
   try {
     const res = await createMcpToken();
     applyToken(res?.data);
@@ -526,7 +519,6 @@ const guideTools = [
   { key: "workbuddy", label: "WorkBuddy", icon: true },
   { key: "doubao", label: "豆包", icon: false },
   { key: "qoderwork", label: "Qoderwork", icon: false },
-  { key: "wps", label: "WPS Comate", icon: false },
 ];
 const guideDoubaoContent = {
   alert: "豆包只能通过新建自定义连接器接入,不支持直接粘贴 JSON 配置。",
@@ -550,11 +542,11 @@ const guideDoubaoContent = {
 };
 const guideWpsContent = {
   alert:
-    "仅支持内置连接:WPS Comate 只能通过内置「北大法宝」连接接入,不支持粘贴 JSON 或自定义 MCP 配置。",
+    "仅支持内置连接:WPS Comate 只能通过内置「环评云助手」连接接入,不支持粘贴 JSON 或自定义 MCP 配置。",
   steps: [
     {
       num: 1,
-      title: "打开 WPS Comate → 进入「连接」→ 找到「北大法宝」→ 点击「连接」",
+      title: "打开 WPS Comate → 进入「连接」→ 找到「环评云助手」→ 点击「连接」",
     },
     {
       num: 2,
@@ -1070,11 +1062,6 @@ const guideVerifyCards = [
   text-align: left;
 }
 
-.home-guide-token-mask {
-  letter-spacing: -0.55em;
-  margin-right: 0.55em;
-}
-
 .home-guide-token-icons {
   display: flex;
   align-items: center;

+ 2 - 2
src/pages/help/faq.vue

@@ -19,7 +19,7 @@ const faqList = [
   {
     question: "Token 在哪里获取?",
     answer:
-      "登录环评云助手平台后,进入「控制台 → 我的账号 → 访问令牌」,点击生成即可获得 Token。一套 Token 可同时用于 MCP 与 CLI 两种接入方式。",
+      "登录环评云助手MCP平台后,点击「获取token」即可获得。一套 Token 可同时用于 MCP 与 CLI 两种接入方式。",
   },
   {
     question: "为什么配置后工具没有出现?",
@@ -29,7 +29,7 @@ const faqList = [
   {
     question: "四个知识库 Server 都要分别配置吗?",
     answer:
-      "四个 Server 共用同一套 Token,建议复制「全部配置」一次性配置完成。每个 Server 对应一个业务领域:法规检索、标准导则、政策问答与省级数据。",
+      "四个 Server 共用同一套 Token,建议复制「全部配置」一次性配置完成。",
   },
   {
     question: "CLI 和 MCP 有什么区别?",

+ 134 - 160
src/pages/home/index.vue

@@ -124,70 +124,32 @@
             </div>
           </div>
           <div class="home-scenes-cards">
-            <div class="home-scenes-card">
+            <div
+              v-for="item in hotServerList"
+              :key="item.id"
+              class="home-scenes-card"
+              :style="item.cardBackground ? { background: item.cardBackground } : undefined"
+            >
               <div class="home-scenes-card-head">
-                <div class="home-scenes-card-icon-box">
-                  <icon-svg name="keyword_search" size="24px" />
+                <div class="home-scenes-card-icon-box" :style="parseCssStyle(item.iconStyle)">
+                  <img
+                    v-if="item.iconUrl"
+                    :src="item.iconUrl"
+                    :alt="item.name"
+                    class="home-scenes-card-icon-img"
+                  />
                 </div>
-                <div class="home-scenes-card-title">法规导则-关键词检索</div>
-              </div>
-              <div class="home-scenes-card-desc">
-                赋能大模型的法规检索工具,让AI助手理解自然语言提问,智能检索相关法规
-              </div>
-              <div class="home-scenes-card-tags">
-                <span>政策法规关键词检索</span>
-                <span>标准导则关键词检索</span>
-              </div>
-              <div class="home-scenes-card-footer">
-                <span class="home-scenes-card-stat">近30天请求 1777万</span>
-                <router-link
-                  class="home-scenes-card-link"
-                  :to="{ name: 'mcpServer', query: { id: '2090355139104735232' } }"
-                >查看详情</router-link>
-              </div>
-            </div>
-            <div class="home-scenes-card">
-              <div class="home-scenes-card-head">
-                <div class="home-scenes-card-icon-box">
-                  <icon-svg name="semantic_retrieval" size="24px" />
-                </div>
-                <div class="home-scenes-card-title">法规导则-语义检索</div>
-              </div>
-              <div class="home-scenes-card-desc">
-                赋能大模型的导则检索工具,让AI助手理解自然语言提问,智能检索相关导则
-              </div>
-              <div class="home-scenes-card-tags">
-                <span>国家环保政策法规知识库语义检索</span>
-                <span>国家环保标准导则知识库语义检索</span>
-                <span>省部环保政策及标准知识库语义检索</span>
-              </div>
-              <div class="home-scenes-card-footer">
-                <span class="home-scenes-card-stat">近30天请求 1777万</span>
-                <router-link
-                  class="home-scenes-card-link"
-                  :to="{ name: 'mcpServer', query: { id: '2090355139108929536' } }"
-                >查看详情</router-link>
-              </div>
-            </div>
-            <div class="home-scenes-card">
-              <div class="home-scenes-card-head">
-                <div class="home-scenes-card-icon-box">
-                  <icon-svg name="question_answer_retrieval" size="24px" />
-                </div>
-                <div class="home-scenes-card-title">问答检索</div>
-              </div>
-              <div class="home-scenes-card-desc">
-                赋能大模型的法规问答工具,让AI助手理解自然语言提问,智能回答
+                <div class="home-scenes-card-title">{{ item.name }}</div>
               </div>
+              <div class="home-scenes-card-desc">{{ item.describe }}</div>
               <div class="home-scenes-card-tags">
-                <span>关键词问答检索</span>
-                <span>全国环保政策问答知识库语义检索</span>
+                <span v-for="tool in item.toolList || []" :key="tool.id">{{ tool.name }}</span>
               </div>
               <div class="home-scenes-card-footer">
-                <span class="home-scenes-card-stat">近30天请求 1777万</span>
+                <span class="home-scenes-card-stat">近30天请求 {{ formatHotRequestCount(item.requestCount30d) }}</span>
                 <router-link
                   class="home-scenes-card-link"
-                  :to="{ name: 'mcpServer', query: { id: '2090355139108929537' } }"
+                  :to="{ name: 'mcpServer', query: { id: item.id } }"
                 >查看详情</router-link>
               </div>
             </div>
@@ -337,15 +299,6 @@
                     <div class="home-guide-tool-step">
                       <div class="home-guide-tool-step-num">1</div>
                       <div class="home-guide-tool-step-copy">
-                        <div class="home-guide-tool-step-title">内置连接器一键接入</div>
-                        <div class="home-guide-tool-step-desc">
-                          打开 WorkBuddy → 连接第三方应用 → 管理连接器 → 找到「环评云助手」→ 一键连接并完成授权
-                        </div>
-                      </div>
-                    </div>
-                    <div class="home-guide-tool-step">
-                      <div class="home-guide-tool-step-num">2</div>
-                      <div class="home-guide-tool-step-copy">
                         <div class="home-guide-tool-step-title">自定义 MCP 配置</div>
                         <div class="home-guide-tool-step-desc">
                           复制右侧 JSON → 对话框发送「这是环评云助手的 MCP 服务 JSON,帮我配置自定义的 mcp」并粘贴
@@ -353,7 +306,7 @@
                       </div>
                     </div>
                     <div class="home-guide-tool-step">
-                      <div class="home-guide-tool-step-num">3</div>
+                      <div class="home-guide-tool-step-num">2</div>
                       <div class="home-guide-tool-step-copy">
                         <div class="home-guide-tool-step-title">验证接入是否成功</div>
                         <div class="home-guide-tool-step-desc">
@@ -684,69 +637,69 @@
           </div>
         </div>
       </section>
-    </div>
-    <section class="home-section home-footer">
-      <div class="home-footer-inner">
-        <div class="home-footer-left">
-          <div class="home-footer-brand">
-            <icon-svg name="systemLogoCircle" size="24px" class="home-footer-brand-icon" />
-            <span class="home-footer-brand-text">环评云助手 MCP · 智能环境法规标准平台</span>
+      <section class="home-section home-footer">
+        <div class="home-guide-inner home-footer-inner">
+          <div class="home-footer-left">
+            <div class="home-footer-brand">
+              <icon-svg name="systemLogoCircle" size="24px" class="home-footer-brand-icon" />
+              <span class="home-footer-brand-text">环评云助手 MCP · 智能环境法规标准平台</span>
+            </div>
+            <p class="home-footer-legal">
+              <span>京公网安备 11010802024562号</span>
+              <span class="home-footer-legal-sep">|</span>
+              <span>Copyright © 2001-2013 Comsenz Inc. All Rights Reserved.</span>
+              <span class="home-footer-legal-sep">|</span>
+              <span>Powered by Discuz! X3.4( 京ICP备17049511号-1 )</span>
+            </p>
           </div>
-          <p class="home-footer-legal">
-            <span>京公网安备 11010802024562号</span>
-            <span class="home-footer-legal-sep">|</span>
-            <span>Copyright © 2001-2013 Comsenz Inc. All Rights Reserved.</span>
-            <span class="home-footer-legal-sep">|</span>
-            <span>Powered by Discuz! X3.4( 京ICP备17049511号-1 )</span>
-          </p>
-        </div>
-        <div class="home-footer-right">
-          <div class="home-footer-icons">
-            <div class="home-footer-icon-item">
-              <icon-svg name="footer_WeChat" size="24px" class="home-footer-icon" />
-              <div class="home-footer-popover">
-                <img src="@/assets/images/service_WeChat.png" alt="云助手客服微信" class="home-footer-popover-img" />
-                <div class="home-footer-popover-sub">云助手客服微信</div>
+          <div class="home-footer-right">
+            <div class="home-footer-icons">
+              <div class="home-footer-icon-item">
+                <icon-svg name="footer_WeChat" size="24px" class="home-footer-icon" />
+                <div class="home-footer-popover">
+                  <img src="@/assets/images/service_WeChat.png" alt="云助手客服微信" class="home-footer-popover-img" />
+                  <div class="home-footer-popover-sub">云助手客服微信</div>
+                </div>
               </div>
-            </div>
-            <div class="home-footer-icon-item">
-              <icon-svg name="footer_phone" size="24px" class="home-footer-icon" />
-              <div class="home-footer-popover">
-                <div class="home-footer-popover-title">官方应用下载</div>
-                <img src="@/assets/images/app_download.png" alt="官方应用下载" class="home-footer-popover-img" />
-                <div class="home-footer-popover-sub">环评云助手APP</div>
+              <div class="home-footer-icon-item">
+                <icon-svg name="footer_phone" size="24px" class="home-footer-icon" />
+                <div class="home-footer-popover">
+                  <div class="home-footer-popover-title">官方应用下载</div>
+                  <img src="@/assets/images/app_download.png" alt="官方应用下载" class="home-footer-popover-img" />
+                  <div class="home-footer-popover-sub">环评云助手APP</div>
+                </div>
               </div>
             </div>
-          </div>
-          <div class="home-footer-links">
-            <a
-              class="home-footer-link"
-              href="https://www.eiacloud.com/lxwmhjzx/550.jhtml"
-              target="_blank"
-              rel="noopener noreferrer"
-            >关于我们</a>
-            <a
-              class="home-footer-link"
-              href="https://www.eiacloud.com/bbs/userAgreement"
-              target="_blank"
-              rel="noopener noreferrer"
-            >用户协议</a>
-            <a
-              class="home-footer-link"
-              href="https://www.eiacloud.com/hpy/lawstandardApp/getDetailYS"
-              target="_blank"
-              rel="noopener noreferrer"
-            >隐私政策</a>
-            <a
-              class="home-footer-link"
-              href="https://www.eiacloud.com/lxwmlxfs/669.jhtml"
-              target="_blank"
-              rel="noopener noreferrer"
-            >联系我们</a>
+            <div class="home-footer-links">
+              <a
+                class="home-footer-link"
+                href="https://www.eiacloud.com/lxwmhjzx/550.jhtml"
+                target="_blank"
+                rel="noopener noreferrer"
+              >关于我们</a>
+              <a
+                class="home-footer-link"
+                href="https://www.eiacloud.com/bbs/userAgreement"
+                target="_blank"
+                rel="noopener noreferrer"
+              >用户协议</a>
+              <a
+                class="home-footer-link"
+                href="https://www.eiacloud.com/hpy/lawstandardApp/getDetailYS"
+                target="_blank"
+                rel="noopener noreferrer"
+              >隐私政策</a>
+              <a
+                class="home-footer-link"
+                href="https://www.eiacloud.com/lxwmlxfs/669.jhtml"
+                target="_blank"
+                rel="noopener noreferrer"
+              >联系我们</a>
+            </div>
           </div>
         </div>
-      </div>
-    </section>
+      </section>
+    </div>
   </div>
 </template>
 
@@ -754,7 +707,8 @@
 import { computed, onMounted, ref } from "vue";
 import { useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getMcpToken, createMcpToken } from "@/api/mcp";
+import { getMcpToken, createMcpToken, getMcpServerHotList } from "@/api/mcp";
+import { requireUserLogin } from "@/utils/auth";
 import globalStore from "@/store/modules/global";
 import demoImgGreen from "@/assets/images/test_instruction_green.jpg";
 import demoImgBlue from "@/assets/images/test_instruction_blue.jpg";
@@ -769,6 +723,42 @@ const demoQuestions = [
 ];
 const demoCaseImages = [demoImgGreen, demoImgBlue, demoImgPurple];
 const activeDemoQuestion = ref(0);
+const hotServerList = ref([]);
+
+const parseCssStyle = (cssText) => {
+  if (!cssText) return {};
+  const style = {};
+  String(cssText)
+    .split(";")
+    .forEach((rule) => {
+      const colonIndex = rule.indexOf(":");
+      if (colonIndex === -1) return;
+      const key = rule.slice(0, colonIndex).trim();
+      const value = rule.slice(colonIndex + 1).trim();
+      if (!key || !value) return;
+      const prop = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
+      style[prop] = value;
+    });
+  return style;
+};
+
+const formatHotRequestCount = (value) => {
+  if (value == null || value === "") return "-";
+  const num = Number(value);
+  if (Number.isNaN(num)) return String(value);
+  return String(num);
+};
+
+const fetchHotServerList = async () => {
+  try {
+    const res = await getMcpServerHotList();
+    hotServerList.value = Array.isArray(res?.data) ? res.data : [];
+  } catch (error) {
+    console.error(error);
+    hotServerList.value = [];
+  }
+};
+
 const guideToken = computed(() => global.mcpToken || "");
 const guideTokenVisible = ref(false);
 const guideTokenMasked = computed(() => {
@@ -778,7 +768,7 @@ const guideTokenMasked = computed(() => {
   }
   return {
     head: token.slice(0, 9),
-    middle: "".repeat(token.length - 15),
+    middle: "*".repeat(token.length - 15),
     tail: token.slice(-6),
   };
 });
@@ -808,6 +798,7 @@ const fetchMcpToken = async () => {
 };
 
 const createToken = async () => {
+  if (!requireUserLogin("请先登录", { redirect: true })) return;
   if (guideToken.value) {
     ElMessage.warning("Token 已存在,无需重复获取");
     return;
@@ -822,6 +813,7 @@ const createToken = async () => {
 
 onMounted(() => {
   fetchMcpToken();
+  fetchHotServerList();
 });
 const guideAccessTab = ref("mcp");
 const guideToolTab = ref("workbuddy");
@@ -829,7 +821,6 @@ const guideTools = [
   { key: "workbuddy", label: "WorkBuddy", icon: true },
   { key: "doubao", label: "豆包", icon: false },
   { key: "qoderwork", label: "Qoderwork", icon: false },
-  { key: "wps", label: "WPS Comate", icon: false },
 ];
 const guideDoubaoContent = {
   alert: "豆包只能通过新建自定义连接器接入,不支持直接粘贴 JSON 配置。",
@@ -853,11 +844,11 @@ const guideDoubaoContent = {
 };
 const guideWpsContent = {
   alert:
-    "仅支持内置连接:WPS Comate 只能通过内置「北大法宝」连接接入,不支持粘贴 JSON 或自定义 MCP 配置。",
+    "仅支持内置连接:WPS Comate 只能通过内置「环评云助手」连接接入,不支持粘贴 JSON 或自定义 MCP 配置。",
   steps: [
     {
       num: 1,
-      title: "打开 WPS Comate → 进入「连接」→ 找到「北大法宝」→ 点击「连接」",
+      title: "打开 WPS Comate → 进入「连接」→ 找到「环评云助手」→ 点击「连接」",
     },
     {
       num: 2,
@@ -1490,18 +1481,6 @@ const guideVerifyCards = [
   border: 1px solid #EEF2F7;
 }
 
-.home-scenes-card:nth-child(1) {
-  background: linear-gradient(180deg, #CAEACE 0%, #FFFFFF 39.2%);
-}
-
-.home-scenes-card:nth-child(2) {
-  background: linear-gradient(180deg, #BBE0FD 0%, #FFFFFF 39.2%);
-}
-
-.home-scenes-card:nth-child(3) {
-  background: linear-gradient(180deg, #DED3FF 0%, #FFFFFF 39.2%);
-}
-
 .home-scenes-card-head {
   display: flex;
   flex-direction: column;
@@ -1518,19 +1497,10 @@ const guideVerifyCards = [
   border-radius: 12px;
 }
 
-.home-scenes-card:nth-child(1) .home-scenes-card-icon-box {
-  background: linear-gradient(135deg, #059669 0%, #10B981 100%);
-  box-shadow: 0px 6px 16px 0px rgba(5, 150, 105, 0.25);
-}
-
-.home-scenes-card:nth-child(2) .home-scenes-card-icon-box {
-  background: linear-gradient(135deg, #3B82F6 0%, #60A5FA 100%);
-  box-shadow: 0px 6px 16px 0px rgba(59, 130, 246, 0.25);
-}
-
-.home-scenes-card:nth-child(3) .home-scenes-card-icon-box {
-  background: linear-gradient(135deg, #7C3AED 0%, #A78BFA 100%);
-  box-shadow: 0px 8px 24px 0px rgba(124, 58, 237, 0.25);
+.home-scenes-card-icon-img {
+  width: 24px;
+  height: 24px;
+  object-fit: contain;
 }
 
 .home-scenes-card-title {
@@ -2056,11 +2026,6 @@ const guideVerifyCards = [
   text-align: left;
 }
 
-.home-guide-token-mask {
-  letter-spacing: -0.55em;
-  margin-right: 0.55em;
-}
-
 .home-guide-token-icons {
   display: flex;
   align-items: center;
@@ -3197,12 +3162,20 @@ const guideVerifyCards = [
 .home-footer {
   background: #F5F7FA;
   border-radius: 0;
-  padding: 70px 280px;
+  padding: 70px 0;
 
   .home-footer-inner {
-    display: flex;
+    box-sizing: border-box;
+    padding: 0 24px;
+    flex-direction: row;
     justify-content: space-between;
     align-items: flex-start;
+    gap: 24px;
+  }
+
+  .home-footer-left {
+    flex: 1;
+    min-width: 0;
   }
 
   .home-footer-left,
@@ -3213,6 +3186,7 @@ const guideVerifyCards = [
   }
 
   .home-footer-right {
+    flex-shrink: 0;
     align-items: flex-end;
   }
 

+ 14 - 7
src/pages/home/mcpIndex.vue

@@ -1,25 +1,32 @@
 <template>
   <div class="mcp-index">
-    <mcp-sider-bar />
-    <div class="mcp-index-content">
-      <router-view />
+    <mcp-breadcrumb />
+    <div class="mcp-index-body">
+      <mcp-sider-bar />
+      <div class="mcp-index-content">
+        <router-view />
+      </div>
     </div>
   </div>
 </template>
 <script setup>
 import McpSiderBar from "@/components/mcpSiderBar.vue";
+import McpBreadcrumb from "@/components/mcpBreadcrumb.vue";
 </script>
 <style scoped lang="less">
 .mcp-index {
-  display: flex;
-  align-items: flex-start;
-  gap: 16px;
   box-sizing: border-box;
   min-height: calc(100vh - 64px);
-  padding: 16px 20px;
+  padding: 16px 20px 24px;
   background: #F7F9FC;
 }
 
+.mcp-index-body {
+  display: flex;
+  align-items: flex-start;
+  gap: 16px;
+}
+
 .mcp-index-content {
   flex: 1;
   min-width: 0;

+ 89 - 21
src/pages/mcp/onlineTest.vue

@@ -42,8 +42,17 @@
           <div class="otp-header-top">
             <div class="otp-header-left">
               <div class="otp-header-title-row">
-                <span class="otp-header-icon">
-                  <icon-svg name="connection_guide" size="16px" />
+                <span
+                  class="otp-header-icon"
+                  :style="currentService.iconStyle ? parseCssStyle(currentService.iconStyle) : undefined"
+                >
+                  <img
+                    v-if="currentService.iconUrl"
+                    :src="currentService.iconUrl"
+                    :alt="currentService.name"
+                    class="otp-header-icon-img"
+                  />
+                  <icon-svg v-else name="connection_guide" size="16px" />
                 </span>
                 <span class="otp-header-title">{{ currentService.name }}</span>
               </div>
@@ -104,19 +113,16 @@
           <div class="otp-info-row otp-info-row--token">
             <span class="otp-info-label">请求头配置</span>
             <div class="otp-token-wrap">
-              <div class="otp-token-box">
+              <div v-if="headerToken" class="otp-token-box">
                 <span class="otp-token-tag">Bearer</span>
                 <span class="otp-token-text">
-                  <template v-if="headerToken">
-                    <template v-if="tokenVisible">{{ headerToken }}</template>
-                    <template v-else>
-                      <span>{{ headerTokenMasked.head }}</span><span class="otp-token-mask">{{
-                        headerTokenMasked.middle }}</span><span>{{ headerTokenMasked.tail }}</span>
-                    </template>
+                  <template v-if="tokenVisible">{{ headerToken }}</template>
+                  <template v-else>
+                    <span>{{ headerTokenMasked.head }}</span><span class="otp-token-mask">{{
+                      headerTokenMasked.middle }}</span><span>{{ headerTokenMasked.tail }}</span>
                   </template>
-                  <template v-else>暂无 Token</template>
                 </span>
-                <div v-if="headerToken" class="otp-token-actions">
+                <div class="otp-token-actions">
                   <button
                     type="button"
                     class="otp-token-action-btn"
@@ -141,6 +147,9 @@
                   </button>
                 </div>
               </div>
+              <button v-else type="button" class="otp-token-get-btn" @click="createToken">
+                获取 Token
+              </button>
               <div v-if="headerToken" class="otp-token-tip">
                 <icon-svg name="" size="12px" class="otp-token-tip-icon" />
                 <span>格式正确</span>
@@ -451,7 +460,9 @@
 import { computed, onMounted, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getMcpServerDetail, getMcpServerList, getMcpToolDetail, callMcpHandle } from "@/api/mcp";
+import { getMcpServerDetail, getMcpServerList, getMcpToolDetail, callMcpHandle, createMcpToken } from "@/api/mcp";
+import { requireUserLogin } from "@/utils/auth";
+import { applyMcpTokenData } from "@/utils/mcpToken";
 import globalStore from "@/store/modules/global";
 
 const route = useRoute();
@@ -530,11 +541,30 @@ const mapServerItem = (item) => {
     connectType: "streamableHttp",
     token: bearer,
     tokenMasked: bearer
-      ? `Bearer ${token.slice(0, 8)}••••••••${token.slice(-8)}`
+      ? `Bearer ${token.slice(0, 8)}${"*".repeat(8)}${token.slice(-8)}`
       : "",
+    iconUrl: item.iconUrl || "",
+    iconStyle: item.iconStyle || "",
   };
 };
 
+const parseCssStyle = (cssText) => {
+  if (!cssText) return {};
+  const style = {};
+  String(cssText)
+    .split(";")
+    .forEach((rule) => {
+      const colonIndex = rule.indexOf(":");
+      if (colonIndex === -1) return;
+      const key = rule.slice(0, colonIndex).trim();
+      const value = rule.slice(colonIndex + 1).trim();
+      if (!key || !value) return;
+      const prop = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
+      style[prop] = value;
+    });
+  return style;
+};
+
 const fetchServerList = async () => {
   try {
     const res = await getMcpServerList();
@@ -744,7 +774,13 @@ const fetchToolsByServerId = async (serverId) => {
   try {
     const detailRes = await getMcpServerDetail(serverId);
     if (requestId !== toolRequestId.value) return;
-    serverDetailUrl.value = detailRes?.data?.url || "";
+    const detailData = detailRes?.data;
+    serverDetailUrl.value = detailData?.url || "";
+    const target = serviceList.value.find((item) => item.id === serverId);
+    if (target && detailData) {
+      target.iconUrl = detailData.iconUrl || "";
+      target.iconStyle = detailData.iconStyle || "";
+    }
     const toolList = Array.isArray(detailRes?.data?.toolList)
       ? detailRes.data.toolList
       : [];
@@ -810,7 +846,7 @@ const headerTokenMasked = computed(() => {
   }
   return {
     head: token.slice(0, 9),
-    middle: "".repeat(token.length - 15),
+    middle: "*".repeat(token.length - 15),
     tail: token.slice(-6),
   };
 });
@@ -1085,6 +1121,20 @@ const copyHeaderToken = () => {
   copyText(`Bearer ${headerToken.value}`);
 };
 
+const createToken = async () => {
+  if (!requireUserLogin("请先登录", { redirect: true })) return;
+  if (headerToken.value) {
+    ElMessage.warning("Token 已存在,无需重复获取");
+    return;
+  }
+  try {
+    const res = await createMcpToken();
+    applyMcpTokenData(res?.data);
+  } catch (error) {
+    console.error(error);
+  }
+};
+
 const runTest = async () => {
   if (!isConnected.value) return;
   const code = currentTool.value?.code;
@@ -1349,6 +1399,12 @@ const runTest = async () => {
   border-radius: 8px;
 }
 
+.otp-header-icon-img {
+  width: 16px;
+  height: 16px;
+  object-fit: contain;
+}
+
 .otp-header-title {
   font-family: PingFang SC, PingFang SC;
   font-weight: 600;
@@ -1587,6 +1643,23 @@ const runTest = async () => {
   border: 1px solid #E2E8F0;
 }
 
+.otp-token-get-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8px 14px;
+  border: none;
+  border-radius: 8px;
+  background: linear-gradient(107deg, #3B82F6 0%, #6366F1 100%);
+  box-shadow: 0px 2px 8px 0px rgba(59, 130, 246, 0.25);
+  font-family: PingFang SC, PingFang SC;
+  font-weight: 500;
+  font-size: 14px;
+  color: #FFFFFF;
+  line-height: 20px;
+  cursor: pointer;
+}
+
 .otp-token-tag {
   flex-shrink: 0;
   font-family: PingFang SC, PingFang SC;
@@ -1609,11 +1682,6 @@ const runTest = async () => {
   word-break: break-all;
 }
 
-.otp-token-mask {
-  letter-spacing: -0.55em;
-  margin-right: 0.55em;
-}
-
 .otp-token-actions {
   display: flex;
   align-items: center;
@@ -1796,7 +1864,7 @@ const runTest = async () => {
   align-items: center;
   justify-content: space-between;
   gap: 12px;
-  padding: 8px 8px 0;
+  padding: 8px 0 0;
   border-bottom: 1px solid #E2E8F0;
 }
 

+ 115 - 20
src/pages/mcp/serverDetail.vue

@@ -1,13 +1,25 @@
 <template>
   <div class="mcp-server-page">
     <div class="mcp-server-main">
-      <div class="mcp-server-card mcp-server-card-overview">
+      <div
+        class="mcp-server-card mcp-server-card-overview"
+        :class="{ 'is-consult-open': consultOpen }"
+      >
         <div class="mcp-server-overview-top-bar"></div>
         <div class="mcp-server-overview-content">
           <div class="mcp-server-header">
             <div class="mcp-server-header-left">
-              <span class="mcp-server-header-icon">
-                <icon-svg name="connection_guide" size="24px" />
+              <span
+                class="mcp-server-header-icon"
+                :style="pageData.iconStyle ? parseCssStyle(pageData.iconStyle) : undefined"
+              >
+                <img
+                  v-if="pageData.iconUrl"
+                  :src="pageData.iconUrl"
+                  :alt="pageData.title"
+                  class="mcp-server-header-icon-img"
+                />
+                <icon-svg v-else name="connection_guide" size="24px" />
               </span>
               <div class="mcp-server-title">{{ pageData.title }}</div>
               <div class="mcp-server-desc">{{ pageData.description }}</div>
@@ -17,7 +29,7 @@
                 <icon-svg name="effect_experience" size="14px" />
                 <span>在线测试</span>
               </button>
-              <div class="mcp-server-consult">
+              <div ref="consultRef" class="mcp-server-consult">
                 <button
                   type="button"
                   class="mcp-server-btn mcp-server-btn-outline"
@@ -181,6 +193,14 @@
             <icon-svg name="key_token" size="14px" />
           </span>
           <div class="mcp-server-token-head-title">我的 Token</div>
+          <button
+            v-if="!mcpToken"
+            type="button"
+            class="mcp-server-token-btn"
+            @click="createToken"
+          >
+            获取 Token
+          </button>
         </div>
         <div v-if="mcpToken" class="mcp-server-token-bar">
           <span class="mcp-server-token-tag">Bearer</span>
@@ -200,7 +220,6 @@
             <icon-svg name="token_copy" size="14px" class="mcp-server-token-icon" @click="copyText(mcpToken)" />
           </div>
         </div>
-        <div v-else class="mcp-server-token-empty">暂无 Token</div>
       </div>
 
       <div class="mcp-server-side-card mcp-server-side-card-connect">
@@ -281,10 +300,12 @@
   </div>
 </template>
 <script setup>
-import { computed, ref, watch } from "vue";
+import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
 import { useRoute, useRouter } from "vue-router";
 import { ElMessage } from "element-plus";
-import { getMcpServerDetail, getMcpServerList, getMcpToolDetail } from "@/api/mcp";
+import { getMcpServerDetail, getMcpServerList, getMcpToolDetail, createMcpToken } from "@/api/mcp";
+import { requireUserLogin } from "@/utils/auth";
+import { applyMcpTokenData } from "@/utils/mcpToken";
 import globalStore from "@/store/modules/global";
 
 defineOptions({
@@ -302,6 +323,22 @@ const toolDetail = ref(null);
 const selectedToolIndex = ref(0);
 const tokenVisible = ref(false);
 const consultOpen = ref(false);
+const consultRef = ref(null);
+
+const handleConsultOutsideClick = (event) => {
+  if (!consultOpen.value) return;
+  if (consultRef.value?.contains(event.target)) return;
+  consultOpen.value = false;
+};
+
+onMounted(() => {
+  document.addEventListener("click", handleConsultOutsideClick);
+});
+
+onBeforeUnmount(() => {
+  document.removeEventListener("click", handleConsultOutsideClick);
+});
+
 const serverRequestId = ref(0);
 const toolRequestId = ref(0);
 
@@ -327,6 +364,23 @@ const formatMetricValue = (value, suffix = "") => {
   return `${num}${suffix}`;
 };
 
+const parseCssStyle = (cssText) => {
+  if (!cssText) return {};
+  const style = {};
+  String(cssText)
+    .split(";")
+    .forEach((rule) => {
+      const colonIndex = rule.indexOf(":");
+      if (colonIndex === -1) return;
+      const key = rule.slice(0, colonIndex).trim();
+      const value = rule.slice(colonIndex + 1).trim();
+      if (!key || !value) return;
+      const prop = key.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
+      style[prop] = value;
+    });
+  return style;
+};
+
 const normalizeJsObjectLiteral = (sample) => {
   let text = String(sample).trim();
   if (!text) return "";
@@ -429,6 +483,8 @@ const mapServerDetail = (data) => {
     description: data.describe || "",
     serverUrl: data.url || "",
     code: data.code || "",
+    iconUrl: data.iconUrl || "",
+    iconStyle: data.iconStyle || "",
     tags,
     metrics: [
       {
@@ -477,7 +533,7 @@ const tokenMasked = computed(() => {
   }
   return {
     head: token.slice(0, 9),
-    middle: "".repeat(token.length - 15),
+    middle: "*".repeat(token.length - 15),
     tail: token.slice(-6),
   };
 });
@@ -672,12 +728,27 @@ const navigateTo = (key) => {
 };
 
 const goOnlineTest = () => {
+  if (!requireUserLogin()) return;
   const id = route.query.id || serverDetail.value?.id;
   router.push({
     name: "mcpOnlineTest",
     query: id ? { id: String(id) } : {},
   });
 };
+
+const createToken = async () => {
+  if (!requireUserLogin("请先登录", { redirect: true })) return;
+  if (mcpToken.value) {
+    ElMessage.warning("Token 已存在,无需重复获取");
+    return;
+  }
+  try {
+    const res = await createMcpToken();
+    applyMcpTokenData(res?.data);
+  } catch (error) {
+    console.error(error);
+  }
+};
 </script>
 <style scoped lang="less">
 .mcp-server-page {
@@ -710,12 +781,18 @@ const goOnlineTest = () => {
 
 .mcp-server-card-overview {
   padding: 0;
-  overflow: hidden;
+  overflow: visible;
+}
+
+.mcp-server-card-overview.is-consult-open {
+  position: relative;
+  z-index: 30;
 }
 
 .mcp-server-overview-top-bar {
   height: 4px;
-  background: linear-gradient(270deg, #3B82F6 0%, #6366F1 50%, #8B5CF6 100%);
+  border-radius: 8px 8px 0 0;
+  background: linear-gradient(90deg, #3B82F6 0%, #6366F1 50%, #8B5CF6 100%);
 }
 
 .mcp-server-overview-content {
@@ -881,6 +958,12 @@ const goOnlineTest = () => {
   color: #ffffff;
 }
 
+.mcp-server-header-icon-img {
+  width: 24px;
+  height: 24px;
+  object-fit: contain;
+}
+
 .mcp-server-title {
   grid-column: 2;
   grid-row: 1;
@@ -951,7 +1034,7 @@ const goOnlineTest = () => {
   position: absolute;
   top: calc(100% + 8px);
   right: 0;
-  z-index: 20;
+  z-index: 31;
   display: flex;
   flex-direction: column;
   align-items: center;
@@ -1211,6 +1294,8 @@ const goOnlineTest = () => {
 }
 
 .mcp-server-params-type--string {
+  box-sizing: border-box;
+  min-width: 52px;
   background: #EDE9FE;
   color: #8B5CF6;
 }
@@ -1222,6 +1307,8 @@ const goOnlineTest = () => {
 
 .mcp-server-params-type--number,
 .mcp-server-params-type--integer {
+  box-sizing: border-box;
+  min-width: 52px;
   background: #EDE9FE;
   color: #8B5CF6;
 }
@@ -1629,6 +1716,8 @@ const goOnlineTest = () => {
 }
 
 .mcp-server-token-head-title {
+  flex: 1;
+  min-width: 0;
   font-family: PingFang SC, PingFang SC;
   font-weight: 600;
   font-size: 16px;
@@ -1674,11 +1763,6 @@ const goOnlineTest = () => {
   word-break: break-all;
 }
 
-.mcp-server-token-mask {
-  letter-spacing: -0.55em;
-  margin-right: 0.55em;
-}
-
 .mcp-server-token-icons {
   display: flex;
   align-items: center;
@@ -1694,11 +1778,22 @@ const goOnlineTest = () => {
   cursor: pointer;
 }
 
-.mcp-server-token-empty {
-  margin-top: 12px;
-  font-size: 13px;
-  color: #94a3b8;
+.mcp-server-token-btn {
+  flex-shrink: 0;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  padding: 8px 14px;
+  border: none;
+  border-radius: 8px;
+  background: linear-gradient(107deg, #3B82F6 0%, #6366F1 100%);
+  box-shadow: 0px 2px 8px 0px rgba(59, 130, 246, 0.25);
+  font-family: PingFang SC, PingFang SC;
+  font-weight: 500;
+  font-size: 14px;
+  color: #FFFFFF;
   line-height: 20px;
+  cursor: pointer;
 }
 
 .mcp-server-side-card-related {

+ 8 - 13
src/router/index.js

@@ -1,10 +1,12 @@
 import { createRouter, createWebHistory } from "vue-router";
-import { getCookie } from "@/utils/auth";
-import globalStore from "@/store/modules/global";
+import { isUserLoggedIn } from "@/utils/auth";
 
 /** 未登录可访问的路由 */
 const AUTH_WHITE_LIST = new Set(["login", "wechatLogin"]);
 
+/** 必须登录才可访问的路由 */
+const AUTH_REQUIRED_ROUTES = new Set(["mcpOnlineTest", "mcpAuth"]);
+
 const routes = [
   {
     path: "/",
@@ -92,16 +94,11 @@ const router = createRouter({
   },
 });
 
-const isLoggedIn = () => {
-  const global = globalStore();
-  return !!(getCookie("WEBSID") || global.WEBSID);
-};
+const isLoggedIn = () => isUserLoggedIn();
 
 router.beforeEach((to, _from, next) => {
-  const loggedIn = isLoggedIn();
-
   if (AUTH_WHITE_LIST.has(to.name)) {
-    if (loggedIn && to.name === "login") {
+    if (isLoggedIn() && to.name === "login") {
       const redirect = to.query.redirect;
       if (redirect) {
         next(String(redirect));
@@ -114,12 +111,10 @@ router.beforeEach((to, _from, next) => {
     return;
   }
 
-  if (!loggedIn) {
+  if (AUTH_REQUIRED_ROUTES.has(to.name) && !isLoggedIn()) {
     next({
       name: "login",
-      query: {
-        redirect: to.fullPath,
-      },
+      query: { redirect: to.fullPath },
     });
     return;
   }

+ 27 - 0
src/utils/auth.js

@@ -1,4 +1,6 @@
 import Cookies from "js-cookie";
+import { ElMessage } from "element-plus";
+import globalStore from "@/store/modules/global";
 
 const TokenKey = "token";
 const rememberKey = "remInfo";
@@ -21,6 +23,31 @@ const rememberKey = "remInfo";
 export function getCookie(name) {
   return Cookies.get(name) || null; // 如果Cookie不存在,返回null
 }
+
+export function isUserLoggedIn() {
+  const global = globalStore();
+  return !!(getCookie("WEBSID") || global.WEBSID);
+}
+
+/**
+ * @param {string} message 提示文案
+ * @param {{ redirect?: boolean }} [options] redirect 为 true 时跳转登录页,登录后回到当前页
+ */
+export function requireUserLogin(message = "请先登录", options = {}) {
+  if (isUserLoggedIn()) return true;
+  ElMessage.warning(message);
+  if (options.redirect) {
+    import("@/router").then((mod) => {
+      const router = mod.default;
+      const redirect = router.currentRoute.value.fullPath || "/";
+      router.push({
+        name: "login",
+        query: { redirect },
+      });
+    });
+  }
+  return false;
+}
 // 获取localStorage里面缓存的对象
 function getLocalObj(key) {
   const storeInfo = localStorage.getItem(key);