Bläddra i källkod

移动端适配

moki 4 veckor sedan
förälder
incheckning
57850c3a14
4 ändrade filer med 151 tillägg och 8 borttagningar
  1. 25 0
      src/components/PortStatusCard.vue
  2. 76 5
      src/layouts/BasicLayout.vue
  3. 30 0
      src/views/Dashboard.vue
  4. 20 3
      src/views/MqttConfig.vue

+ 25 - 0
src/components/PortStatusCard.vue

@@ -165,4 +165,29 @@ const formattedTime = computed(() => {
     box-shadow: 0 0 2px #ff4d4f, inset 0 0 1px rgba(255, 77, 79, 0.1);
   }
 }
+
+@media (max-width: 767px) {
+  .card-header {
+    padding: 8px 12px;
+    font-size: 12px;
+  }
+
+  .card-body {
+    padding: 14px 12px;
+  }
+
+  .status-icon {
+    font-size: 40px;
+    margin-bottom: 8px;
+  }
+
+  .status-tag {
+    font-size: 12px;
+    padding: 1px 8px;
+  }
+
+  .time {
+    font-size: 11px;
+  }
+}
 </style>

+ 76 - 5
src/layouts/BasicLayout.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, computed } from 'vue'
+import { ref, computed, onMounted, onUnmounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import {
   DashboardOutlined,
@@ -8,6 +8,7 @@ import {
   BulbFilled,
   FullscreenOutlined,
   FullscreenExitOutlined,
+  MenuOutlined,
 } from '@ant-design/icons-vue'
 import { useTheme } from '@/composables/useTheme'
 
@@ -15,22 +16,38 @@ const router = useRouter()
 const route = useRoute()
 const collapsed = ref(false)
 const isFullscreen = ref(false)
+const drawerOpen = ref(false)
+const isMobile = ref(window.innerWidth < 768)
 const { theme, toggleTheme } = useTheme()
 
 const selectedKeys = computed(() => [route.path])
 
 function onMenuClick({ key }: { key: string }) {
   router.push(key)
+  if (isMobile.value) drawerOpen.value = false
 }
 
 function toggleFullscreen() {
   isFullscreen.value = !isFullscreen.value
 }
+
+function onResize() {
+  isMobile.value = window.innerWidth < 768
+}
+
+onMounted(() => window.addEventListener('resize', onResize))
+onUnmounted(() => window.removeEventListener('resize', onResize))
 </script>
 
 <template>
   <a-layout style="min-height: 100vh">
-    <a-layout-sider v-if="!isFullscreen" v-model:collapsed="collapsed" collapsible theme="dark">
+    <!-- 桌面端侧边栏 -->
+    <a-layout-sider
+      v-if="!isFullscreen && !isMobile"
+      v-model:collapsed="collapsed"
+      collapsible
+      theme="dark"
+    >
       <div class="logo">
         <ApiOutlined style="font-size: 24px; color: #1890ff" />
         <span v-if="!collapsed" class="logo-text">AI Monitor</span>
@@ -51,9 +68,51 @@ function toggleFullscreen() {
         </a-menu-item>
       </a-menu>
     </a-layout-sider>
+
+    <!-- 移动端抽屉菜单 -->
+    <a-drawer
+      v-if="isMobile"
+      :open="drawerOpen"
+      placement="left"
+      :width="220"
+      @close="drawerOpen = false"
+      :body-style="{ padding: 0, background: '#141414' }"
+      :header-style="{ display: 'none' }"
+    >
+      <div class="logo">
+        <ApiOutlined style="font-size: 24px; color: #1890ff" />
+        <span class="logo-text">AI Monitor</span>
+      </div>
+      <a-menu
+        theme="dark"
+        mode="inline"
+        :selected-keys="selectedKeys"
+        @click="onMenuClick"
+      >
+        <a-menu-item key="/dashboard">
+          <DashboardOutlined />
+          <span>仪表盘</span>
+        </a-menu-item>
+        <a-menu-item key="/mqtt">
+          <ApiOutlined />
+          <span>MQTT 配置</span>
+        </a-menu-item>
+      </a-menu>
+    </a-drawer>
+
     <a-layout>
       <a-layout-header v-if="!isFullscreen" class="header">
-        <span class="header-title">AI Status Monitor</span>
+        <div class="header-left">
+          <a-button
+            v-if="isMobile"
+            type="text"
+            class="icon-btn"
+            @click="drawerOpen = true"
+          >
+            <MenuOutlined />
+          </a-button>
+          <span class="header-title">AI Status Monitor</span>
+        </div>
         <div class="header-actions">
           <a-tooltip :title="isFullscreen ? '退出全屏' : '全屏显示'">
             <a-button type="text" class="icon-btn" @click="toggleFullscreen">
@@ -102,13 +161,19 @@ function toggleFullscreen() {
 
 .header {
   background: var(--header-bg);
-  padding: 0 24px;
+  padding: 0 16px;
   display: flex;
   align-items: center;
   justify-content: space-between;
   border-bottom: 1px solid var(--border-color);
 }
 
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
 .header-title {
   color: var(--text-color);
   font-size: 16px;
@@ -134,7 +199,7 @@ function toggleFullscreen() {
 
 .content-fullscreen {
   margin: 0;
-  padding: 12px 24px;
+  padding: 12px 16px;
   min-height: 100vh;
   background: var(--bg);
 }
@@ -146,4 +211,10 @@ function toggleFullscreen() {
   margin-bottom: 8px;
   border-bottom: 1px solid var(--border-color);
 }
+
+@media (max-width: 767px) {
+  .content {
+    margin: 12px;
+  }
+}
 </style>

+ 30 - 0
src/views/Dashboard.vue

@@ -127,4 +127,34 @@ onUnmounted(removeListener)
   grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
   gap: 16px;
 }
+
+@media (max-width: 767px) {
+  .stats-bar {
+    flex-wrap: wrap;
+    gap: 16px;
+    padding: 16px;
+  }
+
+  .stat-item {
+    flex: 1 1 calc(33% - 16px);
+    min-width: 80px;
+  }
+
+  .stat-num {
+    font-size: 28px;
+  }
+
+  .ws-status {
+    width: 100%;
+    justify-content: center;
+    margin-left: 0;
+    padding-top: 8px;
+    border-top: 1px solid var(--border-color);
+  }
+
+  .port-grid {
+    grid-template-columns: repeat(2, 1fr);
+    gap: 12px;
+  }
+}
 </style>

+ 20 - 3
src/views/MqttConfig.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, reactive, onMounted } from 'vue'
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
 import { message, Modal } from 'ant-design-vue'
 import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons-vue'
 import { getMqttList, createMqtt, updateMqtt, deleteMqtt } from '@/api/mqtt'
@@ -9,6 +9,7 @@ const loading = ref(false)
 const list = ref<MqttConfig[]>([])
 const drawerVisible = ref(false)
 const editingId = ref<number | null>(null)
+const isMobile = ref(window.innerWidth < 768)
 
 const form = reactive<MqttConfigForm>({
   broker: '',
@@ -93,7 +94,15 @@ function onDelete(record: MqttConfig) {
   })
 }
 
-onMounted(fetchList)
+function onResize() {
+  isMobile.value = window.innerWidth < 768
+}
+
+onMounted(() => {
+  fetchList()
+  window.addEventListener('resize', onResize)
+})
+onUnmounted(() => window.removeEventListener('resize', onResize))
 </script>
 
 <template>
@@ -111,6 +120,7 @@ onMounted(fetchList)
       :loading="loading"
       row-key="id"
       :pagination="false"
+      :scroll="isMobile ? { x: 600 } : undefined"
     >
       <template #bodyCell="{ column, record }">
         <template v-if="column.key === 'enabled'">
@@ -133,7 +143,7 @@ onMounted(fetchList)
       :title="editingId !== null ? '编辑 MQTT 配置' : '新建 MQTT 配置'"
       :open="drawerVisible"
       @close="drawerVisible = false"
-      :width="480"
+      :width="isMobile ? '100%' : 480"
     >
       <a-form layout="vertical">
         <a-form-item label="Broker 地址" required>
@@ -180,4 +190,11 @@ onMounted(fetchList)
   font-size: 16px;
   font-weight: 500;
 }
+
+@media (max-width: 767px) {
+  .page-header {
+    margin-bottom: 16px;
+    font-size: 14px;
+  }
+}
 </style>