Переглянути джерело

修改初始化配置,修改路由,增加首页总览页面

cuiHe 1 тиждень тому
батько
коміт
8a2a6d301b

+ 4 - 2
backend/app/config/setting.py

@@ -193,7 +193,8 @@ class Settings(BaseSettings):
     def ASYNC_DB_URI(self) -> str:
         """获取异步数据库连接"""
         if self.DATABASE_TYPE == "mysql":
-            return f"mysql+asyncmy://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
+            return f"mysql+asyncmy://{self.DATABASE_USER}:{self.DATABASE_PASSWORD}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
+            #return f"mysql+asyncmy://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
         elif self.DATABASE_TYPE == "postgres":
             return f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
         elif self.DATABASE_TYPE == "sqlite":
@@ -207,7 +208,8 @@ class Settings(BaseSettings):
     def DB_URI(self) -> str:
         """获取同步数据库连接"""
         if self.DATABASE_TYPE == "mysql":
-            return f"mysql+pymysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
+            return f"mysql+pymysql://{self.DATABASE_USER}:{self.DATABASE_PASSWORD}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
+            #return f"mysql+pymysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
         elif self.DATABASE_TYPE == "postgres":
             return f"postgresql+psycopg2://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
         elif self.DATABASE_TYPE == "sqlite":

BIN
backend/static/image/logo.png


BIN
backend/static/image/logo1.png


BIN
backend/static/upload/2025/12/18/恒达logo@2x_20251218104139A330.png


BIN
backend/static/upload/2025/12/18/恒达logo_20251218104142A996.png


+ 1 - 0
frontend/package.json

@@ -112,6 +112,7 @@
     "eslint-plugin-vue": "^10.4.0",
     "fs-extra": "^11.2.0",
     "husky": "^9.1.7",
+    "less": "^4.5.1",
     "sass": "^1.89.2",
     "stylelint": "^16.25.0",
     "stylelint-config-html": "^1.1.0",

BIN
frontend/src/assets/images/empty-bg.png


BIN
frontend/src/assets/images/overview-left-title.png


BIN
frontend/src/assets/images/overview-right-title.png


+ 2 - 1
frontend/src/components/Breadcrumb/index.vue

