Ver Fonte

手机版本

liuq há 2 semanas atrás
pai
commit
9513551e6e

+ 145 - 14
frontend/src/App.vue

@@ -1,14 +1,16 @@
 <script setup lang="ts">
 <script setup lang="ts">
+import { ref, computed } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { useRoute, useRouter } from 'vue-router'
 import { useAuthStore } from './stores/auth'
 import { useAuthStore } from './stores/auth'
-import { computed } from 'vue'
+import { useBreakpoint } from './composables/useBreakpoint'
 
 
 const route = useRoute()
 const route = useRoute()
 const router = useRouter()
 const router = useRouter()
 const authStore = useAuthStore()
 const authStore = useAuthStore()
+const { isNarrow } = useBreakpoint(768)
+const drawerVisible = ref(false)
 
 
 const showLayout = computed(() => {
 const showLayout = computed(() => {
-  // Check meta first, default to true unless explicitly false (if not login)
   if (route.name === 'login') return false
   if (route.name === 'login') return false
   return route.meta.showLayout !== false
   return route.meta.showLayout !== false
 })
 })
@@ -25,6 +27,11 @@ const handleMenuSelect = (index: string) => {
   } else {
   } else {
     router.push(index)
     router.push(index)
   }
   }
+  drawerVisible.value = false
+}
+
+const openDrawer = () => {
+  drawerVisible.value = true
 }
 }
 </script>
 </script>
 
 
