yourname пре 7 месеци
комит
c6859f998c
100 измењених фајлова са 20187 додато и 0 уклоњено
  1. 14 0
      .gitignore
  2. 27 0
      HISTORY.md
  3. 221 0
      README.md
  4. 89 0
      client/admin/api/alert.ts
  5. 75 0
      client/admin/api/alert_handle.ts
  6. 63 0
      client/admin/api/alert_notify_config.ts
  7. 104 0
      client/admin/api/auth.ts
  8. 66 0
      client/admin/api/charts.ts
  9. 78 0
      client/admin/api/device_alert_rule.ts
  10. 93 0
      client/admin/api/device_instance.ts
  11. 101 0
      client/admin/api/device_type.ts
  12. 159 0
      client/admin/api/files.ts
  13. 41 0
      client/admin/api/index.ts
  14. 301 0
      client/admin/api/inspections.ts
  15. 92 0
      client/admin/api/know_info.ts
  16. 62 0
      client/admin/api/maps.ts
  17. 79 0
      client/admin/api/messages.ts
  18. 47 0
      client/admin/api/modbus_rtu_device.ts
  19. 179 0
      client/admin/api/monitor.ts
  20. 69 0
      client/admin/api/monitor_charts.ts
  21. 60 0
      client/admin/api/rack.ts
  22. 60 0
      client/admin/api/rack_server.ts
  23. 69 0
      client/admin/api/rack_server_type.ts
  24. 25 0
      client/admin/api/sms.ts
  25. 47 0
      client/admin/api/sys.ts
  26. 36 0
      client/admin/api/theme.ts
  27. 79 0
      client/admin/api/users.ts
  28. 308 0
      client/admin/api/work_orders.ts
  29. BIN
      client/admin/api/yangantubiao.png
  30. 61 0
      client/admin/api/zichan.ts
  31. 60 0
      client/admin/api/zichan_area.ts
  32. 60 0
      client/admin/api/zichan_category.ts
  33. 54 0
      client/admin/api/zichan_transfer.ts
  34. 46 0
      client/admin/components/ErrorPage.tsx
  35. 30 0
      client/admin/components/NotFoundPage.tsx
  36. 446 0
      client/admin/components_amap.tsx
  37. 36 0
      client/admin/components_protected_route.tsx
  38. 168 0
      client/admin/components_uploader.tsx
  39. 334 0
      client/admin/hooks_sys.tsx
  40. 222 0
      client/admin/layouts/MainLayout.tsx
  41. 365 0
      client/admin/menu.tsx
  42. 320 0
      client/admin/pages_alert_handle.tsx
  43. 218 0
      client/admin/pages_alert_handle_logs.tsx
  44. 458 0
      client/admin/pages_alert_notify_config.tsx
  45. 294 0
      client/admin/pages_alert_records.tsx
  46. 158 0
      client/admin/pages_alert_trend_chart.tsx
  47. 125 0
      client/admin/pages_asset_category_chart.tsx
  48. 120 0
      client/admin/pages_asset_transfer_chart.tsx
  49. 204 0
      client/admin/pages_chart.tsx
  50. 44 0
      client/admin/pages_dashboard.tsx
  51. 455 0
      client/admin/pages_device_alert_rule.tsx
  52. 421 0
      client/admin/pages_device_instances.tsx
  53. 597 0
      client/admin/pages_device_map.tsx
  54. 261 0
      client/admin/pages_device_monitor.tsx
  55. 383 0
      client/admin/pages_device_types.tsx
  56. 673 0
      client/admin/pages_file_library.tsx
  57. 407 0
      client/admin/pages_greenhouse_protocol.tsx
  58. 473 0
      client/admin/pages_inspections.tsx
  59. 416 0
      client/admin/pages_know_info.tsx
  60. 114 0
      client/admin/pages_login_reg.tsx
  61. 210 0
      client/admin/pages_map.tsx
  62. 281 0
      client/admin/pages_messages.tsx
  63. 252 0
      client/admin/pages_modbus_rtu_device.tsx
  64. 115 0
      client/admin/pages_online_devices_chart.tsx
  65. 364 0
      client/admin/pages_rack.tsx
  66. 414 0
      client/admin/pages_rack_server.tsx
  67. 280 0
      client/admin/pages_rack_server_type.tsx
  68. 298 0
      client/admin/pages_settings.tsx
  69. 306 0
      client/admin/pages_smoke_water.tsx
  70. 134 0
      client/admin/pages_sms.tsx
  71. 199 0
      client/admin/pages_sms_module.tsx
  72. 363 0
      client/admin/pages_temperature_humidity.tsx
  73. 344 0
      client/admin/pages_theme_settings.tsx
  74. 270 0
      client/admin/pages_users.tsx
  75. 904 0
      client/admin/pages_work_orders.tsx
  76. 401 0
      client/admin/pages_zichan.tsx
  77. 263 0
      client/admin/pages_zichan_area.tsx
  78. 263 0
      client/admin/pages_zichan_category.tsx
  79. 401 0
      client/admin/pages_zichan_transfer.tsx
  80. 257 0
      client/admin/routes.tsx
  81. 35 0
      client/admin/style_amap.css
  82. 43 0
      client/admin/utils.ts
  83. 47 0
      client/admin/web_app.tsx
  84. 381 0
      client/big/api.ts
  85. 618 0
      client/big/client.tsx
  86. 632 0
      client/big/components_three.tsx
  87. BIN
      client/big/title-bg.png
  88. 220 0
      client/migrations/migrations_app.tsx
  89. 115 0
      client/mobile/api/auth.ts
  90. 72 0
      client/mobile/api/chart.ts
  91. 173 0
      client/mobile/api/file.ts
  92. 77 0
      client/mobile/api/home.ts
  93. 25 0
      client/mobile/api/index.ts
  94. 62 0
      client/mobile/api/map.ts
  95. 97 0
      client/mobile/api/message.ts
  96. 48 0
      client/mobile/api/system.ts
  97. 37 0
      client/mobile/api/theme.ts
  98. 106 0
      client/mobile/api/user.ts
  99. 167 0
      client/mobile/components_uploader.tsx
  100. 246 0
      client/mobile/hooks.tsx

+ 14 - 0
.gitignore

@@ -0,0 +1,14 @@
+.aider*
+node_modules
+test-results
+.env
+.git
+.git/index
+.git/logs/HEAD
+.git/logs/refs/heads/main
+.git/refs/heads/main
+.git/config
+.git/index
+.git/logs/HEAD
+.git/logs/refs/heads/main
+.git/refs/heads/main

+ 27 - 0
HISTORY.md

@@ -0,0 +1,27 @@
+
+待实现
+迁移管理页面,在正式环境中,需要验证env中配置的密码参数才能打开
+
+2025.05.14 0.1.5
+优化ErrorPage样式,补充了NotFoundPage
+deno.json中去掉 没用的 @testing-library,jsDom
+
+2025.05.14 0.1.4
+开发环境中,前端页面默认打开vConsole
+
+2025.05.14 0.1.3
+web_app.tsx 模块化拆分路由、菜单、布局
+
+2025.05.14 0.1.2
+将 server/migrations.ts 中的静态加载逻辑改为异步函数 loadMigrations()
+
+2025.05.14 0.1.1
+创建 server/middlewares.ts - 集中管理所有中间件配置
+创建 server/router.ts - 统一处理路由注册逻辑
+重构 server/app.tsx - 仅保留应用初始化和服务启动逻辑
+保持原有功能不变,同时提高代码可维护性和扩展性
+
+2025.05.13 0.1.0
+将admin api.ts 拆开
+打开迁移管理页面时,将迁移历史读取出来
+首页添加了迁移管理入口按钮, 无需登录即可访问

+ 221 - 0
README.md

@@ -0,0 +1,221 @@
+# 管理端与移动端启动模板 (Admin-Mobile Starter)
+
+## 项目概述
+
+这是一个基于 Deno 和 Hono 框架开发的管理系统与移动端应用启动模板,提供了完整的用户认证、权限管理、系统设置、文件上传、地图组件、图表组件等功能,可以快速构建企业级应用。
+
+## 技术栈
+
+- **后端框架**:Deno + Hono
+- **前端框架**:React 19 + Ant Design 5
+- **状态管理**:TanStack Query
+- **认证系统**:@d8d-appcontainer/auth
+- **API客户端**:@d8d-appcontainer/api
+- **地图组件**:高德地图(支持在线/离线模式)
+- **图表组件**:Ant Design Charts
+- **日期处理**:Day.js
+- **网络请求**:Axios
+
+## 功能模块
+
+- **用户认证与管理** - 登录、注册、用户信息管理
+- **系统设置** - 站点信息、主题配置、全局参数设置
+- **文件管理** - 文件上传、分类管理
+- **地图组件** - 在线/离线地图、位置标记、地图交互
+- **图表组件** - 数据可视化图表
+- **移动端适配** - 响应式设计,支持移动端访问
+
+## 目录结构
+
+- `asset/` - 前端资源文件
+  - `admin/` - 管理端资源
+  - `mobile/` - 移动端资源
+  - `share/` - 共享资源和类型定义
+- `routes_*.ts` - 各模块路由定义文件
+- `app.tsx` - 应用主入口
+- `migrations.ts` - 数据库迁移
+- `deno.json` - Deno配置文件
+
+## 在D8D(多八多)平台运行
+
+本应用在 [D8D(多八多)开发者平台](https://www.d8d.fun) 上可以直接运行,无需复杂部署:
+
+1. 访问 [www.d8d.fun](https://www.d8d.fun) 网站并注册账号
+2. 登录后进入开发者控制台
+3. 点击"创建应用"按钮创建新应用
+4. 选择"管理端与移动端启动模板"作为应用模板
+5. 配置应用基本信息(名称、描述等)
+6. 完成创建后,直接点击"预览"按钮即可运行应用,无需额外部署步骤
+7. 系统会自动初始化并启动应用,可直接在浏览器中访问和使用
+
+### D8D(多八多)平台专属配置
+
+在D8D(多八多)平台运行时,可以通过平台的"应用配置"面板设置以下内容:
+
+- 应用资源限制(CPU、内存等)
+- 公网访问设置
+- 域名绑定
+- 自动备份
+- 日志记录级别
+
+## 本地开发指南
+
+### 环境要求
+
+- Deno 2.2.8 或更高版本
+- 数据库(由 @d8d-appcontainer/api 支持的数据库)
+
+### 环境变量配置
+
+在启动应用前,可配置以下环境变量:
+
+```
+# 应用配置
+APP_NAME=应用名称
+ENV=development
+JWT_SECRET=your-jwt-secret-key
+
+# OSS配置
+OSS_TYPE=aliyun  # 可选值: aliyun, minio
+OSS_BASE_URL=https://your-oss-url.com
+
+# 地图配置
+MAP_MODE=online  # 可选值: online, offline
+AMAP_KEY=您的地图API密钥
+
+# API客户端配置
+SERVER_URL=https://app-server.d8d.fun
+WORKSPACE_KEY=您的工作空间密钥  # 在多八多(www.d8d.fun)平台注册开通工作空间后获取
+```
+
+### 本地启动应用
+
+要在本地运行此应用,需要创建一个启动文件:
+
+1. 创建一个名为`run_app.ts`的新文件(文件名可自定义)
+2. 将下面的代码复制到该文件中:
+
+```typescript
+// 导入所需模块
+import { Hono } from 'hono'
+import { APIClient } from '@d8d-appcontainer/api'
+import debug from "debug"
+import { cors } from 'hono/cors'
+
+// 初始化debug实例
+const log = {
+  app: debug('app:server'),
+  auth: debug('auth:server'),
+  api: debug('api:server'),
+  debug: debug('debug:server')
+}
+
+// 启用所有日志
+Object.values(log).forEach(logger => logger.enabled = true)
+
+// 初始化 API Client
+const getApiClient = async (workspaceKey: string, serverUrl?: string) => {
+  try {
+    log.api('正在初始化API Client实例')
+    
+    const apiClient = await APIClient.getInstance({
+      scope: 'user',
+      config: {
+        serverUrl: serverUrl || Deno.env.get('SERVER_URL') || 'https://app-server.d8d.fun',
+        workspaceKey: workspaceKey,
+        type: 'http',
+      }
+    })
+    
+    log.api('API Client初始化成功')
+    return apiClient
+  } catch (error) {
+    log.api('API Client初始化失败:', error)
+    throw error
+  }
+}
+
+// 创建Hono应用实例
+const app = new Hono()
+
+// 注册CORS中间件
+app.use('/*', cors())
+
+// 动态加载并运行模板
+const runTemplate = async () => {
+  try {
+    // 创建基础app实例
+    const moduleApp = new Hono()
+    
+    // 初始化API Client
+    // 注意:WORKSPACE_KEY 需要在 多八多(www.d8d.fun) 平台注册并开通工作空间后获取
+    const workspaceKey = Deno.env.get('WORKSPACE_KEY') || ''
+    if (!workspaceKey) {
+      console.warn('未设置WORKSPACE_KEY,请前往 多八多(www.d8d.fun) 注册并开通工作空间以获取密钥')
+    }
+    const apiClient = await getApiClient(workspaceKey)
+    
+    // 导入模板主模块
+    const templateModule = await import('./app.tsx')
+    
+    if (templateModule.default) {
+      // 传入必要参数并初始化应用
+      const appInstance = templateModule.default({
+        apiClient: apiClient,
+        app: moduleApp,
+        moduleDir: './admin-mobile-starter'
+      })
+      
+      // 启动服务器
+      Deno.serve({ port: 8080 }, appInstance.fetch)
+      console.log('应用已启动,监听端口: 8080')
+    }
+  } catch (error) {
+    console.error('模板加载失败:', error)
+  }
+}
+
+// 执行模板
+runTemplate()
+```
+
+3. 运行该文件:
+
+```bash
+deno run -A run_app.ts
+```
+
+> **注意**:上述代码用于本地运行app.tsx。SERVER_URL默认值为`app-server.d8d.fun`,WORKSPACE_KEY需要在 [多八多(www.d8d.fun)](https://www.d8d.fun) 平台注册并开通工作空间后获取。
+
+## 系统配置说明
+
+系统配置可通过环境变量或数据库中的系统设置进行管理,支持以下配置项:
+
+- 站点名称、图标、Logo
+- 主题设置(明/暗模式)
+- 地图模式(在线/离线)
+- 图表主题
+- API 基础路径
+- 文件存储方式
+
+## 数据库迁移
+
+系统首次启动时会自动执行数据库迁移,创建必要的表结构和初始数据。
+
+## 开发者扩展指南
+
+### 添加新路由
+
+在 `routes_*.ts` 文件中定义新的路由处理函数,然后在 `app.tsx` 中引入并注册。
+
+### 前端开发
+
+前端资源位于 `asset/` 目录下,区分为管理端和移动端,可根据需要进行修改和扩展。
+
+## 许可证
+
+[License] - 请参阅LICENSE文件了解详情
+
+---
+
+© 2025 多八多(D8D). 保留所有权利。 

+ 89 - 0
client/admin/api/alert.ts

@@ -0,0 +1,89 @@
+import axios from 'axios';
+import { DeviceAlert } from "../../share/monitorTypes.ts";
+
+// 告警相关响应类型
+interface DeviceAlertDataResponse {
+    data: DeviceAlert[];
+    total: number;
+    page: number;
+    pageSize: number;
+  }
+  
+  interface DeviceAlertResponse {
+    data: DeviceAlert;
+    message?: string;
+  }
+  
+  interface AlertCreateResponse {
+    data: DeviceAlert;
+    message: string;
+  }
+  
+  interface AlertUpdateResponse {
+    data: DeviceAlert;
+    message: string;
+  }
+  
+  interface AlertDeleteResponse {
+    message: string;
+  }
+
+// 告警API接口定义
+export const AlertAPI = {
+    // 获取告警数据
+    getAlertData: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      alert_type?: string, 
+      alert_level?: string, 
+      start_time?: string, 
+      end_time?: string
+    }): Promise<DeviceAlertDataResponse> => {
+      try {
+        const response = await axios.get(`/alerts`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个告警数据
+    getAlert: async (id: number): Promise<DeviceAlert> => {
+      try {
+        const response = await axios.get(`/alerts/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建告警数据
+    createAlert: async (data: Partial<DeviceAlert>): Promise<AlertCreateResponse> => {
+      try {
+        const response = await axios.post(`/alerts`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新告警数据
+    updateAlert: async (id: number, data: Partial<DeviceAlert>): Promise<AlertUpdateResponse> => {
+      try {
+        const response = await axios.put(`/alerts/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除告警数据
+    deleteAlert: async (id: number): Promise<AlertDeleteResponse> => {
+      try {
+        const response = await axios.delete(`/alerts/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 75 - 0
client/admin/api/alert_handle.ts

@@ -0,0 +1,75 @@
+import axios from 'axios';
+import { AlertHandleLog } from "../../share/monitorTypes.ts";
+
+// 告警处理相关响应类型
+interface AlertHandleDataResponse {
+    data: AlertHandleLog[];
+    total: number;
+    page: number;
+    pageSize: number;
+  }
+  
+  interface AlertHandleResponse {
+    data: AlertHandleLog;
+    message?: string;
+  }
+
+// 告警处理API接口定义
+export const AlertHandleAPI = {
+    // 获取告警处理数据
+    getAlertHandleData: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      alert_id?: number, 
+      handle_type?: string, 
+      start_time?: string, 
+      end_time?: string
+    }): Promise<AlertHandleDataResponse> => {
+      try {
+        const response = await axios.get(`/alert-handles`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个告警处理数据
+    getAlertHandle: async (id: number): Promise<AlertHandleResponse> => {
+      try {
+        const response = await axios.get(`/alert-handles/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建告警处理数据
+    createAlertHandle: async (data: Partial<AlertHandleLog>) => {
+      try {
+        const response = await axios.post(`/alert-handles`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新告警处理数据
+    updateAlertHandle: async (id: number, data: Partial<AlertHandleLog>) => {
+      try {
+        const response = await axios.put(`/alert-handles/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除告警处理数据
+    deleteAlertHandle: async (id: number) => {
+      try {
+        const response = await axios.delete(`/alert-handles/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 63 - 0
client/admin/api/alert_notify_config.ts

@@ -0,0 +1,63 @@
+import axios from 'axios';
+import { AlertNotifyConfig } from "../../share/monitorTypes.ts";
+
+// 告警通知配置相关响应类型
+interface AlertNotifyConfigDataResponse {
+    data: AlertNotifyConfig[];
+    total: number;
+    page: number;
+    pageSize: number;
+  }
+  
+  interface AlertNotifyConfigResponse {
+    data: AlertNotifyConfig;
+    message?: string;
+  }
+
+// 告警通知配置API接口定义
+export const AlertNotifyConfigAPI = {
+    // 获取告警通知配置
+    getAlertNotifyConfig: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      device_id?: number, 
+      alert_level?: string 
+    }): Promise<AlertNotifyConfigDataResponse> => {
+      try {
+        const response = await axios.get(`/alert-notify-configs`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建告警通知配置
+    createAlertNotifyConfig: async (data: Partial<AlertNotifyConfig>): Promise<AlertNotifyConfigResponse> => {
+      try {
+        const response = await axios.post(`/alert-notify-configs`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新告警通知配置
+    updateAlertNotifyConfig: async (id: number, data: Partial<AlertNotifyConfig>): Promise<AlertNotifyConfigResponse> => {
+      try {
+        const response = await axios.put(`/alert-notify-configs/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除告警通知配置
+    deleteAlertNotifyConfig: async (id: number): Promise<AlertNotifyConfigResponse> => {
+      try {
+        const response = await axios.delete(`/alert-notify-configs/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 104 - 0
client/admin/api/auth.ts

@@ -0,0 +1,104 @@
+import axios from 'axios';
+import type { User } from '../../share/types.ts';
+
+interface AuthLoginResponse {
+  message: string;
+  token: string;
+  refreshToken?: string;
+  user: User;
+}
+
+interface AuthResponse {
+  message: string;
+  [key: string]: any;
+}
+
+interface AuthAPIType {
+  login: (username: string, password: string, latitude?: number, longitude?: number) => Promise<AuthLoginResponse>;
+  register: (username: string, email: string, password: string) => Promise<AuthResponse>;
+  logout: () => Promise<AuthResponse>;
+  getCurrentUser: () => Promise<User>;
+  updateUser: (userId: number, userData: Partial<User>) => Promise<User>;
+  changePassword: (oldPassword: string, newPassword: string) => Promise<AuthResponse>;
+  requestPasswordReset: (email: string) => Promise<AuthResponse>;
+  resetPassword: (token: string, newPassword: string) => Promise<AuthResponse>;
+}
+
+export const AuthAPI: AuthAPIType = {
+  login: async (username: string, password: string, latitude?: number, longitude?: number) => {
+    try {
+      const response = await axios.post('/auth/login', {
+        username,
+        password,
+        latitude,
+        longitude
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  register: async (username: string, email: string, password: string) => {
+    try {
+      const response = await axios.post('/auth/register', { username, email, password });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  logout: async () => {
+    try {
+      const response = await axios.post('/auth/logout');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  getCurrentUser: async () => {
+    try {
+      const response = await axios.get('/auth/me');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  updateUser: async (userId: number, userData: Partial<User>) => {
+    try {
+      const response = await axios.put(`/auth/users/${userId}`, userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  changePassword: async (oldPassword: string, newPassword: string) => {
+    try {
+      const response = await axios.post('/auth/change-password', { oldPassword, newPassword });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  requestPasswordReset: async (email: string) => {
+    try {
+      const response = await axios.post('/auth/request-password-reset', { email });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  resetPassword: async (token: string, newPassword: string) => {
+    try {
+      const response = await axios.post('/auth/reset-password', { token, newPassword });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 66 - 0
client/admin/api/charts.ts

@@ -0,0 +1,66 @@
+import axios from 'axios';
+
+interface ChartDataResponse<T> {
+  message: string;
+  data: T;
+}
+
+interface UserActivityData {
+  date: string;
+  count: number;
+}
+
+interface FileUploadsData {
+  month: string;
+  count: number;
+}
+
+interface FileTypesData {
+  type: string;
+  value: number;
+}
+
+interface DashboardOverviewData {
+  userCount: number;
+  fileCount: number;
+  articleCount: number;
+  todayLoginCount: number;
+}
+
+export const ChartAPI = {
+  getUserActivity: async (): Promise<ChartDataResponse<UserActivityData[]>> => {
+    try {
+      const response = await axios.get('/charts/user-activity');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getFileUploads: async (): Promise<ChartDataResponse<FileUploadsData[]>> => {
+    try {
+      const response = await axios.get('/charts/file-uploads');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getFileTypes: async (): Promise<ChartDataResponse<FileTypesData[]>> => {
+    try {
+      const response = await axios.get('/charts/file-types');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getDashboardOverview: async (): Promise<ChartDataResponse<DashboardOverviewData>> => {
+    try {
+      const response = await axios.get('/charts/dashboard-overview');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 78 - 0
client/admin/api/device_alert_rule.ts

@@ -0,0 +1,78 @@
+import axios from 'axios';
+import { DeviceAlert, DeviceAlertRule } from "../../share/monitorTypes.ts";
+
+// 告警相关响应类型
+interface DeviceAlertDataResponse {
+    data: DeviceAlert[];
+    total: number;
+    page: number;
+    pageSize: number;
+  }
+  
+  interface DeviceAlertResponse {
+    data: DeviceAlert;
+    message?: string;
+  }
+
+  
+interface AlertDeleteResponse {
+    message: string;
+  }
+
+// 设备告警规则API接口定义
+export const DeviceAlertRuleAPI = {
+    // 获取设备告警规则
+    getDeviceAlertRules: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      device_id?: number, 
+      rule_type?: string
+    }): Promise<DeviceAlertDataResponse> => {
+      try {
+        const response = await axios.get(`/device-alert-rules`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个设备告警规则
+    getDeviceAlertRule: async (id: number): Promise<DeviceAlertResponse> => {
+      try {
+        const response = await axios.get(`/device-alert-rules/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建设备告警规则
+    createDeviceAlertRule: async (data: Partial<DeviceAlertRule>): Promise<DeviceAlertResponse> => {
+      try {
+        const response = await axios.post(`/device-alert-rules`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新设备告警规则
+    updateDeviceAlertRule: async (id: number, data: Partial<DeviceAlertRule>): Promise<DeviceAlertResponse> => {
+      try {
+        const response = await axios.put(`/device-alert-rules/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除设备告警规则
+    deleteDeviceAlertRule: async (id: number): Promise<AlertDeleteResponse> => {
+      try {
+        const response = await axios.delete(`/device-alert-rules/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 93 - 0
client/admin/api/device_instance.ts

@@ -0,0 +1,93 @@
+import axios from "axios";
+import { DeviceInstance, DeviceType, ZichanInfo } from "../../share/monitorTypes.ts";
+
+// 设备实例API接口定义
+interface DeviceInstancesResponse {
+  data: DeviceInstance[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface DeviceInstanceResponse {
+  data: DeviceInstance;
+  asset_info?: ZichanInfo;
+  type_info?: DeviceType;
+  message?: string;
+}
+
+interface DeviceInstanceCreateResponse {
+  message: string;
+  data: DeviceInstance;
+}
+
+interface DeviceInstanceUpdateResponse {
+  message: string;
+  data: DeviceInstance;
+}
+
+interface DeviceInstanceDeleteResponse {
+  message: string;
+  id: number;
+}
+
+export const DeviceInstanceAPI = {
+  // 获取设备实例列表
+  getDeviceInstances: async (params?: { 
+    page?: number, 
+    limit?: number, 
+    type_id?: number,
+    protocol?: string,
+    status?: number
+  }): Promise<DeviceInstancesResponse> => {
+    try {
+      const response = await axios.get('/device/instances', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取单个设备实例信息
+  getDeviceInstance: async (id: number): Promise<DeviceInstanceResponse> => {
+    try {
+      const response = await axios.get(`/device/instances/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 创建设备实例
+  createDeviceInstance: async (data: Partial<DeviceInstance>): Promise<DeviceInstanceCreateResponse> => {
+    try {
+      const response = await axios.post('/device/instances', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 更新设备实例
+  updateDeviceInstance: async (id: number, data: Partial<DeviceInstance>): Promise<DeviceInstanceUpdateResponse> => {
+    try {
+      const response = await axios.put(`/device/instances/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 删除设备实例
+  deleteDeviceInstance: async (id: number): Promise<DeviceInstanceDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/device/instances/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 101 - 0
client/admin/api/device_type.ts

@@ -0,0 +1,101 @@
+import axios from "axios";
+import { DeviceType } from "../../share/monitorTypes.ts";
+
+// 设备类型API接口定义
+interface DeviceTypeResponse {
+  data: DeviceType[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface DeviceTypeDetailResponse {
+  data: DeviceType;
+  message?: string;
+}
+
+interface DeviceTypeCreateResponse {
+  message: string;
+  data: DeviceType;
+}
+
+interface DeviceTypeUpdateResponse {
+  message: string;
+  data: DeviceType;
+}
+
+interface DeviceTypeDeleteResponse {
+  message: string;
+  id: number;
+}
+
+export const DeviceTypeAPI = {
+  // 获取设备类型列表
+  getDeviceTypes: async (params?: { 
+    page?: number, 
+    pageSize?: number, 
+    code?: string,
+    name?: string,
+    is_enabled?: boolean
+  }): Promise<DeviceTypeResponse> => {
+    try {
+      const response = await axios.get('/device/types', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取单个设备类型信息
+  getDeviceType: async (id: number): Promise<DeviceTypeDetailResponse> => {
+    try {
+      const response = await axios.get(`/device/types/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 创建设备类型
+  createDeviceType: async (data: Partial<DeviceType>): Promise<DeviceTypeCreateResponse> => {
+    try {
+      const response = await axios.post('/device/types', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 更新设备类型
+  updateDeviceType: async (id: number, data: Partial<DeviceType>): Promise<DeviceTypeUpdateResponse> => {
+    try {
+      const response = await axios.put(`/device/types/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 删除设备类型
+  deleteDeviceType: async (id: number): Promise<DeviceTypeDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/device/types/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取设备类型图标
+  getTypeIcons: async (): Promise<{data: Record<string, string>, success: boolean}> => {
+    try {
+      const response = await axios.get('/device/types/icons');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 159 - 0
client/admin/api/files.ts

@@ -0,0 +1,159 @@
+import axios from 'axios';
+import type { FileLibrary, FileCategory } from '../../share/types.ts';
+import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
+
+interface FileUploadPolicyResponse {
+  message: string;
+  data: MinioUploadPolicy | OSSUploadPolicy;
+}
+
+interface FileListResponse {
+  message: string;
+  data: {
+    list: FileLibrary[];
+    pagination: {
+      current: number;
+      pageSize: number;
+      total: number;
+    };
+  };
+}
+
+interface FileSaveResponse {
+  message: string;
+  data: FileLibrary;
+}
+
+interface FileInfoResponse {
+  message: string;
+  data: FileLibrary;
+}
+
+interface FileDeleteResponse {
+  message: string;
+}
+
+interface FileCategoryListResponse {
+  data: FileCategory[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+interface FileCategoryCreateResponse {
+  message: string;
+  data: FileCategory;
+}
+
+interface FileCategoryUpdateResponse {
+  message: string;
+  data: FileCategory;
+}
+
+interface FileCategoryDeleteResponse {
+  message: string;
+}
+
+export const FileAPI = {
+  getUploadPolicy: async (filename: string, prefix: string = 'uploads/', maxSize: number = 10 * 1024 * 1024): Promise<FileUploadPolicyResponse> => {
+    try {
+      const response = await axios.get('/upload/policy', {
+        params: { filename, prefix, maxSize } 
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  saveFileInfo: async (fileData: Partial<FileLibrary>): Promise<FileSaveResponse> => {
+    try {
+      const response = await axios.post('/upload/save', fileData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getFileList: async (params?: {
+    page?: number,
+    pageSize?: number,
+    category_id?: number,
+    fileType?: string,
+    keyword?: string
+  }): Promise<FileListResponse> => {
+    try {
+      const response = await axios.get('/upload/list', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getFileInfo: async (id: number): Promise<FileInfoResponse> => {
+    try {
+      const response = await axios.get(`/upload/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateDownloadCount: async (id: number): Promise<FileDeleteResponse> => {
+    try {
+      const response = await axios.post(`/upload/${id}/download`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  deleteFile: async (id: number): Promise<FileDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/upload/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getCategories: async (params?: {
+    page?: number,
+    pageSize?: number,
+    search?: string
+  }): Promise<FileCategoryListResponse> => {
+    try {
+      const response = await axios.get('/file-categories', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createCategory: async (data: Partial<FileCategory>): Promise<FileCategoryCreateResponse> => {
+    try {
+      const response = await axios.post('/file-categories', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateCategory: async (id: number, data: Partial<FileCategory>): Promise<FileCategoryUpdateResponse> => {
+    try {
+      const response = await axios.put(`/file-categories/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  deleteCategory: async (id: number): Promise<FileCategoryDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/file-categories/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 41 - 0
client/admin/api/index.ts

@@ -0,0 +1,41 @@
+import axios from 'axios';
+
+// 基础配置
+export const API_BASE_URL = '/api';
+// 全局axios配置
+axios.defaults.baseURL = API_BASE_URL;
+
+// 获取OSS完整URL
+export const getOssUrl = (path: string): string => {
+  // 获取全局配置中的OSS_HOST,如果不存在使用默认值
+  const ossHost = (window.CONFIG?.OSS_BASE_URL) || '';
+  // 确保path不以/开头
+  const ossPath = path.startsWith('/') ? path.substring(1) : path;
+  return `${ossHost}/${ossPath}`;
+};
+
+export * from './auth.ts';
+export * from './users.ts';
+export * from './files.ts';
+export * from './theme.ts';
+export * from './charts.ts';
+export * from './messages.ts';
+export * from './sys.ts';
+export * from './know_info.ts';
+export * from './maps.ts';
+export * from './zichan.ts'
+export * from './zichan_category.ts'
+export * from './zichan_area.ts'
+export * from './zichan_transfer.ts'
+export * from './device_instance.ts'
+export * from './device_type.ts'
+export * from './rack.ts'
+export * from './rack_server.ts'
+export * from './rack_server_type.ts'
+export * from './monitor.ts'
+export * from './alert.ts'
+export * from './alert_handle.ts'
+export * from './alert_notify_config.ts'
+export * from './device_alert_rule.ts'
+export * from './monitor_charts.ts'
+export * from './work_orders.ts'

+ 301 - 0
client/admin/api/inspections.ts

@@ -0,0 +1,301 @@
+import axios from 'axios';
+
+// 检查模板类型
+export interface InspectionTemplate {
+  id: number;
+  name: string;
+  description?: string;
+  items: InspectionItem[];
+  createdAt: string;
+  updatedAt: string;
+}
+
+export interface InspectionItem {
+  id: number;
+  name: string;
+  description?: string;
+  required: boolean;
+}
+
+// 检查任务类型
+export interface InspectionTask {
+  id: number;
+  templateId: number;
+  name: string;
+  taskNo: string;
+  description?: string;
+  status: 'pending' | 'in_progress' | 'completed' | 'failed';
+  startTime?: string;
+  endTime?: string;
+  createdAt: string;
+  updatedAt: string;
+  schedule_type?: 'manual' | 'scheduled' | 'yearly';
+  cronExpression?: string;
+  intervalDays?: number;
+  deviceTypes?: string[];
+  run_immediately?: boolean;
+  progress?: number;
+  checked_count?: number;
+  issues_found?: number;
+}
+
+// 检查结果类型
+export interface InspectionResult {
+  id: number;
+  taskId: number;
+  itemId: number;
+  status: 'passed' | 'failed' | 'skipped';
+  notes?: string;
+  createdAt: string;
+}
+
+// 报告接收者类型
+export interface ReportReceiver {
+  id: number;
+  name: string;
+  email: string;
+  createdAt: string;
+}
+
+// API响应类型
+export interface ListResponse<T> {
+  data: T[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+export interface SingleResponse<T> {
+  data: T;
+  message?: string;
+}
+
+export interface CreateResponse<T> {
+  data: T;
+  message: string;
+}
+
+export interface UpdateResponse<T> {
+  data: T;
+  message: string;
+}
+
+export interface DeleteResponse {
+  message: string;
+}
+
+// 检查API接口定义
+export const InspectionsAPI = {
+  // 模板相关API
+  getTemplates: async (params?: {
+    page?: number;
+    limit?: number;
+    name?: string;
+  }): Promise<ListResponse<InspectionTemplate>> => {
+    try {
+      const response = await axios.get('/inspections/templates', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createTemplate: async (data: Omit<InspectionTemplate, 'id' | 'createdAt' | 'updatedAt'>): Promise<CreateResponse<InspectionTemplate>> => {
+    try {
+      const response = await axios.post('/inspections/templates', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateTemplate: async (id: number, data: Partial<InspectionTemplate>): Promise<UpdateResponse<InspectionTemplate>> => {
+    try {
+      const response = await axios.put(`/inspections/templates/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createAutoInspectionTask: async (data: {
+    taskNo: string;
+    cronExpression?: string;
+    intervalDays?: number;
+    deviceTypes?: string[];
+    reportReceivers?: string[];
+  }): Promise<CreateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post('/inspections/auto-tasks', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  deleteTemplate: async (id: number): Promise<DeleteResponse> => {
+    try {
+      const response = await axios.delete(`/inspections/templates/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 任务相关API
+  getTasks: async (params?: {
+    page?: number;
+    limit?: number;
+    status?: string;
+    templateId?: number;
+  }): Promise<ListResponse<InspectionTask>> => {
+    try {
+      const response = await axios.get('/inspections/tasks', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createTask: async (data: Omit<InspectionTask, 'id' | 'status' | 'createdAt' | 'updatedAt'>): Promise<CreateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post('/inspections/tasks', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  createAutoTask: async (data: {
+    name: string;
+    intervalDays: number;
+    deviceTypes?: string[];
+  }): Promise<CreateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post('/inspections/tasks/auto', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  runManualTask: async (data: {
+    deviceTypes?: string[];
+  }): Promise<UpdateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post('/inspections/tasks/manual', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  runTask: async (id: number): Promise<UpdateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post(`/inspections/tasks/${id}/run`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 结果相关API
+  getResults: async (params?: {
+    page?: number;
+    limit?: number;
+    taskId?: number;
+    status?: string;
+  }): Promise<ListResponse<InspectionResult>> => {
+    try {
+      const response = await axios.get('/inspections/results', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  exportReport: async (id: number): Promise<Blob> => {
+    try {
+      const response = await axios.get(`/inspections/results/${id}/export`, {
+        responseType: 'blob'
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 接收者相关API
+  getReceivers: async (params?: {
+    page?: number;
+    limit?: number;
+  }): Promise<ListResponse<ReportReceiver>> => {
+    try {
+      const response = await axios.get('/inspections/receivers', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  addReceiver: async (data: Omit<ReportReceiver, 'id' | 'createdAt'>): Promise<CreateResponse<ReportReceiver>> => {
+    try {
+      const response = await axios.post('/inspections/receivers', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  removeReceiver: async (id: number): Promise<DeleteResponse> => {
+    try {
+      const response = await axios.delete(`/inspections/receivers/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 新年巡检专用API
+  createNewYearTask: async (data: {
+    id: string;
+    time: string;
+    deviceType: string;
+    status: string;
+    issuesFound: number;
+    receiverId: string;
+  }): Promise<CreateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post('/inspections/tasks/new-year', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateProgress: async (id: number, data: {
+    progress: number;
+    checkedCount: number;
+    issuesFound: number;
+  }): Promise<UpdateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.put(`/inspections/tasks/${id}/progress`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  completeInspection: async (id: number, results: {
+    itemId: number;
+    status: 'passed' | 'failed' | 'skipped';
+    notes?: string;
+  }[]): Promise<UpdateResponse<InspectionTask>> => {
+    try {
+      const response = await axios.post(`/inspections/tasks/${id}/complete`, { results });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 92 - 0
client/admin/api/know_info.ts

@@ -0,0 +1,92 @@
+import axios from 'axios';
+import type { KnowInfo } from '../../share/types.ts';
+
+export interface KnowInfoListResponse {
+  data: KnowInfo[];
+  pagination: {
+    current: number;
+    pageSize: number;
+    total: number;
+    totalPages: number;
+  };
+}
+
+interface KnowInfoResponse {
+  data: KnowInfo;
+  message?: string;
+}
+
+interface KnowInfoCreateResponse {
+  message: string;
+  data: KnowInfo;
+}
+
+interface KnowInfoUpdateResponse {
+  message: string;
+  data: KnowInfo;
+}
+
+interface KnowInfoDeleteResponse {
+  message: string;
+  id: number;
+}
+
+
+// 知识库API
+export const KnowInfoAPI = {
+  // 获取知识库列表
+  getKnowInfos: async (params?: {
+    page?: number;
+    pageSize?: number;
+    title?: string;
+    category?: string;
+    tags?: string;
+  }): Promise<KnowInfoListResponse> => {
+    try {
+      const response = await axios.get('/know-infos', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取单个知识详情
+  getKnowInfo: async (id: number): Promise<KnowInfoResponse> => {
+    try {
+      const response = await axios.get(`/know-infos/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 创建知识
+  createKnowInfo: async (data: Partial<KnowInfo>): Promise<KnowInfoCreateResponse> => {
+    try {
+      const response = await axios.post('/know-infos', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新知识
+  updateKnowInfo: async (id: number, data: Partial<KnowInfo>): Promise<KnowInfoUpdateResponse> => {
+    try {
+      const response = await axios.put(`/know-infos/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除知识
+  deleteKnowInfo: async (id: number): Promise<KnowInfoDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/know-infos/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 62 - 0
client/admin/api/maps.ts

@@ -0,0 +1,62 @@
+import axios from 'axios';
+import type {
+ LoginLocation, LoginLocationDetail,
+} from '../../share/types.ts';
+
+
+// 地图相关API的接口类型定义
+export interface LoginLocationResponse {
+  message: string;
+  data: LoginLocation[];
+}
+
+export interface LoginLocationDetailResponse {
+  message: string;
+  data: LoginLocationDetail;
+}
+
+export interface LoginLocationUpdateResponse {
+  message: string;
+  data: LoginLocationDetail;
+}
+
+// 地图相关API
+export const MapAPI = {
+  // 获取地图标记点数据
+  getMarkers: async (params?: { 
+    startTime?: string; 
+    endTime?: string; 
+    userId?: number 
+  }): Promise<LoginLocationResponse> => {
+    try {
+      const response = await axios.get(`/map/markers`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取登录位置详情
+  getLocationDetail: async (locationId: number): Promise<LoginLocationDetailResponse> => {
+    try {
+      const response = await axios.get(`/map/location/${locationId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新登录位置信息
+  updateLocation: async (locationId: number, data: { 
+    longitude: number; 
+    latitude: number; 
+    location_name?: string; 
+  }): Promise<LoginLocationUpdateResponse> => {
+    try {
+      const response = await axios.put(`/map/location/${locationId}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 79 - 0
client/admin/api/messages.ts

@@ -0,0 +1,79 @@
+import axios from 'axios';
+import type { UserMessage, Message } from '../../share/types.ts';
+
+interface MessagesResponse {
+  data: UserMessage[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface MessageResponse {
+  data: Message;
+  message?: string;
+}
+
+interface MessageCountResponse {
+  count: number;
+}
+
+export const MessageAPI = {
+  getMessages: async (params?: {
+    page?: number,
+    pageSize?: number,
+    type?: string,
+    status?: string,
+    search?: string
+  }): Promise<MessagesResponse> => {
+    try {
+      const response = await axios.get('/messages', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  sendMessage: async (data: {
+    title: string,
+    content: string,
+    type: string,
+    receiver_ids: number[]
+  }): Promise<MessageResponse> => {
+    try {
+      const response = await axios.post('/messages', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getUnreadCount: async (): Promise<MessageCountResponse> => {
+    try {
+      const response = await axios.get('/messages/count/unread');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  markAsRead: async (id: number): Promise<MessageResponse> => {
+    try {
+      const response = await axios.post(`/messages/${id}/read`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  deleteMessage: async (id: number): Promise<MessageResponse> => {
+    try {
+      const response = await axios.delete(`/messages/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 47 - 0
client/admin/api/modbus_rtu_device.ts

@@ -0,0 +1,47 @@
+import axios from 'axios';
+
+export interface DeviceMonitorData {
+  id: number;
+  device_id: number;
+  device_name: string;
+  protocol: string;
+  address: string;
+  metric_type: string;
+  metric_value: number;
+  unit?: string;
+  status: string;
+  collect_time: Date;
+}
+
+export const ModbusRtuDeviceAPI = {
+  // 获取Modbus RTU设备监控数据
+  getMonitorData: async (params: {
+    page?: number;
+    pageSize?: number;
+    device_id?: number;
+    device_name?: string;
+    protocol?: string;
+    address?: string;
+    metric_type?: string;
+    status?: string;
+  }) => {
+    const response = await axios.get('/modbus-rtu-device/monitor-data', { params });
+    return {
+      data: response.data.data || [],
+      total: response.data.total || 0,
+    };
+  },
+
+  // 测试Modbus RTU设备连接
+  testConnection: async (data: {
+    protocol: string;
+    address: string;
+    baud_rate: number;
+    data_bits: number;
+    stop_bits: number;
+    parity: string;
+  }) => {
+    const response = await axios.post('/modbus-rtu-device/test-connection', data);
+    return response.data;
+  },
+};

+ 179 - 0
client/admin/api/monitor.ts

@@ -0,0 +1,179 @@
+import axios from 'axios';
+import { DeviceMapStats, DeviceMonitorData, DeviceStatus, DeviceTreeNode, DeviceTreeStats, MapViewDevice } from "../../share/monitorTypes.ts";
+
+interface DeviceMonitorDataResponse {
+    data: DeviceMonitorData[];
+    total: number;
+    page: number;
+    pageSize: number;
+  }
+  
+  interface DeviceMonitorResponse {
+    data: DeviceMonitorData;
+    message?: string;
+  }
+  
+  interface MonitorCreateResponse {
+    data: DeviceMonitorData;
+    message: string;
+  }
+  
+  interface MonitorUpdateResponse {
+    data: DeviceMonitorData;
+    message: string;
+  }
+  
+  interface MonitorDeleteResponse {
+    message: string;
+  }
+
+// 监控API接口定义
+export const MonitorAPI = {
+    // 获取监控数据
+    getMonitorData: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      device_id?: number, 
+      device_type?: string, 
+      start_time?: string, 
+      end_time?: string
+    }): Promise<DeviceMonitorDataResponse> => {
+      try {
+        const response = await axios.get(`/monitor/data`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取设备树数据
+    getDeviceTree: async (params?: {
+      status?: string,
+      keyword?: string
+    }): Promise<{ data: DeviceTreeNode[] }> => {
+      try {
+        const response = await axios.get(`/monitor/devices/tree`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取设备树统计数据
+    getDeviceTreeStats: async (): Promise<{ data: DeviceTreeStats }> => {
+      try {
+        const response = await axios.get(`/monitor/devices/tree/statistics`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取设备地图数据
+    getDeviceMapData: async (params?: { 
+      type_code?: string, 
+      device_status?: DeviceStatus, 
+      keyword?: string,
+      device_id?: number
+    }): Promise<{ data: MapViewDevice[], stats: DeviceMapStats }> => {
+      try {
+        const response = await axios.get(`/monitor/devices/map`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个监控数据
+    getMonitor: async (id: number): Promise<DeviceMonitorResponse> => {
+      try {
+        const response = await axios.get(`/monitor/data/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建监控数据
+    createMonitor: async (data: Partial<DeviceMonitorData>): Promise<MonitorCreateResponse> => {
+      try {
+        const response = await axios.post(`/monitor/data`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新监控数据
+    updateMonitor: async (id: number, data: Partial<DeviceMonitorData>): Promise<MonitorUpdateResponse> => {
+      try {
+        const response = await axios.put(`/monitor/data/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除监控数据
+    deleteMonitor: async (id: number): Promise<MonitorDeleteResponse> => {
+      try {
+        const response = await axios.delete(`/monitor/data/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+    };
+
+  // 获取烟雾/水浸传感器状态
+  export async function getDeviceStatus(params: {
+    device_id: number;
+    device_type: 'smoke' | 'water';
+  }): Promise<{
+    status: 0 | 1;
+    timestamp: string;
+  }> {
+    try {
+      const response = await axios.get(`/monitor/devices/smoke-water/status`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  // 获取烟雾/水浸传感器历史数据
+  export async function getDeviceHistory(params: {
+    device_id: number;
+    device_type: 'smoke' | 'water';
+    start_time: string;
+    end_time: string;
+    interval?: number;
+  }): Promise<{
+    data: Array<{
+      timestamp: string;
+      status: 0 | 1;
+    }>;
+  }> {
+    try {
+      const response = await axios.get(`/monitor/devices/smoke-water/history`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+
+  // 获取最新温湿度数据
+  export async function getLatestTemperatureHumidity(params: {
+    device_id?: number;
+  } = {}): Promise<{
+    temperature: number;
+    humidity: number;
+    timestamp: string;
+  }> {
+    try {
+      const response = await axios.get(`/monitor/data/latest-temperature-humidity`, { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }

+ 69 - 0
client/admin/api/monitor_charts.ts

@@ -0,0 +1,69 @@
+import axios from 'axios';
+import { AlarmChartData, CategoryChartData, CategoryChartDataWithPercent, OnlineRateChartData, StateChartData, StateChartDataWithPercent } from "../../share/monitorTypes.ts";
+
+export const MonitorChartsAPI = {
+    // 资产分类数据查询
+    fetchCategoryData: async (): Promise<CategoryChartDataWithPercent[]> => {
+      const res = await axios.get<CategoryChartData[]>(`/big/zichan_category_chart`);
+      
+      // 预先计算百分比
+      const data = res.data;
+      const total = data.reduce((sum: number, item: CategoryChartData) => sum + item['设备数'], 0);
+      
+      // 为每个数据项添加百分比字段
+      return data.map(item => ({
+        ...item,
+        百分比: total > 0 ? (item['设备数'] / total * 100).toFixed(1) : '0'
+      }));
+    },
+  
+    // 在线率变化数据查询
+    fetchOnlineRateData: async (params?: {
+      created_at_gte?: string;
+      created_at_lte?: string;
+      dimension?: string;
+    }): Promise<OnlineRateChartData[]> => {
+      // 可选参数
+      // const params = {
+      //   created_at_gte: dayjs().subtract(7, 'day').format('YYYY-MM-DD HH:mm:ss'),
+      //   created_at_lte: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      //   dimension: 'day'
+      // };
+      
+      const res = await axios.get<OnlineRateChartData[]>(`/big/zichan_online_rate_chart`, { params });
+      return res.data;
+    },
+  
+    // 资产流转状态数据查询
+    fetchStateData: async (): Promise<StateChartDataWithPercent[]> => {
+      const res = await axios.get<StateChartData[]>(`/big/zichan_state_chart`);
+      
+      // 预先计算百分比
+      const data = res.data;
+      const total = data.reduce((sum: number, item: StateChartData) => sum + item['设备数'], 0);
+      
+      // 为每个数据项添加百分比字段
+      return data.map(item => ({
+        ...item,
+        百分比: total > 0 ? (item['设备数'] / total * 100).toFixed(1) : '0'
+      }));
+    },
+  
+    // 告警数据变化查询
+    fetchAlarmData: async (params?: {
+      created_at_gte?: string;
+      created_at_lte?: string;
+      dimension?: string;
+    }): Promise<AlarmChartData[]> => {
+      // 可选参数
+      // const params = {
+      //   created_at_gte: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      //   created_at_lte: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      //   dimension: 'hour'
+      // };
+  
+      
+      const res = await axios.get<AlarmChartData[]>(`/big/zichan_alarm_chart`, { params });
+      return res.data;
+    }
+  };

+ 60 - 0
client/admin/api/rack.ts

@@ -0,0 +1,60 @@
+import axios from 'axios';
+import { RackInfo } from "../../share/monitorTypes.ts";
+// 机柜管理API接口定义
+export const RackAPI = {
+    // 获取机柜列表
+    getRackList: async (params?: { 
+        page?: number, 
+        limit?: number, 
+        rack_name?: string, 
+        rack_code?: string, 
+        area?: string
+    }) => {
+        try {
+        const response = await axios.get(`/racks`, { params });
+        return response.data;
+        } catch (error) {
+        throw error;
+        }
+    },
+
+    // 获取单个机柜信息
+    getRack: async (id: number) => {
+        try {
+        const response = await axios.get(`/racks/${id}`);
+        return response.data;
+        } catch (error) {
+        throw error;
+        }
+    },
+
+    // 创建机柜
+    createRack: async (data: Partial<RackInfo>) => {
+        try {
+        const response = await axios.post(`/racks`, data);
+        return response.data;
+        } catch (error) {
+        throw error;
+        }
+    },
+
+    // 更新机柜
+    updateRack: async (id: number, data: Partial<RackInfo>) => {
+        try {
+        const response = await axios.put(`/racks/${id}`, data);
+        return response.data;
+        } catch (error) {
+        throw error;
+        }
+    },
+
+    // 删除机柜
+    deleteRack: async (id: number) => {
+        try {
+        const response = await axios.delete(`/racks/${id}`);
+        return response.data;
+        } catch (error) {
+        throw error;
+        }
+    }
+};

+ 60 - 0
client/admin/api/rack_server.ts

@@ -0,0 +1,60 @@
+import axios from 'axios';
+import { RackServer } from "../../share/monitorTypes.ts";
+// 机柜服务器API接口定义
+export const RackServerAPI = {
+    // 获取机柜服务器列表
+    getRackServerList: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      rack_id?: number,
+      asset_id?: number,
+      server_type?: string
+    }) => {
+      try {
+        const response = await axios.get(`/rack-servers`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个机柜服务器信息
+    getRackServer: async (id: number) => {
+      try {
+        const response = await axios.get(`/rack-servers/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建机柜服务器
+    createRackServer: async (data: Partial<RackServer>) => {
+      try {
+        const response = await axios.post(`/rack-servers`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新机柜服务器
+    updateRackServer: async (id: number, data: Partial<RackServer>) => {
+      try {
+        const response = await axios.put(`/rack-servers/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除机柜服务器
+    deleteRackServer: async (id: number) => {
+      try {
+        const response = await axios.delete(`/rack-servers/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 69 - 0
client/admin/api/rack_server_type.ts

@@ -0,0 +1,69 @@
+import axios from 'axios';
+import { RackServerType } from "../../share/monitorTypes.ts";
+
+// 机柜服务器类型API响应类型
+interface RackServerTypeResponse {
+    data: RackServerType[];
+    pagination: {
+      total: number;
+      current: number;
+      pageSize: number;
+    };
+  }
+// 机柜服务器类型API接口定义
+export const RackServerTypeAPI = {
+    // 获取机柜服务器类型列表
+    getRackServerTypeList: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      name?: string,
+      code?: string
+    }): Promise<RackServerTypeResponse> => {
+      try {
+        const response = await axios.get(`/rack-server-types`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个机柜服务器类型信息
+    getRackServerType: async (id: number) => {
+      try {
+        const response = await axios.get(`/rack-server-types/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建机柜服务器类型
+    createRackServerType: async (data: Partial<RackServerType>) => {
+      try {
+        const response = await axios.post(`/rack-server-types`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新机柜服务器类型
+    updateRackServerType: async (id: number, data: Partial<RackServerType>) => {
+      try {
+        const response = await axios.put(`/rack-server-types/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除机柜服务器类型
+    deleteRackServerType: async (id: number) => {
+      try {
+        const response = await axios.delete(`/rack-server-types/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 25 - 0
client/admin/api/sms.ts

@@ -0,0 +1,25 @@
+import axios from 'axios'
+
+export const smsApi = {
+  checkLogin: () => axios.get('/sms/check-login'),
+  login: (data: { username: string; password: string }) =>
+    axios.post('/sms/login', data),
+  send: (data: { phone: string; content: string; taskId?: string }) =>
+    axios.post('/sms/send', data),
+  getStatus: () => axios.get('/sms/status'),
+  getList: () => axios.get('/sms/list')
+}
+
+export type SmsStatus = {
+  signalStrength: number
+  carrier: string
+  mode: string
+}
+
+export type SmsItem = {
+  id: string
+  phone: string
+  content: string
+  status: string
+  createdAt: string
+}

+ 47 - 0
client/admin/api/sys.ts

@@ -0,0 +1,47 @@
+import axios from 'axios';
+
+import type {
+ SystemSetting, SystemSettingGroupData,
+} from '../../share/types.ts';
+
+export const SystemAPI = {
+  // 获取所有系统设置
+  getSettings: async (): Promise<SystemSettingGroupData[]> => {
+    try {
+      const response = await axios.get('/settings');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取指定分组的系统设置
+  getSettingsByGroup: async (group: string): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.get(`/settings/group/${group}`);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新系统设置
+  updateSettings: async (settings: Partial<SystemSetting>[]): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.put('/settings', settings);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 重置系统设置
+  resetSettings: async (): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.post('/settings/reset');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 36 - 0
client/admin/api/theme.ts

@@ -0,0 +1,36 @@
+import axios from 'axios';
+import type { ThemeSettings } from '../../share/types.ts';
+
+export interface ThemeSettingsResponse {
+  message: string;
+  data: ThemeSettings;
+}
+
+export const ThemeAPI = {
+  getThemeSettings: async (): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.get('/theme');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  updateThemeSettings: async (themeData: Partial<ThemeSettings>): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.put('/theme', themeData);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  resetThemeSettings: async (): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.post('/theme/reset');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 79 - 0
client/admin/api/users.ts

@@ -0,0 +1,79 @@
+import axios from 'axios';
+import type { User } from '../../share/types.ts';
+
+interface UsersResponse {
+  data: User[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface UserResponse {
+  data: User;
+  message?: string;
+}
+
+interface UserCreateResponse {
+  message: string;
+  data: User;
+}
+
+interface UserUpdateResponse {
+  message: string;
+  data: User;
+}
+
+interface UserDeleteResponse {
+  message: string;
+  id: number;
+}
+
+export const UserAPI = {
+  getUsers: async (params?: { page?: number, limit?: number, search?: string }): Promise<UsersResponse> => {
+    try {
+      const response = await axios.get('/users', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  getUser: async (userId: number): Promise<UserResponse> => {
+    try {
+      const response = await axios.get(`/users/${userId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  createUser: async (userData: Partial<User>): Promise<UserCreateResponse> => {
+    try {
+      const response = await axios.post('/users', userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  updateUser: async (userId: number, userData: Partial<User>): Promise<UserUpdateResponse> => {
+    try {
+      const response = await axios.put(`/users/${userId}`, userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  deleteUser: async (userId: number): Promise<UserDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/users/${userId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 308 - 0
client/admin/api/work_orders.ts

@@ -0,0 +1,308 @@
+import axios from 'axios';
+import { WorkOrder, WorkOrderSettings, WorkOrderStatus, WorkOrderPriority } from '../../share/monitorTypes.ts'
+
+// 分类数据缓存
+let categoriesCache: string[] | null = null;
+let cacheExpireTime = 0;
+const CACHE_EXPIRE_DURATION = 5 * 60 * 1000; // 5分钟缓存
+
+interface WorkOrderListParams {
+  page?: number;
+  pageSize?: number;
+  status?: WorkOrderStatus;
+  priority?: WorkOrderPriority;
+  device_id?: number;
+  creator_id?: number;
+}
+
+interface WorkOrderListResponse {
+  data: WorkOrder[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface WorkOrderDetailResponse {
+  data: WorkOrder;
+  message?: string;
+}
+
+interface WorkOrderCreateResponse {
+  message: string;
+  data: WorkOrder;
+}
+
+interface WorkOrderUpdateResponse {
+  message: string;
+  data: WorkOrder;
+}
+
+interface WorkOrderStatusChangeResponse {
+  message: string;
+  status: string;
+  history_id?: string;
+}
+
+interface WorkOrderDeadlineResponse {
+  data: {
+    deadline: string;
+    remaining_hours: number;
+    is_overdue: boolean;
+  };
+}
+
+interface WorkOrderHistoryItem {
+  id: string;
+  status_from: string;
+  status_to: string;
+  operator: string;
+  comment?: string;
+  created_at: string;
+}
+
+interface WorkOrderHistoryResponse {
+  data: WorkOrderHistoryItem[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+  };
+}
+
+interface WorkOrderAssignResponse {
+  message: string;
+  assignee: string;
+}
+
+interface WorkOrderSettingsResponse {
+  data: WorkOrderSettings;
+  message?: string;
+}
+
+interface WorkOrderCategoryResponse {
+  data: string[];
+  message?: string;
+}
+
+interface WorkOrderAttachmentResponse {
+  message: string;
+  data: {
+    id: string;
+    url: string;
+    name: string;
+  };
+}
+
+interface WorkOrderCommentResponse {
+  message: string;
+  data: {
+    id: string;
+    content: string;
+    created_at: string;
+    author: string;
+  }[];
+}
+
+export const WorkOrderAPI = {
+  getList: async (params?: WorkOrderListParams): Promise<WorkOrderListResponse> => {
+    try {
+      const response = await axios.get('/work-orders', { params });
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        const errorMessage = error.response?.data?.message || error.message;
+        throw new Error(`获取工单列表失败: ${errorMessage}`);
+      }
+      throw new Error('获取工单列表失败: 未知错误');
+    }
+  },
+  
+  create: async (data: {
+    title: string;
+    device_id?: number;
+    problem_desc: string;
+    problem_type: string;
+    priority: WorkOrderPriority;
+    deadline?: Date;
+    attachments?: Array<{
+      id: string;
+      url: string;
+      name: string;
+    }>;
+  }): Promise<WorkOrderCreateResponse> => {
+    try {
+      const response = await axios.post('/work-orders', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  getDetail: async (id: string): Promise<WorkOrderDetailResponse> => {
+    try {
+      const response = await axios.get(`/work-orders/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  update: async (id: string, data: {
+    status?: WorkOrderStatus;
+    priority?: WorkOrderPriority;
+    feedback?: string;
+    assignee_id?: number;
+    deadline?: Date;
+  }): Promise<WorkOrderUpdateResponse> => {
+    try {
+      const response = await axios.put(`/work-orders/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  changeStatus: async (id: string, status: string, operator: string, comment?: string): Promise<WorkOrderStatusChangeResponse> => {
+    try {
+      const response = await axios.post(`/work-orders/${id}/status`, {
+        status,
+        operator,
+        comment
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getDeadline: async (id: string): Promise<WorkOrderDeadlineResponse> => {
+    try {
+      const response = await axios.get(`/work-orders/${id}/deadline`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getHistory: async (id: string, page = 1, pageSize = 10): Promise<WorkOrderHistoryResponse> => {
+    try {
+      const response = await axios.get(`/work-orders/${id}/history`, {
+        params: { page, pageSize }
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  getStatusHistory: async (id: string): Promise<WorkOrderHistoryResponse> => {
+    try {
+      const response = await axios.get(`/work-orders/${id}/status-history`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  assign: async (id: string, assignee: string): Promise<WorkOrderAssignResponse> => {
+    try {
+      const response = await axios.post(`/work-orders/${id}/assign`, { assignee });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  getSettings: async (): Promise<WorkOrderSettingsResponse> => {
+    try {
+      const response = await axios.get('/work-orders/settings');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  updateSettings: async (data: WorkOrderSettings): Promise<WorkOrderSettingsResponse> => {
+    try {
+      const response = await axios.post('/work-orders/settings', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 新增分类相关API
+  getCategories: async (): Promise<WorkOrderCategoryResponse> => {
+    // 检查缓存是否有效
+    if (categoriesCache && Date.now() < cacheExpireTime) {
+      return { data: categoriesCache };
+    }
+    
+    try {
+      const response = await axios.get('/work-orders/categories');
+      categoriesCache = response.data.data;
+      cacheExpireTime = Date.now() + CACHE_EXPIRE_DURATION;
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        const errorMessage = error.response?.data?.message || error.message;
+        throw new Error(`获取分类失败: ${errorMessage}`);
+      }
+      throw new Error('获取分类失败: 未知错误');
+    }
+  },
+
+  // 新增附件相关API
+  uploadAttachment: async (id: string, file: File): Promise<WorkOrderAttachmentResponse> => {
+    try {
+      const formData = new FormData();
+      formData.append('file', file);
+      const response = await axios.post(`/work-orders/${id}/attachments`, formData, {
+        headers: {
+          'Content-Type': 'multipart/form-data'
+        }
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 新增评论相关API
+  getComments: async (id: string): Promise<WorkOrderCommentResponse> => {
+    try {
+      const response = await axios.get(`/work-orders/${id}/comments`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  addComment: async (id: string, content: string): Promise<WorkOrderCommentResponse> => {
+    try {
+      const response = await axios.post(`/work-orders/${id}/comments`, { content });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  exportList: async (params?: WorkOrderListParams): Promise<Blob> => {
+    try {
+      const response = await axios.get('/work-orders/export', {
+        params,
+        responseType: 'blob'
+      });
+      return response.data;
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        const errorMessage = error.response?.data?.message || error.message;
+        throw new Error(`导出工单失败: ${errorMessage}`);
+      }
+      throw new Error('导出工单失败: 未知错误');
+    }
+  },
+};

BIN
client/admin/api/yangantubiao.png


+ 61 - 0
client/admin/api/zichan.ts

@@ -0,0 +1,61 @@
+import axios from "axios";
+import { DeviceCategory, DeviceStatus, ZichanInfo } from "../../share/monitorTypes.ts";
+
+// 资产管理API
+export const ZichanAPI = {
+    // 获取资产列表
+    getZichanList: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      asset_name?: string, 
+      device_category?: DeviceCategory, 
+      device_status?: DeviceStatus 
+    }) => {
+      try {
+        const response = await axios.get(`/zichan`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个资产
+    getZichan: async (id: number) => {
+      try {
+        const response = await axios.get(`/zichan/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建资产
+    createZichan: async (data: Partial<ZichanInfo>) => {
+      try {
+        const response = await axios.post(`/zichan`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新资产
+    updateZichan: async (id: number, data: Partial<ZichanInfo>) => {
+      try {
+        const response = await axios.put(`/zichan/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除资产
+    deleteZichan: async (id: number) => {
+      try {
+        const response = await axios.delete(`/zichan/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 60 - 0
client/admin/api/zichan_area.ts

@@ -0,0 +1,60 @@
+import axios from "axios";
+import { ZichanArea } from "../../share/monitorTypes.ts";
+
+// 资产归属区域API接口定义
+export const ZichanAreaAPI = {
+    // 获取资产归属区域列表
+    getZichanAreaList: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      name?: string,
+      code?: string
+    }) => {
+      try {
+        const response = await axios.get(`/zichan-areas`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个资产归属区域信息
+    getZichanArea: async (id: number) => {
+      try {
+        const response = await axios.get(`/zichan-areas/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建资产归属区域
+    createZichanArea: async (data: Partial<ZichanArea>) => {
+      try {
+        const response = await axios.post(`/zichan-areas`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新资产归属区域
+    updateZichanArea: async (id: number, data: Partial<ZichanArea>) => {
+      try {
+        const response = await axios.put(`/zichan-areas/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除资产归属区域
+    deleteZichanArea: async (id: number) => {
+      try {
+        const response = await axios.delete(`/zichan-areas/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 60 - 0
client/admin/api/zichan_category.ts

@@ -0,0 +1,60 @@
+import axios from "axios";
+import { ZichanCategory } from "../../share/monitorTypes.ts";
+
+// 资产分类API接口定义
+export const ZichanCategoryAPI = {
+    // 获取资产分类列表
+    getZichanCategoryList: async (params?: { 
+      page?: number, 
+      limit?: number, 
+      name?: string,
+      code?: string
+    }) => {
+      try {
+        const response = await axios.get(`/zichan-categories`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取单个资产分类信息
+    getZichanCategory: async (id: number) => {
+      try {
+        const response = await axios.get(`/zichan-categories/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建资产分类
+    createZichanCategory: async (data: Partial<ZichanCategory>) => {
+      try {
+        const response = await axios.post(`/zichan-categories`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新资产分类
+    updateZichanCategory: async (id: number, data: Partial<ZichanCategory>) => {
+      try {
+        const response = await axios.put(`/zichan-categories/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除资产分类
+    deleteZichanCategory: async (id: number) => {
+      try {
+        const response = await axios.delete(`/zichan-categories/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 54 - 0
client/admin/api/zichan_transfer.ts

@@ -0,0 +1,54 @@
+import axios from "axios";
+import { AssetTransferType, ZichanTransLog } from "../../share/monitorTypes.ts";
+// 资产流转API接口定义
+export const ZichanTransferAPI = {
+    // 获取资产流转记录列表
+    getTransferList: async (params?: { page?: number, limit?: number, asset_id?: number, asset_transfer?: AssetTransferType }) => {
+      try {
+        const response = await axios.get(`/zichan-transfer`, { params });
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 获取资产流转记录详情
+    getTransfer: async (id: number) => {
+      try {
+        const response = await axios.get(`/zichan-transfer/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 创建资产流转记录
+    createTransfer: async (data: Partial<ZichanTransLog>) => {
+      try {
+        const response = await axios.post(`/zichan-transfer`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 更新资产流转记录
+    updateTransfer: async (id: number, data: Partial<ZichanTransLog>) => {
+      try {
+        const response = await axios.put(`/zichan-transfer/${id}`, data);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    },
+    
+    // 删除资产流转记录
+    deleteTransfer: async (id: number) => {
+      try {
+        const response = await axios.delete(`/zichan-transfer/${id}`);
+        return response.data;
+      } catch (error) {
+        throw error;
+      }
+    }
+  };

+ 46 - 0
client/admin/components/ErrorPage.tsx

@@ -0,0 +1,46 @@
+import React from 'react';
+import { useRouteError, useNavigate } from 'react-router';
+import { Alert, Button } from 'antd';
+import { useTheme } from '../hooks_sys.tsx';
+
+export const ErrorPage = () => {
+  const navigate = useNavigate();
+  const { isDark } = useTheme();
+  const error = useRouteError() as any;
+  const errorMessage = error?.statusText || error?.message || '未知错误';
+  
+  return (
+    <div className="flex flex-col items-center justify-center flex-grow p-4"
+      style={{ color: isDark ? '#fff' : 'inherit' }}
+    >
+      <div className="max-w-3xl w-full">
+        <h1 className="text-2xl font-bold mb-4">发生错误</h1>
+        <Alert 
+          type="error"
+          message={error?.message || '未知错误'}
+          description={
+            error?.stack ? (
+              <pre className="text-xs overflow-auto p-2 bg-gray-100 dark:bg-gray-800 rounded">
+                {error.stack}
+              </pre>
+            ) : null
+          }
+          className="mb-4"
+        />
+        <div className="flex gap-4">
+          <Button 
+            type="primary" 
+            onClick={() => navigate(0)}
+          >
+            重新加载
+          </Button>
+          <Button 
+            onClick={() => navigate('/admin')}
+          >
+            返回首页
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 30 - 0
client/admin/components/NotFoundPage.tsx

@@ -0,0 +1,30 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+import { Button } from 'antd';
+import { useTheme } from '../hooks_sys.tsx';
+
+export const NotFoundPage = () => {
+  const navigate = useNavigate();
+  const { isDark } = useTheme();
+  
+  return (
+    <div className="flex flex-col items-center justify-center flex-grow p-4"
+      style={{ color: isDark ? '#fff' : 'inherit' }}
+    >
+      <div className="max-w-3xl w-full">
+        <h1 className="text-2xl font-bold mb-4">404 - 页面未找到</h1>
+        <p className="mb-6 text-gray-600 dark:text-gray-300">
+          您访问的页面不存在或已被移除
+        </p>
+        <div className="flex gap-4">
+          <Button 
+            type="primary" 
+            onClick={() => navigate('/admin')}
+          >
+            返回首页
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+};

+ 446 - 0
client/admin/components_amap.tsx

@@ -0,0 +1,446 @@
+import React, { useEffect, useRef } from 'react';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { getGlobalConfig } from './utils.ts';
+import type { GlobalConfig } from '../share/types.ts';
+import { Spin } from 'antd';
+import './style_amap.css';
+import { MapMode, MarkerData } from '../share/types.ts';
+
+// 在线地图配置
+export const AMAP_ONLINE_CONFIG = {
+  // 高德地图 Web API 密钥
+  API_KEY: getGlobalConfig('MAP_CONFIG')?.KEY || '',
+  // 主JS文件路径
+  MAIN_JS: 'https://webapi.amap.com/maps?v=2.0&key=' + (getGlobalConfig('MAP_CONFIG')?.KEY || ''),
+  // 插件列表
+  PLUGINS: ['AMap.MouseTool', 'AMap.RangingTool', 'AMap.Scale', 'AMap.ToolBar', 'AMap.MarkerCluster'],
+};
+
+export const AMAP_OFFLINE_CONFIG = {
+  // 主JS文件路径
+  MAIN_JS: '/amap/amap3.js?v=2.0',
+  // 插件目录
+  PLUGINS_PATH: '/amap/plugins',
+  // 插件列表
+  PLUGINS: ['AMap.MouseTool', 'AMap.RangingTool', 'AMap.Scale', 'AMap.ToolBar', 'AMap.MarkerCluster'],
+};
+
+// 离线瓦片配置
+export const TILE_CONFIG = {
+  // 瓦片地图基础路径
+  BASE_URL: '/amap/tiles',
+  // 缩放级别范围
+  ZOOMS: [3, 20] as [number, number],
+  // 默认中心点
+  DEFAULT_CENTER: [108.25910334, 27.94292459] as [number, number],
+  // 默认缩放级别
+  DEFAULT_ZOOM: 15
+} as const;
+
+// 地图控件配置
+export const MAP_CONTROLS = {
+  scale: true,
+  toolbar: true,
+  mousePosition: true,
+} as const;
+
+export interface AMapProps {
+  style?: React.CSSProperties;
+  width?: string | number;
+  height?: string | number;
+  center?: [number, number];
+  zoom?: number;
+  mode?: MapMode;
+  onMarkerClick?: (markerData: MarkerData) => void;
+  onClick?: (lnglat: [number, number]) => void;
+  markers?: MarkerData[];
+  showCluster?: boolean;
+  queryKey?: string;
+}
+
+export interface MapConfig {
+  zoom: number;
+  center: [number, number];
+  zooms: [number, number];
+  resizeEnable: boolean;
+  rotateEnable: boolean;
+  pitchEnable: boolean;
+  defaultCursor: string;
+  showLabel: boolean;
+  layers?: any[];
+}
+
+export interface AMapInstance {
+  map: any;
+  setZoomAndCenter: (zoom: number, center: [number, number]) => void;
+  setCenter: (center: [number, number]) => void;
+  setZoom: (zoom: number) => void;
+  destroy: () => void;
+  clearMap: () => void;
+  getAllOverlays: (type: string) => any[];
+  on: (event: string, handler: Function) => void;
+} 
+
+declare global {
+  interface Window {
+    AMap: any;
+    CONFIG?: GlobalConfig;
+  }
+}
+
+const loadScript = (url: string,plugins:string[]): Promise<void> => {
+  return new Promise((resolve, reject) => {
+    const script = document.createElement('script');
+    script.type = 'text/javascript';
+    script.src = url + (plugins.length > 0 ? `&plugin=${plugins.join(',')}` : '');
+    script.onerror = (e) => reject(e);
+    script.onload = () => resolve();
+    document.head.appendChild(script);
+  });
+};
+
+export const useAMapLoader = (mode: MapMode = MapMode.ONLINE) => {
+  return useQuery({
+    queryKey: ['amap-loader', mode],
+    queryFn: async () => {
+      if (typeof window === 'undefined') return null;
+      
+      if (!window.AMap) {
+        const config = mode === MapMode.OFFLINE ? AMAP_OFFLINE_CONFIG : AMAP_ONLINE_CONFIG;
+        await loadScript(config.MAIN_JS,config.PLUGINS);
+      }
+      
+      return window.AMap;
+    },
+    staleTime: Infinity, // 地图脚本加载后永不过期
+    gcTime: Infinity,
+    retry: 2,
+  });
+}; 
+
+export const useAMapClick = (
+  map: any,
+  onClick?: (lnglat: [number, number]) => void
+) => {
+  const mouseTool = useRef<any>(null);
+  const clickHandlerRef = useRef<((e: any) => void) | null>(null);
+
+  useEffect(() => {
+    if (!map) return;
+
+    // 清理旧的点击处理器
+    if (clickHandlerRef.current) {
+      map.off('click', clickHandlerRef.current);
+      clickHandlerRef.current = null;
+    }
+
+    // 如果有点击回调,设置新的点击处理器
+    if (onClick) {
+      clickHandlerRef.current = (e: any) => {
+        const lnglat = e.lnglat.getLng ? 
+          [e.lnglat.getLng(), e.lnglat.getLat()] as [number, number] :
+          [e.lnglat.lng, e.lnglat.lat] as [number, number];
+        onClick(lnglat);
+      };
+      map.on('click', clickHandlerRef.current);
+    }
+
+    return () => {
+      if (clickHandlerRef.current) {
+        map.off('click', clickHandlerRef.current);
+        clickHandlerRef.current = null;
+      }
+    };
+  }, [map, onClick]);
+
+  return {
+    mouseTool: mouseTool.current
+  };
+}; 
+
+// 定义图标配置的类型
+interface MarkerIconConfig {
+  size: [number, number];
+  content: string;
+}
+
+// 默认图标配置
+const DEFAULT_MARKER_ICON: MarkerIconConfig = {
+  size: [25, 34],
+  content: `
+    <svg width="25" height="34" viewBox="0 0 25 34" fill="none" xmlns="http://www.w3.org/2000/svg">
+      <path d="M12.5 0C5.59644 0 0 5.59644 0 12.5C0 21.875 12.5 34 12.5 34C12.5 34 25 21.875 25 12.5C25 5.59644 19.4036 0 12.5 0ZM12.5 17C10.0147 17 8 14.9853 8 12.5C8 10.0147 10.0147 8 12.5 8C14.9853 8 17 10.0147 17 12.5C17 14.9853 14.9853 17 12.5 17Z" fill="#1890ff"/>
+    </svg>
+  `
+};
+
+interface UseAMapMarkersProps {
+  map: any;
+  markers: MarkerData[];
+  showCluster?: boolean;
+  onMarkerClick?: (markerData: MarkerData) => void;
+}
+
+export const useAMapMarkers = ({
+  map,
+  markers,
+  showCluster = true,
+  onMarkerClick,
+}: UseAMapMarkersProps) => {
+  const clusterInstance = useRef<any>(null);
+  const markersRef = useRef<any[]>([]);
+
+  // 优化经纬度格式化函数
+  const toFixedDigit = (num: number, n: number): string => {
+    if (typeof num !== "number") return "";
+    return Number(num).toFixed(n);
+  };
+
+  // 创建标记点
+  const createMarker = (markerData: MarkerData) => {
+    const { longitude, latitude, title, iconUrl } = markerData;
+    
+    // 创建标记点
+    const marker = new window.AMap.Marker({
+      position: [longitude, latitude],
+      title: title,
+      icon: iconUrl ? new window.AMap.Icon({
+        size: DEFAULT_MARKER_ICON.size,
+        imageSize: DEFAULT_MARKER_ICON.size,
+        image: iconUrl
+      }) : new window.AMap.Icon({
+        size: DEFAULT_MARKER_ICON.size,
+        imageSize: DEFAULT_MARKER_ICON.size,
+        image: `data:image/svg+xml;charset=utf-8,${encodeURIComponent(DEFAULT_MARKER_ICON.content)}`
+      }),
+      label: title ? {
+        content: title,
+        direction: 'top'
+      } : undefined
+    });
+
+    // 添加点击事件
+    if (onMarkerClick) {
+      marker.on('click', () => onMarkerClick(markerData));
+    }
+
+    return marker;
+  };
+
+  // 处理聚合点
+  const handleCluster = () => {
+    if (!map || !markers.length) return;
+
+    const points = markers.map(item => ({
+      weight: 1,
+      lnglat: [
+        toFixedDigit(item.longitude, 5),
+        toFixedDigit(item.latitude, 5)
+      ],
+      ...item
+    }));
+
+    if (clusterInstance.current) {
+      clusterInstance.current.setData(points);
+      return;
+    }
+
+    if(window.AMap?.MarkerCluster){
+      clusterInstance.current = new window.AMap.MarkerCluster(map, points, {
+        gridSize: 60,
+        renderMarker: (context: { marker: any; data: MarkerData[] }) => {
+          const { marker, data } = context;
+          const firstPoint = data[0];
+          
+          if (firstPoint.iconUrl) {
+            marker.setContent(`<img src="${firstPoint.iconUrl}" style="width:${DEFAULT_MARKER_ICON.size[0]}px;height:${DEFAULT_MARKER_ICON.size[1]}px;">`);
+          } else {
+            marker.setContent(DEFAULT_MARKER_ICON.content);
+          }
+          marker.setAnchor('bottom-center');
+          marker.setOffset(new window.AMap.Pixel(0, 0));
+
+          if (firstPoint.title) {
+            marker.setLabel({
+              direction: 'top',
+              offset: new window.AMap.Pixel(0, -5),
+              content: firstPoint.title
+            });
+          }
+        
+          marker.on('click', () => onMarkerClick?.(firstPoint));
+        }
+      });
+    }
+
+    // 优化聚合点点击逻辑
+    if(clusterInstance.current){
+      clusterInstance.current.on('click', (item: any) => {
+        if (item.clusterData.length <= 1) return;
+
+        const center = item.clusterData.reduce(
+          (acc: number[], curr: any) => [
+            acc[0] + Number(curr.lnglat[0]),
+            acc[1] + Number(curr.lnglat[1])
+          ],
+          [0, 0]
+        ).map((coord: number) => coord / item.clusterData.length);
+
+        map.setZoomAndCenter(map.getZoom() + 2, center);
+      });
+    }
+  };
+
+  // 处理普通标记点
+  const handleMarkers = () => {
+    if (!map || !markers.length) return;
+    
+    // 清除旧的标记点
+    markersRef.current.forEach(marker => marker.setMap(null));
+    markersRef.current = [];
+
+    // 添加新的标记点
+    markersRef.current = markers.map(markerData => {
+      const marker = createMarker(markerData);
+      marker.setMap(map);
+      return marker;
+    });
+  };
+
+  useEffect(() => {
+    if (!map || !Array.isArray(markers)) return;
+
+    // 清理旧的标记点和聚合点
+    if (clusterInstance.current) {
+      clusterInstance.current.setMap(null);
+      clusterInstance.current = null;
+    }
+    markersRef.current.forEach(marker => marker.setMap(null));
+    markersRef.current = [];
+
+    // 根据配置添加新的标记点
+    if (markers.length > 0) {
+      if (showCluster) {
+        handleCluster();
+      } else {
+        handleMarkers();
+      }
+    }
+
+    return () => {
+      if (clusterInstance.current) {
+        clusterInstance.current.setMap(null);
+        clusterInstance.current = null;
+      }
+      markersRef.current.forEach(marker => marker.setMap(null));
+      markersRef.current = [];
+    };
+  }, [map, markers, showCluster]);
+}; 
+
+const AMapComponent: React.FC<AMapProps> = ({
+  width = '100%',
+  height = '400px',
+  center = TILE_CONFIG.DEFAULT_CENTER as [number, number],
+  zoom = TILE_CONFIG.DEFAULT_ZOOM,
+  mode = (getGlobalConfig('MAP_CONFIG')?.MAP_MODE as MapMode) || MapMode.ONLINE,
+  onMarkerClick,
+  onClick,
+  markers = [],
+  showCluster = true,
+  queryKey = 'amap-instance',
+}) => {
+  const mapContainer = useRef<HTMLDivElement>(null);
+  const mapInstance = useRef<AMapInstance | null>(null);
+  const queryClient = useQueryClient();
+
+  // 加载地图脚本
+  const { data: AMap, isLoading: isLoadingScript } = useAMapLoader(mode);
+
+  // 初始化地图实例
+  const { data: map } = useQuery<AMapInstance>({
+    queryKey: [ queryKey ],
+    queryFn: async () => {
+      if (!AMap || !mapContainer.current) return null;
+
+      const config: MapConfig = {
+        zoom,
+        center,
+        zooms: [3, 20],
+        resizeEnable: true,
+        rotateEnable: false,
+        pitchEnable: false,
+        defaultCursor: 'pointer',
+        showLabel: true,
+      };
+
+      if (mode === 'offline') {
+        config.layers = [
+          new AMap.TileLayer({
+            getTileUrl: (x: number, y: number, z: number) => 
+              `${TILE_CONFIG.BASE_URL}/${z}/${x}/${y}.png`,
+            zIndex: 100,
+          })
+        ];
+      }
+
+      const newMap = new AMap.Map(mapContainer.current, config);
+      mapInstance.current = newMap;
+      return newMap;
+    },
+    enabled: !!AMap && !!mapContainer.current && !isLoadingScript,
+    gcTime: Infinity,
+    staleTime: Infinity,
+  });
+
+  // 处理标记点
+  useAMapMarkers({
+    map,
+    markers,
+    showCluster,
+    onMarkerClick,
+  });
+
+  // 处理点击事件
+  useAMapClick(map, onClick);
+
+  // 更新地图视图
+  useEffect(() => {
+    if (!map) return;
+    
+    if (center && zoom) {
+      map.setZoomAndCenter(zoom, center);
+    } else if (center) {
+      map.setCenter(center);
+    } else if (zoom) {
+      map.setZoom(zoom);
+    }
+  }, [map, center, zoom]);
+
+  // 清理地图实例和查询缓存
+  useEffect(() => {
+    return () => {
+      if (mapInstance.current) {
+        mapInstance.current.destroy();
+        mapInstance.current = null;
+        // 清理 React Query 缓存
+        queryClient.removeQueries({ queryKey: [ queryKey ] });
+      }
+    };
+  }, [queryClient]);
+
+  return (
+    <div
+      ref={mapContainer}
+      style={{
+        width,
+        height,
+        position: 'relative',
+      }}
+    >
+      {isLoadingScript && <div className="w-full h-full flex justify-center items-center"><Spin /></div>}
+    </div>
+  );
+};
+
+export default AMapComponent;

+ 36 - 0
client/admin/components_protected_route.tsx

@@ -0,0 +1,36 @@
+import React, { useEffect } from 'react';
+import { 
+  useNavigate,
+} from 'react-router';
+
+
+import { useAuth } from './hooks_sys.tsx';
+
+
+export const ProtectedRoute = ({ children }: { children: React.ReactNode }) => {
+  const { isAuthenticated, isLoading } = useAuth();
+  const navigate = useNavigate();
+  
+  useEffect(() => {
+    // 只有在加载完成且未认证时才重定向
+    if (!isLoading && !isAuthenticated) {
+      navigate('/admin/login', { replace: true });
+    }
+  }, [isAuthenticated, isLoading, navigate]);
+  
+  // 显示加载状态,直到认证检查完成
+  if (isLoading) {
+    return (
+      <div className="flex justify-center items-center h-screen">
+        <div className="loader ease-linear rounded-full border-4 border-t-4 border-gray-200 h-12 w-12"></div>
+      </div>
+    );
+  }
+  
+  // 如果未认证且不再加载中,不显示任何内容(等待重定向)
+  if (!isAuthenticated) {
+    return null;
+  }
+  
+  return children;
+};

+ 168 - 0
client/admin/components_uploader.tsx

@@ -0,0 +1,168 @@
+import React, { useState } from 'react';
+import { 
+  Layout, Menu, Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Spin, Row, Col, Breadcrumb, Avatar,
+  Dropdown, ConfigProvider, theme, Typography,
+  Switch, Badge, Image, Upload, Divider, Descriptions,
+  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
+} from 'antd';
+import {
+  UploadOutlined,
+} from '@ant-design/icons';   
+import { uploadMinIOWithPolicy , uploadOSSWithPolicy} from '@d8d-appcontainer/api';
+import { getGlobalConfig } from './utils.ts';
+import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
+import 'dayjs/locale/zh-cn';
+import { OssType } from '../share/types.ts';
+
+import { FileAPI } from './api/index.ts';
+
+// MinIO文件上传组件
+export const Uploader = ({ 
+  onSuccess, 
+  onError,
+  onProgress, 
+  maxSize = 10 * 1024 * 1024,
+  prefix = 'uploads/',
+  allowedTypes = ['image/jpeg', 'image/png', 'application/pdf', 'text/plain']
+}: {
+  onSuccess?: (fileUrl: string, fileInfo: any) => void;
+  onError?: (error: Error) => void;
+  onProgress?: (percent: number) => void;
+  maxSize?: number;
+  prefix?: string;
+  allowedTypes?: string[];
+}) => {
+  const [uploading, setUploading] = useState(false);
+  const [progress, setProgress] = useState(0);
+  
+  // 处理文件上传
+  const handleUpload = async (options: any) => {
+    const { file, onSuccess: uploadSuccess, onError: uploadError, onProgress: uploadProgress } = options;
+    
+    setUploading(true);
+    setProgress(0);
+    
+    // 文件大小检查
+    if (file.size > maxSize) {
+      message.error(`文件大小不能超过${maxSize / 1024 / 1024}MB`);
+      uploadError(new Error('文件过大'));
+      setUploading(false);
+      return;
+    }
+    
+    // 文件类型检查
+    if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
+      message.error(`不支持的文件类型: ${file.type}`);
+      uploadError(new Error('不支持的文件类型'));
+      setUploading(false);
+      return;
+    }
+    
+    try {
+      // 1. 获取上传策略
+      const policyResponse = await FileAPI.getUploadPolicy(file.name, prefix, maxSize);
+      const policy = policyResponse.data;
+      
+      if (!policy) {
+        throw new Error('获取上传策略失败');
+      }
+      
+      // 生成随机文件名但保留原始扩展名
+      const fileExt = file.name.split('.').pop() || '';
+      const randomName = `${Date.now()}_${Math.random().toString(36).substring(2, 10)}${fileExt ? `.${fileExt}` : ''}`;
+      
+      // 2. 上传文件到MinIO
+      const callbacks = {
+        onProgress: (event: { progress: number }) => {
+          const percent = Math.round(event.progress);
+          setProgress(percent);
+          uploadProgress({ percent });
+          onProgress?.(percent);
+        },
+        onComplete: () => {
+          setUploading(false);
+          setProgress(100);
+        },
+        onError: (err: Error) => {
+          setUploading(false);
+          message.error(`上传失败: ${err.message}`);
+          uploadError(err);
+          onError?.(err);
+        }
+      };
+      
+      // 执行上传
+      const fileUrl = getGlobalConfig('OSS_TYPE') === OssType.MINIO ? 
+        await uploadMinIOWithPolicy(
+          policy as MinioUploadPolicy,
+          file,
+          randomName,
+          callbacks
+        ) : await uploadOSSWithPolicy(
+          policy as OSSUploadPolicy,
+          file,
+          randomName,
+          callbacks
+        );
+      
+      // 从URL中提取相对路径
+      const relativePath = `${policy.prefix}${randomName}`;
+      
+      // 3. 保存文件信息到文件库
+      const fileInfo = {
+        file_name: randomName,
+        original_filename: file.name,
+        file_path: relativePath,
+        file_type: file.type,
+        file_size: file.size,
+        tags: '',
+        description: '',
+        category_id: undefined
+      };
+      
+      const saveResponse = await FileAPI.saveFileInfo(fileInfo);
+      
+      // 操作成功
+      uploadSuccess(relativePath);
+      message.success('文件上传成功');
+      onSuccess?.(relativePath, saveResponse.data);
+    } catch (error: any) {
+      // 上传失败
+      setUploading(false);
+      message.error(`上传失败: ${error.message}`);
+      uploadError(error);
+      onError?.(error);
+    }
+  };
+  
+  return (
+    <Upload.Dragger
+      name="file"
+      multiple={false}
+      customRequest={handleUpload}
+      showUploadList={true}
+      progress={{
+        strokeColor: {
+          '0%': '#108ee9',
+          '100%': '#87d068',
+        },
+        format: (percent) => `${Math.round(percent || 0)}%`,
+      }}
+    >
+      <p className="ant-upload-drag-icon">
+        <UploadOutlined />
+      </p>
+      <p className="ant-upload-text">点击或拖动文件到这里上传</p>
+      <p className="ant-upload-hint">
+        支持单个文件上传,最大{maxSize / 1024 / 1024}MB
+      </p>
+      {uploading && (
+        <div style={{ marginTop: 16 }}>
+          <Progress percent={progress} />
+        </div>
+      )}
+    </Upload.Dragger>
+  );
+};

+ 334 - 0
client/admin/hooks_sys.tsx

@@ -0,0 +1,334 @@
+import React, { useState, useEffect, createContext, useContext } from 'react';
+import { ConfigProvider, theme, message
+} from 'antd';
+import zhCN from "antd/locale/zh_CN";
+
+import { 
+  useQuery,
+  useQueryClient,
+  useMutation
+} from '@tanstack/react-query';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+import type { 
+  User, AuthContextType, ThemeContextType, ThemeSettings
+} from '../share/types.ts';
+import {
+  ThemeMode,
+  FontSize,
+  CompactMode
+} from '../share/types.ts';
+import {
+  AuthAPI,
+  ThemeAPI
+} from './api/index.ts';
+
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+// 设置 dayjs 语言
+dayjs.locale('zh-cn');
+
+
+// 确保ConfigProvider能够正确使用中文日期
+const locale = {
+  ...zhCN,
+  DatePicker: {
+    ...zhCN.DatePicker,
+    lang: {
+      ...zhCN.DatePicker?.lang,
+      shortWeekDays: ['日', '一', '二', '三', '四', '五', '六'],
+      shortMonths: ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月']
+    }
+  }
+};
+
+// 创建认证上下文
+const AuthContext = createContext<AuthContextType | null>(null);
+const ThemeContext = createContext<ThemeContextType | null>(null);
+
+// 认证提供器组件
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [user, setUser] = useState<User | null>(null);
+  const [token, setToken] = useState<string | null>(localStorage.getItem('token'));
+  const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
+  const queryClient = useQueryClient();
+  
+  // 声明handleLogout函数
+  const handleLogout = async () => {
+    try {
+      // 如果已登录,调用登出API
+      if (token) {
+        await AuthAPI.logout();
+      }
+    } catch (error) {
+      console.error('登出请求失败:', error);
+    } finally {
+      // 清除本地状态
+      setToken(null);
+      setUser(null);
+      setIsAuthenticated(false);
+      localStorage.removeItem('token');
+      // 清除Authorization头
+      delete axios.defaults.headers.common['Authorization'];
+      console.log('登出时已删除全局Authorization头');
+      // 清除所有查询缓存
+      queryClient.clear();
+    }
+  };
+  
+  // 使用useQuery检查登录状态
+  const { isLoading } = useQuery({
+    queryKey: ['auth', 'status', token],
+    queryFn: async () => {
+      if (!token) {
+        setIsAuthenticated(false);
+        setUser(null);
+        return null;
+      }
+      
+      try {
+        // 设置全局默认请求头
+        axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
+        // 使用API验证当前用户
+        const currentUser = await AuthAPI.getCurrentUser();
+        setUser(currentUser);
+        setIsAuthenticated(true);
+        return { isValid: true, user: currentUser };
+      } catch (error) {
+        // 如果API调用失败,自动登出
+        handleLogout();
+        return { isValid: false };
+      }
+    },
+    enabled: !!token,
+    refetchOnWindowFocus: false,
+    retry: false
+  });
+  
+  // 设置请求拦截器
+  useEffect(() => {
+    // 设置响应拦截器处理401错误
+    const responseInterceptor = axios.interceptors.response.use(
+      (response) => response,
+      (error) => {
+        if (error.response?.status === 401) {
+          console.log('检测到401错误,执行登出操作');
+          handleLogout();
+        }
+        return Promise.reject(error);
+      }
+    );
+    
+    // 清理拦截器
+    return () => {
+      axios.interceptors.response.eject(responseInterceptor);
+    };
+  }, [token]);
+  
+  const handleLogin = async (username: string, password: string, latitude?: number, longitude?: number): Promise<void> => {
+    try {
+      // 使用AuthAPI登录
+      const response = await AuthAPI.login(username, password, latitude, longitude);
+      
+      // 保存token和用户信息
+      const { token: newToken, user: newUser } = response;
+      
+      // 设置全局默认请求头
+      axios.defaults.headers.common['Authorization'] = `Bearer ${newToken}`;
+      
+      // 保存状态
+      setToken(newToken);
+      setUser(newUser);
+      setIsAuthenticated(true);
+      localStorage.setItem('token', newToken);
+      
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  };
+  
+  return (
+    <AuthContext.Provider 
+      value={{
+        user,
+        token,
+        login: handleLogin,
+        logout: handleLogout,
+        isAuthenticated,
+        isLoading
+      }}
+    >
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+// 主题提供器组件
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [isDark, setIsDark] = useState(false);
+  const [currentTheme, setCurrentTheme] = useState<ThemeSettings>({
+    user_id: 0,
+    theme_mode: ThemeMode.LIGHT,
+    primary_color: '#1890ff',
+    font_size: FontSize.MEDIUM,
+    is_compact: CompactMode.NORMAL
+  });
+  
+  // 获取主题设置
+  const { isLoading: isThemeLoading } = useQuery({
+    queryKey: ['theme', 'settings'],
+    queryFn: async () => {
+      try {
+        const settings = await ThemeAPI.getThemeSettings();
+        setCurrentTheme(settings);
+        setIsDark(settings.theme_mode === ThemeMode.DARK);
+        
+        return settings;
+      } catch (error) {
+        console.error('获取主题设置失败:', error);
+        return null;
+      }
+    },
+    refetchOnWindowFocus: false,
+    enabled: !!localStorage.getItem('token')
+  });
+  
+  // 预览主题设置(不保存到后端)
+  const previewTheme = (newTheme: Partial<ThemeSettings>) => {
+    const updatedTheme = { ...currentTheme, ...newTheme };
+    setCurrentTheme(updatedTheme);
+    setIsDark(updatedTheme.theme_mode === ThemeMode.DARK);
+  };
+  
+  // 更新主题设置(保存到后端)
+  const updateThemeMutation = useMutation({
+    mutationFn: async (newTheme: Partial<ThemeSettings>) => {
+      return await ThemeAPI.updateThemeSettings(newTheme);
+    },
+    onSuccess: (data) => {
+      setCurrentTheme(data);
+      setIsDark(data.theme_mode === ThemeMode.DARK);
+      message.success('主题设置已更新');
+    },
+    onError: (error) => {
+      console.error('更新主题设置失败:', error);
+      message.error('更新主题设置失败');
+    }
+  });
+  
+  // 重置主题设置
+  const resetThemeMutation = useMutation({
+    mutationFn: async () => {
+      return await ThemeAPI.resetThemeSettings();
+    },
+    onSuccess: (data) => {
+      setCurrentTheme(data);
+      setIsDark(data.theme_mode === ThemeMode.DARK);
+      message.success('主题设置已重置为默认值');
+    },
+    onError: (error) => {
+      console.error('重置主题设置失败:', error);
+      message.error('重置主题设置失败');
+    }
+  });
+  
+  // 添加 toggleTheme 方法
+  const toggleTheme = () => {
+    const newTheme = {
+      ...currentTheme,
+      theme_mode: isDark ? ThemeMode.LIGHT : ThemeMode.DARK
+    };
+    setIsDark(!isDark);
+    setCurrentTheme(newTheme);
+  };
+  
+  return (
+    <ThemeContext.Provider value={{ 
+      isDark, 
+      currentTheme,
+      updateTheme: previewTheme,
+      saveTheme: updateThemeMutation.mutateAsync,
+      resetTheme: resetThemeMutation.mutateAsync,
+      toggleTheme
+    }}>
+      <ConfigProvider
+        theme={{
+          algorithm: currentTheme.is_compact === CompactMode.COMPACT
+            ? [isDark ? theme.darkAlgorithm : theme.defaultAlgorithm, theme.compactAlgorithm]
+            : isDark ? theme.darkAlgorithm : theme.defaultAlgorithm,
+          token: {
+            colorPrimary: currentTheme.primary_color,
+            fontSize: currentTheme.font_size === FontSize.SMALL ? 12 :
+                     currentTheme.font_size === FontSize.MEDIUM ? 14 : 16,
+            // colorBgBase: isDark ? undefined : currentTheme.background_color || '#fff',
+            colorBgBase: currentTheme.background_color,
+            borderRadius: currentTheme.border_radius ?? 6,
+            colorTextBase: currentTheme.text_color || (isDark ? '#fff' : '#000'),
+          },
+          components: {
+            Layout: {
+              // headerBg: isDark ? undefined : currentTheme.background_color || '#fff',
+              // siderBg: isDark ? undefined : currentTheme.background_color || '#fff',
+              headerBg: currentTheme.background_color,
+              siderBg: currentTheme.background_color,
+            }
+          }
+        }}
+        locale={locale as any}
+      >
+        {children}
+      </ConfigProvider>
+    </ThemeContext.Provider>
+  );
+};
+
+// 使用上下文的钩子
+export const useAuth = () => {
+  const context = useContext(AuthContext);
+  if (!context) {
+    throw new Error('useAuth必须在AuthProvider内部使用');
+  }
+  return context;
+};
+
+export const useTheme = () => {
+  const context = useContext(ThemeContext);
+  if (!context) {
+    throw new Error('useTheme必须在ThemeProvider内部使用');
+  }
+  return context;
+};
+
+export const useRequest = <T extends any[], R>(requestFn: (...args: T) => Promise<R>, options = {}) => {
+  const [data, setData] = useState<R | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState<any>(null);
+
+  const run = async (...args: T) => {
+    try {
+      setLoading(true);
+      const result = await requestFn(...args);
+      setData(result);
+      return result;
+    } catch (err) {
+      setError(err);
+      throw err;
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return {
+    data,
+    loading,
+    error,
+    run
+  };
+};

+ 222 - 0
client/admin/layouts/MainLayout.tsx

@@ -0,0 +1,222 @@
+import React, { useState, useEffect, useMemo } from 'react';
+import {
+  Outlet,
+  useLocation,
+} from 'react-router';
+import {
+  Layout, Button, Space, Badge, Avatar, Dropdown, Typography, Input, Menu,
+} from 'antd';
+import {
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+  BellOutlined,
+  VerticalAlignTopOutlined,
+  UserOutlined
+} from '@ant-design/icons';
+import { useAuth, useTheme } from '../hooks_sys.tsx';
+import { useMenu, useMenuSearch, type MenuItem } from '../menu.tsx';
+
+const { Header, Sider, Content } = Layout;
+
+/**
+ * 主布局组件
+ * 包含侧边栏、顶部导航和内容区域
+ */
+export const MainLayout = () => {
+  const { user } = useAuth();
+  const { isDark } = useTheme();
+  const [showBackTop, setShowBackTop] = useState(false);
+  const location = useLocation();
+  
+  // 使用菜单hook
+  const {
+    menuItems,
+    userMenuItems,
+    openKeys,
+    collapsed,
+    setCollapsed,
+    handleMenuClick: handleRawMenuClick,
+    onOpenChange
+  } = useMenu();
+  
+  // 处理菜单点击
+  const handleMenuClick = (key: string) => {
+    const item = findMenuItem(menuItems, key);
+    if (item && 'label' in item) {
+      handleRawMenuClick(item);
+    }
+  };
+  
+  // 查找菜单项
+  const findMenuItem = (items: MenuItem[], key: string): MenuItem | null => {
+    for (const item of items) {
+      if (!item) continue;
+      if (item.key === key) return item;
+      if (item.children) {
+        const found = findMenuItem(item.children, key);
+        if (found) return found;
+      }
+    }
+    return null;
+  };
+  
+  // 使用菜单搜索hook
+  const {
+    searchText,
+    setSearchText,
+    filteredMenuItems
+  } = useMenuSearch(menuItems);
+  
+  // 获取当前选中的菜单项
+  const selectedKey = useMemo(() => {
+    const findSelectedKey = (items: MenuItem[]): string | null => {
+      for (const item of items) {
+        if (!item) continue;
+        if (item.path === location.pathname) return item.key || null;
+        if (item.children) {
+          const childKey = findSelectedKey(item.children);
+          if (childKey) return childKey;
+        }
+      }
+      return null;
+    };
+    
+    return findSelectedKey(menuItems) || '';
+  }, [location.pathname, menuItems]);
+  
+  // 检测滚动位置,控制回到顶部按钮显示
+  useEffect(() => {
+    const handleScroll = () => {
+      setShowBackTop(window.pageYOffset > 300);
+    };
+    
+    window.addEventListener('scroll', handleScroll);
+    return () => window.removeEventListener('scroll', handleScroll);
+  }, []);
+  
+  // 回到顶部
+  const scrollToTop = () => {
+    window.scrollTo({
+      top: 0,
+      behavior: 'smooth'
+    });
+  };
+
+  
+  // 应用名称 - 从CONFIG中获取或使用默认值
+  const appName = window.CONFIG?.APP_NAME || '应用Starter';
+  
+  return (
+    <Layout style={{ minHeight: '100vh' }}>
+      <Sider 
+        trigger={null} 
+        collapsible 
+        collapsed={collapsed}
+        width={240}
+        className="custom-sider"
+        style={{
+          overflow: 'auto',
+          height: '100vh',
+          position: 'fixed',
+          left: 0,
+          top: 0,
+          bottom: 0,
+          zIndex: 100,
+        }}
+      >
+        <div className="p-4">
+          <Typography.Title level={2} className="text-xl font-bold truncate">
+            {collapsed ? '应用' : appName}
+          </Typography.Title>
+          
+          {/* 菜单搜索框 */}
+          {!collapsed && (
+            <div className="mb-4">
+              <Input.Search
+                placeholder="搜索菜单..."
+                allowClear
+                value={searchText}
+                onChange={(e) => setSearchText(e.target.value)}
+              />
+            </div>
+          )}
+        </div>
+        
+        {/* 菜单列表 */}
+        <Menu
+          theme={isDark ? 'dark' : 'light'}
+          mode="inline"
+          items={filteredMenuItems}
+          openKeys={openKeys}
+          selectedKeys={[selectedKey]}
+          onOpenChange={onOpenChange}
+          onClick={({ key }) => handleMenuClick(key)}
+          inlineCollapsed={collapsed}
+        />
+      </Sider>
+      
+      <Layout style={{ marginLeft: collapsed ? 80 : 240, transition: 'margin-left 0.2s' }}>
+        <Header className="p-0 flex justify-between items-center" 
+          style={{ 
+            position: 'sticky', 
+            top: 0, 
+            zIndex: 99, 
+            boxShadow: '0 1px 4px rgba(0,21,41,0.08)',
+          }}
+        >
+          <Button
+            type="text"
+            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
+            onClick={() => setCollapsed(!collapsed)}
+            className="w-16 h-16"
+          />
+          
+          <Space size="middle" className="mr-4">
+            <Badge count={5} offset={[0, 5]}>
+              <Button 
+                type="text" 
+                icon={<BellOutlined />}
+              />
+            </Badge>
+            
+            <Dropdown menu={{ items: userMenuItems }}>
+              <Space className="cursor-pointer">
+                <Avatar 
+                  src={user?.avatar || 'https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?q=80&w=40&auto=format&fit=crop'}
+                  icon={!user?.avatar && !navigator.onLine && <UserOutlined />}
+                />
+                <span>
+                  {user?.nickname || user?.username}
+                </span>
+              </Space>
+            </Dropdown>
+          </Space>
+        </Header>
+        
+        <Content className="m-6" style={{ overflow: 'initial' }}>
+          <div className="site-layout-content p-6 rounded-lg">
+            <Outlet />
+          </div>
+          
+          {/* 回到顶部按钮 */}
+          {showBackTop && (
+            <Button
+              type="primary"
+              shape="circle"
+              icon={<VerticalAlignTopOutlined />}
+              size="large"
+              onClick={scrollToTop}
+              style={{
+                position: 'fixed',
+                right: 30,
+                bottom: 30,
+                zIndex: 1000,
+                boxShadow: '0 3px 6px rgba(0,0,0,0.16)',
+              }}
+            />
+          )}
+        </Content>
+      </Layout>
+    </Layout>
+  );
+};

+ 365 - 0
client/admin/menu.tsx

@@ -0,0 +1,365 @@
+import React from 'react';
+import { useNavigate } from 'react-router';
+import type { MenuProps } from 'antd';
+import {
+  UserOutlined,
+  DashboardOutlined,
+  TeamOutlined,
+  SettingOutlined,
+  FileOutlined,
+  MessageOutlined,
+  InfoCircleOutlined,
+  BarChartOutlined,
+  EnvironmentOutlined,
+  MoonOutlined,
+  SunOutlined,
+  CalendarOutlined,
+  DatabaseOutlined,
+  PieChartOutlined,
+  MonitorOutlined
+} from '@ant-design/icons';
+import { useTheme } from './hooks_sys.tsx';
+
+export interface MenuItem {
+  key: string;
+  label: string;
+  icon?: React.ReactNode;
+  children?: MenuItem[];
+  path?: string;
+  permission?: string;
+}
+
+/**
+ * 菜单搜索 Hook
+ * 封装菜单搜索相关逻辑
+ */
+export const useMenuSearch = (menuItems: MenuItem[]) => {
+  const [searchText, setSearchText] = React.useState('');
+
+  // 过滤菜单项
+  const filteredMenuItems = React.useMemo(() => {
+    if (!searchText) return menuItems;
+    
+    const filterItems = (items: MenuItem[]): MenuItem[] => {
+      return items
+        .map(item => {
+          // 克隆对象避免修改原数据
+          const newItem = { ...item };
+          if (newItem.children) {
+            newItem.children = filterItems(newItem.children);
+          }
+          return newItem;
+        })
+        .filter(item => {
+          // 保留匹配项或其子项匹配的项
+          const match = item.label.toLowerCase().includes(searchText.toLowerCase());
+          if (match) return true;
+          if (item.children?.length) return true;
+          return false;
+        });
+    };
+    
+    return filterItems(menuItems);
+  }, [menuItems, searchText]);
+
+  // 清除搜索
+  const clearSearch = () => {
+    setSearchText('');
+  };
+
+  return {
+    searchText,
+    setSearchText,
+    filteredMenuItems,
+    clearSearch
+  };
+};
+
+export const useMenu = () => {
+  const { isDark, toggleTheme } = useTheme();
+  const navigate = useNavigate();
+  const [collapsed, setCollapsed] = React.useState(false);
+  const [openKeys, setOpenKeys] = React.useState<string[]>([]);
+
+  // 基础菜单项配置
+  const menuItems: MenuItem[] = [
+    {
+      key: 'dashboard',
+      label: '控制台',
+      icon: <DashboardOutlined />,
+      path: '/admin/dashboard'
+    },
+    {
+      key: 'monitor',
+      icon: <DatabaseOutlined />,
+      label: '监控管理',
+      children: [
+        {
+          key: 'device-monitor',
+          label: '设备实时监控',
+          path: '/admin/device-monitor',
+        },
+        {
+          key: 'temperature-humidity',
+          label: '温湿度监控',
+          path: '/admin/temperature-humidity',
+        },
+        {
+          key: 'smoke-water',
+          label: '烟感及水浸监控',
+          path: '/admin/smoke-water',
+        },
+        {
+          key: 'device-map',
+          label: '设备地图监控',
+          path: '/admin/device-map',
+        },
+        {
+          key: 'alert-records',
+          label: '告警记录',
+          path: '/admin/alert-records',
+        },
+        {
+          key: 'alert-handle-logs',
+          label: '告警处理记录',
+          path: '/admin/alert-handle-logs',
+        },
+        {
+          key: 'alert-notify-configs',
+          label: '告警通知配置',
+          path: '/admin/alert-notify-configs',
+        },
+        {
+          key: 'device-alert-rules',
+          label: '设备告警规则',
+          path: '/admin/device-alert-rules',
+        },
+      ],
+    },
+    {
+      key: 'zichan',
+      icon: <CalendarOutlined />,
+      label: '资产管理',
+      children: [
+        {
+          key: 'zichan-categorys',
+          label: '资产分类',
+          path: '/admin/zichan-categorys'
+        },
+        {
+          key: 'zichan-areas',
+          label: '资产区域',
+          path: '/admin/zichan-areas',
+        },
+        {
+          key: 'zichan-info',
+          label: '资产信息',
+          path: '/admin/zichan',
+        },
+        {
+          key: 'zichan-transfer',
+          label: '资产流转',
+          path: '/admin/zichan-transfer',
+        },
+      ],
+    },
+    {
+      key: 'device',
+      icon: <SettingOutlined />,
+      label: '设备管理',
+      children: [
+        {
+          key: 'device-types',
+          label: '设备类型',
+          path: '/admin/device-types',
+        },
+        {
+          key: 'device-instances',
+          label: '设备实例',
+          path: '/admin/device-instances',
+        },
+        {
+          key: 'modbus-rtu-devices',
+          label: 'Modbus RTU设备',
+          path: '/admin/modbus-rtu-devices',
+        },
+        {
+          key: "greenhouse-protocol",
+          label: "温室协议设置",
+          path: "/admin/greenhouse-protocol"
+        },
+        {
+          key: 'racks',
+          label: '机柜管理',
+          path: '/admin/racks',
+        },
+        {
+          key: 'rack-server-types',
+          label: '机柜服务器类型',
+          path: '/admin/rack-server-types',
+        },
+        {
+          key: 'rack-servers',
+          label: '机柜服务器',
+          path: '/admin/rack-servers',
+        },
+      ],
+    },
+    {
+      key: "work-orders",
+      label: "工单管理",
+      icon: <MonitorOutlined />,
+      path: "/admin/work-orders"
+    },
+    {
+      key: 'inspection',
+      label: '巡检管理',
+      icon: <MonitorOutlined />,
+      path: '/admin/inspections'
+    },
+    {
+      key: 'analysis',
+      icon: <PieChartOutlined />,
+      label: '分析报表',
+      children: [
+        {
+          key: 'analysis-alert-trend',
+          label: '告警趋势图',
+          path: '/admin/analysis/alert-trend',
+        },
+        {
+          key: 'analysis-asset-category',
+          label: '资产分类统计图',
+          path: '/admin/analysis/asset-category',
+        },
+        {
+          key: 'analysis-online-devices',
+          label: '设备在线统计图',
+          path: '/admin/analysis/online-devices',
+        },
+        {
+          key: 'analysis-asset-transfer',
+          label: '资产流转统计图',
+          path: '/admin/analysis/asset-transfer',
+        },
+      ],
+    },
+    {
+      key: 'users',
+      label: '用户管理',
+      icon: <TeamOutlined />,
+      path: '/admin/users',
+      permission: 'user:manage'
+    },
+    {
+      key: 'settings',
+      label: '系统设置',
+      icon: <SettingOutlined />,
+      children: [
+        {
+          key: 'theme-settings',
+          label: '主题设置',
+          path: '/admin/theme-settings',
+          permission: 'system:settings'
+        },
+        {
+          key: 'system-settings',
+          label: '系统配置',
+          path: '/admin/settings',
+          permission: 'system:settings'
+        }
+      ]
+    },
+    {
+      key: 'content',
+      label: '内容管理',
+      icon: <FileOutlined />,
+      children: [
+        {
+          key: 'know-info',
+          label: '知识库',
+          path: '/admin/know-info',
+          permission: 'content:manage'
+        },
+        {
+          key: 'file-library',
+          label: '文件库',
+          path: '/admin/file-library',
+          permission: 'content:manage'
+        }
+      ]
+    },
+    {
+      key: 'messages',
+      label: '消息中心',
+      icon: <MessageOutlined />,
+      path: '/admin/messages',
+      permission: 'message:view'
+    },
+    {
+      key: 'sms-module',
+      label: '短信模块',
+      icon: <MessageOutlined />,
+      path: '/admin/sms-module',
+      permission: 'message:view'
+    },
+    {
+      key: 'charts',
+      label: '数据图表',
+      icon: <BarChartOutlined />,
+      path: '/admin/chart-dashboard',
+      permission: 'chart:view'
+    },
+    {
+      key: 'maps',
+      label: '地图',
+      icon: <EnvironmentOutlined />,
+      path: '/admin/map-dashboard',
+      permission: 'map:view'
+    }
+  ];
+
+  // 用户菜单项
+  const userMenuItems: MenuProps['items'] = [
+    {
+      key: 'profile',
+      label: '个人资料',
+      icon: <UserOutlined />
+    },
+    {
+      key: 'theme',
+      label: isDark ? '切换到亮色模式' : '切换到暗色模式',
+      icon: isDark ? <SunOutlined /> : <MoonOutlined />,
+      onClick: () => toggleTheme()
+    },
+    {
+      key: 'logout',
+      label: '退出登录',
+      icon: <InfoCircleOutlined />,
+      danger: true
+    }
+  ];
+
+  // 处理菜单点击
+  const handleMenuClick = (item: MenuItem) => {
+    if (item.path) {
+      navigate(item.path);
+    }
+  };
+
+  // 处理菜单展开变化
+  const onOpenChange = (keys: string[]) => {
+    const latestOpenKey = keys.find(key => openKeys.indexOf(key) === -1);
+    setOpenKeys(latestOpenKey ? [latestOpenKey] : []);
+  };
+
+  return {
+    menuItems,
+    userMenuItems,
+    openKeys,
+    collapsed,
+    setCollapsed,
+    handleMenuClick,
+    onOpenChange
+  };
+};

+ 320 - 0
client/admin/pages_alert_handle.tsx

@@ -0,0 +1,320 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  useNavigate,
+  useLocation,
+  useParams
+} from 'react-router';
+import { 
+  Button, Space,
+  Form, Input, Select, message, 
+  Card, Spin, Typography,
+  Switch, Divider, Descriptions,
+  Tag, List,  
+} from 'antd';
+import {
+  FileImageOutlined,
+  FilePdfOutlined,
+  FileOutlined,
+} from '@ant-design/icons';   
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  AlertHandleLog, DeviceAlert,  Attachment, 
+} from '../share/monitorTypes.ts';
+
+import {
+  AlertLevel, AlertStatus, 
+  HandleType, ProblemType, 
+  HandleTypeNameMap, ProblemTypeNameMap,
+} from '../share/monitorTypes.ts';
+
+
+import { getEnumOptions } from './utils.ts';
+
+import { AlertAPI, AlertHandleAPI } from './api/index.ts';
+
+import { Uploader } from "./components_uploader.tsx";
+
+
+const { Text } = Typography;
+
+// 告警处理页面
+export const AlertHandlePage = () => {
+    const { id } = useParams<{ id: string }>();
+    const [loading, setLoading] = useState(false);
+    const [submitting, setSubmitting] = useState(false);
+    const [alert, setAlert] = useState<DeviceAlert | null>(null);
+    const [form] = Form.useForm();
+    const navigate = useNavigate();
+    const [uploadedFiles, setUploadedFiles] = useState<Attachment[]>([]);
+    const location = useLocation();
+    const searchParams = new URLSearchParams(location.search);
+    const mode = searchParams.get('mode') || 'view'; // 默认为查看模式
+    
+    // 判断是否可编辑
+    const isEditable = mode === 'edit' && alert && 
+      (alert.status === AlertStatus.PENDING || alert.status === AlertStatus.HANDLING);
+    
+    useEffect(() => {
+      if (id) {
+        fetchAlertData(parseInt(id));
+      }
+    }, [id]);
+    
+    const fetchAlertData = async (alertId: number) => {
+      setLoading(true);
+      try {
+        const response = await AlertAPI.getAlert(alertId);
+        if (response) {
+          setAlert(response);
+        }
+      } catch (error) {
+        console.error('获取告警数据失败:', error);
+        message.error('获取告警数据失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+    
+    const handleSubmit = async (values: any) => {
+      if (!id) return;
+      
+      setSubmitting(true);
+      try {
+        const alertHandleLog: Partial<AlertHandleLog> = {
+          alert_id: parseInt(id),
+          handle_type: values.handle_type,
+          problem_type: values.problem_type,
+          handle_result: values.handle_result,
+          notify_disabled: values.notify_disabled ? 1 : 0,
+          attachments: uploadedFiles
+        };
+        
+        const response = await AlertHandleAPI.createAlertHandle(alertHandleLog);
+        
+        if (response.data) {
+          message.success('告警处理成功');
+          navigate('/admin/alert-records');
+        }
+      } catch (error) {
+        console.error('告警处理失败:', error);
+        message.error('告警处理失败');
+      } finally {
+        setSubmitting(false);
+      }
+    };
+    
+    // 文件上传成功回调
+    const handleFileUploadSuccess = (fileUrl: string, fileInfo: any) => {
+      // 添加上传成功的文件到列表
+      const newFile: Attachment = {
+        id: fileInfo.id || String(Date.now()),
+        name: fileInfo.file_name,
+        url: fileUrl,
+        type: fileInfo.file_type,
+        size: fileInfo.file_size,
+        upload_time: new Date().toISOString()
+      };
+      
+      setUploadedFiles(prev => [...prev, newFile]);
+    };
+    
+    // 删除已上传文件
+    const handleFileDelete = (fileId: string) => {
+      setUploadedFiles(prev => prev.filter(file => file.id !== fileId));
+    };
+    
+    const handleTypeOptions = getEnumOptions(HandleType, HandleTypeNameMap);
+    
+    const problemTypeOptions = getEnumOptions(ProblemType, ProblemTypeNameMap);
+    
+    const getAlertLevelTag = (level?: AlertLevel) => {
+      if (level === undefined) return <Tag>未知</Tag>;
+      
+      switch (level) {
+        case AlertLevel.MINOR:
+          return <Tag color="blue">次要</Tag>;
+        case AlertLevel.NORMAL:
+          return <Tag color="green">一般</Tag>;
+        case AlertLevel.IMPORTANT:
+          return <Tag color="orange">重要</Tag>;
+        case AlertLevel.URGENT:
+          return <Tag color="red">紧急</Tag>;
+        default:
+          return <Tag>未知</Tag>;
+      }
+    };
+    
+    const getAlertStatusTag = (status?: AlertStatus) => {
+      if (status === undefined) return <Tag>未知</Tag>;
+      
+      switch (status) {
+        case AlertStatus.PENDING:
+          return <Tag color="red">待处理</Tag>;
+        case AlertStatus.HANDLING:
+          return <Tag color="orange">处理中</Tag>;
+        case AlertStatus.RESOLVED:
+          return <Tag color="green">已解决</Tag>;
+        case AlertStatus.IGNORED:
+          return <Tag color="default">已忽略</Tag>;
+        default:
+          return <Tag>未知</Tag>;
+      }
+    };
+    
+    if (loading) {
+      return (
+        <div style={{ textAlign: 'center', padding: '50px' }}>
+          <Spin size="large" />
+        </div>
+      );
+    }
+    
+    if (!alert) {
+      return (
+        <div style={{ textAlign: 'center', padding: '50px' }}>
+          <Text>未找到告警数据</Text>
+        </div>
+      );
+    }
+    
+    return (
+      <div>
+        <Card title={isEditable ? "告警处理" : "告警查看"} style={{ marginBottom: 16 }}>
+          <Descriptions bordered column={2} style={{ marginBottom: 16 }}>
+            <Descriptions.Item label="告警ID">{alert.id}</Descriptions.Item>
+            <Descriptions.Item label="设备名称">{alert.device_name}</Descriptions.Item>
+            <Descriptions.Item label="告警等级">{getAlertLevelTag(alert.alert_level)}</Descriptions.Item>
+            <Descriptions.Item label="状态">{getAlertStatusTag(alert.status)}</Descriptions.Item>
+            <Descriptions.Item label="监控指标">{alert.metric_type}</Descriptions.Item>
+            <Descriptions.Item label="触发值">{alert.metric_value}</Descriptions.Item>
+            <Descriptions.Item label="告警消息">{alert.alert_message}</Descriptions.Item>
+            <Descriptions.Item label="告警时间">{dayjs(alert.created_at).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
+          </Descriptions>
+          
+          {/* 只有可编辑模式或者已经有处理记录的情况下才显示表单 */}
+          {isEditable && (
+            <>
+              <Divider />
+              
+              <Form
+                form={form}
+                layout="vertical"
+                onFinish={handleSubmit}
+                initialValues={{
+                  handle_type: HandleType.CONFIRM,
+                  notify_disabled: false,
+                }}
+              >
+                <Form.Item
+                  name="handle_type"
+                  label="处理类型"
+                  rules={[{ required: true, message: '请选择处理类型' }]}
+                >
+                  <Select options={handleTypeOptions} />
+                </Form.Item>
+                
+                <Form.Item
+                  name="problem_type"
+                  label="问题类型"
+                  rules={[{ required: true, message: '请选择问题类型' }]}
+                >
+                  <Select options={problemTypeOptions} />
+                </Form.Item>
+                
+                <Form.Item
+                  name="handle_result"
+                  label="处理结果"
+                  rules={[{ required: true, message: '请输入处理结果' }]}
+                >
+                  <Input.TextArea rows={4} />
+                </Form.Item>
+                
+                <Form.Item
+                  label="附件"
+                >
+                  <div className="upload-attachments">
+                    {/* 使用MinIOUploader代替原始Upload组件 */}
+                    <Uploader 
+                      onSuccess={handleFileUploadSuccess}
+                      onError={(error) => message.error(`上传失败: ${error.message}`)}
+                      onProgress={(percent) => console.log(`上传进度: ${percent}%`)}
+                      prefix="alerts/"
+                      maxSize={20 * 1024 * 1024}
+                      allowedTypes={['image/jpeg', 'image/png', 'application/pdf', 'text/plain', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document']}
+                    />
+                    
+                    {/* 已上传文件列表 */}
+                    {uploadedFiles.length > 0 && (
+                      <div style={{ marginTop: 16 }}>
+                        <h4>已上传文件:</h4>
+                        <List
+                          size="small"
+                          bordered
+                          dataSource={uploadedFiles}
+                          renderItem={file => (
+                            <List.Item
+                              actions={[
+                                <Button 
+                                  key="delete" 
+                                  type="link" 
+                                  danger 
+                                  onClick={() => handleFileDelete(file.id)}
+                                >
+                                  删除
+                                </Button>
+                              ]}
+                            >
+                              <Space>
+                                {file.type.includes('image') ? <FileImageOutlined /> : 
+                                file.type.includes('pdf') ? <FilePdfOutlined /> : 
+                                <FileOutlined />}
+                                <a href={file.url} target="_blank" rel="noopener noreferrer">
+                                  {file.name}
+                                </a>
+                                <Text type="secondary">
+                                  ({file.size < 1024 * 1024 
+                                    ? `${(file.size / 1024).toFixed(2)} KB` 
+                                    : `${(file.size / 1024 / 1024).toFixed(2)} MB`})
+                                </Text>
+                              </Space>
+                            </List.Item>
+                          )}
+                        />
+                      </div>
+                    )}
+                  </div>
+                </Form.Item>
+                
+                <Form.Item
+                  name="notify_disabled"
+                  valuePropName="checked"
+                >
+                  <Switch checkedChildren="禁用通知" unCheckedChildren="启用通知" />
+                </Form.Item>
+                
+                <Form.Item>
+                  <Button type="primary" htmlType="submit" loading={submitting}>
+                    提交
+                  </Button>
+                  <Button style={{ marginLeft: 8 }} onClick={() => navigate('/admin/alert-records')}>
+                    返回
+                  </Button>
+                </Form.Item>
+              </Form>
+            </>
+          )}
+          
+          {/* 不可编辑模式时只显示返回按钮 */}
+          {!isEditable && (
+            <Form.Item style={{ marginTop: 16 }}>
+              <Button onClick={() => navigate('/admin/alert-records')}>
+                返回
+              </Button>
+            </Form.Item>
+          )}
+        </Card>
+      </div>
+    );
+  };

+ 218 - 0
client/admin/pages_alert_handle_logs.tsx

@@ -0,0 +1,218 @@
+
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, 
+  Card,  DatePicker, 
+} from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  AlertHandleLog, Attachment, 
+} from '../share/monitorTypes.ts';
+
+import {
+  HandleType, ProblemType, 
+  HandleTypeNameMap, ProblemTypeNameMap,
+} from '../share/monitorTypes.ts';
+
+import { AlertHandleAPI } from './api/index.ts';
+
+// 告警处理记录页面
+export const AlertHandleLogsPage = () => {
+    const [loading, setLoading] = useState(false);
+    const [alertHandleData, setAlertHandleData] = useState<AlertHandleLog[]>([]);
+    const [pagination, setPagination] = useState({
+      current: 1,
+      pageSize: 10,
+      total: 0,
+    });
+    const [formRef] = Form.useForm();
+    
+    useEffect(() => {
+      fetchAlertHandleData();
+    }, [pagination.current, pagination.pageSize]);
+  
+    const fetchAlertHandleData = async () => {
+      setLoading(true);
+      try {
+        const values = formRef.getFieldsValue();
+        const params = {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          alert_id: values.alert_id,
+          handler_id: values.handler_id,
+          handle_type: values.handle_type,
+          problem_type: values.problem_type,
+          start_time: values.time?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
+          end_time: values.time?.[1]?.format('YYYY-MM-DD HH:mm:ss'),
+        };
+        
+        const response = await AlertHandleAPI.getAlertHandleData(params);
+        
+        if (response) {
+          setAlertHandleData(response.data || []);
+          setPagination({
+            ...pagination,
+            total: response.total || 0,
+          });
+        }
+      } catch (error) {
+        console.error('获取告警处理记录失败:', error);
+        message.error('获取告警处理记录失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+  
+    const handleSearch = (values: any) => {
+      setPagination({
+        ...pagination,
+        current: 1,
+      });
+      fetchAlertHandleData();
+    };
+  
+    const handleTableChange = (newPagination: any) => {
+      setPagination({
+        ...pagination,
+        current: newPagination.current,
+        pageSize: newPagination.pageSize,
+      });
+    };
+  
+    const handleTypeOptions = [
+      { label: '确认', value: HandleType.CONFIRM },
+      { label: '解决', value: HandleType.RESOLVE },
+      { label: '忽略', value: HandleType.IGNORE },
+    ];
+    
+    const problemTypeOptions = [
+      { label: '设备故障', value: ProblemType.DEVICE },
+      { label: '网络故障', value: ProblemType.NETWORK },
+      { label: '电源故障', value: ProblemType.POWER },
+      { label: '施工影响', value: ProblemType.CONSTRUCTION },
+      { label: '其他原因', value: ProblemType.OTHER },
+    ];
+  
+    const columns = [
+      {
+        title: '记录ID',
+        dataIndex: 'id',
+        key: 'id',
+        width: 80,
+      },
+      {
+        title: '告警ID',
+        dataIndex: 'alert_id',
+        key: 'alert_id',
+        width: 80,
+      },
+      {
+        title: '处理人',
+        dataIndex: 'handler_id',
+        key: 'handler_id',
+      },
+      {
+        title: '处理类型',
+        dataIndex: 'handle_type',
+        key: 'handle_type',
+        render: (type: HandleType) => HandleTypeNameMap[type] || type,
+      },
+      {
+        title: '问题类型',
+        dataIndex: 'problem_type',
+        key: 'problem_type',
+        render: (type: ProblemType) => ProblemTypeNameMap[type] || type,
+      },
+      {
+        title: '处理结果',
+        dataIndex: 'handle_result',
+        key: 'handle_result',
+        ellipsis: true,
+      },
+      {
+        title: '附件数',
+        dataIndex: 'attachments',
+        key: 'attachments',
+        render: (attachments: Attachment[]) => attachments?.length || 0,
+      },
+      {
+        title: '禁用通知',
+        dataIndex: 'notify_disabled',
+        key: 'notify_disabled',
+        render: (value: number) => value === 1 ? '是' : '否',
+      },
+      {
+        title: '处理时间',
+        dataIndex: 'handle_time',
+        key: 'handle_time',
+        render: (text: Date) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+      },
+      {
+        title: '操作',
+        key: 'action',
+        render: (_: any, record: AlertHandleLog) => (
+          <Space size="middle">
+            <Button size="small" onClick={() => message.info(`查看处理记录ID: ${record.id}`)}>
+              查看
+            </Button>
+          </Space>
+        ),
+      },
+    ];
+  
+    return (
+      <div>
+        <Card title="告警处理记录" style={{ marginBottom: 16 }}>
+          <Form
+            form={formRef}
+            layout="inline"
+            onFinish={handleSearch}
+            style={{ marginBottom: 16 }}
+          >
+            <Form.Item name="alert_id" label="告警ID">
+              <Input placeholder="输入告警ID" style={{ width: 120 }} />
+            </Form.Item>
+            <Form.Item name="handle_type" label="处理类型">
+              <Select
+                placeholder="选择处理类型"
+                style={{ width: 150 }}
+                allowClear
+                options={handleTypeOptions}
+              />
+            </Form.Item>
+            <Form.Item name="problem_type" label="问题类型">
+              <Select
+                placeholder="选择问题类型"
+                style={{ width: 150 }}
+                allowClear
+                options={problemTypeOptions}
+              />
+            </Form.Item>
+            <Form.Item name="time" label="处理时间">
+              <DatePicker.RangePicker
+                showTime
+                style={{ width: 380 }}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" htmlType="submit">
+                查询
+              </Button>
+            </Form.Item>
+          </Form>
+          
+          <Table
+            columns={columns}
+            dataSource={alertHandleData}
+            rowKey="id"
+            pagination={pagination}
+            loading={loading}
+            onChange={handleTableChange}
+          />
+        </Card>
+      </div>
+    );
+  };

+ 458 - 0
client/admin/pages_alert_notify_config.tsx

@@ -0,0 +1,458 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, Modal, Badge, 
+  Popconfirm, Tag, Card
+} from 'antd';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  AlertNotifyConfig
+} from '../share/monitorTypes.ts';
+
+import {
+  AlertLevel, NotifyType, 
+  AlertLevelNameMap, 
+  NotifyTypeNameMap, 
+} from '../share/monitorTypes.ts';
+
+import {
+  EnableStatus, EnableStatusNameMap, 
+} from '../share/types.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+import { DeviceInstanceAPI, UserAPI, AlertNotifyConfigAPI } from './api/index.ts';
+
+
+// 告警通知配置页面
+export const AlertNotifyConfigPage = () => {
+  const [loading, setLoading] = useState(false);
+  const [configData, setConfigData] = useState<AlertNotifyConfig[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [deviceOptions, setDeviceOptions] = useState<{label: string, value: number}[]>([]);
+  const [userOptions, setUserOptions] = useState<{label: string, value: number}[]>([]);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalTitle, setModalTitle] = useState('新增告警通知配置');
+  const [currentRecord, setCurrentRecord] = useState<AlertNotifyConfig | null>(null);
+  const [formRef] = Form.useForm();
+  const [modalForm] = Form.useForm();
+  
+  useEffect(() => {
+    fetchDeviceOptions();
+    fetchUserOptions();
+    fetchConfigData();
+  }, [pagination.current, pagination.pageSize]);
+
+  const fetchDeviceOptions = async () => {
+    try {
+      const response = await DeviceInstanceAPI.getDeviceInstances();
+      if (response && response.data) {
+        const options = response.data.map((device) => ({
+          label: device.asset_name || `设备${device.id}`,
+          value: device.id
+        }));
+        setDeviceOptions(options);
+      }
+    } catch (error) {
+      console.error('获取设备列表失败:', error);
+      message.error('获取设备列表失败');
+    }
+  };
+
+  const fetchUserOptions = async () => {
+    try {
+      const response = await UserAPI.getUsers();
+      if (response && response.data) {
+        const options = response.data.map((user) => ({
+          label: user.nickname || user.username,
+          value: user.id
+        }));
+        setUserOptions(options);
+      }
+    } catch (error) {
+      console.error('获取用户列表失败:', error);
+      message.error('获取用户列表失败');
+    }
+  };
+
+  const fetchConfigData = async () => {
+    setLoading(true);
+    try {
+      const values = formRef.getFieldsValue();
+      const params = {
+        page: pagination.current,
+        pageSize: pagination.pageSize,
+        device_id: values.device_id,
+        alert_level: values.alert_level,
+        notify_type: values.notify_type,
+        is_enabled: values.is_enabled,
+      };
+      
+      const response = await AlertNotifyConfigAPI.getAlertNotifyConfig(params);
+      
+      if (response) {
+        setConfigData(response.data || []);
+        setPagination({
+          ...pagination,
+          total: response.total || 0,
+        });
+      }
+    } catch (error) {
+      console.error('获取告警通知配置失败:', error);
+      message.error('获取告警通知配置失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = (values: any) => {
+    setPagination({
+      ...pagination,
+      current: 1,
+    });
+    fetchConfigData();
+  };
+
+  const handleTableChange = (newPagination: any) => {
+    setPagination({
+      ...pagination,
+      current: newPagination.current,
+      pageSize: newPagination.pageSize,
+    });
+  };
+
+  const handleAdd = () => {
+    setModalTitle('新增告警通知配置');
+    setCurrentRecord(null);
+    modalForm.resetFields();
+    setModalVisible(true);
+  };
+
+  const handleEdit = (record: AlertNotifyConfig) => {
+    setModalTitle('编辑告警通知配置');
+    setCurrentRecord(record);
+    
+    // 将用户ID列表转为数组
+    const formData = {
+      ...record,
+      notify_users: record.notify_users || [],
+    };
+    
+    modalForm.setFieldsValue(formData);
+    setModalVisible(true);
+  };
+
+  const handleDelete = async (id: number) => {
+    try {
+      await AlertNotifyConfigAPI.deleteAlertNotifyConfig(id);
+      message.success('删除成功');
+      fetchConfigData();
+    } catch (error) {
+      console.error('删除失败:', error);
+      message.error('删除失败');
+    }
+  };
+
+  const handleModalSubmit = async () => {
+    try {
+      const values = await modalForm.validateFields();
+      
+      // 根据通知类型处理提交数据
+      const submitData = {
+        ...values,
+        notify_users: values.notify_type === NotifyType.SMS ? [] : values.notify_users,
+      };
+
+      // SMS通知特殊处理
+      if (values.notify_type === NotifyType.SMS) {
+        submitData.phone = values.phone_number; // 转换为后端需要的参数名
+        submitData.content = values.notify_template || '系统告警通知';
+      }
+      
+      if (currentRecord) {
+        // 更新
+        await AlertNotifyConfigAPI.updateAlertNotifyConfig(currentRecord.id, submitData);
+        message.success('更新成功');
+      } else {
+        // 新增
+        await AlertNotifyConfigAPI.createAlertNotifyConfig(submitData);
+        message.success('添加成功');
+      }
+      
+      setModalVisible(false);
+      fetchConfigData();
+    } catch (error) {
+      console.error('操作失败:', error);
+      message.error('操作失败');
+    }
+  };
+
+  const alertLevelOptions = getEnumOptions(AlertLevel, AlertLevelNameMap);
+
+  const notifyTypeOptions = getEnumOptions(NotifyType, NotifyTypeNameMap);
+
+  const enableStatusOptions = getEnumOptions(EnableStatus, EnableStatusNameMap);
+
+  const getAlertLevelTag = (level: AlertLevel) => {
+    switch (level) {
+      case AlertLevel.MINOR:
+        return <Tag color="blue">次要</Tag>;
+      case AlertLevel.NORMAL:
+        return <Tag color="green">一般</Tag>;
+      case AlertLevel.IMPORTANT:
+        return <Tag color="orange">重要</Tag>;
+      case AlertLevel.URGENT:
+        return <Tag color="red">紧急</Tag>;
+      default:
+        return <Tag>未知</Tag>;
+    }
+  };
+
+  const columns = [
+    {
+      title: '配置ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '设备',
+      dataIndex: 'device_id',
+      key: 'device_id',
+      render: (id: number) => {
+        const device = deviceOptions.find(opt => opt.value === id);
+        return device ? device.label : id;
+      },
+    },
+    {
+      title: '告警等级',
+      dataIndex: 'alert_level',
+      key: 'alert_level',
+      render: (level: AlertLevel) => getAlertLevelTag(level),
+    },
+    {
+      title: '通知类型',
+      dataIndex: 'notify_type',
+      key: 'notify_type',
+      render: (type: NotifyType) => NotifyTypeNameMap[type] || type,
+    },
+    {
+      title: '通知模板',
+      dataIndex: 'notify_template',
+      key: 'notify_template',
+      ellipsis: true,
+    },
+    {
+      title: '通知用户',
+      dataIndex: 'notify_users',
+      key: 'notify_users',
+      render: (users: number[]) => {
+        if (!users || users.length === 0) return '无';
+        
+        return users.map(id => {
+          const user = userOptions.find(opt => opt.value === id);
+          return user ? user.label : id;
+        }).join(', ');
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (status: EnableStatus) => (
+        status === EnableStatus.ENABLED ? 
+          <Badge status="success" text="启用" /> : 
+          <Badge status="default" text="禁用" />
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: AlertNotifyConfig) => (
+        <Space size="middle">
+          <Button size="small" type="primary" onClick={() => handleEdit(record)}>
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定删除此配置?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button size="small" danger>
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <Card title="告警通知配置" style={{ marginBottom: 16 }}>
+        <Form
+          form={formRef}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: 16 }}
+        >
+          <Form.Item name="device_id" label="设备">
+            <Select
+              placeholder="选择设备"
+              style={{ width: 200 }}
+              allowClear
+              options={deviceOptions}
+            />
+          </Form.Item>
+          <Form.Item name="alert_level" label="告警等级">
+            <Select
+              placeholder="选择告警等级"
+              style={{ width: 120 }}
+              allowClear
+              options={alertLevelOptions}
+            />
+          </Form.Item>
+          <Form.Item name="notify_type" label="通知类型">
+            <Select
+              placeholder="选择通知类型"
+              style={{ width: 120 }}
+              allowClear
+              options={notifyTypeOptions}
+            />
+          </Form.Item>
+          <Form.Item name="is_enabled" label="状态">
+            <Select
+              placeholder="选择状态"
+              style={{ width: 100 }}
+              allowClear
+              options={enableStatusOptions}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              查询
+            </Button>
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" onClick={handleAdd}>
+              新增
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={configData}
+          rowKey="id"
+          pagination={pagination}
+          loading={loading}
+          onChange={handleTableChange}
+        />
+      </Card>
+      
+      <Modal
+        title={modalTitle}
+        open={modalVisible}
+        onOk={handleModalSubmit}
+        onCancel={() => setModalVisible(false)}
+        width={600}
+      >
+        <Form
+          form={modalForm}
+          layout="vertical"
+        >
+          <Form.Item
+            name="device_id"
+            label="设备"
+            rules={[{ required: true, message: '请选择设备' }]}
+          >
+            <Select
+              placeholder="选择设备"
+              options={deviceOptions}
+            />
+          </Form.Item>
+          
+          <Form.Item
+            name="alert_level"
+            label="告警等级"
+            rules={[{ required: true, message: '请选择告警等级' }]}
+          >
+            <Select
+              placeholder="选择告警等级"
+              options={alertLevelOptions}
+            />
+          </Form.Item>
+          
+          <Form.Item
+            name="notify_type"
+            label="通知类型"
+            rules={[{ required: true, message: '请选择通知类型' }]}
+          >
+            <Select
+              placeholder="选择通知类型"
+              options={notifyTypeOptions}
+              onChange={(value) => {
+                if (value === NotifyType.SMS) {
+                  modalForm.setFieldsValue({ notify_users: [] });
+                }
+              }}
+            />
+          </Form.Item>
+          
+          <Form.Item
+            name="notify_template"
+            label="通知模板"
+          >
+            <Input.TextArea rows={3} placeholder="输入通知模板,可使用{{变量}}格式插入动态内容" />
+          </Form.Item>
+          
+          <Form.Item
+            noStyle
+            shouldUpdate={(prevValues, currentValues) => prevValues.notify_type !== currentValues.notify_type}
+          >
+            {({ getFieldValue }) =>
+              getFieldValue('notify_type') === NotifyType.SMS ? (
+                <Form.Item
+                  name="phone_number"
+                  label="手机号码"
+                  rules={[{ required: true, message: '请输入手机号码' }]}
+                >
+                  <Input placeholder="请输入接收短信的手机号码" />
+                </Form.Item>
+              ) : (
+                <Form.Item
+                  name="notify_users"
+                  label="通知用户"
+                  rules={[{ required: true, message: '请选择通知用户' }]}
+                >
+                  <Select
+                    placeholder="选择通知用户"
+                    mode="multiple"
+                    options={userOptions}
+                  />
+                </Form.Item>
+              )
+            }
+          </Form.Item>
+          
+          <Form.Item
+            name="is_enabled"
+            label="状态"
+            initialValue={EnableStatus.ENABLED}
+          >
+            <Select
+              placeholder="选择状态"
+              options={enableStatusOptions}
+            />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 294 - 0
client/admin/pages_alert_records.tsx

@@ -0,0 +1,294 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  useNavigate,
+} from 'react-router';
+import { 
+  Button, Table, Space,
+  Form, Select, message, 
+  Card, Tag, DatePicker
+} from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  ZichanInfo,  DeviceAlert
+} from '../share/monitorTypes.ts';
+
+import {
+AlertLevel, AlertStatus, MetricType,
+  AlertLevelNameMap, AlertStatusNameMap,
+  MetricTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+import { DeviceInstanceAPI, AlertAPI} from './api/index.ts';
+
+
+// 告警记录页面
+export const AlertRecordsPage = () => {
+    const [loading, setLoading] = useState(false);
+    const [alertData, setAlertData] = useState<DeviceAlert[]>([]);
+    const [pagination, setPagination] = useState({
+      current: 1,
+      pageSize: 10,
+      total: 0,
+    });
+    const [deviceOptions, setDeviceOptions] = useState<{label: string, value: number}[]>([]);
+    const [formRef] = Form.useForm();
+    
+    useEffect(() => {
+      fetchDeviceOptions();
+      fetchAlertData();
+    }, [pagination.current, pagination.pageSize]);
+  
+    const fetchDeviceOptions = async () => {
+      try {
+        const response = await DeviceInstanceAPI.getDeviceInstances();
+        if (response && response.data) {
+          const options = response.data.map((device: ZichanInfo) => ({
+            label: device.asset_name || `设备${device.id}`,
+            value: device.id
+          }));
+          setDeviceOptions(options);
+        }
+      } catch (error) {
+        console.error('获取设备列表失败:', error);
+        message.error('获取设备列表失败');
+      }
+    };
+  
+    const fetchAlertData = async () => {
+      setLoading(true);
+      try {
+        const values = formRef.getFieldsValue();
+        const params = {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          device_id: values.device_id,
+          metric_type: values.metric_type,
+          alert_level: values.alert_level,
+          status: values.status,
+          start_time: values.time?.[0]?.format('YYYY-MM-DD HH:mm:ss'),
+          end_time: values.time?.[1]?.format('YYYY-MM-DD HH:mm:ss'),
+        };
+        
+        const response = await AlertAPI.getAlertData(params);
+        
+        if (response) {
+          setAlertData(response.data || []);
+          setPagination({
+            ...pagination,
+            total: response.total || 0,
+          });
+        }
+      } catch (error) {
+        console.error('获取告警数据失败:', error);
+        message.error('获取告警数据失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+  
+    const handleSearch = (values: any) => {
+      setPagination({
+        ...pagination,
+        current: 1,
+      });
+      fetchAlertData();
+    };
+  
+    const handleTableChange = (newPagination: any) => {
+      setPagination({
+        ...pagination,
+        current: newPagination.current,
+        pageSize: newPagination.pageSize,
+      });
+    };
+  
+    const handleAlertHandle = async (record: DeviceAlert, mode: 'view' | 'edit' = 'edit') => {
+      try {
+        navigate(`/admin/alert-handle/${record.id}?mode=${mode}`);
+      } catch (error) {
+        console.error('处理告警失败:', error);
+        message.error('处理告警失败');
+      }
+    };
+  
+    const metricTypeOptions = getEnumOptions(MetricType, MetricTypeNameMap);
+  
+    const alertLevelOptions = getEnumOptions(AlertLevel, AlertLevelNameMap);
+  
+    const alertStatusOptions = getEnumOptions(AlertStatus, AlertStatusNameMap);
+  
+    const getAlertLevelTag = (level: AlertLevel) => {
+      switch (level) {
+        case AlertLevel.MINOR:
+          return <Tag color="blue">次要</Tag>;
+        case AlertLevel.NORMAL:
+          return <Tag color="green">一般</Tag>;
+        case AlertLevel.IMPORTANT:
+          return <Tag color="orange">重要</Tag>;
+        case AlertLevel.URGENT:
+          return <Tag color="red">紧急</Tag>;
+        default:
+          return <Tag>未知</Tag>;
+      }
+    };
+  
+    const getAlertStatusTag = (status: AlertStatus) => {
+      switch (status) {
+        case AlertStatus.PENDING:
+          return <Tag color="red">待处理</Tag>;
+        case AlertStatus.HANDLING:
+          return <Tag color="orange">处理中</Tag>;
+        case AlertStatus.RESOLVED:
+          return <Tag color="green">已解决</Tag>;
+        case AlertStatus.IGNORED:
+          return <Tag color="default">已忽略</Tag>;
+        default:
+          return <Tag>未知</Tag>;
+      }
+    };
+  
+    const navigate = useNavigate();
+  
+    const columns = [
+      {
+        title: '告警ID',
+        dataIndex: 'id',
+        key: 'id',
+        width: 80,
+      },
+      {
+        title: '设备名称',
+        dataIndex: 'device_name',
+        key: 'device_name',
+      },
+      {
+        title: '监控指标',
+        dataIndex: 'metric_type',
+        key: 'metric_type',
+        render: (text: string) => {
+          const option = metricTypeOptions.find(opt => opt.value === text);
+          return option ? option.label : text;
+        },
+      },
+      {
+        title: '触发值',
+        dataIndex: 'metric_value',
+        key: 'metric_value',
+      },
+      {
+        title: '告警等级',
+        dataIndex: 'alert_level',
+        key: 'alert_level',
+        render: (level: AlertLevel) => getAlertLevelTag(level),
+      },
+      {
+        title: '告警消息',
+        dataIndex: 'alert_message',
+        key: 'alert_message',
+        ellipsis: true,
+      },
+      {
+        title: '状态',
+        dataIndex: 'status',
+        key: 'status',
+        render: (status: AlertStatus) => getAlertStatusTag(status),
+      },
+      {
+        title: '告警时间',
+        dataIndex: 'created_at',
+        key: 'created_at',
+        render: (text: Date) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+      },
+      {
+        title: '操作',
+        key: 'action',
+        render: (_: any, record: DeviceAlert) => (
+          <Space size="middle">
+            {record.status === AlertStatus.PENDING && (
+              <Button size="small" type="primary" onClick={() => handleAlertHandle(record, 'edit')}>
+                处理
+              </Button>
+            )}
+            {record.status === AlertStatus.HANDLING && (
+              <Button size="small" type="primary" onClick={() => handleAlertHandle(record, 'edit')}>
+                继续处理
+              </Button>
+            )}
+            <Button size="small" onClick={() => handleAlertHandle(record, 'view')}>
+              查看
+            </Button>
+          </Space>
+        ),
+      },
+    ];
+  
+    return (
+      <div>
+        <Card title="告警记录" style={{ marginBottom: 16 }}>
+          <Form
+            form={formRef}
+            layout="inline"
+            onFinish={handleSearch}
+            style={{ marginBottom: 16 }}
+          >
+            <Form.Item name="device_id" label="设备">
+              <Select
+                placeholder="选择设备"
+                style={{ width: 200 }}
+                allowClear
+                options={deviceOptions}
+              />
+            </Form.Item>
+            <Form.Item name="metric_type" label="监控指标">
+              <Select
+                placeholder="选择监控指标"
+                style={{ width: 150 }}
+                allowClear
+                options={metricTypeOptions}
+              />
+            </Form.Item>
+            <Form.Item name="alert_level" label="告警等级">
+              <Select
+                placeholder="选择告警等级"
+                style={{ width: 120 }}
+                allowClear
+                options={alertLevelOptions}
+              />
+            </Form.Item>
+            <Form.Item name="status" label="状态">
+              <Select
+                placeholder="选择状态"
+                style={{ width: 120 }}
+                allowClear
+                options={alertStatusOptions}
+              />
+            </Form.Item>
+            <Form.Item name="time" label="时间范围">
+              <DatePicker.RangePicker
+                showTime
+                style={{ width: 380 }}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" htmlType="submit">
+                查询
+              </Button>
+            </Form.Item>
+          </Form>
+          
+          <Table
+            columns={columns}
+            dataSource={alertData}
+            rowKey="id"
+            pagination={pagination}
+            loading={loading}
+            onChange={handleTableChange}
+          />
+        </Card>
+      </div>
+    );
+  };

+ 158 - 0
client/admin/pages_alert_trend_chart.tsx

@@ -0,0 +1,158 @@
+import React, { useState } from 'react';
+import { 
+  Button, Form, Select, Card, Typography, DatePicker
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { Line } from "@ant-design/plots";
+import 'dayjs/locale/zh-cn';
+
+
+import { MonitorChartsAPI } from './api/index.ts';
+
+interface ChartTooltipInfo {
+  items: Array<Record<string, any>>;
+  title: string;
+}
+
+
+// 告警数据变化图表页面
+export const AlertTrendChartPage = () => {
+    const [timeRange, setTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>([
+      dayjs().subtract(1, 'day').startOf('day'),
+      dayjs().endOf('day')
+    ]);
+    const [dimension, setDimension] = useState<'hour' | 'day' | 'month'>('hour');
+  
+    const { data: alarmData, isLoading, refetch } = useQuery({
+      queryKey: ['adminZichanAlarm', timeRange, dimension],
+      queryFn: async () => {
+        const params = {
+          created_at_gte: timeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+          created_at_lte: timeRange[1].format('YYYY-MM-DD HH:mm:ss'),
+          dimension
+        };
+        
+        // const res = await axios.get<AlarmChartData[]>(`${API_BASE_URL}/big/zichan_alarm_chart`, { params });
+        const res = await MonitorChartsAPI.fetchAlarmData(params);
+        return res;
+      }
+    });
+  
+    const { Title } = Typography;
+    const { RangePicker } = DatePicker;
+  
+    const handleSearch = () => {
+      refetch();
+    };
+  
+    return (
+      <div>
+        <Title level={2}>告警数据趋势</Title>
+        
+        <Card>
+          <Form layout="inline" style={{ marginBottom: '16px' }}>
+            <Form.Item label="时间范围">
+              <RangePicker 
+                value={timeRange} 
+                onChange={(dates) => dates && setTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])} 
+                showTime
+              />
+            </Form.Item>
+            <Form.Item label="时间维度">
+              <Select 
+                value={dimension} 
+                onChange={setDimension}
+                options={[
+                  { label: '小时', value: 'hour' },
+                  { label: '天', value: 'day' },
+                  { label: '月', value: 'month' }
+                ]}
+                style={{ width: '100px' }}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" onClick={handleSearch}>查询</Button>
+            </Form.Item>
+          </Form>
+          
+          <div style={{ height: '500px' }}>
+            {!isLoading && alarmData && (
+              <Line
+                data={alarmData}
+                xField="time_interval"
+                yField="total_devices"
+                smooth={true}
+                color="#36cfc9"
+                label={{
+                  position: 'top',
+                  style: {
+                    fill: '#000',
+                    fontSize: 12,
+                    fontWeight: 500,
+                  },
+                  text: (items: Record<string, any>) => {
+                    const value = items['total_devices'];
+                    
+                    // if (value === 0) return null;
+                    
+                    // const maxValue = Math.max(...(alarmData || []).map(item => item.total_devices));
+                    
+                    // if (value < maxValue * 0.3 && alarmData && alarmData.length > 8) return null;
+                    
+                    return `${items['time_interval']}\n(${value})`;
+                  },
+                  transform: [{ type: 'overlapDodgeY' }],
+                }}
+                point={{
+                  size: 5,
+                  shape: 'diamond',
+                }}
+                xAxis={{
+                  label: {
+                    style: {
+                      fill: '#000',
+                    },
+                    autoHide: true,
+                    autoRotate: true,
+                  },
+                }}
+                yAxis={{
+                  label: {
+                    style: {
+                      fill: '#000',
+                    },
+                  },
+                }}
+                autoFit={true}
+                interaction={{
+                  tooltip: {
+                    render: (_: unknown, { items, title }: ChartTooltipInfo) => {
+                      if (!items || items.length === 0) return '';
+                      
+                      // 获取当前选中项的数据
+                      const item = items[0];
+                      
+                      // 根据value找到对应的完整数据项
+                      const fullData = alarmData?.find(d => d.total_devices === item.value);
+                      if (!fullData) return '';
+                      
+                      return `<div class="bg-white p-2 rounded">
+                        <div class="flex items-center">
+                          <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                          <span class="font-semibold text-gray-900">${fullData.time_interval}</span>
+                        </div>
+                        <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                      </div>`;
+                    }
+                  }
+                }}
+              />
+            )}
+          </div>
+        </Card>
+      </div>
+    );
+  };

+ 125 - 0
client/admin/pages_asset_category_chart.tsx

@@ -0,0 +1,125 @@
+import React from 'react';
+import { 
+  Table, Card, Typography
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import { Pie } from "@ant-design/plots";
+import 'dayjs/locale/zh-cn';
+import type { 
+  CategoryChartDataWithPercent, 
+} from '../share/monitorTypes.ts';
+
+import { MonitorChartsAPI } from './api/index.ts';
+
+interface ChartTooltipInfo {
+  items: Array<Record<string, any>>;
+  title: string;
+}
+
+export const AssetCategoryChartPage = () => {
+  const { data: categoryData, isLoading } = useQuery({
+    queryKey: ['adminZichanCategory'],
+    queryFn: MonitorChartsAPI.fetchCategoryData
+  });
+
+  const { Title } = Typography;
+
+  return (
+    <div>
+      <Title level={2}>资产分类分布</Title>
+      <Card loading={isLoading}>
+        <div style={{ height: '500px' }}>
+          {categoryData && (
+            <Pie
+              data={categoryData}
+              angleField="设备数"
+              colorField="设备分类"
+              radius={0.9}
+              innerRadius={0.8}
+              label={{
+                position: 'outside',
+                text: ({ 设备分类, 设备数, 百分比, percent }: CategoryChartDataWithPercent & { percent: number }) => {
+                  // 只有占比超过5%的项才显示标签
+                  if (percent < 0.05) return null;
+                  return `${设备分类}\n(${设备数})`;
+                },
+                style: {
+                  fill: '#000',
+                  fontSize: 12,
+                  fontWeight: 500,
+                },
+                transform: [{ type: 'overlapDodgeY' }],
+              }}
+              theme={{
+                colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
+              }}
+              interactions={[
+                { type: 'element-active' },
+                { type: 'element-selected' }
+              ]}
+              interaction={{
+                tooltip: {
+                  render: (_: unknown, { items, title }: ChartTooltipInfo) => {
+                    if (!items || items.length === 0) return '';
+                    
+                    // 获取当前选中项的数据
+                    const item = items[0];
+                    
+                    // 根据value找到对应的完整数据项
+                    const fullData = categoryData?.find(d => d['设备数'] === item.value);
+                    if (!fullData) return '';
+                    
+                    return `<div class="bg-white p-2 rounded">
+                      <div class="flex items-center">
+                        <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                        <span class="font-semibold text-gray-900">${fullData['设备分类']}</span>
+                      </div>
+                      <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                      <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
+                    </div>`;
+                  }
+                }
+              }}
+            />
+          )}
+        </div>
+      </Card>
+      <Card style={{ marginTop: '16px' }}>
+        <Title level={4}>数据明细</Title>
+        <Table 
+          dataSource={categoryData} 
+          rowKey={(record) => record['设备分类']}
+          columns={[
+            {
+              title: '设备分类',
+              dataIndex: '设备分类',
+              key: '设备分类',
+            },
+            {
+              title: '设备数量',
+              dataIndex: '设备数',
+              key: '设备数',
+              sorter: (a, b) => a['设备数'] - b['设备数'],
+            },
+            {
+              title: '占比',
+              dataIndex: '百分比',
+              key: '百分比',
+              render: (text) => `${text}%`,
+              sorter: (a, b) => parseFloat(a['百分比']) - parseFloat(b['百分比']),
+            }
+          ]}
+          pagination={false}
+        />
+      </Card>
+    </div>
+  );
+};
+
+
+
+
+
+

+ 120 - 0
client/admin/pages_asset_transfer_chart.tsx

@@ -0,0 +1,120 @@
+import React from 'react';
+import { 
+  Table, Card, Typography
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import {  Pie } from "@ant-design/plots";
+import 'dayjs/locale/zh-cn';
+import type { 
+  StateChartDataWithPercent, 
+} from '../share/monitorTypes.ts';
+
+import { MonitorChartsAPI } from './api/index.ts';
+
+interface ChartTooltipInfo {
+  items: Array<Record<string, any>>;
+  title: string;
+}
+
+// 资产流转状态图表页面
+export const AssetTransferChartPage = () => {
+    const { data: stateData, isLoading } = useQuery({
+      queryKey: ['adminZichanState'],
+      queryFn: MonitorChartsAPI.fetchStateData
+    });
+  
+    const { Title } = Typography;
+  
+    return (
+      <div>
+        <Title level={2}>资产流转状态分布</Title>
+        <Card loading={isLoading}>
+          <div style={{ height: '500px' }}>
+            {stateData && (
+              <Pie
+                data={stateData}
+                angleField="设备数"
+                colorField="资产流转"
+                radius={0.9}
+                innerRadius={0.8}
+                label={{
+                  position: 'outside',
+                  text: ({ 资产流转, 设备数, 百分比, percent }: StateChartDataWithPercent & { percent: number }) => {
+                    // 只有占比超过5%的项才显示标签
+                    if (percent < 0.05) return null;
+                    return `${资产流转}\n(${设备数})`;
+                  },
+                  style: {
+                    fill: '#000',
+                    fontSize: 12,
+                    fontWeight: 500,
+                  },
+                  transform: [{ type: 'overlapDodgeY' }],
+                }}
+                theme={{
+                  colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
+                }}
+                interactions={[
+                  { type: 'element-active' },
+                  { type: 'element-selected' }
+                ]}
+                interaction={{
+                  tooltip: {
+                    render: (_: unknown, { items, title }: ChartTooltipInfo) => {
+                      if (!items || items.length === 0) return '';
+                      
+                      // 获取当前选中项的数据
+                      const item = items[0];
+                      
+                      // 根据value找到对应的完整数据项
+                      const fullData = stateData?.find(d => d['设备数'] === item.value);
+                      if (!fullData) return '';
+                      
+                      return `<div class="bg-white p-2 rounded">
+                        <div class="flex items-center">
+                          <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                          <span class="font-semibold text-gray-900">${fullData['资产流转']}</span>
+                        </div>
+                        <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                        <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
+                      </div>`;
+                    }
+                  }
+                }}
+              />
+            )}
+          </div>
+        </Card>
+        <Card style={{ marginTop: '16px' }}>
+          <Title level={4}>数据明细</Title>
+          <Table 
+            dataSource={stateData} 
+            rowKey={(record) => record['资产流转']}
+            columns={[
+              {
+                title: '资产流转状态',
+                dataIndex: '资产流转',
+                key: '资产流转',
+              },
+              {
+                title: '设备数量',
+                dataIndex: '设备数',
+                key: '设备数',
+                sorter: (a, b) => a['设备数'] - b['设备数'],
+              },
+              {
+                title: '占比',
+                dataIndex: '百分比',
+                key: '百分比',
+                render: (text) => `${text}%`,
+                sorter: (a, b) => parseFloat(a['百分比']) - parseFloat(b['百分比']),
+              }
+            ]}
+            pagination={false}
+          />
+        </Card>
+      </div>
+    );
+  };

+ 204 - 0
client/admin/pages_chart.tsx

@@ -0,0 +1,204 @@
+import React from 'react';
+import { 
+  Card, Spin, Row, Col, Statistic,
+} from 'antd';
+
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import { Line , Pie, Column} from "@ant-design/plots";
+import 'dayjs/locale/zh-cn';
+
+
+import { ChartAPI } from './api/index.ts';
+import { useTheme } from './hooks_sys.tsx';
+
+
+
+// 用户活跃度图表组件
+const UserActivityChart: React.FC = () => {
+  const { isDark } = useTheme();
+  const { data: activityData, isLoading } = useQuery({
+    queryKey: ['userActivity'],
+    queryFn: async () => {
+      const response = await ChartAPI.getUserActivity();
+      return response.data;
+    }
+  });
+
+  if (isLoading) return <Spin />;
+
+  const config = {
+    data: activityData || [],
+    xField: 'date',
+    yField: 'count',
+    smooth: true,
+    theme: isDark ? 'dark' : 'light',
+    color: '#1890ff',
+    areaStyle: {
+      fill: 'l(270) 0:#1890ff10 1:#1890ff',
+    },
+  };
+
+  return (
+    <Card title="用户活跃度趋势" variant="borderless">
+      <Line {...config} />
+    </Card>
+  );
+};
+
+// 文件上传统计图表组件
+const FileUploadsChart: React.FC = () => {
+  const { isDark } = useTheme();
+  const { data: uploadsData, isLoading } = useQuery({
+    queryKey: ['fileUploads'],
+    queryFn: async () => {
+      const response = await ChartAPI.getFileUploads();
+      return response.data;
+    }
+  });
+
+  if (isLoading) return <Spin />;
+
+  const config = {
+    data: uploadsData || [],
+    xField: 'month',
+    yField: 'count',
+    theme: isDark ? 'dark' : 'light',
+    color: '#52c41a',
+    label: {
+      position: 'middle',
+      style: {
+        fill: '#FFFFFF',
+        opacity: 0.6,
+      },
+    },
+    meta: {
+      month: {
+        alias: '月份',
+      },
+      count: {
+        alias: '上传数量',
+      },
+    },
+  };
+
+  return (
+    <Card title="文件上传统计" variant="borderless">
+      <Column {...config} />
+    </Card>
+  );
+};
+
+// 文件类型分布图表组件
+const FileTypesChart: React.FC = () => {
+  const { isDark } = useTheme();
+  const { data: typesData, isLoading } = useQuery({
+    queryKey: ['fileTypes'],
+    queryFn: async () => {
+      const response = await ChartAPI.getFileTypes();
+      return response.data;
+    }
+  });
+
+  if (isLoading) return <Spin />;
+
+  const config = {
+    data: typesData || [],
+    angleField: 'value',
+    colorField: 'type',
+    radius: 0.8,
+    theme: isDark ? 'dark' : 'light',
+    label: {
+      type: 'spider',
+      labelHeight: 28,
+      content: '{name}\n{percentage}',
+    },
+    interactions: [
+      {
+        type: 'element-active',
+      },
+    ],
+  };
+
+  return (
+    <Card title="文件类型分布" variant="borderless">
+      <Pie {...config} />
+    </Card>
+  );
+};
+
+// 仪表盘概览组件
+const DashboardOverview: React.FC = () => {
+  const { data: overviewData, isLoading } = useQuery({
+    queryKey: ['dashboardOverview'],
+    queryFn: async () => {
+      const response = await ChartAPI.getDashboardOverview();
+      return response.data;
+    }
+  });
+
+  if (isLoading) return <Spin />;
+
+  return (
+    <Row gutter={[16, 16]}>
+      <Col xs={12} sm={12} md={6}>
+        <Card variant="borderless">
+          <Statistic
+            title="用户总数"
+            value={overviewData?.userCount || 0}
+            valueStyle={{ color: '#1890ff' }}
+          />
+        </Card>
+      </Col>
+      <Col xs={12} sm={12} md={6}>
+        <Card variant="borderless">
+          <Statistic
+            title="文件总数"
+            value={overviewData?.fileCount || 0}
+            valueStyle={{ color: '#52c41a' }}
+          />
+        </Card>
+      </Col>
+      <Col xs={12} sm={12} md={6}>
+        <Card variant="borderless">
+          <Statistic
+            title="文章总数"
+            value={overviewData?.articleCount || 0}
+            valueStyle={{ color: '#faad14' }}
+          />
+        </Card>
+      </Col>
+      <Col xs={12} sm={12} md={6}>
+        <Card variant="borderless">
+          <Statistic
+            title="今日登录"
+            value={overviewData?.todayLoginCount || 0}
+            valueStyle={{ color: '#722ed1' }}
+          />
+        </Card>
+      </Col>
+    </Row>
+  );
+};
+
+// 图表仪表盘页面组件
+export const ChartDashboardPage: React.FC = () => {
+  return (
+    <div className="chart-dashboard">
+      <DashboardOverview />
+      <div style={{ height: 24 }} />
+      <Row gutter={[16, 16]}>
+        <Col xs={24} lg={12}>
+          <UserActivityChart />
+        </Col>
+        <Col xs={24} lg={12}>
+          <FileUploadsChart />
+        </Col>
+        <Col xs={24}>
+          <FileTypesChart />
+        </Col>
+      </Row>
+    </div>
+  );
+};

+ 44 - 0
client/admin/pages_dashboard.tsx

@@ -0,0 +1,44 @@
+import React from 'react';
+import { 
+  Card, Row, Col, Typography, Statistic
+} from 'antd';
+
+const { Title } = Typography;
+
+// 仪表盘页面
+export const DashboardPage = () => {
+  return (
+    <div>
+      <Title level={2}>仪表盘</Title>
+      <Row gutter={16}>
+        <Col span={8}>
+          <Card>
+            <Statistic
+              title="活跃用户"
+              value={112893}
+              loading={false}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card>
+            <Statistic
+              title="系统消息"
+              value={93}
+              loading={false}
+            />
+          </Card>
+        </Col>
+        <Col span={8}>
+          <Card>
+            <Statistic
+              title="在线用户"
+              value={1128}
+              loading={false}
+            />
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  );
+};

+ 455 - 0
client/admin/pages_device_alert_rule.tsx

@@ -0,0 +1,455 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Switch, 
+  Popconfirm, Tag, Collapse, 
+} from 'antd';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  ZichanInfo,  DeviceAlertRule,
+} from '../share/monitorTypes.ts';
+
+import {
+  AlertLevel, MetricType,
+  AlertLevelNameMap, MetricTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import {
+  EnableStatus, EnableStatusNameMap, 
+} from '../share/types.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+import { DeviceInstanceAPI,  DeviceAlertRuleAPI} from './api/index.ts';
+
+
+
+// 设备告警规则页面
+export const DeviceAlertRulePage = () => {
+    const [loading, setLoading] = useState(false);
+    const [ruleData, setRuleData] = useState<DeviceAlertRule[]>([]);
+    const [pagination, setPagination] = useState({
+      current: 1,
+      pageSize: 10,
+      total: 0,
+    });
+    const [deviceOptions, setDeviceOptions] = useState<{label: string, value: number}[]>([]);
+    const [modalVisible, setModalVisible] = useState(false);
+    const [modalTitle, setModalTitle] = useState('新增告警规则');
+    const [currentRecord, setCurrentRecord] = useState<DeviceAlertRule | null>(null);
+    const [formRef] = Form.useForm();
+    const [modalForm] = Form.useForm();
+    
+    useEffect(() => {
+      fetchDeviceOptions();
+      fetchRuleData();
+    }, [pagination.current, pagination.pageSize]);
+  
+    const fetchDeviceOptions = async () => {
+      try {
+        const response = await DeviceInstanceAPI.getDeviceInstances();
+        if (response && response.data) {
+          const options = response.data.map((device: ZichanInfo) => ({
+            label: device.asset_name || `设备${device.id}`,
+            value: device.id
+          }));
+          setDeviceOptions(options);
+        }
+      } catch (error) {
+        console.error('获取设备列表失败:', error);
+        message.error('获取设备列表失败');
+      }
+    };
+  
+    const fetchRuleData = async () => {
+      setLoading(true);
+      try {
+        const values = formRef.getFieldsValue();
+        const params = {
+          page: pagination.current,
+          pageSize: pagination.pageSize,
+          device_id: values.device_id,
+          metric_type: values.metric_type,
+          alert_level: values.alert_level,
+          is_enabled: values.is_enabled,
+        };
+        
+        const response = await DeviceAlertRuleAPI.getDeviceAlertRules(params);
+        
+        if (response) {
+          setRuleData(response.data || []);
+          setPagination({
+            ...pagination,
+            total: response.total || 0,
+          });
+        }
+      } catch (error) {
+        console.error('获取告警规则失败:', error);
+        message.error('获取告警规则失败');
+      } finally {
+        setLoading(false);
+      }
+    };
+  
+    const handleSearch = (values: any) => {
+      setPagination({
+        ...pagination,
+        current: 1,
+      });
+      fetchRuleData();
+    };
+  
+    const handleTableChange = (newPagination: any) => {
+      setPagination({
+        ...pagination,
+        current: newPagination.current,
+        pageSize: newPagination.pageSize,
+      });
+    };
+  
+    const handleAdd = () => {
+      setModalTitle('新增告警规则');
+      setCurrentRecord(null);
+      modalForm.resetFields();
+      setModalVisible(true);
+    };
+  
+    const handleEdit = (record: DeviceAlertRule) => {
+      setModalTitle('编辑告警规则');
+      setCurrentRecord(record);
+      modalForm.setFieldsValue(record);
+      setModalVisible(true);
+    };
+  
+    const handleDelete = async (id: number) => {
+      try {
+        await DeviceAlertRuleAPI.deleteDeviceAlertRule(id);
+        message.success('删除成功');
+        fetchRuleData();
+      } catch (error) {
+        console.error('删除失败:', error);
+        message.error('删除失败');
+      }
+    };
+  
+    const handleEnableChange = async (record: DeviceAlertRule, enabled: boolean) => {
+      try {
+        await DeviceAlertRuleAPI.updateDeviceAlertRule(record.id, {
+          is_enabled: enabled ? EnableStatus.ENABLED : EnableStatus.DISABLED
+        });
+        message.success(`${enabled ? '启用' : '禁用'}成功`);
+        fetchRuleData();
+      } catch (error) {
+        console.error('操作失败:', error);
+        message.error('操作失败');
+      }
+    };
+  
+    const handleModalSubmit = async () => {
+      try {
+        const values = await modalForm.validateFields();
+        
+        if (currentRecord) {
+          // 更新
+          await DeviceAlertRuleAPI.updateDeviceAlertRule(currentRecord.id, values);
+          message.success('更新成功');
+        } else {
+          // 新增
+          await DeviceAlertRuleAPI.createDeviceAlertRule(values);
+          message.success('添加成功');
+        }
+        
+        setModalVisible(false);
+        fetchRuleData();
+      } catch (error) {
+        console.error('操作失败:', error);
+        message.error('操作失败');
+      }
+    };
+  
+    const metricTypeOptions = getEnumOptions(MetricType, MetricTypeNameMap);
+  
+    const alertLevelOptions = getEnumOptions(AlertLevel, AlertLevelNameMap);
+  
+    const enableStatusOptions = getEnumOptions(EnableStatus, EnableStatusNameMap);
+  
+    const getAlertLevelTag = (level: AlertLevel) => {
+      switch (level) {
+        case AlertLevel.MINOR:
+          return <Tag color="blue">次要</Tag>;
+        case AlertLevel.NORMAL:
+          return <Tag color="green">一般</Tag>;
+        case AlertLevel.IMPORTANT:
+          return <Tag color="orange">重要</Tag>;
+        case AlertLevel.URGENT:
+          return <Tag color="red">紧急</Tag>;
+        default:
+          return <Tag>未知</Tag>;
+      }
+    };
+  
+    const columns = [
+      {
+        title: '规则ID',
+        dataIndex: 'id',
+        key: 'id',
+        width: 80,
+      },
+      {
+        title: '设备',
+        dataIndex: 'device_id',
+        key: 'device_id',
+        render: (id: number) => {
+          const device = deviceOptions.find(opt => opt.value === id);
+          return device ? device.label : id;
+        },
+      },
+      {
+        title: '监控指标',
+        dataIndex: 'metric_type',
+        key: 'metric_type',
+        render: (text: string) => {
+          const option = metricTypeOptions.find(opt => opt.value === text);
+          return option ? option.label : text;
+        },
+      },
+      {
+        title: '最小阈值',
+        dataIndex: 'min_value',
+        key: 'min_value',
+      },
+      {
+        title: '最大阈值',
+        dataIndex: 'max_value',
+        key: 'max_value',
+      },
+      {
+        title: '持续时间(秒)',
+        dataIndex: 'duration_seconds',
+        key: 'duration_seconds',
+      },
+      {
+        title: '告警等级',
+        dataIndex: 'alert_level',
+        key: 'alert_level',
+        render: (level: AlertLevel) => getAlertLevelTag(level),
+      },
+      {
+        title: '状态',
+        dataIndex: 'is_enabled',
+        key: 'is_enabled',
+        render: (status: EnableStatus, record: DeviceAlertRule) => (
+          <Switch
+            checked={status === EnableStatus.ENABLED}
+            onChange={(checked) => handleEnableChange(record, checked)}
+            checkedChildren="启用"
+            unCheckedChildren="禁用"
+          />
+        ),
+      },
+      {
+        title: '操作',
+        key: 'action',
+        render: (_: any, record: DeviceAlertRule) => (
+          <Space size="middle">
+            <Button size="small" type="primary" onClick={() => handleEdit(record)}>
+              编辑
+            </Button>
+            <Popconfirm
+              title="确定删除此规则?"
+              onConfirm={() => handleDelete(record.id)}
+              okText="确定"
+              cancelText="取消"
+            >
+              <Button size="small" danger>
+                删除
+              </Button>
+            </Popconfirm>
+          </Space>
+        ),
+      },
+    ];
+  
+    return (
+      <div>
+        <Card title="设备告警规则" style={{ marginBottom: 16 }}>
+          <Form
+            form={formRef}
+            layout="inline"
+            onFinish={handleSearch}
+            style={{ marginBottom: 16 }}
+          >
+            <Form.Item name="device_id" label="设备">
+              <Select
+                placeholder="选择设备"
+                style={{ width: 200 }}
+                allowClear
+                options={deviceOptions}
+              />
+            </Form.Item>
+            <Form.Item name="metric_type" label="监控指标">
+              <Select
+                placeholder="选择监控指标"
+                style={{ width: 150 }}
+                allowClear
+                options={metricTypeOptions}
+              />
+            </Form.Item>
+            <Form.Item name="alert_level" label="告警等级">
+              <Select
+                placeholder="选择告警等级"
+                style={{ width: 120 }}
+                allowClear
+                options={alertLevelOptions}
+              />
+            </Form.Item>
+            <Form.Item name="is_enabled" label="状态">
+              <Select
+                placeholder="选择状态"
+                style={{ width: 100 }}
+                allowClear
+                options={enableStatusOptions}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" htmlType="submit">
+                查询
+              </Button>
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" onClick={handleAdd}>
+                新增
+              </Button>
+            </Form.Item>
+          </Form>
+          
+          <Table
+            columns={columns}
+            dataSource={ruleData}
+            rowKey="id"
+            pagination={pagination}
+            loading={loading}
+            onChange={handleTableChange}
+          />
+        </Card>
+        
+        <Modal
+          title={modalTitle}
+          open={modalVisible}
+          onOk={handleModalSubmit}
+          onCancel={() => setModalVisible(false)}
+          width={600}
+        >
+          <Form
+            form={modalForm}
+            layout="vertical"
+          >
+            <Form.Item
+              name="device_id"
+              label="设备"
+              rules={[{ required: true, message: '请选择设备' }]}
+            >
+              <Select
+                placeholder="选择设备"
+                options={deviceOptions}
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="metric_type"
+              label="监控指标"
+              rules={[{ required: true, message: '请选择监控指标' }]}
+            >
+              <Select
+                placeholder="选择监控指标"
+                options={metricTypeOptions}
+              />
+            </Form.Item>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="min_value"
+                  label="最小阈值"
+                  tooltip="指标值低于此阈值时触发告警,例如CPU使用率低于5%时告警。如不需要可留空。"
+                >
+                  <Input type="number" placeholder="输入最小阈值,例如:温度5、湿度20" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="max_value"
+                  label="最大阈值"
+                  tooltip="指标值高于此阈值时触发告警,例如CPU使用率高于90%时告警。如不需要可留空。"
+                >
+                  <Input type="number" placeholder="输入最大阈值,例如:温度30、CPU使用率90" />
+                </Form.Item>
+              </Col>
+            </Row>
+               
+            <Collapse defaultActiveKey={[]} style={{ marginBottom: 16 }}>
+              <Collapse.Panel 
+                header="阈值参考 (点击展开查看)"
+                key="1"
+              >
+                <div>
+                  <p>常见阈值参考:</p>
+                  <ul>
+                    <li>温度(temperature):最大阈值建议设为 30-35℃</li>
+                    <li>湿度(humidity):最小阈值20%,最大阈值80%</li>
+                    <li>CPU使用率(cpu_usage):最大阈值建议设为 85-95%</li>
+                    <li>内存使用率(memory_usage):最大阈值建议设为 85-95%</li>
+                    <li>磁盘使用率(disk_usage):最大阈值建议设为 85-95%</li>
+                    <li>Ping时间(ping_time):最大阈值建议设为 100-200ms</li>
+                    <li>丢包率(packet_loss):最大阈值建议设为 2-5%</li>
+                    <li>连接状态(connection_status):最大阈值建议设为 0(0表示已连接,大于0表示未连接)</li>
+                  </ul>
+                  <p>注意:最小值或最大值必须至少填写一项。</p>
+                </div>
+              </Collapse.Panel>
+            </Collapse>
+            
+            <Form.Item
+              name="duration_seconds"
+              label="持续时间(秒)"
+              rules={[{ required: true, message: '请输入持续时间' }]}
+              initialValue={60}
+            >
+              <Input type="number" placeholder="输入持续时间" />
+            </Form.Item>
+            
+            <Form.Item
+              name="alert_level"
+              label="告警等级"
+              rules={[{ required: true, message: '请选择告警等级' }]}
+            >
+              <Select
+                placeholder="选择告警等级"
+                options={alertLevelOptions}
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="alert_message"
+              label="告警消息模板"
+            >
+              <Input.TextArea rows={3} placeholder="输入告警消息模板,可使用{{变量}}格式插入动态内容" />
+            </Form.Item>
+            
+            <Form.Item
+              name="is_enabled"
+              label="状态"
+              initialValue={EnableStatus.ENABLED}
+            >
+              <Select
+                placeholder="选择状态"
+                options={enableStatusOptions}
+              />
+            </Form.Item>
+          </Form>
+        </Modal>
+      </div>
+    );
+  };

+ 421 - 0
client/admin/pages_device_instances.tsx

@@ -0,0 +1,421 @@
+import React, { useState } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Typography, Badge
+} from 'antd';
+
+import { 
+  useQuery,
+  useMutation,
+  useQueryClient
+} from '@tanstack/react-query';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  ZichanInfo, DeviceInstance, DeviceType,
+} from '../share/monitorTypes.ts';
+
+import { 
+  DeviceProtocolType, 
+} from '../share/monitorTypes.ts';
+import {
+  EnableStatus, DeleteStatus,
+} from '../share/types.ts';
+
+import { DeviceInstanceAPI , DeviceTypeAPI, ZichanAPI} from './api/index.ts';
+const { Title } = Typography;
+
+// 设备管理页面
+export const DeviceInstancesPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [modalVisible, setModalVisible] = useState(false);
+  const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0
+  });
+  
+  // 获取设备列表
+  const {
+    data,
+    isLoading,
+    refetch
+  } = useQuery({
+    queryKey: ['deviceInstances', pagination.current, pagination.pageSize],
+    queryFn: async () => {
+      try {
+        const response = await DeviceInstanceAPI.getDeviceInstances({
+          page: pagination.current,
+          limit: pagination.pageSize
+        });
+        
+        setPagination(prev => ({
+          ...prev,
+          total: response.pagination.total
+        }));
+        
+        return response.data;
+      } catch (error) {
+        message.error('获取设备列表失败');
+        return [];
+      }
+    }
+  });
+  
+  // 获取设备类型
+  const { data: deviceTypes = [] } = useQuery({
+    queryKey: ['deviceTypesAll'],
+    queryFn: async () => {
+      try {
+        const response = await DeviceTypeAPI.getDeviceTypes({ pageSize: 100 })
+        return response.data;
+      } catch (error) {
+        message.error('获取设备类型失败');
+        return [];
+      }
+    }
+  });
+  
+  // 获取可用资产
+  const { data: zichanList = [] } = useQuery({
+    queryKey: ['zichanListAll'],
+    queryFn: async () => {
+      try {
+        const response = await ZichanAPI.getZichanList({ limit: 500 })
+        
+        // 筛选出未分配给设备的资产
+        const deviceInstances = data || [];
+        const usedAssetIds = deviceInstances.map((d: DeviceInstance) => d.id);
+        
+        return response.data.filter(
+          (z: ZichanInfo) => !usedAssetIds.includes(z.id)
+        );
+      } catch (error) {
+        message.error('获取可用资产失败');
+        return [];
+      }
+    }
+  });
+  
+  const queryClient = useQueryClient();
+  
+  // 提交表单
+  const { mutate: submitForm, isPending: isSubmitting } = useMutation({
+    mutationFn: async (values: Partial<DeviceInstance>) => {
+      const deviceData = {
+        ...values,
+      };
+      
+      if (formMode === 'create') {
+        return DeviceInstanceAPI.createDeviceInstance(deviceData);
+      } else {
+        return DeviceInstanceAPI.updateDeviceInstance(selectedId!, deviceData);
+      }
+    },
+    onSuccess: () => {
+      message.success(formMode === 'create' ? '创建成功' : '更新成功');
+      setModalVisible(false);
+      refetch();
+      form.resetFields();
+    },
+    onError: () => {
+      message.error(formMode === 'create' ? '创建失败' : '更新失败');
+    }
+  });
+  
+  // 删除设备
+  const { mutate: deleteDevice, isPending: isDeleting } = useMutation({
+    mutationFn: async (id: number) => {
+      return DeviceInstanceAPI.deleteDeviceInstance(id);
+    },
+    onSuccess: () => {
+      message.success('删除成功');
+      refetch();
+    },
+    onError: () => {
+      message.error('删除失败');
+    }
+  });
+  
+  // 处理表单提交
+  const handleSubmit = (values: Partial<DeviceInstance>) => {
+    submitForm({
+      ...values,
+      is_enabled: values.is_enabled ?? EnableStatus.ENABLED,
+      is_deleted: DeleteStatus.NOT_DELETED
+    });
+  };
+  
+  // 处理编辑
+  const handleEdit = (record: DeviceInstance) => {
+    setSelectedId(record.id);
+    setFormMode('edit');
+    form.setFieldsValue({
+      ...record
+    });
+    setModalVisible(true);
+  };
+  
+  // 处理删除
+  const handleDelete = (id: number) => {
+    Modal.confirm({
+      title: '确认删除',
+      content: '确定要删除这个设备吗?',
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => deleteDevice(id)
+    });
+  };
+  
+  // 处理添加
+  const handleAdd = () => {
+    setFormMode('create');
+    setSelectedId(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+  
+  // 处理页码变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setPagination({
+      ...pagination,
+      current: page,
+      pageSize: pageSize || pagination.pageSize
+    });
+  };
+  
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80
+    },
+    {
+      title: '设备类型',
+      dataIndex: 'type_id',
+      key: 'type_id',
+      render: (typeId: number) => {
+        const deviceType = deviceTypes.find((t: DeviceType) => t.id === typeId);
+        return deviceType ? deviceType.name : `未知类型(${typeId})`;
+      }
+    },
+    {
+      title: '通信协议',
+      dataIndex: 'protocol',
+      key: 'protocol'
+    },
+    {
+      title: '通信地址',
+      dataIndex: 'address',
+      key: 'address'
+    },
+    {
+      title: '采集间隔(秒)',
+      dataIndex: 'collect_interval',
+      key: 'collect_interval'
+    },
+    {
+      title: '最后采集时间',
+      dataIndex: 'last_collect_time',
+      key: 'last_collect_time',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
+    },
+    {
+      title: '状态地址',
+      dataIndex: 'status_address',
+      key: 'status_address'
+    },
+    {
+      title: '连接配置',
+      dataIndex: 'connection_config',
+      key: 'connection_config'
+    },
+    {
+      title: '状态',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (status: number) => (
+        <Badge
+          status={status === EnableStatus.ENABLED ? 'success' : 'error'}
+          text={status === EnableStatus.ENABLED ? '启用' : '禁用'}
+        />
+      )
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: DeviceInstance) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
+          <Button type="link" danger onClick={() => handleDelete(record.id)}>删除</Button>
+        </Space>
+      )
+    }
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>设备管理</Title>
+      <Card>
+        <Row justify="end" style={{ marginBottom: 16 }}>
+          <Button type="primary" onClick={handleAdd}>
+            添加设备
+          </Button>
+        </Row>
+        
+        <Table
+          columns={columns}
+          dataSource={data || []}
+          loading={isLoading}
+          rowKey="id"
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true
+          }}
+        />
+        
+        <Modal
+          title={formMode === 'create' ? '添加设备' : '编辑设备'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          footer={null}
+          width={700}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+          >
+            <Row gutter={16}>
+              {formMode === 'create' && (
+                <Col span={24}>
+                  <Form.Item
+                    name="id"
+                    label="关联资产"
+                    rules={[{ required: true, message: '请选择关联资产' }]}
+                  >
+                    <Select placeholder="请选择关联资产">
+                      {zichanList.map((zichan: ZichanInfo) => (
+                        <Select.Option key={zichan.id} value={zichan.id}>
+                          {zichan.asset_name || `资产ID: ${zichan.id}`}
+                        </Select.Option>
+                      ))}
+                    </Select>
+                  </Form.Item>
+                </Col>
+              )}
+              
+              <Col span={12}>
+                <Form.Item
+                  name="type_id"
+                  label="设备类型"
+                  rules={[{ required: true, message: '请选择设备类型' }]}
+                >
+                  <Select placeholder="请选择设备类型">
+                    {deviceTypes.map((type: DeviceType) => (
+                      <Select.Option key={type.id} value={type.id}>
+                        {type.name}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                </Form.Item>
+              </Col>
+              
+              <Col span={12}>
+                <Form.Item
+                  name="protocol"
+                  label="通信协议"
+                  rules={[{ required: true, message: '请选择通信协议' }]}
+                >
+                  <Select placeholder="请选择通信协议">
+                    {Object.entries(DeviceProtocolType).map(([key, value]) => (
+                      <Select.Option key={key} value={value}>
+                        {value} - {key === value ? '' : key}
+                      </Select.Option>
+                    ))}
+                  </Select>
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="address"
+                  label="通信地址"
+                  rules={[{ required: true, message: '请输入通信地址' }]}
+                >
+                  <Input placeholder="请输入通信地址" />
+                </Form.Item>
+              </Col>
+              
+              <Col span={12}>
+                <Form.Item
+                  name="collect_interval"
+                  label="采集间隔(秒)"
+                >
+                  <Input type="number" placeholder="请输入采集间隔" />
+                </Form.Item>
+              </Col>
+            </Row>
+
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="status_address"
+                  label="状态地址"
+                >
+                  <Input placeholder="请输入状态地址" />
+                </Form.Item>
+              </Col>
+              
+              <Col span={12}>
+                <Form.Item
+                  name="connection_config"
+                  label="连接配置"
+                >
+                  <Input.TextArea rows={1} placeholder="请输入连接配置" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Form.Item
+              name="remark"
+              label="备注"
+            >
+              <Input.TextArea rows={4} placeholder="请输入备注信息" />
+            </Form.Item>
+            
+            <Form.Item
+              name="is_enabled"
+              label="状态"
+              initialValue={EnableStatus.ENABLED}
+            >
+              <Select>
+                <Select.Option value={EnableStatus.ENABLED}>启用</Select.Option>
+                <Select.Option value={EnableStatus.DISABLED}>禁用</Select.Option>
+              </Select>
+            </Form.Item>
+            
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit" loading={isSubmitting}>
+                  {formMode === 'create' ? '创建' : '保存'}
+                </Button>
+                <Button onClick={() => setModalVisible(false)}>取消</Button>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </Card>
+    </div>
+  );
+};

+ 597 - 0
client/admin/pages_device_map.tsx

@@ -0,0 +1,597 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Input, message, 
+  Card, Spin, Badge, Descriptions,
+  Tag, Radio, Tabs, List, Alert, Empty, Drawer,
+  Tree
+} from 'antd';
+import {
+  MenuFoldOutlined,
+  MenuUnfoldOutlined,
+  AppstoreOutlined,
+  EnvironmentOutlined,
+  SearchOutlined
+} from '@ant-design/icons';   
+import { 
+  useQuery,
+  useQueryClient
+} from '@tanstack/react-query';
+import 'dayjs/locale/zh-cn';
+import AMap from './components_amap.tsx'; // 导入地图组件
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+   MapViewDevice, DeviceMapFilter, DeviceMapStats,
+  DeviceTreeNode, DeviceTreeStats
+} from '../share/monitorTypes.ts';
+
+import {
+  DeviceStatus,  DeviceTreeNodeType
+} from '../share/monitorTypes.ts';
+
+
+import { MonitorAPI} from './api/index.ts';
+import { MapMode } from "../share/types.ts";
+
+
+// 设备树组件接口定义
+interface DeviceTreeProps {
+  onSelect: (node: {
+    key: string;
+    type: DeviceTreeNodeType;
+    data?: {
+      total: number;
+      online: number;
+      offline: number;
+      error: number;
+    }
+  }) => void;
+  selectedKey: string | null;
+  statusFilter: string;
+  onStatusFilterChange: (status: string) => void;
+}
+
+// 设备树组件
+const DeviceTree = ({ onSelect, selectedKey, statusFilter, onStatusFilterChange }: DeviceTreeProps) => {
+  const [searchValue, setSearchValue] = useState('');
+
+  // 使用React Query获取设备树数据
+  const { data: treeData = [], isLoading: treeLoading } = useQuery<DeviceTreeNode[]>({
+    queryKey: ['deviceTree', statusFilter, searchValue],
+    queryFn: async () => {
+      try {
+        // 构建API参数
+        const params: {
+          status?: string,
+          keyword?: string
+        } = {};
+        
+        // 添加状态过滤
+        if (statusFilter !== 'all') {
+          params.status = statusFilter;
+        }
+        
+        // 添加关键词搜索
+        if (searchValue) {
+          params.keyword = searchValue;
+        }
+        
+        const response = await MonitorAPI.getDeviceTree(params);
+        return response.data || [];
+      } catch (error) {
+        console.error("获取设备树失败:", error);
+        message.error("获取设备树失败");
+        return [];
+      }
+    },
+    refetchInterval: 30000 // 30秒刷新一次
+  });
+
+  // 使用React Query获取设备统计数据
+  const { data: statistics = {} as DeviceTreeStats } = useQuery<DeviceTreeStats>({
+    queryKey: ['deviceTreeStats'],
+    queryFn: async () => {
+      try {
+        const response = await MonitorAPI.getDeviceTreeStats();
+        return response.data || {};
+      } catch (error) {
+        console.error("获取设备统计失败:", error);
+        message.error("获取设备统计失败");
+        return {};
+      }
+    },
+    refetchInterval: 30000 // 30秒刷新一次
+  });
+
+  // 直接使用从后端获取的已过滤树数据
+  const filteredTreeData = treeData;
+
+  // 处理树节点标题渲染
+  const renderTreeTitle = (node: DeviceTreeNode) => {
+    const stats = statistics[node.key] || {
+      total: 0,
+      online: 0,
+      offline: 0,
+      error: 0
+    };
+
+    return (
+      <div style={{ display: 'flex', justifyContent: 'space-between', width: '100%', paddingRight: 8 }}>
+        <span style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+          {node.icon && (
+            <img 
+              src={node.icon} 
+              alt={node.title} 
+              style={{ 
+                width: 16, 
+                height: 16,
+                filter: node.status === 'offline' ? 'grayscale(100%)' : 'none' 
+              }}
+            />
+          )}
+          <span style={{ color: node.status === 'offline' ? '#999' : 'inherit' }}>
+            {node.title}
+          </span>
+        </span>
+        {node.type === 'category' ? (
+          <div style={{ display: 'flex', gap: 8, fontSize: 12 }}>
+            {stats.error > 0 && (
+              <span style={{ color: '#f5222d' }}>{stats.error}</span>
+            )}
+            {stats.offline > 0 && (
+              <span style={{ color: '#999' }}>{stats.offline}</span>
+            )}
+            {stats.online > 0 && (
+              <span style={{ color: '#52c41a' }}>{stats.online}</span>
+            )}
+            <span style={{ color: '#ccc' }}>/{stats.total}</span>
+          </div>
+        ) : (
+          <div style={{ display: 'flex', gap: 8, fontSize: 12 }}>
+            {node.status === 'error' && (
+              <span style={{ color: '#f5222d' }}>异常</span>
+            )}
+            {node.status === 'warning' && (
+              <span style={{ color: '#faad14' }}>警告</span>
+            )}
+            {node.status === 'offline' && (
+              <span style={{ color: '#999' }}>离线</span>
+            )}
+            {node.status === 'normal' && (
+              <span style={{ color: '#52c41a' }}>正常</span>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  };
+
+  return (
+    <Card 
+      style={{ height: '100%', display: 'flex', flexDirection: 'column' }} 
+      variant="borderless"
+      styles={{body:{ height: '100%', display: 'flex', flexDirection: 'column' }}}
+    >
+      <div style={{ marginBottom: 16 }}>
+        <Radio.Group 
+          value={statusFilter} 
+          onChange={e => onStatusFilterChange(e.target.value)}
+          style={{ width: '100%', display: 'flex', marginBottom: 12 }}
+          size="small"
+        >
+          <Radio.Button value="error" style={{ flex: 1, textAlign: 'center' }}>
+            异常
+          </Radio.Button>
+          <Radio.Button value="normal" style={{ flex: 1, textAlign: 'center' }}>
+            正常
+          </Radio.Button>
+          <Radio.Button value="all" style={{ flex: 1, textAlign: 'center' }}>
+            全部
+          </Radio.Button>
+        </Radio.Group>
+        <Input.Search
+          placeholder="搜索设备分类或设备"
+          allowClear
+          onChange={e => setSearchValue(e.target.value)}
+          prefix={<SearchOutlined />}
+        />
+      </div>
+      <div style={{ flex: 1, overflow: 'auto' }}>
+        {treeLoading ? (
+          <div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
+            <Spin />
+          </div>
+        ) : (
+          <Tree
+            treeData={filteredTreeData}
+            titleRender={renderTreeTitle}
+            selectedKeys={selectedKey ? [selectedKey] : []}
+            onSelect={(selectedKeys, info: any) => {
+              if (selectedKeys.length > 0) {
+                const node = info.node;
+                onSelect({
+                  key: String(selectedKeys[0]),
+                  type: node.type,
+                  data: node.type === 'category' ? statistics[node.key] : {
+                    total: 1,
+                    online: node.status === 'normal' ? 1 : 0,
+                    offline: node.status === 'offline' ? 1 : 0,
+                    error: node.status === 'error' ? 1 : 0
+                  }
+                });
+              }
+            }}
+            defaultExpandAll
+          />
+        )}
+      </div>
+    </Card>
+  );
+};
+
+// 设备地图管理页面
+export const DeviceMapManagePage = () => {
+  // 状态管理
+  const [viewMode, setViewMode] = useState<'list' | 'map'>('map');
+  const [selectedDevice, setSelectedDevice] = useState<MapViewDevice | null>(null);
+  const [detailVisible, setDetailVisible] = useState(false);
+  const [activeDetailTab, setActiveDetailTab] = useState('monitor');
+  const [searchKeyword, setSearchKeyword] = useState('');
+  const [selectedKey, setSelectedKey] = useState<string | null>(null);
+  const [selectedNodeType, setSelectedNodeType] = useState<DeviceTreeNodeType | null>(null);
+  const [collapsed, setCollapsed] = useState(false);
+  const [statusFilter, setStatusFilter] = useState('error');
+  const [mapMode] = useState<MapMode>(MapMode.ONLINE);
+  const [mapCenter, setMapCenter] = useState<[number, number] | undefined>(undefined);
+  const [mapZoom, setMapZoom] = useState<number>(15);
+  const [loading, setLoading] = useState(false);
+  const [devices, setDevices] = useState<MapViewDevice[]>([]);
+  const [stats, setStats] = useState<DeviceMapStats>({ total: 0, online: 0, offline: 0, error: 0 });
+
+  // 使用React Query加载设备数据
+  const queryClient = useQueryClient();
+  
+  // 构建查询参数
+  const buildDeviceParams = (): DeviceMapFilter => {
+    let params: DeviceMapFilter = {};
+    
+    // 根据节点类型设置不同的查询条件
+    if (selectedNodeType === DeviceTreeNodeType.CATEGORY && selectedKey) {
+      params.type_code = selectedKey;
+    } else if (selectedNodeType === DeviceTreeNodeType.DEVICE && selectedKey) {
+      const deviceId = selectedKey.replace('device-', '');
+      params.device_id = parseInt(deviceId);
+    }
+    
+    // 状态筛选
+    if (statusFilter !== 'all') {
+      params.device_status = statusFilter === 'error' ? DeviceStatus.FAULT : DeviceStatus.NORMAL;
+    }
+    
+    // 关键字搜索
+    if (searchKeyword) {
+      params.keyword = searchKeyword;
+    }
+    
+    return params;
+  };
+
+  // 设备数据查询
+  const { data: deviceData, isLoading } = useQuery<{
+    data: MapViewDevice[];
+    stats: DeviceMapStats;
+  }>({
+    queryKey: ['deviceMapData', searchKeyword, selectedKey, selectedNodeType, statusFilter],
+    queryFn: async () => {
+      const params = buildDeviceParams();
+      return await MonitorAPI.getDeviceMapData(params);
+    },
+    refetchInterval: 30000 // 30秒自动刷新
+  });
+
+  // 处理请求成功和失败的副作用
+  useEffect(() => {
+    if (deviceData?.data) {
+      // 如果是设备节点点击,处理设备数据
+      if (selectedNodeType === DeviceTreeNodeType.DEVICE && selectedKey) {
+        const deviceId = selectedKey.replace('device-', '');
+        const device = deviceData.data.find((d: MapViewDevice) => d.id === parseInt(deviceId));
+        if (device) {
+          handleDeviceData(device);
+        }
+      }
+    }
+  }, [deviceData, selectedNodeType, selectedKey]);
+
+  // 更新本地状态
+  useEffect(() => {
+    if (deviceData) {
+      setDevices(deviceData.data || []);
+      setStats(deviceData.stats || { total: 0, online: 0, offline: 0, error: 0 });
+    }
+  }, [deviceData]);
+
+  // 手动刷新数据函数
+  const handleRefresh = () => {
+    queryClient.invalidateQueries({ queryKey: ['deviceMapData'] });
+  };
+
+  const handleDeviceData = (device: MapViewDevice) => {
+    // 如果是地图视图,定位到设备位置
+    if (viewMode === 'map' && device.longitude && device.latitude) {
+      setMapCenter([device.longitude, device.latitude]);
+      setMapZoom(17);
+    } else if (viewMode === 'map' && (!device.longitude || !device.latitude)) {
+      setSelectedDevice(device);
+      setDetailVisible(true);
+      setActiveDetailTab('location');
+    }
+  };
+
+  const handleDeviceSelect = (device: MapViewDevice) => {
+    setSelectedDevice(device);
+    setDetailVisible(true);
+    if (viewMode === 'map' && device.longitude && device.latitude) {
+      setMapCenter([device.longitude, device.latitude]);
+      setMapZoom(17);
+    }
+  };
+
+  const handleTreeSelect = (node: { key: string; type: DeviceTreeNodeType; data?: any }) => {
+    setSelectedKey(node.key);
+    setSelectedNodeType(node.type);
+    
+    // 如果是列表视图切换到该分类下
+    if (viewMode === 'list') {
+      // 可以添加额外处理...
+    }
+    
+    // 如果是地图视图,更新地图显示
+    if (viewMode === 'map') {
+      // 可以添加额外处理...
+    }
+  };
+
+  const handleViewModeChange = (mode: 'list' | 'map') => {
+    setViewMode(mode);
+  };
+
+  return (
+    <div style={{ height: 'calc(100vh - 170px)', display: 'flex', flexDirection: 'column' }}>
+      <div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
+        {/* 设备树侧栏 */}
+        <div style={{ 
+          width: collapsed ? 50 : 300, 
+          transition: 'width 0.3s',
+          borderRight: '1px solid #f0f0f0',
+          position: 'relative',
+          height: '100%'
+        }}>
+          {!collapsed ? (
+            <DeviceTree 
+              onSelect={handleTreeSelect}
+              selectedKey={selectedKey}
+              statusFilter={statusFilter}
+              onStatusFilterChange={setStatusFilter}
+            />
+          ) : null}
+          <Button
+            type="text"
+            icon={collapsed ? <MenuUnfoldOutlined /> : <MenuFoldOutlined />}
+            onClick={() => setCollapsed(!collapsed)}
+            style={{
+              position: 'absolute',
+              right: collapsed ? '50%' : -15,
+              top: 20,
+              transform: collapsed ? 'translateX(50%)' : 'none',
+              zIndex: 2,
+              backgroundColor: '#fff',
+              border: '1px solid #f0f0f0',
+              boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
+              borderRadius: '50%',
+              width: 30,
+              height: 30,
+              display: 'flex',
+              justifyContent: 'center',
+              alignItems: 'center',
+              padding: 0
+            }}
+          />
+        </div>
+        
+        {/* 主内容区域 */}
+        <div style={{ flex: 1, overflow: 'hidden', padding: '0 16px' }}>
+          {/* 查看模式切换和刷新按钮 */}
+          <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
+            <Button 
+              onClick={handleRefresh}
+              style={{ marginRight: 8 }}
+              icon={<span className="icon-refresh"></span>}
+            >
+              刷新
+            </Button>
+            <Button
+              onClick={() => handleRefresh()}
+              style={{ marginRight: 8 }}
+            >
+              自动刷新
+            </Button>
+            <Radio.Group 
+              value={viewMode} 
+              onChange={e => handleViewModeChange(e.target.value)}
+              buttonStyle="solid"
+            >
+              <Radio.Button value="list">
+                <AppstoreOutlined /> 列表视图
+              </Radio.Button>
+              <Radio.Button value="map">
+                <EnvironmentOutlined /> 地图视图
+              </Radio.Button>
+            </Radio.Group>
+          </div>
+          
+          {/* 内容区 */}
+          <div style={{ height: 'calc(100% - 50px)', overflow: 'hidden' }}>
+            {viewMode === 'list' ? (
+              <Card style={{ height: '100%', overflow: 'auto' }}>
+                <List
+                  loading={loading}
+                  grid={{
+                    gutter: 16,
+                    xs: 1,
+                    sm: 2,
+                    md: 3,
+                    lg: 3,
+                    xl: 4,
+                    xxl: 6,
+                  }}
+                  dataSource={devices}
+                  renderItem={(device: MapViewDevice) => (
+                    <List.Item>
+                      <Card 
+                        hoverable
+                        onClick={() => handleDeviceSelect(device)}
+                        cover={device.image_url ? (
+                          <div style={{ padding: '12px', textAlign: 'center' }}>
+                            <img 
+                              src={device.image_url} 
+                              alt={device.name}
+                              style={{ 
+                                height: 80, 
+                                objectFit: 'contain',
+                                filter: device.isOnline === '0' ? 'grayscale(100%)' : 'none'
+                              }}
+                            />
+                          </div>
+                        ) : null}
+                      >
+                        <Card.Meta
+                          title={
+                            <div style={{ display: 'flex', justifyContent: 'space-between' }}>
+                              <span>{device.name}</span>
+                              {device.device_status === DeviceStatus.NORMAL ? (
+                                <Badge status="success" text="正常" />
+                              ) : device.device_status === DeviceStatus.FAULT ? (
+                                <Badge status="error" text="异常" />
+                              ) : device.device_status === DeviceStatus.OFFLINE ? (
+                                <Badge status="default" text="离线" />
+                              ) : (
+                                <Badge status="processing" text="维护" />
+                              )}
+                            </div>
+                          }
+                          description={
+                            <div>
+                              {device.longitude && device.latitude ? (
+                                <Tag color="blue">有位置信息</Tag>
+                              ) : (
+                                <Tag color="orange">无位置信息</Tag>
+                              )}
+                            </div>
+                          }
+                        />
+                      </Card>
+                    </List.Item>
+                  )}
+                />
+              </Card>
+            ) : (
+              <Card style={{ height: '100%' }} styles={{body:{ height: '100%' }}}>
+                <AMap
+                  height="100%"
+                  width="100%"
+                  markers={devices}
+                  center={mapCenter}
+                  zoom={mapZoom}
+                  onMarkerClick={(markerData) => {
+                    // 确保markerData包含设备所需的所有属性
+                    const device = devices.find(d => 
+                      d.longitude === markerData.longitude && 
+                      d.latitude === markerData.latitude
+                    );
+                    if (device) {
+                      handleDeviceSelect(device);
+                    }
+                  }}
+                  mode={mapMode}
+                />
+              </Card>
+            )}
+          </div>
+        </div>
+      </div>
+      
+      {/* 设备详情抽屉 */}
+      <Drawer
+        title="设备详情"
+        placement="right"
+        onClose={() => setDetailVisible(false)}
+        open={detailVisible}
+        width={600}
+      >
+        {selectedDevice ? (
+          <div>
+            <Descriptions bordered column={2} style={{ marginBottom: 16 }}>
+              <Descriptions.Item label="设备名称" span={2}>{selectedDevice.name}</Descriptions.Item>
+              <Descriptions.Item label="设备ID">{selectedDevice.id}</Descriptions.Item>
+              <Descriptions.Item label="设备状态">
+                {selectedDevice.device_status === DeviceStatus.NORMAL ? (
+                  <Badge status="success" text="正常" />
+                ) : selectedDevice.device_status === DeviceStatus.FAULT ? (
+                  <Badge status="error" text="异常" />
+                ) : selectedDevice.device_status === DeviceStatus.OFFLINE ? (
+                  <Badge status="default" text="离线" />
+                ) : (
+                  <Badge status="processing" text="维护" />
+                )}
+              </Descriptions.Item>
+              <Descriptions.Item label="经度">{selectedDevice.longitude || '未设置'}</Descriptions.Item>
+              <Descriptions.Item label="纬度">{selectedDevice.latitude || '未设置'}</Descriptions.Item>
+              <Descriptions.Item label="描述" span={2}>{selectedDevice.description || '无描述'}</Descriptions.Item>
+            </Descriptions>
+            
+            <Tabs
+              activeKey={activeDetailTab}
+              onChange={setActiveDetailTab}
+              items={[
+                {
+                  key: 'monitor',
+                  label: '监控数据',
+                  children: <div style={{ minHeight: 300 }}>
+                    <Alert message="暂无实时监控数据" type="info" />
+                  </div>
+                },
+                {
+                  key: 'alert',
+                  label: '告警记录',
+                  children: <div style={{ minHeight: 300 }}>
+                    <Alert message="暂无告警记录" type="info" />
+                  </div>
+                },
+                {
+                  key: 'location',
+                  label: '位置信息',
+                  children: <div style={{ minHeight: 300 }}>
+                    {selectedDevice.longitude && selectedDevice.latitude ? (
+                      <AMap
+                        height="300px"
+                        width="100%"
+                        center={[selectedDevice.longitude, selectedDevice.latitude]}
+                        zoom={15}
+                        markers={[selectedDevice]}
+                        mode={mapMode}
+                      />
+                    ) : (
+                      <Empty
+                        description="未设置位置信息"
+                      />
+                    )}
+                  </div>
+                }
+              ]}
+            />
+          </div>
+        ) : null}
+      </Drawer>
+    </div>
+  );
+};

+ 261 - 0
client/admin/pages_device_monitor.tsx

@@ -0,0 +1,261 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Form, Input, Select, message, Card, Badge, 
+} from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+  DeviceMonitorData, 
+} from '../share/monitorTypes.ts';
+
+import {
+  DeviceStatus, DeviceProtocolType, MetricType, DeviceStatusNameMap, DeviceProtocolTypeNameMap,
+  MetricTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+import { DeviceInstanceAPI, MonitorAPI } from './api/index.ts';
+
+
+// 设备实时监控页面
+export const DeviceMonitorPage = () => {
+  const [loading, setLoading] = useState(false);
+  const [monitorData, setMonitorData] = useState<DeviceMonitorData[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [deviceOptions, setDeviceOptions] = useState<{label: string, value: number}[]>([]);
+  const [formRef] = Form.useForm();
+  
+  // 监控数据刷新间隔(毫秒)
+  const REFRESH_INTERVAL = 30000;
+
+  useEffect(() => {
+    fetchDeviceOptions();
+    fetchMonitorData();
+    
+    // 设置定时刷新
+    const intervalId = setInterval(() => {
+      fetchMonitorData();
+    }, REFRESH_INTERVAL);
+    
+    return () => clearInterval(intervalId);
+  }, [pagination.current, pagination.pageSize]);
+
+  const fetchDeviceOptions = async () => {
+    try {
+      const response = await DeviceInstanceAPI.getDeviceInstances();
+      if (response && response.data) {
+        const options = response.data.map((device) => ({
+          label: device.asset_name || `设备${device.id}`,
+          value: device.id
+        }));
+        setDeviceOptions(options);
+      }
+    } catch (error) {
+      console.error('获取设备列表失败:', error);
+      message.error('获取设备列表失败');
+    }
+  };
+
+  const fetchMonitorData = async () => {
+    setLoading(true);
+    try {
+      const values = formRef.getFieldsValue();
+      const params = {
+        page: pagination.current,
+        pageSize: pagination.pageSize,
+        device_id: values.device_id,
+        device_name: values.device_name,
+        protocol: values.protocol,
+        address: values.address,
+        metric_type: values.metric_type,
+        status: values.status,
+      };
+      
+      const response = await MonitorAPI.getMonitorData(params);
+      
+      if (response) {
+        setMonitorData(response.data || []);
+        setPagination({
+          ...pagination,
+          total: response.total || 0,
+        });
+      }
+    } catch (error) {
+      console.error('获取监控数据失败:', error);
+      message.error('获取监控数据失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = (values: any) => {
+    setPagination({
+      ...pagination,
+      current: 1,
+    });
+    fetchMonitorData();
+  };
+
+  const handleTableChange = (newPagination: any) => {
+    setPagination({
+      ...pagination,
+      current: newPagination.current,
+      pageSize: newPagination.pageSize,
+    });
+  };
+
+  const metricTypeOptions = getEnumOptions(MetricType, MetricTypeNameMap);
+  const statusOptions = getEnumOptions(DeviceStatus, DeviceStatusNameMap);
+  const protocolOptions = getEnumOptions(DeviceProtocolType, DeviceProtocolTypeNameMap);
+
+  const getStatusBadge = (status?: DeviceStatus) => {
+    switch (status) {
+      case DeviceStatus.NORMAL:
+        return <Badge status="success" text="正常" />;
+      case DeviceStatus.MAINTAIN:
+        return <Badge status="processing" text="维护中" />;
+      case DeviceStatus.FAULT:
+        return <Badge status="error" text="故障" />;
+      case DeviceStatus.OFFLINE:
+        return <Badge status="default" text="下线" />;
+      default:
+        return <Badge status="default" text="未知" />;
+    }
+  };
+
+  const columns = [
+    {
+      title: '设备ID',
+      dataIndex: 'device_id',
+      key: 'device_id',
+      width: 80,
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'device_name',
+      key: 'device_name',
+    },
+    {
+      title: '通信协议',
+      dataIndex: 'protocol',
+      key: 'protocol',
+      render: (text: string) => {
+        const option = protocolOptions.find(opt => opt.value === text);
+        return option ? option.label : text;
+      },
+    },
+    {
+      title: '通信地址',
+      dataIndex: 'address',
+      key: 'address',
+    },
+    {
+      title: '监控指标',
+      dataIndex: 'metric_type',
+      key: 'metric_type',
+      render: (text: string) => {
+        const option = metricTypeOptions.find(opt => opt.value === text);
+        return option ? option.label : text;
+      },
+    },
+    {
+      title: '监控值',
+      dataIndex: 'metric_value',
+      key: 'metric_value',
+      render: (value: number, record: DeviceMonitorData) => {
+        return `${value} ${record.unit || ''}`;
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: DeviceStatus) => getStatusBadge(status),
+    },
+    {
+      title: '采集时间',
+      dataIndex: 'collect_time',
+      key: 'collect_time',
+      render: (text: Date) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+  ];
+
+  return (
+    <div>
+      <Card title="设备实时监控" style={{ marginBottom: 16 }}>
+        <Form
+          form={formRef}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: 16 }}
+        >
+          <Form.Item name="device_id" label="设备">
+            <Select
+              placeholder="选择设备"
+              style={{ width: 200 }}
+              allowClear
+              options={deviceOptions}
+            />
+          </Form.Item>
+          <Form.Item name="device_name" label="设备名称">
+            <Input placeholder="输入设备名称" style={{ width: 200 }} />
+          </Form.Item>
+          <Form.Item name="protocol" label="通信协议">
+            <Select
+              placeholder="选择通信协议"
+              style={{ width: 200 }}
+              allowClear
+              options={protocolOptions}
+            />
+          </Form.Item>
+          <Form.Item name="address" label="通信地址">
+            <Input placeholder="输入通信地址" style={{ width: 200 }} />
+          </Form.Item>
+          <Form.Item name="metric_type" label="监控指标">
+            <Select
+              placeholder="选择监控指标"
+              style={{ width: 200 }}
+              allowClear
+              options={metricTypeOptions}
+            />
+          </Form.Item>
+          <Form.Item name="status" label="状态">
+            <Select
+              placeholder="选择状态"
+              style={{ width: 120 }}
+              allowClear
+              options={statusOptions}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              查询
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={monitorData}
+          rowKey="id"
+          pagination={pagination}
+          loading={loading}
+          onChange={handleTableChange}
+        />
+      </Card>
+    </div>
+  );
+};
+
+
+
+
+
+
+

+ 383 - 0
client/admin/pages_device_types.tsx

@@ -0,0 +1,383 @@
+import React, { useState } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Image, Tag, 
+} from 'antd';
+
+import { 
+  useQuery,
+  useMutation,
+  useQueryClient
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  DeviceType,
+} from '../share/monitorTypes.ts';
+
+import {
+  EnableStatus, DeleteStatus,
+  EnableStatusNameMap, 
+} from '../share/types.ts';
+
+import { DeviceTypeAPI, getOssUrl } from './api/index.ts';
+import { Uploader } from "./components_uploader.tsx";
+
+
+// 设备类型管理页
+export const DeviceTypesPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [modalVisible, setModalVisible] = useState(false);
+  const [selectedId, setSelectedId] = useState<number | null>(null);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0
+  });
+  const [uploadVisible, setUploadVisible] = useState(false);
+  const [previewVisible, setPreviewVisible] = useState(false);
+  const [previewImage, setPreviewImage] = useState('');
+  
+  // 获取设备类型列表
+  const {
+    data,
+    isLoading,
+    refetch
+  } = useQuery({
+    queryKey: ['deviceTypes', pagination.current, pagination.pageSize],
+    queryFn: async () => {
+      try {
+        const response = await DeviceTypeAPI.getDeviceTypes({
+          page: pagination.current,
+          pageSize: pagination.pageSize
+        });
+        
+        setPagination(prev => ({
+          ...prev,
+          total: response.pagination.total
+        }));
+        
+        return response.data;
+      } catch (error) {
+        message.error('获取设备类型列表失败');
+        return [];
+      }
+    }
+  });
+  
+  const queryClient = useQueryClient();
+  
+  // 提交表单
+  const { mutate: submitForm, isPending: isSubmitting } = useMutation({
+    mutationFn: async (values: Partial<DeviceType>) => {
+      if (formMode === 'create') {
+        return DeviceTypeAPI.createDeviceType(values);
+      } else {
+        return DeviceTypeAPI.updateDeviceType(selectedId!, values);
+      }
+    },
+    onSuccess: () => {
+      message.success(formMode === 'create' ? '创建成功' : '更新成功');
+      setModalVisible(false);
+      refetch();
+      form.resetFields();
+      
+      // 刷新设备类型图标缓存
+      queryClient.invalidateQueries({ queryKey: ['deviceTypeIcons'] });
+    },
+    onError: () => {
+      message.error(formMode === 'create' ? '创建失败' : '更新失败');
+    }
+  });
+  
+  // 删除设备类型
+  const { mutate: deleteDeviceType, isPending: isDeleting } = useMutation({
+    mutationFn: async (id: number) => {
+      return DeviceTypeAPI.deleteDeviceType(id);
+    },
+    onSuccess: () => {
+      message.success('删除成功');
+      refetch();
+      
+      // 刷新设备类型图标缓存
+      queryClient.invalidateQueries({ queryKey: ['deviceTypeIcons'] });
+    },
+    onError: () => {
+      message.error('删除失败');
+    }
+  });
+  
+  // 处理文件上传成功
+  const handleUploadSuccess = (fileUrl: string) => {
+    form.setFieldsValue({ image_url: fileUrl });
+    setUploadVisible(false);
+  };
+  
+  // 处理表单提交
+  const handleSubmit = (values: Partial<DeviceType>) => {
+    submitForm({
+      ...values,
+      is_enabled: values.is_enabled ?? EnableStatus.ENABLED,
+      is_deleted: DeleteStatus.NOT_DELETED
+    });
+  };
+  
+  // 处理编辑
+  const handleEdit = (record: DeviceType) => {
+    setSelectedId(record.id);
+    setFormMode('edit');
+    form.setFieldsValue({
+      ...record
+    });
+    setModalVisible(true);
+  };
+  
+  // 处理删除
+  const handleDelete = (id: number) => {
+    Modal.confirm({
+      title: '确认删除',
+      content: '确定要删除这个设备类型吗?',
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => deleteDeviceType(id)
+    });
+  };
+  
+  // 处理添加
+  const handleAdd = () => {
+    setFormMode('create');
+    setSelectedId(null);
+    form.resetFields();
+    form.setFieldsValue({
+      is_enabled: EnableStatus.ENABLED
+    });
+    setModalVisible(true);
+  };
+  
+  // 处理图片预览
+  const handlePreview = (imageUrl: string) => {
+    setPreviewImage(imageUrl);
+    setPreviewVisible(true);
+  };
+  
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80
+    },
+    {
+      title: '类型名称',
+      dataIndex: 'name',
+      key: 'name'
+    },
+    {
+      title: '类型编码',
+      dataIndex: 'code',
+      key: 'code'
+    },
+    {
+      title: '图标',
+      dataIndex: 'image_url',
+      key: 'image_url',
+      render: (imageUrl: string) => {
+        return imageUrl ? (
+          <Image 
+            src={getOssUrl(imageUrl)} 
+            alt="设备类型图标" 
+            width={40} 
+            height={40} 
+            preview={false}
+            style={{ cursor: 'pointer' }}
+            onClick={() => handlePreview(imageUrl)}
+          />
+        ) : (
+          <span>-</span>
+        );
+      }
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description',
+      ellipsis: true
+    },
+    {
+      title: '状态',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (isEnabled: EnableStatus) => (
+        <Tag color={isEnabled === EnableStatus.ENABLED ? 'green' : 'red'}>
+          {EnableStatusNameMap[isEnabled] || '未知'}
+        </Tag>
+      )
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (time: string) => dayjs(time).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: DeviceType) => (
+        <Space size="small">
+          <Button type="link" size="small" onClick={() => handleEdit(record)}>
+            编辑
+          </Button>
+          <Button 
+            type="link" 
+            size="small" 
+            danger
+            onClick={() => handleDelete(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      )
+    }
+  ];
+  
+  return (
+    <div>
+      <Card
+        title="设备类型管理"
+        extra={
+          <Button type="primary" onClick={handleAdd}>
+            添加设备类型
+          </Button>
+        }
+      >
+        <Table
+          rowKey="id"
+          columns={columns}
+          dataSource={data}
+          loading={isLoading}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: (page, pageSize) => {
+              setPagination({
+                ...pagination,
+                current: page,
+                pageSize: pageSize || 10
+              });
+            }
+          }}
+        />
+        
+        <Modal
+          title={formMode === 'create' ? '添加设备类型' : '编辑设备类型'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          footer={null}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+          >
+            <Form.Item
+              name="name"
+              label="类型名称"
+              rules={[{ required: true, message: '请输入类型名称' }]}
+            >
+              <Input placeholder="请输入类型名称" />
+            </Form.Item>
+            
+            <Form.Item
+              name="code"
+              label="类型编码"
+              rules={[{ required: true, message: '请输入类型编码' }]}
+            >
+              <Input placeholder="请输入类型编码" />
+            </Form.Item>
+            
+            <Form.Item
+              name="image_url"
+              label="图标"
+            >
+              <Input placeholder="请输入图片URL" readOnly />
+            </Form.Item>
+
+            {form.getFieldValue('image_url') && (
+              <Form.Item label=" " colon={false}>
+                <Image 
+                  src={getOssUrl(form.getFieldValue('image_url'))} 
+                  alt="类型图标" 
+                  width={80} 
+                  height={80} 
+                  style={{ marginBottom: 16 }}
+                />
+              </Form.Item>
+            )}
+            
+            <Form.Item label=" " colon={false}>
+              <Button type="primary" onClick={() => setUploadVisible(true)}>
+                上传图标
+              </Button>
+            </Form.Item>
+            
+            <Form.Item
+              name="description"
+              label="描述"
+            >
+              <Input.TextArea rows={4} placeholder="请输入描述信息" />
+            </Form.Item>
+            
+            <Form.Item
+              name="is_enabled"
+              label="状态"
+              initialValue={EnableStatus.ENABLED}
+            >
+              <Select>
+                <Select.Option value={EnableStatus.ENABLED}>启用</Select.Option>
+                <Select.Option value={EnableStatus.DISABLED}>禁用</Select.Option>
+              </Select>
+            </Form.Item>
+            
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit" loading={isSubmitting}>
+                  {formMode === 'create' ? '创建' : '保存'}
+                </Button>
+                <Button onClick={() => setModalVisible(false)}>取消</Button>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Modal>
+        
+        {/* 图片上传模态框 */}
+        <Modal
+          title="上传图标"
+          open={uploadVisible}
+          onCancel={() => setUploadVisible(false)}
+          footer={null}
+          styles={{ body: { height: '500px' } }}
+        >
+          <Uploader
+            onSuccess={handleUploadSuccess}
+            onError={() => setUploadVisible(false)}
+            maxSize={2 * 1024 * 1024}
+            prefix="device-types/"
+            allowedTypes={['image/jpeg', 'image/png', 'image/svg+xml']}
+          />
+        </Modal>
+        
+        {/* 图片预览模态框 */}
+        <Modal
+          open={previewVisible}
+          footer={null}
+          onCancel={() => setPreviewVisible(false)}
+        >
+          <img alt="预览图片" style={{ width: '100%' }} src={previewImage} />
+        </Modal>
+      </Card>
+    </div>
+  );
+};

+ 673 - 0
client/admin/pages_file_library.tsx

@@ -0,0 +1,673 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Space, Form, Input, Select, 
+  message, Modal, Card, Typography, Tag, Popconfirm,
+  Tabs, Image, Upload, Descriptions
+} from 'antd';
+import {
+  UploadOutlined,
+  FileImageOutlined,
+  FileExcelOutlined,
+  FileWordOutlined,
+  FilePdfOutlined,
+  FileOutlined,
+} from '@ant-design/icons';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { uploadMinIOWithPolicy, uploadOSSWithPolicy } from '@d8d-appcontainer/api';
+import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
+import { FileAPI } from './api/index.ts';
+import type { FileLibrary, FileCategory } from '../share/types.ts';
+import { OssType } from '../share/types.ts';
+
+const { Title } = Typography;
+
+// 文件库管理页面
+export const FileLibraryPage = () => {
+  const [loading, setLoading] = useState(false);
+  const [fileList, setFileList] = useState<FileLibrary[]>([]);
+  const [categories, setCategories] = useState<FileCategory[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0
+  });
+  const [searchParams, setSearchParams] = useState({
+    fileType: '',
+    keyword: ''
+  });
+  const [uploadModalVisible, setUploadModalVisible] = useState(false);
+  const [fileDetailModalVisible, setFileDetailModalVisible] = useState(false);
+  const [currentFile, setCurrentFile] = useState<FileLibrary | null>(null);
+  const [uploadLoading, setUploadLoading] = useState(false);
+  const [form] = Form.useForm();
+  const [categoryForm] = Form.useForm();
+  const [categoryModalVisible, setCategoryModalVisible] = useState(false);
+  const [currentCategory, setCurrentCategory] = useState<FileCategory | null>(null);
+
+  // 获取文件图标
+  const getFileIcon = (fileType: string) => {
+    if (fileType.includes('image')) {
+      return <FileImageOutlined style={{ fontSize: '24px', color: '#1890ff' }} />;
+    } else if (fileType.includes('pdf')) {
+      return <FilePdfOutlined style={{ fontSize: '24px', color: '#ff4d4f' }} />;
+    } else if (fileType.includes('excel') || fileType.includes('sheet')) {
+      return <FileExcelOutlined style={{ fontSize: '24px', color: '#52c41a' }} />;
+    } else if (fileType.includes('word') || fileType.includes('document')) {
+      return <FileWordOutlined style={{ fontSize: '24px', color: '#2f54eb' }} />;
+    } else {
+      return <FileOutlined style={{ fontSize: '24px', color: '#faad14' }} />;
+    }
+  };
+
+  // 加载文件列表
+  const fetchFileList = async () => {
+    setLoading(true);
+    try {
+      const response = await FileAPI.getFileList({
+        page: pagination.current,
+        pageSize: pagination.pageSize,
+        ...searchParams
+      });
+      
+      if (response && response.data) {
+        setFileList(response.data.list);
+        setPagination({
+          ...pagination,
+          total: response.data.pagination.total
+        });
+      }
+    } catch (error) {
+      console.error('获取文件列表失败:', error);
+      message.error('获取文件列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 加载文件分类
+  const fetchCategories = async () => {
+    try {
+      const response = await FileAPI.getCategories();
+      if (response && response.data) {
+        setCategories(response.data);
+      }
+    } catch (error) {
+      console.error('获取文件分类失败:', error);
+      message.error('获取文件分类失败');
+    }
+  };
+
+  // 组件挂载时加载数据
+  useEffect(() => {
+    fetchFileList();
+    fetchCategories();
+  }, [pagination.current, pagination.pageSize, searchParams]);
+
+  // 上传文件
+  const handleUpload = async (file: File) => {
+    try {
+      setUploadLoading(true);
+      
+      // 1. 获取上传策略
+      const policyResponse = await FileAPI.getUploadPolicy(file.name);
+      if (!policyResponse || !policyResponse.data) {
+        throw new Error('获取上传策略失败');
+      }
+      
+      const policy = policyResponse.data;
+      
+      // 2. 上传文件至 MinIO
+      const uploadProgress = {
+        progress: 0,
+        completed: false,
+        error: null as Error | null
+      };
+      
+      const callbacks = {
+        onProgress: (event: { progress: number }) => {
+          uploadProgress.progress = event.progress;
+        },
+        onComplete: () => {
+          uploadProgress.completed = true;
+        },
+        onError: (err: Error) => {
+          uploadProgress.error = err;
+        }
+      };
+      
+      const uploadUrl = window.CONFIG?.OSS_TYPE === OssType.MINIO ? await uploadMinIOWithPolicy(
+        policy as MinioUploadPolicy,
+        file,
+        file.name,
+        callbacks
+      ) : await uploadOSSWithPolicy(
+        policy as OSSUploadPolicy,
+        file,
+        file.name,
+        callbacks
+      );
+      
+      if (!uploadUrl || uploadProgress.error) {
+        throw uploadProgress.error || new Error('上传文件失败');
+      }
+      
+      // 3. 保存文件信息到文件库
+      const fileValues = form.getFieldsValue();
+      const fileData = {
+        file_name: file.name,
+        file_path: uploadUrl,
+        file_type: file.type,
+        file_size: file.size,
+        category_id: fileValues.category_id ? Number(fileValues.category_id) : undefined,
+        tags: fileValues.tags,
+        description: fileValues.description
+      };
+      
+      const saveResponse = await FileAPI.saveFileInfo(fileData);
+      
+      if (saveResponse && saveResponse.data) {
+        message.success('文件上传成功');
+        setUploadModalVisible(false);
+        form.resetFields();
+        fetchFileList();
+      }
+    } catch (error) {
+      console.error('上传文件失败:', error);
+      message.error('上传文件失败: ' + (error instanceof Error ? error.message : '未知错误'));
+    } finally {
+      setUploadLoading(false);
+    }
+  };
+
+  // 处理文件上传
+  const uploadProps = {
+    name: 'file',
+    multiple: false,
+    showUploadList: false,
+    beforeUpload: (file: File) => {
+      const isLt10M = file.size / 1024 / 1024 < 10;
+      if (!isLt10M) {
+        message.error('文件大小不能超过10MB!');
+        return false;
+      }
+      handleUpload(file);
+      return false;
+    }
+  };
+
+  // 查看文件详情
+  const viewFileDetail = async (id: number) => {
+    try {
+      const response = await FileAPI.getFileInfo(id);
+      if (response && response.data) {
+        setCurrentFile(response.data);
+        setFileDetailModalVisible(true);
+      }
+    } catch (error) {
+      console.error('获取文件详情失败:', error);
+      message.error('获取文件详情失败');
+    }
+  };
+
+  // 下载文件
+  const downloadFile = async (file: FileLibrary) => {
+    try {
+      // 更新下载计数
+      await FileAPI.updateDownloadCount(file.id);
+      
+      // 创建一个暂时的a标签用于下载
+      const link = document.createElement('a');
+      link.href = file.file_path;
+      link.target = '_blank';
+      link.download = file.file_name;
+      document.body.appendChild(link);
+      link.click();
+      document.body.removeChild(link);
+      
+      message.success('下载已开始');
+    } catch (error) {
+      console.error('下载文件失败:', error);
+      message.error('下载文件失败');
+    }
+  };
+
+  // 删除文件
+  const handleDeleteFile = async (id: number) => {
+    try {
+      await FileAPI.deleteFile(id);
+      message.success('文件删除成功');
+      fetchFileList();
+    } catch (error) {
+      console.error('删除文件失败:', error);
+      message.error('删除文件失败');
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(values);
+    setPagination({
+      ...pagination,
+      current: 1
+    });
+  };
+
+  // 处理表格分页变化
+  const handleTableChange = (newPagination: any) => {
+    setPagination({
+      ...pagination,
+      current: newPagination.current,
+      pageSize: newPagination.pageSize
+    });
+  };
+
+  // 添加或更新分类
+  const handleCategorySave = async () => {
+    try {
+      const values = await categoryForm.validateFields();
+      
+      if (currentCategory) {
+        // 更新分类
+        await FileAPI.updateCategory(currentCategory.id, values);
+        message.success('分类更新成功');
+      } else {
+        // 创建分类
+        await FileAPI.createCategory(values);
+        message.success('分类创建成功');
+      }
+      
+      setCategoryModalVisible(false);
+      categoryForm.resetFields();
+      setCurrentCategory(null);
+      fetchCategories();
+    } catch (error) {
+      console.error('保存分类失败:', error);
+      message.error('保存分类失败');
+    }
+  };
+
+  // 编辑分类
+  const handleEditCategory = (category: FileCategory) => {
+    setCurrentCategory(category);
+    categoryForm.setFieldsValue(category);
+    setCategoryModalVisible(true);
+  };
+
+  // 删除分类
+  const handleDeleteCategory = async (id: number) => {
+    try {
+      await FileAPI.deleteCategory(id);
+      message.success('分类删除成功');
+      fetchCategories();
+    } catch (error) {
+      console.error('删除分类失败:', error);
+      message.error('删除分类失败');
+    }
+  };
+
+  // 文件表格列配置
+  const columns = [
+    {
+      title: '文件名',
+      key: 'file_name',
+      render: (text: string, record: FileLibrary) => (
+        <Space>
+          {getFileIcon(record.file_type)}
+          <a onClick={() => viewFileDetail(record.id)}>
+            {record.original_filename || record.file_name}
+          </a>
+        </Space>
+      )
+    },
+    {
+      title: '文件类型',
+      dataIndex: 'file_type',
+      key: 'file_type',
+      width: 120,
+      render: (text: string) => text.split('/').pop()
+    },
+    {
+      title: '大小',
+      dataIndex: 'file_size',
+      key: 'file_size',
+      width: 100,
+      render: (size: number) => {
+        if (size < 1024) {
+          return `${size} B`;
+        } else if (size < 1024 * 1024) {
+          return `${(size / 1024).toFixed(2)} KB`;
+        } else {
+          return `${(size / 1024 / 1024).toFixed(2)} MB`;
+        }
+      }
+    },
+    {
+      title: '分类',
+      dataIndex: 'category_id',
+      key: 'category_id',
+      width: 120
+    },
+    {
+      title: '上传者',
+      dataIndex: 'uploader_name',
+      key: 'uploader_name',
+      width: 120
+    },
+    {
+      title: '下载次数',
+      dataIndex: 'download_count',
+      key: 'download_count',
+      width: 120
+    },
+    {
+      title: '上传时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      width: 180,
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 180,
+      render: (_: any, record: FileLibrary) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => downloadFile(record)}>
+            下载
+          </Button>
+          <Popconfirm
+            title="确定要删除这个文件吗?"
+            onConfirm={() => handleDeleteFile(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      )
+    }
+  ];
+
+  // 分类表格列配置
+  const categoryColumns = [
+    {
+      title: '分类名称',
+      dataIndex: 'name',
+      key: 'name'
+    },
+    {
+      title: '分类编码',
+      dataIndex: 'code',
+      key: 'code'
+    },
+    {
+      title: '描述',
+      dataIndex: 'description',
+      key: 'description'
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: FileCategory) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => handleEditCategory(record)}>
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除这个分类吗?"
+            onConfirm={() => handleDeleteCategory(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      )
+    }
+  ];
+
+  return (
+    <div>
+      <Title level={2}>文件库管理</Title>
+      
+      <Card>
+        <Tabs defaultActiveKey="files">
+          <Tabs.TabPane tab="文件管理" key="files">
+            {/* 搜索表单 */}
+            <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+              <Form.Item name="keyword" label="关键词">
+                <Input placeholder="文件名/描述/标签" allowClear />
+              </Form.Item>
+              <Form.Item name="category_id" label="分类">
+                <Select placeholder="选择分类" allowClear style={{ width: 160 }}>
+                  {categories.map(category => (
+                    <Select.Option key={category.id} value={category.id}>
+                      {category.name}
+                    </Select.Option>
+                  ))}
+                </Select>
+              </Form.Item>
+              <Form.Item name="fileType" label="文件类型">
+                <Select placeholder="选择文件类型" allowClear style={{ width: 160 }}>
+                  <Select.Option value="image">图片</Select.Option>
+                  <Select.Option value="document">文档</Select.Option>
+                  <Select.Option value="application">应用</Select.Option>
+                  <Select.Option value="audio">音频</Select.Option>
+                  <Select.Option value="video">视频</Select.Option>
+                </Select>
+              </Form.Item>
+              <Form.Item>
+                <Button type="primary" htmlType="submit">
+                  搜索
+                </Button>
+              </Form.Item>
+              <Button 
+                type="primary" 
+                onClick={() => setUploadModalVisible(true)}
+                icon={<UploadOutlined />}
+                style={{ marginLeft: 16 }}
+              >
+                上传文件
+              </Button>
+            </Form>
+            
+            {/* 文件列表 */}
+            <Table
+              columns={columns}
+              dataSource={fileList}
+              rowKey="id"
+              loading={loading}
+              pagination={{
+                current: pagination.current,
+                pageSize: pagination.pageSize,
+                total: pagination.total,
+                showSizeChanger: true,
+                showQuickJumper: true,
+                showTotal: (total) => `共 ${total} 条记录`
+              }}
+              onChange={handleTableChange}
+            />
+          </Tabs.TabPane>
+          
+          <Tabs.TabPane tab="分类管理" key="categories">
+            <div style={{ marginBottom: 16 }}>
+              <Button 
+                type="primary" 
+                onClick={() => {
+                  setCurrentCategory(null);
+                  categoryForm.resetFields();
+                  setCategoryModalVisible(true);
+                }}
+              >
+                添加分类
+              </Button>
+            </div>
+            
+            <Table
+              columns={categoryColumns}
+              dataSource={categories}
+              rowKey="id"
+              pagination={{ pageSize: 10 }}
+            />
+          </Tabs.TabPane>
+        </Tabs>
+      </Card>
+      
+      {/* 上传文件弹窗 */}
+      <Modal
+        title="上传文件"
+        open={uploadModalVisible}
+        onCancel={() => setUploadModalVisible(false)}
+        footer={null}
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item
+            name="file"
+            label="文件"
+            rules={[{ required: true, message: '请选择要上传的文件' }]}
+          >
+            <Upload {...uploadProps}>
+              <Button icon={<UploadOutlined />} loading={uploadLoading}>
+                选择文件
+              </Button>
+              <div style={{ marginTop: 8 }}>
+                支持任意类型文件,单个文件不超过10MB
+              </div>
+            </Upload>
+          </Form.Item>
+          
+          <Form.Item
+            name="category_id"
+            label="分类"
+          >
+            <Select placeholder="选择分类" allowClear>
+              {categories.map(category => (
+                <Select.Option key={category.id} value={category.id}>
+                  {category.name}
+                </Select.Option>
+              ))}
+            </Select>
+          </Form.Item>
+          
+          <Form.Item
+            name="tags"
+            label="标签"
+          >
+            <Input placeholder="多个标签用逗号分隔" />
+          </Form.Item>
+          
+          <Form.Item
+            name="description"
+            label="描述"
+          >
+            <Input.TextArea rows={4} placeholder="文件描述..." />
+          </Form.Item>
+        </Form>
+      </Modal>
+      
+      {/* 文件详情弹窗 */}
+      <Modal
+        title="文件详情"
+        open={fileDetailModalVisible}
+        onCancel={() => setFileDetailModalVisible(false)}
+        footer={[
+          <Button key="close" onClick={() => setFileDetailModalVisible(false)}>
+            关闭
+          </Button>,
+          <Button 
+            key="download" 
+            type="primary" 
+            onClick={() => currentFile && downloadFile(currentFile)}
+          >
+            下载
+          </Button>
+        ]}
+        width={700}
+      >
+        {currentFile && (
+          <Descriptions bordered column={2}>
+            <Descriptions.Item label="系统文件名" span={2}>
+              {currentFile.file_name}
+            </Descriptions.Item>
+            {currentFile.original_filename && (
+              <Descriptions.Item label="原始文件名" span={2}>
+                {currentFile.original_filename}
+              </Descriptions.Item>
+            )}
+            <Descriptions.Item label="文件类型">
+              {currentFile.file_type}
+            </Descriptions.Item>
+            <Descriptions.Item label="文件大小">
+              {currentFile.file_size < 1024 * 1024 
+                ? `${(currentFile.file_size / 1024).toFixed(2)} KB` 
+                : `${(currentFile.file_size / 1024 / 1024).toFixed(2)} MB`}
+            </Descriptions.Item>
+            <Descriptions.Item label="上传者">
+              {currentFile.uploader_name}
+            </Descriptions.Item>
+            <Descriptions.Item label="上传时间">
+              {dayjs(currentFile.created_at).format('YYYY-MM-DD HH:mm:ss')}
+            </Descriptions.Item>
+            <Descriptions.Item label="分类">
+              {currentFile.category_id}
+            </Descriptions.Item>
+            <Descriptions.Item label="下载次数">
+              {currentFile.download_count}
+            </Descriptions.Item>
+            <Descriptions.Item label="标签" span={2}>
+              {currentFile.tags?.split(',').map(tag => (
+                <Tag key={tag}>{tag}</Tag>
+              ))}
+            </Descriptions.Item>
+            <Descriptions.Item label="描述" span={2}>
+              {currentFile.description}
+            </Descriptions.Item>
+            {currentFile.file_type.startsWith('image/') && (
+              <Descriptions.Item label="预览" span={2}>
+                <Image src={currentFile.file_path} style={{ maxWidth: '100%' }} />
+              </Descriptions.Item>
+            )}
+          </Descriptions>
+        )}
+      </Modal>
+      
+      {/* 分类管理弹窗 */}
+      <Modal
+        title={currentCategory ? "编辑分类" : "添加分类"}
+        open={categoryModalVisible}
+        onOk={handleCategorySave}
+        onCancel={() => {
+          setCategoryModalVisible(false);
+          categoryForm.resetFields();
+          setCurrentCategory(null);
+        }}
+      >
+        <Form form={categoryForm} layout="vertical">
+          <Form.Item
+            name="name"
+            label="分类名称"
+            rules={[{ required: true, message: '请输入分类名称' }]}
+          >
+            <Input placeholder="请输入分类名称" />
+          </Form.Item>
+          
+          <Form.Item
+            name="code"
+            label="分类编码"
+            rules={[{ required: true, message: '请输入分类编码' }]}
+          >
+            <Input placeholder="请输入分类编码" />
+          </Form.Item>
+          
+          <Form.Item
+            name="description"
+            label="分类描述"
+          >
+            <Input.TextArea rows={4} placeholder="分类描述..." />
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 407 - 0
client/admin/pages_greenhouse_protocol.tsx

@@ -0,0 +1,407 @@
+import React, { useState } from 'react';
+import {
+  Card,
+  Form,
+  Input,
+  Button,
+  Table,
+  Tag,
+  Space,
+  Statistic,
+  InputNumber,
+  message,
+  Modal,
+  Checkbox
+} from 'antd';
+import {
+  ThunderboltOutlined,
+  PlusOutlined,
+  DeleteOutlined,
+  EditOutlined
+} from '@ant-design/icons';
+
+interface DeviceConfig {
+  id: string;
+  name: string;
+  deviceNo: string;
+  ipAddress: string;
+  port: number;
+  gatewayBaudRate: number;
+  address: number;
+  baudRate: number;
+  dataBits: number;
+  stopBits: number;
+  parity: 'none' | 'even' | 'odd';
+  timeout: number;
+  unitId: number;
+  verificationCode?: string;
+  // 报警阈值配置
+  tempThreshold: {
+    min: number;
+    max: number;
+  };
+  humidityThreshold: {
+    min: number;
+    max: number;
+  };
+  alertMethods: {
+    push: boolean;
+    sms: boolean;
+  };
+}
+
+interface SensorData {
+  temperature: number;
+  humidity: number;
+  timestamp: string;
+}
+
+// 临时模拟ModbusRTU功能
+const ModbusRTU = {
+  connect: async (device: Omit<DeviceConfig, 'id'>): Promise<boolean> => {
+    console.log('模拟连接设备:', device);
+    return true;
+  },
+  readHoldingRegisters: async (addr: number, count: number): Promise<number[]> => {
+    console.log('模拟读取寄存器:', addr, count);
+    return [255, 652]; // 模拟温度25.5°C和湿度65.2%
+  }
+};
+
+const GreenhouseProtocolPage: React.FC = () => {
+  const [editingDevice, setEditingDevice] = useState<DeviceConfig | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [form] = Form.useForm();
+  const [devices, setDevices] = useState<DeviceConfig[]>([]);
+  const [currentData, setCurrentData] = useState<SensorData | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [connected, setConnected] = useState(false);
+
+  const generateVerificationCode = () => {
+    const code = Math.floor(Math.random() * 65536).toString(16).toUpperCase();
+    return code.length === 1 ? `0${code}` : code.padStart(2, '0');
+  };
+
+  const handleAddDevice = () => {
+    const values = form.getFieldsValue();
+    setDevices([...devices, {
+      id: `device-${Date.now()}`,
+      deviceNo: values.deviceNo,
+      verificationCode: generateVerificationCode(),
+      tempThreshold: {
+        min: 10,
+        max: 30
+      },
+      humidityThreshold: {
+        min: 30,
+        max: 80
+      },
+      alertMethods: {
+        push: true,
+        sms: false
+      },
+      ...values
+    }]);
+    form.resetFields();
+  };
+
+  const handleSendCommand = (device: DeviceConfig) => {
+    if (!device.verificationCode) {
+      message.error('请先生成校验码');
+      return;
+    }
+    const deviceNoHex = parseInt(device.deviceNo).toString(16).padStart(2, '0').toUpperCase();
+    const command = `[${deviceNoHex} 04 00 00 00 02 ${device.verificationCode}]`;
+    
+    // 模拟返回数据 (温度18-25℃, 湿度40-70%)
+    const temp = (18 + Math.random() * 7).toFixed(1);
+    const humidity = (40 + Math.random() * 30).toFixed(1);
+    const tempInt = parseInt(temp);
+    const tempDec = parseInt((temp.split('.')[1] || '0'));
+    const humInt = parseInt(humidity);
+    const humDec = parseInt((humidity.split('.')[1] || '0'));
+    
+    const response = `[${deviceNoHex} 04 04 ${tempInt.toString(16).padStart(2,'0')} ${tempDec.toString(16).padStart(2,'0')} ${humInt.toString(16).padStart(2,'0')} ${humDec.toString(16).padStart(2,'0')} 9B 1E]`;
+    
+    console.log('发送指令:', command);
+    message.success(<>
+      <div>指令已发送:</div>
+      <div style={{fontFamily: 'monospace', marginBottom: 8}}>{command}</div>
+      <div>返回数据:</div>
+      <div style={{fontFamily: 'monospace', marginBottom: 8}}>{response}</div>
+      <div>解析结果: 温度 {temp}℃, 湿度 {humidity}%</div>
+    </>);
+  };
+
+  const handleConnect = async (device: DeviceConfig) => {
+    setLoading(true);
+    try {
+      await ModbusRTU.connect(device);
+      setConnected(true);
+    } catch (err) {
+      console.error('连接失败:', err);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleReadData = async () => {
+    if (!connected) return;
+    
+    setLoading(true);
+    try {
+      const data = await ModbusRTU.readHoldingRegisters(0, 2);
+      setCurrentData({
+        temperature: data[0] / 10,
+        humidity: data[1] / 10,
+        timestamp: new Date().toISOString()
+      });
+    } catch (err) {
+      console.error('读取数据失败:', err);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const columns = [
+    {
+      title: '报警状态',
+      key: 'alertStatus',
+      render: (_: unknown, record: DeviceConfig) => {
+        const tempAlert = currentData && (
+          currentData.temperature < record.tempThreshold.min ||
+          currentData.temperature > record.tempThreshold.max
+        );
+        const humidityAlert = currentData && (
+          currentData.humidity < record.humidityThreshold.min ||
+          currentData.humidity > record.humidityThreshold.max
+        );
+        
+        return (
+          <Space>
+            {tempAlert && <Tag color="error">温度异常</Tag>}
+            {humidityAlert && <Tag color="error">湿度异常</Tag>}
+            {!tempAlert && !humidityAlert && <Tag color="success">正常</Tag>}
+          </Space>
+        );
+      },
+    },
+    {
+      title: '设备ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '设备编号',
+      dataIndex: 'deviceNo',
+      key: 'deviceNo',
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: 'Modbus地址',
+      dataIndex: 'address',
+      key: 'address',
+    },
+    {
+      title: '状态',
+      key: 'status',
+      render: () => (
+        <Tag color={connected ? 'success' : 'error'}>
+          {connected ? '已连接' : '未连接'}
+        </Tag>
+      ),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: unknown, record: DeviceConfig) => (
+        <Space size="middle">
+          <Button
+            type="link"
+            icon={<EditOutlined />}
+            onClick={() => {
+              setEditingDevice(record);
+              setModalVisible(true);
+            }}
+          >
+            编辑
+          </Button>
+          <Button
+            type="link"
+            danger
+            icon={<DeleteOutlined />}
+            onClick={() => {
+              Modal.confirm({
+                title: '确认删除设备配置?',
+                content: `确定要删除设备 ${record.name} 吗?`,
+                onOk: () => {
+                  setDevices((prev: DeviceConfig[]) => prev.filter(d => d.id !== record.id));
+                  message.success('设备配置已删除');
+                },
+              });
+            }}
+          >
+            删除
+          </Button>
+          <Button
+            type="primary"
+            onClick={() => handleConnect(record)}
+            loading={loading}
+          >
+            连接
+          </Button>
+          <Button
+            icon={<ThunderboltOutlined />}
+            onClick={() => handleReadData()}
+            disabled={!connected}
+          >
+            读取数据
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <Card title="温室设备通信协议配置">
+      <Form form={form} layout="inline">
+        <Form.Item name="name" label="设备名称" rules={[{ required: true }]}>
+          <Input placeholder="例如: 温室1号" />
+        </Form.Item>
+        <Form.Item name="deviceNo" label="设备编号" rules={[{ required: true, message: '请输入设备编号' }]}>
+          <Input placeholder="例如: GH001" />
+        </Form.Item>
+        <Form.Item name="address" label="Modbus地址" rules={[{ required: true }]}>
+          <InputNumber min={1} max={247} />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" onClick={handleAddDevice}>
+            添加设备
+          </Button>
+        </Form.Item>
+      </Form>
+
+      <Modal
+        title="编辑设备配置"
+        visible={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+      >
+        <Form
+          form={form}
+          initialValues={editingDevice || {
+            tempThreshold: { min: 10, max: 30 },
+            humidityThreshold: { min: 30, max: 80 },
+            alertMethods: { push: true, sms: false }
+          }}
+          layout="vertical"
+        >
+          <Form.Item name="deviceNo" label="设备编号" rules={[{ required: true }]}>
+            <Input />
+          </Form.Item>
+          <Form.Item name="ipAddress" label="IP地址" rules={[{ required: true }]}>
+            <Input placeholder="例如: 192.168.1.1" />
+          </Form.Item>
+          <Form.Item name="port" label="端口号" rules={[{ required: true }]}>
+            <InputNumber min={1} max={65535} />
+          </Form.Item>
+          <Form.Item name="gatewayBaudRate" label="网关波特率" rules={[{ required: true }]}>
+            <InputNumber min={1200} max={115200} />
+          </Form.Item>
+          <Form.Item>
+            <Button
+              type="primary"
+              onClick={() => {
+                // 获取校验码逻辑
+                message.info('校验码已发送');
+              }}
+            >
+              获取校验码
+            </Button>
+            <Button
+              icon={<ThunderboltOutlined />}
+              onClick={() => {
+                if (editingDevice) {
+                  handleSendCommand(editingDevice);
+                }
+              }}
+              style={{ marginLeft: 16 }}
+            >
+              发送指令
+            </Button>
+          </Form.Item>
+          <Form.Item name="verificationCode" label="校验码">
+            <Input disabled />
+          </Form.Item>
+          <Card title="报警阈值设置" style={{ marginBottom: 16 }}>
+            <Form.Item label="温度阈值(°C)" style={{ marginBottom: 8 }}>
+              <Space>
+                <Form.Item name={['tempThreshold', 'min']} noStyle>
+                  <InputNumber min={-20} max={100} placeholder="最低" />
+                </Form.Item>
+                <span>~</span>
+                <Form.Item name={['tempThreshold', 'max']} noStyle>
+                  <InputNumber min={-20} max={100} placeholder="最高" />
+                </Form.Item>
+              </Space>
+            </Form.Item>
+            <Form.Item label="湿度阈值(%)" style={{ marginBottom: 8 }}>
+              <Space>
+                <Form.Item name={['humidityThreshold', 'min']} noStyle>
+                  <InputNumber min={0} max={100} placeholder="最低" />
+                </Form.Item>
+                <span>~</span>
+                <Form.Item name={['humidityThreshold', 'max']} noStyle>
+                  <InputNumber min={0} max={100} placeholder="最高" />
+                </Form.Item>
+              </Space>
+            </Form.Item>
+            <Form.Item label="报警方式">
+              <Space>
+                <Form.Item name={['alertMethods', 'push']} valuePropName="checked" noStyle>
+                  <Checkbox>消息推送</Checkbox>
+                </Form.Item>
+                <Form.Item name={['alertMethods', 'sms']} valuePropName="checked" noStyle>
+                  <Checkbox>短信通知</Checkbox>
+                </Form.Item>
+              </Space>
+            </Form.Item>
+          </Card>
+        </Form>
+      </Modal>
+
+      <Table
+        columns={columns}
+        dataSource={devices}
+        rowKey="id"
+        style={{ marginTop: 16 }}
+      />
+
+      {currentData && (
+        <Card title="实时数据" style={{ marginTop: 16 }}>
+          <Space size="large">
+            <Statistic 
+              title="温度" 
+              value={currentData.temperature} 
+              suffix="°C" 
+              precision={1}
+            />
+            <Statistic 
+              title="湿度" 
+              value={currentData.humidity} 
+              suffix="%" 
+              precision={1}
+            />
+            <div>最后更新: {new Date(currentData.timestamp).toLocaleString()}</div>
+          </Space>
+        </Card>
+      )}
+    </Card>
+  );
+};
+
+export { GreenhouseProtocolPage };

+ 473 - 0
client/admin/pages_inspections.tsx

@@ -0,0 +1,473 @@
+import React, { useState, useEffect } from 'react';
+import {
+  Button, Table, Form, Input, Select, message, Card, Badge,
+  Space, Modal, DatePicker, InputNumber, Switch, Tag, Checkbox, Divider, Progress
+} from 'antd';
+import { DeviceTypeAPI } from './api/device_type.ts';
+import { UserAPI } from './api/users.ts';
+const { RangePicker } = DatePicker;
+const { TextArea } = Input;
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { InspectionsAPI } from './api/inspections.ts';
+import type {
+  InspectionTask,
+  InspectionTemplate,
+  ListResponse
+} from './api/inspections.ts';
+import { getEnumOptions } from './utils.ts';
+
+const PriorityOptions = [
+  { label: '低', value: 'low' },
+  { label: '中', value: 'medium' },
+  { label: '高', value: 'high' },
+  { label: '紧急', value: 'urgent' }
+];
+
+const StatusOptions = [
+  { label: '待处理', value: 'pending' },
+  { label: '进行中', value: 'in_progress' },
+  { label: '已完成', value: 'completed' },
+  { label: '已失败', value: 'failed' }
+];
+
+const getStatusBadge = (status: InspectionTask['status']) => {
+  switch (status) {
+    case 'pending':
+      return <Badge status="default" text="待处理" />;
+    case 'in_progress':
+      return <Badge status="processing" text="进行中" />;
+    case 'completed':
+      return <Badge status="success" text="已完成" />;
+    case 'failed':
+      return <Badge status="error" text="已失败" />;
+    default:
+      return <Badge status="default" text="未知" />;
+  }
+};
+
+const getPriorityTag = (priority: string) => {
+  switch (priority) {
+    case 'low':
+      return <Tag color="blue">低</Tag>;
+    case 'medium':
+      return <Tag color="orange">中</Tag>;
+    case 'high':
+      return <Tag color="red">高</Tag>;
+    case 'urgent':
+      return <Tag color="magenta">紧急</Tag>;
+    default:
+      return <Tag>未知</Tag>;
+  }
+};
+
+export const InspectionsPage = () => {
+  const [loading, setLoading] = useState(false);
+  const [data, setData] = useState<InspectionTask[]>([]);
+  const [deviceTypes, setDeviceTypes] = useState<{label: string, value: string}[]>([]);
+  const [autoInspectionLoading, setAutoInspectionLoading] = useState(false);
+  const [manualInspectionVisible, setManualInspectionVisible] = useState(false);
+  const [progress, setProgress] = useState(0);
+  const [autoForm] = Form.useForm();
+  const [manualForm] = Form.useForm();
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [formRef] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [editingId, setEditingId] = useState<number | null>(null);
+
+  useEffect(() => {
+    fetchData();
+    fetchDeviceTypes();
+  }, [pagination.current, pagination.pageSize]);
+
+  const fetchDeviceTypes = async () => {
+    try {
+      const response = await DeviceTypeAPI.getDeviceTypes({pageSize: 100});
+      setDeviceTypes(response.data.map((item: any) => ({
+        label: item.name,
+        value: item.code
+      })));
+    } catch (error) {
+      console.error('获取设备类型失败:', error);
+      message.error('获取设备类型失败');
+    }
+  };
+
+  const fetchData = async () => {
+    setLoading(true);
+    try {
+      const values = formRef.getFieldsValue();
+      const { title, priority, status, timeRange } = formRef.getFieldsValue();
+      const params: Record<string, any> = {
+        page: pagination.current,
+        limit: pagination.pageSize,
+        status,
+        templateId: priority ? parseInt(priority) : undefined
+      };
+      if (timeRange) {
+        params.startTime = timeRange[0].format('YYYY-MM-DD HH:mm:ss');
+        params.endTime = timeRange[1].format('YYYY-MM-DD HH:mm:ss');
+      }
+      const response = await InspectionsAPI.getTasks(params);
+      
+      setData(response.data);
+      setPagination({
+        ...pagination,
+        total: response.total,
+      });
+    } catch (error) {
+      console.error('获取巡检任务失败:', error);
+      message.error('获取巡检任务失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = (values: any) => {
+    setPagination({
+      ...pagination,
+      current: 1,
+    });
+    fetchData();
+  };
+
+  const handleTableChange = (newPagination: any) => {
+    setPagination({
+      ...pagination,
+      current: newPagination.current,
+      pageSize: newPagination.pageSize,
+    });
+  };
+
+  const handleEditAutoTask = (record: InspectionTask) => {
+    setEditingId(record.id);
+    formRef.setFieldsValue({
+      ...record,
+      cronExpression: record.cronExpression || `0 0 */${record.intervalDays} * *`
+    });
+    setModalVisible(true);
+  };
+
+  const handleDeleteAutoTask = async (id: number) => {
+    Modal.confirm({
+      title: '确认删除自动巡检任务?',
+      content: '此操作不可撤销',
+      onOk: async () => {
+        try {
+          await InspectionsAPI.deleteTemplate(id);
+          message.success('删除成功');
+          fetchData();
+        } catch (error) {
+          console.error('删除失败:', error);
+          message.error('删除失败');
+        }
+      },
+    });
+  };
+
+  const handleStatusChange = async (id: number, status: InspectionTask['status']) => {
+    try {
+      await InspectionsAPI.updateTemplate(id, { status } as Partial<InspectionTemplate>);
+      message.success('状态更新成功');
+      fetchData();
+    } catch (error) {
+      console.error('状态更新失败:', error);
+      message.error('状态更新失败');
+    }
+  };
+
+  const handleAutoInspection = async () => {
+    try {
+      const values = await formRef.validateFields();
+      const { intervalDays, deviceTypes } = values;
+      
+      // 自动生成任务编号
+      const taskNo = `INSP-${Date.now()}-${Math.floor(Math.random() * 1000)}`;
+
+      const data = {
+        taskNo,
+        intervalDays: intervalDays || 7, // 默认7天
+        deviceTypes,
+        reportReceivers: ['admin'] // 默认通知admin
+      };
+
+      if (editingId) {
+        // 更新模板使用模板相关参数
+        await InspectionsAPI.updateTemplate(editingId, {
+          name: values.name,
+          description: values.description
+        });
+        message.success('巡检模板更新成功');
+      } else {
+        // 创建任务使用任务相关参数
+        await InspectionsAPI.createAutoInspectionTask(data);
+        message.success('自动巡检任务创建成功');
+      }
+      
+      setModalVisible(false);
+      fetchData();
+    } catch (error) {
+      console.error('操作失败:', error);
+      message.error('操作失败: ' + (error as Error).message);
+    }
+  };
+
+  const columns = [
+    {
+      title: '任务标题',
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: '优先级',
+      dataIndex: 'priority',
+      key: 'priority',
+      render: (priority: string) => getPriorityTag(priority),
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: InspectionTask['status']) => getStatusBadge(status),
+    },
+    {
+      title: '计划开始时间',
+      dataIndex: 'startTime',
+      key: 'startTime',
+      render: (text?: string) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-',
+    },
+    {
+      title: '计划结束时间',
+      dataIndex: 'endTime',
+      key: 'endTime',
+      render: (text?: string) => text ? dayjs(text).format('YYYY-MM-DD HH:mm') : '-',
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: InspectionTask) => (
+        <Space size="middle">
+          {record.schedule_type === 'scheduled' && (
+            <>
+              <Button type="link" onClick={() => handleEditAutoTask(record)}>编辑</Button>
+              <Button type="link" danger onClick={() => handleDeleteAutoTask(record.id)}>删除</Button>
+            </>
+          )}
+          <Select
+            defaultValue={record.status}
+            style={{ width: 120 }}
+            onChange={(value) => handleStatusChange(record.id, value)}
+            options={StatusOptions}
+          />
+        </Space>
+      ),
+    },
+  ];
+
+  const handleManualInspection = async () => {
+    try {
+      const values = await manualForm.validateFields();
+      setProgress(0);
+      
+      // 模拟巡检进度
+      const interval = setInterval(() => {
+        setProgress(prev => {
+          const newProgress = prev + 10;
+          if (newProgress >= 100) {
+            clearInterval(interval);
+            message.success('手动巡检完成');
+            setManualInspectionVisible(false);
+            // 实际调用API执行巡检
+            InspectionsAPI.runManualTask({
+              deviceTypes: values.deviceTypes
+            });
+          }
+          return newProgress;
+        });
+      }, 500);
+    } catch (error) {
+      console.error('手动巡检失败:', error);
+      message.error('手动巡检失败');
+    }
+  };
+
+
+
+  return (
+    <div className="inspections-page">
+      <Card title="巡检任务配置" style={{ marginBottom: 16 }}>
+        <Space>
+          <Button
+            type="primary"
+            onClick={() => setManualInspectionVisible(true)}
+          >
+            手动巡检
+          </Button>
+          <Button
+            type="primary"
+            onClick={() => {
+              setEditingId(null);
+              setModalVisible(true);
+              formRef.setFieldsValue({
+                name: `自动巡检-${new Date().toLocaleDateString()}`,
+                intervalDays: 7,
+                notifyEmails: ['admin'],
+                cronExpression: '0 0 * * *'
+              });
+            }}
+          >
+            自动巡检配置
+          </Button>
+        </Space>
+      </Card>
+
+      <Card title="巡检任务管理" style={{ marginBottom: 16 }}>
+        <Form
+          form={formRef}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: 16 }}
+        >
+          <Form.Item name="title" label="任务标题">
+            <Input placeholder="输入任务标题" style={{ width: 200 }} />
+          </Form.Item>
+          <Form.Item name="priority" label="优先级">
+            <Select
+              placeholder="选择优先级"
+              style={{ width: 120 }}
+              allowClear
+              options={PriorityOptions}
+            />
+          </Form.Item>
+          <Form.Item name="status" label="状态">
+            <Select
+              placeholder="选择状态"
+              style={{ width: 120 }}
+              allowClear
+              options={StatusOptions}
+            />
+          </Form.Item>
+          <Form.Item name="timeRange" label="计划时间">
+            <RangePicker showTime format="YYYY-MM-DD HH:mm" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              查询
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={data}
+          rowKey="id"
+          pagination={pagination}
+          loading={loading}
+          onChange={handleTableChange}
+        />
+      </Card>
+
+      <Modal
+        title={editingId ? '编辑自动巡检任务' : '新建自动巡检任务'}
+        visible={modalVisible}
+        onOk={handleAutoInspection}
+        onCancel={() => setModalVisible(false)}
+        width={800}
+        style={{ textAlign: 'center' }}
+      >
+        <Form form={formRef} layout="vertical">
+          <Form.Item
+            name="name"
+            label="任务名称"
+            rules={[{ required: true, message: '请输入任务名称' }]}
+          >
+            <Input placeholder="例如: 每周自动巡检" />
+          </Form.Item>
+          <Form.Item
+            name="taskNo"
+            label="任务编号"
+            initialValue={`INSP-${Date.now()}`}
+          >
+            <Input disabled placeholder="自动生成" />
+          </Form.Item>
+          <Form.Item
+            name="intervalDays"
+            label="间隔天数"
+            tooltip="如果未设置cron表达式,则使用间隔天数"
+          >
+            <InputNumber
+              min={1}
+              placeholder="例如: 7 (每周执行)"
+              style={{ textAlign: 'left' }}
+            />
+          </Form.Item>
+          <Form.Item
+            name="deviceTypes"
+            label="设备类型"
+            tooltip="不选择则巡检所有设备"
+          >
+            <Select
+              mode="multiple"
+              style={{ width: '100%' }}
+              placeholder="选择设备类型"
+              options={deviceTypes}
+            />
+          </Form.Item>
+          <Form.Item
+            name="notifyEmails"
+            label="通知"
+            tooltip="巡检完成后发送通知"
+          >
+            <Select
+              mode="tags"
+              style={{ width: '100%' }}
+              placeholder="输入邮箱后按回车添加"
+              tokenSeparators={[',', ' ']}
+            />
+          </Form.Item>
+        </Form>
+      </Modal>
+
+     {/* 手动巡检弹窗 */}
+     <Modal
+       title={<div style={{ textAlign: 'center', fontSize: '18px', fontWeight: 'bold' }}>手动巡检</div>}
+       visible={manualInspectionVisible}
+       onOk={handleManualInspection}
+       onCancel={() => setManualInspectionVisible(false)}
+       width={600}
+       centered
+     >
+       <Form form={manualForm} layout="vertical">
+         <Form.Item label="巡检时间">
+           {dayjs().format('YYYY-MM-DD HH:mm:ss')}
+         </Form.Item>
+         <Form.Item
+           name="taskNo"
+           label="巡检任务编号"
+           initialValue={`INSP-${Date.now()}`}
+         >
+           <Input disabled />
+         </Form.Item>
+         <Form.Item
+           name="deviceTypes"
+           label="巡检设备类型"
+         >
+           <Select
+             mode="multiple"
+             style={{ width: '100%' }}
+             placeholder="选择设备类型(不选则为全部设备)"
+             options={deviceTypes}
+           />
+         </Form.Item>
+         <Form.Item label="巡检进度">
+           <Progress percent={progress} status={progress < 100 ? 'active' : 'success'} />
+         </Form.Item>
+       </Form>
+     </Modal>
+   </div>
+  );
+};

+ 416 - 0
client/admin/pages_know_info.tsx

@@ -0,0 +1,416 @@
+import React, { useState } from 'react';
+import { useQueryClient } from '@tanstack/react-query';
+import {
+  Layout, Menu, Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Spin, Row, Col, Breadcrumb, Avatar,
+  Dropdown, ConfigProvider, theme, Typography,
+  Switch, Badge, Image, Upload, Divider, Descriptions,
+  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
+} from 'antd';
+import {
+  UploadOutlined,
+  FileImageOutlined,
+  FileExcelOutlined,
+  FileWordOutlined,
+  FilePdfOutlined,
+  FileOutlined,
+} from '@ant-design/icons';   
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+import type { 
+  KnowInfo
+} from '../share/types.ts';
+
+import {
+   AuditStatus,AuditStatusNameMap,
+} from '../share/types.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+import {
+  KnowInfoAPI,
+  type KnowInfoListResponse
+} from './api/index.ts';
+
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+// 设置 dayjs 语言
+dayjs.locale('zh-cn');
+
+
+
+// 知识库管理页面组件
+export const KnowInfoPage = () => {
+  const queryClient = useQueryClient();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [form] = Form.useForm();
+  const [searchForm] = Form.useForm();
+  const [searchParams, setSearchParams] = useState({
+    title: '',
+    category: '',
+    page: 1,
+    limit: 10,
+  });
+  
+  // 使用React Query获取知识库文章列表
+  const { data: articlesData, isLoading: isListLoading, refetch } = useQuery({
+    queryKey: ['knowInfos', searchParams],
+    queryFn: () => KnowInfoAPI.getKnowInfos({
+      page: searchParams.page,
+      pageSize: searchParams.limit,
+      title: searchParams.title,
+      category: searchParams.category
+    }),
+    placeholderData: {
+      data: [],
+      pagination: {
+        current: 1,
+        pageSize: 10,
+        total: 0,
+        totalPages: 1
+      }
+    }
+  });
+  
+  const articles = React.useMemo(() => (articlesData as KnowInfoListResponse)?.data || [], [articlesData]);
+  const pagination = React.useMemo(() => ({
+    current: (articlesData as KnowInfoListResponse)?.pagination?.current || 1,
+    pageSize: (articlesData as KnowInfoListResponse)?.pagination?.pageSize || 10,
+    total: (articlesData as KnowInfoListResponse)?.pagination?.total || 0,
+    totalPages: (articlesData as KnowInfoListResponse)?.pagination?.totalPages || 1
+  }), [articlesData]);
+  
+  // 获取单个知识库文章
+  const fetchArticle = async (id: number) => {
+    try {
+      const response = await KnowInfoAPI.getKnowInfo(id);
+      return response.data;
+    } catch (error) {
+      message.error('获取知识库文章详情失败');
+      return null;
+    }
+  };
+  
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<KnowInfo>) => {
+    console.log('handleSubmit', values)
+    try {
+      const response = formMode === 'create'
+        ? await KnowInfoAPI.createKnowInfo(values)
+        : await KnowInfoAPI.updateKnowInfo(editingId!, values);
+      
+      message.success(formMode === 'create' ? '创建知识库文章成功' : '更新知识库文章成功');
+      setModalVisible(false);
+      form.resetFields();
+      refetch();
+    } catch (error) {
+      message.error((error as Error).message);
+    }
+  };
+  
+  // 处理编辑
+  const handleEdit = async (id: number) => {
+    const article = await fetchArticle(id);
+    console.log('article', article)
+    if (article) {
+      setFormMode('edit');
+      setEditingId(id);
+      form.setFieldsValue(article);
+      setModalVisible(true);
+    }
+  };
+  
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await KnowInfoAPI.deleteKnowInfo(id);
+      
+      message.success('删除知识库文章成功');
+      refetch();
+    } catch (error) {
+      message.error((error as Error).message);
+    }
+  };
+  
+  // 处理搜索
+  const handleSearch = async (values: any) => {
+    try {
+      queryClient.removeQueries({ queryKey: ['knowInfos'] });
+      setSearchParams({
+        title: values.title || '',
+        category: values.category || '',
+        page: 1,
+        limit: searchParams.limit,
+      });
+    } catch (error) {
+      message.error('搜索失败');
+    }
+  };
+  
+  // 处理分页
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page,
+      limit: pageSize || prev.limit,
+    }));
+  };
+  
+  // 处理添加
+  const handleAdd = () => {
+    setFormMode('create');
+    setEditingId(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+  
+  // 审核状态映射
+  const auditStatusOptions = getEnumOptions(AuditStatus, AuditStatusNameMap);
+  
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80,
+    },
+    {
+      title: '标题',
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: '分类',
+      dataIndex: 'category',
+      key: 'category',
+    },
+    {
+      title: '标签',
+      dataIndex: 'tags',
+      key: 'tags',
+      render: (tags: string) => tags ? tags.split(',').map(tag => (
+        <Tag key={tag}>{tag}</Tag>
+      )) : null,
+    },
+    {
+      title: '作者',
+      dataIndex: 'author',
+      key: 'author',
+    },
+    {
+      title: '审核状态',
+      dataIndex: 'audit_status',
+      key: 'audit_status',
+      render: (status: AuditStatus) => {
+        let color = '';
+        let text = '';
+        
+        switch(status) {
+          case AuditStatus.PENDING:
+            color = 'orange';
+            text = '待审核';
+            break;
+          case AuditStatus.APPROVED:
+            color = 'green';
+            text = '已通过';
+            break;
+          case AuditStatus.REJECTED:
+            color = 'red';
+            text = '已拒绝';
+            break;
+          default:
+            color = 'default';
+            text = '未知';
+        }
+        
+        return <Tag color={color}>{text}</Tag>;
+      },
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: string) => new Date(date).toLocaleString(),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: KnowInfo) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => handleEdit(record.id)}>编辑</Button>
+          <Popconfirm
+            title="确定要删除这篇文章吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div>
+      <Card title="知识库管理" className="mb-4">
+        <Form
+          form={searchForm}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: '16px' }}
+        >
+          <Form.Item name="title" label="标题">
+            <Input placeholder="要搜索的文章标题" />
+          </Form.Item>
+          
+          <Form.Item name="category" label="分类">
+            <Input placeholder="要搜索的文章分类" />
+          </Form.Item>
+          
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                搜索
+              </Button>
+              <Button htmlType="reset" onClick={() => {
+                searchForm.resetFields();
+                setSearchParams({
+                  title: '',
+                  category: '',
+                  page: 1,
+                  limit: 10,
+                });
+              }}>
+                重置
+              </Button>
+              <Button type="primary" onClick={handleAdd}>
+                添加文章
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={articles}
+          rowKey="id"
+          loading={{
+            spinning: isListLoading,
+            tip: '正在加载数据...',
+          }}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条`,
+          }}
+        />
+      </Card>
+      
+      <Modal
+        title={formMode === 'create' ? '添加知识库文章' : '编辑知识库文章'}
+        open={modalVisible}
+        onOk={() => {
+          form.validateFields()
+            .then(values => {
+              handleSubmit(values);
+            })
+            .catch(info => {
+              console.log('表单验证失败:', info);
+            });
+        }}
+        onCancel={() => setModalVisible(false)}
+        width={800}
+        okText="确定"
+        cancelText="取消"
+        destroyOnClose
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          initialValues={{
+            audit_status: AuditStatus.PENDING,
+          }}
+        >
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="title"
+                label="文章标题"
+                rules={[{ required: true, message: '请输入文章标题' }]}
+              >
+                <Input placeholder="请输入文章标题" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="category"
+                label="文章分类"
+              >
+                <Input placeholder="请输入文章分类" />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Form.Item
+            name="tags"
+            label="文章标签"
+            help="多个标签请用英文逗号分隔,如: 服务器,网络,故障"
+          >
+            <Input placeholder="请输入文章标签,多个标签请用英文逗号分隔" />
+          </Form.Item>
+          
+          <Form.Item
+            name="content"
+            label="文章内容"
+            // rules={[{ required: true, message: '请输入文章内容' }]}
+          >
+            <Input.TextArea rows={15} placeholder="请输入文章内容,支持Markdown格式" />
+          </Form.Item>
+          
+          <Row gutter={16}>
+            <Col span={12}>
+              <Form.Item
+                name="author"
+                label="文章作者"
+              >
+                <Input placeholder="请输入文章作者" />
+              </Form.Item>
+            </Col>
+            <Col span={12}>
+              <Form.Item
+                name="cover_url"
+                label="封面图片URL"
+              >
+                <Input placeholder="请输入封面图片URL" />
+              </Form.Item>
+            </Col>
+          </Row>
+          
+          <Form.Item
+            name="audit_status"
+            label="审核状态"
+          >
+            <Select options={auditStatusOptions} />
+          </Form.Item>
+          
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 114 - 0
client/admin/pages_login_reg.tsx

@@ -0,0 +1,114 @@
+import React, { useState } from 'react';
+import {
+  Form,
+  Input,
+  Button,
+  Card,
+  message,
+} from 'antd';
+import {
+  UserOutlined,
+} from '@ant-design/icons';
+import { useNavigate } from 'react-router';
+import {
+  useAuth,
+} from './hooks_sys.tsx';
+
+
+// 登录页面
+export const LoginPage = () => {
+  const { login } = useAuth();
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+  const navigate = useNavigate();
+  
+  const handleSubmit = async (values: { username: string; password: string }) => {
+    try {
+      setLoading(true);
+      
+      // 获取地理位置
+      let latitude: number | undefined;
+      let longitude: number | undefined;
+      
+      try {
+        if (navigator.geolocation) {
+          const position = await new Promise<GeolocationPosition>((resolve, reject) => {
+            navigator.geolocation.getCurrentPosition(resolve, reject);
+          });
+          latitude = position.coords.latitude;
+          longitude = position.coords.longitude;
+        }
+      } catch (geoError) {
+        console.warn('获取地理位置失败:', geoError);
+      }
+      
+      await login(values.username, values.password, latitude, longitude);
+      // 登录成功后跳转到管理后台首页
+      navigate('/admin/dashboard');
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '登录失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+  
+  return (
+    <div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
+      <div className="max-w-md w-full space-y-8">
+        <div>
+          <h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
+            登录管理后台
+          </h2>
+        </div>
+        
+        <Card>
+          <Form
+            form={form}
+            name="login"
+            onFinish={handleSubmit}
+            autoComplete="off"
+            layout="vertical"
+          >
+            <Form.Item
+              name="username"
+              rules={[{ required: true, message: '请输入用户名' }]}
+            >
+              <Input 
+                prefix={<UserOutlined />} 
+                placeholder="用户名" 
+                size="large"
+              />
+            </Form.Item>
+            
+            <Form.Item
+              name="password"
+              rules={[{ required: true, message: '请输入密码' }]}
+            >
+              <Input.Password 
+                placeholder="密码" 
+                size="large"
+              />
+            </Form.Item>
+            
+            <Form.Item>
+              <Button 
+                type="primary" 
+                htmlType="submit" 
+                size="large" 
+                block
+                loading={loading}
+              >
+                登录
+              </Button>
+            </Form.Item>
+          </Form>
+          
+          <div className="mt-4 text-center text-gray-500">
+            <p>测试账号: admin / admin123</p>
+            {/* <p>普通账号: user1 / 123456</p> */}
+          </div>
+        </Card>
+      </div>
+    </div>
+  );
+};

+ 210 - 0
client/admin/pages_map.tsx

@@ -0,0 +1,210 @@
+import React, { useState } from 'react';
+import { 
+  Button, Space, Drawer,
+   Select, message, 
+  Card, Spin, Typography,Descriptions,DatePicker, 
+} from 'antd';
+import {
+  EnvironmentOutlined,
+  ClockCircleOutlined,
+  UserOutlined,
+  GlobalOutlined
+} from '@ant-design/icons';   
+import {
+  useQuery,
+} from '@tanstack/react-query';
+import 'dayjs/locale/zh-cn';
+import AMap from './components_amap.tsx'; // 导入地图组件
+// 从share/types.ts导入所有类型,包括MapMode
+import type { 
+   MarkerData, LoginLocation, LoginLocationDetail, User
+} from '../share/types.ts';
+
+import { UserAPI } from './api/index.ts';
+import { MapAPI } from './api/index.ts';
+import dayjs from 'dayjs';
+
+const { RangePicker } = DatePicker;
+
+
+// 地图页面组件
+export const LoginMapPage = () => {
+  const [selectedTimeRange, setSelectedTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs] | null>(null);
+  const [selectedUserId, setSelectedUserId] = useState<number | null>(null);
+  const [selectedMarker, setSelectedMarker] = useState<MarkerData | null>(null);
+  const [drawerVisible, setDrawerVisible] = useState(false);
+
+  // 获取登录位置数据
+  const { data: locations = [], isLoading: markersLoading } = useQuery<LoginLocation[]>({
+    queryKey: ['loginLocations', selectedTimeRange, selectedUserId],
+    queryFn: async () => {
+      try {
+        let params: any = {};
+        
+        if (selectedTimeRange) {
+          params.startTime = selectedTimeRange[0].format('YYYY-MM-DD HH:mm:ss');
+          params.endTime = selectedTimeRange[1].format('YYYY-MM-DD HH:mm:ss');
+        }
+        
+        if (selectedUserId) {
+          params.userId = selectedUserId;
+        }
+        
+        const result = await MapAPI.getMarkers(params);
+        return result.data;
+      } catch (error) {
+        console.error("获取登录位置数据失败:", error);
+        message.error("获取登录位置数据失败");
+        return [];
+      }
+    },
+    refetchInterval: 30000 // 30秒刷新一次
+  });
+
+  // 获取用户列表
+  const { data: users = [] } = useQuery<User[]>({
+    queryKey: ['users'],
+    queryFn: async () => {
+      try {
+        const response = await UserAPI.getUsers();
+        return response.data || [];
+      } catch (error) {
+        console.error("获取用户列表失败:", error);
+        message.error("获取用户列表失败");
+        return [];
+      }
+    }
+  });
+
+  // 获取选中标记点的详细信息
+  const { data: markerDetail, isLoading: detailLoading } = useQuery<LoginLocationDetail | undefined>({
+    queryKey: ['loginLocation', selectedMarker?.id],
+    queryFn: async () => {
+      if (!selectedMarker?.id) return undefined;
+      try {
+        const result = await MapAPI.getLocationDetail(Number(selectedMarker.id));
+        return result.data;
+      } catch (error) {
+        console.error("获取登录位置详情失败:", error);
+        message.error("获取登录位置详情失败");
+        return undefined;
+      }
+    },
+    enabled: !!selectedMarker?.id
+  });
+
+  // 处理标记点点击
+  const handleMarkerClick = (marker: MarkerData) => {
+    setSelectedMarker(marker);
+    setDrawerVisible(true);
+  };
+
+  // 渲染地图标记点
+  const renderMarkers = (locations: LoginLocation[] = []): MarkerData[] => {
+    if (!Array.isArray(locations)) return [];
+    
+    return locations
+      .filter(location => location?.longitude !== null && location?.latitude !== null)
+      .map(location => ({
+        id: location.id?.toString() || '',
+        longitude: location.longitude as number,
+        latitude: location.latitude as number,
+        title: location.user?.nickname || location.user?.username || '未知用户',
+        description: `登录时间: ${dayjs(location.loginTime).format('YYYY-MM-DD HH:mm:ss')}\nIP地址: ${location.ipAddress}`,
+        status: 'online',
+        type: 'login',
+        extraData: location
+      }));
+  };
+
+  return (
+    <div className="h-full">
+      <Card style={{ marginBottom: 16 }}>
+        <Space direction="horizontal" size={16} wrap>
+          <RangePicker
+            showTime
+            onChange={(dates) => setSelectedTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])}
+            placeholder={['开始时间', '结束时间']}
+          />
+          <Select
+            style={{ width: 200 }}
+            placeholder="选择用户"
+            allowClear
+            onChange={(value) => setSelectedUserId(value)}
+            options={users.map((user: User) => ({
+              label: user.nickname || user.username,
+              value: user.id
+            }))}
+          />
+          <Button 
+            type="primary"
+            onClick={() => {
+              setSelectedTimeRange(null);
+              setSelectedUserId(null);
+            }}
+          >
+            重置筛选
+          </Button>
+        </Space>
+      </Card>
+
+      <Card style={{ height: 'calc(100% - 80px)' }}>
+        <Spin spinning={markersLoading}>
+          <div style={{ height: '100%', minHeight: '500px' }}>
+            <AMap
+              markers={renderMarkers(locations || [])}
+              center={locations[0] && locations[0].longitude !== null && locations[0].latitude !== null 
+                ? [locations[0].longitude, locations[0].latitude] as [number, number] 
+                : undefined}
+              onMarkerClick={handleMarkerClick}
+              height={'100%'}
+            />
+          </div>
+        </Spin>
+      </Card>
+
+      <Drawer
+        title="登录位置详情"
+        placement="right"
+        onClose={() => {
+          setDrawerVisible(false);
+          setSelectedMarker(null);
+        }}
+        open={drawerVisible}
+        width={400}
+      >
+        {detailLoading ? (
+          <Spin />
+        ) : markerDetail ? (
+          <Descriptions column={1}>
+            <Descriptions.Item label={<><UserOutlined /> 用户</>}>
+              {markerDetail.user?.nickname || markerDetail.user?.username || '未知用户'}
+            </Descriptions.Item>
+            <Descriptions.Item label={<><ClockCircleOutlined /> 登录时间</>}>
+              {dayjs(markerDetail.login_time).format('YYYY-MM-DD HH:mm:ss')}
+            </Descriptions.Item>
+            <Descriptions.Item label={<><GlobalOutlined /> IP地址</>}>
+              {markerDetail.ip_address}
+            </Descriptions.Item>
+            <Descriptions.Item label={<><EnvironmentOutlined /> 位置名称</>}>
+              {markerDetail.location_name || '未知位置'}
+            </Descriptions.Item>
+            <Descriptions.Item label="经度">
+              {markerDetail.longitude}
+            </Descriptions.Item>
+            <Descriptions.Item label="纬度">
+              {markerDetail.latitude}
+            </Descriptions.Item>
+            <Descriptions.Item label="浏览器信息">
+              <Typography.Paragraph ellipsis={{ rows: 2 }}>
+                {markerDetail.user_agent}
+              </Typography.Paragraph>
+            </Descriptions.Item>
+          </Descriptions>
+        ) : (
+          <div>暂无详细信息</div>
+        )}
+      </Drawer>
+    </div>
+  );
+};

+ 281 - 0
client/admin/pages_messages.tsx

@@ -0,0 +1,281 @@
+import React, { useState } from 'react';
+import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
+import { Button, Table, Space, Modal, Form, Input, Select, message } from 'antd';
+import type { TableProps } from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+
+import { MessageAPI , UserAPI } from './api/index.ts';
+import type { UserMessage } from '../share/types.ts';
+import { MessageStatusNameMap , MessageStatus} from '../share/types.ts';
+
+export  const MessagesPage = () => {
+  const queryClient = useQueryClient();
+  const [form] = Form.useForm();
+  const [isModalVisible, setIsModalVisible] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    pageSize: 10,
+    type: undefined,
+    status: undefined,
+    search: undefined
+  });
+
+  // 获取消息列表
+  const { data: messages, isLoading } = useQuery({
+    queryKey: ['messages', searchParams],
+    queryFn: () => MessageAPI.getMessages(searchParams),
+  });
+
+  // 获取用户列表
+  const { data: users } = useQuery({
+    queryKey: ['users'],
+    queryFn: () => UserAPI.getUsers({ page: 1, limit: 1000 }),
+  });
+
+  // 获取未读消息数
+  const { data: unreadCount } = useQuery({
+    queryKey: ['unreadCount'],
+    queryFn: () => MessageAPI.getUnreadCount(),
+  });
+
+  // 标记消息为已读
+  const markAsReadMutation = useMutation({
+    mutationFn: (id: number) => MessageAPI.markAsRead(id),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['messages'] });
+      queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
+      message.success('标记已读成功');
+    },
+  });
+
+  // 删除消息
+  const deleteMutation = useMutation({
+    mutationFn: (id: number) => MessageAPI.deleteMessage(id),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['messages'] });
+      message.success('删除成功');
+    },
+  });
+
+  // 发送消息
+  const sendMessageMutation = useMutation({
+    mutationFn: (data: any) => MessageAPI.sendMessage(data),
+    onSuccess: () => {
+      queryClient.invalidateQueries({ queryKey: ['messages'] });
+      queryClient.invalidateQueries({ queryKey: ['unreadCount'] });
+      message.success('发送成功');
+      setIsModalVisible(false);
+      form.resetFields();
+    },
+  });
+
+  const columns: TableProps<UserMessage>['columns'] = [
+    {
+      title: '标题',
+      dataIndex: 'title',
+      key: 'title',
+    },
+    {
+      title: '类型',
+      dataIndex: 'type',
+      key: 'type',
+    },
+    {
+      title: '发送人',
+      dataIndex: 'sender_name',
+      key: 'sender_name',
+    },
+    {
+      title: '状态',
+      dataIndex: 'user_status',
+      key: 'user_status',
+      render: (user_status: MessageStatus) => (
+        <span style={{ color: user_status === MessageStatus.UNREAD ? 'red' : 'green' }}>
+          {MessageStatusNameMap[user_status]}
+        </span>
+      ),
+    },
+    {
+      title: '发送时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record) => (
+        <Space size="middle">
+          <Button 
+            type="link" 
+            onClick={() => markAsReadMutation.mutate(record.id)}
+            disabled={record.user_status === MessageStatus.READ}
+          >
+            标记已读
+          </Button>
+          <Button 
+            type="link" 
+            danger 
+            onClick={() => deleteMutation.mutate(record.id)}
+          >
+            删除
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      ...searchParams,
+      ...values,
+      page: 1
+    });
+  };
+
+  const handleTableChange = (pagination: any) => {
+    setSearchParams({
+      ...searchParams,
+      page: pagination.current,
+      pageSize: pagination.pageSize
+    });
+  };
+
+  const handleSendMessage = (values: any) => {
+    sendMessageMutation.mutate(values);
+  };
+
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h1 className="text-2xl font-bold">消息管理</h1>
+        <div className="flex items-center space-x-4">
+          {unreadCount && unreadCount.count > 0 && (
+            <span className="text-red-500">{unreadCount.count}条未读</span>
+          )}
+          <Button type="primary" onClick={() => setIsModalVisible(true)}>
+            发送消息
+          </Button>
+        </div>
+      </div>
+
+      <div className="bg-white p-4 rounded shadow">
+        <Form layout="inline" onFinish={handleSearch} className="mb-4">
+          <Form.Item name="type" label="类型">
+            <Select
+              style={{ width: 120 }}
+              allowClear
+              options={[
+                { value: 'SYSTEM', label: '系统消息' },
+                { value: 'NOTICE', label: '公告' },
+                { value: 'PERSONAL', label: '个人消息' },
+              ]}
+            />
+          </Form.Item>
+          <Form.Item name="status" label="状态">
+            <Select
+              style={{ width: 120 }}
+              allowClear
+              options={[
+                { value: 'UNREAD', label: '未读' },
+                { value: 'READ', label: '已读' },
+              ]}
+            />
+          </Form.Item>
+          <Form.Item name="search" label="搜索">
+            <Input placeholder="输入标题或内容" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              搜索
+            </Button>
+          </Form.Item>
+        </Form>
+
+        <Table
+          columns={columns}
+          dataSource={messages?.data}
+          loading={isLoading}
+          rowKey="id"
+          pagination={{
+            current: searchParams.page,
+            pageSize: searchParams.pageSize,
+            total: messages?.pagination?.total,
+            showSizeChanger: true,
+          }}
+          onChange={handleTableChange}
+        />
+      </div>
+
+      <Modal
+        title="发送消息"
+        visible={isModalVisible}
+        onCancel={() => setIsModalVisible(false)}
+        footer={null}
+        width={800}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSendMessage}
+        >
+          <Form.Item
+            name="title"
+            label="标题"
+            rules={[{ required: true, message: '请输入标题' }]}
+          >
+            <Input placeholder="请输入消息标题" />
+          </Form.Item>
+
+          <Form.Item
+            name="type"
+            label="消息类型"
+            rules={[{ required: true, message: '请选择消息类型' }]}
+          >
+            <Select
+              options={[
+                { value: 'SYSTEM', label: '系统消息' },
+                { value: 'NOTICE', label: '公告' },
+                { value: 'PERSONAL', label: '个人消息' },
+              ]}
+            />
+          </Form.Item>
+
+          <Form.Item
+            name="receiver_ids"
+            label="接收人"
+            rules={[{ required: true, message: '请选择接收人' }]}
+          >
+            <Select
+              mode="multiple"
+              placeholder="请选择接收人"
+              options={users?.data?.map((user: any) => ({
+                value: user.id,
+                label: user.username,
+              }))}
+            />
+          </Form.Item>
+
+          <Form.Item
+            name="content"
+            label="内容"
+            rules={[{ required: true, message: '请输入消息内容' }]}
+          >
+            <Input.TextArea rows={6} placeholder="请输入消息内容" />
+          </Form.Item>
+
+          <Form.Item>
+            <Button 
+              type="primary" 
+              htmlType="submit"
+              loading={sendMessageMutation.status === 'pending'}
+            >
+              发送
+            </Button>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 252 - 0
client/admin/pages_modbus_rtu_device.tsx

@@ -0,0 +1,252 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Form, Input, Select, message, Card, Badge, 
+} from 'antd';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  DeviceMonitorData, 
+} from '../share/monitorTypes.ts';
+
+import {
+  DeviceStatus, DeviceProtocolType, MetricType, DeviceStatusNameMap, DeviceProtocolTypeNameMap,
+  MetricTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import { getEnumOptions } from './utils.ts';
+import { DeviceInstanceAPI, MonitorAPI } from './api/index.ts';
+
+
+// Modbus RTU设备监控页面
+export const ModbusRtuDevicePage = () => {
+  const [loading, setLoading] = useState(false);
+  const [monitorData, setMonitorData] = useState<DeviceMonitorData[]>([]);
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 10,
+    total: 0,
+  });
+  const [deviceOptions, setDeviceOptions] = useState<{label: string, value: number}[]>([]);
+  const [formRef] = Form.useForm();
+  
+  // 监控数据刷新间隔(毫秒)
+  const REFRESH_INTERVAL = 30000;
+
+  useEffect(() => {
+    fetchDeviceOptions();
+    fetchMonitorData();
+    
+    // 设置定时刷新
+    const intervalId = setInterval(() => {
+      fetchMonitorData();
+    }, REFRESH_INTERVAL);
+    
+    return () => clearInterval(intervalId);
+  }, [pagination.current, pagination.pageSize]);
+
+  const fetchDeviceOptions = async () => {
+    try {
+      const response = await DeviceInstanceAPI.getDeviceInstances();
+      if (response && response.data) {
+        const options = response.data.map((device) => ({
+          label: device.asset_name || `设备${device.id}`,
+          value: device.id
+        }));
+        setDeviceOptions(options);
+      }
+    } catch (error) {
+      console.error('获取设备列表失败:', error);
+      message.error('获取设备列表失败');
+    }
+  };
+
+  const fetchMonitorData = async () => {
+    setLoading(true);
+    try {
+      const values = formRef.getFieldsValue();
+      const params = {
+        page: pagination.current,
+        pageSize: pagination.pageSize,
+        device_id: values.device_id,
+        device_name: values.device_name,
+        protocol: values.protocol,
+        address: values.address,
+        metric_type: values.metric_type,
+        status: values.status,
+      };
+      
+      const response = await MonitorAPI.getMonitorData(params);
+      
+      if (response) {
+        setMonitorData(response.data || []);
+        setPagination({
+          ...pagination,
+          total: response.total || 0,
+        });
+      }
+    } catch (error) {
+      console.error('获取监控数据失败:', error);
+      message.error('获取监控数据失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = (values: any) => {
+    setPagination({
+      ...pagination,
+      current: 1,
+    });
+    fetchMonitorData();
+  };
+
+  const handleTableChange = (newPagination: any) => {
+    setPagination({
+      ...pagination,
+      current: newPagination.current,
+      pageSize: newPagination.pageSize,
+    });
+  };
+
+  const metricTypeOptions = getEnumOptions(MetricType, MetricTypeNameMap);
+  const statusOptions = getEnumOptions(DeviceStatus, DeviceStatusNameMap);
+  const protocolOptions = getEnumOptions(DeviceProtocolType, DeviceProtocolTypeNameMap);
+
+  const getStatusBadge = (status?: DeviceStatus) => {
+    switch (status) {
+      case DeviceStatus.NORMAL:
+        return <Badge status="success" text="正常" />;
+      case DeviceStatus.MAINTAIN:
+        return <Badge status="processing" text="维护中" />;
+      case DeviceStatus.FAULT:
+        return <Badge status="error" text="故障" />;
+      case DeviceStatus.OFFLINE:
+        return <Badge status="default" text="下线" />;
+      default:
+        return <Badge status="default" text="未知" />;
+    }
+  };
+
+  const columns = [
+    {
+      title: '设备ID',
+      dataIndex: 'device_id',
+      key: 'device_id',
+      width: 80,
+    },
+    {
+      title: '设备名称',
+      dataIndex: 'device_name',
+      key: 'device_name',
+    },
+    {
+      title: '通信协议',
+      dataIndex: 'protocol',
+      key: 'protocol',
+      render: (text: string) => {
+        const option = protocolOptions.find(opt => opt.value === text);
+        return option ? option.label : text;
+      },
+    },
+    {
+      title: '通信地址',
+      dataIndex: 'address',
+      key: 'address',
+    },
+    {
+      title: '监控指标',
+      dataIndex: 'metric_type',
+      key: 'metric_type',
+      render: (text: string) => {
+        const option = metricTypeOptions.find(opt => opt.value === text);
+        return option ? option.label : text;
+      },
+    },
+    {
+      title: '监控值',
+      dataIndex: 'metric_value',
+      key: 'metric_value',
+      render: (value: number, record: DeviceMonitorData) => {
+        return `${value} ${record.unit || ''}`;
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: DeviceStatus) => getStatusBadge(status),
+    },
+    {
+      title: '采集时间',
+      dataIndex: 'collect_time',
+      key: 'collect_time',
+      render: (text: Date) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
+    },
+  ];
+
+  return (
+    <div>
+      <Card title="Modbus RTU设备监控" style={{ marginBottom: 16 }}>
+        <Form
+          form={formRef}
+          layout="inline"
+          onFinish={handleSearch}
+          style={{ marginBottom: 16 }}
+        >
+          <Form.Item name="device_id" label="设备">
+            <Select
+              placeholder="选择设备"
+              style={{ width: 200 }}
+              allowClear
+              options={deviceOptions}
+            />
+          </Form.Item>
+          <Form.Item name="device_name" label="设备名称">
+            <Input placeholder="输入设备名称" style={{ width: 200 }} />
+          </Form.Item>
+          <Form.Item name="protocol" label="通信协议">
+            <Select
+              placeholder="选择通信协议"
+              style={{ width: 200 }}
+              allowClear
+              options={protocolOptions}
+            />
+          </Form.Item>
+          <Form.Item name="address" label="通信地址">
+            <Input placeholder="输入通信地址" style={{ width: 200 }} />
+          </Form.Item>
+          <Form.Item name="metric_type" label="监控指标">
+            <Select
+              placeholder="选择监控指标"
+              style={{ width: 200 }}
+              allowClear
+              options={metricTypeOptions}
+            />
+          </Form.Item>
+          <Form.Item name="status" label="状态">
+            <Select
+              placeholder="选择状态"
+              style={{ width: 120 }}
+              allowClear
+              options={statusOptions}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              查询
+            </Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={monitorData}
+          rowKey="id"
+          pagination={pagination}
+          loading={loading}
+          onChange={handleTableChange}
+        />
+      </Card>
+    </div>
+  );
+};

+ 115 - 0
client/admin/pages_online_devices_chart.tsx

@@ -0,0 +1,115 @@
+import React, { useState } from 'react';
+import { 
+  Button, Form, Select, Card, Typography, DatePicker
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { Column} from "@ant-design/plots";
+import 'dayjs/locale/zh-cn';
+
+import { MonitorChartsAPI } from './api/index.ts';
+
+// 在线设备数量图表页面
+export const OnlineDevicesChartPage = () => {
+    const [timeRange, setTimeRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>([
+      dayjs().subtract(7, 'day'),
+      dayjs()
+    ]);
+    const [dimension, setDimension] = useState<'hour' | 'day' | 'month'>('day');
+  
+    const { data: onlineRateData, isLoading, refetch } = useQuery({
+      queryKey: ['adminZichanOnlineRate', timeRange, dimension],
+      queryFn: async () => {
+        const params = {
+          created_at_gte: timeRange[0].format('YYYY-MM-DD HH:mm:ss'),
+          created_at_lte: timeRange[1].format('YYYY-MM-DD HH:mm:ss'),
+          dimension
+        };
+        
+        // const res = await axios.get<OnlineRateChartData[]>(`${API_BASE_URL}/big/zichan_online_rate_chart`, { params });
+        const res = await MonitorChartsAPI.fetchOnlineRateData(params);
+        return res;
+      }
+    });
+  
+    const { Title } = Typography;
+    const { RangePicker } = DatePicker;
+  
+    const handleSearch = () => {
+      refetch();
+    };
+  
+    return (
+      <div>
+        <Title level={2}>在线设备数量变化</Title>
+        
+        <Card>
+          <Form layout="inline" style={{ marginBottom: '16px' }}>
+            <Form.Item label="时间范围">
+              <RangePicker 
+                value={timeRange} 
+                onChange={(dates) => dates && setTimeRange(dates as [dayjs.Dayjs, dayjs.Dayjs])} 
+              />
+            </Form.Item>
+            <Form.Item label="时间维度">
+              <Select 
+                value={dimension} 
+                onChange={setDimension}
+                options={[
+                  { label: '小时', value: 'hour' },
+                  { label: '天', value: 'day' },
+                  { label: '月', value: 'month' }
+                ]}
+                style={{ width: '100px' }}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" onClick={handleSearch}>查询</Button>
+            </Form.Item>
+          </Form>
+          
+          <div style={{ height: '500px' }}>
+            {!isLoading && onlineRateData && (
+              <Column
+                data={onlineRateData}
+                xField="time_interval"
+                yField="total_devices"
+                color="#36cfc9"
+                label={{
+                  position: 'top',
+                  style: {
+                    fill: '#000',
+                  },
+                  text: (items: Record<string, any>) => {
+                    let content = items['time_interval'];
+                    content += `\n(${(items['total_devices'])})`;
+                    return content;
+                  },
+                }}
+                xAxis={{
+                  label: {
+                    style: {
+                      fill: '#000',
+                    },
+                  },
+                }}
+                yAxis={{
+                  label: {
+                    style: {
+                      fill: '#000',
+                    },
+                  },
+                }}
+                autoFit={true}
+                interaction={{
+                  tooltip: false
+                }}
+              />
+            )}
+          </div>
+        </Card>
+      </div>
+    );
+  };

+ 364 - 0
client/admin/pages_rack.tsx

@@ -0,0 +1,364 @@
+import React, { useState } from 'react';
+import { 
+Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Typography,
+  Switch, Badge, Image, 
+  Popconfirm, Tag, 
+} from 'antd';
+import { 
+  useQuery
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  ZichanInfo, RackServerType,
+  RackInfo, RackServer
+} from '../share/monitorTypes.ts';
+
+import {
+  ServerType, ServerTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import { EnableStatus, EnableStatusNameMap } from '../share/types.ts'
+
+import { RackAPI, RackServerAPI , RackServerTypeAPI, ZichanAPI, getOssUrl} from './api/index.ts';
+
+const { Title } = Typography;
+// 机柜管理页面
+export const RackManagePage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    rack_name: '',
+    rack_code: '',
+    area: ''
+  });
+  
+  // 查询机柜列表
+  const { 
+    data: rackResult = { data: [], pagination: { total: 0, current: 1, pageSize: 10 } }, 
+    isLoading: isFetching,
+    refetch
+  } = useQuery({
+    queryKey: ['racks', searchParams],
+    queryFn: () => RackAPI.getRackList(searchParams),
+  });
+  
+  // 提取数据和分页信息
+  const rackList = rackResult.data || [];
+  const pagination = rackResult.pagination || { total: 0, current: 1, pageSize: 10 };
+  
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<RackInfo>) => {
+    try {
+      setIsLoading(true);
+      if (formMode === 'create') {
+        await RackAPI.createRack(values);
+        message.success('机柜创建成功');
+      } else {
+        if (editingId) {
+          await RackAPI.updateRack(editingId, values);
+          message.success('机柜更新成功');
+        }
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '操作失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理编辑
+  const handleEdit = async (id: number) => {
+    try {
+      setIsLoading(true);
+      const data = await RackAPI.getRack(id);
+      form.setFieldsValue(data);
+      setEditingId(id);
+      setFormMode('edit');
+      setModalVisible(true);
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '获取机柜详情失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await RackAPI.deleteRack(id);
+      message.success('机柜删除成功');
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '删除机柜失败');
+    }
+  };
+  
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      ...searchParams,
+      page: 1, // 重置为第一页
+      rack_name: values.rack_name,
+      rack_code: values.rack_code,
+      area: values.area
+    });
+  };
+  
+  // 处理页码变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams({
+      ...searchParams,
+      page,
+      limit: pageSize || 10
+    });
+  };
+  
+  // 处理新增
+  const handleAdd = () => {
+    form.resetFields();
+    setFormMode('create');
+    setEditingId(null);
+    setModalVisible(true);
+  };
+  
+  // 表格列定义
+  const columns = [
+    { 
+      title: '机柜ID', 
+      dataIndex: 'id', 
+      key: 'id',
+      width: 80
+    },
+    { 
+      title: '机柜名称', 
+      dataIndex: 'rack_name', 
+      key: 'rack_name' 
+    },
+    { 
+      title: '机柜编号', 
+      dataIndex: 'rack_code', 
+      key: 'rack_code' 
+    },
+    { 
+      title: '区域', 
+      dataIndex: 'area', 
+      key: 'area' 
+    },
+    { 
+      title: '机房', 
+      dataIndex: 'room', 
+      key: 'room' 
+    },
+    { 
+      title: '容量(U)', 
+      dataIndex: 'capacity', 
+      key: 'capacity' 
+    },
+    { 
+      title: '状态', 
+      dataIndex: 'is_disabled', 
+      key: 'is_disabled',
+      render: (status: EnableStatus) => (
+        <Badge 
+          status={status === EnableStatus.ENABLED ? 'success' : 'error'} 
+          text={status === EnableStatus.ENABLED ? '正常' : '禁用'}
+        />
+      )
+    },
+    { 
+      title: '创建时间', 
+      dataIndex: 'created_at', 
+      key: 'created_at',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: any, record: RackInfo) => (
+        <Space>
+          <Button size="small" type="primary" onClick={() => handleEdit(record.id)}>编辑</Button>
+          <Button size="small" danger onClick={() => 
+            Modal.confirm({
+              title: '确认删除',
+              content: `确定要删除机柜"${record.rack_name}"吗?`,
+              onOk: () => handleDelete(record.id)
+            })
+          }>删除</Button>
+        </Space>
+      )
+    }
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>机柜管理</Title>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+          <Form.Item name="rack_name" label="机柜名称">
+            <Input placeholder="请输入机柜名称" allowClear />
+          </Form.Item>
+          <Form.Item name="rack_code" label="机柜编号">
+            <Input placeholder="请输入机柜编号" allowClear />
+          </Form.Item>
+          <Form.Item name="area" label="区域">
+            <Input placeholder="请输入区域" allowClear />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">查询</Button>
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" onClick={handleAdd}>添加机柜</Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={rackList}
+          rowKey="id"
+          loading={isFetching}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条记录`
+          }}
+        />
+        
+        <Modal
+          title={formMode === 'create' ? '添加机柜' : '编辑机柜'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          footer={null}
+          width={700}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+          >
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="rack_name"
+                  label="机柜名称"
+                  rules={[{ required: true, message: '请输入机柜名称' }]}
+                >
+                  <Input placeholder="请输入机柜名称" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="rack_code"
+                  label="机柜编号"
+                  rules={[{ required: true, message: '请输入机柜编号' }]}
+                >
+                  <Input placeholder="请输入机柜编号" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="area"
+                  label="区域"
+                >
+                  <Input placeholder="请输入区域" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="room"
+                  label="机房"
+                >
+                  <Input placeholder="请输入机房" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="capacity"
+                  label="容量(U)"
+                  initialValue={42}
+                  rules={[{ required: true, message: '请输入容量' }]}
+                >
+                  <Input type="number" placeholder="请输入容量" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="is_disabled"
+                  label="状态"
+                  initialValue={EnableStatus.ENABLED}
+                >
+                  <Select>
+                    <Select.Option value={EnableStatus.ENABLED}>正常</Select.Option>
+                    <Select.Option value={EnableStatus.DISABLED}>禁用</Select.Option>
+                  </Select>
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Item
+                  name="position_x"
+                  label="X轴位置"
+                >
+                  <Input type="number" placeholder="请输入X轴位置" />
+                </Form.Item>
+              </Col>
+              <Col span={8}>
+                <Form.Item
+                  name="position_y"
+                  label="Y轴位置"
+                >
+                  <Input type="number" placeholder="请输入Y轴位置" />
+                </Form.Item>
+              </Col>
+              <Col span={8}>
+                <Form.Item
+                  name="position_z"
+                  label="Z轴位置"
+                >
+                  <Input type="number" placeholder="请输入Z轴位置" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Form.Item
+              name="remark"
+              label="备注信息"
+            >
+              <Input.TextArea rows={3} placeholder="请输入备注信息" />
+            </Form.Item>
+            
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit" loading={isLoading}>
+                  {formMode === 'create' ? '创建' : '保存'}
+                </Button>
+                <Button onClick={() => setModalVisible(false)}>取消</Button>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </Card>
+    </div>
+  );
+};

+ 414 - 0
client/admin/pages_rack_server.tsx

@@ -0,0 +1,414 @@
+import React, { useState } from 'react';
+import { 
+Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Typography, Badge
+} from 'antd';
+import { 
+  useQuery
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  ZichanInfo, RackInfo, RackServer
+} from '../share/monitorTypes.ts';
+
+import {
+  ServerType, ServerTypeNameMap
+} from '../share/monitorTypes.ts';
+
+import { EnableStatus } from '../share/types.ts'
+
+import { RackAPI, RackServerAPI , ZichanAPI} from './api/index.ts';
+
+const { Title } = Typography;
+
+// 机柜服务器管理页面
+export const RackServerPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    rack_id: undefined as number | undefined,
+    server_type: undefined as string | undefined
+  });
+  
+  // 获取机柜列表用于下拉选择
+  const { data: rackOptions = [] } = useQuery({
+    queryKey: ['rackOptions'],
+    queryFn: async () => {
+      try {
+        const response = await RackAPI.getRackList({ limit: 100 });
+        return response.data.map((rack: RackInfo) => ({
+          label: `${rack.rack_name || ''} (${rack.rack_code || ''})`,
+          value: rack.id
+        }));
+      } catch (error) {
+        console.error('获取机柜列表失败:', error);
+        return [];
+      }
+    }
+  });
+  
+  // 获取资产列表用于下拉选择
+  const { data: assetOptions = [] } = useQuery({
+    queryKey: ['assetOptions'],
+    queryFn: async () => {
+      try {
+        const response = await ZichanAPI.getZichanList({ limit: 100 });
+        return response.data.map((asset: ZichanInfo) => ({
+          label: `${asset.asset_name || ''} (ID:${asset.id})`,
+          value: asset.id
+        }));
+      } catch (error) {
+        console.error('获取资产列表失败:', error);
+        return [];
+      }
+    }
+  });
+  
+  // 服务器类型选项
+  const serverTypeOptions = [
+    { label: ServerTypeNameMap[ServerType.STANDARD], value: ServerType.STANDARD },
+    { label: ServerTypeNameMap[ServerType.NETWORK], value: ServerType.NETWORK },
+    { label: ServerTypeNameMap[ServerType.STORAGE], value: ServerType.STORAGE },
+    { label: ServerTypeNameMap[ServerType.SPECIAL], value: ServerType.SPECIAL }
+  ];
+  
+  // 查询机柜服务器列表
+  const { 
+    data: rackServerResult = { data: [], pagination: { total: 0, current: 1, pageSize: 10 } }, 
+    isLoading: isFetching,
+    refetch
+  } = useQuery({
+    queryKey: ['rackServers', searchParams],
+    queryFn: () => RackServerAPI.getRackServerList(searchParams),
+  });
+  
+  // 提取数据和分页信息
+  const rackServerList = rackServerResult.data || [];
+  const pagination = rackServerResult.pagination || { total: 0, current: 1, pageSize: 10 };
+  
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<RackServer>) => {
+    try {
+      setIsLoading(true);
+      if (formMode === 'create') {
+        await RackServerAPI.createRackServer(values);
+        message.success('机柜服务器创建成功');
+      } else {
+        if (editingId) {
+          await RackServerAPI.updateRackServer(editingId, values);
+          message.success('机柜服务器更新成功');
+        }
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '操作失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理编辑
+  const handleEdit = async (id: number) => {
+    try {
+      setIsLoading(true);
+      const data = await RackServerAPI.getRackServer(id);
+      form.setFieldsValue(data);
+      setEditingId(id);
+      setFormMode('edit');
+      setModalVisible(true);
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '获取机柜服务器详情失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await RackServerAPI.deleteRackServer(id);
+      message.success('机柜服务器删除成功');
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '删除机柜服务器失败');
+    }
+  };
+  
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      ...searchParams,
+      page: 1, // 重置为第一页
+      rack_id: values.rack_id,
+      server_type: values.server_type
+    });
+  };
+  
+  // 处理页码变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams({
+      ...searchParams,
+      page,
+      limit: pageSize || 10
+    });
+  };
+  
+  // 处理新增
+  const handleAdd = () => {
+    form.resetFields();
+    setFormMode('create');
+    setEditingId(null);
+    setModalVisible(true);
+  };
+  
+  // 表格列定义
+  const columns = [
+    { 
+      title: 'ID', 
+      dataIndex: 'id', 
+      key: 'id',
+      width: 60
+    },
+    { 
+      title: '机柜', 
+      dataIndex: 'rack_id', 
+      key: 'rack_id',
+      render: (rackId: number) => {
+        const rack = rackOptions.find((r: any) => r.value === rackId);
+        return rack ? rack.label : `ID:${rackId}`;
+      }
+    },
+    { 
+      title: '资产', 
+      dataIndex: 'asset_id', 
+      key: 'asset_id',
+      render: (assetId: number) => {
+        const asset = assetOptions.find((a: any) => a.value === assetId);
+        return asset ? asset.label : `ID:${assetId}`;
+      }
+    },
+    { 
+      title: '起始U位', 
+      dataIndex: 'start_position', 
+      key: 'start_position' 
+    },
+    { 
+      title: '占用U数', 
+      dataIndex: 'size', 
+      key: 'size' 
+    },
+    { 
+      title: '服务器类型', 
+      dataIndex: 'server_type', 
+      key: 'server_type',
+      render: (type: ServerType) => ServerTypeNameMap[type] || '未知类型'
+    },
+    { 
+      title: '状态', 
+      dataIndex: 'is_disabled', 
+      key: 'is_disabled',
+      render: (status: EnableStatus) => (
+        <Badge 
+          status={status === EnableStatus.ENABLED ? 'success' : 'error'} 
+          text={status === EnableStatus.ENABLED ? '正常' : '禁用'}
+        />
+      )
+    },
+    { 
+      title: '创建时间', 
+      dataIndex: 'created_at', 
+      key: 'created_at',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: any, record: RackServer) => (
+        <Space>
+          <Button size="small" type="primary" onClick={() => handleEdit(record.id)}>编辑</Button>
+          <Button size="small" danger onClick={() => 
+            Modal.confirm({
+              title: '确认删除',
+              content: '确定要删除此机柜服务器记录吗?',
+              onOk: () => handleDelete(record.id)
+            })
+          }>删除</Button>
+        </Space>
+      )
+    }
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>机柜服务器管理</Title>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+          <Form.Item name="rack_id" label="机柜">
+            <Select
+              placeholder="请选择机柜"
+              style={{ width: 200 }}
+              allowClear
+              options={rackOptions}
+              showSearch
+              filterOption={(input, option) =>
+                (String(option?.label ?? '')).toLowerCase().includes(input.toLowerCase())
+              }
+            />
+          </Form.Item>
+          <Form.Item name="server_type" label="服务器类型">
+            <Select
+              placeholder="请选择服务器类型"
+              style={{ width: 160 }}
+              allowClear
+              options={serverTypeOptions}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">查询</Button>
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" onClick={handleAdd}>添加服务器</Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={rackServerList}
+          rowKey="id"
+          loading={isFetching}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条记录`
+          }}
+        />
+        
+        <Modal
+          title={formMode === 'create' ? '添加机柜服务器' : '编辑机柜服务器'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          footer={null}
+          width={700}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+          >
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="rack_id"
+                  label="选择机柜"
+                  rules={[{ required: true, message: '请选择机柜' }]}
+                >
+                  <Select
+                    placeholder="请选择机柜"
+                    options={rackOptions}
+                    showSearch
+                    filterOption={(input, option) =>
+                      (String(option?.label ?? '')).toLowerCase().includes(input.toLowerCase())
+                    }
+                  />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="asset_id"
+                  label="选择资产"
+                  rules={[{ required: true, message: '请选择资产' }]}
+                >
+                  <Select
+                    placeholder="请选择资产"
+                    options={assetOptions}
+                    showSearch
+                    filterOption={(input, option) =>
+                      (String(option?.label ?? '')).toLowerCase().includes(input.toLowerCase())
+                    }
+                  />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="start_position"
+                  label="起始U位"
+                  rules={[{ required: true, message: '请输入起始U位' }]}
+                >
+                  <Input type="number" placeholder="请输入起始U位" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="size"
+                  label="占用U数"
+                  initialValue={1}
+                  rules={[{ required: true, message: '请输入占用U数' }]}
+                >
+                  <Input type="number" placeholder="请输入占用U数" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="server_type"
+                  label="服务器类型"
+                  initialValue={ServerType.STANDARD}
+                >
+                  <Select
+                    placeholder="请选择服务器类型"
+                    options={serverTypeOptions}
+                  />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="is_disabled"
+                  label="状态"
+                  initialValue={EnableStatus.ENABLED}
+                >
+                  <Select>
+                    <Select.Option value={EnableStatus.ENABLED}>正常</Select.Option>
+                    <Select.Option value={EnableStatus.DISABLED}>禁用</Select.Option>
+                  </Select>
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Form.Item
+              name="remark"
+              label="备注信息"
+            >
+              <Input.TextArea rows={3} placeholder="请输入备注信息" />
+            </Form.Item>
+            
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit" loading={isLoading}>
+                  {formMode === 'create' ? '创建' : '保存'}
+                </Button>
+                <Button onClick={() => setModalVisible(false)}>取消</Button>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </Card>
+    </div>
+  );
+};

+ 280 - 0
client/admin/pages_rack_server_type.tsx

@@ -0,0 +1,280 @@
+import React, { useState } from 'react';
+import { 
+Button, Table, Space,
+  Form, Input, message, Modal,
+  Card, Typography,
+  Switch, Image, 
+  Popconfirm, Tag, 
+} from 'antd';
+import { 
+  useQuery
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  RackServerType,
+} from '../share/monitorTypes.ts';
+
+
+import { EnableStatus, EnableStatusNameMap } from '../share/types.ts'
+
+import { RackServerTypeAPI, getOssUrl} from './api/index.ts';
+
+const { Title } = Typography;
+
+// 机柜服务器类型管理页面组件
+export const RackServerTypePage = () => {
+  const [form] = Form.useForm();
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalMode, setModalMode] = useState<'create' | 'edit'>('create');
+  const [currentRecord, setCurrentRecord] = useState<RackServerType | null>(null);
+  const [loading, setLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    name: '',
+    code: ''
+  });
+
+  // 获取机柜服务器类型列表
+  const { data, isLoading, refetch } = useQuery({
+    queryKey: ['rack-server-types', searchParams],
+    queryFn: async () => {
+      const response = await RackServerTypeAPI.getRackServerTypeList(searchParams);
+      return response;
+    }
+  });
+
+  console.log('data', data);
+
+  // 创建/编辑机柜服务器类型
+  const handleSubmit = async (values: any) => {
+    try {
+      setLoading(true);
+      if (modalMode === 'create') {
+        await RackServerTypeAPI.createRackServerType(values);
+        message.success('创建成功');
+      } else {
+        await RackServerTypeAPI.updateRackServerType(currentRecord!.id, values);
+        message.success('更新成功');
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error) {
+      message.error('操作失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 删除机柜服务器类型
+  const handleDelete = async (id: number) => {
+    try {
+      await RackServerTypeAPI.deleteRackServerType(id);
+      message.success('删除成功');
+      refetch();
+    } catch (error) {
+      message.error('删除失败');
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      ...searchParams,
+      ...values,
+      page: 1
+    });
+  };
+
+  // 处理分页
+  const handleTableChange = (pagination: any) => {
+    setSearchParams({
+      ...searchParams,
+      page: pagination.current,
+      limit: pagination.pageSize
+    });
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+    },
+    {
+      title: '类型名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '类型编码',
+      dataIndex: 'code',
+      key: 'code',
+    },
+    {
+      title: '类型图片',
+      dataIndex: 'image_url',
+      key: 'image_url',
+      render: (url: string) => url ? <Image width={50} src={getOssUrl(url)} /> : '-'
+    },
+    {
+      title: '状态',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (enabled: EnableStatus) => (
+        <Tag color={enabled === EnableStatus.ENABLED ? 'green' : 'red'}>
+          {EnableStatusNameMap[enabled]}
+        </Tag>
+      )
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: RackServerType) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => {
+            setCurrentRecord(record);
+            setModalMode('edit');
+            form.setFieldsValue(record);
+            setModalVisible(true);
+          }}>
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除吗?"
+            onConfirm={() => handleDelete(record.id)}
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+
+  return (
+    <div>
+      <div className="flex justify-between items-center mb-4">
+        <Title level={2}>机柜服务器类型管理</Title>
+        <Button 
+          type="primary" 
+          onClick={() => {
+            setModalMode('create');
+            form.resetFields();
+            setModalVisible(true);
+          }}
+        >
+          新增类型
+        </Button>
+      </div>
+
+      <Card>
+        <Form
+          layout="inline"
+          onFinish={handleSearch}
+          className="mb-4"
+        >
+          <Form.Item name="name" label="类型名称">
+            <Input placeholder="请输入类型名称" />
+          </Form.Item>
+          <Form.Item name="code" label="类型编码">
+            <Input placeholder="请输入类型编码" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">
+              搜索
+            </Button>
+          </Form.Item>
+        </Form>
+
+        <Table
+          columns={columns}
+          dataSource={data?.data}
+          loading={isLoading}
+          rowKey="id"
+          pagination={{
+            current: searchParams.page,
+            pageSize: searchParams.limit,
+            total: data?.pagination?.total,
+            showSizeChanger: true,
+            showQuickJumper: true
+          }}
+          onChange={handleTableChange}
+        />
+      </Card>
+
+      <Modal
+        title={modalMode === 'create' ? '新增类型' : '编辑类型'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSubmit}
+        >
+          <Form.Item
+            name="name"
+            label="类型名称"
+            rules={[{ required: true, message: '请输入类型名称' }]}
+          >
+            <Input placeholder="请输入类型名称" />
+          </Form.Item>
+
+          <Form.Item
+            name="code"
+            label="类型编码"
+            rules={[{ required: true, message: '请输入类型编码' }]}
+          >
+            <Input placeholder="请输入类型编码" />
+          </Form.Item>
+
+          <Form.Item
+            name="image_url"
+            label="类型图片"
+          >
+            <Input placeholder="请输入图片URL" />
+          </Form.Item>
+
+          <Form.Item
+            name="description"
+            label="类型描述"
+          >
+            <Input.TextArea placeholder="请输入类型描述" />
+          </Form.Item>
+
+          <Form.Item
+            name="is_enabled"
+            label="状态"
+            initialValue={EnableStatus.ENABLED}
+          >
+            <Switch
+              checkedChildren="启用"
+              unCheckedChildren="禁用"
+              defaultChecked
+            />
+          </Form.Item>
+
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit" loading={loading}>
+                确定
+              </Button>
+              <Button onClick={() => setModalVisible(false)}>
+                取消
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 298 - 0
client/admin/pages_settings.tsx

@@ -0,0 +1,298 @@
+import React, { useEffect } from 'react';
+import { 
+  Button,Space,
+  Form, Input, Select, message, Modal,
+  Card, Spin, Typography,
+  Switch, Tabs, Alert, InputNumber
+} from 'antd';
+import {
+  ReloadOutlined,
+  SaveOutlined,
+} from '@ant-design/icons';
+import { 
+  useQuery,
+  useMutation,
+  useQueryClient,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+import type { 
+  SystemSetting, SystemSettingValue
+} from '../share/types.ts';
+
+import {
+  SystemSettingGroup,
+  SystemSettingKey,
+  AllowedFileType
+} from '../share/types.ts';
+
+
+
+import {
+  SystemAPI
+} from './api/index.ts';
+
+import { useTheme } from './hooks_sys.tsx';
+import { validateUrl, validateAuthHeader } from './utils.ts';
+
+import { Uploader } from './components_uploader.tsx';
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+// 设置 dayjs 语言
+dayjs.locale('zh-cn');
+
+const { Title } = Typography;
+
+// 分组标题映射
+const GROUP_TITLES: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
+  [SystemSettingGroup.BASIC]: '基础设置',
+  [SystemSettingGroup.FEATURE]: '功能设置',
+  [SystemSettingGroup.UPLOAD]: '上传设置',
+  [SystemSettingGroup.NOTIFICATION]: '通知设置'
+};
+
+// 分组描述映射
+const GROUP_DESCRIPTIONS: Record<typeof SystemSettingGroup[keyof typeof SystemSettingGroup], string> = {
+  [SystemSettingGroup.BASIC]: '配置站点的基本信息',
+  [SystemSettingGroup.FEATURE]: '配置系统功能的开启状态',
+  [SystemSettingGroup.UPLOAD]: '配置文件上传相关的参数',
+  [SystemSettingGroup.NOTIFICATION]: '配置系统通知的触发条件'
+};
+
+export const SettingsPage = () => {
+  const [form] = Form.useForm();
+  const queryClient = useQueryClient();
+  const { isDark } = useTheme();
+
+  // 获取系统设置
+  const { data: settingsData, isLoading: isLoadingSettings } = useQuery({
+    queryKey: ['systemSettings'],
+    queryFn: SystemAPI.getSettings,
+  });
+
+  // 更新系统设置
+  const updateSettingsMutation = useMutation({
+    mutationFn: (values: Partial<SystemSetting>[]) => SystemAPI.updateSettings(values),
+    onSuccess: () => {
+      message.success('基础设置已更新');
+      queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
+    },
+    onError: (error) => {
+      message.error('更新基础设置失败');
+      console.error('更新基础设置失败:', error);
+    },
+  });
+
+  // 重置系统设置
+  const resetSettingsMutation = useMutation({
+    mutationFn: SystemAPI.resetSettings,
+    onSuccess: () => {
+      message.success('基础设置已重置');
+      queryClient.invalidateQueries({ queryKey: ['systemSettings'] });
+    },
+    onError: (error) => {
+      message.error('重置基础设置失败');
+      console.error('重置基础设置失败:', error);
+    },
+  });
+
+  // 初始化表单数据
+  useEffect(() => {
+    if (settingsData) {
+      const formValues = settingsData.reduce((acc: Record<string, any>, group) => {
+        group.settings.forEach(setting => {
+          // 根据值的类型进行转换
+          let value = setting.value;
+          if (typeof value === 'string') {
+            if (value === 'true' || value === 'false') {
+              value = value === 'true';
+            } else if (!isNaN(Number(value)) && !value.includes('.')) {
+              value = parseInt(value, 10);
+            } else if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
+              value = (value ? (value as string).split(',') : []) as unknown as string;
+            }
+          }
+          acc[setting.key] = value;
+        });
+        return acc;
+      }, {});
+      form.setFieldsValue(formValues);
+    }
+  }, [settingsData, form]);
+
+  // 处理表单提交
+  const handleSubmit = async (values: Record<string, SystemSettingValue>) => {
+    const settings = Object.entries(values).map(([key, value]) => ({
+      key: key as typeof SystemSettingKey[keyof typeof SystemSettingKey],
+      value: String(value),
+      group: key.startsWith('SITE_') ? SystemSettingGroup.BASIC :
+             key.startsWith('ENABLE_') || key.includes('LOGIN_') || key.includes('SESSION_') ? SystemSettingGroup.FEATURE :
+             key.includes('UPLOAD_') || key.includes('FILE_') || key.includes('IMAGE_') ? SystemSettingGroup.UPLOAD :
+             SystemSettingGroup.NOTIFICATION
+    }));
+    updateSettingsMutation.mutate(settings);
+  };
+
+  // 处理重置
+  const handleReset = () => {
+    Modal.confirm({
+      title: '确认重置',
+      content: '确定要将所有设置重置为默认值吗?此操作不可恢复。',
+      okText: '确认',
+      cancelText: '取消',
+      onOk: () => {
+        resetSettingsMutation.mutate();
+      },
+    });
+  };
+
+  // 根据设置类型渲染不同的输入控件
+  const renderSettingInput = (setting: SystemSetting) => {
+    const value = setting.value;
+    
+    if (typeof value === 'boolean' || value === 'true' || value === 'false') {
+      return <Switch checkedChildren="开启" unCheckedChildren="关闭" />;
+    }
+    
+    if (setting.key === SystemSettingKey.ALLOWED_FILE_TYPES) {
+      return <Select
+        mode="tags"
+        placeholder="请输入允许的文件类型"
+        tokenSeparators={[',']}
+        options={Object.values(AllowedFileType).map(type => ({
+          label: type.toUpperCase(),
+          value: type
+        }))}
+      />;
+    }
+    
+    if (setting.key.includes('MAX_SIZE') || setting.key.includes('ATTEMPTS') ||
+        setting.key.includes('TIMEOUT') || setting.key.includes('MAX_WIDTH') ||
+        setting.key === 'SMS_API_TIMEOUT' || setting.key === 'SMS_API_RETRY') {
+      return <InputNumber min={1} style={{ width: '100%' }} />;
+    }
+    
+    if (setting.key === SystemSettingKey.SITE_LOGO || setting.key === SystemSettingKey.SITE_FAVICON) {
+      return (
+        <div>
+          {value && <img src={String(value)} alt="图片" style={{ width: 100, height: 100, objectFit: 'contain', marginBottom: 8 }} />}
+          <div style={{ width: 100 }}>
+            <Uploader
+              maxSize={2 * 1024 * 1024}
+              prefix={setting.key === SystemSettingKey.SITE_LOGO ? 'logo/' : 'favicon/'}
+              allowedTypes={['image/jpeg', 'image/png', 'image/svg+xml', 'image/x-icon']}
+              onSuccess={(fileUrl) => {
+                form.setFieldValue(setting.key, fileUrl);
+                updateSettingsMutation.mutate([{
+                  key: setting.key,
+                  value: fileUrl,
+                  group: SystemSettingGroup.BASIC
+                }]);
+              }}
+              onError={(error) => {
+                message.error(`上传失败:${error.message}`);
+              }}
+            />
+          </div>
+        </div>
+      );
+    }
+
+    if (setting.key === 'SMS_API_URL') {
+      return <Input placeholder="请输入短信接口URL" />;
+    }
+
+    if (setting.key === 'SMS_API_AUTH') {
+      return <Input.Password placeholder="请输入Basic Auth认证信息" />;
+    }
+    
+    return <Input placeholder={`请输入${setting.description || setting.key}`} />;
+  };
+
+  return (
+    <div>
+      <Card
+        title={
+          <Space>
+            <Title level={2} style={{ margin: 0 }}>系统设置</Title>
+          </Space>
+        }
+        extra={
+          <Space>
+            <Button 
+              icon={<ReloadOutlined />}
+              onClick={handleReset}
+              loading={resetSettingsMutation.isPending}
+            >
+              重置默认
+            </Button>
+          </Space>
+        }
+      >
+        <Spin spinning={isLoadingSettings || updateSettingsMutation.isPending}>
+          <Tabs
+            type="card"
+            items={Object.values(SystemSettingGroup).map(group => ({
+              key: group,
+              label: String(GROUP_TITLES[group]),
+                children: (
+                  <div>
+                    <Alert
+                    message={GROUP_DESCRIPTIONS[group]}
+                      type="info"
+                      showIcon
+                      style={{ marginBottom: 24 }}
+                    />
+                    <Form
+                      form={form}
+                      layout="vertical"
+                      onFinish={handleSubmit}
+                    >
+                    {settingsData
+                      ?.find(g => g.name === group)
+                      ?.settings.map(setting => (
+                      <Form.Item
+                          key={setting.key}
+                          label={setting.description || setting.key}
+                          name={setting.key}
+                          rules={[
+                            { required: true, message: `请输入${setting.description || setting.key}` },
+                            ...(setting.key === 'SMS_API_URL' ? [{
+                              validator: (_: unknown, value: string) =>
+                                validateUrl(value) ? Promise.resolve() : Promise.reject(new Error('请输入有效的URL'))
+                            }] : []),
+                            ...(setting.key === 'SMS_API_AUTH' ? [{
+                              validator: (_: unknown, value: string) =>
+                                validateAuthHeader(value) ? Promise.resolve() : Promise.reject(new Error('格式应为Basic base64字符串'))
+                            }] : [])
+                          ]}
+                        >
+                          {renderSettingInput(setting)}
+                      </Form.Item>
+                      ))}
+                      <Form.Item>
+                        <Button
+                          type="primary"
+                          htmlType="submit"
+                          icon={<SaveOutlined />}
+                          loading={updateSettingsMutation.isPending}
+                        >
+                          保存设置
+                        </Button>
+                      </Form.Item>
+                    </Form>
+                  </div>
+                )
+            }))}
+          />
+        </Spin>
+      </Card>
+    </div>
+  );
+};
+

+ 306 - 0
client/admin/pages_smoke_water.tsx

@@ -0,0 +1,306 @@
+import React, { useState, useEffect, useRef } from 'react';
+import { getDeviceStatus, getDeviceHistory } from './api/monitor.ts';
+import dayjs from 'dayjs';
+import { Card, Space, Table, message, Select, Tag, Tabs, Button } from 'antd';
+import ReactECharts from 'echarts-for-react';
+import type { EChartsType } from 'echarts';
+
+interface DeviceStatusResponse {
+  status: 0 | 1;
+  timestamp: string;
+}
+
+interface DeviceData {
+  id: string;
+  name: string;
+  type: 'smoke' | 'water';
+  status: 0 | 1; // 1:正常, 0:异常
+  timestamp: string;
+  value?: number;
+}
+
+interface HistoryData {
+  status: 0 | 1;
+  timestamp: string;
+}
+
+interface WaterIconProps {
+  timestamp?: string;
+}
+
+const WaterIcon = () => (
+  <div style={{position: 'relative', width: 80, height: 80}}>
+    <img
+      src="/client/shuijintubiao.png"
+      alt="水浸图标"
+      style={{width: '100%', height: '100%'}}
+    />
+  </div>
+);
+
+interface SmokeIconProps {
+  status?: 0 | 1;
+  timestamp?: string;
+}
+
+const SmokeIcon = ({status = 1}: SmokeIconProps) => (
+  <div style={{position: 'relative', width: 80, height: 80}}>
+    <img
+      src="/client/admin/api/yangantubiao.png"
+      alt="烟感图标"
+      style={{
+        width: '100%',
+        height: '100%',
+        border: status === 0 ? '2px solid #ff4d4f' : 'none',
+        backgroundColor: status === 0 ? 'rgba(255, 77, 79, 0.1)' : 'transparent',
+        borderRadius: 4
+      }}
+    />
+  </div>
+);
+
+const StatusDisplay = ({type, status}: {type: 'smoke' | 'water', status: 0 | 1}) => (
+  <div style={{
+    display: 'flex',
+    flexDirection: 'column',
+    justifyContent: 'center',
+    alignItems: 'center',
+    backgroundColor: status === 1 ? '#f6ffed' : '#fff2f0',
+    border: `1px solid ${status === 1 ? '#b7eb8f' : '#ffccc7'}`,
+    borderRadius: 4,
+    fontSize: 14,
+    fontWeight: 'bold',
+    padding: 8
+  }}>
+    <div>{type === 'smoke' ? '烟感' : '水浸'}</div>
+    <div style={{color: status === 1 ? '#52c41a' : '#f5222d'}}>
+      {status === 1 ? '正常' : '异常'}
+    </div>
+  </div>
+);
+
+const SmokeWaterPage: React.FC = () => {
+  const [devices, setDevices] = useState<DeviceData[]>([
+    {id: 'smoke1', name: '烟感1', type: 'smoke', status: 0, timestamp: new Date().toISOString()},
+    {id: 'smoke2', name: '烟感2', type: 'smoke', status: 0, timestamp: new Date().toISOString()},
+    {id: 'water1', name: '水浸1', type: 'water', status: 0, timestamp: new Date().toISOString()},
+    {id: 'water2', name: '水浸2', type: 'water', status: 0, timestamp: new Date().toISOString()}
+  ]);
+  const [loading, setLoading] = useState(false);
+  const [selectedDevice, setSelectedDevice] = useState<string>('smoke1');
+  const [activeTab, setActiveTab] = useState<'table' | 'chart'>('table');
+  const [isPolling, setIsPolling] = useState(true);
+  const [pollInterval, setPollInterval] = useState(30000); // 默认30秒
+  const chartRef = useRef<EChartsType>(null);
+
+  const fetchDeviceStatus = async () => {
+    try {
+      setLoading(true);
+      const res = await getDeviceStatus({
+        device_id: Number(selectedDevice.replace(/\D/g, '')),
+        device_type: selectedDevice.startsWith('smoke') ? 'smoke' : 'water'
+      });
+      setDevices(prev => prev.map(d =>
+        d.id === selectedDevice
+          ? {...d, status: res.status, timestamp: res.timestamp}
+          : d
+      ));
+    } catch (error) {
+      message.error('获取设备状态失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const fetchDeviceHistory = async (deviceId: string) => {
+    try {
+      const res = await getDeviceHistory({
+        device_id: Number(deviceId.replace(/\D/g, '')),
+        device_type: selectedDevice.startsWith('smoke') ? 'smoke' : 'water',
+        start_time: new Date(Date.now() - 86400000).toISOString(),
+        end_time: new Date().toISOString()
+      });
+      return res.data;
+    } catch (error) {
+      message.error('获取历史数据失败');
+      return [];
+    }
+  };
+
+  useEffect(() => {
+    fetchDeviceStatus();
+    if (!isPolling) return;
+    
+    const timer = setInterval(fetchDeviceStatus, pollInterval);
+    return () => clearInterval(timer);
+  }, [isPolling, pollInterval]);
+
+  useEffect(() => {
+    if (activeTab === 'chart' && selectedDevice && chartRef.current) {
+      fetchDeviceHistory(selectedDevice).then(data => {
+        const option = {
+          xAxis: {
+            type: 'category',
+            data: data.map((d: HistoryData) => new Date(d.timestamp).toLocaleTimeString())
+          },
+          yAxis: {
+            type: 'value',
+            min: 0,
+            max: 1,
+            interval: 1
+          },
+          series: [{
+            data: data.map((d: HistoryData) => d.status),
+            type: 'line',
+            smooth: true
+          }]
+        };
+        chartRef.current?.setOption(option);
+      });
+    }
+  }, [activeTab, selectedDevice]);
+
+  const columns = [
+    {
+      title: '设备名称',
+      dataIndex: 'name',
+      key: 'name',
+    },
+    {
+      title: '设备类型',
+      dataIndex: 'type',
+      key: 'type',
+      render: (type: string) => type === 'smoke' ? '烟感' : '水浸',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: number) => (
+        <Tag color={status === 1 ? 'green' : 'red'}>
+          {status === 1 ? '正常' : '异常'}
+        </Tag>
+      ),
+    },
+    {
+      title: '更新时间',
+      dataIndex: 'timestamp',
+      key: 'timestamp',
+      render: (text?: string) => text ? new Date(text).toLocaleString() : '-',
+    }
+  ];
+
+  const currentDevice = devices.find(d => d.id === selectedDevice);
+
+  return (
+    <div style={{ padding: 24 }}>
+      <Space direction="vertical" size="large" style={{ width: '100%' }}>
+        <Card
+          title={
+            <Space>
+              <Select
+                value={selectedDevice}
+                style={{ width: 200 }}
+                onChange={setSelectedDevice}
+                options={devices.map(d => ({
+                  value: d.id,
+                  label: d.name
+                }))}
+              />
+              <Button
+                type={isPolling ? 'default' : 'primary'}
+                onClick={() => setIsPolling(!isPolling)}
+              >
+                {isPolling ? '停止轮询' : '开始轮询'}
+              </Button>
+              <Select
+                value={pollInterval}
+                style={{ width: 120 }}
+                onChange={(value) => setPollInterval(value)}
+                options={[
+                  { value: 10000, label: '10秒' },
+                  { value: 30000, label: '30秒' },
+                  { value: 60000, label: '1分钟' }
+                ]}
+              />
+            </Space>
+          }
+        >
+          <div style={{
+            display: 'flex',
+            justifyContent: 'center',
+            alignItems: 'center',
+            height: 120
+          }}>
+            {currentDevice && (
+              <div style={{display: 'flex', gap: 16, position: 'relative', justifyContent: 'center', alignItems: 'center'}}>
+                {currentDevice.type === 'water' ? (
+                  <WaterIcon />
+                ) : (
+                  <SmokeIcon status={currentDevice.status} />
+                )}
+                <StatusDisplay type={currentDevice.type} status={currentDevice.status} />
+                <div style={{marginLeft: '100px'}}>
+                  <div style={{
+                    color: 'black',
+                    fontSize: 12,
+                    padding: '2px 8px',
+                    borderRadius: 4
+                  }}>
+                    时间
+                  </div>
+                  <div style={{
+                    color: 'black',
+                    fontSize: 12,
+                    padding: '2px 8px',
+                    borderRadius: 4,
+                    marginTop: 4
+                  }}>
+                    {dayjs(currentDevice.timestamp).format('YYYY/M/D HH:mm:ss')}
+                  </div>
+                </div>
+              </div>
+            )}
+          </div>
+        </Card>
+
+        <Tabs
+          activeKey={activeTab}
+          onChange={(key: string) => setActiveTab(key as 'table' | 'chart')}
+          items={[
+            {
+              key: 'table',
+              label: '数据表格',
+              children: (
+                <Table
+                  columns={columns}
+                  dataSource={devices}
+                  rowKey="id"
+                  loading={loading}
+                />
+              )
+            },
+            {
+              key: 'chart',
+              label: '趋势图表',
+              children: (
+                <ReactECharts
+                  // @ts-ignore - ReactECharts ref类型问题
+                  ref={chartRef}
+                  style={{ height: 400 }}
+                  option={{
+                    xAxis: { type: 'category' },
+                    yAxis: { type: 'value' },
+                    series: [{ type: 'line' }]
+                  }}
+                />
+              )
+            }
+          ]}
+        />
+      </Space>
+    </div>
+  );
+};
+
+export default SmokeWaterPage;

+ 134 - 0
client/admin/pages_sms.tsx

@@ -0,0 +1,134 @@
+/** @jsxImportSource react */
+import React, { useState } from 'react'
+import { useRequest } from './hooks_sys.tsx'
+import { Button, Card, Form, Input, Table, message } from 'antd'
+import type { ColumnsType } from 'antd/es/table'
+import { smsApi } from './api/sms.ts'
+
+// 配置JSX运行时
+import * as jsxRuntime from 'react/jsx-runtime'
+Object.assign(globalThis, {
+  React: React,
+  ...jsxRuntime
+})
+
+interface SmsItem {
+  id: string
+  phone: string
+  content: string
+  status: string
+  createdAt: string
+}
+
+export default function SmsPage() {
+  const [form] = Form.useForm()
+  const [activeTab, setActiveTab] = useState('send')
+  const [password, setPassword] = useState('')
+
+  // 登录状态检查
+  const { data: isLoggedIn } = useRequest(() => smsApi.checkLogin(), {
+    onError: () => message.error('请先登录短信服务')
+  })
+
+  // 发送短信
+  const { run: sendSms } = useRequest(
+    (values: { phone: string; content: string }) => smsApi.send(values),
+    {
+      manual: true,
+      onSuccess: () => message.success('短信发送任务已创建'),
+      onError: () => message.error('短信发送失败')
+    }
+  )
+
+  // 获取设备状态
+  const { data: deviceStatus } = useRequest(() => smsApi.getStatus(), {
+    ready: isLoggedIn,
+    pollingInterval: 5000
+  })
+
+  // 短信记录
+  const { data: smsList } = useRequest(() => smsApi.getList(), {
+    ready: isLoggedIn,
+    pollingInterval: 10000
+  })
+
+  // 登录
+  const handleLogin = async () => {
+    try {
+      await smsApi.login({ username: 'vsmsd', password })
+      message.success('登录成功')
+    } catch (error) {
+      message.error('登录失败')
+    }
+  }
+
+  const columns: ColumnsType<SmsItem> = [
+    { title: '任务ID', dataIndex: 'id' },
+    { title: '手机号', dataIndex: 'phone' },
+    { title: '内容', dataIndex: 'content' },
+    { title: '状态', dataIndex: 'status' },
+    { title: '发送时间', dataIndex: 'createdAt' }
+  ]
+
+  if (!isLoggedIn) {
+    return (
+      <Card title="短信服务登录">
+        <Form layout="vertical">
+          <Form.Item label="密码" required>
+            <Input.Password 
+              value={password}
+              onChange={(e) => setPassword(e.target.value)}
+              placeholder="请输入初始密码 Vsmsd123"
+            />
+          </Form.Item>
+          <Button type="primary" onClick={handleLogin}>
+            验证登录
+          </Button>
+        </Form>
+      </Card>
+    )
+  }
+
+  return (
+    <Card
+      tabList={[
+        { key: 'send', tab: '发送短信' },
+        { key: 'status', tab: '设备状态' },
+        { key: 'history', tab: '发送记录' }
+      ]}
+      activeTabKey={activeTab}
+      onTabChange={setActiveTab}
+    >
+      {activeTab === 'send' && (
+        <Form form={form} onFinish={sendSms}>
+          <Form.Item name="phone" label="手机号" rules={[{ required: true }]}>
+            <Input placeholder="请输入手机号" />
+          </Form.Item>
+          <Form.Item name="content" label="短信内容" rules={[{ required: true }]}>
+            <Input.TextArea rows={4} placeholder="请输入短信内容" />
+          </Form.Item>
+          <Button type="primary" htmlType="submit">
+            发送短信
+          </Button>
+        </Form>
+      )}
+
+      {activeTab === 'status' && deviceStatus && (
+        <div>
+          <p>信号强度: {deviceStatus.signalStrength}%</p>
+          <p>运营商: {deviceStatus.carrier}</p>
+          <p>当前模式: {deviceStatus.mode}</p>
+        </div>
+      )}
+
+      {activeTab === 'history' && (
+        <Table<SmsItem>
+          columns={columns}
+          dataSource={smsList}
+          rowKey="id"
+          pagination={false}
+        />
+      )}
+    </Card>
+  )
+}

+ 199 - 0
client/admin/pages_sms_module.tsx

@@ -0,0 +1,199 @@
+import React, { useState } from 'react';
+import { Button, Card, Space, Row, Col, Table, Form, Input, message } from 'antd';
+import { useNavigate } from 'react-router';
+
+export const SmsModulePage = () => {
+  const navigate = useNavigate();
+  const [deviceStatus, setDeviceStatus] = useState({
+    io_alive: false,
+    md_alive: false,
+    signal_rate: 0,
+    mno: '未知'
+  });
+  interface TaskStatus {
+    id: string;
+    status: string;
+    result?: string;
+    timestamp?: string;
+  }
+
+  const [taskStatus, setTaskStatus] = useState<TaskStatus | null>(null);
+  const [taskId, setTaskId] = useState<string>('');
+  const [phoneNumber, setPhoneNumber] = useState<string>('');
+  const [smsContent, setSmsContent] = useState<string>('');
+
+  const fetchDeviceStatus = async () => {
+    try {
+      const response = await fetch('http://vsmsd:Vsmsd123@52.55.129.63:12080/api/v1/info');
+      const data = await response.json();
+      setDeviceStatus({
+        io_alive: data.io_alive,
+        md_alive: data.md_alive,
+        signal_rate: data.signal_rate,
+        mno: data.mno
+      });
+      message.success('状态获取成功');
+    } catch (error) {
+      message.error('状态获取失败');
+      console.error('Error fetching device status:', error);
+    }
+  };
+
+  const sendSms = async () => {
+    // 验证手机号码
+    if (!/^1[3-9]\d{9}$/.test(phoneNumber)) {
+      message.error('请输入正确的11位手机号码');
+      return;
+    }
+    
+    // 验证短信内容
+    if (!smsContent.trim()) {
+      message.error('请输入短信内容');
+      return;
+    }
+
+    try {
+      const response = await fetch('http://vsmsd:Vsmsd123@52.55.129.63:12080/api/v1/sms', {
+        method: 'POST',
+        headers: {
+          'Content-Type': 'application/json',
+        },
+        body: JSON.stringify({
+          method: 'send_sms',
+          number: phoneNumber,
+          content: smsContent
+        })
+      });
+      
+      const data = await response.json();
+      setTaskId(data.task_id);
+      message.success('短信发送成功');
+    } catch (error) {
+      message.error('短信发送失败');
+      console.error('Error sending SMS:', error);
+    }
+  };
+
+  const fetchTaskStatus = async (taskId: string) => {
+    try {
+      const response = await fetch(`http://vsmsd:Vsmsd123@52.55.129.63:12080/api/v1/task/${taskId}`);
+      const data = await response.json();
+      setTaskStatus(data);
+      message.success('任务状态获取成功');
+    } catch (error) {
+      message.error('任务状态获取失败');
+      console.error('Error fetching task status:', error);
+    }
+  };
+
+  return (
+    <div className="p-4">
+      <div className="flex justify-between items-center mb-4">
+        <h1 className="text-2xl font-bold">短信模块管理</h1>
+      </div>
+
+      <Row gutter={16}>
+        <Col span={24}>
+          <Card title="功能测试面板" className="mb-4">
+            <Space direction="vertical" size="middle" style={{ width: '100%' }}>
+              <Row gutter={16}>
+                <Col span={12}>
+                  <Card title="设备状态">
+                    <p>信号强度: {deviceStatus.signal_rate}%</p>
+                    <p>运营商: {deviceStatus.mno}</p>
+                    <p>4G模块: {deviceStatus.md_alive ? '在线' : '离线'}</p>
+                    <Button 
+                      type="primary" 
+                      onClick={fetchDeviceStatus}
+                      style={{ marginTop: 16 }}
+                    >
+                      获取状态
+                    </Button>
+                  </Card>
+                </Col>
+                <Col span={12}>
+                  <Card title="模式选择">
+                    <Space>
+                      <Button type="primary">短信</Button>
+                      <Button>电话</Button>
+                      <Button>语音</Button>
+                      <Button>余额查询</Button>
+                      <Button>测试</Button>
+                    </Space>
+                  </Card>
+                </Col>
+              </Row>
+              
+              <Card title="短信测试">
+                <Form layout="vertical">
+                  <Form.Item label="号码" required>
+                    <Input
+                      placeholder="请输入11位手机号码"
+                      value={phoneNumber}
+                      onChange={(e) => setPhoneNumber(e.target.value)}
+                    />
+                  </Form.Item>
+                  <Form.Item label="内容" required>
+                    <Input.TextArea
+                      rows={4}
+                      placeholder="请输入短信内容"
+                      value={smsContent}
+                      onChange={(e) => setSmsContent(e.target.value)}
+                    />
+                  </Form.Item>
+                  <Form.Item label="任务ID">
+                    <Space.Compact style={{ width: '100%' }}>
+                      <Input
+                        placeholder="可手动输入或自动生成"
+                        value={taskId}
+                        onChange={(e) => setTaskId(e.target.value)}
+                      />
+                      <Button
+                        onClick={() => setTaskId(`TASK_${Date.now()}`)}
+                      >
+                        自动生成
+                      </Button>
+                      <Button
+                        onClick={() => setTaskId('')}
+                      >
+                        重置
+                      </Button>
+                    </Space.Compact>
+                  </Form.Item>
+                  <Space>
+                    <Button
+                      type="primary"
+                      onClick={sendSms}
+                    >
+                      发送短信
+                    </Button>
+                    <Button
+                      type="default"
+                      onClick={() => fetchTaskStatus(taskId)}
+                      disabled={!taskId}
+                    >
+                      获取任务状态
+                    </Button>
+                  </Space>
+                </Form>
+              </Card>
+              
+              <Card title="执行信息">
+                <Table
+                  columns={[
+                    { title: '序列号', dataIndex: 'id', key: 'id' },
+                    { title: '运营商', dataIndex: 'carrier', key: 'carrier' },
+                    { title: '时间', dataIndex: 'time', key: 'time' },
+                    { title: '结果', dataIndex: 'result', key: 'result' }
+                  ]}
+                  dataSource={[]}
+                  size="small"
+                />
+              </Card>
+            </Space>
+          </Card>
+        </Col>
+      </Row>
+    </div>
+  );
+};

+ 363 - 0
client/admin/pages_temperature_humidity.tsx

@@ -0,0 +1,363 @@
+import React, { useState, useEffect, useRef } from 'react';
+import ReactECharts from 'echarts-for-react';
+import type { EChartsOption } from 'echarts';
+import { Card, Statistic, Space, Table, message, Select, Button, DatePicker, Radio, Tabs, Tag } from 'antd';
+import { MonitorOutlined, EnvironmentOutlined, CloudOutlined, LineChartOutlined } from '@ant-design/icons';
+import dayjs, { Dayjs } from 'dayjs';
+import { MonitorAPI, getLatestTemperatureHumidity } from './api/monitor.ts';
+import { DeviceMonitorData } from '../share/monitorTypes.ts';
+
+interface SensorData {
+  timestamp: string;
+  temperature?: number;
+  humidity?: number;
+  metric_type: string;
+}
+
+const TemperatureHumidityPage: React.FC = () => {
+  const [currentData, setCurrentData] = useState<SensorData | null>(null);
+  const [tableData, setTableData] = useState<SensorData[]>([]);
+  const [loading, setLoading] = useState(false);
+  const [selectedDevice, setSelectedDevice] = useState('device1');
+  const [timeRange, setTimeRange] = useState<'4h' | '12h' | '1d' | '7d' | 'custom'>('4h');
+  const [customDateRange, setCustomDateRange] = useState<[dayjs.Dayjs, dayjs.Dayjs]>();
+  const [activeTab, setActiveTab] = useState('table');
+  const [isPolling, setIsPolling] = useState(true);
+  const [pollInterval, setPollInterval] = useState(30000); // 默认30秒
+  const chartRef = useRef<typeof ReactECharts | null>(null);
+  const fetchData = async () => {
+    try {
+      setLoading(true);
+      
+      // 获取最新温湿度数据
+      const latestData = await getLatestTemperatureHumidity();
+      setCurrentData({
+        timestamp: latestData.timestamp,
+        temperature: latestData.temperature,
+        humidity: latestData.humidity,
+        metric_type: 'combined'
+      });
+
+      let startTime: Date, endTime = new Date();
+      
+      if (timeRange === '4h') {
+        startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
+      } else if (timeRange === '12h') {
+        startTime = new Date(Date.now() - 12 * 60 * 60 * 1000);
+      } else if (timeRange === '1d') {
+        startTime = new Date(Date.now() - 24 * 60 * 60 * 1000);
+      } else if (timeRange === '7d') {
+        startTime = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
+      } else if (customDateRange) {
+        startTime = customDateRange[0].toDate();
+        endTime = customDateRange[1].toDate();
+      } else {
+        // Default to 4h if no range is selected
+        startTime = new Date(Date.now() - 4 * 60 * 60 * 1000);
+      }
+
+      const response = await MonitorAPI.getMonitorData({
+        device_type: 'temperature_humidity',
+        start_time: startTime.toISOString(),
+        end_time: endTime.toISOString()
+      });
+      if (response.data.length > 0) {
+        // 分别获取温度和湿度数据
+        const tempData = response.data
+          .filter(item => item.metric_type === 'temperature')
+          .map(item => ({
+            timestamp: item.created_at.toISOString(),
+            temperature: item.metric_value,
+            metric_type: item.metric_type
+          }));
+
+        const humidityData = response.data
+          .filter(item => item.metric_type === 'humidity')
+          .map(item => ({
+            timestamp: item.created_at.toISOString(),
+            humidity: item.metric_value,
+            metric_type: item.metric_type
+          }));
+
+        // 合并数据
+        const sensorData = tempData.map(temp => {
+          const humidity = humidityData.find(h => h.timestamp === temp.timestamp);
+          return {
+            timestamp: temp.timestamp,
+            temperature: temp.temperature,
+            humidity: humidity?.humidity,
+            metric_type: 'combined'
+          };
+        });
+        setCurrentData(sensorData[0]);
+        setTableData(sensorData);
+      }
+    } catch (error) {
+      message.error('获取温湿度数据失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    fetchData();
+    if (!isPolling) return;
+    
+    const timer = setInterval(fetchData, pollInterval);
+    return () => clearInterval(timer);
+  }, [isPolling, pollInterval]);
+
+  const getChartOption = (): EChartsOption => {
+    const xAxisData = tableData.map(item =>
+      new Date(item.timestamp).toLocaleTimeString()
+    ).reverse();
+    
+    const temperatureData = tableData.map(item => item.temperature).reverse();
+    const humidityData = tableData.map(item => item.humidity).reverse();
+
+    return {
+      title: {
+        text: '温湿度趋势图',
+        left: 'center'
+      },
+      tooltip: {
+        trigger: 'axis' as const,
+        axisPointer: {
+          type: 'cross' as const
+        }
+      },
+      legend: {
+        data: ['温度(℃)', '湿度(%)'],
+        top: 30
+      },
+      grid: {
+        left: '3%',
+        right: '4%',
+        bottom: '3%',
+        containLabel: true
+      },
+      xAxis: {
+        type: 'category' as const,
+        boundaryGap: false,
+        data: xAxisData
+      },
+      yAxis: [
+        {
+          type: 'value' as const,
+          name: '温度(℃)',
+          min: 10,
+          max: 35,
+          axisLabel: {
+            formatter: '{value} °C'
+          }
+        },
+        {
+          type: 'value' as const,
+          name: '湿度(%)',
+          min: 20,
+          max: 90,
+          axisLabel: {
+            formatter: '{value} %'
+          }
+        }
+      ],
+      series: [
+        {
+          name: '温度(℃)',
+          type: 'line' as const,
+          data: temperatureData,
+          smooth: true,
+          lineStyle: {
+            color: '#ff4d4f'
+          },
+          itemStyle: {
+            color: '#ff4d4f'
+          }
+        },
+        {
+          name: '湿度(%)',
+          type: 'line' as const,
+          yAxisIndex: 1,
+          data: humidityData,
+          smooth: true,
+          lineStyle: {
+            color: '#1890ff'
+          },
+          itemStyle: {
+            color: '#1890ff'
+          }
+        }
+      ]
+    };
+  };
+
+  const getStatus = (temp?: number, humidity?: number) => {
+    // 定义正常范围:温度10-30°C,湿度30-70%
+    const isTempNormal = temp && temp >= 10 && temp <= 30;
+    const isHumidityNormal = humidity && humidity >= 30 && humidity <= 70;
+    return isTempNormal && isHumidityNormal ? '正常' : '异常';
+  };
+
+  const columns = [
+    {
+      title: '序号',
+      key: 'index',
+      render: (text: string, record: any, index: number) => index + 1,
+      width: 80
+    },
+    {
+      title: '设备名称',
+      key: 'device',
+      render: () => selectedDevice === 'device1' ? '温湿度1' : '温湿度2',
+      width: 120
+    },
+    {
+      title: '温度 (°C)',
+      dataIndex: 'temperature',
+      key: 'temperature',
+      render: (text: number) => text?.toFixed(1),
+      width: 100
+    },
+    {
+      title: '湿度 (%)',
+      dataIndex: 'humidity',
+      key: 'humidity',
+      render: (text: number) => text?.toFixed(1),
+      width: 100
+    },
+    {
+      title: '状态',
+      key: 'status',
+      render: (text: string, record: SensorData) => (
+        <span style={{ color: getStatus(record.temperature, record.humidity) === '正常' ? 'green' : 'red' }}>
+          {getStatus(record.temperature, record.humidity)}
+        </span>
+      ),
+      width: 100
+    },
+    {
+      title: '时间',
+      dataIndex: 'timestamp',
+      key: 'timestamp',
+      render: (text: string) => new Date(text).toLocaleString(),
+      width: 180
+    }
+  ];
+
+  return (
+    <div style={{ padding: 24 }}>
+      <Space direction="vertical" size="large" style={{ width: '100%' }}>
+        <Card
+          title={
+            <Space>
+              <Select
+                defaultValue="device1"
+                style={{ width: 200 }}
+                onChange={setSelectedDevice}
+                options={[
+                  { value: 'device1', label: '温湿度1' },
+                  { value: 'device2', label: '温湿度2' }
+                ]}
+              />
+              <Button
+                type={isPolling ? 'default' : 'primary'}
+                onClick={() => setIsPolling(!isPolling)}
+              >
+                {isPolling ? '停止轮询' : '开始轮询'}
+              </Button>
+              <Select
+                value={pollInterval}
+                style={{ width: 120 }}
+                onChange={(value) => setPollInterval(value)}
+                options={[
+                  { value: 10000, label: '10秒' },
+                  { value: 30000, label: '30秒' },
+                  { value: 60000, label: '1分钟' }
+                ]}
+              />
+            </Space>
+          }
+        >
+          <Space size={40} align="center" style={{
+            display: 'flex',
+            justifyContent: 'center',
+            width: '100%'
+          }}>
+            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <Statistic
+                title="温度"
+                value={currentData?.temperature}
+                precision={1}
+                suffix="°C"
+                prefix={<EnvironmentOutlined style={{ fontSize: '80px' }} />}
+                style={{ textAlign: 'center' }}
+                valueStyle={{ fontSize: '32px' }}
+              />
+            </div>
+            <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
+              <Statistic
+                title="湿度"
+                value={currentData?.humidity}
+                precision={1}
+                suffix="%"
+                prefix={<CloudOutlined style={{ fontSize: '80px' }} />}
+                style={{ textAlign: 'center' }}
+                valueStyle={{ fontSize: '32px' }}
+              />
+            </div>
+          </Space>
+        </Card>
+
+        <Tabs
+          activeKey={activeTab}
+          onChange={setActiveTab}
+          tabBarExtraContent={
+            <Space>
+              <Radio.Group
+                value={timeRange}
+                onChange={(e) => setTimeRange(e.target.value)}
+                buttonStyle="solid"
+              >
+                <Radio.Button value="4h">4小时</Radio.Button>
+                <Radio.Button value="12h">12小时</Radio.Button>
+                <Radio.Button value="1d">1天</Radio.Button>
+                <Radio.Button value="7d">7天</Radio.Button>
+                <Radio.Button value="custom">自定义</Radio.Button>
+              </Radio.Group>
+              {timeRange === 'custom' && (
+                <DatePicker.RangePicker
+                  showTime
+                  value={customDateRange}
+                  onChange={(dates) => {
+                    if (dates && dates[0] && dates[1]) {
+                      setCustomDateRange([dates[0], dates[1]]);
+                    }
+                  }}
+                />
+              )}
+            </Space>
+          }
+        >
+          <Tabs.TabPane tab="数据表格" key="table">
+            <Table
+              columns={columns}
+              dataSource={tableData}
+              rowKey="timestamp"
+              loading={loading}
+              pagination={{ pageSize: 10 }}
+            />
+          </Tabs.TabPane>
+          <Tabs.TabPane tab="趋势图表" key="chart">
+            <ReactECharts
+              option={getChartOption()}
+              style={{ height: 500 }}
+              theme="light"
+            />
+          </Tabs.TabPane>
+        </Tabs>
+      </Space>
+    </div>
+  );
+};
+
+export default TemperatureHumidityPage;

+ 344 - 0
client/admin/pages_theme_settings.tsx

@@ -0,0 +1,344 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Space,
+  Form, message, 
+  Card, Spin, Typography,
+  Switch, 
+  Popconfirm, Radio, InputNumber,ColorPicker,
+} from 'antd';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+import type { 
+  ColorScheme
+} from '../share/types.ts';
+import { ThemeMode } from '../share/types.ts';
+
+import {
+  FontSize,
+  CompactMode, 
+} from '../share/types.ts';
+
+
+
+
+import { useTheme } from './hooks_sys.tsx';
+
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+// 设置 dayjs 语言
+dayjs.locale('zh-cn');
+
+const { Title } = Typography;
+
+
+// 定义预设配色方案 - 按明暗模式分组
+const COLOR_SCHEMES: Record<ThemeMode, Record<string, ColorScheme>> = {
+  [ThemeMode.LIGHT]: {
+    DEFAULT: {
+      name: '默认浅色',
+      primary: '#1890ff',
+      background: '#f0f2f5',
+      text: '#000000'
+    },
+    BLUE: {
+      name: '蓝色',
+      primary: '#096dd9', 
+      background: '#e6f7ff',
+      text: '#003a8c'
+    },
+    GREEN: {
+      name: '绿色',
+      primary: '#52c41a',
+      background: '#f6ffed',
+      text: '#135200'
+    },
+    WARM: {
+      name: '暖橙',
+      primary: '#fa8c16',
+      background: '#fff7e6',
+      text: '#873800'
+    }
+  },
+  [ThemeMode.DARK]: {
+    DEFAULT: {
+      name: '默认深色',
+      primary: '#177ddc',
+      background: '#141414',
+      text: '#ffffff'
+    },
+    MIDNIGHT: {
+      name: '午夜蓝',
+      primary: '#1a3b7a',
+      background: '#0a0a1a',
+      text: '#e0e0e0'
+    },
+    FOREST: {
+      name: '森林',
+      primary: '#2e7d32',
+      background: '#121212',
+      text: '#e0e0e0'
+    },
+    SUNSET: {
+      name: '日落',
+      primary: '#f5222d',
+      background: '#1a1a1a',
+      text: '#ffffff'
+    }
+  }
+};
+
+// 主题设置页面
+export const ThemeSettingsPage = () => {
+  const { isDark, currentTheme, updateTheme, saveTheme, resetTheme } = useTheme();
+  const [form] = Form.useForm();
+  const [loading, setLoading] = useState(false);
+
+  // 处理配色方案选择
+  const handleColorSchemeChange = (schemeName: string) => {
+    const currentMode = form.getFieldValue('theme_mode') as ThemeMode;
+    const scheme = COLOR_SCHEMES[currentMode][schemeName];
+    if (!scheme) return;
+    form.setFieldsValue({
+      primary_color: scheme.primary,
+      background_color: scheme.background,
+      text_color: scheme.text
+    });
+    updateTheme({
+      primary_color: scheme.primary,
+      background_color: scheme.background,
+      text_color: scheme.text
+    });
+  };
+
+  // 初始化表单数据
+  useEffect(() => {
+    form.setFieldsValue({
+      theme_mode: currentTheme.theme_mode,
+      primary_color: currentTheme.primary_color,
+      background_color: currentTheme.background_color || (isDark ? '#141414' : '#f0f2f5'),
+      font_size: currentTheme.font_size,
+      is_compact: currentTheme.is_compact
+    });
+  }, [currentTheme, form, isDark]);
+
+  // 处理表单提交
+  const handleSubmit = async (values: any) => {
+    try {
+      setLoading(true);
+      await saveTheme(values);
+    } catch (error) {
+      message.error('保存主题设置失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理重置
+  const handleReset = async () => {
+    try {
+      setLoading(true);
+      await resetTheme();
+    } catch (error) {
+      message.error('重置主题设置失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 处理表单值变化 - 实时预览
+  const handleValuesChange = (changedValues: any) => {
+    updateTheme(changedValues);
+  };
+
+  return (
+    <div>
+      <Title level={2}>主题设置</Title>
+      <Card>
+        <Spin spinning={loading}>
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+            onValuesChange={handleValuesChange}
+            initialValues={{
+              theme_mode: currentTheme.theme_mode,
+              primary_color: currentTheme.primary_color,
+              background_color: currentTheme.background_color || (isDark ? '#141414' : '#f0f2f5'),
+              font_size: currentTheme.font_size,
+              is_compact: currentTheme.is_compact
+            }}
+          >
+            {/* 配色方案选择 */}
+            <Form.Item label="预设配色方案">
+              <Space wrap>
+                {(() => {
+                  const themeMode = (form.getFieldValue('theme_mode') as ThemeMode) || ThemeMode.LIGHT;
+                  const schemes = COLOR_SCHEMES[themeMode] || {};
+                  const currentPrimary = form.getFieldValue('primary_color');
+                  const currentBg = form.getFieldValue('background_color');
+                  const currentText = form.getFieldValue('text_color');
+                  
+                  return Object.entries(schemes).map(([key, scheme]) => {
+                    const isActive = 
+                      scheme.primary === currentPrimary && 
+                      scheme.background === currentBg && 
+                      scheme.text === currentText;
+                    
+                    return (
+                      <Button
+                        key={key}
+                        onClick={() => {
+                          handleColorSchemeChange(key);
+                          form.setFieldValue('scheme_name', scheme.name);
+                        }}
+                        style={{
+                          backgroundColor: scheme.background,
+                          color: scheme.text,
+                          borderColor: isActive ? scheme.text : scheme.primary,
+                          borderWidth: isActive ? 2 : 1,
+                          boxShadow: isActive ? `0 0 0 2px ${scheme.primary}` : 'none',
+                          fontWeight: isActive ? 'bold' : 'normal',
+                          transition: 'all 0.3s'
+                        }}
+                      >
+                        {scheme.name}
+                        {isActive && (
+                          <span style={{ marginLeft: 4 }}>✓</span>
+                        )}
+                      </Button>
+                    );
+                  });
+                })()}
+              </Space>
+            </Form.Item>
+
+            {/* 主题模式 */}
+            <Form.Item
+              label="主题模式"
+              name="theme_mode"
+              rules={[{ required: true, message: '请选择主题模式' }]}
+            >
+              <Radio.Group>
+                <Radio value={ThemeMode.LIGHT}>浅色模式</Radio>
+                <Radio value={ThemeMode.DARK}>深色模式</Radio>
+              </Radio.Group>
+            </Form.Item>
+
+            {/* 主题色 */}
+            <Form.Item
+              label="主题色"
+              name="primary_color"
+              rules={[{ required: true, message: '请选择主题色' }]}
+            >
+              <ColorPicker 
+                value={form.getFieldValue('primary_color')}
+                onChange={(color) => {
+                  form.setFieldValue('primary_color', color.toHexString());
+                  updateTheme({ primary_color: color.toHexString() });
+                }}
+                allowClear
+              />
+            </Form.Item>
+
+            {/* 背景色 */}
+            <Form.Item
+              label="背景色"
+              name="background_color"
+              rules={[{ required: true, message: '请选择背景色' }]}
+            >
+              <ColorPicker 
+                value={form.getFieldValue('background_color')}
+                onChange={(color) => {
+                  form.setFieldValue('background_color', color.toHexString());
+                  updateTheme({ background_color: color.toHexString() });
+                }}
+                allowClear
+              />
+            </Form.Item>
+
+            {/* 文字颜色 */}
+            <Form.Item
+              label="文字颜色"
+              name="text_color"
+              rules={[{ required: true, message: '请选择文字颜色' }]}
+            >
+              <ColorPicker 
+                value={form.getFieldValue('text_color')}
+                onChange={(color) => {
+                  form.setFieldValue('text_color', color.toHexString());
+                  updateTheme({ text_color: color.toHexString() });
+                }}
+                allowClear
+              />
+            </Form.Item>
+
+            {/* 圆角大小 */}
+            <Form.Item
+              label="圆角大小"
+              name="border_radius"
+              rules={[{ required: true, message: '请设置圆角大小' }]}
+              initialValue={6}
+            >
+              <InputNumber<number>
+                min={0} 
+                max={20}
+                addonAfter="px"
+              />
+            </Form.Item>
+
+            {/* 字体大小 */}
+            <Form.Item
+              label="字体大小"
+              name="font_size"
+              rules={[{ required: true, message: '请选择字体大小' }]}
+            >
+              <Radio.Group>
+                <Radio value={FontSize.SMALL}>小</Radio>
+                <Radio value={FontSize.MEDIUM}>中</Radio>
+                <Radio value={FontSize.LARGE}>大</Radio>
+              </Radio.Group>
+            </Form.Item>
+
+            {/* 紧凑模式 */}
+            <Form.Item
+              label="紧凑模式"
+              name="is_compact"
+              valuePropName="checked"
+              getValueFromEvent={(checked: boolean) => checked ? CompactMode.COMPACT : CompactMode.NORMAL}
+              getValueProps={(value: CompactMode) => ({
+                checked: value === CompactMode.COMPACT
+              })}
+            >
+              <Switch 
+                checkedChildren="开启" 
+                unCheckedChildren="关闭"
+              />
+            </Form.Item>
+
+            {/* 操作按钮 */}
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit">
+                  保存设置
+                </Button>
+                <Popconfirm
+                  title="确定要重置主题设置吗?"
+                  onConfirm={handleReset}
+                  okText="确定"
+                  cancelText="取消"
+                >
+                  <Button>重置为默认值</Button>
+                </Popconfirm>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Spin>
+      </Card>
+    </div>
+  );
+};

+ 270 - 0
client/admin/pages_users.tsx

@@ -0,0 +1,270 @@
+import React, { useState } from 'react';
+import { 
+  Button, Table, Space, Form, Input, Select, 
+  message, Modal, Card, Typography, Tag, Popconfirm 
+} from 'antd';
+import { useQuery } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import { UserAPI } from './api/index.ts';
+
+const { Title } = Typography;
+
+// 用户管理页面
+export const UsersPage = () => {
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    search: ''
+  });
+  const [modalVisible, setModalVisible] = useState(false);
+  const [modalTitle, setModalTitle] = useState('');
+  const [editingUser, setEditingUser] = useState<any>(null);
+  const [form] = Form.useForm();
+
+  const { data: usersData, isLoading, refetch } = useQuery({
+    queryKey: ['users', searchParams],
+    queryFn: async () => {
+      return await UserAPI.getUsers(searchParams);
+    }
+  });
+
+  const users = usersData?.data || [];
+  const pagination = {
+    current: searchParams.page,
+    pageSize: searchParams.limit,
+    total: usersData?.pagination?.total || 0
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      search: values.search || '',
+      page: 1
+    }));
+  };
+
+  // 处理分页变化
+  const handleTableChange = (newPagination: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page: newPagination.current,
+      limit: newPagination.pageSize
+    }));
+  };
+
+  // 打开创建用户模态框
+  const showCreateModal = () => {
+    setModalTitle('创建用户');
+    setEditingUser(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 打开编辑用户模态框
+  const showEditModal = (user: any) => {
+    setModalTitle('编辑用户');
+    setEditingUser(user);
+    form.setFieldsValue(user);
+    setModalVisible(true);
+  };
+
+  // 处理模态框确认
+  const handleModalOk = async () => {
+    try {
+      const values = await form.validateFields();
+      
+      if (editingUser) {
+        // 编辑用户
+        await UserAPI.updateUser(editingUser.id, values);
+        message.success('用户更新成功');
+      } else {
+        // 创建用户
+        await UserAPI.createUser(values);
+        message.success('用户创建成功');
+      }
+      
+      setModalVisible(false);
+      form.resetFields();
+      refetch(); // 刷新用户列表
+    } catch (error) {
+      console.error('表单提交失败:', error);
+      message.error('操作失败,请重试');
+    }
+  };
+
+  // 处理删除用户
+  const handleDelete = async (id: number) => {
+    try {
+      await UserAPI.deleteUser(id);
+      message.success('用户删除成功');
+      refetch(); // 刷新用户列表
+    } catch (error) {
+      console.error('删除用户失败:', error);
+      message.error('删除失败,请重试');
+    }
+  };
+  
+  const columns = [
+    {
+      title: '用户名',
+      dataIndex: 'username',
+      key: 'username',
+    },
+    {
+      title: '昵称',
+      dataIndex: 'nickname',
+      key: 'nickname',
+    },
+    {
+      title: '邮箱',
+      dataIndex: 'email',
+      key: 'email',
+    },
+    {
+      title: '角色',
+      dataIndex: 'role',
+      key: 'role',
+      render: (role: string) => (
+        <Tag color={role === 'admin' ? 'red' : 'blue'}>
+          {role === 'admin' ? '管理员' : '普通用户'}
+        </Tag>
+      ),
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: string) => dayjs(date).format('YYYY-MM-DD HH:mm:ss'),
+    },
+    {
+      title: '操作',
+      key: 'action',
+      render: (_: any, record: any) => (
+        <Space size="middle">
+          <Button type="link" onClick={() => showEditModal(record)}>
+            编辑
+          </Button>
+          <Popconfirm
+            title="确定要删除此用户吗?"
+            onConfirm={() => handleDelete(record.id)}
+            okText="确定"
+            cancelText="取消"
+          >
+            <Button type="link" danger>
+              删除
+            </Button>
+          </Popconfirm>
+        </Space>
+      ),
+    },
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>用户管理</Title>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+          <Form.Item name="search" label="搜索">
+            <Input placeholder="用户名/昵称/邮箱" allowClear />
+          </Form.Item>
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit">
+                搜索
+              </Button>
+              <Button type="primary" onClick={showCreateModal}>
+                创建用户
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+
+        <Table
+          columns={columns}
+          dataSource={users}
+          loading={isLoading}
+          rowKey="id"
+          pagination={{
+            ...pagination,
+            showSizeChanger: true,
+            showQuickJumper: true,
+            showTotal: (total) => `共 ${total} 条记录`
+          }}
+          onChange={handleTableChange}
+        />
+      </Card>
+
+      {/* 创建/编辑用户模态框 */}
+      <Modal
+        title={modalTitle}
+        open={modalVisible}
+        onOk={handleModalOk}
+        onCancel={() => {
+          setModalVisible(false);
+          form.resetFields();
+        }}
+        width={600}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+        >
+          <Form.Item
+            name="username"
+            label="用户名"
+            rules={[
+              { required: true, message: '请输入用户名' },
+              { min: 3, message: '用户名至少3个字符' }
+            ]}
+          >
+            <Input placeholder="请输入用户名" />
+          </Form.Item>
+
+          <Form.Item
+            name="nickname"
+            label="昵称"
+            rules={[{ required: true, message: '请输入昵称' }]}
+          >
+            <Input placeholder="请输入昵称" />
+          </Form.Item>
+
+          <Form.Item
+            name="email"
+            label="邮箱"
+            rules={[
+              { required: true, message: '请输入邮箱' },
+              { type: 'email', message: '请输入有效的邮箱地址' }
+            ]}
+          >
+            <Input placeholder="请输入邮箱" />
+          </Form.Item>
+
+          {!editingUser && (
+            <Form.Item
+              name="password"
+              label="密码"
+              rules={[
+                { required: true, message: '请输入密码' },
+                { min: 6, message: '密码至少6个字符' }
+              ]}
+            >
+              <Input.Password placeholder="请输入密码" />
+            </Form.Item>
+          )}
+
+          <Form.Item
+            name="role"
+            label="角色"
+            rules={[{ required: true, message: '请选择角色' }]}
+          >
+            <Select placeholder="请选择角色">
+              <Select.Option value="user">普通用户</Select.Option>
+              <Select.Option value="admin">管理员</Select.Option>
+            </Select>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 904 - 0
client/admin/pages_work_orders.tsx

@@ -0,0 +1,904 @@
+import React, { useState, useEffect } from 'react';
+import { Button, Table, Space, Modal, Form, Input, Select, message, List, Avatar, Progress, Tag, Timeline, DatePicker, Switch, Dropdown, Menu } from 'antd';
+import type { MenuProps } from 'antd';
+import { CloseOutlined } from '@ant-design/icons';
+import dayjs from 'dayjs';
+import { WorkOrderAPI } from './api/work_orders.ts';
+import { DeviceInstanceAPI } from './api/device_instance.ts';
+import { Uploader } from './components_uploader.tsx';
+import { WorkOrderPriority, WorkOrderStatus } from '../../client/share/monitorTypes.ts';
+import type { WorkOrder, WorkOrderSettings, DeadlineInfo } from '../../client/share/monitorTypes.ts';
+
+const { Column } = Table;
+const { Option } = Select;
+const { TextArea } = Input;
+
+export function WorkOrdersPage() {
+  const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
+  const [settings, setSettings] = useState<WorkOrderSettings>();
+  const [loading, setLoading] = useState(false);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [currentOrder, setCurrentOrder] = useState<Partial<WorkOrder>>();
+  const [categories, setCategories] = useState<string[]>([]);
+  const [devices, setDevices] = useState<any[]>([]);
+  const [attachments, setAttachments] = useState<any[]>([]);
+  const [comments, setComments] = useState<any[]>([]);
+  const [commentContent, setCommentContent] = useState('');
+  const [form] = Form.useForm();
+  const [historyVisible, setHistoryVisible] = useState(false);
+  const [statusHistory, setStatusHistory] = useState<any[]>([]);
+  const [deadlineInfo, setDeadlineInfo] = useState<DeadlineInfo>();
+  const [autoDispatchVisible, setAutoDispatchVisible] = useState(false);
+  const [autoDispatchForm] = Form.useForm();
+  const [detailModalVisible, setDetailModalVisible] = useState(false);
+  const [currentDetail, setCurrentDetail] = useState('');
+
+  useEffect(() => {
+    // 开发环境下生成模拟数据
+    if (process.env.NODE_ENV === 'development') {
+      const mockOrders = [
+        {
+          id: 'mock-1',
+          title: '设备网络故障',
+          order_no: `WO-${dayjs().format('YYYYMMDD')}-1001`,
+          device_name: '网络交换机1',
+          problem_desc: '设备无法连接网络',
+          priority: WorkOrderPriority.URGENT,
+          creator_id: 'system',
+          creator_name: '系统管理员',
+          deadline: dayjs().add(1, 'day').toISOString(),
+          created_at: dayjs().toISOString(),
+          updated_at: dayjs().toISOString(),
+          problem_type: '网络',
+          status: WorkOrderStatus.PENDING,
+          feedback: '',
+          attachments: []
+        },
+        {
+          id: 'mock-2',
+          title: '服务器硬件故障',
+          order_no: `WO-${dayjs().format('YYYYMMDD')}-1002`,
+          device_name: '服务器A',
+          problem_desc: '硬盘故障需要更换',
+          priority: WorkOrderPriority.IMPORTANT,
+          creator_id: 'system',
+          creator_name: '系统管理员',
+          deadline: dayjs().add(2, 'day').toISOString(),
+          created_at: dayjs().toISOString(),
+          updated_at: dayjs().toISOString(),
+          problem_type: '硬件',
+          status: WorkOrderStatus.PROCESSING,
+          feedback: '已订购新硬盘',
+          attachments: []
+        },
+        {
+          id: 'mock-3',
+          title: '软件系统升级',
+          order_no: `WO-${dayjs().format('YYYYMMDD')}-1003`,
+          device_name: '办公电脑',
+          problem_desc: '需要升级到最新版本',
+          priority: WorkOrderPriority.NORMAL,
+          creator_id: 'system',
+          creator_name: '系统管理员',
+          deadline: dayjs().add(3, 'day').toISOString(),
+          created_at: dayjs().toISOString(),
+          updated_at: dayjs().toISOString(),
+          problem_type: '软件',
+          status: WorkOrderStatus.CLOSED,
+          feedback: '已完成升级',
+          attachments: []
+        },
+        {
+          id: 'mock-4',
+          title: '打印机维护',
+          order_no: `WO-${dayjs().format('YYYYMMDD')}-1004`,
+          device_name: '办公室打印机',
+          problem_desc: '定期维护保养',
+          priority: WorkOrderPriority.NORMAL,
+          creator_id: 'system',
+          creator_name: '系统管理员',
+          deadline: dayjs().add(4, 'day').toISOString(),
+          created_at: dayjs().toISOString(),
+          updated_at: dayjs().toISOString(),
+          problem_type: '其他',
+          status: WorkOrderStatus.PENDING,
+          feedback: '',
+          attachments: []
+        },
+        {
+          id: 'mock-5',
+          title: '数据库优化',
+          order_no: `WO-${dayjs().format('YYYYMMDD')}-1005`,
+          device_name: '数据库服务器',
+          problem_desc: '查询性能优化',
+          priority: WorkOrderPriority.IMPORTANT,
+          creator_id: 'system',
+          creator_name: '系统管理员',
+          deadline: dayjs().add(5, 'day').toISOString(),
+          created_at: dayjs().toISOString(),
+          updated_at: dayjs().toISOString(),
+          problem_type: '软件',
+          status: WorkOrderStatus.PROCESSING,
+          feedback: '正在优化索引',
+          attachments: []
+        }
+      ];
+      setWorkOrders(mockOrders);
+    } else {
+      fetchData();
+    }
+    fetchSettings();
+    fetchCategories();
+    fetchDevices();
+  }, []);
+
+  const [searchParams, setSearchParams] = useState({
+    status: undefined,
+    problemType: undefined,
+    keyword: undefined,
+    startDate: undefined,
+    endDate: undefined
+  });
+
+  const fetchData = async () => {
+    setLoading(true);
+    try {
+      const result = await WorkOrderAPI.getList(searchParams);
+      setWorkOrders(result.data);
+    } catch (error) {
+      message.error('获取工单列表失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      status: values.status,
+      problemType: values.problemType,
+      keyword: values.keyword,
+      startDate: values.dateRange?.[0]?.toISOString(),
+      endDate: values.dateRange?.[1]?.toISOString()
+    });
+  };
+
+  const handleReset = () => {
+    setSearchParams({
+      status: undefined,
+      problemType: undefined,
+      keyword: undefined,
+      startDate: undefined,
+      endDate: undefined
+    });
+  };
+
+  const fetchSettings = async () => {
+    try {
+      const result = await WorkOrderAPI.getSettings();
+      setSettings(result.data);
+    } catch (error) {
+      message.error('获取工单设置失败');
+    }
+  };
+
+  const fetchCategories = async () => {
+    try {
+      const result = await WorkOrderAPI.getCategories();
+      setCategories(result.data);
+    } catch (error) {
+      message.error('获取分类列表失败');
+    }
+  };
+
+  const fetchDevices = async () => {
+    try {
+      const result = await DeviceInstanceAPI.getDeviceInstances();
+      setDevices(result.data);
+    } catch (error) {
+      message.error('获取设备列表失败');
+    }
+  };
+
+  const fetchComments = async (id: string) => {
+    try {
+      const result = await WorkOrderAPI.getComments(id);
+      setComments(result.data);
+    } catch (error) {
+      message.error('获取评论失败');
+    }
+  };
+
+  const fetchStatusHistory = async (id: string) => {
+    try {
+      const result = await WorkOrderAPI.getStatusHistory(id);
+      setStatusHistory(result.data);
+    } catch (error) {
+      message.error('获取状态历史失败');
+    }
+  };
+
+  const checkDeadline = async (order: WorkOrder) => {
+    if (!order.deadline) return;
+    
+    try {
+      const result = await WorkOrderAPI.getDeadline(order.id);
+      const { remaining_hours, is_overdue } = result.data;
+      
+      let color = 'green';
+      let text = '进行中';
+      let progress = 100;
+      
+      if (is_overdue) {
+        color = 'red';
+        text = '已超时';
+        progress = 0;
+      } else if (remaining_hours < 24) {
+        color = 'orange';
+        text = `即将到期 (剩余${remaining_hours}小时)`;
+        progress = Math.max(10, Math.min(90, remaining_hours * 4));
+      }
+      
+      setDeadlineInfo({
+        color,
+        text,
+        progress: Number(progress),
+        remainingTime: `${remaining_hours}小时`,
+        isOverdue: remaining_hours < 0,
+      });
+    } catch (error) {
+      message.error('获取时限信息失败');
+    }
+  };
+
+  const handleCreate = () => {
+    setCurrentOrder({});
+    setModalVisible(true);
+  };
+
+  const handleEdit = async (record: WorkOrder) => {
+    setCurrentOrder(record);
+    form.setFieldsValue(record);
+    setModalVisible(true);
+    checkDeadline(record);
+    if (record.id) {
+      await fetchComments(record.id);
+    }
+  };
+
+  const handleSubmit = async () => {
+    try {
+      const values = await form.validateFields();
+      if (currentOrder?.id) {
+        await WorkOrderAPI.update(currentOrder.id, values);
+        message.success('更新工单成功');
+      } else {
+        await WorkOrderAPI.create(values);
+        message.success('创建工单成功');
+      }
+      setModalVisible(false);
+      fetchData();
+    } catch (error) {
+      message.error('操作失败');
+    }
+  };
+
+  const handleStatusChange = async (id: string, status: string) => {
+    Modal.confirm({
+      title: '确认状态变更',
+      content: (
+        <Form form={form}>
+          <Form.Item name="comment" label="变更备注" rules={[{required: true}]}>
+            <Input.TextArea placeholder="请输入状态变更原因" />
+          </Form.Item>
+        </Form>
+      ),
+      onOk: async (close) => {
+        try {
+          const values = await form.validateFields();
+          await WorkOrderAPI.changeStatus(
+            id,
+            status,
+            'current_user', // TODO: 替换为实际用户
+            values.comment
+          );
+          message.success('状态更新成功');
+          fetchData();
+          close();
+        } catch (error) {
+          message.error('状态更新失败');
+        }
+      }
+    });
+  };
+
+  const renderStatusActions = (record: WorkOrder) => {
+    const statusOptions = settings?.statusOptions || [];
+    const currentStatusIndex = statusOptions.indexOf(record.status);
+    const nextStatus = statusOptions[currentStatusIndex + 1];
+    const prevStatus = statusOptions[currentStatusIndex - 1];
+
+    return (
+      <Space>
+        {prevStatus && (
+          <Button
+            size="small"
+            onClick={() => handleStatusChange(record.id, prevStatus)}
+          >
+            回退到{prevStatus}
+          </Button>
+        )}
+        {nextStatus && (
+          <Button
+            type="primary"
+            size="small"
+            onClick={() => handleStatusChange(record.id, nextStatus)}
+          >
+            推进到{nextStatus}
+          </Button>
+        )}
+        {!nextStatus && !prevStatus && (
+          <span>无可用操作</span>
+        )}
+      </Space>
+    );
+  };
+
+  const handleShowHistory = async (id: string) => {
+    await fetchStatusHistory(id);
+    setHistoryVisible(true);
+  };
+
+  const handleAssign = async (id: string, assignee: string) => {
+    try {
+      await WorkOrderAPI.assign(id, assignee);
+      message.success('分配成功');
+      fetchData();
+    } catch (error) {
+      message.error('分配失败');
+    }
+  };
+
+  const handleUploadSuccess = (fileUrl: string, fileInfo: any) => {
+    if (currentOrder?.id) {
+      setAttachments(prev => [...prev, {
+        id: fileInfo.id,
+        url: fileUrl,
+        name: fileInfo.original_filename
+      }]);
+      message.success('附件上传成功');
+    }
+  };
+
+  const handleAddComment = async () => {
+    if (!currentOrder?.id) {
+      message.error('请先保存工单');
+      return;
+    }
+    
+    if (!commentContent.trim()) {
+      message.error('评论内容不能为空');
+      return;
+    }
+    
+    if (commentContent.length > 500) {
+      message.error('评论内容不能超过500字');
+      return;
+    }
+    
+    // 简单敏感词过滤
+    const bannedWords = ['敏感词1', '敏感词2', '敏感词3'];
+    if (bannedWords.some(word => commentContent.includes(word))) {
+      message.error('评论包含不允许的内容');
+      return;
+    }
+    
+    try {
+      await WorkOrderAPI.addComment(currentOrder.id, commentContent);
+      setCommentContent('');
+      await fetchComments(currentOrder.id);
+      message.success('评论添加成功');
+    } catch (error) {
+      message.error('评论添加失败');
+    }
+  };
+
+  const handleAccept = async (id: string) => {
+    try {
+      await WorkOrderAPI.changeStatus(id, '处理中', 'current_user', '工单已受理');
+      message.success('工单受理成功');
+      fetchData();
+    } catch (error) {
+      message.error('受理失败');
+    }
+  };
+
+  const handleReassign = async (id: string) => {
+    Modal.confirm({
+      title: '改派工单',
+      content: (
+        <Select placeholder="选择新的处理人" style={{ width: '100%' }}>
+          <Option value="user1">用户1</Option>
+          <Option value="user2">用户2</Option>
+          <Option value="user3">用户3</Option>
+        </Select>
+      ),
+      onOk: async (close) => {
+        try {
+          await WorkOrderAPI.assign(id, 'new_assignee'); // TODO: 替换为实际选择的值
+          message.success('工单改派成功');
+          fetchData();
+          close();
+        } catch (error) {
+          message.error('改派失败');
+        }
+      }
+    });
+  };
+
+  const handleClose = async (id: string) => {
+    Modal.confirm({
+      title: '关闭工单',
+      content: (
+        <Form form={form}>
+          <Form.Item name="feedback" label="处理结果" rules={[{required: true}]}>
+            <TextArea placeholder="请输入处理结果反馈" />
+          </Form.Item>
+        </Form>
+      ),
+      onOk: async (close) => {
+        try {
+          const values = await form.validateFields();
+          await WorkOrderAPI.changeStatus(
+            id,
+            '已关闭',
+            'current_user',
+            values.feedback
+          );
+          message.success('工单已关闭');
+          fetchData();
+          close();
+        } catch (error) {
+          message.error('关闭失败');
+        }
+      }
+    });
+  };
+
+  return (
+    <div>
+      <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
+        <Space>
+          <Button type="primary" onClick={handleCreate}>
+            新建工单
+          </Button>
+          <Button type="primary" onClick={() => setAutoDispatchVisible(true)}>
+            自动派工
+          </Button>
+        </Space>
+        <Space>
+          <Button
+            type="primary"
+            onClick={async () => {
+              try {
+                const data = await WorkOrderAPI.exportList(searchParams);
+                const url = window.URL.createObjectURL(new Blob([data]));
+                const link = document.createElement('a');
+                link.href = url;
+                link.setAttribute('download', `工单列表_${dayjs().format('YYYYMMDD')}.xlsx`);
+                document.body.appendChild(link);
+                link.click();
+                document.body.removeChild(link);
+                message.success('导出成功');
+              } catch (error) {
+                message.error('导出失败');
+              }
+            }}
+          >
+            工单导出
+          </Button>
+        </Space>
+      </div>
+
+      <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+        <Form.Item name="status" label="工单状态">
+          <Select style={{ width: 120 }} allowClear>
+            {Object.values(WorkOrderStatus).map(status => (
+              <Option key={status} value={status}>{status}</Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item name="problemType" label="问题分类">
+          <Select style={{ width: 120 }} allowClear>
+            {categories.map(category => (
+              <Option key={category} value={category}>{category}</Option>
+            ))}
+          </Select>
+        </Form.Item>
+        <Form.Item name="keyword" label="关键字">
+          <Input placeholder="请输入关键字" />
+        </Form.Item>
+        <Form.Item name="dateRange" label="时间范围">
+          <DatePicker.RangePicker />
+        </Form.Item>
+        <Form.Item>
+          <Button type="primary" htmlType="submit">
+            查询
+          </Button>
+          <Button style={{ marginLeft: 8 }} onClick={handleReset}>
+            重置
+          </Button>
+        </Form.Item>
+      </Form>
+
+      <Table dataSource={workOrders} loading={loading} rowKey="id">
+        <Column title="工单编号" dataIndex="order_no" key="order_no" />
+        <Column title="设备名称" dataIndex="device_name" key="device_name" />
+        <Column title="问题描述" dataIndex="problem_desc" key="problem_desc" ellipsis />
+        <Column title="故障等级" dataIndex="priority" key="priority" />
+        <Column title="创建人" dataIndex="creator_name" key="creator_name" />
+        <Column
+          title="截止日期"
+          dataIndex="deadline"
+          key="deadline"
+          render={(deadline) => deadline ? dayjs(deadline).format('YYYY-MM-DD') : '-'}
+        />
+        <Column
+          title="创建日期"
+          dataIndex="created_at"
+          key="created_at"
+          render={(date) => dayjs(date).format('YYYY-MM-DD')}
+        />
+        <Column title="问题分类" dataIndex="problem_type" key="problem_type" />
+        <Column
+          title="状态"
+          dataIndex="status"
+          key="status"
+          render={(status, record: WorkOrder) => (
+            <Space>
+              <span>{status}</span>
+              {deadlineInfo && record.id === currentOrder?.id && (
+                <Tag color={deadlineInfo.color}>{deadlineInfo.text}</Tag>
+              )}
+            </Space>
+          )}
+        />
+        <Column
+          title="结果反馈"
+          dataIndex="feedback"
+          key="feedback"
+          render={(feedback) => (
+            feedback ? (
+              <a onClick={() => {
+                setCurrentDetail(feedback);
+                setDetailModalVisible(true);
+              }}>详情</a>
+            ) : '-'
+          )}
+        />
+        <Column
+          title="附件"
+          dataIndex="attachments"
+          key="attachments"
+          render={(attachments) => attachments?.length > 0 ? `${attachments.length}个` : '无'}
+        />
+        <Column
+          title="操作"
+          key="action"
+          render={(_, record: WorkOrder) => (
+            <Space size="middle">
+              <Dropdown
+                overlay={
+                  <Menu>
+                    {record.status === '待受理' && (
+                      <Menu.Item key="accept" onClick={() => handleAccept(record.id)}>
+                        受理
+                      </Menu.Item>
+                    )}
+                    {record.status === '处理中' && (
+                      <Menu.Item key="reassign" onClick={() => handleReassign(record.id)}>
+                        改派
+                      </Menu.Item>
+                    )}
+                    <Menu.Item
+                      key="close"
+                      onClick={() => handleClose(record.id)}
+                      danger
+                    >
+                      关闭
+                    </Menu.Item>
+                  </Menu>
+                }
+              >
+                <Button type="primary" size="small">操作</Button>
+              </Dropdown>
+              <Button type="link" size="small" onClick={() => handleShowHistory(record.id)}>
+                流程
+              </Button>
+            </Space>
+          )}
+        />
+      </Table>
+
+      <Modal
+       title={
+         <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+           <div>{currentOrder?.id ? '工单详情' : '新建工单'}</div>
+           <Button
+             type="text"
+             icon={<CloseOutlined />}
+             onClick={() => setModalVisible(false)}
+             style={{ marginRight: -16 }}
+           />
+         </div>
+       }
+       visible={modalVisible}
+       onOk={handleSubmit}
+       onCancel={() => setModalVisible(false)}
+       width={800}
+       footer={
+         <div style={{ textAlign: 'center' }}>
+           <Button key="submit" type="primary" onClick={handleSubmit}>
+             确定
+           </Button>
+         </div>
+       }
+       closable={false}
+      >
+        <Form form={form} layout="vertical">
+          <Form.Item name="order_no" label="工单编号" rules={[{ required: true }]}>
+            <Input placeholder="自动生成" disabled />
+          </Form.Item>
+          <Form.Item name="device_id" label="设备" rules={[{ required: true }]}>
+            <Select showSearch optionFilterProp="children">
+              {devices.map(device => (
+                <Option key={device.id} value={device.id}>
+                  {device.name}
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item name="problem_desc" label="问题描述" rules={[{ required: true }]}>
+            <TextArea rows={4} />
+          </Form.Item>
+          <Form.Item name="priority" label="故障等级" rules={[{ required: true }]}>
+            <Select>
+              <Option value="紧急">紧急</Option>
+              <Option value="高">高</Option>
+              <Option value="中">中</Option>
+              <Option value="低">低</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item name="deadline" label="截止日期" rules={[{ required: true }]}>
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item name="problem_type" label="问题分类">
+            <Select>
+              {categories.map(category => (
+                <Option key={category} value={category}>
+                  {category}
+                </Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item name="feedback" label="结果反馈">
+            <TextArea rows={2} />
+          </Form.Item>
+          <Form.Item label="附件">
+            <Uploader
+              onSuccess={handleUploadSuccess}
+              onError={(error) => message.error(`上传失败: ${error.message}`)}
+              onProgress={(percent) => (
+                <Progress percent={percent} status="active" />
+              )}
+            />
+            {attachments.length > 0 && (
+              <List
+                dataSource={attachments}
+                renderItem={item => (
+                  <List.Item>
+                    <a href={item.url} target="_blank" rel="noopener noreferrer">
+                      {item.name}
+                    </a>
+                  </List.Item>
+                )}
+              />
+            )}
+          </Form.Item>
+        </Form>
+
+        {currentOrder?.id && (
+          <div style={{ marginTop: 24 }}>
+            <h3>评论</h3>
+            <List
+              className="comment-list"
+              itemLayout="horizontal"
+              dataSource={comments}
+              renderItem={item => (
+                <List.Item>
+                  <List.Item.Meta
+                    avatar={<Avatar>{item.author.charAt(0)}</Avatar>}
+                    title={item.author}
+                    description={item.content}
+                  />
+                  <div>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</div>
+                </List.Item>
+              )}
+            />
+            <TextArea
+              rows={4}
+              value={commentContent}
+              onChange={(e) => setCommentContent(e.target.value)}
+              placeholder="输入评论内容"
+            />
+            <Button 
+              type="primary" 
+              onClick={handleAddComment}
+              style={{ marginTop: 16 }}
+            >
+              提交评论
+            </Button>
+          </div>
+        )}
+      </Modal>
+
+      <Modal
+       title={`工单流程记录 (共${statusHistory.length}条)`}
+        visible={historyVisible}
+        onCancel={() => setHistoryVisible(false)}
+        footer={null}
+        width={800}
+      >
+        <div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
+          <Timeline mode="alternate">
+            {statusHistory.map(item => (
+              <Timeline.Item
+                key={item.id}
+                color={
+                  item.status_to === '已完成' ? 'green' :
+                  item.status_to === '已取消' ? 'red' : 'blue'
+                }
+              >
+                <div style={{ padding: '8px 16px', background: '#f9f9f9', borderRadius: 4 }}>
+                  <strong>{item.status_from} → {item.status_to}</strong>
+                  <div style={{ marginTop: 8 }}>
+                    <Tag color="geekblue">{item.operator}</Tag>
+                    <Tag>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</Tag>
+                  </div>
+                  {item.comment && (
+                    <div style={{ marginTop: 8 }}>
+                      <p style={{ marginBottom: 0 }}><strong>备注:</strong></p>
+                      <p style={{ marginTop: 4 }}>{item.comment}</p>
+                    </div>
+                  )}
+                </div>
+              </Timeline.Item>
+            ))}
+          </Timeline>
+        </div>
+      </Modal>
+
+      <Modal
+        title={
+          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+            <div>
+              自动派工
+              <Switch
+                style={{ marginLeft: 16 }}
+                checkedChildren="开"
+                unCheckedChildren="关"
+                defaultChecked
+              />
+            </div>
+            <Button
+              type="text"
+              icon={<CloseOutlined />}
+              onClick={() => setAutoDispatchVisible(false)}
+              style={{ marginRight: -16 }}
+            />
+          </div>
+        }
+        open={autoDispatchVisible}
+        footer={null}
+        width={600}
+        closable={false}
+        onCancel={() => setAutoDispatchVisible(false)}
+      >
+        <Form form={autoDispatchForm} layout="vertical">
+          <Form.Item name="device_type" label="设备分类">
+            <Select placeholder="全部类型设备" allowClear>
+              {categories.map(category => (
+                <Option key={category} value={category}>{category}</Option>
+              ))}
+            </Select>
+          </Form.Item>
+          <Form.Item name="problem_desc" label="问题描述">
+            <TextArea rows={3} placeholder="设备名称+告警故障" />
+          </Form.Item>
+          <Form.Item name="priority" label="故障等级" initialValue="中">
+            <Select>
+              <Option value="紧急">紧急</Option>
+              <Option value="高">高</Option>
+              <Option value="中">中</Option>
+              <Option value="低">低</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item name="assignee" label="处理人" initialValue="admin">
+            <Select>
+              <Option value="admin">admin</Option>
+              <Option value="user1">用户1</Option>
+              <Option value="user2">用户2</Option>
+              <Option value="user3">用户3</Option>
+            </Select>
+          </Form.Item>
+          <Form.Item
+            name="deadline"
+            label="截止时间"
+            initialValue={dayjs().add(2, 'day')}
+          >
+            <DatePicker style={{ width: '100%' }} />
+          </Form.Item>
+          <Form.Item>
+            <div style={{ textAlign: 'center', marginTop: 24 }}>
+              <Button
+                type="primary"
+                onClick={() => {
+                  autoDispatchForm.validateFields()
+                    .then(async values => {
+                      try {
+                        if (!values.problem_desc) {
+                          values.problem_desc = `设备${values.device_type || '全部'}告警故障`;
+                        }
+                        
+                        await WorkOrderAPI.create({
+                          ...values,
+                          title: '自动派工工单',
+                          creator_id: 'system',
+                          creator_name: '系统自动派工',
+                          status: '待受理'
+                        });
+                        
+                        message.success('自动派工成功');
+                        setAutoDispatchVisible(false);
+                        fetchData();
+                      } catch (error) {
+                        message.error('自动派工失败');
+                      }
+                    })
+                    .catch(info => {
+                      console.log('Validate Failed:', info);
+                    });
+                }}
+                style={{ width: 120 }}
+              >
+                确认
+              </Button>
+            </div>
+          </Form.Item>
+        </Form>
+      </Modal>
+
+      <Modal
+        title={
+          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
+            <div>工单详情</div>
+            <Button
+              type="text"
+              icon={<CloseOutlined />}
+              onClick={() => setDetailModalVisible(false)}
+              style={{ marginRight: -16 }}
+            />
+          </div>
+        }
+        visible={detailModalVisible}
+        onCancel={() => setDetailModalVisible(false)}
+        footer={null}
+        width={600}
+        closable={false}
+      >
+        <div style={{ padding: 16 }}>
+          {currentDetail}
+        </div>
+      </Modal>
+    </div>
+  );
+}

+ 401 - 0
client/admin/pages_zichan.tsx

@@ -0,0 +1,401 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Layout, Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Typography,
+  Popconfirm, Tag, DatePicker, Radio
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { EnableStatus } from '../share/types.ts'
+import type { 
+  ZichanInfo, ZichanTransLog, ZichanCategory, ZichanArea, 
+} from '../share/monitorTypes.ts';
+import {
+  DeviceCategory, DeviceStatus, AssetTransferType, 
+  AssetTransferTypeNameMap, AssetTransferTypeColorMap,
+  DeviceStatusNameMap, DeviceStatusColorMap, DeviceCategoryNameMap
+} from '../share/monitorTypes.ts';
+
+import { ZichanAPI, ZichanCategoryAPI, ZichanAreaAPI, ZichanTransferAPI } from './api/index.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+const { Title } = Typography;
+
+// 资产管理页面组件
+export const ZichanPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    asset_name: '',
+    device_category: undefined as DeviceCategory | undefined,
+    device_status: undefined as DeviceStatus | undefined
+  });
+  
+  // 设备分类选项
+  const categoryOptions = getEnumOptions(DeviceCategory, DeviceCategoryNameMap);
+  
+  // 设备状态选项
+  const statusOptions = getEnumOptions(DeviceStatus, DeviceStatusNameMap);
+  
+  // 查询资产列表
+  const { 
+    data: zichanResult = { data: [], pagination: { total: 0, current: 1, pageSize: 10 } }, 
+    isLoading: isFetching,
+    refetch
+  } = useQuery({
+    queryKey: ['zichan', searchParams],
+    queryFn: () => ZichanAPI.getZichanList(searchParams),
+    // staleTime: 5000, // 数据5秒内不会被认为是过期的
+  });
+  
+  // 提取数据和分页信息
+  const zichanList = zichanResult.data || [];
+  const pagination = zichanResult.pagination || { total: 0, current: 1, pageSize: 10 };
+  
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<ZichanInfo>) => {
+    try {
+      setIsLoading(true);
+      if (formMode === 'create') {
+        await ZichanAPI.createZichan(values);
+        message.success('资产创建成功');
+      } else {
+        if (editingId) {
+          await ZichanAPI.updateZichan(editingId, values);
+          message.success('资产更新成功');
+        }
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '操作失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理编辑
+  const handleEdit = async (id: number) => {
+    try {
+      setIsLoading(true);
+      const data = await ZichanAPI.getZichan(id);
+      form.setFieldsValue(data);
+      setEditingId(id);
+      setFormMode('edit');
+      setModalVisible(true);
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '获取资产详情失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+  
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await ZichanAPI.deleteZichan(id);
+      message.success('资产删除成功');
+      refetch();
+    } catch (error: any) {
+      message.error(error.response?.data?.error || '删除资产失败');
+    }
+  };
+  
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams({
+      ...searchParams,
+      page: 1, // 重置为第一页
+      asset_name: values.asset_name,
+      device_category: values.device_category,
+      device_status: values.device_status
+    });
+  };
+  
+  // 处理页码变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams({
+      ...searchParams,
+      page,
+      limit: pageSize || 10
+    });
+  };
+  
+  // 处理新增
+  const handleAdd = () => {
+    form.resetFields();
+    setFormMode('create');
+    setEditingId(null);
+    setModalVisible(true);
+  };
+  
+  // 表格列定义
+  const columns = [
+    { 
+      title: '资产ID', 
+      dataIndex: 'id', 
+      key: 'id',
+      width: 80
+    },
+    { 
+      title: '资产名称', 
+      dataIndex: 'asset_name', 
+      key: 'asset_name' 
+    },
+    { 
+      title: '设备分类', 
+      dataIndex: 'device_category', 
+      key: 'device_category',
+      render: (category: DeviceCategory) => {
+        return DeviceCategoryNameMap[category] || '未知分类';
+      }
+    },
+    { 
+      title: '品牌', 
+      dataIndex: 'brand', 
+      key: 'brand' 
+    },
+    { 
+      title: '使用地址', 
+      dataIndex: 'use_address', 
+      key: 'use_address' 
+    },
+    { 
+      title: '运行情况', 
+      dataIndex: 'operation_status', 
+      key: 'operation_status' 
+    },
+    { 
+      title: '设备状态', 
+      dataIndex: 'device_status', 
+      key: 'device_status',
+      render: (status: DeviceStatus) => {
+        return <Tag color={DeviceStatusColorMap[status] || 'default'}>
+          {DeviceStatusNameMap[status] || '未知状态'}
+        </Tag>;
+      }
+    },
+    { 
+      title: 'IP地址', 
+      dataIndex: 'ip_address', 
+      key: 'ip_address' 
+    },
+    { 
+      title: '创建时间', 
+      dataIndex: 'created_at', 
+      key: 'created_at',
+      render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: any, record: ZichanInfo) => (
+        <Space>
+          <Button size="small" type="primary" onClick={() => handleEdit(record.id)}>编辑</Button>
+          <Button size="small" danger onClick={() => 
+            Modal.confirm({
+              title: '确认删除',
+              content: `确定要删除资产"${record.asset_name}"吗?`,
+              onOk: () => handleDelete(record.id)
+            })
+          }>删除</Button>
+        </Space>
+      )
+    }
+  ];
+  
+  return (
+    <div>
+      <Title level={2}>资产管理</Title>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+          <Form.Item name="asset_name" label="资产名称">
+            <Input placeholder="请输入资产名称" allowClear />
+          </Form.Item>
+          <Form.Item name="device_category" label="设备分类">
+            <Select
+              placeholder="请选择设备分类"
+              style={{ width: 140 }}
+              allowClear
+              options={categoryOptions}
+            />
+          </Form.Item>
+          <Form.Item name="device_status" label="设备状态">
+            <Select
+              placeholder="请选择设备状态"
+              style={{ width: 140 }}
+              allowClear
+              options={statusOptions}
+            />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">查询</Button>
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" onClick={handleAdd}>添加资产</Button>
+          </Form.Item>
+        </Form>
+        
+        <Table
+          columns={columns}
+          dataSource={zichanList}
+          rowKey="id"
+          loading={isFetching}
+          pagination={{
+            current: pagination.current,
+            pageSize: pagination.pageSize,
+            total: pagination.total,
+            onChange: handlePageChange,
+            showSizeChanger: true,
+            showTotal: (total) => `共 ${total} 条记录`
+          }}
+        />
+        
+        <Modal
+          title={formMode === 'create' ? '添加资产' : '编辑资产'}
+          open={modalVisible}
+          onCancel={() => setModalVisible(false)}
+          footer={null}
+          width={800}
+        >
+          <Form
+            form={form}
+            layout="vertical"
+            onFinish={handleSubmit}
+          >
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="asset_name"
+                  label="资产名称"
+                  rules={[{ required: true, message: '请输入资产名称' }]}
+                >
+                  <Input placeholder="请输入资产名称" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="device_category"
+                  label="设备分类"
+                  rules={[{ required: true, message: '请选择设备分类' }]}
+                >
+                  <Select
+                    placeholder="请选择设备分类"
+                    options={categoryOptions}
+                  />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="brand"
+                  label="品牌"
+                >
+                  <Input placeholder="请输入品牌" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="supplier"
+                  label="供应商"
+                >
+                  <Input placeholder="请输入供应商" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="use_address"
+                  label="使用地址"
+                >
+                  <Input placeholder="请输入使用地址" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="device_status"
+                  label="设备状态"
+                >
+                  <Select
+                    placeholder="请选择设备状态"
+                    options={statusOptions}
+                  />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={12}>
+                <Form.Item
+                  name="ip_address"
+                  label="IP地址"
+                >
+                  <Input placeholder="请输入IP地址" />
+                </Form.Item>
+              </Col>
+              <Col span={12}>
+                <Form.Item
+                  name="operation_status"
+                  label="运行情况"
+                >
+                  <Input placeholder="请输入运行情况" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Row gutter={16}>
+              <Col span={8}>
+                <Form.Item
+                  name="cpu"
+                  label="CPU信息"
+                >
+                  <Input placeholder="请输入CPU信息" />
+                </Form.Item>
+              </Col>
+              <Col span={8}>
+                <Form.Item
+                  name="memory"
+                  label="内存信息"
+                >
+                  <Input placeholder="请输入内存信息" />
+                </Form.Item>
+              </Col>
+              <Col span={8}>
+                <Form.Item
+                  name="disk"
+                  label="硬盘信息"
+                >
+                  <Input placeholder="请输入硬盘信息" />
+                </Form.Item>
+              </Col>
+            </Row>
+            
+            <Form.Item>
+              <Space>
+                <Button type="primary" htmlType="submit" loading={isLoading}>
+                  {formMode === 'create' ? '创建' : '保存'}
+                </Button>
+                <Button onClick={() => setModalVisible(false)}>取消</Button>
+              </Space>
+            </Form.Item>
+          </Form>
+        </Modal>
+      </Card>
+    </div>
+  );
+};

+ 263 - 0
client/admin/pages_zichan_area.tsx

@@ -0,0 +1,263 @@
+import React, { useState } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, message, Modal, Card, 
+  Popconfirm, Tag, Radio
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { EnableStatus } from '../share/types.ts'
+import type { 
+  ZichanArea, 
+} from '../share/monitorTypes.ts';
+
+import { ZichanAreaAPI } from './api/index.ts';
+
+
+// 资产归属区域管理页面
+export const ZichanAreaPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    name: '',
+    code: ''
+  });
+
+  // 获取资产归属区域列表
+  const { data, isLoading: isTableLoading, refetch } = useQuery({
+    queryKey: ['zichanAreas', searchParams],
+    queryFn: () => ZichanAreaAPI.getZichanAreaList(searchParams)
+  });
+
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<ZichanArea>) => {
+    setIsLoading(true);
+    try {
+      if (formMode === 'create') {
+        await ZichanAreaAPI.createZichanArea(values);
+        message.success('创建成功');
+      } else {
+        await ZichanAreaAPI.updateZichanArea(editingId!, values);
+        message.success('更新成功');
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error) {
+      message.error('操作失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  // 处理编辑
+  const handleEdit = async (record: ZichanArea) => {
+    setFormMode('edit');
+    setEditingId(record.id);
+    form.setFieldsValue(record);
+    setModalVisible(true);
+  };
+
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await ZichanAreaAPI.deleteZichanArea(id);
+      message.success('删除成功');
+      refetch();
+    } catch (error) {
+      message.error('删除失败');
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      ...values,
+      page: 1
+    }));
+  };
+
+  // 处理分页变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page,
+      limit: pageSize || prev.limit
+    }));
+  };
+
+  // 处理新增
+  const handleAdd = () => {
+    setFormMode('create');
+    setEditingId(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80
+    },
+    {
+      title: '区域名称',
+      dataIndex: 'name',
+      key: 'name'
+    },
+    {
+      title: '区域编码',
+      dataIndex: 'code',
+      key: 'code'
+    },
+    {
+      title: '区域图片',
+      dataIndex: 'image_url',
+      key: 'image_url',
+      render: (url?: string) => url ? <img src={url} alt="区域图片" style={{ width: 50, height: 50 }} /> : '-'
+    },
+    {
+      title: '是否启用',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (enabled?: EnableStatus) => (
+        <Tag color={enabled === EnableStatus.ENABLED ? 'success' : 'default'}>
+          {enabled === EnableStatus.ENABLED ? '启用' : '禁用'}
+        </Tag>
+      )
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: any, record: ZichanArea) => (
+        <Space>
+          <Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
+          <Popconfirm
+            title="确定要删除吗?"
+            onConfirm={() => handleDelete(record.id)}
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      )
+    }
+  ];
+
+  return (
+    <div>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch}>
+          <Form.Item name="name" label="区域名称">
+            <Input placeholder="请输入区域名称" />
+          </Form.Item>
+          <Form.Item name="code" label="区域编码">
+            <Input placeholder="请输入区域编码" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">搜索</Button>
+          </Form.Item>
+          <Form.Item>
+            <Button onClick={handleAdd}>新增</Button>
+          </Form.Item>
+        </Form>
+      </Card>
+
+      <Card style={{ marginTop: 16 }}>
+        <Table
+          columns={columns}
+          dataSource={data?.data}
+          loading={isTableLoading}
+          rowKey="id"
+          pagination={{
+            current: searchParams.page,
+            pageSize: searchParams.limit,
+            total: data?.pagination.total,
+            onChange: handlePageChange
+          }}
+        />
+      </Card>
+
+      <Modal
+        title={formMode === 'create' ? '新增资产归属区域' : '编辑资产归属区域'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSubmit}
+        >
+          <Form.Item
+            name="name"
+            label="区域名称"
+            rules={[{ required: true, message: '请输入区域名称' }]}
+          >
+            <Input placeholder="请输入区域名称" />
+          </Form.Item>
+
+          <Form.Item
+            name="code"
+            label="区域编码"
+            rules={[{ required: true, message: '请输入区域编码' }]}
+          >
+            <Input placeholder="请输入区域编码" />
+          </Form.Item>
+
+          <Form.Item
+            name="image_url"
+            label="区域图片"
+          >
+            <Input placeholder="请输入图片URL" />
+          </Form.Item>
+
+          <Form.Item
+            name="description"
+            label="区域描述"
+          >
+            <Input.TextArea placeholder="请输入区域描述" />
+          </Form.Item>
+
+          <Form.Item
+            name="is_enabled"
+            label="是否启用"
+            initialValue={EnableStatus.ENABLED}
+          >
+            <Radio.Group>
+              <Radio value={EnableStatus.ENABLED}>启用</Radio>
+              <Radio value={EnableStatus.DISABLED}>禁用</Radio>
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit" loading={isLoading}>
+                确定
+              </Button>
+              <Button onClick={() => setModalVisible(false)}>
+                取消
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 263 - 0
client/admin/pages_zichan_category.tsx

@@ -0,0 +1,263 @@
+import React, { useState } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, message, Modal,Card, 
+  Popconfirm, Tag, Radio
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import { EnableStatus } from '../share/types.ts'
+import type { 
+  ZichanCategory
+} from '../share/monitorTypes.ts';
+
+import { ZichanCategoryAPI } from './api/index.ts';
+
+
+// 资产分类管理页面
+export const ZichanCategoryPage = () => {
+  const [form] = Form.useForm();
+  const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+  const [editingId, setEditingId] = useState<number | null>(null);
+  const [modalVisible, setModalVisible] = useState(false);
+  const [isLoading, setIsLoading] = useState(false);
+  const [searchParams, setSearchParams] = useState({
+    page: 1,
+    limit: 10,
+    name: '',
+    code: ''
+  });
+
+  // 获取资产分类列表
+  const { data, isLoading: isTableLoading, refetch } = useQuery({
+    queryKey: ['zichanCategories', searchParams],
+    queryFn: () => ZichanCategoryAPI.getZichanCategoryList(searchParams)
+  });
+
+  // 处理表单提交
+  const handleSubmit = async (values: Partial<ZichanCategory>) => {
+    setIsLoading(true);
+    try {
+      if (formMode === 'create') {
+        await ZichanCategoryAPI.createZichanCategory(values);
+        message.success('创建成功');
+      } else {
+        await ZichanCategoryAPI.updateZichanCategory(editingId!, values);
+        message.success('更新成功');
+      }
+      setModalVisible(false);
+      refetch();
+    } catch (error) {
+      message.error('操作失败');
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  // 处理编辑
+  const handleEdit = async (record: ZichanCategory) => {
+    setFormMode('edit');
+    setEditingId(record.id);
+    form.setFieldsValue(record);
+    setModalVisible(true);
+  };
+
+  // 处理删除
+  const handleDelete = async (id: number) => {
+    try {
+      await ZichanCategoryAPI.deleteZichanCategory(id);
+      message.success('删除成功');
+      refetch();
+    } catch (error) {
+      message.error('删除失败');
+    }
+  };
+
+  // 处理搜索
+  const handleSearch = (values: any) => {
+    setSearchParams(prev => ({
+      ...prev,
+      ...values,
+      page: 1
+    }));
+  };
+
+  // 处理分页变化
+  const handlePageChange = (page: number, pageSize?: number) => {
+    setSearchParams(prev => ({
+      ...prev,
+      page,
+      limit: pageSize || prev.limit
+    }));
+  };
+
+  // 处理新增
+  const handleAdd = () => {
+    setFormMode('create');
+    setEditingId(null);
+    form.resetFields();
+    setModalVisible(true);
+  };
+
+  // 表格列定义
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+      key: 'id',
+      width: 80
+    },
+    {
+      title: '分类名称',
+      dataIndex: 'name',
+      key: 'name'
+    },
+    {
+      title: '分类编码',
+      dataIndex: 'code',
+      key: 'code'
+    },
+    {
+      title: '分类图片',
+      dataIndex: 'image_url',
+      key: 'image_url',
+      render: (url?: string) => url ? <img src={url} alt="分类图片" style={{ width: 50, height: 50 }} /> : '-'
+    },
+    {
+      title: '是否启用',
+      dataIndex: 'is_enabled',
+      key: 'is_enabled',
+      render: (enabled?: EnableStatus) => (
+        <Tag color={enabled === EnableStatus.ENABLED ? 'success' : 'default'}>
+          {enabled === EnableStatus.ENABLED ? '启用' : '禁用'}
+        </Tag>
+      )
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'created_at',
+      key: 'created_at',
+      render: (date: Date) => dayjs(date).format('YYYY-MM-DD HH:mm:ss')
+    },
+    {
+      title: '操作',
+      key: 'action',
+      width: 200,
+      render: (_: any, record: ZichanCategory) => (
+        <Space>
+          <Button type="link" onClick={() => handleEdit(record)}>编辑</Button>
+          <Popconfirm
+            title="确定要删除吗?"
+            onConfirm={() => handleDelete(record.id)}
+          >
+            <Button type="link" danger>删除</Button>
+          </Popconfirm>
+        </Space>
+      )
+    }
+  ];
+
+  return (
+    <div>
+      <Card>
+        <Form layout="inline" onFinish={handleSearch}>
+          <Form.Item name="name" label="分类名称">
+            <Input placeholder="请输入分类名称" />
+          </Form.Item>
+          <Form.Item name="code" label="分类编码">
+            <Input placeholder="请输入分类编码" />
+          </Form.Item>
+          <Form.Item>
+            <Button type="primary" htmlType="submit">搜索</Button>
+          </Form.Item>
+          <Form.Item>
+            <Button onClick={handleAdd}>新增</Button>
+          </Form.Item>
+        </Form>
+      </Card>
+
+      <Card style={{ marginTop: 16 }}>
+        <Table
+          columns={columns}
+          dataSource={data?.data}
+          loading={isTableLoading}
+          rowKey="id"
+          pagination={{
+            current: searchParams.page,
+            pageSize: searchParams.limit,
+            total: data?.pagination.total,
+            onChange: handlePageChange
+          }}
+        />
+      </Card>
+
+      <Modal
+        title={formMode === 'create' ? '新增资产分类' : '编辑资产分类'}
+        open={modalVisible}
+        onCancel={() => setModalVisible(false)}
+        footer={null}
+      >
+        <Form
+          form={form}
+          layout="vertical"
+          onFinish={handleSubmit}
+        >
+          <Form.Item
+            name="name"
+            label="分类名称"
+            rules={[{ required: true, message: '请输入分类名称' }]}
+          >
+            <Input placeholder="请输入分类名称" />
+          </Form.Item>
+
+          <Form.Item
+            name="code"
+            label="分类编码"
+            rules={[{ required: true, message: '请输入分类编码' }]}
+          >
+            <Input placeholder="请输入分类编码" />
+          </Form.Item>
+
+          <Form.Item
+            name="image_url"
+            label="分类图片"
+          >
+            <Input placeholder="请输入图片URL" />
+          </Form.Item>
+
+          <Form.Item
+            name="description"
+            label="分类描述"
+          >
+            <Input.TextArea placeholder="请输入分类描述" />
+          </Form.Item>
+
+          <Form.Item
+            name="is_enabled"
+            label="是否启用"
+            initialValue={EnableStatus.ENABLED}
+          >
+            <Radio.Group>
+              <Radio value={EnableStatus.ENABLED}>启用</Radio>
+              <Radio value={EnableStatus.DISABLED}>禁用</Radio>
+            </Radio.Group>
+          </Form.Item>
+
+          <Form.Item>
+            <Space>
+              <Button type="primary" htmlType="submit" loading={isLoading}>
+                确定
+              </Button>
+              <Button onClick={() => setModalVisible(false)}>
+                取消
+              </Button>
+            </Space>
+          </Form.Item>
+        </Form>
+      </Modal>
+    </div>
+  );
+};

+ 401 - 0
client/admin/pages_zichan_transfer.tsx

@@ -0,0 +1,401 @@
+import React, { useState, useEffect } from 'react';
+import { 
+  Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Row, Col, Typography,
+  Tag, DatePicker
+} from 'antd';
+import { 
+  useQuery,
+} from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import 'dayjs/locale/zh-cn';
+import type { 
+  ZichanInfo, ZichanTransLog
+} from '../share/monitorTypes.ts';
+import {
+  AssetTransferType, 
+  AssetTransferTypeNameMap, AssetTransferTypeColorMap,
+} from '../share/monitorTypes.ts';
+
+import { ZichanAPI, ZichanTransferAPI } from './api/index.ts';
+
+import { getEnumOptions } from './utils.ts';
+
+const { Title } = Typography;
+
+// 资产流转管理页面
+export const ZichanTransferPage = () => {
+    const [form] = Form.useForm();
+    const [formMode, setFormMode] = useState<'create' | 'edit'>('create');
+    const [editingId, setEditingId] = useState<number | null>(null);
+    const [modalVisible, setModalVisible] = useState(false);
+    const [isLoading, setIsLoading] = useState(false);
+    const [zichanOptions, setZichanOptions] = useState<{ label: string, value: number }[]>([]);
+    const [searchParams, setSearchParams] = useState({
+      page: 1,
+      limit: 10,
+      asset_id: undefined as number | undefined,
+      asset_transfer: undefined as AssetTransferType | undefined
+    });
+    
+    // 资产流转类型选项
+    const transferTypeOptions = getEnumOptions(AssetTransferType, AssetTransferTypeNameMap);
+    
+    // 获取资产列表用于下拉选择
+    useEffect(() => {
+      const fetchZichanList = async () => {
+        try {
+          const result = await ZichanAPI.getZichanList({ limit: 100 });
+          const options = result.data.map((item: ZichanInfo) => ({
+            label: `${item.asset_name} (ID:${item.id})`,
+            value: item.id
+          }));
+          setZichanOptions(options);
+        } catch (error) {
+          console.error('获取资产列表失败', error);
+        }
+      };
+      
+      fetchZichanList();
+    }, []);
+    
+    // 查询资产流转记录列表
+    const { 
+      data: transferResult = { data: [], pagination: { total: 0, current: 1, pageSize: 10 } }, 
+      isLoading: isFetching,
+      refetch
+    } = useQuery({
+      queryKey: ['zichan-transfer', searchParams],
+      queryFn: () => ZichanTransferAPI.getTransferList(searchParams),
+    });
+    
+    // 提取数据和分页信息
+    const transferList = transferResult.data || [];
+    const pagination = transferResult.pagination || { total: 0, current: 1, pageSize: 10 };
+    
+    // 处理表单提交
+    const handleSubmit = async (values: Partial<ZichanTransLog>) => {
+      try {
+        setIsLoading(true);
+        
+        // 创建一个新的对象以避免修改原始值
+        const submitData = { ...values };
+        
+        // 处理流转时间格式
+        if (submitData.transfer_time && dayjs.isDayjs(submitData.transfer_time)) {
+          // 使用字符串格式,确保后端可以正确处理
+          submitData.transfer_time = (submitData.transfer_time as any).format('YYYY-MM-DD HH:mm:ss');
+        }
+        
+        if (formMode === 'create') {
+          await ZichanTransferAPI.createTransfer(submitData);
+          message.success('资产流转记录创建成功');
+        } else {
+          if (editingId) {
+            await ZichanTransferAPI.updateTransfer(editingId, submitData);
+            message.success('资产流转记录更新成功');
+          }
+        }
+        setModalVisible(false);
+        refetch();
+      } catch (error: any) {
+        message.error(error.response?.data?.error || '操作失败');
+      } finally {
+        setIsLoading(false);
+      }
+    };
+    
+    // 处理编辑
+    const handleEdit = async (id: number) => {
+      try {
+        setIsLoading(true);
+        const data = await ZichanTransferAPI.getTransfer(id);
+        
+        // 处理日期格式
+        if (data.transfer_time) {
+          // 使用dayjs解析时间,确保传入字符串
+          data.transfer_time = dayjs(String(data.transfer_time));
+        }
+        
+        form.setFieldsValue(data);
+        setEditingId(id);
+        setFormMode('edit');
+        setModalVisible(true);
+      } catch (error: any) {
+        message.error(error.response?.data?.error || '获取资产流转记录详情失败');
+      } finally {
+        setIsLoading(false);
+      }
+    };
+    
+    // 处理删除
+    const handleDelete = async (id: number) => {
+      try {
+        await ZichanTransferAPI.deleteTransfer(id);
+        message.success('资产流转记录删除成功');
+        refetch();
+      } catch (error: any) {
+        message.error(error.response?.data?.error || '删除资产流转记录失败');
+      }
+    };
+    
+    // 处理搜索
+    const handleSearch = (values: any) => {
+      setSearchParams({
+        ...searchParams,
+        page: 1, // 重置为第一页
+        asset_id: values.asset_id,
+        asset_transfer: values.asset_transfer
+      });
+    };
+    
+    // 处理页码变化
+    const handlePageChange = (page: number, pageSize?: number) => {
+      setSearchParams({
+        ...searchParams,
+        page,
+        limit: pageSize || 10
+      });
+    };
+    
+    // 处理新增
+    const handleAdd = () => {
+      form.resetFields();
+      form.setFieldsValue({
+        transfer_time: dayjs() // 默认设置为当前时间
+      });
+      setFormMode('create');
+      setEditingId(null);
+      setModalVisible(true);
+    };
+    
+    // 表格列定义
+    const columns = [
+      { 
+        title: 'ID', 
+        dataIndex: 'id', 
+        key: 'id',
+        width: 80 
+      },
+      { 
+        title: '资产', 
+        dataIndex: 'asset_id', 
+        key: 'asset_id',
+        render: (asset_id: number, record: ZichanTransLog) => {
+          return record.asset_info ? record.asset_info.asset_name : `资产ID: ${asset_id}`;
+        }
+      },
+      { 
+        title: '流转类型', 
+        dataIndex: 'asset_transfer', 
+        key: 'asset_transfer',
+        render: (type: AssetTransferType) => {
+          return <Tag color={AssetTransferTypeColorMap[type] || 'default'}>
+            {AssetTransferTypeNameMap[type] || '未知类型'}
+          </Tag>;
+        }
+      },
+      { 
+        title: '人员', 
+        dataIndex: 'person', 
+        key: 'person' 
+      },
+      { 
+        title: '部门', 
+        dataIndex: 'department', 
+        key: 'department' 
+      },
+      { 
+        title: '联系电话', 
+        dataIndex: 'phone', 
+        key: 'phone' 
+      },
+      { 
+        title: '流转事由', 
+        dataIndex: 'transfer_reason', 
+        key: 'transfer_reason',
+        ellipsis: true 
+      },
+      { 
+        title: '流转时间', 
+        dataIndex: 'transfer_time', 
+        key: 'transfer_time',
+        render: (date: string) => date ? dayjs(date).format('YYYY-MM-DD HH:mm:ss') : '-'
+      },
+      {
+        title: '操作',
+        key: 'action',
+        width: 200,
+        render: (_: any, record: ZichanTransLog) => (
+          <Space>
+            <Button size="small" type="primary" onClick={() => handleEdit(record.id)}>编辑</Button>
+            <Button size="small" danger onClick={() => 
+              Modal.confirm({
+                title: '确认删除',
+                content: `确定要删除此资产流转记录吗?`,
+                onOk: () => handleDelete(record.id)
+              })
+            }>删除</Button>
+          </Space>
+        )
+      }
+    ];
+    
+    return (
+      <div>
+        <Title level={2}>资产流转管理</Title>
+        <Card>
+          <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
+            <Form.Item name="asset_id" label="选择资产">
+              <Select
+                placeholder="请选择资产"
+                style={{ width: 240 }}
+                allowClear
+                options={zichanOptions}
+                showSearch
+                filterOption={(input, option) =>
+                  (String(option?.label ?? '')).toLowerCase().includes(input.toLowerCase())
+                }
+              />
+            </Form.Item>
+            <Form.Item name="asset_transfer" label="流转类型">
+              <Select
+                placeholder="请选择流转类型"
+                style={{ width: 140 }}
+                allowClear
+                options={transferTypeOptions}
+              />
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" htmlType="submit">查询</Button>
+            </Form.Item>
+            <Form.Item>
+              <Button type="primary" onClick={handleAdd}>新增流转记录</Button>
+            </Form.Item>
+          </Form>
+          
+          <Table
+            columns={columns}
+            dataSource={transferList}
+            rowKey="id"
+            loading={isFetching}
+            pagination={{
+              current: pagination.current,
+              pageSize: pagination.pageSize,
+              total: pagination.total,
+              onChange: handlePageChange,
+              showSizeChanger: true,
+              showTotal: (total) => `共 ${total} 条记录`
+            }}
+          />
+          
+          <Modal
+            title={formMode === 'create' ? '新增资产流转记录' : '编辑资产流转记录'}
+            open={modalVisible}
+            onCancel={() => setModalVisible(false)}
+            footer={null}
+            width={720}
+          >
+            <Form
+              form={form}
+              layout="vertical"
+              onFinish={handleSubmit}
+            >
+              <Row gutter={16}>
+                <Col span={12}>
+                  <Form.Item
+                    name="asset_id"
+                    label="选择资产"
+                    rules={[{ required: true, message: '请选择资产' }]}
+                  >
+                    <Select
+                      placeholder="请选择资产"
+                      options={zichanOptions}
+                      showSearch
+                      filterOption={(input, option) =>
+                        (String(option?.label ?? '')).toLowerCase().includes(input.toLowerCase())
+                      }
+                    />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    name="asset_transfer"
+                    label="流转类型"
+                    rules={[{ required: true, message: '请选择流转类型' }]}
+                  >
+                    <Select
+                      placeholder="请选择流转类型"
+                      options={transferTypeOptions}
+                    />
+                  </Form.Item>
+                </Col>
+              </Row>
+              
+              <Row gutter={16}>
+                <Col span={12}>
+                  <Form.Item
+                    name="person"
+                    label="人员"
+                    rules={[{ required: true, message: '请输入人员姓名' }]}
+                  >
+                    <Input placeholder="请输入人员姓名" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    name="department"
+                    label="部门"
+                  >
+                    <Input placeholder="请输入部门" />
+                  </Form.Item>
+                </Col>
+              </Row>
+              
+              <Row gutter={16}>
+                <Col span={12}>
+                  <Form.Item
+                    name="phone"
+                    label="联系电话"
+                  >
+                    <Input placeholder="请输入联系电话" />
+                  </Form.Item>
+                </Col>
+                <Col span={12}>
+                  <Form.Item
+                    name="transfer_time"
+                    label="流转时间"
+                    rules={[{ required: true, message: '请选择流转时间' }]}
+                  >
+                    <DatePicker
+                      showTime={{ format: 'HH:mm:ss' }}
+                      format="YYYY-MM-DD HH:mm:ss"
+                      style={{ width: '100%' }}
+                      placeholder="请选择流转时间"
+                    />
+                  </Form.Item>
+                </Col>
+              </Row>
+              
+              <Form.Item
+                name="transfer_reason"
+                label="流转事由"
+              >
+                <Input.TextArea rows={4} placeholder="请输入流转事由" />
+              </Form.Item>
+              
+              <Form.Item>
+                <Space>
+                  <Button type="primary" htmlType="submit" loading={isLoading}>
+                    {formMode === 'create' ? '创建' : '保存'}
+                  </Button>
+                  <Button onClick={() => setModalVisible(false)}>取消</Button>
+                </Space>
+              </Form.Item>
+            </Form>
+          </Modal>
+        </Card>
+      </div>
+    );
+  };

+ 257 - 0
client/admin/routes.tsx

@@ -0,0 +1,257 @@
+import React from 'react';
+import { createBrowserRouter, Navigate } from 'react-router';
+import { ProtectedRoute } from './components_protected_route.tsx';
+import { MainLayout } from './layouts/MainLayout.tsx';
+import { ErrorPage } from './components/ErrorPage.tsx';
+import { NotFoundPage } from './components/NotFoundPage.tsx';
+import { DashboardPage } from './pages_dashboard.tsx';
+import { UsersPage } from './pages_users.tsx';
+import { FileLibraryPage } from './pages_file_library.tsx';
+import { KnowInfoPage } from './pages_know_info.tsx';
+import { MessagesPage } from './pages_messages.tsx';
+import { SettingsPage } from './pages_settings.tsx';
+import { ThemeSettingsPage } from './pages_theme_settings.tsx';
+import { ChartDashboardPage } from './pages_chart.tsx';
+import { LoginMapPage } from './pages_map.tsx';
+import { LoginPage } from './pages_login_reg.tsx';
+import { ZichanPage } from './pages_zichan.tsx';
+import { ZichanTransferPage } from './pages_zichan_transfer.tsx'
+import { ZichanCategoryPage } from './pages_zichan_category.tsx'
+import { ZichanAreaPage } from './pages_zichan_area.tsx'
+import { DeviceTypesPage } from './pages_device_types.tsx'
+import { DeviceInstancesPage } from "./pages_device_instances.tsx";
+import { RackServerTypePage } from "./pages_rack_server_type.tsx";
+import { RackServerPage } from "./pages_rack_server.tsx";
+import { RackManagePage } from "./pages_rack.tsx";
+import { DeviceMonitorPage } from "./pages_device_monitor.tsx";
+import { AlertRecordsPage } from "./pages_alert_records.tsx";
+import { AlertHandlePage } from "./pages_alert_handle.tsx";
+import { AlertHandleLogsPage } from "./pages_alert_handle_logs.tsx";
+import { AlertNotifyConfigPage } from "./pages_alert_notify_config.tsx";
+import { DeviceAlertRulePage } from "./pages_device_alert_rule.tsx";
+import { DeviceMapManagePage } from "./pages_device_map.tsx";
+import { AssetCategoryChartPage } from "./pages_asset_category_chart.tsx";
+import { AssetTransferChartPage } from "./pages_asset_transfer_chart.tsx";
+import { AlertTrendChartPage } from "./pages_alert_trend_chart.tsx";
+import { OnlineDevicesChartPage } from "./pages_online_devices_chart.tsx";
+import { GreenhouseProtocolPage } from "./pages_greenhouse_protocol.tsx";
+import { WorkOrdersPage } from './pages_work_orders.tsx';
+import { ModbusRtuDevicePage } from './pages_modbus_rtu_device.tsx';
+import { InspectionsPage } from './pages_inspections.tsx';
+import TemperatureHumidityPage from './pages_temperature_humidity.tsx';
+import SmokeWaterPage from './pages_smoke_water.tsx';
+import { SmsModulePage } from './pages_sms_module.tsx';
+
+export const router = createBrowserRouter([
+  {
+    path: '/',
+    element: <Navigate to="/admin" replace />
+  },
+  {
+    path: '/admin/login',
+    element: <LoginPage />
+  },
+  {
+    path: '/admin',
+    element: (
+      <ProtectedRoute>
+        <MainLayout />
+      </ProtectedRoute>
+    ),
+    children: [
+      {
+        index: true,
+        element: <Navigate to="/admin/dashboard" />
+      },
+      {
+        path: 'dashboard',
+        element: <DashboardPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'users',
+        element: <UsersPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'settings',
+        element: <SettingsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'theme-settings',
+        element: <ThemeSettingsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'chart-dashboard',
+        element: <ChartDashboardPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'map-dashboard',
+        element: <LoginMapPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'know-info',
+        element: <KnowInfoPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'file-library',
+        element: <FileLibraryPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'messages',
+        element: <MessagesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'zichan',
+        element: <ZichanPage />,
+        errorElement: <ErrorPage />
+      },{
+        path: 'zichan-categorys',
+        element: <ZichanCategoryPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'zichan-areas',
+        element: <ZichanAreaPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'zichan-transfer',
+        element: <ZichanTransferPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'device-types',
+        element: <DeviceTypesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'device-instances',
+        element: <DeviceInstancesPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'rack-server-types',
+        element: <RackServerTypePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'racks',
+        element: <RackManagePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'rack-servers',
+        element: <RackServerPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'device-monitor',
+        element: <DeviceMonitorPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'alert-records',
+        element: <AlertRecordsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'alert-handle/:id',
+        element: <AlertHandlePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'alert-handle-logs',
+        element: <AlertHandleLogsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'alert-notify-configs',
+        element: <AlertNotifyConfigPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'device-alert-rules',
+        element: <DeviceAlertRulePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'device-map',
+        element: <DeviceMapManagePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'analysis/asset-category',
+        element: <AssetCategoryChartPage />,
+        errorElement: <ErrorPage />
+      },  
+      {
+        path: 'analysis/asset-transfer',
+        element: <AssetTransferChartPage />,
+        errorElement: <ErrorPage />
+      },  
+      {
+        path: 'analysis/alert-trend',
+        element: <AlertTrendChartPage />,
+        errorElement: <ErrorPage />
+      },  
+      {
+        path: 'analysis/online-devices',
+        element: <OnlineDevicesChartPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'greenhouse-protocol',
+        element: <GreenhouseProtocolPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'work-orders',
+        element: <WorkOrdersPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'modbus-rtu-devices',
+        element: <ModbusRtuDevicePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'inspections',
+        element: <InspectionsPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'temperature-humidity',
+        element: <TemperatureHumidityPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'smoke-water',
+        element: <SmokeWaterPage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: 'sms-module',
+        element: <SmsModulePage />,
+        errorElement: <ErrorPage />
+      },
+      {
+        path: '*',
+        element: <NotFoundPage />,
+        errorElement: <ErrorPage />
+      },
+    ],
+  },
+  {
+    path: '*',
+    element: <NotFoundPage />,
+    errorElement: <ErrorPage />
+  },
+]);

+ 35 - 0
client/admin/style_amap.css

@@ -0,0 +1,35 @@
+
+.marker-label {
+  color: #333;
+  font-size: 12px;
+  background: #fff;
+  padding: 2px 6px;
+  border: 1px solid #ccc;
+  border-radius: 2px;
+  white-space: nowrap;
+  box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
+  display: flex;
+  align-items: center;
+  gap: 4px;
+}
+
+.amap-marker-label {
+    border: none !important;
+    background: none !important;
+}
+
+.status-dot {
+  display: inline-block;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  margin-left: 4px;
+}
+
+.status-dot.online {
+  background-color: #4CAF50;
+}
+
+.status-dot.offline {
+  background-color: #FF5252;
+}

+ 43 - 0
client/admin/utils.ts

@@ -0,0 +1,43 @@
+import type { GlobalConfig } from '../share/types.ts';
+
+export function getEnumOptions<T extends string | number, M extends Record<T, string>>(enumObj: Record<string, T>, nameMap: M) {
+  return Object.entries(enumObj)
+    .filter(([_key, value]) => !isNaN(Number(value)) || typeof value === 'string')  // 保留数字和字符串类型的值
+    .filter(([key, _value]) => isNaN(Number(key)))  // 过滤掉数字键(枚举的反向映射)
+    .map(([_key, value]) => ({
+      label: nameMap[value as T],
+      value: value
+    }));
+}
+
+/**
+ * 获取全局配置项 (严格类型版本)
+ * @param key 配置键名
+ * @returns 配置值或undefined
+ */
+export function getGlobalConfig<T extends keyof GlobalConfig>(key: T): GlobalConfig[T] | undefined {
+  return (window as typeof window & { CONFIG?: GlobalConfig }).CONFIG?.[key];
+}
+
+/**
+ * 验证URL格式
+ * @param url 待验证URL
+ * @returns 验证结果
+ */
+export const validateUrl = (url: string): boolean => {
+  try {
+    new URL(url);
+    return true;
+  } catch {
+    return false;
+  }
+};
+
+/**
+ * 验证Authorization头格式
+ * @param auth 待验证字符串
+ * @returns 验证结果
+ */
+export const validateAuthHeader = (auth: string): boolean => {
+  return /^Basic [A-Za-z0-9+/]+={0,2}$/.test(auth);
+};

+ 47 - 0
client/admin/web_app.tsx

@@ -0,0 +1,47 @@
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import { RouterProvider } from 'react-router';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import dayjs from 'dayjs';
+import weekday from 'dayjs/plugin/weekday';
+import localeData from 'dayjs/plugin/localeData';
+import 'dayjs/locale/zh-cn';
+
+import { AuthProvider } from './hooks_sys.tsx';
+import { ThemeProvider } from './hooks_sys.tsx';
+import { router } from './routes.tsx';
+import type { GlobalConfig } from '../share/types.ts';
+
+// 配置 dayjs 插件
+dayjs.extend(weekday);
+dayjs.extend(localeData);
+
+// 设置 dayjs 语言
+dayjs.locale('zh-cn');
+
+// 创建QueryClient实例
+const queryClient = new QueryClient();
+
+// 声明全局配置对象类型
+declare global {
+  interface Window {
+    CONFIG?: GlobalConfig;
+  }
+}
+
+// 应用入口组件
+const App = () => {
+  return <RouterProvider router={router} />
+};
+
+// 渲染应用
+const root = createRoot(document.getElementById('root') as HTMLElement);
+root.render(
+  <QueryClientProvider client={queryClient}>
+    <ThemeProvider>
+      <AuthProvider>
+        <App />
+      </AuthProvider>
+    </ThemeProvider>
+  </QueryClientProvider>
+);

+ 381 - 0
client/big/api.ts

@@ -0,0 +1,381 @@
+import axios from 'axios';
+import * as THREE from 'three';
+import { 
+  AlarmDeviceData,
+  CategoryChartData,
+  CategoryChartDataWithPercent,
+  OnlineRateChartData,
+  StateChartData,
+  StateChartDataWithPercent,
+  AlarmChartData,
+  DeviceWithAssetInfo,
+} from '../share/monitorTypes.ts';
+
+
+// API请求参数类型定义
+interface OnlineRateChartParams {
+  created_at?: {
+    $gte: string;
+    $lte: string;
+  };
+  dimension?: {
+    $eq: 'hour' | 'day' | 'month';
+  };
+  pagination?: {
+    page: number;
+    pageSize: number;
+  };
+}
+
+interface AlarmChartParams {
+  created_at?: {
+    $gte: string;
+    $lte: string;
+  };
+  dimension?: {
+    $eq: 'hour' | 'day' | 'month';
+  };
+}
+
+interface DeviceInstancesParams {
+  is_deleted?: number;
+}
+
+interface RackQueryParams {
+  is_deleted?: number;
+}
+
+interface RackServerQueryParams {
+  rack_id?: number;
+  is_deleted?: number;
+}
+
+interface RackServerTypeParams {
+  is_enabled?: number;
+  is_deleted?: number;
+  page?: number;
+  pageSize?: number;
+}
+
+// 机柜数据
+interface RackData {
+  id: number;
+  rack_name: string;
+  rack_code: string;
+  capacity: number;
+  position_x: number;
+  position_y: number;
+  position_z: number;
+  area?: string;
+  room?: string;
+  remark?: string;
+  is_disabled: number;
+  is_deleted: number;
+  created_at: string;
+  updated_at: string;
+}
+
+// 机柜服务器数据
+interface RackServerData {
+  id: number;
+  rack_id: number;
+  asset_id: number;
+  start_position: number;
+  size: number;
+  server_type?: string;
+  remark?: string;
+  is_disabled: number;
+  is_deleted: number;
+  created_at: string;
+  updated_at: string;
+  // 关联字段 asset
+  asset_name: string;
+  device_category: number;
+  ip_address?: string;
+  device_status: number;
+  network_status: number;
+  packet_loss: number;
+  cpu?: string;
+  memory?: string;
+  disk?: string;
+  // 关联字段 rack
+  rack_name: string;
+  rack_code: string;
+}
+
+// 设备类型图标数据
+interface DeviceIconData {
+  id: number;
+  category_id: number;
+  icon: string;
+  icon_name?: string;
+  icon_type: string;
+  sort: number;
+  is_default: number;
+  is_disabled: number;
+  is_deleted: number;
+  created_at: string;
+  updated_at: string;
+  category?: {
+    name: string;
+  };
+}
+
+// 机柜服务器类型数据
+interface RackServerType {
+  /** 主键ID */
+  id: number;
+  
+  /** 类型名称 */
+  name: string;
+  
+  /** 类型编码 */
+  code: string;
+
+  /** 类型图片 */
+  image_url?: string;
+  
+  /** 类型描述 */
+  description?: string;
+  
+  /** 是否启用 (0否 1是) */
+  is_enabled?: number;
+  
+  /** 是否被删除 (0否 1是) */
+  is_deleted?: number;
+  
+  /** 创建时间 */
+  created_at: string;
+  
+  /** 更新时间 */
+  updated_at: string;
+}
+
+// 服务器基本信息接口
+export interface ServerData {
+  id: number;
+  slot: number;
+  u: number;
+  type: number;
+  name: string;
+  ip: string;
+  cpu: string;
+  memory: string;
+  disk: string;
+  deviceStatus: number;
+  networkStatus: number;
+  packetLoss: number;
+}
+
+// 设备状态和监控数据接口
+export interface DeviceStatusInfo {
+  status: 'online' | 'offline' | 'warning';
+  usage: {
+    cpu: number;
+    memory: number;
+    disk: number;
+  };
+}
+
+// 机柜配置接口
+export interface RackConfig {
+  position: THREE.Vector3;
+  serverCount: number;
+  id: number;
+  name: string;
+  servers: ServerData[];
+}
+
+// 服务器图标接口
+export interface ServerIconConfig {
+  textureUrl: string | null;
+  color: number;
+}
+
+export interface ServerIconConfigs {
+  [key: number]: ServerIconConfig;
+} 
+
+
+
+
+const API_BASE_URL = '/api';
+// 从全局配置中获取OSS_HOST,如果不存在则使用默认值
+const OSS_BASE_URL = window.CONFIG?.OSS_BASE_URL || '';
+
+
+// 计算风险等级
+const calculateRiskLevel = (onlineRate: number): { level: string; color: string } => {
+  if (onlineRate >= 95) {
+    return { level: '健康', color: '#52c41a' }; // 绿色
+  } else if (onlineRate >= 80) {
+    return { level: '风险低', color: '#faad14' }; // 黄色
+  } else if (onlineRate >= 70) {
+    return { level: '风险中', color: '#faad14' }; // 黄色
+  } else {
+    return { level: '高风险', color: '#f5222d' }; // 红色
+  }
+};
+
+// 统一将查询函数提取到顶部
+export const queryFns = {
+  // 资产分类数据查询
+  fetchCategoryData: async (): Promise<CategoryChartDataWithPercent[]> => {
+    const res = await axios.get<CategoryChartData[]>(`${API_BASE_URL}/big/zichan_category_chart`);
+    
+    // 预先计算百分比
+    const data = res.data;
+    const total = data.reduce((sum: number, item: CategoryChartData) => sum + item['设备数'], 0);
+    
+    // 为每个数据项添加百分比字段
+    return data.map(item => ({
+      ...item,
+      百分比: total > 0 ? (item['设备数'] / total * 100).toFixed(1) : '0'
+    }));
+  },
+
+  // 在线率变化数据查询
+  fetchOnlineRateData: async (): Promise<OnlineRateChartData[]> => {
+    // 直接使用扁平化参数
+    const params = {
+      // created_at_gte: dayjs().subtract(7, 'day').format('YYYY-MM-DD HH:mm:ss'),
+      // created_at_lte: dayjs().format('YYYY-MM-DD HH:mm:ss'),
+      // dimension: 'day',
+      // page: 1,
+      // pageSize: 1000
+    };
+    
+    const res = await axios.get<OnlineRateChartData[]>(`${API_BASE_URL}/big/zichan_online_rate_chart`, { params });
+    return res.data;
+  },
+
+  // 资产流转状态数据查询
+  fetchStateData: async (): Promise<StateChartDataWithPercent[]> => {
+    const res = await axios.get<StateChartData[]>(`${API_BASE_URL}/big/zichan_state_chart`);
+    
+    // 预先计算百分比
+    const data = res.data;
+    const total = data.reduce((sum: number, item: StateChartData) => sum + item['设备数'], 0);
+    
+    // 为每个数据项添加百分比字段
+    return data.map(item => ({
+      ...item,
+      百分比: total > 0 ? (item['设备数'] / total * 100).toFixed(1) : '0'
+    }));
+  },
+
+  // 告警数据变化查询
+  fetchAlarmData: async (): Promise<AlarmChartData[]> => {
+    // 直接使用扁平化参数
+    const params = {
+      // created_at_gte: dayjs().startOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      // created_at_lte: dayjs().endOf('day').format('YYYY-MM-DD HH:mm:ss'),
+      // dimension: 'hour'
+    };
+    
+    const res = await axios.get<AlarmChartData[]>(`${API_BASE_URL}/big/zichan_alarm_chart`, { params });
+    return res.data;
+  },
+
+  // 获取设备数据
+  fetchDeviceMetrics: async () => {
+    const params = { 
+      is_deleted: 0
+    };
+    const response = await axios.get<DeviceWithAssetInfo[]>(`${API_BASE_URL}/big/device-instances`, { params });
+
+    const devices = response.data;
+    const totalDevices = devices.length;
+    const onlineDevices = devices.filter(device => device.device_status === 1).length;
+    const offlineDevices = totalDevices - onlineDevices;
+    const onlineRate = totalDevices ? ((onlineDevices / totalDevices) * 100) : 0;
+    const riskLevel = calculateRiskLevel(onlineRate);
+
+    return {
+      totalDevices,
+      onlineDevices,
+      offlineDevices,
+      onlineRate: onlineRate.toFixed(2),
+      riskLevel
+    };
+  },
+
+  // 获取告警设备数据
+  fetchTopAlarmDevices: async (): Promise<AlarmDeviceData[]> => {
+    const response = await axios.get<AlarmDeviceData[]>(`${API_BASE_URL}/big/zichan_alarm_device`);
+    return response.data;
+  },
+
+  // 获取机柜配置数据
+  fetchRackConfigs: async (): Promise<RackConfig[]> => {
+    const params: RackQueryParams = {
+      is_deleted: 0
+    }
+    // 1. 获取机柜数据
+    const rackResponse = await axios.get<RackData[]>(`${API_BASE_URL}/big/rack`, { params });
+
+    const racks = rackResponse.data;
+    
+    // 2. 获取所有机柜的服务器数据
+    const serverPromises = racks.map(async (rack: RackData) => {
+      const params: RackServerQueryParams = { 
+        rack_id: rack.id,
+        is_deleted: 0
+      }
+      const serverResponse = await axios.get<RackServerData[]>(`${API_BASE_URL}/big/rack-server`, { params });
+
+      // 3. 获取每个服务器对应的基本信息
+      let servers: ServerData[] = [];
+      try {
+        servers = serverResponse.data.map((server: RackServerData) => ({
+          id: server.id,
+          slot: server.start_position,
+          u: server.size,
+          type: server.device_category,
+          name: server.asset_name || '',
+          ip: server.ip_address || '',
+          cpu: server.cpu || '',
+          memory: server.memory || '',
+          disk: server.disk || '',
+          deviceStatus: server.device_status || 0,
+          networkStatus: server.network_status || 0,
+          packetLoss: server.packet_loss || 0
+        }));
+      } catch (error) {
+        console.error("error", error);
+      }
+
+      // 返回包含服务器数据的机柜配置
+      return {
+        position: new THREE.Vector3(Number(rack.position_x) || 0, Number(rack.position_y) || 0, Number(rack.position_z) || 0),
+        serverCount: rack.capacity || 42,
+        id: rack.id,
+        name: rack.rack_name,
+        servers
+      };
+    });
+
+    // 等待所有数据获取完成
+    return Promise.all(serverPromises);
+  },
+
+  // 获取服务器类型图标配置
+  fetchDeviceIcons: async (): Promise<ServerIconConfigs> => {
+    const params: RackServerTypeParams = {
+      is_enabled: 1,
+      is_deleted: 0,
+      page: 1,
+      pageSize: 100
+    }
+    const response = await axios.get<RackServerType[]>(`${API_BASE_URL}/big/rack/server/type/image`, { params });
+
+    const ossHost = OSS_BASE_URL;
+    return response.data.reduce((acc: ServerIconConfigs, type: RackServerType) => {
+      acc[type.id] = {
+        textureUrl: type.image_url ? `${ossHost}/${type.image_url}` : null,
+        color: 0x444444
+      };
+      return acc;
+    }, {});
+  }
+}

+ 618 - 0
client/big/client.tsx

@@ -0,0 +1,618 @@
+import React, { useState, useCallback, ReactNode } from 'react';
+import { createRoot } from 'react-dom/client';
+import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
+import { Pie, Column, Line } from '@ant-design/plots';
+import { FullscreenOutlined, FullscreenExitOutlined } from '@ant-design/icons';
+import { 
+  Card
+} from 'antd';
+
+import { AlarmDeviceData } from '../share/monitorTypes.ts';
+import { queryFns } from './api.ts';
+import { ThreeJSRoom } from './components_three.tsx';
+import { GlobalConfig } from "../share/types.ts";
+const queryClient = new QueryClient();
+
+
+
+// 声明全局配置对象类型
+declare global {
+  interface Window {
+    CONFIG?: GlobalConfig;
+  }
+}
+
+interface ServerMonitorChartsProps {
+  type?: 'pie' | 'column' | 'line' | 'pie2';
+}
+
+const pushStateAndTrigger = (url: string, target: string) => {
+  window.history.pushState({}, '', url);
+  window.dispatchEvent(new Event('popstate'));
+}
+
+// 统一的链接处理函数
+const handleNavigate = (url: string) => {
+  // 判断是否在iframe中
+  const isInIframe = window.self !== window.top;
+  if (isInIframe) {
+    pushStateAndTrigger(url, 'top');
+  } else {
+    window.open(url, '_blank');
+  }
+}; 
+
+
+function AlarmDeviceTable() {
+  const { data = [], isLoading } = useQuery({
+    queryKey: ['topAlarmDevices'],
+    queryFn: queryFns.fetchTopAlarmDevices,
+    refetchInterval: 30000
+  });
+
+  if (isLoading) {
+    return (
+      <div className="flex items-center justify-center h-full">
+        <span className="text-[#00dff9]">加载中...</span>
+      </div>
+    );
+  }
+
+  return (
+    <div className="flex flex-col h-full text-sm">
+      {/* 表头 */}
+      <div className="grid grid-cols-[80px_100px_1fr] gap-2 px-4 py-2 text-gray-300 border-b border-[#00dff9]/20">
+        <div className="text-center">排名</div>
+        <div className="text-center">告警次数</div>
+        <div className="text-left">点位名称</div>
+      </div>
+
+      {/* 表格内容 */}
+      <div className="flex-1 overflow-y-auto">
+        {data.map((item: AlarmDeviceData) => (
+          <div 
+            key={item.rank}
+            className="grid grid-cols-[80px_100px_1fr] gap-2 px-4 py-2 hover:bg-[#001529] transition-colors duration-200 border-b border-[#00dff9]/10"
+          >
+            <div className="text-center text-[#00dff9]">{item.rank}</div>
+            <div className="text-center text-yellow-400">{item.alarmCount}</div>
+            <a 
+              href="/admin/alarms" 
+              className="text-left text-[#00dff9] hover:text-white truncate"
+              title={item.deviceName}
+            >
+              {item.deviceName}
+            </a>
+          </div>
+        ))}
+
+        {data.length === 0 && (
+          <div className="flex items-center justify-center h-full text-gray-400">
+            暂无数据
+          </div>
+        )}
+      </div>
+    </div>
+  );
+} 
+
+function CustomCard({ title, children, className = '', bodyStyle = {}, onClick }: {
+  title?: string;
+  children: ReactNode;
+  className?: string;
+  bodyStyle?: React.CSSProperties;
+  onClick?: () => void;
+}) {
+  return (
+    <div 
+      className={`relative bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] hover:shadow-[0_0_15px_rgba(0,223,249,0.4)] transition-shadow rounded-md ${className}`}
+      onClick={onClick}
+    >
+      {title && (
+        <div className="absolute -top-3 left-4 px-2 bg-[#001529] text-[#00dff9] text-sm">
+          {title}
+        </div>
+      )}
+      <div style={bodyStyle} className="p-4">
+        {children}
+      </div>
+    </div>
+  );
+} 
+
+function MetricCards() {
+  const { data: metrics = {
+    totalDevices: 0,
+    onlineDevices: 0,
+    offlineDevices: 0,
+    onlineRate: "0.00",
+    riskLevel: { level: '健康', color: '#52c41a' }
+  }, isLoading } = useQuery({
+    queryKey: ['deviceMetrics'],
+    queryFn: queryFns.fetchDeviceMetrics,
+    refetchInterval: 30000,
+    refetchIntervalInBackground: true
+  });
+
+  const metricConfigs = [
+    { 
+      title: "设备数", 
+      value: metrics.totalDevices,
+      link: "/admin/alarm/manage"
+    },
+    { 
+      title: "正常数", 
+      value: metrics.onlineDevices,
+      link: "/admin/alarm/manage"
+    },
+    { 
+      title: "在线率", 
+      value: `${metrics.onlineRate}%`,
+      link: "/admin/device/rate"
+    },
+    { 
+      title: "异常数", 
+      value: metrics.offlineDevices,
+      link: "/admin/alert/manage"
+    },
+    { 
+      title: "风险等级", 
+      value: metrics.riskLevel.level,
+      color: metrics.riskLevel.color,
+      link: undefined
+    }
+  ];
+
+  return (
+    <>
+      {metricConfigs.map((metric, index) => (
+        <div
+          key={index}
+          onClick={() => metric.link && handleNavigate(metric.link)}
+          className={metric.link ? "cursor-pointer" : undefined}
+        >
+          <Card
+            className="bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] hover:shadow-[0_0_15px_rgba(0,223,249,0.4)] transition-shadow relative before:content-[''] before:absolute before:left-[3px] before:top-[3px] before:h-[calc(100%-6px)] before:w-1 before:bg-gradient-to-b before:from-yellow-500 before:via-yellow-400 before:to-transparent rounded-md"
+            bordered={false}
+            styles={{
+              body: {
+                padding: "16px",
+                paddingLeft: "24px",
+                display: "flex",
+                alignItems: "center",
+                justifyContent: "space-between",
+              },
+            }}
+          >
+            <span className="text-gray-300 text-base">{metric.title}</span>
+            <span className="text-3xl" style={{ color: metric.color || '#00dff9' }}>
+              {isLoading ? "-" : metric.value}
+            </span>
+          </Card>
+        </div>
+      ))}
+    </>
+  );
+}
+
+function ServerMonitorCharts({ type = 'pie' }: ServerMonitorChartsProps) {
+  // 资产分类数据
+  const { data: categoryData } = useQuery({
+    queryKey: ['zichanCategory'],
+    queryFn: queryFns.fetchCategoryData,
+    enabled: type === 'pie'
+  });
+
+  // 在线率变化数据
+  const { data: onlineRateData } = useQuery({
+    queryKey: ['zichanOnlineRate'],
+    queryFn: queryFns.fetchOnlineRateData,
+    enabled: type === 'column'
+  });
+
+  // 资产流转状态数据
+  const { data: stateData } = useQuery({
+    queryKey: ['zichanState'],
+    queryFn: queryFns.fetchStateData,
+    enabled: type === 'pie2'
+  });
+
+  // 告警数据变化
+  const { data: alarmData } = useQuery({
+    queryKey: ['pingAlarm'],
+    queryFn: queryFns.fetchAlarmData,
+    enabled: type === 'line'
+  });
+
+  const renderChart = () => {
+    switch (type) {
+      case 'pie':
+        return (
+          <Pie
+            data={categoryData || []}
+            angleField="设备数"
+            colorField="设备分类"
+            radius={0.8}
+            label={{
+              position: 'outside',
+              text: ({ 设备分类, 设备数, 百分比, percent }: { 设备分类: string, 设备数: number, 百分比: string, percent: number }) => {
+                // 只有占比超过5%的项才显示标签
+                if (percent < 0.05) return null;
+                return `${设备分类}\n(${设备数})`;
+              },
+              style: {
+                fill: '#fff',
+                fontSize: 12,
+                fontWeight: 500,
+              },
+              transform: [{ type: 'overlapDodgeY' }],
+            }}
+            theme={{
+              colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
+            }}
+            legend={false}
+            autoFit={true}
+            interaction={{
+              tooltip: {
+                render: (_: any, { items, title }: { items: any[], title: string }) => {
+                  if (!items || items.length === 0) return '';
+                  
+                  // 获取当前选中项的数据
+                  const item = items[0];
+                  
+                  // 根据value找到对应的完整数据项
+                  const fullData = categoryData?.find(d => d['设备数'] === item.value);
+                  if (!fullData) return '';
+                  
+                  return `<div class="bg-white p-2 rounded">
+                    <div class="flex items-center">
+                      <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                      <span class="font-semibold text-gray-900">${fullData['设备分类']}</span>
+                    </div>
+                    <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                    <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
+                  </div>`;
+                }
+              }
+            }}
+          />
+        );
+      case 'column':
+        return (
+          <Column
+            data={onlineRateData || []}
+            xField="time_interval"
+            yField="total_devices"
+            color="#36cfc9"
+            label={{
+              position: 'top',
+              style: {
+                fill: '#fff',
+              },
+              text: (items: any) => {
+                let content = items['time_interval'];
+                content += `\n(${(items['total_devices'])})`;
+                return content;
+              },
+            }}
+            xAxis={{
+              label: {
+                style: {
+                  fill: '#fff',
+                },
+              },
+            }}
+            yAxis={{
+              label: {
+                style: {
+                  fill: '#fff',
+                },
+              },
+            }}
+            autoFit={true}
+            interaction={{
+              tooltip:false
+            }}
+          />
+        );
+      case 'line':
+        return (
+          <Line
+            data={alarmData || []}
+            xField="time_interval"
+            yField="total_devices"
+            smooth={true}
+            color="#36cfc9"
+            label={{
+              position: 'top',
+              style: {
+                fill: '#fff',
+                fontSize: 12,
+                fontWeight: 500,
+              },
+              text: (items: any) => {
+                const value = items['total_devices'];
+                
+                // if (value === 0) return null;
+                
+                // const maxValue = Math.max(...(alarmData || []).map(item => item.total_devices));
+                
+                // if (value < maxValue * 0.3 && alarmData && alarmData.length > 8) return null;
+                
+                return `${items['time_interval']}\n(${value})`;
+              },
+              transform: [{ type: 'overlapDodgeY' }],
+            }}
+            xAxis={{
+              label: {
+                style: {
+                  fill: '#fff',
+                },
+                autoHide: true,
+                autoRotate: true,
+              },
+            }}
+            yAxis={{
+              label: {
+                style: {
+                  fill: '#fff',
+                },
+              },
+            }}
+            autoFit={true}
+            interaction={{
+              tooltip: {
+                render: (_: any, { items, title }: { items: any[], title: string }) => {
+                  if (!items || items.length === 0) return '';
+                  
+                  // 获取当前选中项的数据
+                  const item = items[0];
+                  
+                  // 根据value找到对应的完整数据项
+                  const fullData = alarmData?.find(d => d.total_devices === item.value);
+                  if (!fullData) return '';
+                  
+                  return `<div class="bg-white p-2 rounded">
+                    <div class="flex items-center">
+                      <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                      <span class="font-semibold text-gray-900">${fullData.time_interval}</span>
+                    </div>
+                    <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                  </div>`;
+                }
+              }
+            }}
+          />
+        );
+      case 'pie2':
+        return (
+          <Pie
+            data={stateData || []}
+            angleField="设备数"
+            colorField="资产流转"
+            radius={0.9}
+            innerRadius={0.8}
+            label={{
+              position: 'outside',
+              text: ({ 资产流转, 设备数, 百分比, percent }: { 资产流转: string, 设备数: number, 百分比: string, percent: number }) => {
+                // 只有占比超过5%的项才显示标签
+                if (percent < 0.05) return null;
+                return `${资产流转}\n(${设备数})`;
+              },
+              style: {
+                fill: '#fff',
+                fontSize: 12,
+                fontWeight: 500,
+              },
+              transform: [{ type: 'overlapDodgeY' }],
+            }}
+            theme={{
+              colors10: ['#36cfc9', '#ff4d4f', '#ffa940', '#73d13d', '#4096ff'],
+            }}
+            legend={{
+              color: {
+                itemLabelFill: '#fff',
+              }
+            }}
+            autoFit={true}
+            interaction={{
+              tooltip: {
+                render: (_: any, { items, title }: { items: any[], title: string }) => {
+                  if (!items || items.length === 0) return '';
+                  
+                  // 获取当前选中项的数据
+                  const item = items[0];
+                  
+                  // 根据value找到对应的完整数据项
+                  const fullData = stateData?.find(d => d['设备数'] === item.value);
+                  if (!fullData) return '';
+                  
+                  return `<div class="bg-white p-2 rounded">
+                    <div class="flex items-center">
+                      <div class="w-3 h-3 rounded-full mr-2" style="background:${item.color}"></div>
+                      <span class="font-semibold text-gray-900">${fullData['资产流转']}</span>
+                    </div>
+                    <p class="text-sm text-gray-800">数量: ${item.value}</p>
+                    <p class="text-sm text-gray-800">占比: ${fullData['百分比']}%</p>
+                  </div>`;
+                }
+              }
+            }}
+          />
+        );
+    }
+  };
+
+  return (
+    <div className="w-full h-full">
+      {renderChart()}
+    </div>
+  );
+} 
+
+function PageTitle() {
+  return (
+    <div className="relative w-full">
+      {/* 背景图片 */}
+      <div className="w-full h-[60px]">
+        <img
+          src="/client/big/title-bg.png" 
+          alt="title background"
+          className="w-full h-full object-fill"
+        />
+      </div>
+    </div>
+  );
+}
+
+function DataCenter() {
+  const [isFullscreen, setIsFullscreen] = useState(false);
+
+  const toggleFullscreen = useCallback(() => {
+    if (!document.fullscreenElement) {
+      document.documentElement.requestFullscreen();
+      setIsFullscreen(true);
+    } else {
+      document.exitFullscreen().then(() => {
+        setIsFullscreen(false);
+        window.location.reload();
+      });
+    }
+  }, []);
+
+  return (
+    <div className="h-screen w-full bg-[#000C17] text-white overflow-hidden">
+      {/* 顶部标题区域 */}
+      <div className="relative">
+        <PageTitle />
+        
+        {/* 全屏切换按钮 */}
+        <button
+          type="button"
+          onClick={toggleFullscreen}
+          className="absolute right-4 top-4 text-[#00dff9] hover:text-white bg-transparent border-none cursor-pointer p-2"
+        >
+          {isFullscreen ? <FullscreenExitOutlined /> : <FullscreenOutlined />}
+        </button>
+      </div>
+
+      {/* 主要内容区域 */}
+      <div className="p-4">
+        {/* 顶部指标卡片 */}
+        <div className="grid grid-cols-5 gap-3 mb-3">
+          <MetricCards />
+        </div>
+
+        <div className="grid grid-cols-[25%_75%] gap-3 h-[calc(100vh-160px)] pb-4 pr-4">
+          {/* 左侧图表区域 */}
+          <div>
+            <div className="grid grid-rows-[1.2fr_1fr_1fr] gap-3 h-full">
+              {/* 饼图 */}
+              <CustomCard 
+                title="资产分类"
+                bodyStyle={{ 
+                  height: '100%',
+                  overflow: 'hidden'
+                }}
+                className="cursor-pointer hover:shadow-lg transition-shadow"
+                onClick={() => handleNavigate('/admin/device/type')}
+              >
+                <div className="h-full w-full">
+                  <ServerMonitorCharts type="pie" />
+                </div>
+              </CustomCard>
+              
+              {/* 柱状图 */}
+              <CustomCard 
+                title="在线设备数量"
+                bodyStyle={{ 
+                  height: '100%',
+                  overflow: 'hidden'
+                }}
+                className="cursor-pointer hover:shadow-lg transition-shadow"
+                onClick={() => handleNavigate('/admin/device/rate')}
+              >
+                <div className="h-full w-full">
+                  <ServerMonitorCharts type="column" />
+                </div>
+              </CustomCard>
+              
+              {/* 饼图2 */}
+              <CustomCard 
+                title="资产流转状态汇总"
+                bodyStyle={{ 
+                  height: '100%',
+                  overflow: 'hidden'
+                }}
+                className="cursor-pointer hover:shadow-lg transition-shadow"
+                onClick={() => handleNavigate('/admin/asset/transfer/chart')}
+              >
+                <div className="h-full w-full">
+                  <ServerMonitorCharts type="pie2" />
+                </div>
+              </CustomCard>
+            </div>
+          </div>
+
+          {/* 右侧区域 */}
+          <div className="grid grid-rows-[2fr_1fr] gap-3 h-full">
+            {/* 中间3D机房区域 */}
+            <CustomCard 
+              title="机房可视化"
+              bodyStyle={{ 
+                height: '100%',
+                display: 'flex',
+                flexDirection: 'column'
+              }}
+            >
+              <div className="flex-1 min-h-0" >
+                <ThreeJSRoom />
+              </div>
+            </CustomCard>
+
+            {/* 底部区域分为两列 */}
+            <div className="grid grid-cols-2 gap-3">
+              {/* 左侧告警曲线图 */}
+              <CustomCard 
+                title="近期告警数据变化"
+                bodyStyle={{ 
+                  height: '100%',
+                  overflow: 'hidden'
+                }}
+                className="cursor-pointer hover:shadow-lg transition-shadow"
+                onClick={() => handleNavigate('/admin/alarm/trend')}
+              >
+                <div className="h-full w-full">
+                  <ServerMonitorCharts type="line" />
+                </div>
+              </CustomCard>
+
+              {/* 右侧告警设备表格 */}
+              <CustomCard 
+                title="告警靠前设备"
+                bodyStyle={{ 
+                  height: 'calc(100vh - 700px)',
+                  overflow: 'hidden'
+                }}
+                className="cursor-pointer hover:shadow-lg transition-shadow"
+                onClick={() => handleNavigate('/admin/alert/manage')}
+              >
+                <div className="h-full w-full">
+                  <AlarmDeviceTable />
+                </div>
+              </CustomCard>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// 渲染应用
+const root = createRoot(document.getElementById('root') as HTMLElement);
+root.render(
+  <QueryClientProvider client={queryClient}>
+    <DataCenter />
+  </QueryClientProvider>
+);

+ 632 - 0
client/big/components_three.tsx

@@ -0,0 +1,632 @@
+import React, { useRef, useEffect } from 'react';
+import { useQuery } from '@tanstack/react-query';
+
+import * as THREE from 'three';
+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
+
+
+import type { ServerData, ServerIconConfig, RackConfig, ServerIconConfigs,DeviceStatusInfo } from './api.ts';
+import { queryFns } from './api.ts';
+
+interface UseServerHoverProps {
+  scene: THREE.Scene;
+  camera: THREE.Camera;
+  containerRef: React.RefObject<HTMLDivElement>;
+  tooltipRef: React.RefObject<HTMLDivElement>;
+}
+
+// 合并服务器基本信息和状态信息
+interface ServerStatus extends ServerData, DeviceStatusInfo {}
+
+
+
+// 颜色常量定义
+const COLORS = {
+  // 背景色(调亮为 #002952)
+  BACKGROUND: 0x002952,
+  
+  // 地板颜色(相应调亮)
+  FLOOR: 0x1D5491,        // 调亮地板颜色
+  FLOOR_GRID: 0x2E66A3,   // 相应调亮网格线条
+  
+  // 机柜颜色
+  RACK: {
+    FRAME: 0x2E5483,      // 机柜框架颜色(调亮)
+    FRONT: 0x2E5483,      // 机柜前面板
+    SIDE: 0x274D70,       // 机柜侧面板(稍暗)
+  },
+  
+  // 服务器颜色
+  SERVER: {
+    DEFAULT: 0x2E4D75,    // 服务器默认颜色(调亮)
+    FRONT: 0x355882,      // 服务器前面板(调亮)
+    STATUS: {
+      ONLINE: 0x00FF00,   // 在线状态指示灯(绿色)
+      WARNING: 0xFFFF00,  // 警告状态指示灯(黄色)
+      OFFLINE: 0xFF0000   // 离线状态指示灯(红色)
+    },
+    HOVER: {
+      COLOR: 0x00DDFF,    // 悬停发光颜色(青色)
+      INTENSITY: 0.05      // 悬停发光强度
+    }
+  },
+  
+  // 灯光颜色
+  LIGHTS: {
+    AMBIENT: 0xCCE0FF,    // 环境光(偏蓝色冷光)
+    DIRECT: 0xFFFFFF,     // 直射光(白光)
+    SPOT: 0x00DDFF,       // 聚光灯(青色)
+  }
+} as const; 
+
+// UI样式常量
+const UI_STYLES = {
+  TOOLTIP: {
+    BACKGROUND: '#001529',
+    BORDER: '#00dff9',
+    BORDER_INNER: '#15243A',
+    SHADOW: '0 0 10px rgba(0,223,249,0.3)',
+    TEXT: '#ffffff',
+    PADDING: '12px 16px',
+    BORDER_RADIUS: '4px',
+    FONT_SIZE: '14px'
+  }
+} as const; 
+
+// 定义1U的高度(米)
+const U_HEIGHT = 0.04445; // 1U = 44.45mm = 0.04445m
+
+
+function useServerHover({ scene, camera, containerRef, tooltipRef }: UseServerHoverProps) {
+  const HOVER_COLOR = new THREE.Color(COLORS.SERVER.HOVER.COLOR);
+  const HOVER_INTENSITY = COLORS.SERVER.HOVER.INTENSITY;
+  let INTERSECTED: THREE.Mesh | null = null;
+  const raycaster = new THREE.Raycaster();
+  const mouse = new THREE.Vector2();
+
+  const setMaterialEmissive = (materials: THREE.Material | THREE.Material[], color: THREE.Color, intensity: number) => {
+    if (Array.isArray(materials)) {
+      materials.forEach(material => {
+        if (material instanceof THREE.MeshPhongMaterial) {
+          material.emissive.copy(color);
+          material.emissiveIntensity = intensity;
+          material.needsUpdate = true;
+        }
+      });
+    } else if (materials instanceof THREE.MeshPhongMaterial) {
+      materials.emissive.copy(color);
+      materials.emissiveIntensity = intensity;
+      materials.needsUpdate = true;
+    }
+  };
+
+  const resetMaterialEmissive = (materials: THREE.Material | THREE.Material[]) => {
+    if (Array.isArray(materials)) {
+      materials.forEach(material => {
+        if (material instanceof THREE.MeshPhongMaterial) {
+          material.emissive.setHex(0x000000);
+          material.emissiveIntensity = 0.2;
+          material.needsUpdate = true;
+        }
+      });
+    } else if (materials instanceof THREE.MeshPhongMaterial) {
+      materials.emissive.setHex(0x000000);
+      materials.emissiveIntensity = 0.2;
+      materials.needsUpdate = true;
+    }
+  };
+
+  const handleMouseMove = (event: MouseEvent) => {
+    const rect = containerRef.current?.getBoundingClientRect();
+    if (!rect) return;
+    
+    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
+    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;
+
+    raycaster.setFromCamera(mouse, camera);
+    const intersects = raycaster.intersectObjects(scene.children, true);
+
+    if (intersects.length > 0) {
+      const found = intersects.find(item => 
+        item.object instanceof THREE.Mesh && 
+        item.object.userData.type === 'server'
+      );
+
+      if (found) {
+        const serverMesh = found.object as THREE.Mesh;
+        
+        if (INTERSECTED !== serverMesh) {
+          if (INTERSECTED) {
+            resetMaterialEmissive(INTERSECTED.material);
+          }
+          
+          INTERSECTED = serverMesh;
+          setMaterialEmissive(INTERSECTED.material, HOVER_COLOR, HOVER_INTENSITY);
+
+          updateTooltip(event, INTERSECTED.userData.status);
+        }
+      } else {
+        resetHoverState();
+      }
+    } else {
+      resetHoverState();
+    }
+  };
+
+  const resetIntersected = () => {
+    if (INTERSECTED) {
+      resetMaterialEmissive(INTERSECTED.material);
+    }
+  };
+
+  const resetHoverState = () => {
+    resetIntersected();
+    INTERSECTED = null;
+    if (tooltipRef.current) {
+      tooltipRef.current.style.display = 'none';
+    }
+  };
+
+  const updateTooltip = (event: MouseEvent, serverStatus: ServerStatus) => {
+    if (tooltipRef.current) {
+      const containerRect = containerRef.current?.getBoundingClientRect();
+      if (!containerRect) return;
+
+      const tooltipX = event.clientX - containerRect.left;
+      const tooltipY = event.clientY - containerRect.top;
+
+      const tooltipRect = tooltipRef.current.getBoundingClientRect();
+      const tooltipWidth = tooltipRect.width;
+      const tooltipHeight = tooltipRect.height;
+
+      const finalX = Math.min(
+        tooltipX + 10,
+        containerRect.width - tooltipWidth - 10
+      );
+      const finalY = Math.min(
+        tooltipY + 10,
+        containerRect.height - tooltipHeight - 10
+      );
+
+      tooltipRef.current.style.left = `${finalX}px`;
+      tooltipRef.current.style.top = `${finalY}px`;
+      tooltipRef.current.innerHTML = `
+        <div class="bg-[${UI_STYLES.TOOLTIP.BACKGROUND}] border border-[${UI_STYLES.TOOLTIP.BORDER}] 
+             shadow-[${UI_STYLES.TOOLTIP.SHADOW}] p-3 px-4 rounded text-[${UI_STYLES.TOOLTIP.TEXT}] text-sm">
+          <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
+            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
+              <path d="M19 3H5C3.9 3 3 3.9 3 5V19C3 20.1 3.9 21 5 21H19C20.1 21 21 20.1 21 19V5C21 3.9 20.1 3 19 3ZM5 19V5H19V19H5Z" fill="currentColor"/>
+              <path d="M12 17H17V15H12V17ZM7 13H17V11H7V13ZM7 9H17V7H7V9Z" fill="currentColor"/>
+            </svg>
+            <span class="font-medium">资产信息</span>
+          </div>
+          <div class="grid gap-0.5 ml-1 mb-2">
+            <div>名称: ${serverStatus.name}</div>
+            <div>IP地址: ${serverStatus.ip}</div>
+          </div>
+          
+          <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
+            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
+              <path d="M15.9 4.99999C15.9 4.99999 15.9 4.89999 15.8 4.89999C15.4 4.39999 14.8 3.99999 14.2 3.69999L12.2 6.29999L15.9 4.99999Z" fill="currentColor"/>
+              <path d="M18.2 7.3L15.9 5C15.9 5 15.9 4.9 15.8 4.9C16.8 5.7 17.6 6.4 18.2 7.3Z" fill="currentColor"/>
+              <path d="M21 16V8C21 6.7 20.2 5.6 19 5.2C18.3 4.1 17.3 3.2 16.2 2.5C14.7 1.6 13 1 11.2 1C6.1 1 2 5.1 2 10.2C2 13.6 3.9 16.5 6.5 18.1C7.8 18.9 9 20.2 9 21.7V23H15V21.7C15 20.2 16.2 18.9 17.5 18.1C19.4 17 20.7 15.1 21 16ZM13 17H11V15H13V17ZM13 13H11V7H13V13Z" fill="currentColor"/>
+            </svg>
+            <span class="font-medium">网络状态</span>
+          </div>
+          <div class="grid gap-0.5 ml-1 mb-2">
+            <div class="flex justify-between">
+              <span>状态:</span>
+              <div class="flex items-center gap-1.5">
+                <span>${serverStatus.networkStatus === 1 ? '在线' : '离线'}</span>
+                <span class="w-2 h-2 rounded-full ${serverStatus.networkStatus === 1 ? 'bg-green-500' : 'bg-red-500'}"></span>
+              </div>
+            </div>
+            <div>丢包率: ${serverStatus.packetLoss}%</div>
+          </div>
+
+          <div class="flex items-center gap-1.5 mb-1.5 pb-1 border-b border-[${UI_STYLES.TOOLTIP.BORDER_INNER}]">
+            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none">
+              <path d="M4 6H20V8H4V6Z" fill="currentColor"/>
+              <path d="M4 11H20V13H4V11Z" fill="currentColor"/>
+              <path d="M4 16H20V18H4V16Z" fill="currentColor"/>
+              <path d="M2 4.5C2 3.67157 2.67157 3 3.5 3H20.5C21.3284 3 22 3.67157 22 4.5V19.5C22 20.3284 21.3284 21 20.5 21H3.5C2.67157 21 2 20.3284 2 19.5V4.5Z" stroke="currentColor" stroke-width="2"/>
+            </svg>
+            <span class="font-medium">配置信息</span>
+          </div>
+          <div class="grid gap-0.5 ml-1">
+            <div>CPU: ${serverStatus.cpu || '-'}</div>
+            <div>内存: ${serverStatus.memory || '-'}</div>
+            <div>硬盘: ${serverStatus.disk || '-'}</div>
+          </div>
+        </div>
+      `;
+      tooltipRef.current.style.display = 'block';
+    }
+  };
+
+  const cleanup = () => {
+    resetHoverState();
+  };
+
+  return {
+    handleMouseMove,
+    cleanup
+  };
+} 
+
+// 创建机柜模型 - 修改机柜的原点为底部中心
+function createRack(position: THREE.Vector3): THREE.Mesh {
+  // 创建机柜的六个面
+  const rackGeometry = new THREE.BoxGeometry(0.6, 2, 1);
+  
+  // 创建两种材质:
+  // 1. 通用材质 - 用于除前面外的所有面,双面渲染
+  const commonMaterial = new THREE.MeshPhongMaterial({
+    color: COLORS.RACK.SIDE,
+    side: THREE.DoubleSide, // 双面渲染
+    opacity: 0.9,
+  });
+  
+  // 2. 前面材质 - 半透明
+  const frontMaterial = new THREE.MeshPhongMaterial({
+    color: COLORS.RACK.FRONT,
+    transparent: true,
+    opacity: 0.1,  // 前面设置为更透明
+  });
+
+  // 创建材质数组,按照几何体的面的顺序设置材质
+  // BoxGeometry的面顺序:右、左、上、下、前、后
+  const materials = [
+    commonMaterial,  // 右面 - 双面渲染
+    commonMaterial,  // 左面 - 双面渲染
+    commonMaterial,  // 上面 - 双面渲染
+    commonMaterial,  // 下面 - 双面渲染
+    frontMaterial,   // 前面 - 半透明
+    commonMaterial,  // 后面 - 双面渲染
+  ];
+
+  const rack = new THREE.Mesh(rackGeometry, materials);
+  rack.position.copy(position);
+  rackGeometry.translate(0, 1, 0);
+  return rack;
+}
+
+// 创建服务器模型
+function createServer(
+  position: THREE.Vector3,
+  serverData: ServerData,
+  serverIconConfig: ServerIconConfig
+): { server: THREE.Mesh } {
+  const config = serverIconConfig;
+  const U = serverData.u;
+  
+  const serverGeometry = new THREE.BoxGeometry(
+    0.483,  // 19英寸 ≈ 0.483米
+    U * U_HEIGHT,  // 将U数转换为实际高度
+    0.8     // 深度保持0.8米
+  );
+  
+  serverGeometry.translate(0, U * U_HEIGHT/2, 0);
+
+  // 创建基础材质(用于侧面、顶面、底面和后面)
+  const baseMaterial = new THREE.MeshPhongMaterial({ 
+    color: config.color,
+    shininess: 30,  // 降低反光度
+  });
+
+  // 创建前面的材质(用于贴图)
+  const frontMaterial = new THREE.MeshPhongMaterial({ 
+    color: config.color,
+    shininess: 30,
+    map: null,  // 初始化时设为null,等待贴图加载
+  });
+
+  // 创建材质数组
+  const materials = [
+    baseMaterial,  // 右面
+    baseMaterial,  // 左面
+    baseMaterial,  // 上面
+    baseMaterial,  // 下面
+    frontMaterial, // 前面 - 用于贴图
+    baseMaterial,  // 后面
+  ];
+
+  const server = new THREE.Mesh(serverGeometry, materials);
+  server.position.copy(position);
+
+  // 加载贴图(如果有)
+  if (config.textureUrl) {
+    const textureLoader = new THREE.TextureLoader();
+    textureLoader.load(config.textureUrl, (texture) => {
+      // 设置贴图属性以提高清晰度
+      texture.minFilter = THREE.LinearFilter;
+      texture.magFilter = THREE.LinearFilter;
+      texture.anisotropy = 16;  // 增加各向异性过滤
+      
+      // 调整贴图的重复和偏移
+      texture.repeat.set(1, 1);
+      texture.offset.set(0, 0);
+      
+      // 更新材质
+      frontMaterial.map = texture;
+      frontMaterial.needsUpdate = true;
+    });
+  }
+
+  return { server };
+}
+
+// 添加创建地板的函数
+function createFloor(): THREE.Mesh {
+  const floorGeometry = new THREE.PlaneGeometry(10, 10);
+  const floorMaterial = new THREE.MeshPhongMaterial({
+    color: COLORS.FLOOR,
+    side: THREE.DoubleSide
+  });
+  
+  // 添加网格纹理
+  const gridHelper = new THREE.GridHelper(10, 20, COLORS.FLOOR_GRID, COLORS.FLOOR_GRID);
+  gridHelper.rotation.x = Math.PI / 2;
+  
+  const floor = new THREE.Mesh(floorGeometry, floorMaterial);
+  floor.rotation.x = -Math.PI / 2;
+  floor.add(gridHelper);
+  
+  return floor;
+} 
+
+class ServerRack {
+  rack: THREE.Mesh;
+  servers: THREE.Mesh[] = [];
+  statusLights: THREE.PointLight[] = [];
+
+  constructor(scene: THREE.Scene, rackConfig: RackConfig, serverIconConfigs: ServerIconConfigs) {
+    // 创建机柜
+    this.rack = createRack(rackConfig.position);
+    scene.add(this.rack);
+
+    const bottomSpace = 0.04445;    // 底部预留空间1U
+    const slotHeight = 0.04445;  // 每个槽位高度1U
+    
+    // 使用配置中的服务器数据创建服务器
+    rackConfig.servers.forEach((serverData: ServerData) => {
+      // 从底部开始计算高度
+      const currentHeight = bottomSpace + (slotHeight * (serverData.slot - 1));
+      
+      const serverPosition = new THREE.Vector3(
+        rackConfig.position.x,
+        rackConfig.position.y + currentHeight,
+        rackConfig.position.z
+      );
+
+      const serverIconConfig = serverIconConfigs[serverData.type];
+      // console.log(serverIconConfig, serverIconConfigs);
+      // console.log(serverData);
+      // console.log(serverPosition);
+      const { server } = createServer(
+        serverPosition,
+        serverData,
+        serverIconConfig
+      );
+
+      server.userData.type = 'server';
+      server.userData.status = getServerStatus(serverData);
+
+      this.servers.push(server);
+
+      scene.add(server);
+    });
+  }
+
+  // 更新服务器状态
+  updateServerStatus(serverId: string, status: ServerStatus) {
+    const server = this.servers.find(s => s.userData.id === serverId);
+    if (server) {
+      server.userData.status = status;
+      const index = this.servers.indexOf(server);
+      if (index !== -1) {
+        const light = this.statusLights[index];
+        light.color.setHex(
+          status.status === 'online' ? COLORS.SERVER.STATUS.ONLINE :
+          status.status === 'warning' ? COLORS.SERVER.STATUS.WARNING : 
+          COLORS.SERVER.STATUS.OFFLINE
+        );
+      }
+    }
+  }
+} 
+
+// 将服务器状态根据设备状态转换为对应的status格式
+const getServerStatus = (serverData: ServerData): ServerStatus => {
+  // 转换网络状态为UI显示状态
+  let status: 'online' | 'offline' | 'warning' = 'offline';
+  
+  // 根据设备状态判断显示状态
+  if (serverData.deviceStatus === 0) { // 正常
+    status = 'online';
+  } else if (serverData.deviceStatus === 2) { // 故障
+    status = 'warning';
+  } else { // 其他情况(维护中、下线)
+    status = 'offline';
+  }
+
+  // 随机生成资源使用率用于展示
+  return {
+    ...serverData,
+    status,
+    usage: {
+      cpu: Math.floor(Math.random() * 100),
+      memory: Math.floor(Math.random() * 100),
+      disk: Math.floor(Math.random() * 100)
+    }
+  };
+};
+
+export function ThreeJSRoom() {
+  const containerRef = useRef<HTMLDivElement>(null);
+  const tooltipRef = useRef<HTMLDivElement>(null);
+  const racksRef = useRef<ServerRack[]>([]);
+  const sceneRef = useRef<{
+    scene: THREE.Scene;
+    camera: THREE.PerspectiveCamera;
+    renderer: THREE.WebGLRenderer;
+    controls: typeof OrbitControls;
+    cleanup: () => void;
+  } | null>(null);
+
+  // 获取服务器类型图标
+  const { data: deviceIcons } = useQuery({
+    queryKey: ['deviceIcons'],
+    queryFn: queryFns.fetchDeviceIcons
+  });
+
+  // 获取机柜配置,添加自动刷新
+  const { data: rackConfigs } = useQuery({
+    queryKey: ['rackConfigs'],
+    queryFn: queryFns.fetchRackConfigs,
+    refetchInterval: 30000,
+    refetchIntervalInBackground: true
+  });
+
+  // 初始化3D场景
+  useEffect(() => {
+    if (!containerRef.current) return;
+
+    // 初始化场景
+    const scene = new THREE.Scene();
+    scene.background = new THREE.Color(COLORS.BACKGROUND);
+
+    const camera = new THREE.PerspectiveCamera(
+      22,
+      containerRef.current.clientWidth / containerRef.current.clientHeight,
+      0.1,
+      1000
+    );
+
+    const renderer = new THREE.WebGLRenderer({ antialias: true });
+    renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
+    containerRef.current.appendChild(renderer.domElement);
+
+    const controls = new OrbitControls(camera, renderer.domElement);
+    controls.enableDamping = true;
+    controls.maxPolarAngle = Math.PI / 2;
+    controls.minDistance = 6;
+    controls.maxDistance = 10;
+    controls.enablePan = false;
+    controls.target.set(0, 1, 0);
+    controls.enabled = false;
+
+    // 添加地板
+    const floor = createFloor();
+    floor.position.y = -0.01;
+    scene.add(floor);
+
+    // 设置场景光照
+    const ambientLight = new THREE.AmbientLight(COLORS.LIGHTS.AMBIENT, 0.6);
+    scene.add(ambientLight);
+
+    const directionalLight = new THREE.DirectionalLight(COLORS.LIGHTS.DIRECT, 0.8);
+    directionalLight.position.set(5, 5, 5);
+    scene.add(directionalLight);
+
+    const spotLight = new THREE.SpotLight(COLORS.LIGHTS.SPOT, 0.8);
+    spotLight.position.set(0, 5, 0);
+    spotLight.angle = Math.PI / 4;
+    spotLight.penumbra = 0.1;
+    scene.add(spotLight);
+
+    camera.position.set(0, 1, 1);
+    camera.lookAt(0, 1, 0);
+
+    const { handleMouseMove, cleanup } = useServerHover({
+      scene,
+      camera,
+      containerRef: containerRef as React.RefObject<HTMLDivElement>,
+      tooltipRef: tooltipRef as React.RefObject<HTMLDivElement>
+    });
+
+    containerRef.current.addEventListener('mousemove', handleMouseMove);
+
+    // 动画循环
+    const animate = () => {
+      requestAnimationFrame(animate);
+      controls.update();
+      renderer.render(scene, camera);
+    };
+
+    animate();
+
+    // 保存场景引用
+    sceneRef.current = {
+      scene,
+      camera,
+      renderer,
+      controls: controls as unknown as typeof OrbitControls,
+      cleanup: () => {
+        cleanup();
+        if (containerRef.current) {
+          containerRef.current.removeEventListener('mousemove', handleMouseMove);
+          containerRef.current.removeChild(renderer.domElement);
+        }
+      }
+    };
+
+    // 使用 ResizeObserver 监听容器大小变化
+    const resizeObserver = new ResizeObserver(() => {
+      if (!containerRef.current || !sceneRef.current) return;
+      // console.log('resizeObserver', containerRef.current.clientWidth, containerRef.current.clientHeight);
+      const { camera, renderer } = sceneRef.current;
+      camera.aspect = containerRef.current.clientWidth / containerRef.current.clientHeight;
+      camera.updateProjectionMatrix();
+      renderer.setSize(containerRef.current.clientWidth, containerRef.current.clientHeight);
+    });
+
+    resizeObserver.observe(containerRef.current);
+
+    // 清理
+    return () => {
+      resizeObserver.disconnect();
+      sceneRef.current?.cleanup();
+    };
+  }, []);
+
+  // 更新机柜数据
+  useEffect(() => {
+    if (!sceneRef.current || !rackConfigs || !deviceIcons) return;
+
+    // 清除现有机柜
+    racksRef.current.forEach(rack => {
+      rack.servers.forEach(server => {
+        sceneRef.current!.scene.remove(server);
+      });
+      sceneRef.current!.scene.remove(rack.rack);
+    });
+    racksRef.current = [];
+
+
+    // 创建新机柜
+    racksRef.current = rackConfigs.map(config => {
+      return new ServerRack(sceneRef.current!.scene, config, deviceIcons);
+    });
+
+  }, [rackConfigs, deviceIcons]);
+
+  return (
+    <div className="relative w-full h-full">
+      <div ref={containerRef} className="w-full h-full" />
+      <div 
+        ref={tooltipRef}
+        className="absolute hidden z-10"
+        style={{ 
+          pointerEvents: 'none',
+          position: 'absolute',
+          transform: 'translate3d(0, 0, 0)'
+        }}
+      >
+        <div className="bg-[#001529] border border-[#00dff9] shadow-[0_0_10px_rgba(0,223,249,0.3)] p-2 rounded">
+          {/* 工具提示内容由useServerHover处理 */}
+        </div>
+      </div>
+    </div>
+  );
+} 

BIN
client/big/title-bg.png


+ 220 - 0
client/migrations/migrations_app.tsx

@@ -0,0 +1,220 @@
+import React, { useState } from 'react';
+import { createRoot } from 'react-dom/client';
+import { Button, Space, Alert, Spin, Typography, Table } from 'antd';
+import axios from 'axios';
+import dayjs from 'dayjs';
+import {
+  QueryClient,
+  QueryClientProvider,
+  useQuery,
+} from '@tanstack/react-query';
+
+const { Title } = Typography;
+
+// 创建QueryClient实例
+const queryClient = new QueryClient();
+
+interface MigrationResponse {
+  success: boolean;
+  error?: string;
+  failedResult?: any;
+}
+
+interface MigrationHistory {
+  id: string;
+  name: string;
+  status: string;
+  timestamp: string;
+  batch: string;
+}
+
+const MigrationsApp: React.FC = () => {
+  const [loading, setLoading] = useState(false);
+  const [migrationResult, setMigrationResult] = useState<MigrationResponse | null>(null);
+
+  const { data: historyData, isLoading: isHistoryLoading, error: historyError } = useQuery({
+    queryKey: ['migrations-history'],
+    queryFn: async () => {
+      const response = await axios.get('/api/migrations/history');
+      return response.data.history;
+    }
+  });
+
+  const runMigrations = async () => {
+    try {
+      setLoading(true);
+      setMigrationResult(null);
+      
+      const response = await axios.get('/api/migrations');
+      setMigrationResult(response.data);
+      if (response.data.success) {
+        queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
+      }
+    } catch (error: any) {
+      setMigrationResult({
+        success: false,
+        error: error.response?.data?.error || '数据库迁移失败',
+        failedResult: error.response?.data?.failedResult
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const rollbackMigrations = async (all: boolean) => {
+    try {
+      setLoading(true);
+      setMigrationResult(null);
+      
+      const response = await axios.get(`/api/migrations/rollback?all=${all}`);
+      setMigrationResult(response.data);
+      if (response.data.success) {
+        queryClient.invalidateQueries({ queryKey: ['migrations-history'] });
+      }
+    } catch (error: any) {
+      setMigrationResult({
+        success: false,
+        error: error.response?.data?.error || '数据库回滚失败',
+        failedResult: error.response?.data?.failedResult
+      });
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const columns = [
+    {
+      title: '迁移名称',
+      dataIndex: 'name',
+      key: 'name',
+      sorter: (a: MigrationHistory, b: MigrationHistory) => a.name.localeCompare(b.name),
+    },
+    {
+      title: '批次',
+      dataIndex: 'batch',
+      key: 'batch',
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: string) => (
+        <span style={{ color: status === 'completed' ? 'green' : 'red' }}>
+          {status === 'completed' ? '已完成' : '失败'}
+        </span>
+      )
+    },
+    {
+      title: '时间',
+      dataIndex: 'migration_time',
+      key: 'migration_time',
+      render: (migration_time: string) => dayjs(migration_time).format('YYYY-MM-DD HH:mm:ss')
+    },
+  ];
+
+  return (
+    <div className="p-4">
+      <Title level={3}>数据库迁移管理</Title>
+      
+      <Space direction="vertical" size="middle" style={{ width: '100%' }}>
+        <Space>
+          <Button
+            type="primary"
+            onClick={runMigrations}
+            loading={loading}
+            disabled={loading}
+          >
+            执行迁移
+          </Button>
+          <Button
+            danger
+            onClick={() => rollbackMigrations(false)}
+            loading={loading}
+            disabled={loading}
+          >
+            回滚最近一次
+          </Button>
+          <Button
+            danger
+            onClick={() => rollbackMigrations(true)}
+            loading={loading}
+            disabled={loading}
+          >
+            回滚全部
+          </Button>
+        </Space>
+        <Alert
+          message="警告"
+          description="回滚操作将删除数据,请谨慎使用"
+          type="warning"
+          showIcon
+          style={{ marginBottom: 16 }}
+        />
+
+        {loading && <Spin tip="迁移执行中..." />}
+
+        {migrationResult && (
+          migrationResult.success ? (
+            <Alert
+              message="迁移成功"
+              type="success"
+              showIcon
+            />
+          ) : (
+            <Alert
+              message="迁移失败"
+              description={
+                <>
+                  <p>{migrationResult.error}</p>
+                  {migrationResult.failedResult && (
+                    <pre style={{ marginTop: 10 }}>
+                      {JSON.stringify(migrationResult.failedResult, null, 2)}
+                    </pre>
+                  )}
+                </>
+              }
+              type="error"
+              showIcon
+            />
+          )
+        )}
+
+        <Title level={4}>迁移历史记录</Title>
+        
+        {isHistoryLoading ? (
+          <Spin tip="加载历史记录中..." />
+        ) : historyError ? (
+          <Alert
+            message="加载历史记录失败"
+            description={historyError.message}
+            type="error"
+            showIcon
+          />
+        ) : (
+          <Table
+            columns={columns}
+            dataSource={historyData}
+            rowKey="id"
+            pagination={{
+              pageSize: 10,
+              showSizeChanger: true,
+              pageSizeOptions: ['10', '20', '50', '100'],
+              showTotal: (total) => `共 ${total} 条记录`,
+            }}
+            bordered
+            className="migration-history-table"
+            style={{ marginTop: 16 }}
+          />
+        )}
+      </Space>
+    </div>
+  );
+};
+
+// 渲染应用
+const root = createRoot(document.getElementById('root') as HTMLElement);
+root.render(
+  <QueryClientProvider client={queryClient}>
+    <MigrationsApp />
+  </QueryClientProvider>
+);

+ 115 - 0
client/mobile/api/auth.ts

@@ -0,0 +1,115 @@
+import axios from 'axios';
+import type { User } from '../../share/types.ts';
+
+// 定义API返回数据类型
+interface AuthLoginResponse {
+  message: string;
+  token: string;
+  refreshToken?: string;
+  user: User;
+}
+
+interface AuthResponse {
+  message: string;
+  [key: string]: any;
+}
+
+// 定义Auth API接口类型
+interface AuthAPIType {
+  login: (username: string, password: string, latitude?: number, longitude?: number) => Promise<AuthLoginResponse>;
+  register: (username: string, email: string, password: string) => Promise<AuthResponse>;
+  logout: () => Promise<AuthResponse>;
+  getCurrentUser: () => Promise<User>;
+  updateUser: (userId: number, userData: Partial<User>) => Promise<User>;
+  changePassword: (oldPassword: string, newPassword: string) => Promise<AuthResponse>;
+  requestPasswordReset: (email: string) => Promise<AuthResponse>;
+  resetPassword: (token: string, newPassword: string) => Promise<AuthResponse>;
+}
+
+// Auth相关API
+export const AuthAPI: AuthAPIType = {
+  // 登录API
+  login: async (username: string, password: string, latitude?: number, longitude?: number) => {
+    try {
+      const response = await axios.post('/auth/login', {
+        username,
+        password,
+        latitude,
+        longitude
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 注册API
+  register: async (username: string, email: string, password: string) => {
+    try {
+      const response = await axios.post('/auth/register', { username, email, password });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 登出API
+  logout: async () => {
+    try {
+      const response = await axios.post('/auth/logout');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取当前用户信息
+  getCurrentUser: async () => {
+    try {
+      const response = await axios.get('/auth/me');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 更新用户信息
+  updateUser: async (userId: number, userData: Partial<User>) => {
+    try {
+      const response = await axios.put(`/auth/users/${userId}`, userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 修改密码
+  changePassword: async (oldPassword: string, newPassword: string) => {
+    try {
+      const response = await axios.post('/auth/change-password', { oldPassword, newPassword });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 请求重置密码
+  requestPasswordReset: async (email: string) => {
+    try {
+      const response = await axios.post('/auth/request-password-reset', { email });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 重置密码
+  resetPassword: async (token: string, newPassword: string) => {
+    try {
+      const response = await axios.post('/auth/reset-password', { token, newPassword });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 72 - 0
client/mobile/api/chart.ts

@@ -0,0 +1,72 @@
+import axios from 'axios';
+
+// 图表数据API接口类型
+interface ChartDataResponse<T> {
+  message: string;
+  data: T;
+}
+
+interface UserActivityData {
+  date: string;
+  count: number;
+}
+
+interface FileUploadsData {
+  month: string;
+  count: number;
+}
+
+interface FileTypesData {
+  type: string;
+  value: number;
+}
+
+interface DashboardOverviewData {
+  userCount: number;
+  fileCount: number;
+  articleCount: number;
+  todayLoginCount: number;
+}
+
+// 图表数据API
+export const ChartAPI = {
+  // 获取用户活跃度数据
+  getUserActivity: async (): Promise<ChartDataResponse<UserActivityData[]>> => {
+    try {
+      const response = await axios.get('/charts/user-activity');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取文件上传统计数据
+  getFileUploads: async (): Promise<ChartDataResponse<FileUploadsData[]>> => {
+    try {
+      const response = await axios.get('/charts/file-uploads');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取文件类型分布数据
+  getFileTypes: async (): Promise<ChartDataResponse<FileTypesData[]>> => {
+    try {
+      const response = await axios.get('/charts/file-types');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取仪表盘概览数据
+  getDashboardOverview: async (): Promise<ChartDataResponse<DashboardOverviewData>> => {
+    try {
+      const response = await axios.get('/charts/dashboard-overview');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 173 - 0
client/mobile/api/file.ts

@@ -0,0 +1,173 @@
+import axios from 'axios';
+import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
+import type {
+  FileLibrary, FileCategory
+} from '../../share/types.ts';
+
+interface FileUploadPolicyResponse {
+  message: string;
+  data: MinioUploadPolicy | OSSUploadPolicy;
+}
+
+interface FileListResponse {
+  message: string;
+  data: {
+    list: FileLibrary[];
+    pagination: {
+      current: number;
+      pageSize: number;
+      total: number;
+    };
+  };
+}
+
+interface FileSaveResponse {
+  message: string;
+  data: FileLibrary;
+}
+
+interface FileInfoResponse {
+  message: string;
+  data: FileLibrary;
+}
+
+interface FileDeleteResponse {
+  message: string;
+}
+
+
+interface FileCategoryListResponse {
+  data: FileCategory[];
+  total: number;
+  page: number;
+  pageSize: number;
+}
+
+interface FileCategoryCreateResponse {
+  message: string;
+  data: FileCategory;
+}
+
+interface FileCategoryUpdateResponse {
+  message: string;
+  data: FileCategory;
+}
+
+interface FileCategoryDeleteResponse {
+  message: string;
+}
+
+// 文件管理API
+export const FileAPI = {
+  // 获取文件上传策略
+  getUploadPolicy: async (filename: string, prefix: string = 'uploads/', maxSize: number = 10 * 1024 * 1024): Promise<FileUploadPolicyResponse> => {
+    try {
+      const response = await axios.get('/upload/policy', {
+        params: { filename, prefix, maxSize } 
+      });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 保存文件信息
+  saveFileInfo: async (fileData: Partial<FileLibrary>): Promise<FileSaveResponse> => {
+    try {
+      const response = await axios.post('/upload/save', fileData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取文件列表
+  getFileList: async (params?: {
+    page?: number,
+    pageSize?: number,
+    category_id?: number,
+    fileType?: string,
+    keyword?: string
+  }): Promise<FileListResponse> => {
+    try {
+      const response = await axios.get('/upload/list', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取单个文件信息
+  getFileInfo: async (id: number): Promise<FileInfoResponse> => {
+    try {
+      const response = await axios.get(`/upload/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新文件下载计数
+  updateDownloadCount: async (id: number): Promise<FileDeleteResponse> => {
+    try {
+      const response = await axios.post(`/upload/${id}/download`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除文件
+  deleteFile: async (id: number): Promise<FileDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/upload/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取文件分类列表
+  getCategories: async (params?: {
+    page?: number,
+    pageSize?: number,
+    search?: string
+  }): Promise<FileCategoryListResponse> => {
+    try {
+      const response = await axios.get('/file-categories', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 创建文件分类
+  createCategory: async (data: Partial<FileCategory>): Promise<FileCategoryCreateResponse> => {
+    try {
+      const response = await axios.post('/file-categories', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新文件分类
+  updateCategory: async (id: number, data: Partial<FileCategory>): Promise<FileCategoryUpdateResponse> => {
+    try {
+      const response = await axios.put(`/file-categories/${id}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除文件分类
+  deleteCategory: async (id: number): Promise<FileCategoryDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/file-categories/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 77 - 0
client/mobile/api/home.ts

@@ -0,0 +1,77 @@
+import axios from 'axios';
+import type {
+  KnowInfo
+} from '../../share/types.ts';
+
+// 首页数据相关类型定义
+interface HomeBannersResponse {
+  message: string;
+  data: KnowInfo[];
+}
+
+interface HomeNewsResponse {
+  message: string;
+  data: KnowInfo[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+interface HomeNoticesResponse {
+  message: string;
+    data: {
+      id: number;
+      title: string;
+      content: string;
+      created_at: string;
+    }[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+// 首页API
+export const HomeAPI = {
+  // 获取轮播图
+  getBanners: async (): Promise<HomeBannersResponse> => {
+    try {
+      const response = await axios.get('/home/banners');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取新闻列表
+  getNews: async (params?: {
+    page?: number,
+    pageSize?: number,
+    category?: string
+  }): Promise<HomeNewsResponse> => {
+    try {
+      const response = await axios.get('/home/news', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取通知列表
+  getNotices: async (params?: {
+    page?: number,
+    pageSize?: number
+  }): Promise<HomeNoticesResponse> => {
+    try {
+      const response = await axios.get('/home/notices', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 25 - 0
client/mobile/api/index.ts

@@ -0,0 +1,25 @@
+import axios from 'axios';
+
+// 基础配置
+const API_BASE_URL = '/api';
+// 全局axios配置
+axios.defaults.baseURL = API_BASE_URL;
+
+// 获取OSS完整URL
+export const getOssUrl = (path: string): string => {
+  // 获取全局配置中的OSS_HOST,如果不存在使用默认值
+  const ossHost = (window.CONFIG?.OSS_BASE_URL) || '';
+  // 确保path不以/开头
+  const ossPath = path.startsWith('/') ? path.substring(1) : path;
+  return `${ossHost}/${ossPath}`;
+};
+
+export * from './auth.ts';
+export * from './user.ts';
+export * from './file.ts';
+export * from './theme.ts';
+export * from './chart.ts';
+export * from './home.ts';
+export * from './map.ts';
+export * from './system.ts';
+export * from './message.ts';

+ 62 - 0
client/mobile/api/map.ts

@@ -0,0 +1,62 @@
+import axios from 'axios';
+import type {
+  LoginLocation, LoginLocationDetail,
+} from '../../share/types.ts';
+
+
+// 地图相关API的接口类型定义
+export interface LoginLocationResponse {
+  message: string;
+  data: LoginLocation[];
+}
+
+export interface LoginLocationDetailResponse {
+  message: string;
+  data: LoginLocationDetail;
+}
+
+export interface LoginLocationUpdateResponse {
+  message: string;
+  data: LoginLocationDetail;
+}
+
+// 地图相关API
+export const MapAPI = {
+  // 获取地图标记点数据
+  getMarkers: async (params?: { 
+    startTime?: string; 
+    endTime?: string; 
+    userId?: number 
+  }): Promise<LoginLocationResponse> => {
+    try {
+      const response = await axios.get('/map/markers', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取登录位置详情
+  getLocationDetail: async (locationId: number): Promise<LoginLocationDetailResponse> => {
+    try {
+      const response = await axios.get(`/map/location/${locationId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新登录位置信息
+  updateLocation: async (locationId: number, data: { 
+    longitude: number; 
+    latitude: number; 
+    location_name?: string; 
+  }): Promise<LoginLocationUpdateResponse> => {
+    try {
+      const response = await axios.put(`/map/location/${locationId}`, data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 97 - 0
client/mobile/api/message.ts

@@ -0,0 +1,97 @@
+import axios from 'axios';
+import type {
+  MessageType, MessageStatus, UserMessage
+} from '../../share/types.ts';
+
+// 消息API响应类型
+export interface MessageResponse {
+  message: string;
+  data?: any;
+}
+
+export interface MessagesResponse {
+  data: UserMessage[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+export interface UnreadCountResponse {
+  count: number;
+}
+
+// 消息API
+export const MessageAPI = {
+  // 获取消息列表
+  getMessages: async (params?: {
+    page?: number,
+    pageSize?: number,
+    type?: MessageType,
+    status?: MessageStatus
+  }): Promise<MessagesResponse> => {
+    try {
+      const response = await axios.get('/messages', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取消息详情
+  getMessage: async (id: number): Promise<MessageResponse> => {
+    try {
+      const response = await axios.get(`/messages/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 发送消息
+  sendMessage: async (data: {
+    title: string,
+    content: string,
+    type: MessageType,
+    receiver_ids: number[]
+  }): Promise<MessageResponse> => {
+    try {
+      const response = await axios.post('/messages', data);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 删除消息(软删除)
+  deleteMessage: async (id: number): Promise<MessageResponse> => {
+    try {
+      const response = await axios.delete(`/messages/${id}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取未读消息数量
+  getUnreadCount: async (): Promise<UnreadCountResponse> => {
+    try {
+      const response = await axios.get('/messages/count/unread');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 标记消息为已读
+  markAsRead: async (id: number): Promise<MessageResponse> => {
+    try {
+      const response = await axios.post(`/messages/${id}/read`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 48 - 0
client/mobile/api/system.ts

@@ -0,0 +1,48 @@
+import axios from 'axios';
+import type {
+  SystemSetting, SystemSettingGroupData,
+} from '../../share/types.ts';
+
+// 系统设置API
+export const SystemAPI = {
+  // 获取所有系统设置
+  getSettings: async (): Promise<SystemSettingGroupData[]> => {
+    try {
+      const response = await axios.get('/settings');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 获取指定分组的系统设置
+  getSettingsByGroup: async (group: string): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.get(`/settings/group/${group}`);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+    
+  },
+
+  // 更新系统设置
+  updateSettings: async (settings: Partial<SystemSetting>[]): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.put('/settings', settings);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 重置系统设置
+  resetSettings: async (): Promise<SystemSetting[]> => {
+    try {
+      const response = await axios.post('/settings/reset');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 37 - 0
client/mobile/api/theme.ts

@@ -0,0 +1,37 @@
+import axios from 'axios';
+import type {
+  ThemeSettings
+} from '../../share/types.ts';
+
+// Theme API 定义
+export const ThemeAPI = {
+  // 获取主题设置
+  getThemeSettings: async (): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.get('/theme');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 更新主题设置
+  updateThemeSettings: async (themeData: Partial<ThemeSettings>): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.put('/theme', themeData);
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+
+  // 重置主题设置
+  resetThemeSettings: async (): Promise<ThemeSettings> => {
+    try {
+      const response = await axios.post('/theme/reset');
+      return response.data.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 106 - 0
client/mobile/api/user.ts

@@ -0,0 +1,106 @@
+import axios from 'axios';
+import type { User } from '../../share/types.ts';
+
+// 为UserAPI添加的接口响应类型
+interface UsersResponse {
+  data: User[];
+  pagination: {
+    total: number;
+    current: number;
+    pageSize: number;
+    totalPages: number;
+  };
+}
+
+export interface UserResponse {
+  data: User;
+  message?: string;
+}
+
+interface UserCreateResponse {
+  message: string;
+  data: User;
+}
+
+interface UserUpdateResponse {
+  message: string;
+  data: User;
+}
+
+interface UserDeleteResponse {
+  message: string;
+  id: number;
+}
+
+// 用户管理API
+export const UserAPI = {
+  // 获取用户列表
+  getUsers: async (params?: { page?: number, limit?: number, search?: string }): Promise<UsersResponse> => {
+    try {
+      const response = await axios.get('/users', { params });
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取单个用户详情
+  getUser: async (userId: number): Promise<UserResponse> => {
+    try {
+      const response = await axios.get(`/users/${userId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 创建用户
+  createUser: async (userData: Partial<User>): Promise<UserCreateResponse> => {
+    try {
+      const response = await axios.post('/users', userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 更新用户信息
+  updateUser: async (userId: number, userData: Partial<User>): Promise<UserUpdateResponse> => {
+    try {
+      const response = await axios.put(`/users/${userId}`, userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 删除用户
+  deleteUser: async (userId: number): Promise<UserDeleteResponse> => {
+    try {
+      const response = await axios.delete(`/users/${userId}`);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 获取当前用户信息
+  getCurrentUser: async (): Promise<UserResponse> => {
+    try {
+      const response = await axios.get('/users/me/profile');
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  },
+  
+  // 更新当前用户信息
+  updateCurrentUser: async (userData: Partial<User>): Promise<UserUpdateResponse> => {
+    try {
+      const response = await axios.put('/users/me/profile', userData);
+      return response.data;
+    } catch (error) {
+      throw error;
+    }
+  }
+};

+ 167 - 0
client/mobile/components_uploader.tsx

@@ -0,0 +1,167 @@
+import React, { useState } from 'react';
+import { 
+  Layout, Menu, Button, Table, Space,
+  Form, Input, Select, message, Modal,
+  Card, Spin, Row, Col, Breadcrumb, Avatar,
+  Dropdown, ConfigProvider, theme, Typography,
+  Switch, Badge, Image, Upload, Divider, Descriptions,
+  Popconfirm, Tag, Statistic, DatePicker, Radio, Progress, Tabs, List, Alert, Collapse, Empty, Drawer
+} from 'antd';
+import {
+  UploadOutlined,
+} from '@ant-design/icons';   
+import { uploadMinIOWithPolicy , uploadOSSWithPolicy} from '@d8d-appcontainer/api';
+import type { MinioUploadPolicy, OSSUploadPolicy } from '@d8d-appcontainer/types';
+import 'dayjs/locale/zh-cn';
+import { OssType } from '../share/types.ts';
+
+import { FileAPI } from './api/index.ts';
+
+// MinIO文件上传组件
+export const Uploader = ({ 
+  onSuccess, 
+  onError,
+  onProgress, 
+  maxSize = 10 * 1024 * 1024,
+  prefix = 'uploads/',
+  allowedTypes = ['image/jpeg', 'image/png', 'application/pdf', 'text/plain']
+}: {
+  onSuccess?: (fileUrl: string, fileInfo: any) => void;
+  onError?: (error: Error) => void;
+  onProgress?: (percent: number) => void;
+  maxSize?: number;
+  prefix?: string;
+  allowedTypes?: string[];
+}) => {
+  const [uploading, setUploading] = useState(false);
+  const [progress, setProgress] = useState(0);
+  
+  // 处理文件上传
+  const handleUpload = async (options: any) => {
+    const { file, onSuccess: uploadSuccess, onError: uploadError, onProgress: uploadProgress } = options;
+    
+    setUploading(true);
+    setProgress(0);
+    
+    // 文件大小检查
+    if (file.size > maxSize) {
+      message.error(`文件大小不能超过${maxSize / 1024 / 1024}MB`);
+      uploadError(new Error('文件过大'));
+      setUploading(false);
+      return;
+    }
+    
+    // 文件类型检查
+    if (allowedTypes && allowedTypes.length > 0 && !allowedTypes.includes(file.type)) {
+      message.error(`不支持的文件类型: ${file.type}`);
+      uploadError(new Error('不支持的文件类型'));
+      setUploading(false);
+      return;
+    }
+    
+    try {
+      // 1. 获取上传策略
+      const policyResponse = await FileAPI.getUploadPolicy(file.name, prefix, maxSize);
+      const policy = policyResponse.data;
+      
+      if (!policy) {
+        throw new Error('获取上传策略失败');
+      }
+      
+      // 生成随机文件名但保留原始扩展名
+      const fileExt = file.name.split('.').pop() || '';
+      const randomName = `${Date.now()}_${Math.random().toString(36).substring(2, 10)}${fileExt ? `.${fileExt}` : ''}`;
+      
+      // 2. 上传文件到MinIO
+      const callbacks = {
+        onProgress: (event: { progress: number }) => {
+          const percent = Math.round(event.progress);
+          setProgress(percent);
+          uploadProgress({ percent });
+          onProgress?.(percent);
+        },
+        onComplete: () => {
+          setUploading(false);
+          setProgress(100);
+        },
+        onError: (err: Error) => {
+          setUploading(false);
+          message.error(`上传失败: ${err.message}`);
+          uploadError(err);
+          onError?.(err);
+        }
+      };
+      
+      // 执行上传
+      const fileUrl = window.CONFIG?.OSS_TYPE === OssType.MINIO ? 
+        await uploadMinIOWithPolicy(
+          policy as MinioUploadPolicy,
+          file,
+          randomName,
+          callbacks
+        ) : await uploadOSSWithPolicy(
+          policy as OSSUploadPolicy,
+          file,
+          randomName,
+          callbacks
+        );
+      
+      // 从URL中提取相对路径
+      const relativePath = `${policy.prefix}${randomName}`;
+      
+      // 3. 保存文件信息到文件库
+      const fileInfo = {
+        file_name: randomName,
+        original_filename: file.name,
+        file_path: relativePath,
+        file_type: file.type,
+        file_size: file.size,
+        tags: '',
+        description: '',
+        category_id: undefined
+      };
+      
+      const saveResponse = await FileAPI.saveFileInfo(fileInfo);
+      
+      // 操作成功
+      uploadSuccess(relativePath);
+      message.success('文件上传成功');
+      onSuccess?.(relativePath, saveResponse.data);
+    } catch (error: any) {
+      // 上传失败
+      setUploading(false);
+      message.error(`上传失败: ${error.message}`);
+      uploadError(error);
+      onError?.(error);
+    }
+  };
+  
+  return (
+    <Upload.Dragger
+      name="file"
+      multiple={false}
+      customRequest={handleUpload}
+      showUploadList={true}
+      progress={{
+        strokeColor: {
+          '0%': '#108ee9',
+          '100%': '#87d068',
+        },
+        format: (percent) => `${Math.round(percent || 0)}%`,
+      }}
+    >
+      <p className="ant-upload-drag-icon">
+        <UploadOutlined />
+      </p>
+      <p className="ant-upload-text">点击或拖动文件到这里上传</p>
+      <p className="ant-upload-hint">
+        支持单个文件上传,最大{maxSize / 1024 / 1024}MB
+      </p>
+      {uploading && (
+        <div style={{ marginTop: 16 }}>
+          <Progress percent={progress} />
+        </div>
+      )}
+    </Upload.Dragger>
+  );
+};

+ 246 - 0
client/mobile/hooks.tsx

@@ -0,0 +1,246 @@
+import React, { createContext, useContext, useState, useEffect } from 'react';
+import axios from 'axios';
+import { useQuery, useQueryClient } from '@tanstack/react-query';
+import { getLocalStorageWithExpiry, setLocalStorageWithExpiry } from './utils.ts';
+import type { User, AuthContextType, ThemeContextType, ThemeSettings } from '../share/types.ts';
+import { ThemeMode, FontSize, CompactMode } from '../share/types.ts';
+import { AuthAPI, ThemeAPI } from './api/index.ts';
+
+// 默认主题设置
+const defaultThemeSettings: ThemeSettings = {
+  user_id: 0,
+  theme_mode: ThemeMode.LIGHT,
+  primary_color: '#3B82F6', // 蓝色
+  background_color: '#F9FAFB',
+  text_color: '#111827',
+  border_radius: 8,
+  font_size: FontSize.MEDIUM,
+  is_compact: CompactMode.NORMAL
+};
+
+// 创建认证上下文
+const AuthContext = createContext<AuthContextType | null>(null);
+
+// 创建主题上下文
+const ThemeContext = createContext<ThemeContextType | null>(null);
+
+// 认证提供者组件
+export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [user, setUser] = useState<User | null>(null);
+  const [token, setToken] = useState<string | null>(getLocalStorageWithExpiry('mobile_token'));
+  const [isAuthenticated, setIsAuthenticated] = useState(false);
+  const queryClient = useQueryClient();
+
+  // 使用useQuery检查登录状态
+  const { isLoading: isAuthChecking } = useQuery({
+    queryKey: ['auth', 'status', token],
+    queryFn: async () => {
+      if (!token) {
+        setUser(null);
+        setIsAuthenticated(false);
+        return null;
+      }
+      
+      try {
+        // 设置请求头
+        axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
+        // 获取当前用户信息
+        const currentUser = await AuthAPI.getCurrentUser();
+        setUser(currentUser);
+        setIsAuthenticated(true);
+        setLocalStorageWithExpiry('mobile_user', currentUser, 24);
+        return { isValid: true, user: currentUser };
+      } catch (error) {
+        // 如果API调用失败,自动登出
+        logout();
+        return { isValid: false };
+      }
+    },
+    enabled: !!token,
+    refetchOnWindowFocus: false,
+    retry: false,
+  });
+  
+
+  // 登录函数
+  const login = async (username: string, password: string, latitude?: number, longitude?: number) => {
+    try {
+      const response = await AuthAPI.login(username, password, latitude, longitude);
+      const { token, user } = response;
+      
+      // 保存到状态和本地存储
+      setToken(token);
+      setUser(user);
+      setLocalStorageWithExpiry('mobile_token', token, 24); // 24小时过期
+      setLocalStorageWithExpiry('mobile_user', user, 24);
+      
+      // 设置请求头
+      axios.defaults.headers.common['Authorization'] = `Bearer ${token}`;
+      
+    } catch (error) {
+      console.error('登录失败:', error);
+      throw error;
+    }
+  };
+
+  // 登出函数
+  const logout = async () => {
+    try {
+      // 调用登出API
+      await AuthAPI.logout();
+    } catch (error) {
+      console.error('登出API调用失败:', error);
+    } finally {
+      // 无论API调用成功与否,都清除本地状态
+      setToken(null);
+      setUser(null);
+      localStorage.removeItem('mobile_token');
+      localStorage.removeItem('mobile_user');
+      // 清除请求头
+      delete axios.defaults.headers.common['Authorization'];
+      // 清除所有查询缓存
+      queryClient.clear();
+    }
+  };
+
+  return (
+    <AuthContext.Provider
+      value={{
+        user,
+        token,
+        login,
+        logout,
+        isAuthenticated,
+        isLoading: isAuthChecking
+      }}
+    >
+      {children}
+    </AuthContext.Provider>
+  );
+};
+
+// 主题提供者组件
+export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+  const [currentTheme, setCurrentTheme] = useState<ThemeSettings>(() => {
+    const storedTheme = localStorage.getItem('theme');
+    return storedTheme ? JSON.parse(storedTheme) : defaultThemeSettings;
+  });
+  
+  const isDark = currentTheme.theme_mode === ThemeMode.DARK;
+
+  // 更新主题(实时预览)
+  const updateTheme = (theme: Partial<ThemeSettings>) => {
+    setCurrentTheme(prev => {
+      const updatedTheme = { ...prev, ...theme };
+      localStorage.setItem('theme', JSON.stringify(updatedTheme));
+      return updatedTheme;
+    });
+  };
+
+  // 保存主题到后端
+  const saveTheme = async (theme: Partial<ThemeSettings>): Promise<ThemeSettings> => {
+    try {
+      const updatedTheme = { ...currentTheme, ...theme };
+      const data = await ThemeAPI.updateThemeSettings(updatedTheme);
+      
+      setCurrentTheme(data);
+      localStorage.setItem('theme', JSON.stringify(data));
+      
+      return data;
+    } catch (error) {
+      console.error('保存主题失败:', error);
+      throw error;
+    }
+  };
+
+  // 重置主题
+  const resetTheme = async (): Promise<ThemeSettings> => {
+    try {
+      const data = await ThemeAPI.resetThemeSettings();
+      
+      setCurrentTheme(data);
+      localStorage.setItem('theme', JSON.stringify(data));
+      
+      return data;
+    } catch (error) {
+      console.error('重置主题失败:', error);
+      
+      // 如果API失败,至少重置到默认主题
+      setCurrentTheme(defaultThemeSettings);
+      localStorage.setItem('theme', JSON.stringify(defaultThemeSettings));
+      
+      return defaultThemeSettings;
+    }
+  };
+
+  // 切换主题模式(亮色/暗色)
+  const toggleTheme = () => {
+    const newMode = isDark ? ThemeMode.LIGHT : ThemeMode.DARK;
+    const updatedTheme = {
+      ...currentTheme,
+      theme_mode: newMode,
+      // 暗色和亮色模式下自动调整背景色和文字颜色
+      background_color: newMode === ThemeMode.DARK ? '#121212' : '#F9FAFB',
+      text_color: newMode === ThemeMode.DARK ? '#E5E7EB' : '#111827'
+    };
+    
+    setCurrentTheme(updatedTheme);
+    localStorage.setItem('theme', JSON.stringify(updatedTheme));
+  };
+
+  // 主题变化时应用CSS变量
+  useEffect(() => {
+    document.documentElement.style.setProperty('--primary-color', currentTheme.primary_color);
+    document.documentElement.style.setProperty('--background-color', currentTheme.background_color || '#F9FAFB');
+    document.documentElement.style.setProperty('--text-color', currentTheme.text_color || '#111827');
+    document.documentElement.style.setProperty('--border-radius', `${currentTheme.border_radius || 8}px`);
+    
+    // 设置字体大小
+    let rootFontSize = '16px'; // 默认中等字体
+    if (currentTheme.font_size === FontSize.SMALL) {
+      rootFontSize = '14px';
+    } else if (currentTheme.font_size === FontSize.LARGE) {
+      rootFontSize = '18px';
+    }
+    document.documentElement.style.setProperty('--font-size', rootFontSize);
+    
+    // 设置暗色模式类
+    if (isDark) {
+      document.documentElement.classList.add('dark');
+    } else {
+      document.documentElement.classList.remove('dark');
+    }
+  }, [currentTheme, isDark]);
+
+  return (
+    <ThemeContext.Provider
+      value={{
+        isDark,
+        currentTheme,
+        updateTheme,
+        saveTheme,
+        resetTheme,
+        toggleTheme
+      }}
+    >
+      {children}
+    </ThemeContext.Provider>
+  );
+};
+
+// 使用上下文的钩子
+export const useAuth = () => {
+  const context = useContext(AuthContext);
+  if (!context) {
+    throw new Error('useAuth必须在AuthProvider内部使用');
+  }
+  return context;
+};
+
+export const useTheme = () => {
+  const context = useContext(ThemeContext);
+  if (!context) {
+    throw new Error('useTheme必须在ThemeProvider内部使用');
+  }
+  return context;
+};

Неке датотеке нису приказане због велике количине промена