| New file |
| | |
| | | package com.zy.component.influxdb.domain; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.Date; |
| | | |
| | | @Data |
| | | public class BaseMessage implements Serializable { |
| | | |
| | | private Long timestamp; |
| | | } |
| | |
| | | import com.influxdb.v3.client.query.QueryOptions; |
| | | import com.influxdb.v3.client.query.QueryType; |
| | | import com.influxdb.v3.client.write.WritePrecision; |
| | | import com.zy.component.influxdb.domain.BaseMessage; |
| | | import org.slf4j.Logger; |
| | | import org.slf4j.LoggerFactory; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | |
| | | import java.lang.reflect.InvocationTargetException; |
| | | import java.time.Instant; |
| | | import java.util.Arrays; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.stream.Collectors; |
| | |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | /** |
| | | * 查询数据 |
| | | * |
| | | * @param sql sql语句 |
| | | * @return 查询结果列表 |
| | | */ |
| | | public <T> List<T> queryPoints(String sql, Class<T> clazz) { |
| | | public <T extends BaseMessage> List<T> queryPoints(String sql, Class<T> clazz) { |
| | | return queryPoints(sql,new HashMap<>(),clazz); |
| | | } |
| | | /** |
| | | * 查询数据 |
| | | * |
| | | * @param sql sql语句 |
| | | * @return 查询结果列表 |
| | | */ |
| | | public <T extends BaseMessage> List<T> queryPoints(String sql,Map<String,Object> queryParams, Class<T> clazz) { |
| | | try { |
| | | // 执行查询 |
| | | Stream<PointValues> queryPoints = influxDBClient.queryPoints(sql); |
| | | Stream<PointValues> queryPoints = influxDBClient.queryPoints(sql, queryParams); |
| | | Field[] declaredFields = clazz.getDeclaredFields(); |
| | | |
| | | // 创建一个列表用于存储结果 |
| | |
| | | } |
| | | } |
| | | } |
| | | newInstance.setTimestamp(point.getTimestamp().longValue()); |
| | | return newInstance; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | logger.info("查询数据:{}", result); |
| | | return result; |
| | | } catch (Exception e) { |
| | | logger.error("Failed to query data from the database."); |
| | |
| | | Target Server Version : 50744 |
| | | File Encoding : 65001 |
| | | |
| | | Date: 09/03/2026 13:09:19 |
| | | Date: 12/03/2026 16:50:09 |
| | | */ |
| | | |
| | | SET NAMES utf8mb4; |
| | |
| | | `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', |
| | | `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', |
| | | PRIMARY KEY (`id`) USING BTREE |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 8 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '设备表' ROW_FORMAT = Dynamic; |
| | | |
| | | -- ---------------------------- |
| | | -- Records of cv_devp |
| | | -- ---------------------------- |
| | | INSERT INTO `cv_devp` VALUES (1, 1001, NULL, 'Y', 'N', 'Y', 'N', 'Y', 'Y', 0, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (2, 1002, NULL, 'Y', 'N', 'Y', 'N', 'Y', 'Y', 0, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (3, 1003, NULL, 'Y', 'N', 'Y', 'N', 'Y', 'Y', 0, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (4, 1004, NULL, 'Y', 'N', 'Y', 'Y', 'Y', 'Y', 0, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (5, 1005, NULL, 'N', 'Y', 'Y', 'Y', 'Y', 'Y', 14, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (6, 1006, NULL, 'N', 'Y', 'Y', 'Y', 'Y', 'Y', 13, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (7, 1007, NULL, 'N', 'Y', 'Y', 'Y', 'Y', 'Y', 9, NULL, 1, 1, 0, NULL, NULL, '2026-03-04 10:30:55', NULL, '2026-03-05 14:57:11', NULL); |
| | | INSERT INTO `cv_devp` VALUES (8, 2, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_devp` VALUES (9, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | |
| | | -- ---------------------------- |
| | | -- Table structure for cv_job |
| | |
| | | `deleted` int(11) NULL DEFAULT NULL COMMENT '是否删除 1:是 0:否', |
| | | `tenant_id` bigint(20) NULL DEFAULT NULL COMMENT '租户', |
| | | `create_by` bigint(20) NULL DEFAULT NULL COMMENT '添加人员', |
| | | `create_time` datetime NULL DEFAULT NULL COMMENT '添加时间', |
| | | `create_time` datetime NULL DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '添加时间', |
| | | `update_by` bigint(20) NULL DEFAULT NULL COMMENT '修改人员', |
| | | `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', |
| | | `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', |
| | | PRIMARY KEY (`id`) USING BTREE |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '任务表' ROW_FORMAT = Dynamic; |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '任务表' ROW_FORMAT = Dynamic; |
| | | |
| | | -- ---------------------------- |
| | | -- Records of cv_job |
| | | -- ---------------------------- |
| | | INSERT INTO `cv_job` VALUES (7, 'TK2603054400', NULL, NULL, NULL, NULL, 8, 2, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job` VALUES (8, 'TK2603054408', '00000011', 'TK2603054408', '1007', 'A102900101', 9, 7, '2026-03-05 13:55:03', '2026-03-05 13:55:01', '2026-03-05 13:55:09', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job` VALUES (9, 'TK2603054404', NULL, NULL, NULL, NULL, 10, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '模拟按按钮'); |
| | | INSERT INTO `cv_job` VALUES (10, 'TK2603054405', NULL, NULL, NULL, NULL, 11, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '模拟按按钮'); |
| | | INSERT INTO `cv_job` VALUES (11, 'TK2603054406', NULL, NULL, NULL, NULL, 12, 3, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '模拟按按钮'); |
| | | INSERT INTO `cv_job` VALUES (12, 'TK2603054409', '00000006', 'TK2603054409', '1007', 'A102400201', 13, 5, '2026-03-05 14:07:16', '2026-03-05 14:07:15', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job` VALUES (13, 'TK2603054410', '00000005', 'TK2603054410', '1007', 'A102500201', 14, 5, '2026-03-05 14:14:19', '2026-03-05 14:09:20', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | |
| | | -- ---------------------------- |
| | | -- Table structure for cv_job_log |
| | |
| | | `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间', |
| | | `memo` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', |
| | | PRIMARY KEY (`id`) USING BTREE |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '任务日志表' ROW_FORMAT = Dynamic; |
| | | ) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '任务日志表' ROW_FORMAT = Dynamic; |
| | | |
| | | -- ---------------------------- |
| | | -- Records of cv_job_log |
| | | -- ---------------------------- |
| | | INSERT INTO `cv_job_log` VALUES (1, 'TK2603054397', '00000006', 'TK2603054397', '1007', 'A103200101', 2, 8, '2026-03-05 13:21:57', '2026-03-05 13:21:55', '2026-03-05 13:22:03', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job_log` VALUES (2, 'TK2603054398', '00000011', 'TK2603054398', '1007', 'A102900101', 3, 8, '2026-03-05 13:36:45', '2026-03-05 13:36:43', '2026-03-05 13:36:50', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job_log` VALUES (3, 'TK2603054399', '00000006', 'TK2603054399', '1007', 'A102400201', 4, 8, '2026-03-05 13:37:25', '2026-03-05 13:37:23', '2026-03-05 13:37:31', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job_log` VALUES (4, 'TK2603054401', '00000009', 'TK2603054401', '1007', 'A102500201', 5, 8, '2026-03-05 13:43:49', '2026-03-05 13:43:47', '2026-03-05 13:43:55', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job_log` VALUES (5, 'TK2603054402', '00000005', 'TK2603054402', '1007', 'A102600201', 6, 8, '2026-03-05 13:43:55', '2026-03-05 13:43:53', '2026-03-05 13:44:13', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_job_log` VALUES (6, 'TK2603054403', '00000012', 'TK2603054403', '1007', 'A102700201', 7, 8, '2026-03-05 13:44:14', '2026-03-05 13:44:01', '2026-03-05 13:46:10', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | |
| | | -- ---------------------------- |
| | | -- Table structure for cv_work_lastno |
| | |
| | | -- ---------------------------- |
| | | -- Records of cv_work_lastno |
| | | -- ---------------------------- |
| | | INSERT INTO `cv_work_lastno` VALUES (1, 1, 14, 1, 8999, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | INSERT INTO `cv_work_lastno` VALUES (1, 1, 1, 1, 8999, 1, 0, NULL, NULL, NULL, NULL, NULL, NULL); |
| | | |
| | | SET FOREIGN_KEY_CHECKS = 1; |
| | |
| | | |
| | | @Data |
| | | @AllArgsConstructor |
| | | @NoArgsConstructor |
| | | public class DeviceMessage implements Serializable { |
| | | |
| | | private String sourceHexStr; |
| | | |
| | | private Long timestamp = System.currentTimeMillis(); |
| | | |
| | | |
| | | public DeviceMessage(String sourceHexStr) { |
| | | this.sourceHexStr = sourceHexStr; |
| | | } |
| | |
| | | mark: 10 |
| | | max-retries: 3 |
| | | retry-delay: 800 |
| | | - type: FAKEUSER |
| | | mark: 20 |
| | | max-retries: 2 |
| | | retry-delay: 800 |
| | | # - type: FAKEUSER |
| | | # mark: 20 |
| | | # max-retries: 2 |
| | | # retry-delay: 800 |
| | | - type: APPLYLOC |
| | | mark: 30 |
| | | max-retries: 2 |
| | |
| | | select * |
| | | from cv_job |
| | | where job_sts = #{jobSts} |
| | | and DATEDIFF(NOW(), #{day}) > 1 |
| | | and DATEDIFF(NOW(), create_time) > #{day} |
| | | </select> |
| | | |
| | | |
| | |
| | | select * |
| | | from cv_job |
| | | where job_sts = #{jobSts} |
| | | and DATEDIFF(NOW(), #{day}) > 1 |
| | | and DATEDIFF(NOW(), create_time) > #{day} |
| | | </select> |
| | | </mapper> |
| New file |
| | |
| | | package com.zy.acs.gateway.controller; |
| | | |
| | | import com.zy.acs.framework.common.R; |
| | | import com.zy.acs.gateway.utils.PacCoder; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping(value = "/utils") |
| | | public class UtilsController { |
| | | |
| | | /** |
| | | * 查询最新的十条数据 |
| | | * |
| | | * @return |
| | | */ |
| | | @RequestMapping(value = "/decode/{hex}") |
| | | @ResponseBody |
| | | public R query(@PathVariable String hex) { |
| | | return R.ok(PacCoder.decode(hex)); |
| | | } |
| | | |
| | | } |
| | |
| | | font-family: 'Inter', sans-serif; |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .layui-card { |
| | | margin-bottom: 16px; |
| | | } |
| | | |
| | | .layui-card-header { |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .status-online { |
| | | color: #10b981; |
| | | } |
| | | |
| | | .status-offline { |
| | | color: #ef4444; |
| | | } |
| | | |
| | | .chart-container { |
| | | height: 300px; |
| | | } |
| | | |
| | | .realtime-container { |
| | | height: 160px; |
| | | overflow-y: auto; |
| | |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div id="app"> |
| | | <!-- 顶部导航栏 --> |
| | | <lay-header height="60px" bg-color="#fff" shadow> |
| | | <template #left> |
| | | <div class="flex items-center space-x-2"> |
| | | <i class="fa fa-android text-2xl" style="color: #3b82f6"></i> |
| | | <h1 class="text-xl font-bold" style="color: #1e293b">机器人数据监控</h1> |
| | | </div> |
| | | </template> |
| | | <template #right> |
| | | <div class="flex items-center space-x-4"> |
| | | <lay-input placeholder="搜索机器人..." prefix-icon="search" style="width: 200px"></lay-input> |
| | | <lay-button type="primary" @click="refreshData"> |
| | | <i class="fa fa-refresh mr-1"></i> 刷新 |
| | | </lay-button> |
| | | </div> |
| | | </template> |
| | | </lay-header> |
| | | <div id="app"> |
| | | <!-- 顶部导航栏 --> |
| | | <lay-header height="60px" bg-color="#fff" shadow> |
| | | <template #left> |
| | | <div class="flex items-center space-x-2"> |
| | | <i class="fa fa-android text-2xl" style="color: #3b82f6"></i> |
| | | <h1 class="text-xl font-bold" style="color: #1e293b">机器人数据监控</h1> |
| | | </div> |
| | | </template> |
| | | <template #right> |
| | | <div class="flex items-center space-x-4"> |
| | | <lay-input placeholder="搜索机器人..." prefix-icon="search" style="width: 200px"></lay-input> |
| | | <lay-button type="primary" @click="refreshData"> |
| | | <i class="fa fa-refresh mr-1"></i> 刷新 |
| | | </lay-button> |
| | | </div> |
| | | </template> |
| | | </lay-header> |
| | | |
| | | <!-- 主内容区 --> |
| | | <lay-container style="padding: 20px"> |
| | | <!-- 状态概览 --> |
| | | <lay-row :gutter="16"> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">总机器人数</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #1e293b">24</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #dbeafe; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-microchip text-xl" style="color: #3b82f6"></i> |
| | | </div> |
| | | <!-- 主内容区 --> |
| | | <lay-container style="padding: 20px"> |
| | | <!-- 状态概览 --> |
| | | <lay-row :gutter="16"> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">总机器人数</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #1e293b">24</h3> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">在线机器人</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #10b981">18</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #d1fae5; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-check-circle text-xl" style="color: #10b981"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">离线机器人</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #ef4444">6</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #fee2e2; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-exclamation-circle text-xl" style="color: #ef4444"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">今日数据量</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #f59e0b">1.2k</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #fef3c7; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-database text-xl" style="color: #f59e0b"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | </lay-row> |
| | | |
| | | <!-- 数据图表 --> |
| | | <lay-row :gutter="16" style="margin-top: 16px"> |
| | | <lay-col :span="12"> |
| | | <lay-card shadow> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">上行数据趋势</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm" type="primary">小时</lay-button> |
| | | <lay-button size="sm">天</lay-button> |
| | | <lay-button size="sm">周</lay-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <canvas ref="upDataChart"></canvas> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="12"> |
| | | <lay-card shadow> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">下行数据趋势</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm" type="primary">小时</lay-button> |
| | | <lay-button size="sm">天</lay-button> |
| | | <lay-button size="sm">周</lay-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <canvas ref="downDataChart"></canvas> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | </lay-row> |
| | | |
| | | <!-- 设备数据表格 --> |
| | | <lay-card shadow style="margin-top: 16px"> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">机器人数据列表</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm"> |
| | | <i class="fa fa-filter mr-1"></i> 筛选 |
| | | </lay-button> |
| | | <lay-button size="sm"> |
| | | <i class="fa fa-download mr-1"></i> 导出 |
| | | </lay-button> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #dbeafe; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-microchip text-xl" style="color: #3b82f6"></i> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <lay-table :data="devices" :height="400"> |
| | | <lay-table-column prop="id" label="设备ID" width="120"></lay-table-column> |
| | | <lay-table-column prop="name" label="设备名称" width="150"></lay-table-column> |
| | | <lay-table-column prop="status" label="状态" width="100"> |
| | | <template #default="{ row }"> |
| | | <lay-badge v-if="row.status === 'online'" type="success">在线</lay-badge> |
| | | <lay-badge v-else type="danger">离线</lay-badge> |
| | | </template> |
| | | </lay-table-column> |
| | | <lay-table-column prop="upData" label="上行数据" width="120"></lay-table-column> |
| | | <lay-table-column prop="downData" label="下行数据" width="120"></lay-table-column> |
| | | <lay-table-column prop="lastComm" label="最后通信" width="150"></lay-table-column> |
| | | <lay-table-column label="操作" width="150"> |
| | | <template #default="{ row }"> |
| | | <lay-button size="sm" type="primary" style="margin-right: 8px"> |
| | | <i class="fa fa-eye"></i> |
| | | </lay-button> |
| | | <lay-button size="sm" type="warning" style="margin-right: 8px"> |
| | | <i class="fa fa-edit"></i> |
| | | </lay-button> |
| | | <lay-button size="sm" type="danger"> |
| | | <i class="fa fa-trash"></i> |
| | | </lay-button> |
| | | </template> |
| | | </lay-table-column> |
| | | </lay-table> |
| | | <div class="flex justify-between items-center mt-4"> |
| | | <p style="color: #64748b; font-size: 14px">显示 1-10 条,共 24 条</p> |
| | | <lay-pagination |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">在线机器人</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #10b981">18</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #d1fae5; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-check-circle text-xl" style="color: #10b981"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">离线机器人</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #ef4444">6</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #fee2e2; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-exclamation-circle text-xl" style="color: #ef4444"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="6"> |
| | | <lay-card shadow> |
| | | <div class="flex items-center justify-between"> |
| | | <div> |
| | | <p style="color: #64748b; font-size: 14px">今日数据量</p> |
| | | <h3 style="font-size: 24px; font-weight: bold; color: #f59e0b">1.2k</h3> |
| | | </div> |
| | | <div style="width: 48px; height: 48px; border-radius: 50%; background-color: #fef3c7; display: flex; align-items: center; justify-content: center"> |
| | | <i class="fa fa-database text-xl" style="color: #f59e0b"></i> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | </lay-row> |
| | | |
| | | <!-- 数据图表 --> |
| | | <lay-row :gutter="16" style="margin-top: 16px"> |
| | | <lay-col :span="12"> |
| | | <lay-card shadow> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">上行数据趋势</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm" type="primary">小时</lay-button> |
| | | <lay-button size="sm">天</lay-button> |
| | | <lay-button size="sm">周</lay-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <canvas ref="upDataChart"></canvas> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | <lay-col :span="12"> |
| | | <lay-card shadow> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">下行数据趋势</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm" type="primary">小时</lay-button> |
| | | <lay-button size="sm">天</lay-button> |
| | | <lay-button size="sm">周</lay-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="chart-container"> |
| | | <canvas ref="downDataChart"></canvas> |
| | | </div> |
| | | </lay-card> |
| | | </lay-col> |
| | | </lay-row> |
| | | |
| | | <!-- 设备数据表格 --> |
| | | <lay-card shadow style="margin-top: 16px"> |
| | | <template #header> |
| | | <div class="flex justify-between items-center"> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">机器人数据列表</h2> |
| | | <div class="flex space-x-2"> |
| | | <lay-button size="sm"> |
| | | <i class="fa fa-filter mr-1"></i> 筛选 |
| | | </lay-button> |
| | | <lay-button size="sm"> |
| | | <i class="fa fa-download mr-1"></i> 导出 |
| | | </lay-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <lay-table :data="devices" :height="400"> |
| | | <lay-table-column prop="id" label="设备ID" width="120"></lay-table-column> |
| | | <lay-table-column prop="name" label="设备名称" width="150"></lay-table-column> |
| | | <lay-table-column prop="status" label="状态" width="100"> |
| | | <template #default="{ row }"> |
| | | <lay-badge v-if="row.status === 'online'" type="success">在线</lay-badge> |
| | | <lay-badge v-else type="danger">离线</lay-badge> |
| | | </template> |
| | | </lay-table-column> |
| | | <lay-table-column prop="upData" label="上行数据" width="120"></lay-table-column> |
| | | <lay-table-column prop="downData" label="下行数据" width="120"></lay-table-column> |
| | | <lay-table-column prop="lastComm" label="最后通信" width="150"></lay-table-column> |
| | | <lay-table-column label="操作" width="150"> |
| | | <template #default="{ row }"> |
| | | <lay-button size="sm" type="primary" style="margin-right: 8px"> |
| | | <i class="fa fa-eye"></i> |
| | | </lay-button> |
| | | <lay-button size="sm" type="warning" style="margin-right: 8px"> |
| | | <i class="fa fa-edit"></i> |
| | | </lay-button> |
| | | <lay-button size="sm" type="danger"> |
| | | <i class="fa fa-trash"></i> |
| | | </lay-button> |
| | | </template> |
| | | </lay-table-column> |
| | | </lay-table> |
| | | <div class="flex justify-between items-center mt-4"> |
| | | <p style="color: #64748b; font-size: 14px">显示 1-10 条,共 24 条</p> |
| | | <lay-pagination |
| | | v-model:current="currentPage" |
| | | v-model:limit="pageSize" |
| | | :total="total" |
| | | :limits="[10, 20, 50, 100]" |
| | | layout="prev, pager, next, jumper, sizes, total" |
| | | ></lay-pagination> |
| | | </div> |
| | | </lay-card> |
| | | |
| | | <!-- 实时数据更新 --> |
| | | <lay-card shadow style="margin-top: 16px"> |
| | | <template #header> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">实时数据更新</h2> |
| | | </template> |
| | | <div class="realtime-container p-2 border border-gray-200 rounded-lg"> |
| | | <div v-for="(item, index) in realtimeData" :key="index" class="py-1 border-b border-gray-100"> |
| | | <span style="color: #64748b; font-size: 12px">{{ item.timestamp }}</span> |
| | | <span style="color: #1e293b; margin-left: 10px">{{ item.message }}</span> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-container> |
| | | |
| | | <!-- 页脚 --> |
| | | <lay-footer height="60px" bg-color="#fff" shadow> |
| | | <div class="text-center" style="color: #64748b; font-size: 14px"> |
| | | © 2026 机器人数据监控系统 | 版本 1.0.0 |
| | | ></lay-pagination> |
| | | </div> |
| | | </lay-footer> |
| | | </div> |
| | | </lay-card> |
| | | |
| | | <script> |
| | | const { createApp, ref, onMounted } = Vue; |
| | | const app = createApp({ |
| | | components: { |
| | | LayHeader: layui.LayHeader, |
| | | LayContainer: layui.LayContainer, |
| | | LayRow: layui.LayRow, |
| | | LayCol: layui.LayCol, |
| | | LayCard: layui.LayCard, |
| | | LayInput: layui.LayInput, |
| | | LayButton: layui.LayButton, |
| | | LayTable: layui.LayTable, |
| | | LayTableColumn: layui.LayTableColumn, |
| | | LayBadge: layui.LayBadge, |
| | | LayPagination: layui.LayPagination, |
| | | LayFooter: layui.LayFooter |
| | | }, |
| | | setup() { |
| | | // 模拟机器人数据 |
| | | const devices = ref([ |
| | | { id: 'ROB-001', name: '配送机器人1号', status: 'online', upData: '2.4KB', downData: '0.8KB', lastComm: '2分钟前' }, |
| | | { id: 'ROB-002', name: '配送机器人2号', status: 'online', upData: '1.8KB', downData: '0.5KB', lastComm: '5分钟前' }, |
| | | { id: 'ROB-003', name: '巡检机器人1号', status: 'offline', upData: '0KB', downData: '0KB', lastComm: '2小时前' }, |
| | | { id: 'ROB-004', name: '配送机器人3号', status: 'online', upData: '3.2KB', downData: '1.2KB', lastComm: '1分钟前' }, |
| | | { id: 'ROB-005', name: '巡检机器人2号', status: 'online', upData: '1.5KB', downData: '0.6KB', lastComm: '3分钟前' }, |
| | | { id: 'ROB-006', name: '配送机器人4号', status: 'offline', upData: '0KB', downData: '0KB', lastComm: '5小时前' }, |
| | | { id: 'ROB-007', name: '巡检机器人3号', status: 'online', upData: '2.1KB', downData: '0.9KB', lastComm: '4分钟前' }, |
| | | { id: 'ROB-008', name: '配送机器人5号', status: 'online', upData: '2.8KB', downData: '1.1KB', lastComm: '2分钟前' }, |
| | | { id: 'ROB-009', name: '巡检机器人4号', status: 'offline', upData: '0KB', downData: '0KB', lastComm: '1天前' }, |
| | | { id: 'ROB-010', name: '配送机器人6号', status: 'online', upData: '1.9KB', downData: '0.7KB', lastComm: '6分钟前' } |
| | | ]); |
| | | <!-- 实时数据更新 --> |
| | | <lay-card shadow style="margin-top: 16px"> |
| | | <template #header> |
| | | <h2 style="font-size: 16px; font-weight: 600; color: #1e293b">实时数据更新</h2> |
| | | </template> |
| | | <div class="realtime-container p-2 border border-gray-200 rounded-lg"> |
| | | <div v-for="(item, index) in realtimeData" :key="index" class="py-1 border-b border-gray-100"> |
| | | <span style="color: #64748b; font-size: 12px">{{ item.timestamp }}</span> |
| | | <span style="color: #1e293b; margin-left: 10px">{{ item.message }}</span> |
| | | </div> |
| | | </div> |
| | | </lay-card> |
| | | </lay-container> |
| | | |
| | | // 分页数据 |
| | | const currentPage = ref(1); |
| | | const pageSize = ref(10); |
| | | const total = ref(24); |
| | | <!-- 页脚 --> |
| | | <lay-footer height="60px" bg-color="#fff" shadow> |
| | | <div class="text-center" style="color: #64748b; font-size: 14px"> |
| | | © 2026 机器人数据监控系统 | 版本 1.0.0 |
| | | </div> |
| | | </lay-footer> |
| | | </div> |
| | | |
| | | // 实时数据 |
| | | const realtimeData = ref([]); |
| | | <script> |
| | | const {createApp, ref, onMounted} = Vue; |
| | | const app = createApp({ |
| | | components: { |
| | | LayHeader: layui.LayHeader, |
| | | LayContainer: layui.LayContainer, |
| | | LayRow: layui.LayRow, |
| | | LayCol: layui.LayCol, |
| | | LayCard: layui.LayCard, |
| | | LayInput: layui.LayInput, |
| | | LayButton: layui.LayButton, |
| | | LayTable: layui.LayTable, |
| | | LayTableColumn: layui.LayTableColumn, |
| | | LayBadge: layui.LayBadge, |
| | | LayPagination: layui.LayPagination, |
| | | LayFooter: layui.LayFooter |
| | | }, |
| | | setup() { |
| | | // 模拟机器人数据 |
| | | const devices = ref([ |
| | | { |
| | | id: 'ROB-001', |
| | | name: '配送机器人1号', |
| | | status: 'online', |
| | | upData: '2.4KB', |
| | | downData: '0.8KB', |
| | | lastComm: '2分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-002', |
| | | name: '配送机器人2号', |
| | | status: 'online', |
| | | upData: '1.8KB', |
| | | downData: '0.5KB', |
| | | lastComm: '5分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-003', |
| | | name: '巡检机器人1号', |
| | | status: 'offline', |
| | | upData: '0KB', |
| | | downData: '0KB', |
| | | lastComm: '2小时前' |
| | | }, |
| | | { |
| | | id: 'ROB-004', |
| | | name: '配送机器人3号', |
| | | status: 'online', |
| | | upData: '3.2KB', |
| | | downData: '1.2KB', |
| | | lastComm: '1分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-005', |
| | | name: '巡检机器人2号', |
| | | status: 'online', |
| | | upData: '1.5KB', |
| | | downData: '0.6KB', |
| | | lastComm: '3分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-006', |
| | | name: '配送机器人4号', |
| | | status: 'offline', |
| | | upData: '0KB', |
| | | downData: '0KB', |
| | | lastComm: '5小时前' |
| | | }, |
| | | { |
| | | id: 'ROB-007', |
| | | name: '巡检机器人3号', |
| | | status: 'online', |
| | | upData: '2.1KB', |
| | | downData: '0.9KB', |
| | | lastComm: '4分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-008', |
| | | name: '配送机器人5号', |
| | | status: 'online', |
| | | upData: '2.8KB', |
| | | downData: '1.1KB', |
| | | lastComm: '2分钟前' |
| | | }, |
| | | { |
| | | id: 'ROB-009', |
| | | name: '巡检机器人4号', |
| | | status: 'offline', |
| | | upData: '0KB', |
| | | downData: '0KB', |
| | | lastComm: '1天前' |
| | | }, |
| | | { |
| | | id: 'ROB-010', |
| | | name: '配送机器人6号', |
| | | status: 'online', |
| | | upData: '1.9KB', |
| | | downData: '0.7KB', |
| | | lastComm: '6分钟前' |
| | | } |
| | | ]); |
| | | |
| | | // 图表引用 |
| | | const upDataChart = ref(null); |
| | | const downDataChart = ref(null); |
| | | // 分页数据 |
| | | const currentPage = ref(1); |
| | | const pageSize = ref(10); |
| | | const total = ref(24); |
| | | |
| | | // 刷新数据 |
| | | const refreshData = () => { |
| | | console.log('刷新数据'); |
| | | // 这里可以添加实际的刷新逻辑 |
| | | }; |
| | | // 实时数据 |
| | | const realtimeData = ref([]); |
| | | |
| | | // 初始化图表 |
| | | const initCharts = () => { |
| | | // 上行数据图表 |
| | | const upCtx = upDataChart.value.getContext('2d'); |
| | | new Chart(upCtx, { |
| | | type: 'line', |
| | | data: { |
| | | labels: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'], |
| | | datasets: [{ |
| | | label: '上行数据 (KB)', |
| | | data: [12, 19, 15, 25, 22, 30, 28, 35], |
| | | borderColor: '#3b82f6', |
| | | backgroundColor: 'rgba(59, 130, 246, 0.1)', |
| | | tension: 0.4, |
| | | fill: true |
| | | }] |
| | | // 图表引用 |
| | | const upDataChart = ref(null); |
| | | const downDataChart = ref(null); |
| | | |
| | | // 刷新数据 |
| | | const refreshData = () => { |
| | | console.log('刷新数据'); |
| | | // 这里可以添加实际的刷新逻辑 |
| | | }; |
| | | |
| | | // 初始化图表 |
| | | const initCharts = () => { |
| | | // 上行数据图表 |
| | | const upCtx = upDataChart.value.getContext('2d'); |
| | | new Chart(upCtx, { |
| | | type: 'line', |
| | | data: { |
| | | labels: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'], |
| | | datasets: [{ |
| | | label: '上行数据 (KB)', |
| | | data: [12, 19, 15, 25, 22, 30, 28, 35], |
| | | borderColor: '#3b82f6', |
| | | backgroundColor: 'rgba(59, 130, 246, 0.1)', |
| | | tension: 0.4, |
| | | fill: true |
| | | }] |
| | | }, |
| | | options: { |
| | | responsive: true, |
| | | maintainAspectRatio: false, |
| | | plugins: { |
| | | legend: { |
| | | display: false |
| | | } |
| | | }, |
| | | options: { |
| | | responsive: true, |
| | | maintainAspectRatio: false, |
| | | plugins: { |
| | | legend: { |
| | | display: false |
| | | } |
| | | }, |
| | | scales: { |
| | | y: { |
| | | beginAtZero: true |
| | | } |
| | | scales: { |
| | | y: { |
| | | beginAtZero: true |
| | | } |
| | | } |
| | | }); |
| | | |
| | | // 下行数据图表 |
| | | const downCtx = downDataChart.value.getContext('2d'); |
| | | new Chart(downCtx, { |
| | | type: 'line', |
| | | data: { |
| | | labels: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'], |
| | | datasets: [{ |
| | | label: '下行数据 (KB)', |
| | | data: [5, 8, 6, 12, 10, 15, 13, 18], |
| | | borderColor: '#10b981', |
| | | backgroundColor: 'rgba(16, 185, 129, 0.1)', |
| | | tension: 0.4, |
| | | fill: true |
| | | }] |
| | | }, |
| | | options: { |
| | | responsive: true, |
| | | maintainAspectRatio: false, |
| | | plugins: { |
| | | legend: { |
| | | display: false |
| | | } |
| | | }, |
| | | scales: { |
| | | y: { |
| | | beginAtZero: true |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 模拟实时数据更新 |
| | | const simulateRealtimeData = () => { |
| | | const messages = [ |
| | | 'ROB-001 配送机器人1号: 运行中,位置: A1区', |
| | | 'ROB-002 配送机器人2号: 待机中,位置: B2区', |
| | | 'ROB-004 配送机器人3号: 充电中,电量: 85%', |
| | | 'ROB-005 巡检机器人2号: 巡检中,已完成3/5任务', |
| | | 'ROB-007 巡检机器人3号: 待机中,位置: C3区', |
| | | 'ROB-008 配送机器人5号: 运行中,位置: D4区' |
| | | ]; |
| | | |
| | | setInterval(() => { |
| | | const message = messages[Math.floor(Math.random() * messages.length)]; |
| | | const timestamp = new Date().toLocaleTimeString(); |
| | | realtimeData.value.unshift({ timestamp, message }); |
| | | // 限制显示条数 |
| | | if (realtimeData.value.length > 20) { |
| | | realtimeData.value.pop(); |
| | | } |
| | | }, 2000); |
| | | }; |
| | | |
| | | // 页面加载完成后初始化 |
| | | onMounted(() => { |
| | | initCharts(); |
| | | simulateRealtimeData(); |
| | | } |
| | | }); |
| | | |
| | | return { |
| | | devices, |
| | | currentPage, |
| | | pageSize, |
| | | total, |
| | | realtimeData, |
| | | upDataChart, |
| | | downDataChart, |
| | | refreshData |
| | | }; |
| | | } |
| | | }); |
| | | app.mount('#app'); |
| | | </script> |
| | | // 下行数据图表 |
| | | const downCtx = downDataChart.value.getContext('2d'); |
| | | new Chart(downCtx, { |
| | | type: 'line', |
| | | data: { |
| | | labels: ['00:00', '03:00', '06:00', '09:00', '12:00', '15:00', '18:00', '21:00'], |
| | | datasets: [{ |
| | | label: '下行数据 (KB)', |
| | | data: [5, 8, 6, 12, 10, 15, 13, 18], |
| | | borderColor: '#10b981', |
| | | backgroundColor: 'rgba(16, 185, 129, 0.1)', |
| | | tension: 0.4, |
| | | fill: true |
| | | }] |
| | | }, |
| | | options: { |
| | | responsive: true, |
| | | maintainAspectRatio: false, |
| | | plugins: { |
| | | legend: { |
| | | display: false |
| | | } |
| | | }, |
| | | scales: { |
| | | y: { |
| | | beginAtZero: true |
| | | } |
| | | } |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // 模拟实时数据更新 |
| | | const simulateRealtimeData = () => { |
| | | const messages = [ |
| | | 'ROB-001 配送机器人1号: 运行中,位置: A1区', |
| | | 'ROB-002 配送机器人2号: 待机中,位置: B2区', |
| | | 'ROB-004 配送机器人3号: 充电中,电量: 85%', |
| | | 'ROB-005 巡检机器人2号: 巡检中,已完成3/5任务', |
| | | 'ROB-007 巡检机器人3号: 待机中,位置: C3区', |
| | | 'ROB-008 配送机器人5号: 运行中,位置: D4区' |
| | | ]; |
| | | |
| | | setInterval(() => { |
| | | const message = messages[Math.floor(Math.random() * messages.length)]; |
| | | const timestamp = new Date().toLocaleTimeString(); |
| | | realtimeData.value.unshift({timestamp, message}); |
| | | // 限制显示条数 |
| | | if (realtimeData.value.length > 20) { |
| | | realtimeData.value.pop(); |
| | | } |
| | | }, 2000); |
| | | }; |
| | | |
| | | // 页面加载完成后初始化 |
| | | onMounted(() => { |
| | | initCharts(); |
| | | simulateRealtimeData(); |
| | | }); |
| | | |
| | | return { |
| | | devices, |
| | | currentPage, |
| | | pageSize, |
| | | total, |
| | | realtimeData, |
| | | upDataChart, |
| | | downDataChart, |
| | | refreshData |
| | | }; |
| | | } |
| | | }); |
| | | app.mount('#app'); |
| | | </script> |
| | | </body> |
| | | </html> |
| | |
| | | import org.springframework.context.annotation.Bean; |
| | | import org.springframework.context.annotation.Configuration; |
| | | import org.springframework.web.servlet.AsyncHandlerInterceptor; |
| | | import org.springframework.web.servlet.config.annotation.CorsRegistry; |
| | | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; |
| | | import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; |
| | | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; |
| | |
| | | |
| | | @Bean |
| | | public AsyncHandlerInterceptor getAsyncHandlerInterceptor() { |
| | | return new AsyncHandlerInterceptor(){ |
| | | return new AsyncHandlerInterceptor() { |
| | | @Override |
| | | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { |
| | | cors(response); |
| | | cors(response); |
| | | return true; |
| | | } |
| | | }; |
| | | } |
| | | |
| | | |
| | | |
| | | @Override |
| | | public void addResourceHandlers(ResourceHandlerRegistry registry) { |
| | | // 配置静态资源处理器 |
| | | registry.addResourceHandler("/static/**") |
| | | .addResourceLocations("classpath:/static/") |
| | | .addResourceLocations("/static/"); |
| | | // 配置视图文件处理器 |
| | | registry.addResourceHandler("/views/**") |
| | | .addResourceLocations("/views/"); |
| | | } |
| | | |
| | | public static void cors(HttpServletResponse response){ |
| | | public static void cors(HttpServletResponse response) { |
| | | // 跨域设置 |
| | | response.setHeader("Access-Control-Max-Age", "3600"); |
| | | response.setHeader("Access-Control-Allow-Origin", "*"); |
| | |
| | | package com.zy.acs.hex.controller; |
| | | |
| | | import com.zy.acs.common.domain.mq.DeviceMessage; |
| | | import com.influxdb.v3.client.PointValues; |
| | | import com.zy.acs.framework.common.R; |
| | | import com.zy.acs.hex.domain.DeviceLog; |
| | | import com.zy.component.influxdb.service.InfluxDBService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.ResponseBody; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.util.MultiValueMap; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.lang.reflect.Field; |
| | | import java.util.ArrayList; |
| | | import java.util.HashMap; |
| | | import java.util.List; |
| | | import java.util.Map; |
| | | import java.util.stream.Stream; |
| | | |
| | | @RestController |
| | | @Slf4j |
| | |
| | | */ |
| | | @GetMapping(value = "/query") |
| | | @ResponseBody |
| | | public R query() { |
| | | List<DeviceMessage> deviceMessages = influxDBService.queryPoints("select * from device order by time desc limit 10", DeviceMessage.class); |
| | | return R.ok(deviceMessages); |
| | | public R query(@RequestParam(required = false, defaultValue = "device") String measurement, |
| | | @RequestParam(required = false) Map<String, Object> conditions, |
| | | @RequestParam(required = false, defaultValue = "100") Integer limit, |
| | | @RequestParam(required = false, defaultValue = "time") String orderBy, |
| | | @RequestParam(required = false, defaultValue = "DESC") String orderDirection) { |
| | | return R.ok(getData(measurement, conditions, limit, orderBy, orderDirection)); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 通用查询方法,支持动态条件 |
| | | */ |
| | | private List<DeviceLog> getData(String measurement, Map<String, Object> conditions,int limit, String orderBy, String orderDirection) { |
| | | // 构建查询语句 |
| | | StringBuilder sqlBuilder = new StringBuilder("SELECT * FROM ").append(measurement).append(" WHERE 1=1"); |
| | | Map<String, Object> params = new HashMap<>(); |
| | | |
| | | // 动态添加条件 |
| | | if (conditions != null && !conditions.isEmpty()) { |
| | | if (conditions.get("startTime") != null) { |
| | | if (conditions.get("startTime") != null) { |
| | | sqlBuilder.append(" AND ").append("time").append(" >= :").append("startTime"); |
| | | params.put("startTime", conditions.get("startTime")); |
| | | } |
| | | }else if (conditions.get("endTime") != null) { |
| | | if (conditions.get("endTime") != null) { |
| | | sqlBuilder.append(" AND ").append("time").append(" <= :").append("endTime"); |
| | | params.put("endTime", conditions.get("endTime")); |
| | | } |
| | | }else { |
| | | conditions.forEach((key, value) -> { |
| | | if (value != null) { |
| | | sqlBuilder.append(" AND ").append(key).append(" = :").append(key); |
| | | params.put(key, value); |
| | | } |
| | | }); |
| | | } |
| | | } |
| | | // 添加排序和限制 |
| | | sqlBuilder.append(" ORDER BY ").append(orderBy).append(" ").append(orderDirection); |
| | | sqlBuilder.append(" LIMIT :limit"); |
| | | params.put("limit", limit); |
| | | return influxDBService.queryPoints(sqlBuilder.toString(),params, DeviceLog.class); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.controller; |
| | | |
| | | import com.zy.acs.framework.common.R; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.bind.annotation.PostMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | |
| | | /** |
| | | * 登录控制器 |
| | | */ |
| | | @RestController |
| | | @RequestMapping(value = "/login") |
| | | public class LoginController { |
| | | |
| | | @Value("${login.username}") |
| | | private String username; |
| | | |
| | | @Value("${login.password}") |
| | | private String password; |
| | | |
| | | /** |
| | | * 登录接口 |
| | | * |
| | | * @param user 用户名 |
| | | * @param pass 密码 |
| | | * @return 登录结果 |
| | | */ |
| | | @PostMapping(value = "/auth") |
| | | public R login(@RequestParam String user, @RequestParam String pass) { |
| | | if (username.equals(user) && password.equals(pass)) { |
| | | return R.ok("登录成功"); |
| | | } else { |
| | | return R.error("用户名或密码错误"); |
| | | } |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.controller; |
| | | |
| | | import com.zy.acs.framework.common.R; |
| | | import org.springframework.http.HttpEntity; |
| | | import org.springframework.http.HttpHeaders; |
| | | import org.springframework.http.HttpMethod; |
| | | import org.springframework.http.ResponseEntity; |
| | | import org.springframework.web.bind.annotation.GetMapping; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import org.springframework.web.client.RestTemplate; |
| | | |
| | | @RestController |
| | | @RequestMapping(value = "/proxy") |
| | | public class ProxyController { |
| | | |
| | | private final RestTemplate restTemplate = new RestTemplate(); |
| | | |
| | | @GetMapping(value = "/decode") |
| | | public R decode(@RequestParam String hexData) { |
| | | try { |
| | | String url = "http://127.0.0.1:9060/utils/decode/" + hexData; |
| | | HttpHeaders headers = new HttpHeaders(); |
| | | headers.set("Content-Type", "application/json"); |
| | | HttpEntity<String> entity = new HttpEntity<>(headers); |
| | | ResponseEntity<Object> response = restTemplate.exchange(url, HttpMethod.GET, entity, Object.class); |
| | | return R.ok(response.getBody()); |
| | | } catch (Exception e) { |
| | | return R.error("解析失败: " + e.getMessage()); |
| | | } |
| | | } |
| | | } |
| | |
| | | package com.zy.acs.hex.controller; |
| | | |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.stereotype.Controller; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | |
| | |
| | | public class RouterController { |
| | | |
| | | |
| | | |
| | | |
| | | @RequestMapping("/") |
| | | public void index(HttpServletResponse response) { |
| | | try { |
| | | response.sendRedirect( "/views/index.html"); |
| | | response.sendRedirect("/views/index.html"); |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | } |
| | |
| | | @RequestMapping("/login") |
| | | public void login(HttpServletResponse response) { |
| | | try { |
| | | response.sendRedirect( "/views/login.html"); |
| | | response.sendRedirect("/views/login.html"); |
| | | } catch (Exception ex) { |
| | | ex.printStackTrace(); |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.controller; |
| | | |
| | | import com.zy.acs.framework.common.R; |
| | | import com.zy.acs.hex.domain.SelectOption; |
| | | import com.zy.acs.hex.enums.DirectionType; |
| | | import com.zy.acs.hex.enums.ProtocolType; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | |
| | | @RestController |
| | | @Slf4j |
| | | @RequestMapping(value = "/deviceLog") |
| | | public class SelectTypeController { |
| | | |
| | | |
| | | /** |
| | | * 查询消息类型 |
| | | * |
| | | * @return |
| | | */ |
| | | @GetMapping(value = "/queryType") |
| | | @ResponseBody |
| | | public R queryType() { |
| | | DirectionType[] values = DirectionType.values(); |
| | | List<SelectOption> messageTypes = new ArrayList<>(); |
| | | for (DirectionType value : values) { |
| | | messageTypes.add(new SelectOption(value.getText(), value.name().toLowerCase())); |
| | | } |
| | | return R.ok(messageTypes); |
| | | } |
| | | |
| | | /** |
| | | * 查询标签类型 |
| | | * |
| | | * @return |
| | | */ |
| | | @GetMapping(value = "/queryEvent") |
| | | @ResponseBody |
| | | public R queryEvent(@RequestParam(required = false) DirectionType directionType) { |
| | | List<SelectOption> messageTypes = new ArrayList<>(); |
| | | if (directionType == null) { |
| | | ProtocolType[] values = ProtocolType.values(); |
| | | for (ProtocolType value : values) { |
| | | messageTypes.add(new SelectOption(value.name(), value.getDirection().getText() + "-" + value.getDes() + value.name())); |
| | | } |
| | | return R.ok(messageTypes); |
| | | } |
| | | List<ProtocolType> protocolTypes = ProtocolType.listByDirectionType(directionType); |
| | | for (ProtocolType value : protocolTypes) { |
| | | messageTypes.add(new SelectOption(value.name(), value.getDirection().getText() + "-" + value.getDes() + value.name())); |
| | | } |
| | | return R.ok(messageTypes); |
| | | } |
| | | } |
| | |
| | | |
| | | import com.zy.acs.common.domain.mq.DeviceMessage; |
| | | import com.zy.acs.hex.constant.RabbitConstant; |
| | | import com.zy.acs.hex.domain.DeviceLog; |
| | | import com.zy.component.influxdb.service.InfluxDBService; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.amqp.rabbit.core.RabbitTemplate; |
| | |
| | | @GetMapping(value = "/query2") |
| | | @ResponseBody |
| | | public Object queryTest2() { |
| | | return influxDBService.queryPoints("select * from device order by time desc limit 10", DeviceMessage.class); |
| | | return influxDBService.queryPoints("select * from device order by time desc limit 10", DeviceLog.class); |
| | | } |
| | | |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.domain; |
| | | |
| | | import com.zy.component.influxdb.domain.BaseMessage; |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class DeviceLog extends BaseMessage implements Serializable { |
| | | |
| | | private String deviceId; |
| | | |
| | | private String event; |
| | | |
| | | private String type; |
| | | |
| | | private String sourceHexStr; |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.domain; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | import java.util.HashMap; |
| | | import java.util.Map; |
| | | |
| | | @Data |
| | | public class GenericQuery implements Serializable { |
| | | |
| | | private Map<String, Object> conditions = new HashMap<>(); // 动态查询条件 |
| | | private int limit = 100; // 默认返回100条 |
| | | private String orderBy = "time"; // 默认按时间排序 |
| | | private String orderDirection = "DESC"; // 默认降序 |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.domain; |
| | | |
| | | import lombok.Data; |
| | | |
| | | import java.io.Serializable; |
| | | |
| | | @Data |
| | | public class SelectOption implements Serializable { |
| | | private String value; |
| | | private String label; |
| | | |
| | | public SelectOption() { |
| | | } |
| | | |
| | | public SelectOption(String value, String label) { |
| | | this.value = value; |
| | | this.label = label; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.enums; |
| | | |
| | | public enum DirectionType { |
| | | |
| | | DOWN("下行"), |
| | | UP("上行"), |
| | | ; |
| | | |
| | | private String text; |
| | | |
| | | DirectionType(String text) { |
| | | this.text = text; |
| | | } |
| | | |
| | | public String getText() { |
| | | return text; |
| | | } |
| | | |
| | | public void setText(String text) { |
| | | this.text = text; |
| | | } |
| | | } |
| New file |
| | |
| | | package com.zy.acs.hex.enums; |
| | | |
| | | import java.util.ArrayList; |
| | | import java.util.List; |
| | | |
| | | /** |
| | | * 标识枚举 |
| | | * Created by vincent on 2019-04-02 |
| | | */ |
| | | public enum ProtocolType { |
| | | |
| | | // 下行 ------------------------------------------------------------------- |
| | | |
| | | PATH_COMMAND(0x01, "路径数据包", DirectionType.DOWN), |
| | | |
| | | PICK_PLACE_ACK(0x06, "取放货应答包", DirectionType.DOWN), |
| | | |
| | | ACTION_COMMAND(0x02, "动作命令包", DirectionType.DOWN), |
| | | |
| | | HEARTBEAT_COMMAND(0x03, "心跳包", DirectionType.DOWN), |
| | | |
| | | FAULT_CLEAR_COMMAND(0x04, "故障命令包", DirectionType.DOWN), |
| | | |
| | | ACTIVATION_COMMAND(0x80, "激活包", DirectionType.DOWN), |
| | | |
| | | LOGIN_ACK(0xF0, "登录应答包", DirectionType.DOWN), |
| | | |
| | | ACTION_SUCCESS_ACK(0xA1, "动作完成成功应答", DirectionType.DOWN), |
| | | |
| | | ACTION_FAIL_ACK(0xA0, "动作完成失败应答", DirectionType.DOWN), |
| | | |
| | | PATH_ACK_RESPONSE(0xB1, "路径数据包回复ack", DirectionType.DOWN), |
| | | |
| | | // 上行 ------------------------------------------------------------------- |
| | | |
| | | PATH_ACK(0x01, "路径应答包", DirectionType.UP), |
| | | |
| | | PICK_PLACE_REQUEST(0x06, "取放货请求包", DirectionType.UP), |
| | | |
| | | COMMAND_ACK(0x02, "命令应答包", DirectionType.UP), |
| | | |
| | | ACTION_COMPLETE(0x11, "动作完成包", DirectionType.UP), |
| | | |
| | | DATA_CODE_REPORT(0x12, "有码实时数据包", DirectionType.UP), |
| | | |
| | | DATA_WITHOUT_CODE_REPORT(0x13, "无码实时数据包", DirectionType.UP), |
| | | |
| | | HEARTBEAT_REPORT(0x03, "心跳包", DirectionType.UP), |
| | | |
| | | FAULT_REPORT(0x04, "故障数据包", DirectionType.UP), |
| | | |
| | | HANDLE_FALUT_ACK(0x14, "故障清除应答包", DirectionType.UP), |
| | | |
| | | SILO_REPORT(0x70, "料仓信息包", DirectionType.UP), |
| | | |
| | | LOGIN_REPORT(0xF0, "机器人登陆数据包", DirectionType.UP), |
| | | |
| | | ; |
| | | |
| | | private final int code; // 编码 |
| | | private final String des; // 描述 |
| | | private final DirectionType direction; |
| | | |
| | | ProtocolType(int code, String des, DirectionType direction) { |
| | | this.code = code; |
| | | this.des = des; |
| | | this.direction = direction; |
| | | } |
| | | |
| | | public int getCode() { |
| | | return code; |
| | | } |
| | | |
| | | public String getDes() { |
| | | return des; |
| | | } |
| | | |
| | | public DirectionType getDirection() { |
| | | return direction; |
| | | } |
| | | |
| | | public static List<ProtocolType> listByDirectionType(DirectionType direction) { |
| | | List<ProtocolType> protocolTypes = new ArrayList<ProtocolType>(); |
| | | for (ProtocolType type : ProtocolType.values()) { |
| | | if (type.getDirection() == direction) { |
| | | protocolTypes.add(type); |
| | | } |
| | | } |
| | | return protocolTypes; |
| | | } |
| | | |
| | | public static ProtocolType getByCode(int code, DirectionType direction) { |
| | | for (ProtocolType type : ProtocolType.values()) { |
| | | if (type.getCode() == code && type.getDirection() == direction) { |
| | | return type; |
| | | } |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | } |
| | |
| | | parames.put("retention-period", retentionPeriod); |
| | | HttpGo.HttpResponse postResponse = this.http.postJson(createDatabaseUrl, headers, JSON.toJSONString(parames)); |
| | | log.info("是否创建数据库:{}", postResponse); |
| | | }else { |
| | | } else { |
| | | log.info("数据库:{}", response.body()); |
| | | } |
| | | } catch (IOException e) { |
| | |
| | | |
| | | /** |
| | | * Minimal OkHttp wrapper: GET / POST only. |
| | | * |
| | | * <p> |
| | | * - fluent API (get / postJson / postForm) |
| | | * - default singleton instance (thread-safe) |
| | | * - per-request headers + default headers |
| | |
| | | : Collections.unmodifiableMap(new LinkedHashMap<>(defaultHeaders)); |
| | | } |
| | | |
| | | /** Shared default instance (safe SSL by default). */ |
| | | /** |
| | | * Shared default instance (safe SSL by default). |
| | | */ |
| | | public static HttpGo defaults() { |
| | | return Holder.DEFAULT; |
| | | } |
| | |
| | | |
| | | // ===================== POST ===================== |
| | | |
| | | /** POST JSON string payload (null/blank -> "{}"). */ |
| | | /** |
| | | * POST JSON string payload (null/blank -> "{}"). |
| | | */ |
| | | public HttpResponse postJson(String url, String json) throws IOException { |
| | | return postJson(url, null, json); |
| | | } |
| | | |
| | | /** POST JSON string payload (null/blank -> "{}"). */ |
| | | /** |
| | | * POST JSON string payload (null/blank -> "{}"). |
| | | */ |
| | | public HttpResponse postJson(String url, Map<String, String> headers, String json) throws IOException { |
| | | String payload = (json == null || json.trim().isEmpty()) ? "{}" : json; |
| | | RequestBody body = RequestBody.create(payload, JSON); |
| | |
| | | return execute(rb.build()); |
| | | } |
| | | |
| | | /** POST x-www-form-urlencoded fields. */ |
| | | /** |
| | | * POST x-www-form-urlencoded fields. |
| | | */ |
| | | public HttpResponse postForm(String url, Map<String, String> formFields) throws IOException { |
| | | return postForm(url, null, formFields); |
| | | } |
| | | |
| | | /** POST x-www-form-urlencoded fields. */ |
| | | /** |
| | | * POST x-www-form-urlencoded fields. |
| | | */ |
| | | public HttpResponse postForm(String url, Map<String, String> headers, Map<String, String> formFields) throws IOException { |
| | | FormBody.Builder fb = new FormBody.Builder(DEFAULT_CHARSET); |
| | | if (formFields != null) { |
| | |
| | | this.tookMs = tookMs; |
| | | } |
| | | |
| | | public int statusCode() { return statusCode; } |
| | | public Headers headers() { return headers; } |
| | | public String body() { return body; } |
| | | public long tookMs() { return tookMs; } |
| | | public int statusCode() { |
| | | return statusCode; |
| | | } |
| | | |
| | | public Headers headers() { |
| | | return headers; |
| | | } |
| | | |
| | | public String body() { |
| | | return body; |
| | | } |
| | | |
| | | public long tookMs() { |
| | | return tookMs; |
| | | } |
| | | |
| | | public boolean is2xx() { |
| | | return statusCode >= 200 && statusCode < 300; |
| | |
| | | return this; |
| | | } |
| | | |
| | | /** Trust ALL certificates. ONLY for internal testing/self-signed endpoints. */ |
| | | /** |
| | | * Trust ALL certificates. ONLY for internal testing/self-signed endpoints. |
| | | */ |
| | | public Builder trustAllSsl(boolean enable) { |
| | | this.trustAllSsl = enable; |
| | | return this; |
| | |
| | | TrustAll() { |
| | | try { |
| | | this.trustManager = new X509TrustManager() { |
| | | @Override public void checkClientTrusted(X509Certificate[] chain, String authType) { } |
| | | @Override public void checkServerTrusted(X509Certificate[] chain, String authType) { } |
| | | @Override public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } |
| | | @Override |
| | | public void checkClientTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public void checkServerTrusted(X509Certificate[] chain, String authType) { |
| | | } |
| | | |
| | | @Override |
| | | public X509Certificate[] getAcceptedIssuers() { |
| | | return new X509Certificate[0]; |
| | | } |
| | | }; |
| | | |
| | | SSLContext sslContext = SSLContext.getInstance("TLS"); |
| | |
| | | String[] parts = routingKey.split("\\."); |
| | | if (parts.length == 4) { |
| | | Map<String, String> data = new HashMap<>(); |
| | | data.put(InfluxDBConstant.DEVICE_MEASUREMENT_TAG_TYPE, parts[1]); |
| | | data.put(InfluxDBConstant.DEVICE_MEASUREMENT_TAG_TYPE, parts[1].toUpperCase()); |
| | | data.put(InfluxDBConstant.DEVICE_MEASUREMENT_TAG_DEVICEID, parts[2]); |
| | | data.put(InfluxDBConstant.DEVICE_MEASUREMENT_TAG_EVENT, parts[3]); |
| | | return data; |
| | |
| | | #token: apiv3_116RKycNhxbf62Nys4zthC05aRD-aidzhEpEpLtsFuedhJTaCtVklNrzHs9LHxBWMuzDclBHVgToGoQuWGiIIA |
| | | retention-period: 30d |
| | | createDatabaseUrl: ${influxdb3.url}/api/v3/configure/database |
| | | |
| | | # 登录配置 |
| | | login: |
| | | username: admin |
| | | password: admin123 |
| | |
| | | padding: 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | body { |
| | | font-family: Arial, sans-serif; |
| | | background-color: #f0f2f5; |
| | | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | | background-color: #f5f7fa; |
| | | color: #333; |
| | | } |
| | | |
| | | .header { |
| | | background-color: #1890ff; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: white; |
| | | padding: 15px 20px; |
| | | padding: 15px 30px; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
| | | } |
| | | |
| | | .header h1 { |
| | | font-size: 22px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .header button { |
| | | background-color: rgba(255, 255, 255, 0.2); |
| | | color: white; |
| | | border: 1px solid rgba(255, 255, 255, 0.3); |
| | | padding: 8px 20px; |
| | | border-radius: 20px; |
| | | cursor: pointer; |
| | | font-size: 14px; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .header button:hover { |
| | | background-color: rgba(255, 255, 255, 0.3); |
| | | } |
| | | |
| | | .container { |
| | | padding: 30px; |
| | | } |
| | | |
| | | .filter-section { |
| | | background-color: white; |
| | | padding: 20px; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| | | margin-bottom: 20px; |
| | | display: grid; |
| | | grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); |
| | | gap: 15px; |
| | | animation: fadeIn 0.5s ease-in-out; |
| | | } |
| | | |
| | | @keyframes fadeIn { |
| | | from { |
| | | opacity: 0; |
| | | transform: translateY(20px); |
| | | } |
| | | to { |
| | | opacity: 1; |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | .filter-group { |
| | | display: flex; |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .filter-group label { |
| | | margin-bottom: 8px; |
| | | font-weight: 500; |
| | | color: #666; |
| | | font-size: 14px; |
| | | } |
| | | |
| | | .filter-group select, |
| | | .filter-group input { |
| | | padding: 10px 12px; |
| | | border: 2px solid #e0e0e0; |
| | | border-radius: 8px; |
| | | font-size: 14px; |
| | | transition: border-color 0.3s ease; |
| | | } |
| | | |
| | | .filter-group select:focus, |
| | | .filter-group input:focus { |
| | | outline: none; |
| | | border-color: #667eea; |
| | | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
| | | } |
| | | |
| | | .filter-actions { |
| | | grid-column: 1 / -1; |
| | | display: flex; |
| | | gap: 10px; |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .btn { |
| | | padding: 10px 20px; |
| | | border: none; |
| | | border-radius: 8px; |
| | | font-size: 14px; |
| | | font-weight: 500; |
| | | cursor: pointer; |
| | | transition: all 0.3s ease; |
| | | } |
| | | |
| | | .btn-primary { |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: white; |
| | | } |
| | | |
| | | .btn-primary:hover { |
| | | transform: translateY(-2px); |
| | | box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); |
| | | } |
| | | |
| | | .btn-secondary { |
| | | background-color: #f0f0f0; |
| | | color: #333; |
| | | } |
| | | |
| | | .btn-secondary:hover { |
| | | background-color: #e0e0e0; |
| | | } |
| | | |
| | | .log-section { |
| | | background-color: white; |
| | | border-radius: 12px; |
| | | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); |
| | | overflow: hidden; |
| | | } |
| | | |
| | | .log-header { |
| | | padding: 20px; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | .header h1 { |
| | | font-size: 20px; |
| | | |
| | | .log-header h2 { |
| | | font-size: 18px; |
| | | font-weight: 600; |
| | | color: #333; |
| | | } |
| | | .header button { |
| | | background-color: transparent; |
| | | color: white; |
| | | border: 1px solid white; |
| | | padding: 5px 15px; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | |
| | | .log-content { |
| | | padding: 0; |
| | | } |
| | | .header button:hover { |
| | | background-color: rgba(255,255,255,0.1); |
| | | } |
| | | .container { |
| | | padding: 20px; |
| | | } |
| | | .refresh-btn { |
| | | background-color: #1890ff; |
| | | color: white; |
| | | border: none; |
| | | padding: 8px 16px; |
| | | border-radius: 4px; |
| | | cursor: pointer; |
| | | margin-bottom: 20px; |
| | | } |
| | | .refresh-btn:hover { |
| | | background-color: #40a9ff; |
| | | } |
| | | |
| | | table { |
| | | width: 100%; |
| | | border-collapse: collapse; |
| | | background-color: white; |
| | | box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| | | } |
| | | |
| | | th, td { |
| | | padding: 12px; |
| | | padding: 15px 20px; |
| | | text-align: left; |
| | | border-bottom: 1px solid #ddd; |
| | | border-bottom: 1px solid #f0f0f0; |
| | | } |
| | | |
| | | th { |
| | | background-color: #f5f5f5; |
| | | font-weight: bold; |
| | | background-color: #f9f9f9; |
| | | font-weight: 600; |
| | | color: #666; |
| | | font-size: 14px; |
| | | text-transform: uppercase; |
| | | letter-spacing: 0.5px; |
| | | } |
| | | |
| | | tr:hover { |
| | | background-color: #f5f5f5; |
| | | background-color: #f9f9f9; |
| | | } |
| | | |
| | | td { |
| | | font-size: 14px; |
| | | color: #555; |
| | | } |
| | | |
| | | .loading { |
| | | text-align: center; |
| | | padding: 20px; |
| | | color: #666; |
| | | padding: 60px; |
| | | color: #999; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .error { |
| | | text-align: center; |
| | | padding: 20px; |
| | | color: red; |
| | | padding: 60px; |
| | | color: #ff4d4f; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .empty { |
| | | text-align: center; |
| | | padding: 60px; |
| | | color: #999; |
| | | font-size: 16px; |
| | | } |
| | | |
| | | .status-badge { |
| | | display: inline-block; |
| | | padding: 4px 12px; |
| | | border-radius: 12px; |
| | | font-size: 12px; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | .status-up { |
| | | background-color: #e6f7ee; |
| | | color: #52c41a; |
| | | } |
| | | |
| | | .status-down { |
| | | background-color: #fff2e8; |
| | | color: #fa8c16; |
| | | } |
| | | |
| | | @media (max-width: 768px) { |
| | | .container { |
| | | padding: 15px; |
| | | } |
| | | |
| | | .filter-section { |
| | | grid-template-columns: 1fr; |
| | | } |
| | | |
| | | .filter-actions { |
| | | flex-direction: column; |
| | | } |
| | | |
| | | .btn { |
| | | width: 100%; |
| | | } |
| | | |
| | | table { |
| | | display: block; |
| | | overflow-x: auto; |
| | | } |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div class="header"> |
| | | <h1>机器人上下行日志</h1> |
| | | <button id="logoutBtn">登出</button> |
| | | <div class="header"> |
| | | <h1>机器人上下行日志</h1> |
| | | <button id="logoutBtn">登出</button> |
| | | </div> |
| | | <div class="container"> |
| | | <div class="filter-section"> |
| | | <div class="filter-group"> |
| | | <label for="deviceId">小车编号</label> |
| | | <input type="text" id="deviceId" placeholder="请输入小车编号"> |
| | | </div> |
| | | <div class="filter-group"> |
| | | <label for="messageType">消息类型</label> |
| | | <select id="messageType"> |
| | | <option value="">全部</option> |
| | | <!-- 从后台接口获取 --> |
| | | </select> |
| | | </div> |
| | | <div class="filter-group"> |
| | | <label for="tag">标签</label> |
| | | <select id="tag"> |
| | | <option value="">全部</option> |
| | | <!-- 从后台接口获取 --> |
| | | </select> |
| | | </div> |
| | | <div class="filter-group"> |
| | | <label for="startTime">开始时间</label> |
| | | <input type="datetime-local" id="startTime" step="1"> |
| | | </div> |
| | | <div class="filter-group"> |
| | | <label for="endTime">结束时间</label> |
| | | <input type="datetime-local" id="endTime" step="1"> |
| | | </div> |
| | | <div class="filter-actions"> |
| | | <button class="btn btn-primary" id="searchBtn">查询</button> |
| | | <button class="btn btn-secondary" id="resetBtn">重置</button> |
| | | <button class="btn btn-secondary" id="refreshBtn">刷新</button> |
| | | </div> |
| | | </div> |
| | | <div class="container"> |
| | | <button class="refresh-btn" id="refreshBtn">刷新数据</button> |
| | | <div id="loading" class="loading">加载中...</div> |
| | | <div id="error" class="error" style="display: none;"></div> |
| | | <table id="logTable" style="display: none;"> |
| | | <thead> |
| | | <tr> |
| | | <th>时间</th> |
| | | <th>设备ID</th> |
| | | <th>消息类型</th> |
| | | <th>消息内容</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody id="logTableBody"> |
| | | </tbody> |
| | | </table> |
| | | |
| | | <div class="log-section"> |
| | | <div class="log-header"> |
| | | <h2>日志记录</h2> |
| | | <span id="recordCount">共 0 条记录</span> |
| | | </div> |
| | | <div class="log-content"> |
| | | <div id="loading" class="loading">加载中...</div> |
| | | <div id="error" class="error" style="display: none;"></div> |
| | | <div id="empty" class="empty" style="display: none;">暂无符合条件的日志记录</div> |
| | | <table id="logTable" style="display: none;"> |
| | | <thead> |
| | | <tr> |
| | | <th>时间</th> |
| | | <th>设备ID</th> |
| | | <th>消息类型</th> |
| | | <th>标签</th> |
| | | <th>消息内容</th> |
| | | <th>操作</th> |
| | | </tr> |
| | | </thead> |
| | | <tbody id="logTableBody"> |
| | | </tbody> |
| | | </table> |
| | | </div> |
| | | </div> |
| | | <script> |
| | | // 检查登录状态 |
| | | function checkLogin() { |
| | | if (!localStorage.getItem('loggedIn')) { |
| | | window.location.href = '/login'; |
| | | } |
| | | } |
| | | |
| | | // 登出功能 |
| | | document.getElementById('logoutBtn').addEventListener('click', function() { |
| | | localStorage.removeItem('loggedIn'); |
| | | </div> |
| | | <script> |
| | | // 检查登录状态 |
| | | function checkLogin() { |
| | | if (!localStorage.getItem('loggedIn')) { |
| | | window.location.href = '/login'; |
| | | }); |
| | | |
| | | // 加载日志数据 |
| | | function loadLogData() { |
| | | document.getElementById('loading').style.display = 'block'; |
| | | document.getElementById('error').style.display = 'none'; |
| | | document.getElementById('logTable').style.display = 'none'; |
| | | |
| | | fetch('/deviceLog/query') |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | document.getElementById('loading').style.display = 'none'; |
| | | if (data && data.length > 0) { |
| | | } |
| | | } |
| | | |
| | | // 登出功能 |
| | | document.getElementById('logoutBtn').addEventListener('click', function () { |
| | | localStorage.removeItem('loggedIn'); |
| | | window.location.href = '/login'; |
| | | }); |
| | | |
| | | // 重置筛选条件 |
| | | document.getElementById('resetBtn').addEventListener('click', function () { |
| | | document.getElementById('deviceId').value = ''; |
| | | document.getElementById('messageType').value = ''; |
| | | document.getElementById('tag').value = ''; |
| | | document.getElementById('startTime').value = ''; |
| | | document.getElementById('endTime').value = ''; |
| | | loadLogData(); |
| | | }); |
| | | |
| | | // 查询按钮点击事件 |
| | | document.getElementById('searchBtn').addEventListener('click', loadLogData); |
| | | |
| | | // 刷新按钮点击事件 |
| | | document.getElementById('refreshBtn').addEventListener('click', loadLogData); |
| | | |
| | | // 加载日志数据 |
| | | function loadLogData() { |
| | | document.getElementById('loading').style.display = 'block'; |
| | | document.getElementById('error').style.display = 'none'; |
| | | document.getElementById('empty').style.display = 'none'; |
| | | document.getElementById('logTable').style.display = 'none'; |
| | | |
| | | // 获取筛选条件 |
| | | const deviceId = document.getElementById('deviceId').value; |
| | | const messageType = document.getElementById('messageType').value; |
| | | const tag = document.getElementById('tag').value; |
| | | const startTime = document.getElementById('startTime').value; |
| | | const endTime = document.getElementById('endTime').value; |
| | | |
| | | // 构建查询参数 |
| | | const params = new URLSearchParams(); |
| | | if (deviceId) params.append('deviceId', deviceId); |
| | | if (messageType) params.append('type', messageType); |
| | | if (tag) params.append('event', tag); |
| | | if (startTime) { |
| | | // 使用原始的日期格式 |
| | | params.append('startTime', startTime+"Z"); |
| | | } |
| | | if (endTime) { |
| | | // 使用原始的日期格式 |
| | | params.append('endTime', endTime+"Z"); |
| | | } |
| | | |
| | | // 调用查询接口 |
| | | fetch(`/deviceLog/query?${params.toString()}`) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | document.getElementById('loading').style.display = 'none'; |
| | | |
| | | // 检查接口返回格式 |
| | | if (data && data.code === 200 && data.data) { |
| | | // 应用筛选条件(如果后台没有处理筛选) |
| | | let filteredData = data.data; |
| | | if (deviceId) { |
| | | // 模糊匹配设备ID |
| | | filteredData = filteredData.filter(item => { |
| | | return item.deviceId && item.deviceId.includes(deviceId); |
| | | }); |
| | | } |
| | | if (messageType) { |
| | | // 直接使用messageType进行匹配 |
| | | filteredData = filteredData.filter(item => { |
| | | return item.type && item.type === messageType; |
| | | }); |
| | | } |
| | | if (tag) { |
| | | // 使用event字段作为标签进行筛选 |
| | | filteredData = filteredData.filter(item => { |
| | | return item.event && item.event === tag; |
| | | }); |
| | | } |
| | | if (startTime) { |
| | | const start = new Date(startTime).getTime(); |
| | | filteredData = filteredData.filter(item => { |
| | | const itemTime = new Date(item.timestamp).getTime(); |
| | | return itemTime >= start; |
| | | }); |
| | | } |
| | | if (endTime) { |
| | | const end = new Date(endTime).getTime(); |
| | | filteredData = filteredData.filter(item => { |
| | | const itemTime = new Date(item.timestamp).getTime(); |
| | | return itemTime <= end; |
| | | }); |
| | | } |
| | | |
| | | // 更新记录数 |
| | | document.getElementById('recordCount').textContent = `共 ${filteredData.length} 条记录`; |
| | | |
| | | if (filteredData && filteredData.length > 0) { |
| | | document.getElementById('logTable').style.display = 'table'; |
| | | const tbody = document.getElementById('logTableBody'); |
| | | tbody.innerHTML = ''; |
| | | |
| | | data.forEach(item => { |
| | | const row = document.createElement('tr'); |
| | | row.innerHTML = ` |
| | | <td>${item.time || '-'}</td> |
| | | <td>${item.deviceId || '-'}</td> |
| | | <td>${item.messageType || '-'}</td> |
| | | <td>${item.messageContent || '-'}</td> |
| | | `; |
| | | tbody.appendChild(row); |
| | | }); |
| | | |
| | | filteredData.forEach(item => { |
| | | const row = document.createElement('tr'); |
| | | const statusClass = item.type === 'up' ? 'status-up' : 'status-down'; |
| | | const statusText = item.type === 'up' ? '上行' : '下行'; |
| | | const sourceHexStr = item.sourceHexStr || ''; |
| | | |
| | | // 格式化时间戳 |
| | | let formattedTime = '-'; |
| | | if (item.timestamp) { |
| | | try { |
| | | const date = new Date(item.timestamp/1000/1000); |
| | | formattedTime = date.toLocaleString('zh-CN', { |
| | | year: 'numeric', |
| | | month: '2-digit', |
| | | day: '2-digit', |
| | | hour: '2-digit', |
| | | minute: '2-digit', |
| | | second: '2-digit' |
| | | }); |
| | | } catch (e) { |
| | | formattedTime = item.timestamp; |
| | | } |
| | | } |
| | | |
| | | row.innerHTML = ` |
| | | <td>${formattedTime}</td> |
| | | <td>${item.deviceId || '-'}</td> |
| | | <td><span class="status-badge ${statusClass}">${statusText}</span></td> |
| | | <td>${item.event || '-'}</td> |
| | | <td>${sourceHexStr}</td> |
| | | <td> |
| | | <button class="btn btn-secondary parse-btn" data-hex="${sourceHexStr}">解析</button> |
| | | </td> |
| | | `; |
| | | tbody.appendChild(row); |
| | | }); |
| | | |
| | | // 为解析按钮添加点击事件 |
| | | document.querySelectorAll('.parse-btn').forEach(btn => { |
| | | btn.addEventListener('click', function() { |
| | | const hexData = this.getAttribute('data-hex'); |
| | | console.log('解析按钮点击,hexData:', hexData); |
| | | if (hexData) { |
| | | parseHexData(hexData); |
| | | } else { |
| | | alert('没有可解析的消息内容'); |
| | | } |
| | | }); |
| | | }); |
| | | } else { |
| | | document.getElementById('error').textContent = '暂无日志数据'; |
| | | document.getElementById('error').style.display = 'block'; |
| | | document.getElementById('empty').style.display = 'block'; |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | document.getElementById('loading').style.display = 'none'; |
| | | document.getElementById('error').textContent = '加载数据失败: ' + error.message; |
| | | } else { |
| | | document.getElementById('error').textContent = '加载数据失败: ' + (data.message || '未知错误'); |
| | | document.getElementById('error').style.display = 'block'; |
| | | }); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | document.getElementById('loading').style.display = 'none'; |
| | | document.getElementById('error').textContent = '加载数据失败: ' + error.message; |
| | | document.getElementById('error').style.display = 'block'; |
| | | }); |
| | | } |
| | | |
| | | // 从后台接口获取消息类型和标签 |
| | | function loadFilterOptions() { |
| | | // 获取消息类型 |
| | | fetch('/deviceLog/queryType') |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data && data.code === 200 && data.data) { |
| | | const messageTypeSelect = document.getElementById('messageType'); |
| | | messageTypeSelect.innerHTML = '<option value="">全部</option>'; |
| | | data.data.forEach(type => { |
| | | const option = document.createElement('option'); |
| | | option.value = type.label; |
| | | option.textContent = type.value; |
| | | messageTypeSelect.appendChild(option); |
| | | }); |
| | | // 消息类型变化时,重新加载标签 |
| | | messageTypeSelect.addEventListener('change', function () { |
| | | loadTags(this.value); |
| | | }); |
| | | // 初始加载标签 |
| | | loadTags(''); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error('加载消息类型失败:', error); |
| | | }); |
| | | } |
| | | |
| | | // 加载标签 |
| | | function loadTags(directionType) { |
| | | let url = '/deviceLog/queryEvent'; |
| | | if (directionType) { |
| | | url += '?directionType=' + directionType; |
| | | } |
| | | fetch(url) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data && data.code === 200 && data.data) { |
| | | const tagSelect = document.getElementById('tag'); |
| | | tagSelect.innerHTML = '<option value="">全部</option>'; |
| | | data.data.forEach(tag => { |
| | | const option = document.createElement('option'); |
| | | option.value = tag.value; |
| | | option.textContent = tag.label; |
| | | tagSelect.appendChild(option); |
| | | }); |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | console.error('加载标签失败:', error); |
| | | }); |
| | | } |
| | | |
| | | // 解析十六进制数据 |
| | | function parseHexData(hexData) { |
| | | console.log('开始解析,hexData:', hexData); |
| | | |
| | | // 先关闭之前的加载状态和结果弹窗 |
| | | const oldLoading = document.getElementById('parseLoading'); |
| | | if (oldLoading) { |
| | | oldLoading.remove(); |
| | | } |
| | | |
| | | // 刷新按钮点击事件 |
| | | document.getElementById('refreshBtn').addEventListener('click', loadLogData); |
| | | const oldResult = document.querySelector('[id^="resultDiv"]'); |
| | | if (oldResult) { |
| | | oldResult.remove(); |
| | | } |
| | | |
| | | // 显示加载状态 |
| | | const loadingDiv = document.createElement('div'); |
| | | loadingDiv.className = 'loading'; |
| | | loadingDiv.textContent = '解析中...'; |
| | | loadingDiv.style.position = 'fixed'; |
| | | loadingDiv.style.top = '50%'; |
| | | loadingDiv.style.left = '50%'; |
| | | loadingDiv.style.transform = 'translate(-50%, -50%)'; |
| | | loadingDiv.style.backgroundColor = 'rgba(255, 255, 255, 0.9)'; |
| | | loadingDiv.style.padding = '20px'; |
| | | loadingDiv.style.borderRadius = '8px'; |
| | | loadingDiv.style.boxShadow = '0 2px 10px rgba(0,0,0,0.1)'; |
| | | loadingDiv.id = 'parseLoading'; |
| | | document.body.appendChild(loadingDiv); |
| | | |
| | | // 构建请求URL |
| | | const url = `/proxy/decode?hexData=${encodeURIComponent(hexData)}`; |
| | | console.log('请求URL:', url); |
| | | |
| | | // 调用解析接口 |
| | | fetch(url, { |
| | | method: 'GET', |
| | | headers: { |
| | | 'Content-Type': 'application/json', |
| | | 'Accept': 'application/json' |
| | | }, |
| | | mode: 'cors' // 允许跨域请求 |
| | | }) |
| | | .then(response => { |
| | | console.log('响应状态:', response.status); |
| | | console.log('响应头:', response.headers); |
| | | if (!response.ok) { |
| | | throw new Error(`解析失败,状态码: ${response.status}`); |
| | | } |
| | | return response.json(); |
| | | }) |
| | | .then(data => { |
| | | console.log('解析结果:', data); |
| | | // 移除加载状态 |
| | | document.getElementById('parseLoading').remove(); |
| | | |
| | | // 显示解析结果 |
| | | const resultDiv = document.createElement('div'); |
| | | resultDiv.id = 'resultDiv_' + Date.now(); // 添加唯一ID |
| | | resultDiv.style.position = 'fixed'; |
| | | resultDiv.style.top = '50%'; |
| | | resultDiv.style.left = '50%'; |
| | | resultDiv.style.transform = 'translate(-50%, -50%)'; |
| | | resultDiv.style.backgroundColor = 'white'; |
| | | resultDiv.style.padding = '20px'; |
| | | resultDiv.style.borderRadius = '8px'; |
| | | resultDiv.style.boxShadow = '0 2px 20px rgba(0,0,0,0.2)'; |
| | | resultDiv.style.maxWidth = '80%'; |
| | | resultDiv.style.maxHeight = '80%'; |
| | | resultDiv.style.overflow = 'auto'; |
| | | resultDiv.style.zIndex = '1000'; |
| | | |
| | | // 构建结果HTML |
| | | let resultHTML = '<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px;">'; |
| | | resultHTML += '<h3 style="margin: 0;">解析结果</h3>'; |
| | | resultHTML += '<button id="closeResult" style="padding: 5px 10px; background-color: #f0f0f0; color: #333; border: none; border-radius: 4px; cursor: pointer;">×</button>'; |
| | | resultHTML += '</div>'; |
| | | resultHTML += '<pre style="background-color: #f5f5f5; padding: 10px; border-radius: 4px; font-family: monospace; margin: 0;">'; |
| | | resultHTML += JSON.stringify(data, null, 2); |
| | | resultHTML += '</pre>'; |
| | | |
| | | resultDiv.innerHTML = resultHTML; |
| | | document.body.appendChild(resultDiv); |
| | | |
| | | // 关闭按钮点击事件 |
| | | document.getElementById('closeResult').addEventListener('click', function() { |
| | | resultDiv.remove(); |
| | | }); |
| | | }) |
| | | .catch(error => { |
| | | console.error('解析错误:', error); |
| | | // 移除加载状态 |
| | | document.getElementById('parseLoading').remove(); |
| | | |
| | | // 显示详细的错误信息 |
| | | let errorMessage = '解析失败: ' + error.message; |
| | | if (error.message.includes('Failed to fetch')) { |
| | | errorMessage += '\n可能的原因:\n1. 解析服务未启动\n2. 跨域问题\n3. 网络连接问题'; |
| | | } |
| | | alert(errorMessage); |
| | | }); |
| | | } |
| | | |
| | | // 页面加载时检查登录状态并加载数据 |
| | | checkLogin(); |
| | | loadFilterOptions(); |
| | | loadLogData(); |
| | | </script> |
| | | </script> |
| | | </body> |
| | | </html> |
| | |
| | | padding: 0; |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | body { |
| | | font-family: Arial, sans-serif; |
| | | background-color: #f0f2f5; |
| | | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | display: flex; |
| | | justify-content: center; |
| | | align-items: center; |
| | | height: 100vh; |
| | | } |
| | | |
| | | .login-container { |
| | | background-color: white; |
| | | padding: 40px; |
| | | border-radius: 8px; |
| | | box-shadow: 0 2px 10px rgba(0,0,0,0.1); |
| | | border-radius: 12px; |
| | | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1); |
| | | width: 100%; |
| | | max-width: 400px; |
| | | animation: fadeIn 0.5s ease-in-out; |
| | | } |
| | | |
| | | @keyframes fadeIn { |
| | | from { |
| | | opacity: 0; |
| | | transform: translateY(20px); |
| | | } |
| | | to { |
| | | opacity: 1; |
| | | transform: translateY(0); |
| | | } |
| | | } |
| | | |
| | | h2 { |
| | | text-align: center; |
| | | margin-bottom: 30px; |
| | | color: #333; |
| | | font-size: 24px; |
| | | font-weight: 600; |
| | | } |
| | | |
| | | .form-group { |
| | | margin-bottom: 20px; |
| | | } |
| | | |
| | | label { |
| | | display: block; |
| | | margin-bottom: 8px; |
| | | color: #666; |
| | | font-weight: 500; |
| | | } |
| | | |
| | | input { |
| | | width: 100%; |
| | | padding: 10px; |
| | | border: 1px solid #ddd; |
| | | border-radius: 4px; |
| | | padding: 12px 16px; |
| | | border: 2px solid #e0e0e0; |
| | | border-radius: 8px; |
| | | font-size: 16px; |
| | | transition: border-color 0.3s ease; |
| | | } |
| | | |
| | | input:focus { |
| | | outline: none; |
| | | border-color: #667eea; |
| | | box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); |
| | | } |
| | | |
| | | button { |
| | | width: 100%; |
| | | padding: 12px; |
| | | background-color: #1890ff; |
| | | padding: 14px; |
| | | background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| | | color: white; |
| | | border: none; |
| | | border-radius: 4px; |
| | | border-radius: 8px; |
| | | font-size: 16px; |
| | | font-weight: 600; |
| | | cursor: pointer; |
| | | margin-top: 10px; |
| | | transition: transform 0.2s ease; |
| | | } |
| | | |
| | | button:hover { |
| | | background-color: #40a9ff; |
| | | transform: translateY(-2px); |
| | | } |
| | | |
| | | button:active { |
| | | transform: translateY(0); |
| | | } |
| | | |
| | | .error-message { |
| | | color: red; |
| | | margin-top: 10px; |
| | | color: #ff4d4f; |
| | | margin-top: 12px; |
| | | text-align: center; |
| | | font-size: 14px; |
| | | } |
| | | </style> |
| | | </head> |
| | | <body> |
| | | <div class="login-container"> |
| | | <h2>机器人日志系统</h2> |
| | | <form id="loginForm"> |
| | | <div class="form-group"> |
| | | <label for="username">用户名</label> |
| | | <input type="text" id="username" name="username" required> |
| | | </div> |
| | | <div class="form-group"> |
| | | <label for="password">密码</label> |
| | | <input type="password" id="password" name="password" required> |
| | | </div> |
| | | <button type="submit">登录</button> |
| | | <div id="errorMessage" class="error-message"></div> |
| | | </form> |
| | | </div> |
| | | <script> |
| | | document.getElementById('loginForm').addEventListener('submit', function(e) { |
| | | e.preventDefault(); |
| | | const username = document.getElementById('username').value; |
| | | const password = document.getElementById('password').value; |
| | | |
| | | // 简单的登录验证(实际项目中应该调用后端API) |
| | | if (username === 'admin' && password === 'admin123') { |
| | | // 存储登录状态 |
| | | localStorage.setItem('loggedIn', 'true'); |
| | | // 跳转到主页面 |
| | | window.location.href = '/'; |
| | | } else { |
| | | document.getElementById('errorMessage').textContent = '用户名或密码错误'; |
| | | } |
| | | }); |
| | | </script> |
| | | <div class="login-container"> |
| | | <h2>机器人日志系统</h2> |
| | | <form id="loginForm"> |
| | | <div class="form-group"> |
| | | <label for="username">用户名</label> |
| | | <input type="text" id="username" name="username" required> |
| | | </div> |
| | | <div class="form-group"> |
| | | <label for="password">密码</label> |
| | | <input type="password" id="password" name="password" required> |
| | | </div> |
| | | <button type="submit">登录</button> |
| | | <div id="errorMessage" class="error-message"></div> |
| | | </form> |
| | | </div> |
| | | <script> |
| | | document.getElementById('loginForm').addEventListener('submit', function (e) { |
| | | e.preventDefault(); |
| | | const username = document.getElementById('username').value; |
| | | const password = document.getElementById('password').value; |
| | | const errorMessage = document.getElementById('errorMessage'); |
| | | |
| | | // 显示加载状态 |
| | | errorMessage.textContent = '登录中...'; |
| | | |
| | | // 调用后端登录接口 |
| | | fetch('/login/auth', { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/x-www-form-urlencoded' |
| | | }, |
| | | body: `user=${encodeURIComponent(username)}&pass=${encodeURIComponent(password)}` |
| | | }) |
| | | .then(response => response.json()) |
| | | .then(data => { |
| | | if (data && data.code === 200) { |
| | | // 登录成功,存储登录状态 |
| | | localStorage.setItem('loggedIn', 'true'); |
| | | // 跳转到主页面 |
| | | window.location.href = '/'; |
| | | } else { |
| | | // 登录失败,显示错误信息 |
| | | errorMessage.textContent = data.message || '用户名或密码错误'; |
| | | } |
| | | }) |
| | | .catch(error => { |
| | | // 网络错误,显示错误信息 |
| | | errorMessage.textContent = '登录失败,请稍后重试'; |
| | | console.error('登录失败:', error); |
| | | }); |
| | | }); |
| | | </script> |
| | | </body> |
| | | </html> |