@@ -32,14 +39,27 @@ const handleMenuSelect = (index: string) => {
   <div v-if="showLayout" class="app-container">
   <div v-if="showLayout" class="app-container">
     <el-container class="main-container">
     <el-container class="main-container">
       <el-header class="header">
       <el-header class="header">
-        <div class="logo">AI 智能值班平台</div>
+        <div class="header-left">
+          <el-button
+            v-if="isNarrow"
+            class="nav-toggle"
+            text
+            type="primary"
+            @click="openDrawer"
+          >
+            <el-icon :size="22"><Menu /></el-icon>
+          </el-button>
+          <div class="logo">AI 智能值班平台</div>
+        </div>
         <div class="user-info">
         <div class="user-info">
-          <span>{{ authStore.username }}</span>
-          <el-button type="danger" size="small" @click="handleLogout" style="margin-left: 10px;">退出登录</el-button>
+          <span class="username-text">{{ authStore.username }}</span>
+          <el-button type="danger" size="small" class="logout-btn" @click="handleLogout">
+            退出登录
+          </el-button>
         </div>
         </div>
       </el-header>
       </el-header>
       <el-container class="content-container">
       <el-container class="content-container">
-        <el-aside width="200px" class="aside">
+        <el-aside v-if="!isNarrow" width="200px" class="aside">
           <el-menu
           <el-menu
             :default-active="route.path"
             :default-active="route.path"
             class="el-menu-vertical"
             class="el-menu-vertical"
@@ -80,6 +100,50 @@ const handleMenuSelect = (index: string) => {
         </el-main>
         </el-main>
       </el-container>
       </el-container>
     </el-container>
     </el-container>
+
+    <el-drawer
+      v-if="isNarrow"
+      v-model="drawerVisible"
+      direction="ltr"
+      size="85%"
+      :with-header="false"
+      class="mobile-nav-drawer"
+    >
+      <el-menu
+        :default-active="route.path"
+        class="el-menu-mobile"
+        @select="handleMenuSelect"
+      >
+        <el-menu-item index="/big-screen">
+          <el-icon><Monitor /></el-icon>
+          <span>监控大屏</span>
+        </el-menu-item>
+        <el-menu-item index="/cameras" v-if="authStore.isSuperuser">
+          <el-icon><VideoCamera /></el-icon>
+          <span>摄像头管理</span>
+        </el-menu-item>
+        <el-menu-item index="/models" v-if="authStore.isSuperuser">
+          <el-icon><Connection /></el-icon>
+          <span>模型管理</span>
+        </el-menu-item>
+        <el-menu-item index="/tasks" v-if="authStore.isSuperuser">
+          <el-icon><List /></el-icon>
+          <span>任务调度</span>
+        </el-menu-item>
+        <el-menu-item index="/logs">
+          <el-icon><Document /></el-icon>
+          <span>报警日志</span>
+        </el-menu-item>
+        <el-menu-item index="/reports">
+          <el-icon><Document /></el-icon>
+          <span>值班报告</span>
+        </el-menu-item>
+        <el-menu-item index="/users" v-if="authStore.isSuperuser">
+          <el-icon><User /></el-icon>
+          <span>用户管理</span>
+        </el-menu-item>
+      </el-menu>
+    </el-drawer>
   </div>
   </div>
   <div v-else class="login-container">
   <div v-else class="login-container">
     <router-view />
     <router-view />
@@ -90,6 +154,7 @@ const handleMenuSelect = (index: string) => {
 .app-container {
 .app-container {
   height: 100%;
   height: 100%;
   width: 100%;
   width: 100%;
+  min-width: 0;
 }
 }
 
 
 .main-container {
 .main-container {
@@ -99,18 +164,51 @@ const handleMenuSelect = (index: string) => {
 }
 }
 
 
 .header {
 .header {
-  background-color: #409EFF;
+  background-color: #409eff;
   color: white;
   color: white;
   display: flex;
   display: flex;
   justify-content: space-between;
   justify-content: space-between;
   align-items: center;
   align-items: center;
-  height: 60px;
-  padding: 0 20px;
+  min-height: 56px;
+  height: auto;
+  padding: 8px 12px;
+  flex-shrink: 0;
+  gap: 8px;
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  min-width: 0;
+  flex: 1;
+}
+
+.nav-toggle {
+  color: #fff !important;
+  padding: 4px 8px;
+  flex-shrink: 0;
+}
+
+.user-info {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  flex-shrink: 0;
+}
+
+.username-text {
+  max-width: 42vw;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
 }
 }
 
 
 .content-container {
 .content-container {
-  flex: 1; /* Fill remaining height */
-  overflow: hidden; /* Prevent double scrollbar */
+  flex: 1;
+  min-height: 0;
+  min-width: 0;
+  overflow: hidden;
 }
 }
 
 
 .aside {
 .aside {
@@ -124,14 +222,17 @@ const handleMenuSelect = (index: string) => {
 }
 }
 
 
 .main-content {
 .main-content {
-  padding: 20px;
-  overflow-y: auto; /* Scroll inside main content */
+  padding: 16px;
+  overflow-y: auto;
+  overflow-x: hidden;
   background-color: #fff;
   background-color: #fff;
+  min-width: 0;
 }
 }
 
 
 .logo {
 .logo {
-  font-size: 20px;
+  font-size: clamp(16px, 4vw, 20px);
   font-weight: bold;
   font-weight: bold;
+  min-width: 0;
 }
 }
 
 
 .login-container {
 .login-container {
@@ -140,5 +241,35 @@ const handleMenuSelect = (index: string) => {
   justify-content: center;
   justify-content: center;
   align-items: center;
   align-items: center;
   background-color: #f5f7fa;
   background-color: #f5f7fa;
+  padding: 16px;
+  box-sizing: border-box;
+}
+
+@media (min-width: 768px) {
+  .header {
+    padding: 0 20px;
+    height: 60px;
+    min-height: 60px;
+  }
+
+  .main-content {
+    padding: 20px;
+  }
+
+  .username-text {
+    max-width: none;
+  }
+}
+</style>
+
+<style>
+/* Drawer: full-height menu without scoped pierce */
+.mobile-nav-drawer .el-drawer__body {
+  padding: 0;
+}
+
+.el-menu-mobile {
+  border-right: none;
+  min-height: 100%;
 }
 }
 </style>
 </style>

+ 18 - 0
frontend/src/composables/useBreakpoint.ts

@@ -0,0 +1,18 @@
+import { ref, onMounted, onUnmounted } from 'vue'
+
+/** Viewport width below this is treated as phone/small layout (matches Element Plus sm). */
+export function useBreakpoint(maxWidth = 768) {
+  const isNarrow = ref(false)
+
+  const update = () => {
+    isNarrow.value = window.innerWidth < maxWidth
+  }
+
+  onMounted(() => {
+    update()
+    window.addEventListener('resize', update, { passive: true })
+  })
+  onUnmounted(() => window.removeEventListener('resize', update))
+
+  return { isNarrow }
+}

+ 45 - 0
frontend/src/style.css

@@ -31,3 +31,48 @@ html, body, #app {
     background-color: #ffffff;
     background-color: #ffffff;
   }
   }
 }
 }
+
+/* Mobile-friendly tables: horizontal scroll without breaking page width */
+.table-scroll-wrap {
+  width: 100%;
+  max-width: 100%;
+  overflow-x: auto;
+  -webkit-overflow-scrolling: touch;
+}
+
+@media (max-width: 767px) {
+  .responsive-dialog.el-dialog {
+    width: 92% !important;
+    max-width: 100%;
+    margin: 5vh auto !important;
+  }
+
+  .responsive-dialog .el-dialog__footer .el-button + .el-button {
+    margin-left: 0;
+  }
+
+  .responsive-dialog .el-form-item {
+    display: block;
+  }
+
+  .responsive-dialog .el-form-item__content {
+    margin-left: 0 !important;
+  }
+
+  .responsive-dialog .el-form-item__label {
+    width: 100% !important;
+    text-align: left;
+    justify-content: flex-start;
+    margin-bottom: 4px;
+  }
+
+  .toolbar-responsive {
+    flex-wrap: wrap;
+    gap: 8px;
+  }
+
+  .toolbar-responsive .el-button,
+  .toolbar-responsive .el-upload {
+    margin-left: 0 !important;
+  }
+}

+ 21 - 4
frontend/src/views/Cameras.vue

@@ -145,7 +145,7 @@ onMounted(fetchCameras)
 
 
 <template>
 <template>
   <div class="cameras-view">
   <div class="cameras-view">
-    <div class="toolbar">
+    <div class="toolbar toolbar-responsive">
       <el-button type="primary" @click="handleAdd">添加摄像头</el-button>
       <el-button type="primary" @click="handleAdd">添加摄像头</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-upload
       <el-upload
@@ -153,12 +153,12 @@ onMounted(fetchCameras)
         :show-file-list="false"
         :show-file-list="false"
         :http-request="handleImport"
         :http-request="handleImport"
         accept=".xlsx, .xls"
         accept=".xlsx, .xls"
-        style="display: inline-block; margin-left: 12px;"
       >
       >
         <el-button type="warning">导入模板</el-button>
         <el-button type="warning">导入模板</el-button>
       </el-upload>
       </el-upload>
     </div>
     </div>
-    <el-table :data="cameras" style="width: 100%">
+    <div class="table-scroll-wrap">
+    <el-table :data="cameras" style="width: 100%" table-layout="fixed">
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="name" label="名称" />
       <el-table-column prop="name" label="名称" />
       <el-table-column prop="stream_url" label="流地址" show-overflow-tooltip />
       <el-table-column prop="stream_url" label="流地址" show-overflow-tooltip />
@@ -176,8 +176,13 @@ onMounted(fetchCameras)
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
+    </div>
 
 
-    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑摄像头' : '添加摄像头'">
+    <el-dialog
+      v-model="dialogVisible"
+      class="responsive-dialog"
+      :title="isEdit ? '编辑摄像头' : '添加摄像头'"
+    >
       <el-form :model="form" label-width="80px">
       <el-form :model="form" label-width="80px">
         <el-form-item label="名称">
         <el-form-item label="名称">
           <el-input v-model="form.name" />
           <el-input v-model="form.name" />
@@ -204,6 +209,18 @@ onMounted(fetchCameras)
 </template>
 </template>
 
 
 <style scoped>
 <style scoped>
+.toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px 12px;
+  margin-bottom: 16px;
+}
+
+.upload-demo {
+  display: inline-block;
+}
+
 .preview-container {
 .preview-container {
   margin-top: 20px;
   margin-top: 20px;
   text-align: center;
   text-align: center;

+ 94 - 0
frontend/src/views/Dashboard.vue

@@ -526,4 +526,98 @@ onUnmounted(() => {
     background: #1e293b;
     background: #1e293b;
     border-radius: 12px;
     border-radius: 12px;
 }
 }
+
+@media (max-width: 767px) {
+    .dashboard-container {
+        overflow-y: auto;
+        height: auto;
+        min-height: 100vh;
+        min-height: 100dvh;
+    }
+
+    .header {
+        flex: 0 0 auto;
+        flex-wrap: wrap;
+        padding: 10px 14px;
+        gap: 10px;
+    }
+
+    .header h1 {
+        font-size: 1rem;
+        letter-spacing: 0.5px;
+        line-height: 1.3;
+    }
+
+    .fs-btn {
+        padding: 8px 14px;
+        font-size: 0.9rem;
+    }
+
+    .content-wrapper {
+        flex-direction: column;
+        height: auto;
+        min-height: 0;
+        overflow: visible;
+        padding: 12px;
+        gap: 16px;
+    }
+
+    .left-panel,
+    .right-panel {
+        flex: none;
+        width: 100%;
+        border-left: none;
+        padding-left: 0;
+    }
+
+    .latest-card {
+        flex-direction: column;
+        padding: 14px;
+        gap: 14px;
+    }
+
+    .latest-img-wrapper {
+        flex: none;
+        width: 100%;
+        min-height: 160px;
+        max-height: 40vh;
+    }
+
+    .info-row {
+        font-size: 0.95rem;
+        flex-wrap: wrap;
+    }
+
+    .info-row .label {
+        min-width: 88px;
+    }
+
+    .highlight-name {
+        font-size: 1.1rem;
+    }
+
+    .highlight-content {
+        font-size: 1.15rem;
+    }
+
+    .section-header {
+        font-size: 1.1rem;
+    }
+
+    .recent-card {
+        height: auto;
+        min-height: 100px;
+        flex-wrap: wrap;
+    }
+
+    .recent-img {
+        flex: 0 0 100px;
+        height: 80px;
+    }
+
+    .no-data {
+        font-size: 1.25rem;
+        min-height: 120px;
+    }
+}
 </style>
 </style>

+ 3 - 1
frontend/src/views/Login.vue

@@ -90,7 +90,9 @@ const handleLogin = async () => {
 
 
 <style scoped>
 <style scoped>
 .login-card {
 .login-card {
-  width: 400px;
+  width: 100%;
+  max-width: 400px;
+  box-sizing: border-box;
 }
 }
 .card-header {
 .card-header {
   text-align: center;
   text-align: center;

+ 36 - 8
frontend/src/views/Logs.vue

@@ -1,7 +1,9 @@
 <script setup lang="ts">
 <script setup lang="ts">
-import { ref, onMounted } from 'vue'
+import { ref, computed, onMounted } from 'vue'
+import { Refresh } from '@element-plus/icons-vue'
 import api from '../api'
 import api from '../api'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { useBreakpoint } from '../composables/useBreakpoint'
 
 
 interface Log {
 interface Log {
   id: number
   id: number
@@ -22,6 +24,11 @@ const currentImage = ref('')
 const selectedLogIds = ref<number[]>([])
 const selectedLogIds = ref<number[]>([])
 const dateRange = ref<[Date, Date] | null>(null)
 const dateRange = ref<[Date, Date] | null>(null)
 
 
+const { isNarrow } = useBreakpoint(768)
+const tableHeight = computed(() =>
+  isNarrow.value ? 'calc(100vh - 260px)' : '80vh'
+)
+
 const fetchLogs = async () => {
 const fetchLogs = async () => {
   const res = await api.get('/logs', { params: { only_alarm: onlyAlarm.value } })
   const res = await api.get('/logs', { params: { only_alarm: onlyAlarm.value } })
   logs.value = res.data
   logs.value = res.data
@@ -107,8 +114,8 @@ onMounted(fetchLogs)
 
 
 <template>
 <template>
   <div class="logs-view">
   <div class="logs-view">
-    <div class="toolbar">
-      <el-checkbox v-model="onlyAlarm" @change="fetchLogs" label="仅显示异常" style="margin-right: 20px;" />
+    <div class="toolbar toolbar-responsive logs-toolbar">
+      <el-checkbox v-model="onlyAlarm" @change="fetchLogs" label="仅显示异常" class="logs-filter-check" />
       
       
       <el-date-picker
       <el-date-picker
         v-model="dateRange"
         v-model="dateRange"
@@ -116,15 +123,16 @@ onMounted(fetchLogs)
         range-separator="至"
         range-separator="至"
         start-placeholder="开始时间"
         start-placeholder="开始时间"
         end-placeholder="结束时间"
         end-placeholder="结束时间"
-        style="margin-right: 10px;"
+        class="logs-date-range"
       />
       />
       
       
       <el-button type="success" @click="handleExport">导出 Excel</el-button>
       <el-button type="success" @click="handleExport">导出 Excel</el-button>
       <el-button type="danger" @click="handleBatchDelete" :disabled="selectedLogIds.length === 0">批量删除</el-button>
       <el-button type="danger" @click="handleBatchDelete" :disabled="selectedLogIds.length === 0">批量删除</el-button>
-      <el-button @click="fetchLogs" icon="Refresh" circle style="margin-left: 10px;" />
+      <el-button @click="fetchLogs" :icon="Refresh" circle />
     </div>
     </div>
     
     
-    <el-table :data="logs" style="width: 100%" height="80vh" @selection-change="handleSelectionChange">
+    <div class="table-scroll-wrap logs-table-wrap">
+    <el-table :data="logs" style="width: 100%" :height="tableHeight" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" />
       <el-table-column type="selection" width="55" />
       <el-table-column prop="id" label="ID" width="80" />
       <el-table-column prop="id" label="ID" width="80" />
       <el-table-column prop="check_time" label="时间" width="180">
       <el-table-column prop="check_time" label="时间" width="180">
@@ -158,8 +166,9 @@ onMounted(fetchLogs)
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
+    </div>
 
 
-    <el-dialog v-model="dialogVisible" title="报警截图" width="80%">
+    <el-dialog v-model="dialogVisible" class="responsive-dialog" title="报警截图" width="80%">
       <img :src="currentImage" style="width: 100%;" />
       <img :src="currentImage" style="width: 100%;" />
     </el-dialog>
     </el-dialog>
   </div>
   </div>
@@ -167,8 +176,27 @@ onMounted(fetchLogs)
 
 
 <style scoped>
 <style scoped>
 .toolbar {
 .toolbar {
-  padding: 20px 0;
+  padding: 12px 0;
   display: flex;
   display: flex;
   align-items: center;
   align-items: center;
+  flex-wrap: wrap;
+  gap: 10px;
+}
+
+.logs-date-range {
+  width: 100%;
+  max-width: 100%;
+}
+
+@media (min-width: 768px) {
+  .logs-date-range {
+    width: auto;
+    max-width: 400px;
+    margin-right: 10px;
+  }
+
+  .toolbar {
+    padding: 20px 0;
+  }
 }
 }
 </style>
 </style>

+ 18 - 5
frontend/src/views/Models.vue

@@ -152,7 +152,7 @@ onMounted(fetchModels)
 
 
 <template>
 <template>
   <div class="models-view">
   <div class="models-view">
-    <div class="toolbar">
+    <div class="toolbar toolbar-responsive">
       <el-button type="primary" @click="handleAdd">添加模型配置</el-button>
       <el-button type="primary" @click="handleAdd">添加模型配置</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-upload
       <el-upload
@@ -163,12 +163,12 @@ onMounted(fetchModels)
         :on-success="handleImportSuccess"
         :on-success="handleImportSuccess"
         :on-error="handleImportError"
         :on-error="handleImportError"
         accept=".xlsx,.xls"
         accept=".xlsx,.xls"
-        style="display: inline-block; margin-left: 12px;"
       >
       >
         <el-button type="warning">导入模板</el-button>
         <el-button type="warning">导入模板</el-button>
       </el-upload>
       </el-upload>
     </div>
     </div>
-    <el-table :data="models" style="width: 100%">
+    <div class="table-scroll-wrap">
+    <el-table :data="models" style="width: 100%" table-layout="fixed">
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="name" label="配置名称" />
       <el-table-column prop="name" label="配置名称" />
       <el-table-column prop="model_name" label="模型ID" />
       <el-table-column prop="model_name" label="模型ID" />
@@ -185,8 +185,13 @@ onMounted(fetchModels)
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
+    </div>
 
 
-    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑模型配置' : '添加模型配置'">
+    <el-dialog
+      v-model="dialogVisible"
+      class="responsive-dialog"
+      :title="isEdit ? '编辑模型配置' : '添加模型配置'"
+    >
       <el-form :model="form" label-width="120px">
       <el-form :model="form" label-width="120px">
         <el-form-item label="配置名称">
         <el-form-item label="配置名称">
           <el-input v-model="form.name" placeholder="例如:OpenAI 生产环境" />
           <el-input v-model="form.name" placeholder="例如:OpenAI 生产环境" />
@@ -214,6 +219,14 @@ onMounted(fetchModels)
 
 
 <style scoped>
 <style scoped>
 .toolbar {
 .toolbar {
-  margin-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px 12px;
+  margin-bottom: 16px;
+}
+
+.upload-demo {
+  display: inline-block;
 }
 }
 </style>
 </style>

+ 39 - 0
frontend/src/views/Reports.vue

@@ -282,4 +282,43 @@ th {
 .status-failed {
 .status-failed {
   color: red;
   color: red;
 }
 }
+
+@media (max-width: 767px) {
+  .reports-container {
+    padding: 12px;
+  }
+
+  .report-form {
+    padding: 16px;
+  }
+
+  .form-group {
+    flex-direction: column;
+    align-items: stretch;
+  }
+
+  .form-group label {
+    width: auto;
+    margin-bottom: 6px;
+  }
+
+  .form-group input {
+    width: 100%;
+    max-width: none;
+  }
+
+  .report-list {
+    overflow-x: auto;
+    -webkit-overflow-scrolling: touch;
+  }
+
+  table {
+    min-width: 640px;
+  }
+
+  .pagination {
+    flex-wrap: wrap;
+    justify-content: center;
+  }
+}
 </style>
 </style>

+ 43 - 8
frontend/src/views/Tasks.vue

@@ -282,7 +282,7 @@ onMounted(() => {
 
 
 <template>
 <template>
   <div class="tasks-view">
   <div class="tasks-view">
-    <div class="toolbar">
+    <div class="toolbar toolbar-responsive">
       <el-button type="primary" @click="handleAdd">新建任务</el-button>
       <el-button type="primary" @click="handleAdd">新建任务</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-button type="success" @click="handleExport">导出模板</el-button>
       <el-upload
       <el-upload
@@ -293,12 +293,12 @@ onMounted(() => {
         :on-success="handleImportSuccess"
         :on-success="handleImportSuccess"
         :on-error="handleImportError"
         :on-error="handleImportError"
         accept=".xlsx,.xls"
         accept=".xlsx,.xls"
-        style="display: inline-block; margin-left: 12px;"
       >
       >
         <el-button type="warning">导入模板</el-button>
         <el-button type="warning">导入模板</el-button>
       </el-upload>
       </el-upload>
     </div>
     </div>
-    <el-table :data="tasks" style="width: 100%">
+    <div class="table-scroll-wrap">
+    <el-table :data="tasks" style="width: 100%" table-layout="fixed">
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="name" label="任务名称" />
       <el-table-column prop="name" label="任务名称" />
       <el-table-column label="轮询频率">
       <el-table-column label="轮询频率">
@@ -318,8 +318,14 @@ onMounted(() => {
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
+    </div>
 
 
-    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑任务' : '新建任务'" width="70%">
+    <el-dialog
+      v-model="dialogVisible"
+      class="responsive-dialog task-edit-dialog"
+      :title="isEdit ? '编辑任务' : '新建任务'"
+      width="70%"
+    >
       <div class="dialog-content">
       <div class="dialog-content">
         <el-form :model="form" label-width="120px">
         <el-form :model="form" label-width="120px">
           <el-form-item label="任务名称">
           <el-form-item label="任务名称">
@@ -377,8 +383,8 @@ onMounted(() => {
                </el-tag>
                </el-tag>
              </div>
              </div>
              <!-- Parse and display JSON result -->
              <!-- Parse and display JSON result -->
-             <div class="result-content">
-               <el-table :data="parseResult(res.ai_result)" size="small" border style="width: 100%">
+             <div class="result-content table-scroll-wrap">
+               <el-table :data="parseResult(res.ai_result)" size="small" border style="width: 100%; min-width: 520px">
                  <el-table-column prop="alarm_name" label="告警名称" width="120" />
                  <el-table-column prop="alarm_name" label="告警名称" width="120" />
                  <el-table-column prop="alarm_content" label="内容" />
                  <el-table-column prop="alarm_content" label="内容" />
                  <el-table-column prop="time" label="时间" width="160" />
                  <el-table-column prop="time" label="时间" width="160" />
@@ -397,20 +403,42 @@ onMounted(() => {
       </div>
       </div>
       
       
       <template #footer>
       <template #footer>
-        <span class="dialog-footer">
+        <div class="dialog-footer dialog-footer-stack">
           <el-button @click="handleTest" :loading="testing" type="success" plain>立即运行测试</el-button>
           <el-button @click="handleTest" :loading="testing" type="success" plain>立即运行测试</el-button>
           <el-button @click="dialogVisible = false">取消</el-button>
           <el-button @click="dialogVisible = false">取消</el-button>
           <el-button type="primary" @click="handleSave">保存</el-button>
           <el-button type="primary" @click="handleSave">保存</el-button>
-        </span>
+        </div>
       </template>
       </template>
     </el-dialog>
     </el-dialog>
   </div>
   </div>
 </template>
 </template>
 
 
 <style scoped>
 <style scoped>
+.toolbar {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 8px 12px;
+  margin-bottom: 16px;
+}
+
+.upload-demo {
+  display: inline-block;
+}
+
 .cron-input {
 .cron-input {
   display: flex;
   display: flex;
+  flex-wrap: wrap;
   align-items: center;
   align-items: center;
+  gap: 8px;
+}
+
+.dialog-footer-stack {
+  display: flex;
+  flex-wrap: wrap;
+  justify-content: flex-end;
+  gap: 8px;
+  width: 100%;
 }
 }
 .test-results {
 .test-results {
   margin-top: 20px;
   margin-top: 20px;
@@ -446,4 +474,11 @@ onMounted(() => {
   font-size: 12px;
   font-size: 12px;
   color: #606266;
   color: #606266;
 }
 }
+
+@media (max-width: 767px) {
+  .cron-input .el-input-number {
+    width: 100% !important;
+    max-width: 160px;
+  }
+}
 </style>
 </style>

+ 13 - 4
frontend/src/views/Users.vue

@@ -101,10 +101,11 @@ onMounted(fetchUsers)
 
 
 <template>
 <template>
   <div class="users-view">
   <div class="users-view">
-    <div class="toolbar">
+    <div class="toolbar toolbar-responsive">
       <el-button type="primary" @click="handleAdd">添加用户</el-button>
       <el-button type="primary" @click="handleAdd">添加用户</el-button>
     </div>
     </div>
-    <el-table :data="users" style="width: 100%">
+    <div class="table-scroll-wrap">
+    <el-table :data="users" style="width: 100%" table-layout="fixed">
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="id" label="ID" width="60" />
       <el-table-column prop="username" label="用户名" />
       <el-table-column prop="username" label="用户名" />
       <el-table-column label="状态">
       <el-table-column label="状态">
@@ -128,8 +129,13 @@ onMounted(fetchUsers)
         </template>
         </template>
       </el-table-column>
       </el-table-column>
     </el-table>
     </el-table>
+    </div>
 
 
-    <el-dialog v-model="dialogVisible" :title="isEdit ? '编辑用户' : '添加用户'">
+    <el-dialog
+      v-model="dialogVisible"
+      class="responsive-dialog"
+      :title="isEdit ? '编辑用户' : '添加用户'"
+    >
       <el-form :model="form" label-width="80px">
       <el-form :model="form" label-width="80px">
         <el-form-item label="用户名">
         <el-form-item label="用户名">
           <el-input v-model="form.username" :disabled="isEdit" />
           <el-input v-model="form.username" :disabled="isEdit" />
@@ -156,7 +162,10 @@ onMounted(fetchUsers)
 
 
 <style scoped>
 <style scoped>
 .toolbar {
 .toolbar {
-  margin-bottom: 20px;
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-bottom: 16px;
 }
 }
 </style>
 </style>