Parcourir la source

添加vconsole调试工具

wuyi il y a 2 mois
Parent
commit
c59844d7e2
5 fichiers modifiés avec 509 ajouts et 3 suppressions
  1. 1 0
      package.json
  2. 2 1
      src/App.vue
  3. 319 0
      src/components/VConsoleToggle.vue
  4. 5 2
      src/main.ts
  5. 182 0
      src/utils/vconsole.ts

+ 1 - 0
package.json

@@ -17,6 +17,7 @@
     "pinia": "^2.1.0",
     "primeicons": "^7.0.0",
     "primevue": "^4.3.9",
+    "vconsole": "^3.15.1",
     "vue": "^3.5.18",
     "vue-router": "^4.5.1",
     "zod": "^4.1.9"

+ 2 - 1
src/App.vue

@@ -1,10 +1,11 @@
 <script setup lang="ts">
 import PWAUpdatePrompt from '@/components/PWAUpdatePrompt.vue';
+import VConsoleToggle from '@/components/VConsoleToggle.vue';
 </script>
 
 <template>
   <router-view />
-  
+  <VConsoleToggle />
   <!-- PWA 更新通知 -->
   <PWAUpdatePrompt />
 </template>

+ 319 - 0
src/components/VConsoleToggle.vue

@@ -0,0 +1,319 @@
+<template>
+  <div v-if="showToggle" class="vconsole-toggle">
+    <!-- 悬浮按钮 -->
+    <button
+      class="toggle-btn"
+      @click="handleToggle"
+      @touchstart="handleTouchStart"
+      @touchmove="handleTouchMove"
+      @touchend="handleTouchEnd"
+      :style="buttonStyle"
+    >
+      <svg
+        v-if="!isDragging"
+        class="icon"
+        viewBox="0 0 24 24"
+        fill="none"
+        xmlns="http://www.w3.org/2000/svg"
+      >
+        <path
+          d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"
+          fill="currentColor"
+        />
+      </svg>
+      <span v-else class="drag-icon">✋</span>
+    </button>
+
+    <!-- 快捷菜单 -->
+    <transition name="menu">
+      <div v-if="showMenu" class="quick-menu">
+        <button @click="toggleVConsole" class="menu-item">
+          {{ isVConsoleVisible ? '隐藏' : '显示' }} vConsole
+        </button>
+        <button @click="clearConsole" class="menu-item">清空日志</button>
+        <button @click="togglePersist" class="menu-item">
+          {{ isPersistent ? '关闭' : '开启' }}持久化
+        </button>
+        <button @click="showSystemInfo" class="menu-item">系统信息</button>
+        <button @click="closeMenu" class="menu-item close">关闭</button>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted, onUnmounted } from 'vue';
+import { 
+  toggleVConsole as toggle, 
+  enableVConsole, 
+  disableVConsole 
+} from '@/utils/vconsole';
+
+// 是否显示切换按钮
+const showToggle = ref(false);
+const showMenu = ref(false);
+const isVConsoleVisible = ref(false);
+const isPersistent = ref(false);
+const isDragging = ref(false);
+
+// 按钮位置
+const buttonPosition = ref({ x: 10, y: 200 });
+const buttonStyle = ref({
+  right: '10px',
+  top: '200px',
+});
+
+// 触摸相关
+let touchStartX = 0;
+let touchStartY = 0;
+let buttonStartX = 0;
+let buttonStartY = 0;
+let longPressTimer: any = null;
+
+// 检查是否应该显示切换按钮
+const checkShouldShow = () => {
+  // 开发环境显示
+  if (import.meta.env.DEV) {
+    showToggle.value = true;
+    return;
+  }
+  
+  // URL参数控制
+  const urlParams = new URLSearchParams(window.location.search);
+  if (urlParams.get('debug') === 'true' || urlParams.get('vconsole') === 'true') {
+    showToggle.value = true;
+    return;
+  }
+  
+  // localStorage 控制
+  if (localStorage.getItem('vconsole_enabled') === 'true') {
+    showToggle.value = true;
+    isPersistent.value = true;
+    return;
+  }
+};
+
+// 切换 vConsole 显示
+const toggleVConsole = () => {
+  toggle();
+  isVConsoleVisible.value = !isVConsoleVisible.value;
+  closeMenu();
+};
+
+// 清空控制台
+const clearConsole = () => {
+  console.clear();
+  console.log('[vConsole] 日志已清空');
+  closeMenu();
+};
+
+// 切换持久化
+const togglePersist = () => {
+  if (isPersistent.value) {
+    disableVConsole();
+    isPersistent.value = false;
+    console.log('[vConsole] 持久化已关闭');
+  } else {
+    enableVConsole();
+    isPersistent.value = true;
+    console.log('[vConsole] 持久化已开启');
+  }
+  closeMenu();
+};
+
+// 显示系统信息
+const showSystemInfo = () => {
+  const info = {
+    '用户代理': navigator.userAgent,
+    '平台': navigator.platform,
+    '语言': navigator.language,
+    '屏幕尺寸': `${screen.width}x${screen.height}`,
+    '视口尺寸': `${window.innerWidth}x${window.innerHeight}`,
+    '像素比': window.devicePixelRatio,
+    '在线状态': navigator.onLine ? '在线' : '离线',
+    '网络类型': (navigator as any).connection?.effectiveType || '未知',
+  };
+  
+  console.group('📱 系统信息');
+  Object.entries(info).forEach(([key, value]) => {
+    console.log(`${key}:`, value);
+  });
+  console.groupEnd();
+  
+  closeMenu();
+};
+
+// 处理按钮点击
+const handleToggle = () => {
+  if (!isDragging.value) {
+    showMenu.value = !showMenu.value;
+  }
+};
+
+// 关闭菜单
+const closeMenu = () => {
+  showMenu.value = false;
+};
+
+// 触摸开始
+const handleTouchStart = (e: TouchEvent) => {
+  const touch = e.touches[0];
+  touchStartX = touch.clientX;
+  touchStartY = touch.clientY;
+  
+  const rect = (e.target as HTMLElement).getBoundingClientRect();
+  buttonStartX = window.innerWidth - rect.right;
+  buttonStartY = rect.top;
+  
+  // 长按开始拖动
+  longPressTimer = setTimeout(() => {
+    isDragging.value = true;
+    closeMenu();
+  }, 500);
+};
+
+// 触摸移动
+const handleTouchMove = (e: TouchEvent) => {
+  if (!isDragging.value) return;
+  
+  e.preventDefault();
+  const touch = e.touches[0];
+  const deltaX = touchStartX - touch.clientX;
+  const deltaY = touch.clientY - touchStartY;
+  
+  const newRight = Math.max(10, Math.min(window.innerWidth - 60, buttonStartX + deltaX));
+  const newTop = Math.max(50, Math.min(window.innerHeight - 60, buttonStartY + deltaY));
+  
+  buttonStyle.value = {
+    right: `${newRight}px`,
+    top: `${newTop}px`,
+  };
+};
+
+// 触摸结束
+const handleTouchEnd = () => {
+  clearTimeout(longPressTimer);
+  setTimeout(() => {
+    isDragging.value = false;
+  }, 100);
+};
+
+// 键盘快捷键
+const handleKeyboard = (e: KeyboardEvent) => {
+  // Ctrl + Shift + D 切换 vConsole
+  if (e.ctrlKey && e.shiftKey && e.key === 'D') {
+    e.preventDefault();
+    toggleVConsole();
+  }
+};
+
+onMounted(() => {
+  checkShouldShow();
+  document.addEventListener('keydown', handleKeyboard);
+});
+
+onUnmounted(() => {
+  document.removeEventListener('keydown', handleKeyboard);
+  clearTimeout(longPressTimer);
+});
+</script>
+
+<style scoped>
+.vconsole-toggle {
+  position: fixed;
+  z-index: 10000;
+}
+
+.toggle-btn {
+  position: fixed;
+  width: 50px;
+  height: 50px;
+  border-radius: 50%;
+  background: rgba(67, 207, 124, 0.9);
+  border: 2px solid rgba(255, 255, 255, 0.3);
+  color: white;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
+  cursor: pointer;
+  transition: all 0.3s ease;
+  -webkit-tap-highlight-color: transparent;
+  user-select: none;
+}
+
+.toggle-btn:active {
+  transform: scale(0.95);
+}
+
+.toggle-btn .icon {
+  width: 24px;
+  height: 24px;
+}
+
+.toggle-btn .drag-icon {
+  font-size: 20px;
+}
+
+.quick-menu {
+  position: fixed;
+  right: 70px;
+  background: rgba(0, 0, 0, 0.9);
+  border-radius: 8px;
+  padding: 8px 0;
+  min-width: 150px;
+  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(10px);
+}
+
+.menu-item {
+  display: block;
+  width: 100%;
+  padding: 10px 16px;
+  text-align: left;
+  background: none;
+  border: none;
+  color: white;
+  font-size: 14px;
+  cursor: pointer;
+  transition: background 0.2s;
+}
+
+.menu-item:hover {
+  background: rgba(255, 255, 255, 0.1);
+}
+
+.menu-item.close {
+  border-top: 1px solid rgba(255, 255, 255, 0.1);
+  margin-top: 8px;
+  padding-top: 12px;
+  color: #ff6b6b;
+}
+
+/* 过渡动画 */
+.menu-enter-active,
+.menu-leave-active {
+  transition: all 0.3s ease;
+}
+
+.menu-enter-from,
+.menu-leave-to {
+  opacity: 0;
+  transform: translateX(20px);
+}
+
+/* 移动端适配 */
+@media (max-width: 768px) {
+  .toggle-btn {
+    width: 45px;
+    height: 45px;
+  }
+  
+  .quick-menu {
+    right: 10px;
+    top: 60px;
+    left: auto;
+    min-width: 140px;
+  }
+}
+</style>