@@ -33,7 +33,7 @@ function getBreadcrumb() {
   let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title);
   const first = matched[0];
   if (!isDashboard(first)) {
-    matched = [{ path: "/dashboard", meta: { title: "dashboard" } } as any].concat(matched);
+    matched = [{ path: "/home", meta: { title: "首页" } } as any].concat(matched);
   }
   breadcrumbs.value = matched.filter((item) => {
     return item.meta && item.meta.title && item.meta.breadcrumb !== false;
@@ -49,6 +49,7 @@ function isDashboard(route: RouteLocationMatched) {
 }
 
 function handleLink(item: any) {
+  console.log(item)
   const { redirect, path } = item;
   if (redirect) {
     router.push(redirect).catch((err) => {

+ 73 - 0
frontend/src/components/pro-table/index.vue

@@ -0,0 +1,73 @@
+<template>
+  <div class="table">
+    <el-table v-loading="props.loading" :data="props.data" :height="props.height">
+      <template v-for="({ prop, label, slot, width,minWidth }) in props.config" :key="prop">
+        <el-table-column :prop="prop" :label="label" :min-width="minWidth" :width="width">
+          <template v-if="slot" #default="{ row }">
+            <slot :row="row" :label="label"></slot>
+          </template>
+        </el-table-column>
+      </template>
+      <template #empty>
+        <div class="el-table-empty">
+          <el-image :src="emptybg"></el-image>
+          <span>当前无数据</span>
+        </div>
+      </template>
+    </el-table>
+  </div>
+</template>
+
+<script setup>
+import emptybgUrl from '@/assets/images/empty-bg.png';
+const emptybg = ref(emptybgUrl)
+const props = defineProps({
+  loading: {
+    type: Boolean,
+    default: false
+  },
+  height: {
+    type: String,
+    default: ''
+  },
+  data: {
+    type: Array,
+    default: () => []
+  },
+  config: {
+    type: Array,
+    default: () => []
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.table {
+  :deep(.el-table) {
+    background-color: transparent;
+    --el-table-border-color: #223555;
+    --el-table-row-hover-bg-color: #223555;
+    --el-mask-color: rgba(255, 255, 255, 0.1);
+  }
+
+  :deep(.el-table .el-table__cell) {
+    text-align: center;
+    background-color: transparent;
+  }
+
+  :deep(.el-table tr) {
+    background-color: transparent;
+  }
+
+  :deep(.el-table__header) {
+
+    font-size: 18px;
+    color: #7992C4;
+  }
+
+  :deep(.el-table__body) {
+    color: #ffffff;
+    font-size: 16px;
+  }
+}
+</style>

+ 8 - 8
frontend/src/layouts/components/NavBar/components/NavbarActions.vue

@@ -50,22 +50,22 @@
               <el-icon><Setting /></el-icon>
               {{ t("navbar.config") }}
             </el-dropdown-item>
-            <el-dropdown-item @click="handleDocumentClick">
+            <!-- <el-dropdown-item @click="handleDocumentClick">
               <el-icon><Document /></el-icon>
               {{ t("navbar.document") }}
-            </el-dropdown-item>
-            <el-dropdown-item @click="handleGiteeClick">
+            </el-dropdown-item> -->
+            <!-- <el-dropdown-item @click="handleGiteeClick">
               <el-icon><Reading /></el-icon>
               {{ t("navbar.gitee") }}
-            </el-dropdown-item>
-            <el-dropdown-item @click="handleTourClick">
+            </el-dropdown-item> -->
+            <!-- <el-dropdown-item @click="handleTourClick">
               <el-icon><Position /></el-icon>
               {{ t("navbar.tour") }}
-            </el-dropdown-item>
-            <el-dropdown-item divided @click="handlelockScreen">
+            </el-dropdown-item> -->
+            <!-- <el-dropdown-item divided @click="handlelockScreen">
               <el-icon><Lock /></el-icon>
               {{ t("navbar.lock") }}
-            </el-dropdown-item>
+            </el-dropdown-item> -->
             <el-dropdown-item @click="logout">
               <el-icon><SwitchButton /></el-icon>
               {{ t("navbar.logout") }}

+ 8 - 2
frontend/src/router/index.ts

@@ -45,15 +45,21 @@ export const constantRoutes: RouteRecordRaw[] = [
     component: () => import("@/views/error/404.vue"),
     meta: { hidden: true, title: "404" },
   },
+  {
+    path: "/overview",
+    name: "Overview",
+    meta: { hidden: true, title: "总览" },
+    component: () => import("@/views/web/overview/index.vue"),
+  },
   // 以下内容必须放在后面
   {
     path: "/",
     name: "/",
-    redirect: "/home",
+    redirect: "/overview",
     component: Layout,
     children: [
       {
-        path: "home",
+        path: "/home",
         component: () => import("@/views/dashboard/index.vue"),
         // 用于 keep-alive 功能,需要与 SFC 中自动推导或显式声明的组件名称一致
         // 参考文档: https://cn.vuejs.org/guide/built-ins/keep-alive.html#include-exclude

+ 3 - 3
frontend/src/settings.ts

@@ -22,7 +22,7 @@ export const defaultSettings: AppSettings = {
   // 是否显示标签视图
   showTagsView: true,
   // 是否显示应用Logo
-  showAppLogo: true,
+  showAppLogo: false,
   // 布局方式,默认为左侧布局
   layout: LayoutMode.LEFT,
   // 主题,根据操作系统的色彩方案自动选择
@@ -35,7 +35,7 @@ export const defaultSettings: AppSettings = {
   themeColor: "#4080FF",
   // 是否显示水印 (修改默认开启水印)
   // showWatermark: false,
-  showWatermark: true,
+  showWatermark: false,
   // 水印内容
   watermarkContent: pkg.name,
   // 侧边栏配色方案
@@ -43,7 +43,7 @@ export const defaultSettings: AppSettings = {
   // 项目引导
   guideVisible: false,
   /** 是否启动引导 */
-  showGuide: true,
+  showGuide: false,
 };
 
 // 主题色预设 - 现代化配色方案

+ 34 - 5
frontend/src/store/modules/dict.store.ts

@@ -13,13 +13,42 @@ export const useDictStore = defineStore("dict", {
 
     // 获取指定类型的字典数据,确保返回数组
     getDictArray() {
-      return (type: string): Array<{ dict_value: string; dict_label: string }> => {
+      return (type: string): Array<{ dict_value: string | number; dict_label: string }> => {
+        // 辅助函数:判断字符串是否为纯数字格式(整数/负数/小数)
+        const isNumericString = (value: string): boolean => {
+          // 先去除首尾空格,避免 " 123 " 这类情况误判
+          const trimmedValue = value.trim();
+          // 正则匹配:支持整数、负数、小数(排除空字符串、非数字字符串)
+          return /^-?\d+(\.\d+)?$/.test(trimmedValue);
+        };
+    
         return (this.dictData[type] || [])
           .filter((item) => item.dict_value !== undefined && item.dict_label !== undefined)
-          .map((item) => ({
-            dict_value: item.dict_value!,
-            dict_label: item.dict_label!,
-          }));
+          .map((item) => {
+            let processedValue: string | number;
+            const rawValue = item.dict_value;
+    
+            // 核心逻辑:
+            // 1. 如果是字符串类型,且是数字格式 → 转为数字类型
+            if (typeof rawValue === 'string') {
+              processedValue = isNumericString(rawValue) 
+                ? Number(rawValue.trim())  // 数字字符串 → 转数字
+                : rawValue.trim();         // 普通字符串 → 去空格保留字符串
+            }
+            // 2. 如果本身就是数字类型 → 直接保留
+            else if (typeof rawValue === 'number') {
+              processedValue = rawValue;
+            }
+            // 3. 其他类型(如boolean/null等)→ 兜底转为字符串
+            else {
+              processedValue = rawValue?.toString() || '';
+            }
+    
+            return {
+              dict_value: processedValue,
+              dict_label: item.dict_label!.toString().trim(), // 标签统一转字符串+去空格
+            };
+          });
       };
     },
   },

+ 2 - 549
frontend/src/views/dashboard/index.vue

@@ -1,323 +1,19 @@
 <template>
   <div class="dashboard-container">
-    <!-- github 角标 -->
-    <GithubCorner class="github-corner" />
 
     <el-card shadow="hover">
       <div class="flex flex-wrap">
         <!-- 左侧问候语区域 -->
         <div class="flex-1 flex items-start">
-          <img
-            class="w80px h80px rounded-full"
-            :src="userStore.basicInfo.avatar + '?imageView2/1/w/80/h/80'"
-          />
           <div class="ml-5">
             <div class="text-20px font-bold mb-5px">
               {{ timefix }}{{ userStore.basicInfo.name }},{{ welcome }}
             </div>
-            <p class="text-sm text-gray">今日天气晴朗,气温在15℃至25℃之间,东南风。</p>
-          </div>
-        </div>
-
-        <!-- 右侧图标区域 - PC端 -->
-        <div class="hidden sm:block">
-          <div class="flex items-end space-x-6">
-            <!-- 文档 -->
-            <div class="flex flex-col items-center">
-              <div class="font-bold color-#4080ff text-sm flex items-center">
-                <el-icon class="mr-2px"><Document /></el-icon>
-                文档
-              </div>
-              <div class="mt-3 whitespace-nowrap">
-                <el-link
-                  href="https://blog.csdn.net/weixin_46768253/article/details/149569141?spm=1001.2014.3001.5502"
-                  target="_blank"
-                >
-                  <div class="i-svg:csdn text-lg" />
-                </el-link>
-              </div>
-            </div>
-            <!-- 仓库 -->
-            <div class="flex flex-col items-center">
-              <div class="font-bold color-#ff9a2e text-sm flex items-center">
-                <el-icon class="mr-2px">
-                  <Folder />
-                </el-icon>
-                仓库
-              </div>
-              <div class="mt-3 whitespace-nowrap">
-                <el-link href="https://gitee.com/tao__tao/FastapiAdmin" target="_blank">
-                  <div class="i-svg:gitee text-lg color-#F76560" />
-                </el-link>
-                <el-divider direction="vertical" />
-                <el-link href="https://github.com/1014TaoTao/FastapiAdmin" target="_blank">
-                  <div class="i-svg:github text-lg color-#4080FF" />
-                </el-link>
-                <el-divider direction="vertical" />
-                <el-link href="https://gitcode.com/qq_36002987/FastapiAdmin" target="_blank">
-                  <div class="i-svg:gitcode text-lg color-#FF9A2E" />
-                </el-link>
-              </div>
-            </div>
-          </div>
-        </div>
-
-        <!-- 移动端图标区域 -->
-        <div class="w-full sm:hidden mt-3">
-          <div class="flex justify-end space-x-4 overflow-x-auto">
-            <!-- 仓库图标 -->
-            <el-link href="https://gitee.com/tao__tao/FastapiAdmin" target="_blank">
-              <div class="i-svg:gitee text-lg color-#F76560" />
-            </el-link>
-            <el-divider direction="vertical" />
-            <el-link href="https://github.com/1014TaoTao/FastapiAdmin" target="_blank">
-              <div class="i-svg:github text-lg color-#4080FF" />
-            </el-link>
-            <el-divider direction="vertical" />
-            <el-link href="https://gitcode.com/qq_36002987/FastapiAdmin" target="_blank">
-              <div class="i-svg:gitcode text-lg color-#FF9A2E" />
-            </el-link>
+            <!-- <p class="text-sm text-gray">今日天气晴朗,气温在15℃至25℃之间,东南风。</p> -->
           </div>
         </div>
       </div>
     </el-card>
-
-    <!-- 数据统计 -->
-    <el-row :gutter="10" class="mt-4">
-      <!-- 在线用户数量 -->
-      <el-col :span="8" :xs="24" class="mb-xs-3">
-        <el-card shadow="hover" class="h-full flex flex-col">
-          <template #header>
-            <div class="flex-x-between">
-              <span class="text-gray">在线用户</span>
-              <el-tag type="danger" size="small">实时</el-tag>
-            </div>
-          </template>
-
-          <div class="flex-x-between mt-2 flex-1">
-            <div class="flex-y-center">
-              <span class="text-lg transition-all duration-300 hover:scale-110">9999</span>
-              <span v-if="true" class="ml-2 text-xs text-[#67c23a]">
-                <el-icon>
-                  <Connection />
-                </el-icon>
-                已连接
-              </span>
-              <span v-else class="ml-2 text-xs text-[#f56c6c]">
-                <el-icon>
-                  <Failed />
-                </el-icon>
-                未连接
-              </span>
-            </div>
-            <div class="i-svg:people w-8 h-8 animate-[pulse_2s_infinite]" />
-          </div>
-
-          <div class="flex-x-between mt-2 text-sm text-gray">
-            <span>更新时间</span>
-            <span>2025-07-12 00:00:00</span>
-          </div>
-        </el-card>
-      </el-col>
-
-      <!-- 访客数(UV) -->
-      <el-col :span="8" :xs="24" class="mb-xs-3">
-        <el-skeleton :loading="visitStatsLoading" :rows="5" animated>
-          <template #template>
-            <el-card>
-              <template #header>
-                <div>
-                  <el-skeleton-item variant="h3" style="width: 40%" />
-                  <el-skeleton-item variant="rect" style="float: right; width: 1em; height: 1em" />
-                </div>
-              </template>
-
-              <div class="flex-x-between">
-                <el-skeleton-item variant="text" style="width: 30%" />
-                <el-skeleton-item variant="circle" style="width: 2em; height: 2em" />
-              </div>
-              <div class="mt-5 flex-x-between">
-                <el-skeleton-item variant="text" style="width: 50%" />
-                <el-skeleton-item variant="text" style="width: 1em" />
-              </div>
-            </el-card>
-          </template>
-          <template v-if="!visitStatsLoading">
-            <el-card shadow="hover" class="h-full flex flex-col">
-              <template #header>
-                <div class="flex-x-between">
-                  <span class="text-gray">访客数(UV)</span>
-                  <el-tag type="success" size="small">日</el-tag>
-                </div>
-              </template>
-
-              <div class="flex-x-between mt-2 flex-1">
-                <div class="flex-y-center">
-                  <span class="text-lg">{{ Math.round(transitionUvCount) }}</span>
-                  <span
-                    :class="[
-                      'text-xs',
-                      'ml-2',
-                      computeGrowthRateClass(visitStatsData.uvGrowthRate),
-                    ]"
-                  >
-                    <el-icon>
-                      <Top v-if="visitStatsData.uvGrowthRate > 0" />
-                      <Bottom v-else-if="visitStatsData.uvGrowthRate < 0" />
-                    </el-icon>
-                    {{ formatGrowthRate(visitStatsData.uvGrowthRate) }}
-                  </span>
-                </div>
-                <div class="i-svg:visitor w-8 h-8" />
-              </div>
-
-              <div class="flex-x-between mt-2 text-sm text-gray">
-                <span>总访客数</span>
-                <span>{{ Math.round(transitionTotalUvCount) }}</span>
-              </div>
-            </el-card>
-          </template>
-        </el-skeleton>
-      </el-col>
-
-      <!-- 浏览量(PV) -->
-      <el-col :span="8" :xs="24">
-        <el-skeleton :loading="visitStatsLoading" :rows="5" animated>
-          <template #template>
-            <el-card>
-              <template #header>
-                <div>
-                  <el-skeleton-item variant="h3" style="width: 40%" />
-                  <el-skeleton-item variant="rect" style="float: right; width: 1em; height: 1em" />
-                </div>
-              </template>
-
-              <div class="flex-x-between">
-                <el-skeleton-item variant="text" style="width: 30%" />
-                <el-skeleton-item variant="circle" style="width: 2em; height: 2em" />
-              </div>
-              <div class="mt-5 flex-x-between">
-                <el-skeleton-item variant="text" style="width: 50%" />
-                <el-skeleton-item variant="text" style="width: 1em" />
-              </div>
-            </el-card>
-          </template>
-          <template v-if="!visitStatsLoading">
-            <el-card shadow="hover" class="h-full flex flex-col">
-              <template #header>
-                <div class="flex-x-between">
-                  <span class="text-gray">浏览量(PV)</span>
-                  <el-tag type="primary" size="small">日</el-tag>
-                </div>
-              </template>
-
-              <div class="flex-x-between mt-2 flex-1">
-                <div class="flex-y-center">
-                  <span class="text-lg">{{ Math.round(transitionPvCount) }}</span>
-                  <span
-                    :class="[
-                      'text-xs',
-                      'ml-2',
-                      computeGrowthRateClass(visitStatsData.pvGrowthRate),
-                    ]"
-                  >
-                    <el-icon>
-                      <Top v-if="visitStatsData.pvGrowthRate > 0" />
-                      <Bottom v-else-if="visitStatsData.pvGrowthRate < 0" />
-                    </el-icon>
-                    {{ formatGrowthRate(visitStatsData.pvGrowthRate) }}
-                  </span>
-                </div>
-                <div class="i-svg:browser w-8 h-8" />
-              </div>
-
-              <div class="flex-x-between mt-2 text-sm text-gray">
-                <span>总浏览量</span>
-                <span>{{ Math.round(transitionTotalPvCount) }}</span>
-              </div>
-            </el-card>
-          </template>
-        </el-skeleton>
-      </el-col>
-    </el-row>
-
-    <el-row :gutter="10" class="mt-4">
-      <!-- 访问趋势统计图 -->
-      <el-col :xs="24" :span="16">
-        <el-card>
-          <template #header>
-            <div class="flex-x-between">
-              <span>访问趋势</span>
-              <el-radio-group v-model="visitTrendDateRange" size="small">
-                <el-radio-button :value="7">近7天</el-radio-button>
-                <el-radio-button :value="30">近30天</el-radio-button>
-              </el-radio-group>
-            </div>
-          </template>
-          <ECharts :options="visitTrendChartOptions" height="400px" />
-        </el-card>
-      </el-col>
-      <!-- 最新动态 -->
-      <el-col :xs="24" :span="8">
-        <el-card>
-          <template #header>
-            <div class="flex-x-between">
-              <span class="header-title">最新动态</span>
-              <el-link
-                type="primary"
-                underline="never"
-                href="https://gitee.com/tao__tao/FastapiAdmin/releases"
-                target="_blank"
-              >
-                完整记录
-                <el-icon class="link-icon">
-                  <TopRight />
-                </el-icon>
-              </el-link>
-            </div>
-          </template>
-
-          <el-scrollbar height="400px">
-            <el-timeline class="p-3">
-              <el-timeline-item
-                v-for="(item, index) in vesionList"
-                :key="index"
-                :timestamp="item.date"
-                placement="top"
-                :color="index === 0 ? '#67C23A' : '#909399'"
-                :hollow="index !== 0"
-                size="large"
-              >
-                <div class="version-item" :class="{ 'latest-item': index === 0 }">
-                  <div>
-                    <el-text tag="strong">{{ item.title }}</el-text>
-                    <el-tag v-if="item.tag" :type="index === 0 ? 'success' : 'info'" size="small">
-                      {{ item.tag }}
-                    </el-tag>
-                  </div>
-
-                  <el-text class="version-content">{{ item.content }}</el-text>
-
-                  <div v-if="item.link">
-                    <el-link
-                      :type="index === 0 ? 'primary' : 'info'"
-                      :href="item.link"
-                      target="_blank"
-                      underline="never"
-                    >
-                      详情
-                      <el-icon class="link-icon">
-                        <TopRight />
-                      </el-icon>
-                    </el-link>
-                  </div>
-                </div>
-              </el-timeline-item>
-            </el-timeline>
-          </el-scrollbar>
-        </el-card>
-      </el-col>
-    </el-row>
   </div>
 </template>
 
@@ -327,260 +23,17 @@ defineOptions({
   inheritAttrs: false,
 });
 
-import { dayjs } from "element-plus";
 import { useUserStore } from "@/store/modules/user.store";
-import { formatGrowthRate } from "@/utils";
-import { useTransition } from "@vueuse/core";
-import { Connection, Failed } from "@element-plus/icons-vue";
 import { greetings } from "@/utils/common";
 
 const timefix = greetings();
 const welcome = "祝你开心每一天!";
-
-interface VersionItem {
-  id: string;
-  title: string; // 版本标题(如:v2.4.0)
-  date: string; // 发布时间
-  content: string; // 版本描述
-  link: string; // 详情链接
-  tag?: string; // 版本标签(可选)
-}
-
 const userStore = useUserStore();
 
-// 当前通知公告列表
-const vesionList = ref<VersionItem[]>([
-  {
-    id: "1",
-    title: "v3.2.1",
-    date: dayjs().format("YYYY-MM-DD HH:mm:ss"),
-    content: "优化性能,修复若干小bug。",
-    link: "https://gitee.com/tao__tao/FastapiAdmin/releases",
-    tag: "更新",
-  },
-  {
-    id: "2",
-    title: "v3.2.0",
-    date: dayjs().subtract(1, "day").format("YYYY-MM-DD HH:mm:ss"),
-    content: "新增用户行为分析功能。",
-    link: "https://gitee.com/tao__tao/FastapiAdmin/releases",
-    tag: "新功能",
-  },
-  {
-    id: "3",
-    title: "v3.1.0",
-    date: dayjs().subtract(3, "day").format("YYYY-MM-DD HH:mm:ss"),
-    content: "优化权限管理系统。",
-    link: "https://gitee.com/tao__tao/FastapiAdmin/releases",
-    tag: "优化",
-  },
-]);
-
-// 访客统计数据加载状态
-const visitStatsLoading = ref(true);
-// 访客统计数据
-const visitStatsData = ref({
-  todayUvCount: 0,
-  uvGrowthRate: 0,
-  totalUvCount: 0,
-  todayPvCount: 0,
-  pvGrowthRate: 0,
-  totalPvCount: 0,
-});
-
-// 数字过渡动画
-const transitionUvCount = useTransition(
-  computed(() => visitStatsData.value.todayUvCount),
-  {
-    duration: 1000,
-    transition: [0.25, 0.1, 0.25, 1.0], // CSS cubic-bezier
-  }
-);
-
-const transitionTotalUvCount = useTransition(
-  computed(() => visitStatsData.value.totalUvCount),
-  {
-    duration: 1200,
-    transition: [0.25, 0.1, 0.25, 1.0],
-  }
-);
-
-const transitionPvCount = useTransition(
-  computed(() => visitStatsData.value.todayPvCount),
-  {
-    duration: 1000,
-    transition: [0.25, 0.1, 0.25, 1.0],
-  }
-);
-
-const transitionTotalPvCount = useTransition(
-  computed(() => visitStatsData.value.totalPvCount),
-  {
-    duration: 1200,
-    transition: [0.25, 0.1, 0.25, 1.0],
-  }
-);
-
-// 访问趋势日期范围(单位:天)
-const visitTrendDateRange = ref(7);
-// 访问趋势图表配置
-const visitTrendChartOptions = ref();
-
-/**
- * 更新访问趋势图表的配置项
- *
- * @param data - 访问趋势数据
- */
-const updateVisitTrendChartOptions = () => {
-  visitTrendChartOptions.value = {
-    tooltip: {
-      trigger: "axis",
-    },
-    legend: {
-      data: ["浏览量(PV)", "访客数(UV)"],
-      bottom: 0,
-    },
-    grid: {
-      left: "1%",
-      right: "5%",
-      bottom: "10%",
-      containLabel: true,
-    },
-    xAxis: {
-      type: "category",
-      data: Array.from({ length: visitTrendDateRange.value }, (_, index) =>
-        dayjs()
-          .subtract(visitTrendDateRange.value - index - 1, "day")
-          .format("YYYY-MM-DD")
-      ),
-    },
-    yAxis: {
-      type: "value",
-      splitLine: {
-        show: true,
-        lineStyle: {
-          type: "dashed",
-        },
-      },
-    },
-    series: [
-      {
-        name: "浏览量(PV)",
-        type: "line",
-        data: Array.from(
-          { length: visitTrendDateRange.value },
-          () => Math.floor(Math.random() * 500) + 100
-        ),
-        areaStyle: {
-          color: "rgba(64, 158, 255, 0.1)",
-        },
-        smooth: true,
-        itemStyle: {
-          color: "#4080FF",
-        },
-        lineStyle: {
-          color: "#4080FF",
-        },
-      },
-      {
-        name: "访客数(UV)",
-        type: "line",
-        data: Array.from(
-          { length: visitTrendDateRange.value },
-          () => Math.floor(Math.random() * 200) + 50
-        ),
-        areaStyle: {
-          color: "rgba(103, 194, 58, 0.1)",
-        },
-        smooth: true,
-        itemStyle: {
-          color: "#67C23A",
-        },
-        lineStyle: {
-          color: "#67C23A",
-        },
-      },
-    ],
-  };
-};
-
-/**
- * 根据增长率计算对应的 CSS 类名
- *
- * @param growthRate - 增长率数值
- */
-const computeGrowthRateClass = (growthRate?: number): string => {
-  if (!growthRate) {
-    return "text-[--el-color-info]";
-  }
-  if (growthRate > 0) {
-    return "text-[--el-color-danger]";
-  } else if (growthRate < 0) {
-    return "text-[--el-color-success]";
-  } else {
-    return "text-[--el-color-info]";
-  }
-};
-
-// 监听访问趋势日期范围的变化,重新获取趋势数据
-watch(
-  () => visitTrendDateRange.value,
-  () => {
-    updateVisitTrendChartOptions();
-  },
-  { immediate: true }
-);
-
-// 组件挂载后加载访客统计数据和通知公告数据
 onMounted(() => {
-  visitStatsLoading.value = false;
-  visitStatsData.value = {
-    todayUvCount: Math.floor(Math.random() * 200) + 50,
-    uvGrowthRate: parseFloat((Math.random() * 20 - 10).toFixed(2)),
-    totalUvCount: Math.floor(Math.random() * 5000) + 1000,
-    todayPvCount: Math.floor(Math.random() * 500) + 100,
-    pvGrowthRate: parseFloat((Math.random() * 20 - 10).toFixed(2)),
-    totalPvCount: Math.floor(Math.random() * 20000) + 5000,
-  };
-  updateVisitTrendChartOptions();
+
 });
 </script>
 
 <style lang="scss" scoped>
-.dashboard-container {
-  position: relative;
-  padding: 16px;
-
-  .github-corner {
-    position: absolute;
-    top: 0;
-    right: 0;
-    z-index: 1;
-    border: 0;
-  }
-
-  .version-item {
-    padding: 16px;
-    margin-bottom: 12px;
-    background: var(--el-fill-color-lighter);
-    border-radius: 8px;
-    transition: all 0.2s;
-
-    &.latest-item {
-      background: var(--el-color-primary-light-9);
-      border: 1px solid var(--el-color-primary-light-5);
-    }
-
-    &:hover {
-      transform: translateX(5px);
-    }
-
-    .version-content {
-      margin-bottom: 12px;
-      font-size: 13px;
-      line-height: 1.5;
-      color: var(--el-text-color-secondary);
-    }
-  }
-}
 </style>

+ 148 - 116
frontend/src/views/module_business/crane/components/CraneTableSelect.vue

@@ -1,125 +1,157 @@
 <!-- 列表选择器 -->
 <template>
-    <table-select
-      :text="text"
-      :select-config="selectConfig"
-      @confirm-click="handleConfirm"
-      @clear-click="handleClearSelection"
-    >
-      <template #status="scope">
-        <el-tag :type="scope.row[scope.prop] === '0' ? 'success' : 'danger'">
-          {{ scope.row[scope.prop] === "0" ? "启用" : "停用" }}
-        </el-tag>
-      </template>
-    </table-select>
-  </template>
-  
-  <script setup lang="ts">
-  import type { ISelectConfig } from "@/components/TableSelect/index.vue";
-  import UserAPI from "@/api/module_business/crane";
-  
-  // 父组件双向绑定的值(选中行车ID)
-  const props = defineProps<{ modelValue?: number }>();
-  
-  // 事件向上派发,支持父组件监听和 v-model(仅回传选中行车ID)
-  const emit = defineEmits<{
-    confirmClick: [data: ICrane[]];
-    "update:modelValue": [val: number | undefined];
-  }>();
-  
-  const selectConfig: ISelectConfig = {
-    pk: "id",
-    width: "167.5px", // 与搜索表单其他输入宽度一致
-    placeholder: "请选择行车",
-    popover: {
-      width: 720, // 弹出层宽度,提升可用性(约 720px)
-    },
-    formItems: [
-      {
-        type: "select",
-        label: "状态",
-        prop: "status",
-        initialValue: "0",
-        attrs: {
-          placeholder: "全部",
-          clearable: true,
-          style: {
-            width: "140px",
-          },
+  <table-select
+    :text="text"
+    :select-config="selectConfig"
+    @confirm-click="handleConfirm"
+    @clear-click="handleClearSelection"
+  >
+    <template #status="scope">
+      <el-tag :type="scope.row[scope.prop] === '0' ? 'success' : 'danger'">
+        {{ scope.row[scope.prop] === "0" ? "启用" : "停用" }}
+      </el-tag>
+    </template>
+  </table-select>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, watch, onMounted } from "vue";
+import type { ISelectConfig } from "@/components/TableSelect/index.vue";
+import CraneAPI from "@/api/module_business/crane";
+
+// 父组件双向绑定的值(选中行车ID)
+const props = defineProps<{ modelValue?: number }>();
+
+// 事件向上派发,支持父组件监听和 v-model(仅回传选中行车ID)
+const emit = defineEmits<{
+  confirmClick: [data: ICrane[]];
+  "update:modelValue": [val: number | undefined];
+}>();
+
+const selectConfig: ISelectConfig = {
+  pk: "id",
+  width: "167.5px", // 与搜索表单其他输入宽度一致
+  placeholder: "请选择行车",
+  popover: {
+    width: 720, // 弹出层宽度,提升可用性(约 720px)
+  },
+  formItems: [
+    {
+      type: "select",
+      label: "状态",
+      prop: "status",
+      initialValue: "0",
+      attrs: {
+        placeholder: "全部",
+        clearable: true,
+        style: {
+          width: "140px",
         },
-        options: [
-          { label: "启用", value: "0" },
-          { label: "停用", value: "1" },
-        ],
       },
-    ],
-    indexAction(params) {
-      // 映射查询参数到后端接口
-      const query: any = { ...params };
-      // 清理空字符串/空值,避免后端 422
-      Object.keys(query).forEach((k) => {
-        const v = query[k];
-        if (v === "" || v === null || v === undefined) {
-          delete query[k];
-        }
-      });
-      // 规范化状态为布尔值
-      if (typeof query.status === "string") {
-        if (query.status === "true") query.status = true;
-        else if (query.status === "false") query.status = false;
+      options: [
+        { label: "启用", value: "0" },
+        { label: "停用", value: "1" },
+      ],
+    },
+  ],
+  indexAction(params) {
+    // 映射查询参数到后端接口
+    const query: any = { ...params };
+    // 清理空字符串/空值,避免后端 422
+    Object.keys(query).forEach((k) => {
+      const v = query[k];
+      if (v === "" || v === null || v === undefined) {
+        delete query[k];
       }
-      // 请求用户分页列表并适配 TableSelect 需要的结构
-      return UserAPI.listBizCrane(query).then((res: any) => {
-        return {
-          total: res.data.data.total,
-          list: res.data.data.items,
-        };
-      });
+    });
+    // 规范化状态为布尔值
+    if (typeof query.status === "string") {
+      if (query.status === "true") query.status = true;
+      else if (query.status === "false") query.status = false;
+    }
+    // 请求用户分页列表并适配 TableSelect 需要的结构
+    return CraneAPI.listBizCrane(query).then((res: any) => {
+      return {
+        total: res.data.data.total,
+        list: res.data.data.items,
+      };
+    });
+  },
+  tableColumns: [
+    { type: "selection", width: 50, align: "center" },
+    { label: "编号", align: "center", prop: "id", width: 100 },
+    { label: "行车名称", align: "center", prop: "crane_name" },
+    { label: "行车类型", align: "center", prop: "crane_model", width: 120 },
+    {
+      label: "状态",
+      align: "center",
+      prop: "status",
+      templet: "custom",
+      slotName: "status",
     },
-    tableColumns: [
-      { type: "selection", width: 50, align: "center" },
-      { label: "编号", align: "center", prop: "id", width: 100 },
-      { label: "行车名称", align: "center", prop: "crane_name" },
-      { label: "行车类型", align: "center", prop: "crane_model", width: 120 },
-      {
-        label: "状态",
-        align: "center",
-        prop: "status",
-        templet: "custom",
-        slotName: "status",
-      },
-    ],
-  };
-  
-  interface ICrane {
-    id: number;
-    crane_name: string;
-    crane_model?: string;
-  }
-  const selectedCrane = ref<ICrane>();
-  function handleConfirm(data: ICrane[]) {
-    selectedCrane.value = data[0];
-    const id = selectedCrane.value?.id;
-    emit("update:modelValue", id);
-    emit("confirmClick", data);
-  }
-  function handleClearSelection() {
+  ],
+};
+
+interface ICrane {
+  id: number;
+  crane_name: string;
+  crane_model?: string;
+  status?: string; // 兼容详情返回的状态字段
+}
+const selectedCrane = ref<ICrane>();
+
+// 核心:使用详情接口查询行车信息
+const getCraneDetail = async (id: number) => {
+  try {
+    // 调用专属详情接口 detailBizCrane
+    const res = await CraneAPI.detailBizCrane(id);
+    // 严格校验核心字段,避免接口返回异常数据
+    if (res.data.status_code === 200 && res.data.data && res.data.data.id) {
+      selectedCrane.value = res.data.data;
+    } else {
+      selectedCrane.value = undefined;
+    }
+  } catch (error) {
     selectedCrane.value = undefined;
-    emit("update:modelValue", undefined);
   }
-  
-  // 当父组件重置 v-model(modelValue)为 undefined 时,清空本地显示
-  watch(
-    () => props.modelValue,
-    (val) => {
-      if (val === undefined || val === null) {
-        selectedCrane.value = undefined;
-      }
+};
+
+function handleConfirm(data: ICrane[]) {
+  selectedCrane.value = data[0];
+  const id = selectedCrane.value?.id;
+  emit("update:modelValue", id);
+  emit("confirmClick", data);
+}
+
+function handleClearSelection() {
+  selectedCrane.value = undefined;
+  emit("update:modelValue", undefined);
+}
+
+// 监听modelValue变化:有值则查询详情回显,无值则清空
+watch(
+  () => props.modelValue,
+  async (val) => {
+    if (val !== undefined && val !== null && val > 0) {
+      await getCraneDetail(val); // 有有效ID则查询详情
+    } else {
+      selectedCrane.value = undefined; // 无ID则清空
     }
-  );
-  
-  const text = computed(() => {
-    return selectedCrane.value ? `${selectedCrane.value.crane_name} - ${selectedCrane.value.crane_model}` : "";
-  });
-  </script>
-  
+  },
+  { immediate: true } // 初始化时立即执行
+);
+
+// 组件挂载时兜底查询,确保编辑页初始化回显
+onMounted(async () => {
+  const { modelValue } = props;
+  if (modelValue !== undefined && modelValue !== null && modelValue > 0) {
+    await getCraneDetail(modelValue);
+  }
+});
+
+const text = computed(() => {
+  return selectedCrane.value 
+    ? `${selectedCrane.value.crane_name} - ${selectedCrane.value.crane_model || ""}` 
+    : "";
+});
+</script>

+ 10 - 10
frontend/src/views/module_business/crane/index.vue

@@ -29,8 +29,8 @@
             style="width: 170px"
             clearable
           >
-            <el-option value="0" label="用" />
-            <el-option value="1" label="用" />
+            <el-option value="0" label="用" />
+            <el-option value="1" label="用" />
           </el-select>
         </el-form-item>
         <el-form-item v-if="isExpand" prop="created_time" label="创建时间">
@@ -147,7 +147,7 @@
         </div>
         <div class="data-table__toolbar--right">
           <el-row :gutter="10">
-            <el-col :span="1.5">
+            <!-- <el-col :span="1.5">
               <el-tooltip content="导入">
                 <el-button
                   v-hasPerm="['module_business:crane:import']"
@@ -157,7 +157,7 @@
                   @click="handleOpenImportDialog"
                 />
               </el-tooltip>
-            </el-col>
+            </el-col> -->
             <el-col :span="1.5">
               <el-tooltip content="导出">
                 <el-button
@@ -375,8 +375,8 @@
               {{ detailFormData.uuid }}
             </el-descriptions-item>
             <el-descriptions-item label="状态" :span="2">
-              <el-tag :type="detailFormData.status == '0' ? 'success' : 'danger'">
-                {{ detailFormData.status == '0' ? "启用" : "停用" }}
+              <el-tag :type="detailFormData.status == '0' ? 'danger' : 'success'">
+                {{ detailFormData.status == '0' ? "停用" : "启用" }}
               </el-tag>
             </el-descriptions-item>
             <el-descriptions-item label="备注/描述" :span="2">
@@ -429,8 +429,8 @@
           </el-form-item>
           <el-form-item label="状态" prop="status" :required="true">
             <el-radio-group v-model="formData.status">
-              <el-radio value="0">启用</el-radio>
-              <el-radio value="1">停用</el-radio>
+              <el-radio value="1">启用</el-radio>
+              <el-radio value="0">停用</el-radio>
             </el-radio-group>
           </el-form-item>
           <el-form-item label="描述" prop="description">
@@ -526,7 +526,7 @@ const tableColumns = ref([
   { prop: 'ip_address', label: 'ip地址', show: true },
   { prop: 'modbus_port', label: 'modbus端口号', show: true },
   { prop: 'order', label: '排序', show: true },
-  { prop: 'status', label: '是否启用(0:启用 1:禁用)', show: true },
+  { prop: 'status', label: '是否启用(0:停用 1:启用)', show: true },
   { prop: 'description', label: '备注/描述', show: true },
   { prop: 'created_time', label: '创建时间', show: true },
   { prop: 'updated_time', label: '更新时间', show: true },
@@ -546,7 +546,7 @@ const exportColumns = [
   { prop: 'ip_address', label: 'ip地址' },
   { prop: 'modbus_port', label: 'modbus端口号' },
   { prop: 'order', label: '排序' },
-  { prop: 'status', label: '是否启用(0:启用 1:禁用)' },
+  { prop: 'status', label: '是否启用(0:禁用 1:启用)' },
   { prop: 'description', label: '备注/描述' },
   { prop: 'created_time', label: '创建时间' },
   { prop: 'updated_time', label: '更新时间' },

+ 15 - 10
frontend/src/views/module_business/vardict/index.vue

@@ -230,7 +230,7 @@
         </div>
         <div class="data-table__toolbar--right">
           <el-row :gutter="10">
-            <el-col :span="1.5">
+            <!-- <el-col :span="1.5">
               <el-tooltip content="导入">
                 <el-button
                   v-hasPerm="['module_business:vardict:import']"
@@ -240,7 +240,7 @@
                   @click="handleOpenImportDialog"
                 />
               </el-tooltip>
-            </el-col>
+            </el-col> -->
             <el-col :span="1.5">
               <el-tooltip content="导出">
                 <el-button
@@ -489,6 +489,14 @@
       <!-- 详情 -->
       <template v-if="dialogVisible.type === 'detail'">
         <el-descriptions :column="4" border>
+          <el-descriptions-item label="行车" :span="2">
+              <CraneTableSelect
+                v-model="detailFormData.crane_id"
+                @confirm-click="handleConfirm"
+                @clear-click="handleQuery"
+                style="pointer-events: none;"
+              />
+            </el-descriptions-item>
             <el-descriptions-item label="变量code" :span="2">
               {{ detailFormData.var_code }}
             </el-descriptions-item>
@@ -507,13 +515,13 @@
                   : undefined
                 )?.dict_label || detailFormData.data_type }}
             </el-descriptions-item>
-            <el-descriptions-item label="指令灯颜色" :span="2">
+            <el-descriptions-item label="指令灯颜色" :span="2" v-if="detailFormData.data_type === 1">
               {{ (detailFormData.switch_type 
                   ? (dictStore.getDictLabel("switch_type", detailFormData.switch_type) as any)
                   : undefined
                 )?.dict_label || detailFormData.switch_type }}
             </el-descriptions-item>
-            <el-descriptions-item label="是否报警" :span="2">
+            <el-descriptions-item label="是否报警" :span="2" v-if="detailFormData.data_type === 1">
               {{ detailFormData.is_alert == '0' ? '否' : '是' }}
             </el-descriptions-item>
             <el-descriptions-item label="modbus地址" :span="2">
@@ -525,18 +533,15 @@
                   : undefined
                 )?.dict_label || detailFormData.modbus_data_type }}
             </el-descriptions-item>
-            <el-descriptions-item label="是否取反" :span="2">
+            <el-descriptions-item label="是否取反" :span="2" v-if="detailFormData.data_type === 1">
               {{ detailFormData.is_reverse == '0' ? '否' : '是' }}
             </el-descriptions-item>
-            <el-descriptions-item label="单位" :span="2">
+            <el-descriptions-item label="单位" :span="2" v-if="detailFormData.data_type === 2">
               {{ (detailFormData.unit 
                   ? (dictStore.getDictLabel("unit", detailFormData.unit) as any)
                   : undefined
                 )?.dict_label || detailFormData.unit }}
             </el-descriptions-item>
-            <el-descriptions-item label="行车" :span="2">
-              {{ detailFormData.crane_id }}
-            </el-descriptions-item>
             <el-descriptions-item label="排序" :span="2">
               {{ detailFormData.order }}
             </el-descriptions-item>
@@ -1043,7 +1048,7 @@ async function handleQuery() {
   loadingData();
 }
 
-// 选择创建人后触发查询
+// 选择天车后触发查询
 function handleConfirm() {
   handleQuery();
 }

+ 5 - 5
frontend/src/views/module_system/auth/components/Login.vue

@@ -73,15 +73,15 @@
       </el-form-item>
     </el-form>
 
-    <div flex-center gap-10px>
+    <!-- <div flex-center gap-10px>
       <el-text size="default">{{ t("login.noAccount") }}</el-text>
       <el-link type="primary" underline="never" @click="toOtherForm('register')">
         {{ t("login.reg") }}
       </el-link>
-    </div>
+    </div> -->
 
     <!-- 第三方登录 -->
-    <div class="third-party-login">
+    <!-- <div class="third-party-login">
       <div class="divider-container">
         <div class="divider-line"></div>
         <span class="divider-text">{{ t("login.otherLoginMethods") }}</span>
@@ -101,7 +101,7 @@
           <div text-20px cursor-pointer class="i-svg:gitee" />
         </CommonWrapper>
       </div>
-    </div>
+    </div> -->
   </div>
 </template>
 <script setup lang="ts">
@@ -111,7 +111,7 @@ import { useI18n } from "vue-i18n";
 import { onActivated, onMounted, watch } from "vue";
 import AuthAPI, { type LoginFormData, type CaptchaInfo } from "@/api/module_system/auth";
 import { useAppStore, useUserStore, useSettingsStore } from "@/store";
-import CommonWrapper from "@/components/CommonWrapper/index.vue";
+//import CommonWrapper from "@/components/CommonWrapper/index.vue";
 
 const { t } = useI18n();
 const userStore = useUserStore();

+ 6 - 6
frontend/src/views/module_system/auth/index.vue

@@ -23,22 +23,22 @@
     <!-- 登录页主体 -->
     <div flex-1 flex-center>
       <div
-        class="p-4xl w-full h-auto sm:w-450px border-rd-10px sm:h-680px shadow-[var(--el-box-shadow-light)] backdrop-blur-3px"
+        class="p-4xl w-full h-auto sm:w-450px border-rd-10px sm:h-480px shadow-[var(--el-box-shadow-light)] backdrop-blur-3px"
       >
         <div w-full flex flex-col items-center>
           <!-- logo -->
           <!-- <el-image :src="logo" style="width: 84px" /> -->
-          <el-image :src="configStore.configData.sys_web_logo.config_value" style="width: 140px" />
+          <!-- <el-image :src="configStore.configData.sys_web_logo.config_value" style="width: 140px" /> -->
 
           <!-- 标题 -->
           <!-- 添加小图标用于显示提示信息 -->
           <div class="flex items-center justify-center mb-4">
-            <el-tooltip
+            <!-- <el-tooltip
               :content="configStore.configData.sys_web_description.config_value"
               placement="bottom"
             >
               <el-icon class="cursor-help"><QuestionFilled /></el-icon>
-            </el-tooltip>
+            </el-tooltip> -->
             <div class="ml-2 text-xl font-bold">
               <el-badge
                 :value="`v ${configStore.configData.sys_web_version.config_value}`"
@@ -62,7 +62,7 @@
         </div>
       </div>
       <!-- 登录页底部版权 -->
-      <el-text size="small" class="py-2.5! fixed bottom-0 text-center">
+      <!-- <el-text size="small" class="py-2.5! fixed bottom-0 text-center">
         <a :href="configStore.configData.sys_git_code.config_value" target="_blank">
           {{ configStore.configData.sys_web_copyright.config_value }} |
         </a>
@@ -70,7 +70,7 @@
         <a :href="configStore.configData.sys_web_privacy.config_value" target="_blank">隐私 |</a>
         <a :href="configStore.configData.sys_web_clause.config_value" target="_blank">条款</a>
         {{ configStore.configData.sys_keep_record.config_value }}
-      </el-text>
+      </el-text> -->
     </div>
   </div>
 </template>

+ 281 - 0
frontend/src/views/web/overview/index.vue

@@ -0,0 +1,281 @@
+<template>
+      <el-row :gutter="0" class="overview-row">
+    <el-col :xs="8" :sm="8" :md="8" :lg="8" :xl="8">
+      <div class="overview-left">
+        <div class="overview-left-title">
+          <span>报警总览</span>
+        </div>
+        <div class="overview-left-content">
+          <ul v-loading="alarm_loading">
+            <li v-for="(item, index) in alertData" :key="index">
+              <div class="content-item" :class="getColor(item.switch_type)">
+                <IconAlarm />
+                <span style="width:120px; margin-left: 15px;">{{ item.crane_name }}</span>
+                <span style="width: 2px;height: 60%;margin: 0px 30px;" :class="getColorSpan(item.switch_type)" />
+                <span>{{ item.msg }}</span>
+              </div>
+            </li>
+          </ul>
+          <div v-if="!alarm_loading && alertData.length === 0" class="el-table-empty">
+            <el-image :src="emptybg"></el-image>
+            <span>当前无数据</span>
+          </div>
+        </div>
+
+      </div>
+    </el-col>
+    <el-col :xs="16" :sm="16" :md="16" :lg="16" :xl="16">
+      <div class="overview-right">
+        <div class="overview-right-title">
+          <span>起重机总览</span>
+        </div>
+        <div class="overview-right-content">
+          <pro-table :height="tabHeight" :loading="tab_loading" :data="craneData" :config="tableConfig">
+            <template #default="{ row, label }">
+              <div v-if="label === '操作'" style="width: 100%;height: 100%;display: flex;justify-content: center;">
+                <div class="button-container" @click="handleClick(row)">
+                  查看
+                </div>
+              </div>
+              <div v-if="label === '在线状态'">
+                <el-tag effect="dark" v-if="row.Status != '在线' && row.Status != '离线'" type="warning">连接中</el-tag>
+                <el-tag effect="dark" v-if="row.Status === '在线'" type="success">在线</el-tag>
+                <el-tag effect="dark" v-if="row.Status === '离线'" type="danger">离线</el-tag>
+              </div>
+            </template>
+          </pro-table>
+        </div>
+      </div>
+    </el-col>
+  </el-row>
+</template>
+  
+<script lang="ts" setup>
+import { useDictStore } from "@/store";
+import VraneAPI from "@/api/module_business/crane";
+import BizCraneAPI, { BizCranePageQuery } from '@/api/module_business/crane'
+
+const alarm_loading = ref(true);
+const alertData = ref([])
+const tab_loading = ref(true)
+const craneData = ref([])
+const tableConfig = ref([
+  {
+    prop: 'crane_no',
+    label: '编号',
+    width: 210
+  },
+  {
+    prop: 'crane_name',
+    label: '起重机名称'
+  },
+  {
+    prop: 'crane_model',
+    label: '型号'
+  },
+  {
+    prop: 'ip_address',
+    label: 'IP地址'
+  },
+  {
+    prop: 'status',
+    label: '在线状态',
+    slot: true
+  },
+  {
+    label: '操作',
+    slot: true
+  }
+])
+
+// 分页查询参数
+const queryFormData = reactive<BizCranePageQuery>({
+  page_no: 1,
+  page_size: 10,
+  crane_name: undefined,
+  crane_no: undefined,
+  crane_model: undefined,
+  ip_address: undefined,
+  status: undefined,
+  created_time: undefined,
+  updated_time: undefined,
+  created_id: undefined,
+  updated_id: undefined,
+});
+
+const dictStore = useDictStore();
+
+// 字典数据
+const getOptions = async () => {
+  return await dictStore.getDict(["sys_user_sex"]);
+};
+
+const getCraneListData = async () => {
+  try {
+    tab_loading.value = true
+    const response = await BizCraneAPI.listBizCrane(queryFormData);
+    craneData.value = response.data.data.items;
+    console.log(craneData.value)
+  } finally {
+    tab_loading.value = false
+  }
+}
+const getData = () => {
+getCraneListData()
+// mqttService.connect();
+// topics.forEach(topic => {
+//   mqttService.subscribe(topic, (message) => {
+//     message = JSON.parse(message.toString());
+//     if (topic === 'gc/alert') {
+//       alertData.value = message.data;
+//       alarm_loading.value = false
+//     }
+//     if (topic === 'gc/crane_status') {
+//       message.forEach(item => {
+//         craneData.value.forEach(craneItem => {
+//           if (craneItem.CraneNo === item.crane_no) {
+//             craneItem.Status = item.is_online ? '在线' : '离线';
+//           }
+//         });
+//       });
+//     }
+//   });
+// });
+}
+
+onMounted(async () => {
+  getData()
+  await getOptions();
+});
+</script>
+
+<style lang="less" scoped>
+.overview-row {
+  padding: 20px 20px;
+}
+
+.overview-left {
+  height: calc(100vh - 70px - 40px);
+  ;
+  margin-right: 20px;
+  background: linear-gradient(to bottom, #14428C 0%, #121F34 12%);
+  border: 1px solid #284988;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0px 20px;
+}
+
+.overview-left-title {
+  width: 400px;
+  height: 60px;
+  background-image: url('../../assets/img/overview-left-title.png');
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #8ECAFF;
+  font-size: 24px;
+}
+
+.overview-left-content {
+  flex: 1;
+  width: 100%;
+  margin-top: 40px;
+  overflow: auto;
+
+  :deep(.el-loading-parent--relative) {
+    height: 100%;
+    --el-mask-color: rgba(255, 255, 255, 0.1);
+  }
+}
+
+.content-item {
+  width: 100%;
+  height: 40px;
+  margin-bottom: 10px;
+  display: flex;
+  align-items: center;
+  padding: 0px 17px;
+  font-size: 16px;
+}
+
+.content-item-red {
+  background: linear-gradient(to right, rgba(249, 20, 61, 0.3) 0%, rgba(89, 105, 164, 0.3) 40%, rgba(23, 41, 68, 0.3) 80%);
+  color: #F9143D;
+}
+
+.content-item-yellow {
+  background: linear-gradient(to right, rgba(255, 216, 0, 0.3) 0%, rgba(89, 105, 164, 0.3) 40%, rgba(23, 41, 68, 0.3) 80%);
+  color: #FFD800;
+}
+
+.content-item-orange {
+  background: linear-gradient(to right, rgba(243, 153, 2, 0.3) 0%, rgba(89, 105, 164, 0.3) 40%, rgba(23, 41, 68, 0.3) 80%);
+  color: #f39902;
+}
+
+.content-item-span-red {
+  background-color: #F9143D;
+}
+
+.content-item-span-yellow {
+  background-color: #FFD800;
+}
+
+.content-item-span-orange {
+  background-color: #f39902;
+}
+
+.overview-right {
+  height: calc(100vh - 70px - 40px);
+  ;
+  background: linear-gradient(to bottom, #14428C 0%, #121F34 12%);
+  border: 1px solid #284988;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  padding: 0px 20px;
+}
+
+.overview-right-title {
+  width: 640px;
+  height: 60px;
+  background-image: url('../../assets/img/overview-right-title.png');
+  background-size: cover;
+  background-position: center;
+  background-repeat: no-repeat;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  color: #8ECAFF;
+  font-size: 24px;
+}
+
+.overview-right-content {
+  width: 100%;
+  margin-top: 40px;
+  overflow: auto;
+
+  :deep(.el-table__header) {
+    border-bottom: 1px solid #3E5487;
+  }
+
+  :deep(.el-table__body) {
+    font-size: 16px;
+  }
+
+  .button-container {
+    width: 80px;
+    height: 35px;
+    background: linear-gradient(to bottom, #0949C6, #0A8DD8);
+    text-align: center;
+    line-height: 35px;
+    border-radius: 5px;
+    font-size: 16px;
+  }
+}
+</style>
+