+ 5 - 2
src/main.ts

@@ -5,13 +5,16 @@ import { createPinia } from "pinia";
 import App from "./App.vue";
 import router from "./router";
 import { initPWA } from "./utils/pwa";
+import { initVConsole } from "@/utils/vconsole";
 
 const app = createApp(App);
 
 app.use(router);
 app.use(createPinia());
 
-app.mount("#app");
-
+// 初始化 vConsole(内部已做环境与参数判断)
+initVConsole();
 // 初始化 PWA 功能
 initPWA();
+
+app.mount("#app");

+ 182 - 0
src/utils/vconsole.ts

@@ -0,0 +1,182 @@
+/**
+ * vConsole 移动端调试工具
+ * 用于在移动设备上调试应用
+ */
+
+import VConsole from "vconsole";
+
+let vConsoleInstance: VConsole | null = null;
+
+/**
+ * 检查是否应该启用 vConsole
+ * @returns {boolean} 是否启用
+ */
+const shouldEnableVConsole = (): boolean => {
+  // 1. 开发环境自动启用
+  if (import.meta.env.DEV) {
+    return true;
+  }
+
+  // 2. 通过 URL 参数启用(生产环境调试)
+  const urlParams = new URLSearchParams(window.location.search);
+  if (
+    urlParams.get("debug") === "true" ||
+    urlParams.get("vconsole") === "true"
+  ) {
+    return true;
+  }
+
+  // 3. 通过 localStorage 启用(持久化调试模式)
+  if (localStorage.getItem("vconsole_enabled") === "true") {
+    return true;
+  }
+
+  // 4. 检测是否为移动设备(可选:仅在移动设备上启用)
+  const isMobile =
+    /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
+      navigator.userAgent
+    );
+
+  // 如果是移动设备且有特殊标记
+  if (isMobile && sessionStorage.getItem("debug_mode") === "true") {
+    return true;
+  }
+
+  // 5. 特定域名或环境启用
+  const hostname = window.location.hostname;
+  if (hostname.includes("test") || hostname.includes("staging")) {
+    return true;
+  }
+
+  return false;
+};
+
+/**
+ * 初始化 vConsole
+ */
+export const initVConsole = (): void => {
+  if (shouldEnableVConsole() && !vConsoleInstance) {
+    try {
+      vConsoleInstance = new VConsole({
+        // 配置选项
+        theme: "light", // 'light' | 'dark' | 'auto'
+        disableLogScrolling: false, // 是否禁用日志自动滚动
+
+        // 默认展示的面板
+        defaultPlugins: ["system", "network", "element", "storage"],
+
+        // 日志最大条数
+        maxLogNumber: 1000,
+
+        // 按钮位置
+        // onReady: () => {
+        //   // 自定义按钮位置
+        //   const vconsoleBtn = document.querySelector('.vc-switch') as HTMLElement;
+        //   if (vconsoleBtn) {
+        //     vconsoleBtn.style.right = '10px';
+        //     vconsoleBtn.style.bottom = '100px';
+        //   }
+        // }
+      });
+
+      console.log("[vConsole] 调试工具已启动");
+
+      // 添加自定义日志
+      console.log("[System]", {
+        userAgent: navigator.userAgent,
+        platform: navigator.platform,
+        language: navigator.language,
+        screenSize: `${screen.width}x${screen.height}`,
+        viewport: `${window.innerWidth}x${window.innerHeight}`,
+        pixelRatio: window.devicePixelRatio,
+      });
+
+      // 检测浏览器类型并输出
+      detectBrowser();
+    } catch (error) {
+      console.error("[vConsole] 初始化失败:", error);
+    }
+  }
+};
+
+/**
+ * 销毁 vConsole 实例
+ */
+export const destroyVConsole = (): void => {
+  if (vConsoleInstance) {
+    vConsoleInstance.destroy();
+    vConsoleInstance = null;
+    console.log("[vConsole] 调试工具已关闭");
+  }
+};
+
+/**
+ * 切换 vConsole 显示/隐藏
+ */
+export const toggleVConsole = (): void => {
+  if (vConsoleInstance) {
+    // @ts-ignore
+    if (vConsoleInstance.isShow) {
+      // @ts-ignore
+      vConsoleInstance.hide();
+    } else {
+      // @ts-ignore
+      vConsoleInstance.show();
+    }
+  } else {
+    initVConsole();
+  }
+};
+
+/**
+ * 启用 vConsole(保存到 localStorage)
+ */
+export const enableVConsole = (): void => {
+  localStorage.setItem("vconsole_enabled", "true");
+  initVConsole();
+};
+
+/**
+ * 禁用 vConsole(从 localStorage 移除)
+ */
+export const disableVConsole = (): void => {
+  localStorage.removeItem("vconsole_enabled");
+  destroyVConsole();
+};
+
+/**
+ * 检测浏览器类型
+ */
+const detectBrowser = (): void => {
+  const ua = navigator.userAgent.toLowerCase();
+  const browserInfo: any = {
+    isIOS: /iphone|ipad|ipod/.test(ua),
+    isAndroid: /android/.test(ua),
+    isWechat: /micromessenger/.test(ua),
+    isQQ: /qq\//.test(ua),
+    isUC: /ucbrowser|ubrowser|ucweb/.test(ua),
+    isQuark: /quark/.test(ua),
+    isSafari: /safari/.test(ua) && !/chrome/.test(ua),
+    isChrome: /chrome/.test(ua) && !/edg/.test(ua),
+    isFirefox: /firefox/.test(ua),
+    isEdge: /edg/.test(ua),
+  };
+
+  const detected = Object.entries(browserInfo)
+    .filter(([_, value]) => value)
+    .map(([key]) => key.replace("is", ""));
+
+  console.log("[Browser]", detected.join(", "));
+  console.log("[Browser Detail]", browserInfo);
+};
+
+// 导出到全局,方便在控制台手动调用
+if (typeof window !== "undefined") {
+  (window as any).vConsoleUtils = {
+    init: initVConsole,
+    destroy: destroyVConsole,
+    toggle: toggleVConsole,
+    enable: enableVConsole,
+    disable: disableVConsole,
+  };
+}