修改管理后台
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
# 2. 应用类型是否为「浏览器端」
|
||||
# 3. Referer白名单配置(开发环境可设置为 *)
|
||||
# 4. API密钥状态是否为「启用」
|
||||
VITE_BAIDU_MAP_API_KEY=your_valid_baidu_map_api_key_here
|
||||
VITE_BAIDU_MAP_API_KEY=SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo
|
||||
|
||||
# API服务地址
|
||||
VITE_API_BASE_URL=http://localhost:5350/api
|
||||
|
||||
65
admin-system/frontend/Dockerfile
Normal file
65
admin-system/frontend/Dockerfile
Normal file
@@ -0,0 +1,65 @@
|
||||
# 宁夏智慧养殖监管平台 - 前端管理系统容器
|
||||
# 多阶段构建,优化镜像大小
|
||||
|
||||
# 阶段1:构建阶段
|
||||
FROM node:18-alpine as builder
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装构建依赖
|
||||
RUN npm ci && npm cache clean --force
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
# 构建应用
|
||||
RUN npm run build
|
||||
|
||||
# 阶段2:生产阶段
|
||||
FROM nginx:alpine
|
||||
|
||||
# 安装基本工具
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
# 创建nginx用户目录
|
||||
RUN mkdir -p /var/cache/nginx/client_temp \
|
||||
&& mkdir -p /var/cache/nginx/proxy_temp \
|
||||
&& mkdir -p /var/cache/nginx/fastcgi_temp \
|
||||
&& mkdir -p /var/cache/nginx/uwsgi_temp \
|
||||
&& mkdir -p /var/cache/nginx/scgi_temp
|
||||
|
||||
# 复制构建产物
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
|
||||
# 复制Nginx配置
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY default.conf /etc/nginx/conf.d/default.conf
|
||||
|
||||
# 创建日志目录
|
||||
RUN mkdir -p /var/log/nginx
|
||||
|
||||
# 设置文件权限
|
||||
RUN chown -R nginx:nginx /usr/share/nginx/html \
|
||||
&& chown -R nginx:nginx /var/cache/nginx \
|
||||
&& chown -R nginx:nginx /var/log/nginx
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 80
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD curl -f http://localhost:80/ || exit 1
|
||||
|
||||
# 启动Nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
# 元数据标签
|
||||
LABEL maintainer="宁夏智慧养殖监管平台 <support@nxxm.com>" \
|
||||
version="2.1.0" \
|
||||
description="宁夏智慧养殖监管平台前端管理系统" \
|
||||
application="nxxm-farming-platform" \
|
||||
tier="frontend"
|
||||
131
admin-system/frontend/ENV_CONFIG.md
Normal file
131
admin-system/frontend/ENV_CONFIG.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# 环境变量配置说明
|
||||
|
||||
## 概述
|
||||
本项目支持通过环境变量配置API地址和其他配置项,实现开发、测试、生产环境的灵活切换。
|
||||
|
||||
## 环境变量列表
|
||||
|
||||
### API配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `VITE_API_BASE_URL` | `/api` | API基础路径(相对路径,通过代理转发) |
|
||||
| `VITE_API_FULL_URL` | `http://localhost:5350/api` | 完整API地址(直接调用) |
|
||||
| `VITE_API_TIMEOUT` | `10000` | 请求超时时间(毫秒) |
|
||||
| `VITE_USE_PROXY` | `true` | 是否使用Vite代理 |
|
||||
|
||||
### 百度地图配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `VITE_BAIDU_MAP_API_KEY` | `SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo` | 百度地图API密钥 |
|
||||
|
||||
### 应用配置
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `VITE_APP_TITLE` | `宁夏智慧养殖监管平台` | 应用标题 |
|
||||
| `VITE_APP_VERSION` | `1.0.0` | 应用版本 |
|
||||
|
||||
## 环境配置文件
|
||||
|
||||
### 开发环境 (.env.development)
|
||||
```bash
|
||||
# API配置
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_FULL_URL=http://localhost:5350/api
|
||||
VITE_API_TIMEOUT=10000
|
||||
VITE_USE_PROXY=true
|
||||
|
||||
# 百度地图API
|
||||
VITE_BAIDU_MAP_API_KEY=your_baidu_map_api_key
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=宁夏智慧养殖监管平台
|
||||
VITE_APP_VERSION=1.0.0
|
||||
```
|
||||
|
||||
### 生产环境 (.env.production)
|
||||
```bash
|
||||
# API配置
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_FULL_URL=https://your-domain.com/api
|
||||
VITE_API_TIMEOUT=15000
|
||||
VITE_USE_PROXY=false
|
||||
|
||||
# 百度地图API
|
||||
VITE_BAIDU_MAP_API_KEY=your_production_baidu_map_api_key
|
||||
|
||||
# 应用配置
|
||||
VITE_APP_TITLE=宁夏智慧养殖监管平台
|
||||
VITE_APP_VERSION=1.0.0
|
||||
```
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 1. 创建环境文件
|
||||
```bash
|
||||
# 复制示例文件
|
||||
cp .env.example .env.development
|
||||
cp .env.example .env.production
|
||||
|
||||
# 编辑配置文件
|
||||
vim .env.development
|
||||
vim .env.production
|
||||
```
|
||||
|
||||
### 2. 在代码中使用
|
||||
```javascript
|
||||
// 在组件中使用环境变量
|
||||
import { API_CONFIG } from '@/config/env.js'
|
||||
|
||||
// 获取API基础URL
|
||||
const apiUrl = API_CONFIG.baseUrl
|
||||
|
||||
// 获取完整API URL
|
||||
const fullApiUrl = API_CONFIG.fullBaseUrl
|
||||
|
||||
// 检查是否为开发环境
|
||||
if (API_CONFIG.isDev) {
|
||||
console.log('开发环境')
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API工具使用
|
||||
```javascript
|
||||
// 使用代理模式(推荐)
|
||||
import { api } from '@/utils/api'
|
||||
const result = await api.get('/farms')
|
||||
|
||||
// 使用直接调用模式
|
||||
import { directApi } from '@/utils/api'
|
||||
const result = await directApi.get('/farms')
|
||||
```
|
||||
|
||||
## 配置优先级
|
||||
|
||||
1. 环境变量文件 (.env.development, .env.production)
|
||||
2. 默认配置 (env.js)
|
||||
3. 硬编码默认值
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **环境变量必须以 `VITE_` 开头**,才能在客户端代码中访问
|
||||
2. **生产环境建议使用HTTPS**,确保安全性
|
||||
3. **API密钥不要提交到版本控制系统**,使用环境变量管理
|
||||
4. **代理模式适用于开发环境**,生产环境建议使用直接调用
|
||||
5. **修改环境变量后需要重启开发服务器**
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 1. 环境变量不生效
|
||||
- 检查变量名是否以 `VITE_` 开头
|
||||
- 确认环境文件位置正确
|
||||
- 重启开发服务器
|
||||
|
||||
### 2. API请求失败
|
||||
- 检查 `VITE_API_FULL_URL` 配置是否正确
|
||||
- 确认后端服务是否启动
|
||||
- 检查网络连接
|
||||
|
||||
### 3. 代理不工作
|
||||
- 检查 `vite.config.js` 中的代理配置
|
||||
- 确认 `VITE_USE_PROXY` 设置为 `true`
|
||||
- 检查后端服务端口是否正确
|
||||
BIN
admin-system/frontend/cows.jpg
Normal file
BIN
admin-system/frontend/cows.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 484 KiB |
114
admin-system/frontend/default.conf
Normal file
114
admin-system/frontend/default.conf
Normal file
@@ -0,0 +1,114 @@
|
||||
# 宁夏智慧养殖监管平台 - 前端服务配置
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
|
||||
# 字符编码
|
||||
charset utf-8;
|
||||
|
||||
# 访问日志
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 静态资源缓存配置
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
add_header Vary Accept-Encoding;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# 处理Vue Router的history模式
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
add_header Pragma "no-cache";
|
||||
add_header Expires "0";
|
||||
}
|
||||
|
||||
# API代理到后端服务
|
||||
location /api/ {
|
||||
# 后端服务地址(在docker-compose中定义)
|
||||
proxy_pass http://backend:5350;
|
||||
|
||||
# 代理头设置
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# 超时设置
|
||||
proxy_connect_timeout 10s;
|
||||
proxy_send_timeout 10s;
|
||||
proxy_read_timeout 10s;
|
||||
|
||||
# 缓冲设置
|
||||
proxy_buffering on;
|
||||
proxy_buffer_size 4k;
|
||||
proxy_buffers 8 4k;
|
||||
|
||||
# WebSocket支持
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# WebSocket专用代理
|
||||
location /socket.io/ {
|
||||
proxy_pass http://backend:5350;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# WebSocket特定超时
|
||||
proxy_read_timeout 86400;
|
||||
}
|
||||
|
||||
# 百度地图API代理(解决跨域问题)
|
||||
location /map-api/ {
|
||||
proxy_pass https://api.map.baidu.com/;
|
||||
proxy_set_header Host api.map.baidu.com;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
# 添加CORS头
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
|
||||
add_header Access-Control-Allow-Headers 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
|
||||
}
|
||||
|
||||
# 健康检查端点
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# 安全配置
|
||||
location ~ /\. {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# 防止访问敏感文件
|
||||
location ~* \.(env|log|sql)$ {
|
||||
deny all;
|
||||
access_log off;
|
||||
log_not_found off;
|
||||
}
|
||||
|
||||
# 错误页面
|
||||
error_page 404 /404.html;
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>🐄</text></svg>" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宁夏智慧养殖监管平台</title>
|
||||
</head>
|
||||
|
||||
66
admin-system/frontend/nginx.conf
Normal file
66
admin-system/frontend/nginx.conf
Normal file
@@ -0,0 +1,66 @@
|
||||
# 宁夏智慧养殖监管平台 - Nginx主配置
|
||||
user nginx;
|
||||
worker_processes auto;
|
||||
|
||||
# 错误日志配置
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
# 事件配置
|
||||
events {
|
||||
worker_connections 1024;
|
||||
use epoll;
|
||||
multi_accept on;
|
||||
}
|
||||
|
||||
# HTTP配置
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
# 日志格式
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
# 基础配置
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
keepalive_timeout 65;
|
||||
types_hash_max_size 2048;
|
||||
server_tokens off;
|
||||
|
||||
# 文件上传大小限制
|
||||
client_max_body_size 50M;
|
||||
client_body_buffer_size 128k;
|
||||
|
||||
# Gzip压缩
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_min_length 1000;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
text/xml
|
||||
text/javascript
|
||||
application/json
|
||||
application/javascript
|
||||
application/xml+rss
|
||||
application/atom+xml
|
||||
image/svg+xml;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
|
||||
|
||||
# 包含站点配置
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
||||
4112
admin-system/frontend/package-lock.json
generated
4112
admin-system/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,70 @@
|
||||
{
|
||||
"name": "nxxmdata-frontend",
|
||||
"version": "1.0.0",
|
||||
"description": "宁夏智慧养殖监管平台前端",
|
||||
"version": "2.2.0",
|
||||
"description": "宁夏智慧养殖监管平台前端管理界面",
|
||||
"author": "NXXM Development Team",
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"vue3",
|
||||
"vite",
|
||||
"ant-design-vue",
|
||||
"echarts",
|
||||
"pinia",
|
||||
"smart-farming",
|
||||
"admin-dashboard"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0",
|
||||
"npm": ">=8.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"serve": "vite preview"
|
||||
"serve": "vite preview",
|
||||
"preview": "vite preview --port 5300",
|
||||
"lint": "eslint . --ext .vue,.js,.ts --fix",
|
||||
"lint:check": "eslint . --ext .vue,.js,.ts",
|
||||
"type-check": "vue-tsc --noEmit",
|
||||
"test": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest --coverage",
|
||||
"clean": "rimraf dist node_modules/.vite",
|
||||
"analyze": "vite-bundle-analyzer dist/stats.html",
|
||||
"deploy": "npm run build && npm run preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"ant-design-vue": "^4.0.0",
|
||||
"axios": "^1.11.0",
|
||||
"echarts": "^5.4.0",
|
||||
"pinia": "^2.0.0",
|
||||
"vue": "^3.4.0",
|
||||
"vue-router": "^4.0.0"
|
||||
"ant-design-vue": "^4.0.6",
|
||||
"axios": "^1.6.2",
|
||||
"echarts": "^5.4.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"moment": "^2.29.4",
|
||||
"pinia": "^2.1.7",
|
||||
"socket.io-client": "^4.7.4",
|
||||
"vue": "^3.4.15",
|
||||
"vue-router": "^4.2.5",
|
||||
"xlsx": "^0.18.5",
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"dayjs": "^1.11.10",
|
||||
"lodash-es": "^4.17.21",
|
||||
"nprogress": "^0.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.0.0",
|
||||
"vite": "^5.0.0"
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"vite": "^5.0.10",
|
||||
"@types/node": "^20.10.5",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/nprogress": "^0.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^6.14.0",
|
||||
"@typescript-eslint/parser": "^6.14.0",
|
||||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"eslint": "^8.55.0",
|
||||
"eslint-plugin-vue": "^9.19.2",
|
||||
"rimraf": "^5.0.5",
|
||||
"typescript": "^5.3.3",
|
||||
"vite-bundle-analyzer": "^0.7.0",
|
||||
"vitest": "^1.0.4",
|
||||
"@vitest/ui": "^1.0.4",
|
||||
"@vitest/coverage-v8": "^1.0.4",
|
||||
"vue-tsc": "^1.8.25"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,373 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>经纬度输入调试</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.debug-container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
}
|
||||
.debug-section {
|
||||
margin-bottom: 30px;
|
||||
padding: 20px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.debug-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 15px;
|
||||
color: #1890ff;
|
||||
}
|
||||
.debug-info {
|
||||
background: #f0f0f0;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
margin-top: 10px;
|
||||
font-family: 'Courier New', monospace;
|
||||
white-space: pre-wrap;
|
||||
border-left: 4px solid #52c41a;
|
||||
font-size: 13px;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.input-item {
|
||||
flex: 1;
|
||||
}
|
||||
.input-item label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.input-item input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.input-item input:focus {
|
||||
border-color: #1890ff;
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
|
||||
}
|
||||
.test-case {
|
||||
margin-bottom: 10px;
|
||||
padding: 12px;
|
||||
background: white;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.test-case-info {
|
||||
flex: 1;
|
||||
}
|
||||
.test-case-name {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.test-case-values {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
button {
|
||||
padding: 6px 12px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
}
|
||||
button:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.log-area {
|
||||
height: 200px;
|
||||
overflow-y: auto;
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.clear-btn {
|
||||
background: #ff4d4f;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.clear-btn:hover {
|
||||
background: #ff7875;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="debug-container">
|
||||
<h1>经纬度输入调试工具</h1>
|
||||
|
||||
<!-- 实时输入测试 -->
|
||||
<div class="debug-section">
|
||||
<div class="debug-title">实时输入测试</div>
|
||||
<div class="input-group">
|
||||
<div class="input-item">
|
||||
<label>经度 (Longitude):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="longitude"
|
||||
placeholder="请输入经度 (-180 到 180)"
|
||||
oninput="handleCoordinateInput('longitude', this.value)"
|
||||
/>
|
||||
</div>
|
||||
<div class="input-item">
|
||||
<label>纬度 (Latitude):</label>
|
||||
<input
|
||||
type="text"
|
||||
id="latitude"
|
||||
placeholder="请输入纬度 (-90 到 90)"
|
||||
oninput="handleCoordinateInput('latitude', this.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="debug-info" id="currentValues">
|
||||
当前值:
|
||||
经度: undefined (类型: undefined)
|
||||
纬度: undefined (类型: undefined)
|
||||
|
||||
提交数据:
|
||||
{}
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 15px;">
|
||||
<button onclick="simulateSubmit()">模拟提交到后端</button>
|
||||
<button onclick="clearCoordinates()" style="margin-left: 10px;">清空坐标</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预设测试用例 -->
|
||||
<div class="debug-section">
|
||||
<div class="debug-title">预设测试用例</div>
|
||||
<div id="testCases"></div>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<div class="debug-section">
|
||||
<div class="debug-title">操作日志</div>
|
||||
<div class="log-area" id="logArea"></div>
|
||||
<button class="clear-btn" onclick="clearLogs()">清空日志</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// 全局状态
|
||||
let formData = {
|
||||
longitude: undefined,
|
||||
latitude: undefined
|
||||
};
|
||||
|
||||
let logs = [];
|
||||
|
||||
const testCases = [
|
||||
{ name: '正常小数', longitude: '106.2400', latitude: '38.4900' },
|
||||
{ name: '整数', longitude: '106', latitude: '38' },
|
||||
{ name: '高精度小数', longitude: '106.234567', latitude: '38.487654' },
|
||||
{ name: '负数', longitude: '-106.2400', latitude: '-38.4900' },
|
||||
{ name: '包含字母', longitude: '106.24abc', latitude: '38.49xyz' },
|
||||
{ name: '多个小数点', longitude: '106.24.56', latitude: '38.49.78' },
|
||||
{ name: '空字符串', longitude: '', latitude: '' },
|
||||
{ name: '只有小数点', longitude: '.', latitude: '.' },
|
||||
{ name: '边界值', longitude: '180.0', latitude: '90.0' },
|
||||
{ name: '超出范围', longitude: '200.0', latitude: '100.0' }
|
||||
];
|
||||
|
||||
// 坐标解析器
|
||||
function parseCoordinate(value) {
|
||||
if (!value) return value;
|
||||
|
||||
// 移除非数字字符,保留小数点和负号
|
||||
const cleaned = value.toString().replace(/[^\d.-]/g, '');
|
||||
|
||||
// 确保只有一个小数点
|
||||
const parts = cleaned.split('.');
|
||||
let result;
|
||||
if (parts.length > 2) {
|
||||
result = parts[0] + '.' + parts.slice(1).join('');
|
||||
} else {
|
||||
result = cleaned;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 处理坐标输入
|
||||
function handleCoordinateInput(type, value) {
|
||||
log(`${type === 'longitude' ? '经度' : '纬度'}输入: "${value}"`);
|
||||
|
||||
const parsed = parseCoordinate(value);
|
||||
log(`${type === 'longitude' ? '经度' : '纬度'}解析后: "${parsed}"`);
|
||||
|
||||
// 转换为数字
|
||||
const numValue = parsed === '' ? undefined : parseFloat(parsed);
|
||||
formData[type] = isNaN(numValue) ? undefined : numValue;
|
||||
|
||||
log(`${type === 'longitude' ? '经度' : '纬度'}最终值: ${formData[type]} (类型: ${typeof formData[type]})`);
|
||||
|
||||
// 更新输入框显示
|
||||
document.getElementById(type).value = parsed;
|
||||
|
||||
// 更新显示
|
||||
updateDisplay();
|
||||
}
|
||||
|
||||
// 更新显示
|
||||
function updateDisplay() {
|
||||
const currentValuesDiv = document.getElementById('currentValues');
|
||||
const submitData = getSubmitData();
|
||||
|
||||
currentValuesDiv.textContent = `当前值:
|
||||
经度: ${formData.longitude} (类型: ${typeof formData.longitude})
|
||||
纬度: ${formData.latitude} (类型: ${typeof formData.latitude})
|
||||
|
||||
提交数据:
|
||||
${JSON.stringify(submitData, null, 2)}`;
|
||||
}
|
||||
|
||||
// 获取提交数据
|
||||
function getSubmitData() {
|
||||
const submitData = {
|
||||
name: '测试农场',
|
||||
type: 'farm',
|
||||
owner: '测试负责人',
|
||||
phone: '13800138000',
|
||||
address: '测试地址',
|
||||
status: 'active'
|
||||
};
|
||||
|
||||
// 只有当经纬度有效时才添加
|
||||
if (formData.longitude !== undefined && formData.longitude !== null) {
|
||||
submitData.longitude = formData.longitude;
|
||||
}
|
||||
if (formData.latitude !== undefined && formData.latitude !== null) {
|
||||
submitData.latitude = formData.latitude;
|
||||
}
|
||||
|
||||
return submitData;
|
||||
}
|
||||
|
||||
// 应用测试用例
|
||||
function applyTestCase(testCase) {
|
||||
log(`\n=== 应用测试用例: ${testCase.name} ===`);
|
||||
log(`设置经度: "${testCase.longitude}"`);
|
||||
log(`设置纬度: "${testCase.latitude}"`);
|
||||
|
||||
// 设置输入框值并触发处理
|
||||
document.getElementById('longitude').value = testCase.longitude;
|
||||
document.getElementById('latitude').value = testCase.latitude;
|
||||
|
||||
handleCoordinateInput('longitude', testCase.longitude);
|
||||
handleCoordinateInput('latitude', testCase.latitude);
|
||||
}
|
||||
|
||||
// 模拟提交
|
||||
async function simulateSubmit() {
|
||||
log(`\n=== 模拟API提交 ===`);
|
||||
|
||||
const submitData = getSubmitData();
|
||||
log(`提交数据: ${JSON.stringify(submitData, null, 2)}`);
|
||||
|
||||
try {
|
||||
// 模拟API调用
|
||||
const response = await fetch('http://localhost:5350/api/farms', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(submitData)
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
log(`API响应成功: ${JSON.stringify(result)}`);
|
||||
alert('提交成功');
|
||||
} catch (error) {
|
||||
log(`API调用失败: ${error.message}`);
|
||||
alert('API调用失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 清空坐标
|
||||
function clearCoordinates() {
|
||||
formData.longitude = undefined;
|
||||
formData.latitude = undefined;
|
||||
document.getElementById('longitude').value = '';
|
||||
document.getElementById('latitude').value = '';
|
||||
updateDisplay();
|
||||
log('已清空坐标');
|
||||
}
|
||||
|
||||
// 日志功能
|
||||
function log(message) {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
logs.push(`[${timestamp}] ${message}`);
|
||||
updateLogDisplay();
|
||||
}
|
||||
|
||||
function updateLogDisplay() {
|
||||
const logArea = document.getElementById('logArea');
|
||||
logArea.textContent = logs.join('\n');
|
||||
logArea.scrollTop = logArea.scrollHeight;
|
||||
}
|
||||
|
||||
function clearLogs() {
|
||||
logs = [];
|
||||
updateLogDisplay();
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// 渲染测试用例
|
||||
const testCasesDiv = document.getElementById('testCases');
|
||||
testCases.forEach(testCase => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'test-case';
|
||||
div.innerHTML = `
|
||||
<div class="test-case-info">
|
||||
<div class="test-case-name">${testCase.name}</div>
|
||||
<div class="test-case-values">经度: ${testCase.longitude}, 纬度: ${testCase.latitude}</div>
|
||||
</div>
|
||||
<button onclick="applyTestCase({name: '${testCase.name}', longitude: '${testCase.longitude}', latitude: '${testCase.latitude}'})">应用</button>
|
||||
`;
|
||||
testCasesDiv.appendChild(div);
|
||||
});
|
||||
|
||||
log('调试工具已加载');
|
||||
log('请使用测试用例或手动输入来测试经纬度输入框');
|
||||
updateDisplay();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,52 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>设备数据调试</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>设备数据调试</h1>
|
||||
<div id="result"></div>
|
||||
|
||||
<script>
|
||||
async function testDeviceAPI() {
|
||||
const resultDiv = document.getElementById('result');
|
||||
|
||||
try {
|
||||
console.log('开始测试设备API...');
|
||||
|
||||
// 测试API调用
|
||||
const response = await fetch('http://localhost:5350/api/devices/public');
|
||||
const data = await response.json();
|
||||
|
||||
console.log('API响应:', data);
|
||||
|
||||
// 显示结果
|
||||
const devices = data.data || [];
|
||||
const statusCount = {};
|
||||
devices.forEach(device => {
|
||||
statusCount[device.status] = (statusCount[device.status] || 0) + 1;
|
||||
});
|
||||
|
||||
resultDiv.innerHTML = `
|
||||
<h2>API测试结果</h2>
|
||||
<p><strong>设备总数:</strong> ${devices.length}</p>
|
||||
<p><strong>在线设备:</strong> ${statusCount.online || 0}</p>
|
||||
<p><strong>离线设备:</strong> ${statusCount.offline || 0}</p>
|
||||
<p><strong>维护设备:</strong> ${statusCount.maintenance || 0}</p>
|
||||
<h3>前3个设备:</h3>
|
||||
<pre>${JSON.stringify(devices.slice(0, 3), null, 2)}</pre>
|
||||
`;
|
||||
|
||||
} catch (error) {
|
||||
console.error('API测试失败:', error);
|
||||
resultDiv.innerHTML = `<p style="color: red;">API测试失败: ${error.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载后执行测试
|
||||
window.addEventListener('load', testDeviceAPI);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,187 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户管理调试页面</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success {
|
||||
background-color: #d4edda;
|
||||
color: #155724;
|
||||
border: 1px solid #c3e6cb;
|
||||
}
|
||||
.error {
|
||||
background-color: #f8d7da;
|
||||
color: #721c24;
|
||||
border: 1px solid #f5c6cb;
|
||||
}
|
||||
.info {
|
||||
background-color: #d1ecf1;
|
||||
color: #0c5460;
|
||||
border: 1px solid #bee5eb;
|
||||
}
|
||||
button {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
pre {
|
||||
background-color: #f8f9fa;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>用户管理调试页面</h1>
|
||||
|
||||
<div id="tokenStatus" class="status info">
|
||||
检查中...
|
||||
</div>
|
||||
|
||||
<div id="apiStatus" class="status info">
|
||||
等待测试...
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button onclick="checkToken()">检查Token</button>
|
||||
<button onclick="testLogin()">测试登录</button>
|
||||
<button onclick="testUsersAPI()">测试用户API</button>
|
||||
<button onclick="clearToken()">清除Token</button>
|
||||
</div>
|
||||
|
||||
<div id="results">
|
||||
<h3>测试结果:</h3>
|
||||
<pre id="output"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = 'http://localhost:5350/api';
|
||||
|
||||
function log(message) {
|
||||
const output = document.getElementById('output');
|
||||
output.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
||||
}
|
||||
|
||||
function checkToken() {
|
||||
const token = localStorage.getItem('token');
|
||||
const tokenStatus = document.getElementById('tokenStatus');
|
||||
|
||||
if (token) {
|
||||
tokenStatus.className = 'status success';
|
||||
tokenStatus.textContent = `Token存在 (长度: ${token.length})`;
|
||||
log('Token存在: ' + token.substring(0, 50) + '...');
|
||||
} else {
|
||||
tokenStatus.className = 'status error';
|
||||
tokenStatus.textContent = 'Token不存在';
|
||||
log('Token不存在');
|
||||
}
|
||||
}
|
||||
|
||||
async function testLogin() {
|
||||
try {
|
||||
log('开始测试登录...');
|
||||
const response = await fetch(`${API_BASE_URL}/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log('登录响应: ' + JSON.stringify(data, null, 2));
|
||||
|
||||
if (data.success && data.token) {
|
||||
localStorage.setItem('token', data.token);
|
||||
log('Token已保存到localStorage');
|
||||
checkToken();
|
||||
} else {
|
||||
log('登录失败: ' + data.message);
|
||||
}
|
||||
} catch (error) {
|
||||
log('登录错误: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
async function testUsersAPI() {
|
||||
try {
|
||||
log('开始测试用户API...');
|
||||
const token = localStorage.getItem('token');
|
||||
|
||||
if (!token) {
|
||||
log('错误: 没有找到token,请先登录');
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/users`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
log('用户API响应: ' + JSON.stringify(data, null, 2));
|
||||
|
||||
const apiStatus = document.getElementById('apiStatus');
|
||||
if (data.success && data.data) {
|
||||
apiStatus.className = 'status success';
|
||||
apiStatus.textContent = `API正常 (获取到${data.data.length}个用户)`;
|
||||
} else {
|
||||
apiStatus.className = 'status error';
|
||||
apiStatus.textContent = 'API异常';
|
||||
}
|
||||
} catch (error) {
|
||||
log('用户API错误: ' + error.message);
|
||||
const apiStatus = document.getElementById('apiStatus');
|
||||
apiStatus.className = 'status error';
|
||||
apiStatus.textContent = 'API调用失败';
|
||||
}
|
||||
}
|
||||
|
||||
function clearToken() {
|
||||
localStorage.removeItem('token');
|
||||
log('Token已清除');
|
||||
checkToken();
|
||||
}
|
||||
|
||||
// 页面加载时自动检查token
|
||||
window.onload = function() {
|
||||
checkToken();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
256
admin-system/frontend/public/electronic-fence-demo.html
Normal file
256
admin-system/frontend/public/electronic-fence-demo.html
Normal file
@@ -0,0 +1,256 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>电子围栏功能演示</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
||||
margin: 0;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
.header h1 {
|
||||
margin: 0;
|
||||
font-size: 28px;
|
||||
}
|
||||
.header p {
|
||||
margin: 10px 0 0 0;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.content {
|
||||
padding: 30px;
|
||||
}
|
||||
.feature-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 20px;
|
||||
margin: 30px 0;
|
||||
}
|
||||
.feature-card {
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
}
|
||||
.feature-card h3 {
|
||||
color: #52c41a;
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 18px;
|
||||
}
|
||||
.feature-card ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
.feature-card li {
|
||||
margin: 8px 0;
|
||||
color: #666;
|
||||
}
|
||||
.demo-section {
|
||||
margin: 30px 0;
|
||||
padding: 20px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.demo-section h3 {
|
||||
color: #52c41a;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
.screenshot {
|
||||
width: 100%;
|
||||
max-width: 800px;
|
||||
height: 400px;
|
||||
background: #f0f0f0;
|
||||
border: 2px dashed #ccc;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
font-size: 16px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.btn {
|
||||
display: inline-block;
|
||||
padding: 10px 20px;
|
||||
background: #52c41a;
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 4px;
|
||||
margin: 10px 10px 10px 0;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
.btn:hover {
|
||||
background: #73d13d;
|
||||
}
|
||||
.btn-secondary {
|
||||
background: #1890ff;
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
.tech-stack {
|
||||
background: #f0f8ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.tech-stack h3 {
|
||||
color: #1890ff;
|
||||
margin: 0 0 15px 0;
|
||||
}
|
||||
.tech-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
.tech-item {
|
||||
background: white;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #d9d9d9;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<h1>电子围栏功能演示</h1>
|
||||
<p>宁夏智慧养殖监管平台 - 智能设备模块</p>
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
<div class="demo-section">
|
||||
<h3>🎯 功能概述</h3>
|
||||
<p>电子围栏功能允许用户在地图上绘制围栏区域,监控动物活动范围,并提供完整的围栏管理功能。支持多种围栏类型,实时统计区域内外的动物数量,为智慧养殖提供精准的地理围栏管理。</p>
|
||||
|
||||
<div class="screenshot">
|
||||
📍 地图界面截图区域<br>
|
||||
<small>显示围栏绘制和选择功能</small>
|
||||
</div>
|
||||
|
||||
<a href="/smart-devices/fence" class="btn">进入电子围栏页面</a>
|
||||
<a href="/api-docs" class="btn btn-secondary">查看API文档</a>
|
||||
</div>
|
||||
|
||||
<div class="feature-grid">
|
||||
<div class="feature-card">
|
||||
<h3>🗺️ 地图绘制功能</h3>
|
||||
<ul>
|
||||
<li>支持多边形围栏绘制</li>
|
||||
<li>实时预览绘制过程</li>
|
||||
<li>自动保存坐标数据</li>
|
||||
<li>支持地图缩放和平移</li>
|
||||
<li>地图类型切换(地图/卫星)</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>📊 围栏管理功能</h3>
|
||||
<ul>
|
||||
<li>围栏列表展示</li>
|
||||
<li>围栏搜索和筛选</li>
|
||||
<li>围栏信息面板</li>
|
||||
<li>围栏类型管理</li>
|
||||
<li>围栏状态监控</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>📈 统计功能</h3>
|
||||
<ul>
|
||||
<li>区域内动物数量统计</li>
|
||||
<li>区域外动物数量统计</li>
|
||||
<li>放牧状态监控</li>
|
||||
<li>围栏使用率分析</li>
|
||||
<li>实时数据更新</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="feature-card">
|
||||
<h3>🔧 围栏类型</h3>
|
||||
<ul>
|
||||
<li>采集器电子围栏</li>
|
||||
<li>放牧围栏</li>
|
||||
<li>安全围栏</li>
|
||||
<li>自定义围栏类型</li>
|
||||
<li>围栏权限控制</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tech-stack">
|
||||
<h3>🛠️ 技术栈</h3>
|
||||
<div class="tech-list">
|
||||
<div class="tech-item">
|
||||
<strong>前端</strong><br>
|
||||
Vue 3 + Vite
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>UI组件</strong><br>
|
||||
Ant Design Vue
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>地图服务</strong><br>
|
||||
百度地图API
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>后端</strong><br>
|
||||
Node.js + Express
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>数据库</strong><br>
|
||||
MySQL + Sequelize
|
||||
</div>
|
||||
<div class="tech-item">
|
||||
<strong>认证</strong><br>
|
||||
JWT Token
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>🚀 快速开始</h3>
|
||||
<ol>
|
||||
<li><strong>登录系统</strong> - 使用管理员账号登录管理后台</li>
|
||||
<li><strong>导航到电子围栏</strong> - 进入"智能设备" → "电子围栏"页面</li>
|
||||
<li><strong>开始绘制</strong> - 点击"开始绘制"按钮,在地图上点击绘制围栏</li>
|
||||
<li><strong>保存围栏</strong> - 完成绘制后填写围栏信息并保存</li>
|
||||
<li><strong>管理围栏</strong> - 使用下拉框选择和管理现有围栏</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="demo-section">
|
||||
<h3>📋 API接口</h3>
|
||||
<p>电子围栏功能提供完整的RESTful API接口:</p>
|
||||
<ul>
|
||||
<li><code>GET /api/electronic-fence</code> - 获取围栏列表</li>
|
||||
<li><code>POST /api/electronic-fence</code> - 创建围栏</li>
|
||||
<li><code>PUT /api/electronic-fence/:id</code> - 更新围栏</li>
|
||||
<li><code>DELETE /api/electronic-fence/:id</code> - 删除围栏</li>
|
||||
<li><code>GET /api/electronic-fence/stats/overview</code> - 获取统计概览</li>
|
||||
</ul>
|
||||
<a href="/api-docs" class="btn btn-secondary">查看完整API文档</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,135 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>自动登录测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 800px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-item {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success {
|
||||
background-color: #f6ffed;
|
||||
border-color: #b7eb8f;
|
||||
}
|
||||
.error {
|
||||
background-color: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
}
|
||||
button {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>自动登录功能测试</h1>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>测试步骤:</h3>
|
||||
<ol>
|
||||
<li>点击"清除登录状态"按钮</li>
|
||||
<li>点击"访问登录页面"按钮</li>
|
||||
<li>观察是否自动登录并跳转到仪表盘</li>
|
||||
<li>点击"访问用户管理页面"测试路由守卫</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>当前状态:</h3>
|
||||
<p>Token: <span id="token-status">检查中...</span></p>
|
||||
<p>用户信息: <span id="user-status">检查中...</span></p>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>测试操作:</h3>
|
||||
<button onclick="clearLoginState()">清除登录状态</button>
|
||||
<button onclick="checkLoginState()">检查登录状态</button>
|
||||
<button onclick="visitLoginPage()">访问登录页面</button>
|
||||
<button onclick="visitUsersPage()">访问用户管理页面</button>
|
||||
<button onclick="visitDashboard()">访问仪表盘</button>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>测试结果:</h3>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addResult(message, isSuccess = true) {
|
||||
const results = document.getElementById('test-results');
|
||||
const div = document.createElement('div');
|
||||
div.className = isSuccess ? 'success' : 'error';
|
||||
div.style.margin = '10px 0';
|
||||
div.style.padding = '10px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
|
||||
results.appendChild(div);
|
||||
}
|
||||
|
||||
function clearLoginState() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('user');
|
||||
addResult('已清除登录状态');
|
||||
checkLoginState();
|
||||
}
|
||||
|
||||
function checkLoginState() {
|
||||
const token = localStorage.getItem('token');
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
document.getElementById('token-status').textContent = token ? '已存在' : '不存在';
|
||||
document.getElementById('user-status').textContent = user ? JSON.parse(user).username : '未登录';
|
||||
|
||||
addResult(`登录状态检查 - Token: ${token ? '存在' : '不存在'}, 用户: ${user ? JSON.parse(user).username : '未登录'}`);
|
||||
}
|
||||
|
||||
function visitLoginPage() {
|
||||
addResult('正在访问登录页面...');
|
||||
window.open('http://localhost:5300/#/login', '_blank');
|
||||
}
|
||||
|
||||
function visitUsersPage() {
|
||||
addResult('正在访问用户管理页面...');
|
||||
window.open('http://localhost:5300/#/users', '_blank');
|
||||
}
|
||||
|
||||
function visitDashboard() {
|
||||
addResult('正在访问仪表盘...');
|
||||
window.open('http://localhost:5300/#/dashboard', '_blank');
|
||||
}
|
||||
|
||||
// 页面加载时检查状态
|
||||
window.onload = function() {
|
||||
checkLoginState();
|
||||
addResult('测试页面已加载,请按照测试步骤进行操作');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
150
admin-system/frontend/public/test-export.html
Normal file
150
admin-system/frontend/public/test-export.html
Normal file
@@ -0,0 +1,150 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>导出功能测试</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
.test-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
button {
|
||||
padding: 10px 20px;
|
||||
margin: 10px;
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover {
|
||||
background: #40a9ff;
|
||||
}
|
||||
.log {
|
||||
background: #f5f5f5;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
border-radius: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="test-container">
|
||||
<h1>智能项圈预警导出功能测试</h1>
|
||||
|
||||
<div>
|
||||
<button onclick="testExportColumns()">测试列配置</button>
|
||||
<button onclick="testExportData()">测试导出数据</button>
|
||||
<button onclick="clearLog()">清除日志</button>
|
||||
</div>
|
||||
|
||||
<div id="log" class="log"></div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
// 模拟ExportUtils类
|
||||
class ExportUtils {
|
||||
static getCollarAlertColumns() {
|
||||
return [
|
||||
{ title: '耳标编号', dataIndex: 'collarNumber', key: 'collarNumber' },
|
||||
{ title: '预警类型', dataIndex: 'alertType', key: 'alertType' },
|
||||
{ title: '预警级别', dataIndex: 'alertLevel', key: 'alertLevel' },
|
||||
{ title: '预警时间', dataIndex: 'alertTime', key: 'alertTime', dataType: 'datetime' },
|
||||
{ title: '设备电量', dataIndex: 'battery', key: 'battery' },
|
||||
{ title: '设备温度', dataIndex: 'temperature', key: 'temperature' },
|
||||
{ title: '当日步数', dataIndex: 'dailySteps', key: 'dailySteps' }
|
||||
]
|
||||
}
|
||||
|
||||
static exportAlertData(data, alertType) {
|
||||
const alertTypeMap = {
|
||||
collar: { name: '智能项圈预警', columns: this.getCollarAlertColumns() },
|
||||
eartag: { name: '智能耳标预警', columns: this.getEartagAlertColumns() }
|
||||
}
|
||||
|
||||
const config = alertTypeMap[alertType]
|
||||
if (!config) {
|
||||
throw new Error(`不支持的预警类型: ${alertType}`)
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
filename: `${config.name}数据_${new Date().toISOString().slice(0,10)}.xlsx`,
|
||||
columns: config.columns,
|
||||
data: data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 测试数据
|
||||
const testData = [
|
||||
{
|
||||
collarNumber: '22012000108',
|
||||
alertType: '低电量预警',
|
||||
alertLevel: '高级',
|
||||
alertTime: '2025-01-18 10:30:00',
|
||||
battery: 98,
|
||||
temperature: 32.0,
|
||||
dailySteps: 66
|
||||
},
|
||||
{
|
||||
collarNumber: '15010000008',
|
||||
alertType: '离线预警',
|
||||
alertLevel: '高级',
|
||||
alertTime: '2025-01-18 09:15:00',
|
||||
battery: 83,
|
||||
temperature: 27.8,
|
||||
dailySteps: 5135
|
||||
}
|
||||
]
|
||||
|
||||
function log(message) {
|
||||
const logDiv = document.getElementById('log');
|
||||
logDiv.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
||||
}
|
||||
|
||||
function testExportColumns() {
|
||||
log('=== 测试列配置 ===');
|
||||
const columns = ExportUtils.getCollarAlertColumns();
|
||||
log('列配置数量: ' + columns.length);
|
||||
columns.forEach((col, index) => {
|
||||
log(`${index + 1}. ${col.title} (${col.dataIndex})`);
|
||||
});
|
||||
}
|
||||
|
||||
function testExportData() {
|
||||
log('=== 测试导出数据 ===');
|
||||
try {
|
||||
const result = ExportUtils.exportAlertData(testData, 'collar');
|
||||
log('导出成功: ' + result.filename);
|
||||
log('列配置: ' + JSON.stringify(result.columns, null, 2));
|
||||
log('数据示例: ' + JSON.stringify(result.data[0], null, 2));
|
||||
} catch (error) {
|
||||
log('导出失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
function clearLog() {
|
||||
document.getElementById('log').textContent = '';
|
||||
}
|
||||
|
||||
// 将函数暴露到全局作用域
|
||||
window.testExportColumns = testExportColumns;
|
||||
window.testExportData = testExportData;
|
||||
window.clearLog = clearLog;
|
||||
|
||||
// 页面加载完成后自动测试
|
||||
window.addEventListener('load', () => {
|
||||
log('页面加载完成,开始自动测试');
|
||||
testExportColumns();
|
||||
testExportData();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,227 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>用户数据显示测试</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
max-width: 1000px;
|
||||
margin: 50px auto;
|
||||
padding: 20px;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.container {
|
||||
background: white;
|
||||
padding: 30px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||
}
|
||||
.test-item {
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.success {
|
||||
background-color: #f6ffed;
|
||||
border-color: #b7eb8f;
|
||||
}
|
||||
.error {
|
||||
background-color: #fff2f0;
|
||||
border-color: #ffccc7;
|
||||
}
|
||||
button {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
margin: 5px;
|
||||
}
|
||||
button:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
th {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
.json-display {
|
||||
background: #f8f8f8;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
white-space: pre-wrap;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>用户数据显示测试</h1>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>测试步骤:</h3>
|
||||
<ol>
|
||||
<li>点击"测试API调用"按钮</li>
|
||||
<li>查看API响应数据</li>
|
||||
<li>点击"访问用户管理页面"查看前端显示</li>
|
||||
<li>对比数据是否一致</li>
|
||||
</ol>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>API测试:</h3>
|
||||
<button onclick="testAPI()">测试API调用</button>
|
||||
<button onclick="visitUsersPage()">访问用户管理页面</button>
|
||||
<button onclick="checkLocalStorage()">检查本地存储</button>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>API响应数据:</h3>
|
||||
<div id="api-response" class="json-display">点击"测试API调用"查看数据...</div>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>用户数据表格:</h3>
|
||||
<div id="users-table">暂无数据</div>
|
||||
</div>
|
||||
|
||||
<div class="test-item">
|
||||
<h3>测试结果:</h3>
|
||||
<div id="test-results"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function addResult(message, isSuccess = true) {
|
||||
const results = document.getElementById('test-results');
|
||||
const div = document.createElement('div');
|
||||
div.className = isSuccess ? 'success' : 'error';
|
||||
div.style.margin = '10px 0';
|
||||
div.style.padding = '10px';
|
||||
div.style.borderRadius = '4px';
|
||||
div.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
|
||||
results.appendChild(div);
|
||||
}
|
||||
|
||||
async function testAPI() {
|
||||
try {
|
||||
addResult('正在调用用户API...');
|
||||
|
||||
const token = localStorage.getItem('token');
|
||||
if (!token) {
|
||||
addResult('未找到token,请先登录', false);
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await fetch('http://localhost:5350/api/users', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 显示API响应
|
||||
document.getElementById('api-response').textContent = JSON.stringify(data, null, 2);
|
||||
|
||||
// 创建用户表格
|
||||
if (data.success && data.data) {
|
||||
createUsersTable(data.data);
|
||||
addResult(`API调用成功,获取到 ${data.data.length} 个用户`);
|
||||
} else {
|
||||
addResult('API响应格式异常', false);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
addResult(`API调用失败: ${error.message}`, false);
|
||||
document.getElementById('api-response').textContent = `错误: ${error.message}`;
|
||||
}
|
||||
}
|
||||
|
||||
function createUsersTable(users) {
|
||||
const tableContainer = document.getElementById('users-table');
|
||||
|
||||
if (!users || users.length === 0) {
|
||||
tableContainer.innerHTML = '<p>没有用户数据</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
let tableHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>用户名</th>
|
||||
<th>邮箱</th>
|
||||
<th>角色</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`;
|
||||
|
||||
users.forEach(user => {
|
||||
tableHTML += `
|
||||
<tr>
|
||||
<td>${user.id}</td>
|
||||
<td>${user.username}</td>
|
||||
<td>${user.email}</td>
|
||||
<td>${user.role || '未知'}</td>
|
||||
<td>${user.status || '未知'}</td>
|
||||
<td>${user.created_at ? new Date(user.created_at).toLocaleString('zh-CN') : '未知'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
|
||||
tableHTML += `
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
|
||||
tableContainer.innerHTML = tableHTML;
|
||||
}
|
||||
|
||||
function visitUsersPage() {
|
||||
addResult('正在打开用户管理页面...');
|
||||
window.open('http://localhost:5300/#/users', '_blank');
|
||||
}
|
||||
|
||||
function checkLocalStorage() {
|
||||
const token = localStorage.getItem('token');
|
||||
const user = localStorage.getItem('user');
|
||||
|
||||
addResult(`Token: ${token ? '存在' : '不存在'}, 用户信息: ${user ? '存在' : '不存在'}`);
|
||||
|
||||
if (token) {
|
||||
addResult('Token内容: ' + token.substring(0, 50) + '...');
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检查登录状态
|
||||
window.onload = function() {
|
||||
checkLocalStorage();
|
||||
addResult('测试页面已加载,请按照测试步骤进行操作');
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,6 +1,15 @@
|
||||
<template>
|
||||
<div v-if="isLoggedIn">
|
||||
<a-layout style="min-height: 100vh">
|
||||
<!-- 移动端布局 -->
|
||||
<div v-if="isMobile" class="mobile-layout">
|
||||
<MobileNav ref="mobileNavRef" />
|
||||
<div class="mobile-content">
|
||||
<router-view />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 桌面端布局 -->
|
||||
<a-layout v-else style="min-height: 100vh">
|
||||
<a-layout-header class="header">
|
||||
<div class="logo">
|
||||
<a-button
|
||||
@@ -14,6 +23,7 @@
|
||||
宁夏智慧养殖监管平台
|
||||
</div>
|
||||
<div class="user-info">
|
||||
<a href="https://nxzhyz.xiyumuge.com/" target="_blank" style="color: white; text-decoration: none; margin-right: 16px;">大屏展示</a>
|
||||
<span>欢迎, {{ userData?.username }}</span>
|
||||
<a-button type="link" @click="handleLogout" style="color: white;">退出</a-button>
|
||||
</div>
|
||||
@@ -22,11 +32,11 @@
|
||||
<a-layout>
|
||||
<a-layout-sider
|
||||
width="200"
|
||||
style="background: #fff"
|
||||
style="background: #001529"
|
||||
:collapsed="sidebarCollapsed"
|
||||
collapsible
|
||||
>
|
||||
<Menu />
|
||||
<DynamicMenu :collapsed="sidebarCollapsed" />
|
||||
</a-layout-sider>
|
||||
|
||||
<a-layout style="padding: 0 24px 24px">
|
||||
@@ -51,7 +61,8 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Menu from './components/Menu.vue'
|
||||
import DynamicMenu from './components/DynamicMenu.vue'
|
||||
import MobileNav from './components/MobileNav.vue'
|
||||
import { useUserStore, useSettingsStore } from './stores'
|
||||
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
@@ -60,6 +71,17 @@ const userStore = useUserStore()
|
||||
const settingsStore = useSettingsStore()
|
||||
const router = useRouter()
|
||||
|
||||
// 移动端导航引用
|
||||
const mobileNavRef = ref()
|
||||
|
||||
// 响应式检测
|
||||
const isMobile = ref(false)
|
||||
|
||||
// 检测屏幕尺寸
|
||||
const checkScreenSize = () => {
|
||||
isMobile.value = window.innerWidth <= 768
|
||||
}
|
||||
|
||||
// 计算属性
|
||||
const isLoggedIn = computed(() => userStore.isLoggedIn)
|
||||
const userData = computed(() => userStore.userData)
|
||||
@@ -82,15 +104,19 @@ const handleLogout = () => {
|
||||
|
||||
onMounted(() => {
|
||||
userStore.checkLoginStatus()
|
||||
checkScreenSize()
|
||||
window.addEventListener('storage', handleStorageChange)
|
||||
window.addEventListener('resize', checkScreenSize)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('storage', handleStorageChange)
|
||||
window.removeEventListener('resize', checkScreenSize)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 桌面端样式 */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -110,4 +136,132 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
/* 移动端布局样式 */
|
||||
.mobile-layout {
|
||||
min-height: 100vh;
|
||||
background: #f0f2f5;
|
||||
}
|
||||
|
||||
.mobile-content {
|
||||
padding: 12px;
|
||||
padding-top: 68px; /* 为固定头部留出空间 */
|
||||
min-height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
/* 移动端页面内容优化 */
|
||||
.mobile-layout :deep(.page-header) {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.search-area) {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.search-input) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.search-buttons) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.search-buttons .ant-btn) {
|
||||
flex: 1;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* 移动端表格优化 */
|
||||
.mobile-layout :deep(.ant-table-wrapper) {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-table) {
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-table-thead > tr > th) {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-table-tbody > tr > td) {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 移动端模态框优化 */
|
||||
.mobile-layout :deep(.ant-modal) {
|
||||
margin: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-modal-content) {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-modal-body) {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-modal-footer) {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* 移动端卡片优化 */
|
||||
.mobile-layout :deep(.ant-card) {
|
||||
margin-bottom: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-card-body) {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 移动端按钮优化 */
|
||||
.mobile-layout :deep(.ant-btn) {
|
||||
min-height: 40px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-space) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-space-item) {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mobile-layout :deep(.ant-space-item .ant-btn) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 响应式调试 */
|
||||
@media (max-width: 768px) {
|
||||
.debug-indicator {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
background: rgba(24, 144, 255, 0.9);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -87,7 +87,8 @@ const alertTypeOptions = computed(() => {
|
||||
// 按类型分组统计
|
||||
const typeCount = {}
|
||||
dataStore.alerts.forEach(alert => {
|
||||
typeCount[alert.type] = (typeCount[alert.type] || 0) + 1
|
||||
const chineseType = getTypeText(alert.type)
|
||||
typeCount[chineseType] = (typeCount[chineseType] || 0) + 1
|
||||
})
|
||||
|
||||
// 转换为图表数据格式
|
||||
@@ -148,7 +149,7 @@ const alertTableData = computed(() => {
|
||||
return {
|
||||
key: alert.id,
|
||||
id: alert.id,
|
||||
type: alert.type,
|
||||
type: getTypeText(alert.type),
|
||||
level: alert.level,
|
||||
message: alert.message,
|
||||
farmId: alert.farm_id,
|
||||
@@ -277,6 +278,25 @@ function handleStatusUpdate(record) {
|
||||
})
|
||||
}
|
||||
|
||||
// 获取类型文本(英文转中文)
|
||||
function getTypeText(type) {
|
||||
const texts = {
|
||||
temperature_alert: '温度异常',
|
||||
humidity_alert: '湿度异常',
|
||||
feed_alert: '饲料异常',
|
||||
health_alert: '健康异常',
|
||||
device_alert: '设备异常',
|
||||
temperature: '温度异常',
|
||||
humidity: '湿度异常',
|
||||
device_failure: '设备故障',
|
||||
animal_health: '动物健康',
|
||||
security: '安全警报',
|
||||
maintenance: '维护提醒',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 获取状态标签
|
||||
function getStatusLabel(status) {
|
||||
switch (status) {
|
||||
|
||||
@@ -69,7 +69,7 @@ const totalAnimals = computed(() => {
|
||||
|
||||
// 动物种类
|
||||
const animalTypes = computed(() => {
|
||||
const types = new Set(dataStore.animals.map(animal => animal.type))
|
||||
const types = new Set(dataStore.animals.map(animal => getTypeText(animal.type)))
|
||||
return Array.from(types)
|
||||
})
|
||||
|
||||
@@ -84,7 +84,8 @@ const typeDistributionOptions = computed(() => {
|
||||
// 按类型分组统计
|
||||
const typeCount = {}
|
||||
dataStore.animals.forEach(animal => {
|
||||
typeCount[animal.type] = (typeCount[animal.type] || 0) + (animal.count || 0)
|
||||
const chineseType = getTypeText(animal.type)
|
||||
typeCount[chineseType] = (typeCount[chineseType] || 0) + (animal.count || 0)
|
||||
})
|
||||
|
||||
// 转换为图表数据格式
|
||||
@@ -150,7 +151,7 @@ const animalTableData = computed(() => {
|
||||
key: key,
|
||||
farmId: animal.farm_id,
|
||||
farmName: farm ? farm.name : `养殖场 #${animal.farm_id}`,
|
||||
type: animal.type,
|
||||
type: getTypeText(animal.type),
|
||||
count: 0
|
||||
})
|
||||
}
|
||||
@@ -193,6 +194,26 @@ function getPercentage(value, max) {
|
||||
return Math.round((value / max) * 100)
|
||||
}
|
||||
|
||||
// 获取类型文本(英文转中文)
|
||||
function getTypeText(type) {
|
||||
const texts = {
|
||||
pig: '猪',
|
||||
chicken: '鸡',
|
||||
cow: '牛',
|
||||
sheep: '羊',
|
||||
duck: '鸭',
|
||||
goose: '鹅',
|
||||
fish: '鱼',
|
||||
shrimp: '虾',
|
||||
cattle: '牛',
|
||||
poultry: '家禽',
|
||||
swine: '猪',
|
||||
livestock: '牲畜',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 获取进度条颜色
|
||||
function getProgressColor(value, max) {
|
||||
const percentage = getPercentage(value, max)
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import { createMap, addMarkers, clearOverlays, setViewToFitMarkers } from '../utils/mapService'
|
||||
import { baiduMapTester } from '../utils/baiduMapTest'
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
@@ -60,7 +61,7 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
// 定义事件
|
||||
const emit = defineEmits(['map-ready', 'marker-click'])
|
||||
const emit = defineEmits(['map-ready', 'marker-click', 'map-click'])
|
||||
|
||||
// 地图容器引用
|
||||
const mapContainer = ref(null)
|
||||
@@ -81,6 +82,7 @@ const defaultZoom = ref(10)
|
||||
let baiduMap = null
|
||||
let mapMarkers = []
|
||||
let markersInitialized = false
|
||||
let clickListenerAdded = false
|
||||
|
||||
// 初始化地图
|
||||
async function initMap() {
|
||||
@@ -100,6 +102,46 @@ async function initMap() {
|
||||
try {
|
||||
console.log('BaiduMap组件: 调用createMap函数')
|
||||
statusText.value = '正在创建地图实例...'
|
||||
|
||||
// 确保容器有尺寸
|
||||
if (mapContainer.value.offsetWidth === 0 || mapContainer.value.offsetHeight === 0) {
|
||||
console.log('BaiduMap组件: 容器尺寸为0,等待容器渲染...')
|
||||
statusText.value = '等待容器渲染...'
|
||||
// 等待容器渲染
|
||||
await new Promise(resolve => setTimeout(resolve, 200))
|
||||
|
||||
// 再次检查容器尺寸
|
||||
if (mapContainer.value.offsetWidth === 0 || mapContainer.value.offsetHeight === 0) {
|
||||
// 强制设置容器尺寸
|
||||
mapContainer.value.style.width = '100%'
|
||||
mapContainer.value.style.height = '400px'
|
||||
mapContainer.value.style.minHeight = '400px'
|
||||
console.log('BaiduMap组件: 强制设置容器尺寸')
|
||||
await new Promise(resolve => setTimeout(resolve, 100))
|
||||
}
|
||||
}
|
||||
|
||||
// 验证容器最终状态
|
||||
console.log('BaiduMap组件: 容器最终状态:', {
|
||||
offsetWidth: mapContainer.value.offsetWidth,
|
||||
offsetHeight: mapContainer.value.offsetHeight,
|
||||
clientWidth: mapContainer.value.clientWidth,
|
||||
clientHeight: mapContainer.value.clientHeight
|
||||
})
|
||||
|
||||
// 确保容器仍然存在且有效
|
||||
if (!mapContainer.value || !mapContainer.value.parentNode) {
|
||||
throw new Error('地图容器已从DOM中移除')
|
||||
}
|
||||
|
||||
// 等待下一个事件循环,确保DOM完全稳定
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
|
||||
// 再次验证容器
|
||||
if (!mapContainer.value || mapContainer.value.offsetWidth === 0 || mapContainer.value.offsetHeight === 0) {
|
||||
throw new Error('地图容器尺寸无效')
|
||||
}
|
||||
|
||||
// 创建地图实例
|
||||
baiduMap = await createMap(mapContainer.value, props.options)
|
||||
|
||||
@@ -138,6 +180,56 @@ async function initMap() {
|
||||
})
|
||||
}
|
||||
|
||||
// 添加地图点击事件监听器(只添加一次)
|
||||
if (!clickListenerAdded) {
|
||||
baiduMap.addEventListener('click', function(e) {
|
||||
console.log('BaiduMap组件: 地图被点击', e)
|
||||
console.log('点击事件详情:', {
|
||||
type: e.type,
|
||||
lnglat: e.lnglat,
|
||||
point: e.point,
|
||||
pixel: e.pixel,
|
||||
overlay: e.overlay,
|
||||
target: e.target,
|
||||
currentTarget: e.currentTarget
|
||||
})
|
||||
|
||||
// 百度地图点击事件的标准处理方式
|
||||
let lng, lat
|
||||
if (e.lnglat) {
|
||||
lng = e.lnglat.lng
|
||||
lat = e.lnglat.lat
|
||||
} else if (e.point) {
|
||||
lng = e.point.lng
|
||||
lat = e.point.lat
|
||||
} else {
|
||||
// 如果事件对象没有直接的位置信息,尝试从地图获取
|
||||
const point = baiduMap.getPosition(e.pixel)
|
||||
if (point) {
|
||||
lng = point.lng
|
||||
lat = point.lat
|
||||
} else {
|
||||
console.warn('BaiduMap组件: 无法获取点击位置信息', e)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.log('解析到的坐标:', { lng, lat })
|
||||
|
||||
// 触发地图点击事件,传递点击位置信息
|
||||
emit('map-click', {
|
||||
lnglat: {
|
||||
lng: lng,
|
||||
lat: lat
|
||||
},
|
||||
pixel: e.pixel || { x: 0, y: 0 },
|
||||
overlay: e.overlay || null
|
||||
})
|
||||
})
|
||||
clickListenerAdded = true
|
||||
console.log('BaiduMap组件: 点击事件监听器已添加')
|
||||
}
|
||||
|
||||
isReady.value = true
|
||||
isLoading.value = false
|
||||
statusText.value = '地图加载完成'
|
||||
@@ -170,6 +262,10 @@ async function initMap() {
|
||||
if (baiduMap) {
|
||||
baiduMap._markersAdded = true
|
||||
}
|
||||
|
||||
// 备用方案中不重复添加点击事件监听器,避免重复触发
|
||||
console.log('BaiduMap组件: 备用方案 - 跳过重复的点击事件监听器')
|
||||
|
||||
isReady.value = true
|
||||
isLoading.value = false
|
||||
statusText.value = '地图加载完成(兼容模式)'
|
||||
@@ -196,13 +292,33 @@ async function initMap() {
|
||||
} catch (error) {
|
||||
console.error('初始化百度地图失败:', error)
|
||||
console.error('错误详情:', error.stack)
|
||||
statusText.value = '地图加载失败,请检查网络或密钥配置'
|
||||
statusText.value = '地图加载失败,正在诊断问题...'
|
||||
isLoading.value = false
|
||||
isReady.value = false
|
||||
baiduMap = null
|
||||
|
||||
// 运行诊断测试
|
||||
try {
|
||||
console.log('🔍 开始运行百度地图诊断测试...')
|
||||
const testResults = await baiduMapTester.runFullTest()
|
||||
const suggestions = baiduMapTester.getFixSuggestions()
|
||||
|
||||
console.log('📊 诊断结果:', testResults)
|
||||
console.log('💡 修复建议:', suggestions)
|
||||
|
||||
if (suggestions.length > 0) {
|
||||
statusText.value = `地图加载失败: ${error.message}。请查看控制台获取修复建议。`
|
||||
} else {
|
||||
statusText.value = '地图加载失败,请检查网络或密钥配置'
|
||||
}
|
||||
} catch (diagnosticError) {
|
||||
console.error('诊断测试失败:', diagnosticError)
|
||||
statusText.value = '地图加载失败,请检查网络或密钥配置'
|
||||
}
|
||||
|
||||
// 清理状态文本
|
||||
if (clearStatusTimer) clearTimeout(clearStatusTimer)
|
||||
clearStatusTimer = setTimeout(() => { statusText.value = '' }, 3000)
|
||||
clearStatusTimer = setTimeout(() => { statusText.value = '' }, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,23 +336,42 @@ function addMarkersToMap() {
|
||||
return
|
||||
}
|
||||
|
||||
// 验证BMap对象是否可用
|
||||
if (!window.BMap) {
|
||||
console.error('BaiduMap组件: BMap对象不可用')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('BaiduMap组件: 开始添加标记,数量:', props.markers?.length || 0)
|
||||
|
||||
// 清除现有标记
|
||||
clearOverlays(baiduMap)
|
||||
|
||||
// 验证标记数据
|
||||
if (!props.markers || !Array.isArray(props.markers) || props.markers.length === 0) {
|
||||
console.log('BaiduMap组件: 没有标记数据需要添加')
|
||||
return
|
||||
}
|
||||
|
||||
// 添加新标记
|
||||
mapMarkers = addMarkers(baiduMap, props.markers, (markerData, marker) => {
|
||||
// 触发标记点击事件
|
||||
emit('marker-click', markerData, marker)
|
||||
})
|
||||
|
||||
console.log('BaiduMap组件: 成功添加标记,数量:', mapMarkers.length)
|
||||
|
||||
// 调整视图以显示所有标记
|
||||
if (mapMarkers.length > 0) {
|
||||
const points = mapMarkers.map(marker => marker.getPosition())
|
||||
setViewToFitMarkers(baiduMap, points)
|
||||
const points = mapMarkers.map(marker => marker.getPosition()).filter(point => point)
|
||||
if (points.length > 0) {
|
||||
setViewToFitMarkers(baiduMap, points)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('BaiduMap组件: 添加标记失败:', error)
|
||||
console.error('错误详情:', error.stack)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +416,43 @@ watch(() => props.markers, (newMarkers) => {
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
// 重试机制
|
||||
let retryCount = 0
|
||||
const maxRetries = 3
|
||||
|
||||
async function retryInitMap() {
|
||||
if (retryCount >= maxRetries) {
|
||||
console.error('BaiduMap组件: 达到最大重试次数,停止重试')
|
||||
statusText.value = '地图初始化失败,请刷新页面重试'
|
||||
isLoading.value = false
|
||||
return
|
||||
}
|
||||
|
||||
retryCount++
|
||||
console.log(`BaiduMap组件: 第${retryCount}次重试初始化地图`)
|
||||
statusText.value = `正在重试初始化地图 (${retryCount}/${maxRetries})...`
|
||||
|
||||
// 清理之前的状态
|
||||
if (baiduMap) {
|
||||
baiduMap = null
|
||||
}
|
||||
|
||||
// 等待一段时间后重试
|
||||
await new Promise(resolve => setTimeout(resolve, 1000 * retryCount))
|
||||
|
||||
try {
|
||||
await initMap()
|
||||
} catch (error) {
|
||||
console.error(`BaiduMap组件: 第${retryCount}次重试失败:`, error)
|
||||
if (retryCount < maxRetries) {
|
||||
retryInitMap()
|
||||
} else {
|
||||
statusText.value = '地图初始化失败,请检查网络连接或刷新页面'
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时初始化地图
|
||||
onMounted(() => {
|
||||
console.log('BaiduMap组件: 组件已挂载')
|
||||
@@ -289,10 +461,21 @@ onMounted(() => {
|
||||
console.log('地图容器类名:', mapContainer.value?.className)
|
||||
|
||||
// 延迟初始化,确保DOM完全渲染
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
console.log('BaiduMap组件: 延迟初始化地图')
|
||||
initMap()
|
||||
}, 100)
|
||||
// 确保容器完全可见
|
||||
if (mapContainer.value) {
|
||||
mapContainer.value.style.visibility = 'visible'
|
||||
mapContainer.value.style.display = 'block'
|
||||
}
|
||||
|
||||
try {
|
||||
await initMap()
|
||||
} catch (error) {
|
||||
console.error('BaiduMap组件: 初始化失败,开始重试:', error)
|
||||
retryInitMap()
|
||||
}
|
||||
}, 200)
|
||||
})
|
||||
|
||||
// 组件卸载时清理资源
|
||||
|
||||
@@ -1,30 +1,26 @@
|
||||
<template>
|
||||
<div class="dashboard">
|
||||
<h1>系统概览</h1>
|
||||
<!-- <h1>系统概览</h1> -->
|
||||
|
||||
<div class="dashboard-stats">
|
||||
<a-card title="养殖场数量" :bordered="false">
|
||||
<a-card title="栏舍数量" :bordered="false">
|
||||
<template #extra><a-tag color="blue">总计</a-tag></template>
|
||||
<p class="stat-number">{{ farmCount }}</p>
|
||||
<p class="stat-change"><rise-outlined /> 较上月增长 {{ statsData.farmGrowth }}%</p>
|
||||
<p class="stat-number">{{ penCount }}</p>
|
||||
</a-card>
|
||||
|
||||
<a-card title="动物种类" :bordered="false">
|
||||
<a-card title="牛只数量" :bordered="false">
|
||||
<template #extra><a-tag color="green">总计</a-tag></template>
|
||||
<p class="stat-number">{{ animalCount }}</p>
|
||||
<p class="stat-change"><rise-outlined /> 较上月增长 {{ statsData.animalGrowth }}%</p>
|
||||
</a-card>
|
||||
|
||||
<a-card title="监测设备" :bordered="false">
|
||||
<a-card title="智能设备" :bordered="false">
|
||||
<template #extra><a-tag color="orange">在线率</a-tag></template>
|
||||
<p class="stat-number">{{ deviceCount }}</p>
|
||||
<p class="stat-change">在线率 {{ deviceOnlineRate }}%</p>
|
||||
</a-card>
|
||||
|
||||
<a-card title="预警信息" :bordered="false">
|
||||
<a-card title="智能预警" :bordered="false">
|
||||
<template #extra><a-tag color="red">本月</a-tag></template>
|
||||
<p class="stat-number">{{ alertCount }}</p>
|
||||
<p class="stat-change"><fall-outlined /> 较上月下降 {{ statsData.alertReduction }}%</p>
|
||||
<p class="stat-number clickable" @click="handleAlertClick">{{ alertCount }}</p>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
@@ -36,10 +32,6 @@
|
||||
@marker-click="handleFarmClick"
|
||||
/>
|
||||
</a-card>
|
||||
|
||||
<!-- <a-card title="月度数据趋势" :bordered="false" class="chart-card">
|
||||
<div ref="trendChart" class="chart-container"></div>
|
||||
</a-card> -->
|
||||
</div>
|
||||
|
||||
<!-- 养殖场详情抽屉 -->
|
||||
@@ -57,6 +49,7 @@
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { RiseOutlined, FallOutlined } from '@ant-design/icons-vue'
|
||||
import { useDataStore } from '../stores'
|
||||
import { convertFarmsToMarkers } from '../utils/mapService'
|
||||
@@ -67,6 +60,9 @@ import { message } from 'ant-design-vue'
|
||||
// 使用数据存储
|
||||
const dataStore = useDataStore()
|
||||
|
||||
// 使用路由
|
||||
const router = useRouter()
|
||||
|
||||
// 图表容器引用
|
||||
const trendChart = ref(null)
|
||||
|
||||
@@ -78,6 +74,7 @@ const farmCount = computed(() => dataStore.farmCount)
|
||||
const animalCount = computed(() => dataStore.animalCount)
|
||||
const deviceCount = computed(() => dataStore.deviceCount)
|
||||
const alertCount = computed(() => dataStore.alertCount)
|
||||
const penCount = computed(() => dataStore.penCount)
|
||||
const deviceOnlineRate = computed(() => dataStore.deviceOnlineRate)
|
||||
|
||||
// 养殖场地图标记
|
||||
@@ -106,9 +103,18 @@ function closeDrawer() {
|
||||
drawerVisible.value = false
|
||||
}
|
||||
|
||||
// 处理智能预警点击事件
|
||||
function handleAlertClick() {
|
||||
console.log('点击智能预警,跳转到智能耳标预警页面')
|
||||
router.push('/smart-alerts/eartag')
|
||||
}
|
||||
|
||||
// 图表实例
|
||||
let trendChartInstance = null
|
||||
|
||||
// 定时刷新器
|
||||
let refreshTimer = null
|
||||
|
||||
// 组件挂载后初始化数据和图表
|
||||
onMounted(async () => {
|
||||
console.log('Dashboard: 组件挂载,开始加载数据')
|
||||
@@ -124,6 +130,9 @@ onMounted(async () => {
|
||||
|
||||
// 添加窗口大小变化监听
|
||||
window.addEventListener('resize', handleResize)
|
||||
|
||||
// 启动定时刷新智能预警数据(每30秒刷新一次)
|
||||
startSmartAlertRefresh()
|
||||
})
|
||||
|
||||
// 组件卸载时清理资源
|
||||
@@ -131,6 +140,12 @@ onUnmounted(() => {
|
||||
// 移除窗口大小变化监听
|
||||
window.removeEventListener('resize', handleResize)
|
||||
|
||||
// 清理定时器
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
|
||||
// 销毁图表实例
|
||||
if (trendChartInstance) {
|
||||
trendChartInstance.dispose()
|
||||
@@ -166,6 +181,28 @@ function handleResize() {
|
||||
trendChartInstance.resize()
|
||||
}
|
||||
}
|
||||
|
||||
// 启动智能预警数据定时刷新
|
||||
function startSmartAlertRefresh() {
|
||||
// 每30秒刷新一次智能预警数据
|
||||
refreshTimer = setInterval(async () => {
|
||||
try {
|
||||
console.log('定时刷新智能预警数据...')
|
||||
await dataStore.fetchSmartAlerts()
|
||||
console.log('智能预警数据刷新完成,当前预警数量:', dataStore.alertCount)
|
||||
} catch (error) {
|
||||
console.error('刷新智能预警数据失败:', error)
|
||||
}
|
||||
}, 30000) // 30秒
|
||||
}
|
||||
|
||||
// 停止智能预警数据定时刷新
|
||||
function stopSmartAlertRefresh() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -186,6 +223,15 @@ function handleResize() {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.stat-number.clickable {
|
||||
cursor: pointer;
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.stat-number.clickable:hover {
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.stat-change {
|
||||
color: #8c8c8c;
|
||||
display: flex;
|
||||
|
||||
@@ -263,7 +263,7 @@ const deviceTableData = computed(() => {
|
||||
key: device.id,
|
||||
id: device.id,
|
||||
name: device.name,
|
||||
type: device.type,
|
||||
type: getTypeText(device.type),
|
||||
farmId: device.farm_id || device.farmId,
|
||||
farmName: farm ? farm.name : `养殖场 #${device.farm_id || device.farmId}`,
|
||||
status: getStatusText(device.status),
|
||||
@@ -310,6 +310,24 @@ const deviceColumns = [
|
||||
}
|
||||
]
|
||||
|
||||
// 获取类型文本(英文转中文)
|
||||
function getTypeText(type) {
|
||||
const texts = {
|
||||
temperature_sensor: '温度传感器',
|
||||
humidity_sensor: '湿度传感器',
|
||||
feed_dispenser: '饲料分配器',
|
||||
water_system: '水系统',
|
||||
ventilation_system: '通风系统',
|
||||
sensor: '传感器',
|
||||
camera: '摄像头',
|
||||
feeder: '喂食器',
|
||||
monitor: '监控器',
|
||||
controller: '控制器',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 获取状态文本(英文转中文)
|
||||
function getStatusText(status) {
|
||||
switch (status) {
|
||||
|
||||
544
admin-system/frontend/src/components/DynamicMenu.vue
Normal file
544
admin-system/frontend/src/components/DynamicMenu.vue
Normal file
@@ -0,0 +1,544 @@
|
||||
<template>
|
||||
<a-menu
|
||||
:selectedKeys="selectedKeys"
|
||||
:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="dark"
|
||||
:inline-collapsed="collapsed"
|
||||
@click="handleMenuClick"
|
||||
@update:selectedKeys="selectedKeys = $event"
|
||||
@openChange="handleOpenChange"
|
||||
>
|
||||
<template v-for="menu in menuTree">
|
||||
<!-- 有子菜单的父菜单 -->
|
||||
<a-sub-menu v-if="menu.children && menu.children.length > 0" :key="menu.id">
|
||||
<template #icon>
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<HomeOutlined v-else />
|
||||
</template>
|
||||
<template #title>{{ menu.name }}</template>
|
||||
|
||||
<template v-for="subMenu in menu.children">
|
||||
<!-- 子菜单还有子菜单 -->
|
||||
<a-sub-menu v-if="subMenu.children && subMenu.children.length > 0" :key="subMenu.id">
|
||||
<template #icon>
|
||||
<component :is="subMenu.icon" v-if="subMenu.icon" />
|
||||
<SettingOutlined v-else />
|
||||
</template>
|
||||
<template #title>{{ subMenu.name }}</template>
|
||||
|
||||
<a-menu-item v-for="item in subMenu.children" :key="item.id">
|
||||
<template #icon>
|
||||
<component :is="item.icon" v-if="item.icon" />
|
||||
<FileOutlined v-else />
|
||||
</template>
|
||||
{{ item.name }}
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 子菜单是叶子节点 -->
|
||||
<a-menu-item v-else :key="subMenu.id">
|
||||
<template #icon>
|
||||
<component :is="subMenu.icon" v-if="subMenu.icon" />
|
||||
<FileOutlined v-else />
|
||||
</template>
|
||||
{{ subMenu.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 没有子菜单的叶子菜单 -->
|
||||
<a-menu-item v-else :key="menu.id">
|
||||
<template #icon>
|
||||
<component :is="menu.icon" v-if="menu.icon" />
|
||||
<HomeOutlined v-else />
|
||||
</template>
|
||||
{{ menu.name }}
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import {
|
||||
HomeOutlined,
|
||||
SettingOutlined,
|
||||
FileOutlined,
|
||||
DashboardOutlined,
|
||||
UserOutlined,
|
||||
ShopOutlined,
|
||||
ShoppingCartOutlined,
|
||||
AlertOutlined,
|
||||
BarChartOutlined,
|
||||
EnvironmentOutlined,
|
||||
TeamOutlined,
|
||||
SafetyOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { menuService } from '../utils/dataService'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { useUserStore } from '../stores'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
// Emits
|
||||
const emit = defineEmits(['menu-click'])
|
||||
|
||||
// Router
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 响应式数据
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
const menuTree = ref([])
|
||||
|
||||
// 图标映射
|
||||
const iconMap = {
|
||||
'DashboardOutlined': DashboardOutlined,
|
||||
'UserOutlined': UserOutlined,
|
||||
'ShopOutlined': ShopOutlined,
|
||||
'ShoppingCartOutlined': ShoppingCartOutlined,
|
||||
'AlertOutlined': AlertOutlined,
|
||||
'BarChartOutlined': BarChartOutlined,
|
||||
'EnvironmentOutlined': EnvironmentOutlined,
|
||||
'TeamOutlined': TeamOutlined,
|
||||
'SafetyOutlined': SafetyOutlined,
|
||||
'SettingOutlined': SettingOutlined,
|
||||
'FileOutlined': FileOutlined
|
||||
}
|
||||
|
||||
// 根据用户权限过滤菜单树
|
||||
const filterMenuTreeByPermission = (menuTree) => {
|
||||
console.log('开始权限过滤菜单树,原始菜单树数量:', menuTree.length)
|
||||
console.log('用户可访问菜单:', userStore.userData?.accessibleMenus)
|
||||
|
||||
if (!userStore.userData?.accessibleMenus || userStore.userData.accessibleMenus.length === 0) {
|
||||
console.log('用户无菜单权限,返回空数组')
|
||||
return []
|
||||
}
|
||||
|
||||
const filterMenu = (menu) => {
|
||||
// 检查当前菜单是否有权限
|
||||
const hasPermission = userStore.canAccessMenu(menu.menu_key)
|
||||
console.log(`菜单权限检查 ${menu.name} (${menu.menu_key}):`, hasPermission)
|
||||
|
||||
// 递归过滤子菜单
|
||||
const filteredChildren = menu.children ? menu.children.map(filterMenu).filter(child => child !== null) : []
|
||||
console.log(`子菜单过滤结果 ${menu.name}:`, filteredChildren.length, '个子菜单')
|
||||
|
||||
// 如果当前菜单有权限,或者有子菜单有权限,则保留
|
||||
if (hasPermission || filteredChildren.length > 0) {
|
||||
const result = {
|
||||
...menu,
|
||||
children: filteredChildren
|
||||
}
|
||||
console.log(`保留菜单 ${menu.name}:`, result)
|
||||
return result
|
||||
}
|
||||
|
||||
console.log(`过滤掉菜单 ${menu.name}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const filteredMenus = menuTree.map(filterMenu).filter(menu => menu !== null)
|
||||
|
||||
console.log('权限过滤完成,过滤后菜单树数量:', filteredMenus.length)
|
||||
return filteredMenus
|
||||
}
|
||||
|
||||
// 加载菜单数据
|
||||
const loadMenus = async () => {
|
||||
try {
|
||||
console.log('开始加载菜单数据...')
|
||||
const response = await menuService.getAllMenus()
|
||||
console.log('菜单API响应:', response)
|
||||
|
||||
let allMenus = []
|
||||
// 检查响应格式
|
||||
if (response && Array.isArray(response)) {
|
||||
// 直接是数组
|
||||
allMenus = response
|
||||
} else if (response && response.data && Array.isArray(response.data)) {
|
||||
// 包含data字段,直接是数组
|
||||
allMenus = response.data
|
||||
} else if (response && response.data && response.data.list && Array.isArray(response.data.list)) {
|
||||
// 包含data.list字段
|
||||
allMenus = response.data.list
|
||||
} else if (response && response.list && Array.isArray(response.list)) {
|
||||
// 直接包含list字段
|
||||
allMenus = response.list
|
||||
} else {
|
||||
console.warn('菜单数据格式不正确,使用默认菜单')
|
||||
console.warn('响应数据:', response)
|
||||
menuTree.value = getDefaultMenus()
|
||||
return
|
||||
}
|
||||
|
||||
// 先构建菜单树,再过滤权限
|
||||
const fullMenuTree = buildMenuTree(allMenus)
|
||||
console.log('构建的完整菜单树:', fullMenuTree)
|
||||
|
||||
// 根据用户权限过滤菜单树
|
||||
const filteredMenus = filterMenuTreeByPermission(fullMenuTree)
|
||||
console.log('权限过滤后的菜单树:', filteredMenus)
|
||||
|
||||
menuTree.value = filteredMenus
|
||||
console.log('菜单树构建完成:', menuTree.value)
|
||||
console.log('菜单树长度:', menuTree.value.length)
|
||||
|
||||
// 设置当前选中的菜单项
|
||||
setActiveMenu()
|
||||
} catch (error) {
|
||||
console.error('加载菜单数据失败:', error)
|
||||
message.error('加载菜单数据失败')
|
||||
|
||||
// 使用默认菜单作为后备
|
||||
console.log('使用默认菜单作为后备')
|
||||
menuTree.value = getDefaultMenus()
|
||||
console.log('默认菜单设置完成:', menuTree.value.length, '个菜单项')
|
||||
}
|
||||
}
|
||||
|
||||
// 构建菜单树
|
||||
const buildMenuTree = (menus) => {
|
||||
console.log('开始构建菜单树,原始菜单数量:', menus.length)
|
||||
|
||||
// 转换字段名
|
||||
const convertMenuFields = (menu) => {
|
||||
return {
|
||||
id: menu.id,
|
||||
name: menu.menu_name || menu.name,
|
||||
path: menu.menu_path || menu.path,
|
||||
icon: menu.icon,
|
||||
parent_id: menu.parent_id,
|
||||
sort_order: menu.sort_order || 0,
|
||||
menu_key: menu.menu_key,
|
||||
children: []
|
||||
}
|
||||
}
|
||||
|
||||
// 转换所有菜单项
|
||||
const convertedMenus = menus.map(convertMenuFields)
|
||||
console.log('转换后的菜单:', convertedMenus)
|
||||
|
||||
// 创建菜单映射表
|
||||
const menuMap = new Map()
|
||||
const rootMenus = []
|
||||
|
||||
// 先创建所有菜单项
|
||||
convertedMenus.forEach(menu => {
|
||||
menuMap.set(menu.id, menu)
|
||||
})
|
||||
|
||||
// 构建父子关系
|
||||
convertedMenus.forEach(menu => {
|
||||
if (menu.parent_id && menuMap.has(menu.parent_id)) {
|
||||
// 有父级,添加到父级的children中
|
||||
const parent = menuMap.get(menu.parent_id)
|
||||
parent.children.push(menu)
|
||||
} else {
|
||||
// 没有父级,是根级菜单
|
||||
rootMenus.push(menu)
|
||||
}
|
||||
})
|
||||
|
||||
// 对每个层级的菜单进行排序
|
||||
const sortMenus = (menuList) => {
|
||||
menuList.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0))
|
||||
menuList.forEach(menu => {
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
sortMenus(menu.children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
sortMenus(rootMenus)
|
||||
|
||||
console.log('构建完成的菜单树:', rootMenus)
|
||||
console.log('根级菜单数量:', rootMenus.length)
|
||||
|
||||
return rootMenus
|
||||
}
|
||||
|
||||
// 设置当前激活的菜单项
|
||||
const setActiveMenu = () => {
|
||||
const currentPath = route.path
|
||||
console.log('当前路径:', currentPath)
|
||||
|
||||
// 查找匹配的菜单项
|
||||
const findMenuByPath = (menus, path) => {
|
||||
for (const menu of menus) {
|
||||
if (menu.path === path) {
|
||||
return menu
|
||||
}
|
||||
if (menu.children) {
|
||||
const found = findMenuByPath(menu.children, path)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const activeMenu = findMenuByPath(menuTree.value, currentPath)
|
||||
if (activeMenu && activeMenu.id != null) {
|
||||
selectedKeys.value = [activeMenu.id.toString()]
|
||||
|
||||
// 设置打开的父菜单
|
||||
const parentIds = getParentMenuIds(activeMenu.id, menuTree.value)
|
||||
openKeys.value = parentIds ? parentIds.map(id => id != null ? id.toString() : '').filter(id => id) : []
|
||||
}
|
||||
}
|
||||
|
||||
// 获取父菜单ID
|
||||
const getParentMenuIds = (menuId, menus, parentIds = []) => {
|
||||
for (const menu of menus) {
|
||||
if (menu && menu.id === menuId) {
|
||||
return parentIds
|
||||
}
|
||||
if (menu && menu.children) {
|
||||
const found = getParentMenuIds(menuId, menu.children, [...parentIds, menu.id])
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 处理菜单展开/收起
|
||||
const handleOpenChange = (keys) => {
|
||||
console.log('菜单展开状态变化:', keys)
|
||||
openKeys.value = keys
|
||||
}
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
console.log('菜单点击:', key)
|
||||
|
||||
// 检查key是否为null或undefined
|
||||
if (!key) {
|
||||
console.warn('菜单点击事件中key为空:', key)
|
||||
return
|
||||
}
|
||||
|
||||
// 直接使用key作为ID,不再需要处理前缀
|
||||
const actualKey = key.toString()
|
||||
console.log('实际查找的key:', actualKey)
|
||||
|
||||
// 查找菜单项
|
||||
const findMenuById = (menus, id) => {
|
||||
for (const menu of menus) {
|
||||
// 检查menu和menu.id是否存在
|
||||
if (menu && menu.id != null && menu.id.toString() === id) {
|
||||
return menu
|
||||
}
|
||||
if (menu && menu.children) {
|
||||
const found = findMenuById(menu.children, id)
|
||||
if (found) return found
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const menuItem = findMenuById(menuTree.value, actualKey)
|
||||
if (menuItem && menuItem.path) {
|
||||
console.log('导航到:', menuItem.path)
|
||||
router.push(menuItem.path)
|
||||
emit('menu-click', menuItem)
|
||||
} else {
|
||||
console.warn('未找到菜单项:', actualKey)
|
||||
console.log('当前菜单树:', menuTree.value)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认菜单(作为后备)
|
||||
const getDefaultMenus = () => {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
name: '系统概览',
|
||||
path: '/dashboard',
|
||||
icon: 'DashboardOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: '智能设备',
|
||||
path: '/smart-devices',
|
||||
icon: 'SettingOutlined',
|
||||
children: [
|
||||
{
|
||||
id: 21,
|
||||
name: '智能耳标',
|
||||
path: '/smart-devices/eartag',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: '智能项圈',
|
||||
path: '/smart-devices/collar',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 23,
|
||||
name: '智能主机',
|
||||
path: '/smart-devices/host',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 24,
|
||||
name: '电子围栏',
|
||||
path: '/smart-devices/fence',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: '智能预警总览',
|
||||
path: '/smart-alerts',
|
||||
icon: 'AlertOutlined',
|
||||
children: [
|
||||
{
|
||||
id: 31,
|
||||
name: '智能耳标预警',
|
||||
path: '/smart-alerts/eartag',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 32,
|
||||
name: '智能项圈预警',
|
||||
path: '/smart-alerts/collar',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: '牛只管理',
|
||||
path: '/cattle-management',
|
||||
icon: 'BugOutlined',
|
||||
children: [
|
||||
{
|
||||
id: 41,
|
||||
name: '牛只档案',
|
||||
path: '/cattle-management/archives',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 42,
|
||||
name: '栏舍设置',
|
||||
path: '/cattle-management/pens',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 43,
|
||||
name: '批次设置',
|
||||
path: '/cattle-management/batches',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 44,
|
||||
name: '转栏记录',
|
||||
path: '/cattle-management/transfer-records',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 45,
|
||||
name: '离栏记录',
|
||||
path: '/cattle-management/exit-records',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
name: '养殖场管理',
|
||||
path: '/farm-management',
|
||||
icon: 'HomeOutlined',
|
||||
children: [
|
||||
{
|
||||
id: 51,
|
||||
name: '养殖场信息管理',
|
||||
path: '/farm-management/info',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 52,
|
||||
name: '栏舍管理',
|
||||
path: '/farm-management/pens',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 53,
|
||||
name: '用户管理',
|
||||
path: '/farm-management/users',
|
||||
icon: 'FileOutlined',
|
||||
children: []
|
||||
},
|
||||
{
|
||||
id: 54,
|
||||
name: '角色权限管理',
|
||||
path: '/farm-management/role-permissions',
|
||||
icon: 'SafetyOutlined',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
name: '系统管理',
|
||||
path: '/system',
|
||||
icon: 'SettingOutlined',
|
||||
children: []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 监听路由变化
|
||||
watch(() => route.path, () => {
|
||||
setActiveMenu()
|
||||
})
|
||||
|
||||
// 监听用户数据变化,当用户数据加载完成后再加载菜单
|
||||
watch(() => userStore.userData, (newUserData) => {
|
||||
console.log('用户数据变化,重新加载菜单:', newUserData)
|
||||
if (newUserData && newUserData.id) {
|
||||
loadMenus()
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 组件挂载时也尝试加载菜单(作为后备)
|
||||
onMounted(() => {
|
||||
console.log('DynamicMenu组件挂载,当前用户数据:', userStore.userData)
|
||||
if (userStore.userData && userStore.userData.id) {
|
||||
loadMenus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ant-menu {
|
||||
border-right: none;
|
||||
}
|
||||
</style>
|
||||
@@ -1,34 +1,180 @@
|
||||
<template>
|
||||
<a-menu
|
||||
mode="inline"
|
||||
:selected-keys="[activeRoute]"
|
||||
:selected-keys="selectedKeys"
|
||||
:open-keys="openKeys"
|
||||
@openChange="handleOpenChange"
|
||||
:style="{ height: '100%', borderRight: 0 }"
|
||||
>
|
||||
<a-menu-item v-for="route in menuRoutes" :key="route.name">
|
||||
<template v-for="route in menuRoutes">
|
||||
<!-- 有子菜单的路由 -->
|
||||
<a-sub-menu v-if="route.children && route.children.length > 0" :key="route.name">
|
||||
<template #title>
|
||||
<component :is="route.meta.icon" />
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</template>
|
||||
<a-menu-item v-for="child in route.children" :key="child.name">
|
||||
<router-link :to="child.path">
|
||||
<span>{{ child.meta.title }}</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
|
||||
<!-- 普通菜单项 -->
|
||||
<a-menu-item v-else :key="`menu-${route.name}`">
|
||||
<router-link :to="route.path">
|
||||
<component :is="route.meta.icon" />
|
||||
<span>{{ route.meta.title }}</span>
|
||||
</router-link>
|
||||
</a-menu-item>
|
||||
</template>
|
||||
</a-menu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import * as Icons from '@ant-design/icons-vue'
|
||||
import { setupGlobalDebugger, debugMenuPermissions } from '../utils/menuDebugger'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 菜单状态
|
||||
const openKeys = ref([])
|
||||
|
||||
// 检查路由权限
|
||||
const hasRoutePermission = (routeItem) => {
|
||||
console.log('检查路由权限:', routeItem.name, routeItem.meta)
|
||||
console.log('用户数据:', userStore.userData)
|
||||
console.log('用户权限:', userStore.getUserPermissions())
|
||||
console.log('用户角色:', userStore.getUserRoleName())
|
||||
|
||||
// 检查权限
|
||||
if (routeItem.meta?.permission) {
|
||||
const hasPerm = userStore.hasPermission(routeItem.meta.permission)
|
||||
console.log('权限检查结果:', routeItem.meta.permission, hasPerm)
|
||||
return hasPerm
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if (routeItem.meta?.roles && routeItem.meta.roles.length > 0) {
|
||||
const hasRole = userStore.hasRole(routeItem.meta.roles)
|
||||
console.log('角色检查结果:', routeItem.meta.roles, hasRole)
|
||||
return hasRole
|
||||
}
|
||||
|
||||
// 检查菜单权限
|
||||
if (routeItem.meta?.menu) {
|
||||
const canAccess = userStore.canAccessMenu(routeItem.meta.menu)
|
||||
console.log('菜单权限检查结果:', routeItem.meta.menu, canAccess)
|
||||
return canAccess
|
||||
}
|
||||
|
||||
console.log('无权限要求,允许访问')
|
||||
return true
|
||||
}
|
||||
|
||||
// 获取所有需要在菜单中显示的路由
|
||||
const menuRoutes = computed(() => {
|
||||
return router.options.routes.filter(route => {
|
||||
// 只显示需要认证且有图标的路由
|
||||
return route.meta && route.meta.requiresAuth && route.meta.icon
|
||||
})
|
||||
console.log('所有路由:', router.options.routes)
|
||||
|
||||
const filteredRoutes = router.options.routes
|
||||
.filter(route => {
|
||||
console.log('检查路由:', route.name, route.meta)
|
||||
|
||||
// 只显示需要认证的路由,主路由需要图标,子路由不需要
|
||||
if (!route.meta || !route.meta.requiresAuth) {
|
||||
console.log('路由被过滤 - 无meta/requiresAuth:', route.name)
|
||||
return false
|
||||
}
|
||||
|
||||
// 主路由必须有图标,子路由不需要
|
||||
if (!route.meta.icon && (!route.children || route.children.length === 0)) {
|
||||
console.log('路由被过滤 - 主路由无图标:', route.name)
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查主路由权限 - 优先检查菜单权限
|
||||
if (route.meta?.menu) {
|
||||
const canAccess = userStore.canAccessMenu(route.meta.menu)
|
||||
console.log(`菜单权限检查 ${route.name} (${route.meta.menu}):`, canAccess)
|
||||
if (!canAccess) {
|
||||
console.log('路由被过滤 - 无菜单权限:', route.name)
|
||||
return false
|
||||
}
|
||||
} else if (!hasRoutePermission(route)) {
|
||||
console.log('路由被过滤 - 无权限:', route.name)
|
||||
return false
|
||||
}
|
||||
|
||||
console.log('路由通过过滤:', route.name)
|
||||
return true
|
||||
})
|
||||
.map(route => {
|
||||
// 如果有子路由,过滤有权限的子路由
|
||||
if (route.children && route.children.length > 0) {
|
||||
console.log('处理子路由:', route.name, route.children)
|
||||
const accessibleChildren = route.children.filter(child => {
|
||||
const hasAccess = child.meta && hasRoutePermission(child)
|
||||
console.log('子路由权限检查:', child.name, hasAccess)
|
||||
return hasAccess
|
||||
})
|
||||
|
||||
console.log('可访问的子路由:', route.name, accessibleChildren)
|
||||
return {
|
||||
...route,
|
||||
children: accessibleChildren
|
||||
}
|
||||
}
|
||||
|
||||
return route
|
||||
})
|
||||
.filter(route => {
|
||||
// 如果是子菜单,至少要有一个可访问的子项
|
||||
if (route.children && route.children.length === 0) {
|
||||
console.log('子菜单无子项,过滤掉:', route.name)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
console.log('最终菜单路由:', filteredRoutes)
|
||||
return filteredRoutes
|
||||
})
|
||||
|
||||
// 当前活动路由
|
||||
const activeRoute = computed(() => route.name)
|
||||
// 当前选中的菜单项
|
||||
const selectedKeys = computed(() => {
|
||||
const currentRoute = route.name
|
||||
return [currentRoute]
|
||||
})
|
||||
|
||||
// 监听路由变化,自动展开对应的子菜单
|
||||
watch(() => route.name, (newRouteName) => {
|
||||
const currentRoute = router.options.routes.find(r =>
|
||||
r.children && r.children.some(child => child.name === newRouteName)
|
||||
)
|
||||
|
||||
if (currentRoute && !openKeys.value.includes(currentRoute.name)) {
|
||||
openKeys.value = [...openKeys.value, currentRoute.name]
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
// 处理子菜单展开/收起
|
||||
const handleOpenChange = (keys) => {
|
||||
openKeys.value = keys
|
||||
}
|
||||
|
||||
// 组件挂载时设置调试工具
|
||||
onMounted(() => {
|
||||
setupGlobalDebugger()
|
||||
|
||||
// 调试菜单权限
|
||||
if (userStore.userData) {
|
||||
console.log('🔍 开始调试菜单权限...')
|
||||
debugMenuPermissions(userStore.userData, router.options.routes)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
476
admin-system/frontend/src/components/MobileNav.vue
Normal file
476
admin-system/frontend/src/components/MobileNav.vue
Normal file
@@ -0,0 +1,476 @@
|
||||
<template>
|
||||
<div class="mobile-nav">
|
||||
<!-- 移动端头部 -->
|
||||
<div class="mobile-header">
|
||||
<button
|
||||
class="mobile-menu-button"
|
||||
@click="toggleSidebar"
|
||||
:aria-label="sidebarVisible ? '关闭菜单' : '打开菜单'"
|
||||
>
|
||||
<MenuOutlined v-if="!sidebarVisible" />
|
||||
<CloseOutlined v-else />
|
||||
</button>
|
||||
|
||||
<div class="mobile-title">
|
||||
{{ currentPageTitle }}
|
||||
</div>
|
||||
|
||||
<div class="mobile-user-info">
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="profile">
|
||||
<UserOutlined />
|
||||
个人信息
|
||||
</a-menu-item>
|
||||
<a-menu-item key="settings">
|
||||
<SettingOutlined />
|
||||
系统设置
|
||||
</a-menu-item>
|
||||
<a-menu-divider />
|
||||
<a-menu-item key="logout" @click="handleLogout">
|
||||
<LogoutOutlined />
|
||||
退出登录
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="text" size="small">
|
||||
<UserOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 移动端侧边栏遮罩 -->
|
||||
<div
|
||||
v-if="sidebarVisible"
|
||||
class="mobile-sidebar-overlay"
|
||||
@click="closeSidebar"
|
||||
></div>
|
||||
|
||||
<!-- 移动端侧边栏 -->
|
||||
<div
|
||||
class="mobile-sidebar"
|
||||
:class="{ 'sidebar-open': sidebarVisible }"
|
||||
>
|
||||
<div class="sidebar-header">
|
||||
<div class="logo">
|
||||
<span class="logo-text">智慧养殖监管平台</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<a-menu
|
||||
v-model:selectedKeys="selectedKeys"
|
||||
v-model:openKeys="openKeys"
|
||||
mode="inline"
|
||||
theme="light"
|
||||
@click="handleMenuClick"
|
||||
>
|
||||
<!-- 主要功能模块 -->
|
||||
<a-menu-item-group title="核心功能">
|
||||
<a-menu-item key="/" :icon="h(HomeOutlined)">
|
||||
<router-link to="/">首页</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/dashboard" :icon="h(DashboardOutlined)">
|
||||
<router-link to="/dashboard">系统概览</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/monitor" :icon="h(LineChartOutlined)">
|
||||
<router-link to="/monitor">实时监控</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/analytics" :icon="h(BarChartOutlined)">
|
||||
<router-link to="/analytics">数据分析</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
|
||||
<!-- 管理功能模块 -->
|
||||
<a-menu-item-group title="管理功能">
|
||||
<a-menu-item key="/farms" :icon="h(HomeOutlined)">
|
||||
<router-link to="/farms">牛只管理</router-link>
|
||||
</a-menu-item>
|
||||
<a-sub-menu key="cattle-management" :icon="h(BugOutlined)">
|
||||
<template #title>牛只管理</template>
|
||||
<a-menu-item key="/cattle-management/archives">
|
||||
<router-link to="/cattle-management/archives">牛只档案</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/cattle-management/pens">
|
||||
<router-link to="/cattle-management/pens">栏舍设置</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/cattle-management/batches">
|
||||
<router-link to="/cattle-management/batches">批次设置</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/cattle-management/transfer-records">
|
||||
<router-link to="/cattle-management/transfer-records">转栏记录</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/cattle-management/exit-records">
|
||||
<router-link to="/cattle-management/exit-records">离栏记录</router-link>
|
||||
</a-menu-item>
|
||||
</a-sub-menu>
|
||||
<a-menu-item key="/devices" :icon="h(DesktopOutlined)">
|
||||
<router-link to="/devices">设备管理</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/alerts" :icon="h(AlertOutlined)">
|
||||
<router-link to="/alerts">预警管理</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group>
|
||||
|
||||
<!-- 业务功能模块 -->
|
||||
<!-- <a-menu-item-group title="业务功能">
|
||||
<a-menu-item key="/products" :icon="h(ShoppingOutlined)">
|
||||
<router-link to="/products">产品管理</router-link>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="/orders" :icon="h(ShoppingCartOutlined)">
|
||||
<router-link to="/orders">订单管理</router-link>
|
||||
</a-menu-item> -->
|
||||
<!-- <a-menu-item key="/reports" :icon="h(FileTextOutlined)">
|
||||
<router-link to="/reports">报表管理</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group> -->
|
||||
|
||||
<!-- 系统管理模块 -->
|
||||
<!-- <a-menu-item-group title="系统管理" v-if="userStore.userData?.roles?.includes('admin')">
|
||||
<a-menu-item key="/users" :icon="h(UserOutlined)">
|
||||
<router-link to="/users">用户管理</router-link>
|
||||
</a-menu-item>
|
||||
</a-menu-item-group> -->
|
||||
</a-menu>
|
||||
</div>
|
||||
|
||||
<!-- 侧边栏底部 -->
|
||||
<div class="sidebar-footer">
|
||||
<div class="user-info">
|
||||
<a-avatar size="small" :icon="h(UserOutlined)" />
|
||||
<span class="username">{{ userStore.userData?.username }}</span>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
v2.1.0
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch, h } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
MenuOutlined,
|
||||
CloseOutlined,
|
||||
UserOutlined,
|
||||
SettingOutlined,
|
||||
LogoutOutlined,
|
||||
HomeOutlined,
|
||||
DashboardOutlined,
|
||||
LineChartOutlined,
|
||||
BarChartOutlined,
|
||||
BugOutlined,
|
||||
DesktopOutlined,
|
||||
AlertOutlined,
|
||||
ShoppingOutlined,
|
||||
ShoppingCartOutlined,
|
||||
FileTextOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
// Store 和路由
|
||||
const userStore = useUserStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
// 响应式数据
|
||||
const sidebarVisible = ref(false)
|
||||
const selectedKeys = ref([])
|
||||
const openKeys = ref([])
|
||||
|
||||
// 计算当前页面标题
|
||||
const currentPageTitle = computed(() => {
|
||||
const titleMap = {
|
||||
'/': '首页',
|
||||
'/dashboard': '系统概览',
|
||||
'/monitor': '实时监控',
|
||||
'/analytics': '数据分析',
|
||||
'/farms': '养殖场管理',
|
||||
'/cattle-management/archives': '牛只档案',
|
||||
'/devices': '设备管理',
|
||||
'/alerts': '预警管理',
|
||||
'/products': '产品管理',
|
||||
'/orders': '订单管理',
|
||||
'/reports': '报表管理',
|
||||
'/users': '用户管理'
|
||||
}
|
||||
return titleMap[route.path] || '智慧养殖监管平台'
|
||||
})
|
||||
|
||||
// 监听路由变化,更新选中的菜单项
|
||||
watch(() => route.path, (newPath) => {
|
||||
selectedKeys.value = [newPath]
|
||||
closeSidebar() // 移动端路由跳转后自动关闭侧边栏
|
||||
}, { immediate: true })
|
||||
|
||||
// 切换侧边栏显示状态
|
||||
const toggleSidebar = () => {
|
||||
sidebarVisible.value = !sidebarVisible.value
|
||||
}
|
||||
|
||||
// 关闭侧边栏
|
||||
const closeSidebar = () => {
|
||||
sidebarVisible.value = false
|
||||
}
|
||||
|
||||
// 处理菜单点击
|
||||
const handleMenuClick = ({ key }) => {
|
||||
if (key !== route.path) {
|
||||
router.push(key)
|
||||
}
|
||||
closeSidebar()
|
||||
}
|
||||
|
||||
// 处理退出登录
|
||||
const handleLogout = async () => {
|
||||
try {
|
||||
await userStore.logout()
|
||||
message.success('退出登录成功')
|
||||
router.push('/login')
|
||||
} catch (error) {
|
||||
console.error('退出登录失败:', error)
|
||||
message.error('退出登录失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
defineExpose({
|
||||
toggleSidebar,
|
||||
closeSidebar
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.mobile-nav {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* 移动端头部样式 */
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.mobile-menu-button:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
margin: 0 12px;
|
||||
}
|
||||
|
||||
.mobile-user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* 侧边栏遮罩 */
|
||||
.mobile-sidebar-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.45);
|
||||
z-index: 1001;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
/* 移动端侧边栏 */
|
||||
.mobile-sidebar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 280px;
|
||||
background: #fff;
|
||||
border-right: 1px solid #f0f0f0;
|
||||
z-index: 1002;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.mobile-sidebar.sidebar-open {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 侧边栏头部 */
|
||||
.sidebar-header {
|
||||
padding: 20px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
background: linear-gradient(135deg, #1890ff 0%, #722ed1 100%);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.logo img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.logo-text {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
/* 侧边栏内容 */
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-menu) {
|
||||
border: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-group-title) {
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item) {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
margin: 0;
|
||||
border-radius: 0;
|
||||
padding: 0 16px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item:hover) {
|
||||
background: #f0f8ff;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-selected) {
|
||||
background: #e6f7ff !important;
|
||||
border-right: 3px solid #1890ff;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item a) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item-icon) {
|
||||
font-size: 16px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
/* 侧边栏底部 */
|
||||
.sidebar-footer {
|
||||
padding: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-size: 14px;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.version-info {
|
||||
font-size: 12px;
|
||||
color: #8c8c8c;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 只在移动端显示 */
|
||||
@media (min-width: 769px) {
|
||||
.mobile-nav {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 响应式断点调整 */
|
||||
@media (max-width: 480px) {
|
||||
.mobile-sidebar {
|
||||
width: 100vw;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 横屏模式调整 */
|
||||
@media (max-width: 768px) and (orientation: landscape) {
|
||||
.mobile-header {
|
||||
padding: 8px 16px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
:deep(.ant-menu-item) {
|
||||
height: 40px;
|
||||
line-height: 40px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
75
admin-system/frontend/src/components/PermissionGuard.vue
Normal file
75
admin-system/frontend/src/components/PermissionGuard.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<template>
|
||||
<div v-if="hasAccess">
|
||||
<slot />
|
||||
</div>
|
||||
<div v-else-if="showFallback" class="permission-denied">
|
||||
<a-result
|
||||
status="403"
|
||||
title="权限不足"
|
||||
sub-title="抱歉,您没有访问此功能的权限。"
|
||||
>
|
||||
<template #extra>
|
||||
<a-button type="primary" @click="$router.push('/dashboard')">
|
||||
返回首页
|
||||
</a-button>
|
||||
</template>
|
||||
</a-result>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
import { useUserStore } from '../stores/user'
|
||||
|
||||
const props = defineProps({
|
||||
// 需要的权限
|
||||
permission: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
// 需要的角色
|
||||
role: {
|
||||
type: [String, Array],
|
||||
default: null
|
||||
},
|
||||
// 菜单键
|
||||
menu: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
// 是否显示无权限时的fallback内容
|
||||
showFallback: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
const hasAccess = computed(() => {
|
||||
// 如果指定了权限要求
|
||||
if (props.permission) {
|
||||
return userStore.hasPermission(props.permission)
|
||||
}
|
||||
|
||||
// 如果指定了角色要求
|
||||
if (props.role) {
|
||||
return userStore.hasRole(props.role)
|
||||
}
|
||||
|
||||
// 如果指定了菜单要求
|
||||
if (props.menu) {
|
||||
return userStore.canAccessMenu(props.menu)
|
||||
}
|
||||
|
||||
// 默认允许访问
|
||||
return true
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.permission-denied {
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
@@ -6,11 +6,16 @@
|
||||
// 百度地图API配置
|
||||
export const BAIDU_MAP_CONFIG = {
|
||||
// 百度地图API密钥
|
||||
// 注意:实际项目中应该使用环境变量存储密钥,而不是硬编码
|
||||
// 从环境变量读取API密钥,如果没有则使用开发测试密钥
|
||||
// 生产环境请设置 VITE_BAIDU_MAP_API_KEY 环境变量
|
||||
// 请访问 http://lbsyun.baidu.com/apiconsole/key 申请有效的API密钥
|
||||
apiKey: import.meta.env.VITE_BAIDU_MAP_API_KEY || '3AN3VahoqaXUs32U8luXD2Dwn86KK5B7',
|
||||
apiKey: import.meta.env.VITE_BAIDU_MAP_API_KEY || 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo',
|
||||
|
||||
// 备用API密钥(用于不同环境)
|
||||
fallbackApiKey: import.meta.env.VITE_BAIDU_MAP_FALLBACK_KEY || 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo',
|
||||
|
||||
// 是否启用Referer校验(开发环境建议关闭)
|
||||
enableRefererCheck: import.meta.env.VITE_BAIDU_MAP_ENABLE_REFERER !== 'false',
|
||||
|
||||
// 默认中心点(宁夏中心位置)
|
||||
defaultCenter: {
|
||||
@@ -19,16 +24,34 @@ export const BAIDU_MAP_CONFIG = {
|
||||
},
|
||||
|
||||
// 默认缩放级别
|
||||
defaultZoom: 8
|
||||
defaultZoom: 8,
|
||||
|
||||
// 重试次数
|
||||
maxRetries: 2,
|
||||
|
||||
// 重试延迟(毫秒)
|
||||
retryDelay: 2000
|
||||
};
|
||||
|
||||
// API服务配置
|
||||
export const API_CONFIG = {
|
||||
// API基础URL
|
||||
baseUrl: 'http://localhost:5350/api',
|
||||
// API基础URL - 支持环境变量配置
|
||||
baseUrl: import.meta.env.VITE_API_BASE_URL || '/api',
|
||||
|
||||
// 完整API URL - 用于直接调用
|
||||
fullBaseUrl: import.meta.env.VITE_API_FULL_URL || 'http://localhost:5350/api',
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
timeout: 10000
|
||||
timeout: parseInt(import.meta.env.VITE_API_TIMEOUT) || 10000,
|
||||
|
||||
// 是否启用代理
|
||||
useProxy: import.meta.env.VITE_USE_PROXY !== 'false',
|
||||
|
||||
// 开发环境配置
|
||||
isDev: import.meta.env.DEV,
|
||||
|
||||
// 生产环境配置
|
||||
isProd: import.meta.env.PROD
|
||||
};
|
||||
|
||||
// 其他环境配置
|
||||
|
||||
@@ -6,6 +6,7 @@ import Antd from 'ant-design-vue'
|
||||
import 'ant-design-vue/dist/reset.css'
|
||||
import { themeConfig } from './styles/theme.js'
|
||||
import './styles/global.css'
|
||||
import './styles/responsive.css'
|
||||
import { useUserStore } from './stores/user.js'
|
||||
|
||||
// 导入图标组件
|
||||
|
||||
@@ -24,20 +24,17 @@ router.beforeEach(async (to, from, next) => {
|
||||
// 获取用户存储
|
||||
const userStore = useUserStore()
|
||||
|
||||
// 如果访问登录页面且已有token,尝试自动登录
|
||||
if (to.path === '/login' && userStore.token) {
|
||||
try {
|
||||
// 验证token是否有效
|
||||
const isValid = await userStore.validateToken()
|
||||
if (isValid) {
|
||||
// token有效,直接跳转到目标页面或仪表盘
|
||||
const redirectPath = to.query.redirect || '/dashboard'
|
||||
next(redirectPath)
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Token验证失败,继续到登录页面')
|
||||
}
|
||||
// 处理旧版本路由重定向
|
||||
if (to.path === '/admin' || to.path === '/admin/') {
|
||||
next('/dashboard')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果访问登录页面且已有有效token,重定向到仪表盘
|
||||
if (to.path === '/login' && userStore.token && userStore.isLoggedIn) {
|
||||
const redirectPath = to.query.redirect || '/dashboard'
|
||||
next(redirectPath)
|
||||
return
|
||||
}
|
||||
|
||||
// 检查该路由是否需要登录权限
|
||||
|
||||
@@ -5,14 +5,28 @@
|
||||
|
||||
// 主布局路由
|
||||
export const mainRoutes = [
|
||||
// 根路径重定向到仪表盘
|
||||
{
|
||||
path: '/',
|
||||
name: 'Home',
|
||||
component: () => import('../views/Home.vue'),
|
||||
redirect: '/dashboard',
|
||||
meta: {
|
||||
title: '首页',
|
||||
requiresAuth: true,
|
||||
icon: 'home-outlined'
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
// 确保 /admin/ 路径也重定向到仪表盘(兼容旧版本)
|
||||
{
|
||||
path: '/admin/',
|
||||
redirect: '/dashboard',
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
// 确保 /admin 路径也重定向到仪表盘(兼容旧版本)
|
||||
{
|
||||
path: '/admin',
|
||||
redirect: '/dashboard',
|
||||
meta: {
|
||||
requiresAuth: true
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -26,96 +40,312 @@ export const mainRoutes = [
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/analytics',
|
||||
name: 'Analytics',
|
||||
component: () => import('../views/Analytics.vue'),
|
||||
path: '/smart-devices',
|
||||
name: 'SmartDevices',
|
||||
redirect: '/smart-devices/eartag',
|
||||
meta: {
|
||||
title: '数据分析',
|
||||
title: '智能设备',
|
||||
requiresAuth: true,
|
||||
icon: 'bar-chart-outlined'
|
||||
}
|
||||
icon: 'SettingOutlined',
|
||||
permission: 'smart_device:view',
|
||||
menu: 'smart_device.main',
|
||||
isSubmenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/smart-devices/eartag',
|
||||
name: 'SmartEartag',
|
||||
component: () => import('../views/SmartEartag.vue'),
|
||||
meta: {
|
||||
title: '智能耳标',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_eartag:view',
|
||||
menu: 'smart_device.eartag',
|
||||
parent: 'SmartDevices'
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
path: '/smart-devices/collar',
|
||||
name: 'SmartCollar',
|
||||
component: () => import('../views/SmartCollar.vue'),
|
||||
meta: {
|
||||
title: '智能项圈',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_collar:view',
|
||||
menu: 'smart_device.collar',
|
||||
parent: 'SmartDevices'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/smart-devices/host',
|
||||
name: 'SmartHost',
|
||||
component: () => import('../views/SmartHost.vue'),
|
||||
meta: {
|
||||
title: '智能主机',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_host:view',
|
||||
menu: 'smart_device.host',
|
||||
parent: 'SmartDevices'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/smart-devices/fence',
|
||||
name: 'ElectronicFence',
|
||||
component: () => import('../views/ElectronicFence.vue'),
|
||||
meta: {
|
||||
title: '电子围栏',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_fence:view',
|
||||
menu: 'smart_device.fence',
|
||||
parent: 'SmartDevices'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/monitor',
|
||||
name: 'Monitor',
|
||||
component: () => import('../views/Monitor.vue'),
|
||||
path: '/smart-alerts',
|
||||
name: 'SmartAlerts',
|
||||
redirect: '/smart-alerts/eartag',
|
||||
meta: {
|
||||
title: '实时监控',
|
||||
title: '智能预警总览',
|
||||
requiresAuth: true,
|
||||
icon: 'line-chart-outlined'
|
||||
}
|
||||
icon: 'AlertOutlined',
|
||||
permission: 'smart_alert:view',
|
||||
menu: 'smart_alert.main',
|
||||
isSubmenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/smart-alerts/eartag',
|
||||
name: 'SmartEartagAlert',
|
||||
component: () => import('../views/SmartEartagAlert.vue'),
|
||||
meta: {
|
||||
title: '智能耳标预警',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_eartag_alert:view',
|
||||
menu: 'smart_alert.eartag',
|
||||
parent: 'SmartAlerts'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/smart-alerts/collar',
|
||||
name: 'SmartCollarAlert',
|
||||
component: () => import('../views/SmartCollarAlert.vue'),
|
||||
meta: {
|
||||
title: '智能项圈预警',
|
||||
requiresAuth: true,
|
||||
permission: 'smart_collar_alert:view',
|
||||
menu: 'smart_alert.collar',
|
||||
parent: 'SmartAlerts'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/cattle-management',
|
||||
name: 'CattleManagement',
|
||||
redirect: '/cattle-management/archives',
|
||||
meta: {
|
||||
title: '牛只管理',
|
||||
requiresAuth: true,
|
||||
icon: 'BugOutlined',
|
||||
permission: 'animal:view',
|
||||
menu: 'animal.management',
|
||||
isSubmenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/cattle-management/archives',
|
||||
name: 'CattleArchives',
|
||||
component: () => import('../views/Animals.vue'),
|
||||
meta: {
|
||||
title: '牛只档案',
|
||||
requiresAuth: true,
|
||||
permission: 'cattle:archives:view',
|
||||
menu: 'cattle.archives',
|
||||
parent: 'CattleManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cattle-management/pens',
|
||||
name: 'CattlePens',
|
||||
component: () => import('../views/CattlePens.vue'),
|
||||
meta: {
|
||||
title: '栏舍设置',
|
||||
requiresAuth: true,
|
||||
permission: 'cattle:pens:view',
|
||||
menu: 'cattle.pens',
|
||||
parent: 'CattleManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cattle-management/batches',
|
||||
name: 'CattleBatches',
|
||||
component: () => import('../views/CattleBatches.vue'),
|
||||
meta: {
|
||||
title: '批次设置',
|
||||
requiresAuth: true,
|
||||
permission: 'cattle:batches:view',
|
||||
menu: 'cattle.batches',
|
||||
parent: 'CattleManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cattle-management/transfer-records',
|
||||
name: 'CattleTransferRecords',
|
||||
component: () => import('../views/CattleTransferRecords.vue'),
|
||||
meta: {
|
||||
title: '转栏记录',
|
||||
requiresAuth: true,
|
||||
permission: 'cattle:transfer:view',
|
||||
menu: 'cattle.transfer',
|
||||
parent: 'CattleManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/cattle-management/exit-records',
|
||||
name: 'CattleExitRecords',
|
||||
component: () => import('../views/CattleExitRecords.vue'),
|
||||
meta: {
|
||||
title: '离栏记录',
|
||||
requiresAuth: true,
|
||||
permission: 'cattle:exit:view',
|
||||
menu: 'cattle.exit',
|
||||
parent: 'CattleManagement'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/users',
|
||||
name: 'Users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
requiresAuth: true,
|
||||
icon: 'user-outlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/products',
|
||||
name: 'Products',
|
||||
component: () => import('../views/Products.vue'),
|
||||
meta: {
|
||||
title: '产品管理',
|
||||
requiresAuth: true,
|
||||
icon: 'shopping-outlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/orders',
|
||||
name: 'Orders',
|
||||
component: () => import('../views/Orders.vue'),
|
||||
meta: {
|
||||
title: '订单管理',
|
||||
requiresAuth: true,
|
||||
icon: 'ShoppingCartOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/devices',
|
||||
name: 'Devices',
|
||||
component: () => import('../views/Devices.vue'),
|
||||
meta: {
|
||||
title: '设备管理',
|
||||
requiresAuth: true,
|
||||
icon: 'DesktopOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/animals',
|
||||
name: 'Animals',
|
||||
component: () => import('../views/Animals.vue'),
|
||||
meta: {
|
||||
title: '动物管理',
|
||||
requiresAuth: true,
|
||||
icon: 'BugOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/alerts',
|
||||
name: 'Alerts',
|
||||
component: () => import('../views/Alerts.vue'),
|
||||
meta: {
|
||||
title: '预警管理',
|
||||
requiresAuth: true,
|
||||
icon: 'AlertOutlined'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/farms',
|
||||
name: 'Farms',
|
||||
component: () => import('../views/Farms.vue'),
|
||||
path: '/farm-management',
|
||||
name: 'FarmManagement',
|
||||
redirect: '/farm-management/info',
|
||||
meta: {
|
||||
title: '养殖场管理',
|
||||
requiresAuth: true,
|
||||
icon: 'HomeOutlined'
|
||||
}
|
||||
}
|
||||
icon: 'HomeOutlined',
|
||||
permission: 'farm:view',
|
||||
menu: 'farm.main',
|
||||
isSubmenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/farm-management/info',
|
||||
name: 'FarmInfoManagement',
|
||||
component: () => import('../views/FarmInfoManagement.vue'),
|
||||
meta: {
|
||||
title: '养殖场信息管理',
|
||||
requiresAuth: true,
|
||||
permission: 'farm_info:view',
|
||||
menu: 'farm.info',
|
||||
parent: 'FarmManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/farm-management/pens',
|
||||
name: 'PenManagement',
|
||||
component: () => import('../views/PenManagement.vue'),
|
||||
meta: {
|
||||
title: '栏舍管理',
|
||||
requiresAuth: true,
|
||||
permission: 'pen:view',
|
||||
menu: 'farm.pens',
|
||||
parent: 'FarmManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/farm-management/users',
|
||||
name: 'Users',
|
||||
component: () => import('../views/Users.vue'),
|
||||
meta: {
|
||||
title: '用户管理',
|
||||
requiresAuth: true,
|
||||
permission: 'user:view',
|
||||
menu: 'farm.users',
|
||||
parent: 'FarmManagement'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/farm-management/role-permissions',
|
||||
name: 'RolePermissions',
|
||||
component: () => import('../views/RolePermissions.vue'),
|
||||
meta: {
|
||||
title: '角色权限管理',
|
||||
requiresAuth: true,
|
||||
permission: 'role:view',
|
||||
menu: 'farm.role_permissions',
|
||||
parent: 'FarmManagement'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
path: '/system',
|
||||
name: 'System',
|
||||
redirect: '/system/config',
|
||||
meta: {
|
||||
title: '系统管理',
|
||||
requiresAuth: true,
|
||||
icon: 'setting-outlined',
|
||||
permission: 'system:view',
|
||||
menu: 'system.main',
|
||||
isSubmenu: true
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: '/system/config',
|
||||
name: 'SystemConfig',
|
||||
component: () => import('../views/System.vue'),
|
||||
meta: {
|
||||
title: '系统配置',
|
||||
requiresAuth: true,
|
||||
permission: 'system:config',
|
||||
menu: 'system.config',
|
||||
parent: 'System'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/logs',
|
||||
name: 'FormLogManagement',
|
||||
component: () => import('../views/FormLogManagement.vue'),
|
||||
meta: {
|
||||
title: '表单日志管理',
|
||||
requiresAuth: true,
|
||||
permission: 'log:view',
|
||||
menu: 'system.logs',
|
||||
parent: 'System'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/search-monitor',
|
||||
name: 'SearchMonitor',
|
||||
component: () => import('../views/SearchMonitor.vue'),
|
||||
meta: {
|
||||
title: '搜索监听数据查询',
|
||||
requiresAuth: true,
|
||||
permission: 'log:view',
|
||||
menu: 'system.search_monitor',
|
||||
parent: 'System'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/system/operation-logs',
|
||||
name: 'OperationLogs',
|
||||
component: () => import('../views/OperationLogs.vue'),
|
||||
meta: {
|
||||
title: '操作日志',
|
||||
requiresAuth: true,
|
||||
permission: 'operation_log:view',
|
||||
menu: 'system.operation_logs',
|
||||
parent: 'System'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
]
|
||||
|
||||
// 认证相关路由
|
||||
|
||||
@@ -7,6 +7,10 @@ export const useDataStore = defineStore('data', () => {
|
||||
const animals = ref([])
|
||||
const devices = ref([])
|
||||
const alerts = ref([])
|
||||
const pens = ref([])
|
||||
const cattle = ref([])
|
||||
const smartDevices = ref([])
|
||||
const smartAlerts = ref({ totalAlerts: 0, eartagAlerts: 0, collarAlerts: 0 })
|
||||
const stats = ref({
|
||||
farmGrowth: 0,
|
||||
animalGrowth: 0,
|
||||
@@ -19,24 +23,30 @@ export const useDataStore = defineStore('data', () => {
|
||||
animals: false,
|
||||
devices: false,
|
||||
alerts: false,
|
||||
pens: false,
|
||||
cattle: false,
|
||||
smartDevices: false,
|
||||
smartAlerts: false,
|
||||
stats: false
|
||||
})
|
||||
|
||||
// 计算属性
|
||||
const farmCount = computed(() => farms.value.length)
|
||||
const animalCount = computed(() => animals.value.length)
|
||||
const deviceCount = computed(() => devices.value.length)
|
||||
const alertCount = computed(() => alerts.value.length)
|
||||
const animalCount = computed(() => Array.isArray(cattle.value) ? cattle.value.length : 0) // 使用牛只数据
|
||||
const deviceCount = computed(() => smartAlerts.value?.eartagDevices || 0) // 使用智能设备统计数据
|
||||
const alertCount = computed(() => smartAlerts.value?.eartagAlerts || 0) // 使用智能耳标预警数据
|
||||
const penCount = computed(() => Array.isArray(pens.value) ? pens.value.length : 0)
|
||||
|
||||
// 在线设备数量
|
||||
const onlineDeviceCount = computed(() =>
|
||||
devices.value.filter(device => device.status === 'online').length
|
||||
)
|
||||
const onlineDeviceCount = computed(() => {
|
||||
if (!Array.isArray(smartDevices.value)) return 0
|
||||
return smartDevices.value.filter(device => device.state === 1).length
|
||||
})
|
||||
|
||||
// 设备在线率
|
||||
const deviceOnlineRate = computed(() => {
|
||||
if (devices.value.length === 0) return 0
|
||||
return (onlineDeviceCount.value / devices.value.length * 100).toFixed(1)
|
||||
if (!Array.isArray(smartDevices.value) || smartDevices.value.length === 0) return 0
|
||||
return (onlineDeviceCount.value / smartDevices.value.length * 100).toFixed(1)
|
||||
})
|
||||
|
||||
// 获取养殖场数据
|
||||
@@ -112,6 +122,101 @@ export const useDataStore = defineStore('data', () => {
|
||||
loading.value.alerts = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取栏舍数据(基于cattle_pens表)
|
||||
async function fetchPens() {
|
||||
loading.value.pens = true
|
||||
try {
|
||||
console.log('开始获取栏舍数据(cattle_pens表)...')
|
||||
// 导入数据服务
|
||||
const { penService } = await import('../utils/dataService')
|
||||
const data = await penService.getAllPens()
|
||||
|
||||
console.log('栏舍API返回数据:', data)
|
||||
|
||||
// penService.getAllPens()通过api.get返回的是result.data.list,直接使用
|
||||
pens.value = data || []
|
||||
console.log('设置pens.value:', pens.value.length, '条记录')
|
||||
} catch (error) {
|
||||
console.error('获取栏舍数据失败:', error)
|
||||
pens.value = []
|
||||
} finally {
|
||||
loading.value.pens = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取牛只数据(基于iot_cattle表)
|
||||
async function fetchCattle() {
|
||||
loading.value.cattle = true
|
||||
try {
|
||||
console.log('开始获取牛只数据(iot_cattle表)...')
|
||||
// 导入数据服务
|
||||
const { cattleService } = await import('../utils/dataService')
|
||||
const data = await cattleService.getAllCattle()
|
||||
|
||||
console.log('牛只API返回数据:', data)
|
||||
|
||||
// cattleService.getAllCattle()通过api.get返回的是result.data.list,直接使用
|
||||
cattle.value = data || []
|
||||
console.log('设置cattle.value:', cattle.value.length, '条记录')
|
||||
} catch (error) {
|
||||
console.error('获取牛只数据失败:', error)
|
||||
cattle.value = []
|
||||
} finally {
|
||||
loading.value.cattle = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取智能设备数据(基于iot_jbq_client表)
|
||||
async function fetchSmartDevices() {
|
||||
loading.value.smartDevices = true
|
||||
try {
|
||||
console.log('开始获取智能设备数据(iot_jbq_client表)...')
|
||||
// 导入数据服务
|
||||
const { smartDeviceService } = await import('../utils/dataService')
|
||||
const data = await smartDeviceService.getAllSmartDevices()
|
||||
|
||||
console.log('智能设备API返回数据:', data)
|
||||
|
||||
// 确保smartDevices.value始终是数组
|
||||
if (Array.isArray(data)) {
|
||||
smartDevices.value = data
|
||||
} else if (data && Array.isArray(data.list)) {
|
||||
smartDevices.value = data.list
|
||||
} else {
|
||||
console.warn('智能设备数据格式不正确:', data)
|
||||
smartDevices.value = []
|
||||
}
|
||||
console.log('设置smartDevices.value:', smartDevices.value.length, '条记录')
|
||||
} catch (error) {
|
||||
console.error('获取智能设备数据失败:', error)
|
||||
smartDevices.value = []
|
||||
} finally {
|
||||
loading.value.smartDevices = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取智能预警数据
|
||||
async function fetchSmartAlerts() {
|
||||
loading.value.smartAlerts = true
|
||||
try {
|
||||
console.log('开始获取智能预警数据...')
|
||||
// 导入数据服务
|
||||
const { smartAlertService } = await import('../utils/dataService')
|
||||
console.log('智能预警服务导入成功')
|
||||
const data = await smartAlertService.getSmartAlertStats()
|
||||
console.log('智能预警API响应:', data)
|
||||
|
||||
// smartAlertService.getSmartAlertStats()通过api.get返回的是result.data,直接使用
|
||||
smartAlerts.value = data || { totalAlerts: 0, eartagAlerts: 0, collarAlerts: 0 }
|
||||
console.log('智能预警数据设置完成:', smartAlerts.value)
|
||||
} catch (error) {
|
||||
console.error('获取智能预警数据失败:', error)
|
||||
smartAlerts.value = { totalAlerts: 0, eartagAlerts: 0, collarAlerts: 0 }
|
||||
} finally {
|
||||
loading.value.smartAlerts = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取统计数据
|
||||
async function fetchStats() {
|
||||
@@ -147,6 +252,10 @@ export const useDataStore = defineStore('data', () => {
|
||||
fetchAnimals(),
|
||||
fetchDevices(),
|
||||
fetchAlerts(),
|
||||
fetchPens(),
|
||||
fetchCattle(),
|
||||
fetchSmartDevices(),
|
||||
fetchSmartAlerts(),
|
||||
fetchStats()
|
||||
])
|
||||
|
||||
@@ -154,7 +263,11 @@ export const useDataStore = defineStore('data', () => {
|
||||
farms: farms.value.length,
|
||||
animals: animals.value.length,
|
||||
devices: devices.value.length,
|
||||
alerts: alerts.value.length
|
||||
alerts: alerts.value.length,
|
||||
pens: pens.value.length,
|
||||
cattle: cattle.value.length,
|
||||
smartDevices: smartDevices.value.length,
|
||||
smartAlerts: smartAlerts.value.totalAlerts
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('数据加载过程中出现错误:', error)
|
||||
@@ -162,12 +275,66 @@ export const useDataStore = defineStore('data', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 实时数据更新方法(WebSocket调用)
|
||||
function updateDeviceRealtime(deviceData) {
|
||||
const index = devices.value.findIndex(device => device.id === deviceData.id)
|
||||
if (index !== -1) {
|
||||
// 更新现有设备数据
|
||||
devices.value[index] = { ...devices.value[index], ...deviceData }
|
||||
console.log(`设备 ${deviceData.id} 实时数据已更新`)
|
||||
} else {
|
||||
// 如果是新设备,添加到列表
|
||||
devices.value.push(deviceData)
|
||||
console.log(`新设备 ${deviceData.id} 已添加`)
|
||||
}
|
||||
}
|
||||
|
||||
function addNewAlert(alertData) {
|
||||
// 添加新预警到列表顶部
|
||||
alerts.value.unshift(alertData)
|
||||
console.log(`新预警 ${alertData.id} 已添加`)
|
||||
}
|
||||
|
||||
function updateAnimalRealtime(animalData) {
|
||||
const index = animals.value.findIndex(animal => animal.id === animalData.id)
|
||||
if (index !== -1) {
|
||||
// 更新现有动物数据
|
||||
animals.value[index] = { ...animals.value[index], ...animalData }
|
||||
console.log(`动物 ${animalData.id} 实时数据已更新`)
|
||||
} else {
|
||||
// 如果是新动物记录,添加到列表
|
||||
animals.value.push(animalData)
|
||||
console.log(`新动物记录 ${animalData.id} 已添加`)
|
||||
}
|
||||
}
|
||||
|
||||
function updateStatsRealtime(statsData) {
|
||||
// 更新统计数据
|
||||
stats.value = { ...stats.value, ...statsData }
|
||||
console.log('系统统计数据已实时更新')
|
||||
}
|
||||
|
||||
function updateAlertStatus(alertId, status) {
|
||||
const index = alerts.value.findIndex(alert => alert.id === alertId)
|
||||
if (index !== -1) {
|
||||
alerts.value[index].status = status
|
||||
if (status === 'resolved') {
|
||||
alerts.value[index].resolved_at = new Date()
|
||||
}
|
||||
console.log(`预警 ${alertId} 状态已更新为: ${status}`)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
farms,
|
||||
animals,
|
||||
devices,
|
||||
alerts,
|
||||
pens,
|
||||
cattle,
|
||||
smartDevices,
|
||||
smartAlerts,
|
||||
stats,
|
||||
loading,
|
||||
|
||||
@@ -176,15 +343,27 @@ export const useDataStore = defineStore('data', () => {
|
||||
animalCount,
|
||||
deviceCount,
|
||||
alertCount,
|
||||
penCount,
|
||||
onlineDeviceCount,
|
||||
deviceOnlineRate,
|
||||
|
||||
// 方法
|
||||
// 数据获取方法
|
||||
fetchFarms,
|
||||
fetchAnimals,
|
||||
fetchDevices,
|
||||
fetchAlerts,
|
||||
fetchPens,
|
||||
fetchCattle,
|
||||
fetchSmartDevices,
|
||||
fetchSmartAlerts,
|
||||
fetchStats,
|
||||
fetchAllData
|
||||
fetchAllData,
|
||||
|
||||
// 实时数据更新方法
|
||||
updateDeviceRealtime,
|
||||
addNewAlert,
|
||||
updateAnimalRealtime,
|
||||
updateStatsRealtime,
|
||||
updateAlertStatus
|
||||
}
|
||||
})
|
||||
@@ -58,13 +58,20 @@ export const useUserStore = defineStore('user', () => {
|
||||
if (result.success && result.token) {
|
||||
token.value = result.token;
|
||||
userData.value = {
|
||||
username: username,
|
||||
email: result.user?.email || `${username}@example.com`
|
||||
id: result.user?.id,
|
||||
username: result.user?.username || username,
|
||||
email: result.user?.email || `${username}@example.com`,
|
||||
role: result.role || result.user?.role,
|
||||
permissions: result.permissions || result.user?.permissions || [],
|
||||
accessibleMenus: result.accessibleMenus || result.user?.accessibleMenus || []
|
||||
};
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('token', result.token);
|
||||
localStorage.setItem('user', JSON.stringify(userData.value));
|
||||
|
||||
// 建立WebSocket连接
|
||||
await connectWebSocket();
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -79,8 +86,44 @@ export const useUserStore = defineStore('user', () => {
|
||||
}
|
||||
}
|
||||
|
||||
// WebSocket连接状态
|
||||
const isWebSocketConnected = ref(false)
|
||||
|
||||
// 建立WebSocket连接
|
||||
async function connectWebSocket() {
|
||||
if (!token.value) {
|
||||
console.log('无token,跳过WebSocket连接')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const webSocketService = await import('../utils/websocketService')
|
||||
webSocketService.default.connect(token.value)
|
||||
isWebSocketConnected.value = true
|
||||
console.log('WebSocket连接已建立')
|
||||
} catch (error) {
|
||||
console.error('WebSocket连接失败:', error)
|
||||
isWebSocketConnected.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 断开WebSocket连接
|
||||
async function disconnectWebSocket() {
|
||||
try {
|
||||
const webSocketService = await import('../utils/websocketService')
|
||||
webSocketService.default.disconnect()
|
||||
isWebSocketConnected.value = false
|
||||
console.log('WebSocket连接已断开')
|
||||
} catch (error) {
|
||||
console.error('断开WebSocket连接失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 登出操作
|
||||
function logout() {
|
||||
async function logout() {
|
||||
// 断开WebSocket连接
|
||||
await disconnectWebSocket()
|
||||
|
||||
token.value = ''
|
||||
userData.value = null
|
||||
|
||||
@@ -95,14 +138,102 @@ export const useUserStore = defineStore('user', () => {
|
||||
localStorage.setItem('user', JSON.stringify(userData.value))
|
||||
}
|
||||
|
||||
// 权限检查方法
|
||||
function hasPermission(permission) {
|
||||
console.log('检查权限:', permission)
|
||||
console.log('用户数据:', userData.value)
|
||||
console.log('用户权限:', userData.value?.permissions)
|
||||
|
||||
if (!userData.value || !userData.value.permissions) {
|
||||
console.log('无用户数据或权限,返回false')
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(permission)) {
|
||||
const hasAny = permission.some(p => userData.value.permissions.includes(p))
|
||||
console.log('权限数组检查结果:', permission, hasAny)
|
||||
return hasAny
|
||||
}
|
||||
|
||||
const hasPerm = userData.value.permissions.includes(permission)
|
||||
console.log('单个权限检查结果:', permission, hasPerm)
|
||||
return hasPerm
|
||||
}
|
||||
|
||||
// 角色检查方法
|
||||
function hasRole(role) {
|
||||
console.log('检查角色:', role)
|
||||
console.log('用户角色:', userData.value?.role)
|
||||
|
||||
if (!userData.value || !userData.value.role) {
|
||||
console.log('无用户数据或角色,返回false')
|
||||
return false
|
||||
}
|
||||
|
||||
if (Array.isArray(role)) {
|
||||
const hasAny = role.includes(userData.value.role.name)
|
||||
console.log('角色数组检查结果:', role, hasAny)
|
||||
return hasAny
|
||||
}
|
||||
|
||||
const hasRole = userData.value.role.name === role
|
||||
console.log('单个角色检查结果:', role, hasRole)
|
||||
return hasRole
|
||||
}
|
||||
|
||||
// 检查是否可以访问菜单
|
||||
function canAccessMenu(menuKey) {
|
||||
console.log('检查菜单访问:', menuKey)
|
||||
console.log('可访问菜单:', userData.value?.accessibleMenus)
|
||||
|
||||
if (!userData.value) {
|
||||
console.log('无用户数据,返回false')
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果没有accessibleMenus数组,返回false
|
||||
if (!Array.isArray(userData.value.accessibleMenus)) {
|
||||
console.log('可访问菜单不是数组,返回false')
|
||||
return false
|
||||
}
|
||||
|
||||
// 如果accessibleMenus为空数组,返回false(没有菜单权限)
|
||||
if (userData.value.accessibleMenus.length === 0) {
|
||||
console.log('可访问菜单为空数组,返回false')
|
||||
return false
|
||||
}
|
||||
|
||||
const canAccess = userData.value.accessibleMenus.includes(menuKey)
|
||||
console.log('菜单访问检查结果:', menuKey, canAccess)
|
||||
return canAccess
|
||||
}
|
||||
|
||||
// 获取用户角色名称
|
||||
function getUserRoleName() {
|
||||
return userData.value?.role?.name || 'user'
|
||||
}
|
||||
|
||||
// 获取用户权限列表
|
||||
function getUserPermissions() {
|
||||
return userData.value?.permissions || []
|
||||
}
|
||||
|
||||
return {
|
||||
token,
|
||||
userData,
|
||||
isLoggedIn,
|
||||
isWebSocketConnected,
|
||||
checkLoginStatus,
|
||||
validateToken,
|
||||
login,
|
||||
logout,
|
||||
updateUserInfo
|
||||
updateUserInfo,
|
||||
connectWebSocket,
|
||||
disconnectWebSocket,
|
||||
hasPermission,
|
||||
hasRole,
|
||||
canAccessMenu,
|
||||
getUserRoleName,
|
||||
getUserPermissions
|
||||
}
|
||||
})
|
||||
719
admin-system/frontend/src/styles/responsive.css
Normal file
719
admin-system/frontend/src/styles/responsive.css
Normal file
@@ -0,0 +1,719 @@
|
||||
/**
|
||||
* 响应式设计样式
|
||||
* @file responsive.css
|
||||
* @description 提供全面的响应式设计支持,确保在各种设备上的良好体验
|
||||
*/
|
||||
|
||||
/* ========== 断点定义 ========== */
|
||||
:root {
|
||||
/* 屏幕断点 */
|
||||
--screen-xs: 480px; /* 超小屏幕 */
|
||||
--screen-sm: 576px; /* 小屏幕 */
|
||||
--screen-md: 768px; /* 中等屏幕 */
|
||||
--screen-lg: 992px; /* 大屏幕 */
|
||||
--screen-xl: 1200px; /* 超大屏幕 */
|
||||
--screen-xxl: 1600px; /* 超超大屏幕 */
|
||||
|
||||
/* 移动端优化变量 */
|
||||
--mobile-padding: 12px;
|
||||
--mobile-margin: 8px;
|
||||
--mobile-font-size: 14px;
|
||||
--mobile-button-height: 40px;
|
||||
--mobile-input-height: 40px;
|
||||
|
||||
/* 触摸友好的最小点击区域 */
|
||||
--touch-target-min: 44px;
|
||||
}
|
||||
|
||||
/* ========== 全局响应式基础 ========== */
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
overflow-x: hidden;
|
||||
-webkit-overflow-scrolling: touch; /* iOS 平滑滚动 */
|
||||
}
|
||||
|
||||
/* ========== 移动端基础布局 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 页面标题区域 */
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch !important;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
font-size: 20px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* 搜索区域移动端优化 */
|
||||
.search-area {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.search-buttons {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-buttons .ant-btn {
|
||||
flex: 1;
|
||||
height: var(--mobile-button-height);
|
||||
}
|
||||
|
||||
/* 表格移动端优化 */
|
||||
.ant-table-wrapper {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
.ant-table {
|
||||
min-width: 600px; /* 确保表格在小屏幕上可滚动 */
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 操作按钮移动端优化 */
|
||||
.table-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.table-actions .ant-btn {
|
||||
width: 100%;
|
||||
height: 32px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 模态框移动端优化 */
|
||||
.ant-modal {
|
||||
margin: 0 !important;
|
||||
width: 100vw !important;
|
||||
max-width: 100vw !important;
|
||||
top: 0 !important;
|
||||
padding-bottom: 0 !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
border-radius: 0;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ant-modal-body {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer {
|
||||
border-top: 1px solid #f0f0f0;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
.ant-modal-footer .ant-btn {
|
||||
height: var(--mobile-button-height);
|
||||
margin: 0 4px;
|
||||
}
|
||||
|
||||
/* 表单移动端优化 */
|
||||
.ant-form-item-label {
|
||||
font-size: var(--mobile-font-size);
|
||||
}
|
||||
|
||||
.ant-input,
|
||||
.ant-input-number,
|
||||
.ant-select-selector,
|
||||
.ant-picker {
|
||||
height: var(--mobile-input-height) !important;
|
||||
font-size: var(--mobile-font-size);
|
||||
}
|
||||
|
||||
.ant-select-selection-item {
|
||||
line-height: calc(var(--mobile-input-height) - 2px);
|
||||
}
|
||||
|
||||
/* 卡片组件移动端优化 */
|
||||
.ant-card {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ant-card-head {
|
||||
padding: 0 12px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* 分页器移动端优化 */
|
||||
.ant-pagination {
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ant-pagination-item,
|
||||
.ant-pagination-prev,
|
||||
.ant-pagination-next {
|
||||
min-width: var(--touch-target-min);
|
||||
height: var(--touch-target-min);
|
||||
line-height: var(--touch-target-min);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 平板端优化 (768px - 992px) ========== */
|
||||
@media (min-width: 768px) and (max-width: 992px) {
|
||||
.page-header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th,
|
||||
.ant-table-tbody > tr > td {
|
||||
padding: 12px 8px;
|
||||
}
|
||||
|
||||
.ant-modal {
|
||||
width: 90vw !important;
|
||||
max-width: 800px !important;
|
||||
}
|
||||
|
||||
.search-area {
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
min-width: 250px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 大屏幕优化 (>1600px) ========== */
|
||||
@media (min-width: 1600px) {
|
||||
.page-container {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 500px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 移动端导航优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 侧边栏移动端优化 */
|
||||
.ant-layout-sider {
|
||||
position: fixed !important;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
z-index: 1001;
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.ant-layout-sider.ant-layout-sider-collapsed {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
.ant-layout-sider:not(.ant-layout-sider-collapsed) {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
/* 移动端顶部栏 */
|
||||
.mobile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.mobile-menu-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--touch-target-min);
|
||||
height: var(--touch-target-min);
|
||||
border: none;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.mobile-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* 内容区域移动端调整 */
|
||||
.ant-layout-content {
|
||||
padding: 12px !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
/* 面包屑移动端优化 */
|
||||
.ant-breadcrumb {
|
||||
font-size: 12px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 移动端表格优化 ========== */
|
||||
@media (max-width: 576px) {
|
||||
.responsive-table {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-wrapper {
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table {
|
||||
display: block;
|
||||
min-width: auto;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-thead {
|
||||
display: none; /* 在超小屏幕上隐藏表头 */
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-tbody {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-row {
|
||||
display: block;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-cell {
|
||||
display: block;
|
||||
border: none;
|
||||
padding: 4px 0;
|
||||
text-align: left !important;
|
||||
}
|
||||
|
||||
.responsive-table .ant-table-cell:before {
|
||||
content: attr(data-label) ": ";
|
||||
font-weight: 600;
|
||||
color: #595959;
|
||||
min-width: 80px;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 移动端图表优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.chart-container {
|
||||
height: 300px !important;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.dashboard-stats {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
padding: 12px !important;
|
||||
}
|
||||
|
||||
.stat-card .stat-value {
|
||||
font-size: 20px !important;
|
||||
}
|
||||
|
||||
.stat-card .stat-label {
|
||||
font-size: 12px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 移动端地图优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.map-container {
|
||||
height: 60vh !important;
|
||||
margin: 0 -12px;
|
||||
}
|
||||
|
||||
.map-controls {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 1000;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.map-controls .ant-btn {
|
||||
width: var(--touch-target-min);
|
||||
height: var(--touch-target-min);
|
||||
padding: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 移动端性能优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
/* 减少阴影效果以提升性能 */
|
||||
.ant-card,
|
||||
.ant-modal-content,
|
||||
.ant-drawer-content {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 优化动画性能 */
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
/* 禁用某些在移动端不必要的动画 */
|
||||
.ant-table-tbody > tr {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 触摸设备优化 ========== */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
/* 为触摸设备优化点击区域 */
|
||||
.ant-btn,
|
||||
.ant-input,
|
||||
.ant-select-selector,
|
||||
.ant-picker,
|
||||
.ant-table-row {
|
||||
min-height: var(--touch-target-min);
|
||||
}
|
||||
|
||||
/* 移除悬停效果,因为触摸设备不需要 */
|
||||
.ant-btn:hover,
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: inherit !important;
|
||||
}
|
||||
|
||||
/* 触摸反馈 */
|
||||
.ant-btn:active {
|
||||
transform: scale(0.98);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 横屏模式优化 ========== */
|
||||
@media (max-width: 768px) and (orientation: landscape) {
|
||||
.mobile-header {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
height: 40vh !important;
|
||||
}
|
||||
|
||||
.ant-modal-content {
|
||||
height: 100vh;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 可访问性改进 ========== */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
/* 为喜欢减少动画的用户提供静态体验 */
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 高对比度模式支持 ========== */
|
||||
@media (prefers-contrast: high) {
|
||||
.ant-btn-primary {
|
||||
background: #000 !important;
|
||||
border-color: #000 !important;
|
||||
}
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: #f0f0f0 !important;
|
||||
border: 1px solid #d9d9d9 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 深色模式基础支持 ========== */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.ant-layout {
|
||||
background: #141414 !important;
|
||||
}
|
||||
|
||||
.ant-layout-content {
|
||||
background: #000 !important;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
background: #1f1f1f !important;
|
||||
border-color: #434343 !important;
|
||||
}
|
||||
|
||||
.ant-table-wrapper {
|
||||
background: #1f1f1f !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 特定组件移动端优化 ========== */
|
||||
|
||||
/* 分页器移动端优化 */
|
||||
@media (max-width: 576px) {
|
||||
.ant-pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ant-pagination-options {
|
||||
display: none; /* 隐藏页面大小选择器 */
|
||||
}
|
||||
|
||||
.ant-pagination-simple {
|
||||
margin: 8px 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* 标签移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.ant-tag {
|
||||
margin: 2px;
|
||||
padding: 0 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 统计卡片移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr) !important;
|
||||
gap: 8px !important;
|
||||
}
|
||||
|
||||
.stats-grid .ant-col {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 图表容器移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.charts-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.chart-card .ant-card-body {
|
||||
padding: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 自定义移动端布局类 ========== */
|
||||
.mobile-container {
|
||||
padding: var(--mobile-padding);
|
||||
}
|
||||
|
||||
.mobile-hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.mobile-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.mobile-only {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mobile-flex {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
.mobile-full-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
.mobile-text-center {
|
||||
text-align: center !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 加载状态移动端优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-spin-container {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.ant-empty {
|
||||
margin: 32px 0;
|
||||
}
|
||||
|
||||
.ant-empty-description {
|
||||
font-size: var(--mobile-font-size);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 消息提示移动端优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-message {
|
||||
top: 20px !important;
|
||||
}
|
||||
|
||||
.ant-message-notice {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ant-notification {
|
||||
width: calc(100vw - 32px) !important;
|
||||
margin-right: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 菜单移动端优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-menu-item {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.ant-menu-item-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ant-menu-submenu-title {
|
||||
height: 48px;
|
||||
line-height: 48px;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 工具提示移动端优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-tooltip {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 在移动端禁用工具提示,因为没有悬停 */
|
||||
.ant-tooltip-hidden-arrow {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 表单布局移动端优化 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.ant-row {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.ant-col {
|
||||
padding: 0 4px !important;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-form-item-control {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 调试辅助类 ========== */
|
||||
.debug-breakpoint {
|
||||
position: fixed;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
background: rgba(255, 0, 0, 0.8);
|
||||
color: white;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
z-index: 9999;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.debug-breakpoint:after {
|
||||
content: 'Desktop';
|
||||
}
|
||||
|
||||
@media (max-width: 1600px) {
|
||||
.debug-breakpoint:after {
|
||||
content: 'XL';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.debug-breakpoint:after {
|
||||
content: 'LG';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 992px) {
|
||||
.debug-breakpoint:after {
|
||||
content: 'MD';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.debug-breakpoint:after {
|
||||
content: 'SM';
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 576px) {
|
||||
.debug-breakpoint:after {
|
||||
content: 'XS';
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
* 封装了基本的API请求方法,包括处理认证Token
|
||||
*/
|
||||
|
||||
// API基础URL
|
||||
const API_BASE_URL = 'http://localhost:5350/api';
|
||||
import { API_CONFIG } from '../config/env.js';
|
||||
|
||||
// API基础URL - 支持环境变量配置
|
||||
const API_BASE_URL = API_CONFIG.baseUrl;
|
||||
|
||||
/**
|
||||
* 创建请求头,自动添加认证Token
|
||||
@@ -19,6 +21,11 @@ const createHeaders = (headers = {}) => {
|
||||
|
||||
if (token) {
|
||||
defaultHeaders['Authorization'] = `Bearer ${token}`;
|
||||
} else {
|
||||
console.warn('没有找到认证token,请先登录');
|
||||
// 重定向到登录页
|
||||
window.location.href = '/login';
|
||||
throw new Error('未认证,请先登录');
|
||||
}
|
||||
|
||||
return { ...defaultHeaders, ...headers };
|
||||
@@ -64,6 +71,15 @@ const handleResponse = async (response) => {
|
||||
throw new Error('认证失败,请重新登录');
|
||||
}
|
||||
|
||||
if (response.status === 403) {
|
||||
console.error('API 403错误详情:', {
|
||||
url: response.url,
|
||||
method: response.method,
|
||||
headers: response.headers
|
||||
});
|
||||
throw new Error('您没有权限访问此资源,请联系管理员');
|
||||
}
|
||||
|
||||
if (response.status === 404) {
|
||||
throw new Error('请求的资源不存在');
|
||||
}
|
||||
@@ -108,6 +124,17 @@ const handleResponse = async (response) => {
|
||||
}
|
||||
}
|
||||
|
||||
// 检查响应类型
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
// 如果是blob类型(如文件下载),直接返回blob
|
||||
if (contentType && (contentType.includes('text/csv') ||
|
||||
contentType.includes('application/octet-stream') ||
|
||||
contentType.includes('application/vnd.ms-excel') ||
|
||||
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'))) {
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
// 返回JSON数据
|
||||
const result = await response.json();
|
||||
|
||||
@@ -124,7 +151,7 @@ const handleResponse = async (response) => {
|
||||
});
|
||||
throw new Error(result.message || 'API请求失败');
|
||||
}
|
||||
return result.data;
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -142,6 +169,92 @@ const updateFarm = async (id, data) => {
|
||||
return handleResponse(response);
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建完整URL
|
||||
* @param {string} endpoint - API端点
|
||||
* @returns {string} 完整URL
|
||||
*/
|
||||
const createFullUrl = (endpoint) => {
|
||||
if (API_CONFIG.useProxy) {
|
||||
return `${API_BASE_URL}${endpoint}`;
|
||||
} else {
|
||||
return `${API_CONFIG.fullBaseUrl}${endpoint}`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 直接API调用工具(用于需要完整URL的场景)
|
||||
*/
|
||||
export const directApi = {
|
||||
/**
|
||||
* 直接GET请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async get(endpoint, options = {}) {
|
||||
const url = createFullUrl(endpoint);
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
headers: createHeaders(options.headers),
|
||||
});
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* 直接POST请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} data - 请求数据
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async post(endpoint, data, options = {}) {
|
||||
const url = createFullUrl(endpoint);
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: createHeaders(options.headers),
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* 直接PUT请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} data - 请求数据
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async put(endpoint, data, options = {}) {
|
||||
const url = createFullUrl(endpoint);
|
||||
const response = await fetch(url, {
|
||||
method: 'PUT',
|
||||
headers: createHeaders(options.headers),
|
||||
body: JSON.stringify(data),
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* 直接DELETE请求
|
||||
* @param {string} endpoint - API端点
|
||||
* @param {Object} options - 请求选项
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async delete(endpoint, options = {}) {
|
||||
const url = createFullUrl(endpoint);
|
||||
const response = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: createHeaders(options.headers),
|
||||
...options,
|
||||
});
|
||||
return handleResponse(response);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* API请求方法
|
||||
*/
|
||||
@@ -195,12 +308,35 @@ export const api = {
|
||||
* @returns {Promise} 响应数据
|
||||
*/
|
||||
async get(endpoint, options = {}) {
|
||||
const url = `${API_BASE_URL}${endpoint}`;
|
||||
let url = `${API_BASE_URL}${endpoint}`;
|
||||
|
||||
// 处理查询参数
|
||||
if (options.params && Object.keys(options.params).length > 0) {
|
||||
const searchParams = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(options.params)) {
|
||||
if (value !== null && value !== undefined) {
|
||||
searchParams.append(key, value);
|
||||
}
|
||||
}
|
||||
url += `?${searchParams.toString()}`;
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
method: 'GET',
|
||||
headers: createHeaders(options.headers),
|
||||
// 移除params,因为已经添加到URL中了
|
||||
params: undefined,
|
||||
});
|
||||
|
||||
// 如果指定了responseType为blob,直接返回blob
|
||||
if (options.responseType === 'blob') {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.blob();
|
||||
}
|
||||
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
@@ -255,6 +391,747 @@ export const api = {
|
||||
});
|
||||
return handleResponse(response);
|
||||
},
|
||||
|
||||
// 电子围栏相关API
|
||||
electronicFence: {
|
||||
/**
|
||||
* 获取围栏列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 围栏列表
|
||||
*/
|
||||
async getFences(params = {}) {
|
||||
return api.get('/electronic-fence', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取围栏详情
|
||||
* @param {number} id - 围栏ID
|
||||
* @returns {Promise} 围栏详情
|
||||
*/
|
||||
async getFenceById(id) {
|
||||
return api.get(`/electronic-fence/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建围栏
|
||||
* @param {Object} data - 围栏数据
|
||||
* @returns {Promise} 创建的围栏
|
||||
*/
|
||||
async createFence(data) {
|
||||
return api.post('/electronic-fence', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新围栏
|
||||
* @param {number} id - 围栏ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新后的围栏
|
||||
*/
|
||||
async updateFence(id, data) {
|
||||
return api.put(`/electronic-fence/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除围栏
|
||||
* @param {number} id - 围栏ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async deleteFence(id) {
|
||||
return api.delete(`/electronic-fence/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新围栏统计信息
|
||||
* @param {number} id - 围栏ID
|
||||
* @param {Object} stats - 统计信息
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async updateFenceStats(id, stats) {
|
||||
return api.put(`/electronic-fence/${id}/stats`, stats);
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查点是否在围栏内
|
||||
* @param {number} id - 围栏ID
|
||||
* @param {number} lng - 经度
|
||||
* @param {number} lat - 纬度
|
||||
* @returns {Promise} 检查结果
|
||||
*/
|
||||
async checkPointInFence(id, lng, lat) {
|
||||
return api.get(`/electronic-fence/${id}/check-point`, {
|
||||
params: { lng, lat }
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取围栏统计概览
|
||||
* @returns {Promise} 统计概览
|
||||
*/
|
||||
async getFenceStats() {
|
||||
return api.get('/electronic-fence/stats/overview');
|
||||
}
|
||||
},
|
||||
|
||||
// 电子围栏坐标点相关API
|
||||
electronicFencePoints: {
|
||||
/**
|
||||
* 获取围栏的所有坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 坐标点列表
|
||||
*/
|
||||
async getByFenceId(fenceId, params = {}) {
|
||||
return api.get(`/electronic-fence-points/fence/${fenceId}`, { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取坐标点详情
|
||||
* @param {number} id - 坐标点ID
|
||||
* @returns {Promise} 坐标点详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/electronic-fence-points/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建坐标点
|
||||
* @param {Object} data - 坐标点数据
|
||||
* @returns {Promise} 创建的坐标点
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/electronic-fence-points', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量创建坐标点
|
||||
* @param {Object} data - 批量创建数据
|
||||
* @returns {Promise} 创建的坐标点列表
|
||||
*/
|
||||
async createPoints(data) {
|
||||
return api.post('/electronic-fence-points/batch', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新坐标点
|
||||
* @param {number} id - 坐标点ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新后的坐标点
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/electronic-fence-points/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新围栏的所有坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新后的坐标点列表
|
||||
*/
|
||||
async updateFencePoints(fenceId, data) {
|
||||
return api.put(`/electronic-fence-points/fence/${fenceId}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除坐标点
|
||||
* @param {number} id - 坐标点ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/electronic-fence-points/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除围栏的所有坐标点
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async deleteFencePoints(fenceId) {
|
||||
return api.delete(`/electronic-fence-points/fence/${fenceId}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取围栏边界框
|
||||
* @param {number} fenceId - 围栏ID
|
||||
* @returns {Promise} 边界框数据
|
||||
*/
|
||||
async getBounds(fenceId) {
|
||||
return api.get(`/electronic-fence-points/fence/${fenceId}/bounds`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索坐标点
|
||||
* @param {Object} params - 搜索参数
|
||||
* @returns {Promise} 搜索结果
|
||||
*/
|
||||
async search(params = {}) {
|
||||
return api.get('/electronic-fence-points/search', { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 栏舍管理API
|
||||
pens: {
|
||||
/**
|
||||
* 获取栏舍列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 栏舍列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/pens/public', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取栏舍详情
|
||||
* @param {number} id - 栏舍ID
|
||||
* @returns {Promise} 栏舍详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/pens/public/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建栏舍
|
||||
* @param {Object} data - 栏舍数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/pens', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新栏舍
|
||||
* @param {number} id - 栏舍ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/pens/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除栏舍
|
||||
* @param {number} id - 栏舍ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/pens/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除栏舍
|
||||
* @param {Array} ids - 栏舍ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.delete('/pens/batch', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取栏舍统计信息
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 统计信息
|
||||
*/
|
||||
async getStats(params = {}) {
|
||||
return api.get('/pens/public/stats/summary', { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 栏舍设置API
|
||||
cattlePens: {
|
||||
/**
|
||||
* 获取栏舍设置列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 栏舍设置列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/cattle-pens', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取栏舍设置详情
|
||||
* @param {number} id - 栏舍设置ID
|
||||
* @returns {Promise} 栏舍设置详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/cattle-pens/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建栏舍设置
|
||||
* @param {Object} data - 栏舍设置数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/cattle-pens', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新栏舍设置
|
||||
* @param {number} id - 栏舍设置ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/cattle-pens/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除栏舍设置
|
||||
* @param {number} id - 栏舍设置ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/cattle-pens/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除栏舍设置
|
||||
* @param {Array} ids - 栏舍设置ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.post('/cattle-pens/batch-delete', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取栏舍中的牛只
|
||||
* @param {number} id - 栏舍设置ID
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 牛只列表
|
||||
*/
|
||||
async getAnimals(id, params = {}) {
|
||||
return api.get(`/cattle-pens/${id}/animals`, { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 批次设置API
|
||||
cattleBatches: {
|
||||
/**
|
||||
* 获取批次设置列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 批次设置列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/cattle-batches', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取批次设置详情
|
||||
* @param {number} id - 批次设置ID
|
||||
* @returns {Promise} 批次设置详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/cattle-batches/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建批次设置
|
||||
* @param {Object} data - 批次设置数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/cattle-batches', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新批次设置
|
||||
* @param {number} id - 批次设置ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/cattle-batches/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除批次设置
|
||||
* @param {number} id - 批次设置ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/cattle-batches/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除批次设置
|
||||
* @param {Array} ids - 批次设置ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.post('/cattle-batches/batch-delete', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取批次中的牛只
|
||||
* @param {number} id - 批次设置ID
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 牛只列表
|
||||
*/
|
||||
async getAnimals(id, params = {}) {
|
||||
return api.get(`/cattle-batches/${id}/animals`, { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加牛只到批次
|
||||
* @param {number} id - 批次设置ID
|
||||
* @param {Array} animalIds - 牛只ID数组
|
||||
* @returns {Promise} 添加结果
|
||||
*/
|
||||
async addAnimals(id, animalIds) {
|
||||
return api.post(`/cattle-batches/${id}/animals`, { animalIds });
|
||||
},
|
||||
|
||||
/**
|
||||
* 从批次中移除牛只
|
||||
* @param {number} id - 批次设置ID
|
||||
* @param {number} animalId - 牛只ID
|
||||
* @returns {Promise} 移除结果
|
||||
*/
|
||||
async removeAnimal(id, animalId) {
|
||||
return api.delete(`/cattle-batches/${id}/animals/${animalId}`);
|
||||
}
|
||||
},
|
||||
|
||||
// 转栏记录API
|
||||
cattleTransferRecords: {
|
||||
/**
|
||||
* 获取转栏记录列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 转栏记录列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/cattle-transfer-records', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取转栏记录详情
|
||||
* @param {number} id - 转栏记录ID
|
||||
* @returns {Promise} 转栏记录详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/cattle-transfer-records/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建转栏记录
|
||||
* @param {Object} data - 转栏记录数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/cattle-transfer-records', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新转栏记录
|
||||
* @param {number} id - 转栏记录ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/cattle-transfer-records/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除转栏记录
|
||||
* @param {number} id - 转栏记录ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/cattle-transfer-records/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除转栏记录
|
||||
* @param {Array} ids - 转栏记录ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.post('/cattle-transfer-records/batch-delete', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 牛只列表
|
||||
*/
|
||||
async getAvailableAnimals(params = {}) {
|
||||
return api.get('/cattle-transfer-records/available-animals', { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 离栏记录API
|
||||
cattleExitRecords: {
|
||||
/**
|
||||
* 获取离栏记录列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 离栏记录列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/cattle-exit-records', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取离栏记录详情
|
||||
* @param {number} id - 离栏记录ID
|
||||
* @returns {Promise} 离栏记录详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/cattle-exit-records/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建离栏记录
|
||||
* @param {Object} data - 离栏记录数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/cattle-exit-records', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新离栏记录
|
||||
* @param {number} id - 离栏记录ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/cattle-exit-records/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除离栏记录
|
||||
* @param {number} id - 离栏记录ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/cattle-exit-records/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除离栏记录
|
||||
* @param {Array} ids - 离栏记录ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.post('/cattle-exit-records/batch-delete', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 确认离栏记录
|
||||
* @param {number} id - 离栏记录ID
|
||||
* @returns {Promise} 确认结果
|
||||
*/
|
||||
async confirm(id) {
|
||||
return api.post(`/cattle-exit-records/${id}/confirm`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 牛只列表
|
||||
*/
|
||||
async getAvailableAnimals(params = {}) {
|
||||
return api.get('/cattle-exit-records/available-animals', { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 表单日志相关API
|
||||
formLogs: {
|
||||
/**
|
||||
* 添加表单日志
|
||||
* @param {Object} logData - 日志数据
|
||||
* @returns {Promise} 添加结果
|
||||
*/
|
||||
async add(logData) {
|
||||
return api.post('/form-logs/add', logData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取表单日志列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 日志列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/form-logs/list', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取日志详情
|
||||
* @param {number} id - 日志ID
|
||||
* @returns {Promise} 日志详情
|
||||
*/
|
||||
async getDetail(id) {
|
||||
return api.get(`/form-logs/detail/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除日志
|
||||
* @param {number} id - 日志ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/form-logs/delete/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 批量删除日志
|
||||
* @param {Array} ids - 日志ID数组
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async batchDelete(ids) {
|
||||
return api.delete('/form-logs/batch-delete', { ids });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取日志统计信息
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 统计信息
|
||||
*/
|
||||
async getStats(params = {}) {
|
||||
return api.get('/form-logs/stats', { params });
|
||||
}
|
||||
},
|
||||
|
||||
// 日志相关API(兼容性)
|
||||
logs: {
|
||||
/**
|
||||
* 添加表单日志(兼容性方法)
|
||||
* @param {Object} logData - 日志数据
|
||||
* @returns {Promise} 添加结果
|
||||
*/
|
||||
async addFormLog(logData) {
|
||||
return api.formLogs.add(logData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 添加搜索日志(兼容性方法)
|
||||
* @param {Object} logData - 日志数据
|
||||
* @returns {Promise} 添加结果
|
||||
*/
|
||||
async addSearchLog(logData) {
|
||||
return api.formLogs.add({
|
||||
...logData,
|
||||
action: 'search'
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// 养殖场管理API
|
||||
farms: {
|
||||
/**
|
||||
* 获取养殖场列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 养殖场列表
|
||||
*/
|
||||
async getList(params = {}) {
|
||||
return api.get('/farms', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取养殖场详情
|
||||
* @param {number} id - 养殖场ID
|
||||
* @returns {Promise} 养殖场详情
|
||||
*/
|
||||
async getById(id) {
|
||||
return api.get(`/farms/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建养殖场
|
||||
* @param {Object} data - 养殖场数据
|
||||
* @returns {Promise} 创建结果
|
||||
*/
|
||||
async create(data) {
|
||||
return api.post('/farms', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新养殖场
|
||||
* @param {number} id - 养殖场ID
|
||||
* @param {Object} data - 更新数据
|
||||
* @returns {Promise} 更新结果
|
||||
*/
|
||||
async update(id, data) {
|
||||
return api.put(`/farms/${id}`, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除养殖场
|
||||
* @param {number} id - 养殖场ID
|
||||
* @returns {Promise} 删除结果
|
||||
*/
|
||||
async delete(id) {
|
||||
return api.delete(`/farms/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 搜索养殖场
|
||||
* @param {string} name - 养殖场名称
|
||||
* @returns {Promise} 搜索结果
|
||||
*/
|
||||
async search(name) {
|
||||
return api.get('/farms/search', { params: { name } });
|
||||
}
|
||||
},
|
||||
|
||||
// 操作日志API
|
||||
operationLogs: {
|
||||
/**
|
||||
* 获取操作日志列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 操作日志列表
|
||||
*/
|
||||
async getOperationLogs(params = {}) {
|
||||
return api.get('/operation-logs', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取操作日志详情
|
||||
* @param {number} id - 操作日志ID
|
||||
* @returns {Promise} 操作日志详情
|
||||
*/
|
||||
async getOperationLogById(id) {
|
||||
return api.get(`/operation-logs/${id}`);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取操作统计
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 操作统计
|
||||
*/
|
||||
async getOperationStats(params = {}) {
|
||||
return api.get('/operation-logs/stats/overview', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取操作日志图表数据
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 图表数据
|
||||
*/
|
||||
async getOperationChartData(params = {}) {
|
||||
return api.get('/operation-logs/stats/chart', { params });
|
||||
},
|
||||
|
||||
/**
|
||||
* 清理过期日志
|
||||
* @param {Object} data - 清理参数
|
||||
* @returns {Promise} 清理结果
|
||||
*/
|
||||
async cleanExpiredLogs(data = {}) {
|
||||
return api.post('/operation-logs/clean', data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 导出操作日志
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise} 导出结果
|
||||
*/
|
||||
async exportOperationLogs(params = {}) {
|
||||
return api.get('/operation-logs/export', {
|
||||
params,
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
273
admin-system/frontend/src/utils/baiduMapLoader.js
Normal file
273
admin-system/frontend/src/utils/baiduMapLoader.js
Normal file
@@ -0,0 +1,273 @@
|
||||
/**
|
||||
* 百度地图加载器
|
||||
* 提供更健壮的百度地图API加载和初始化功能
|
||||
*/
|
||||
|
||||
// 百度地图API加载状态
|
||||
let BMapLoaded = false;
|
||||
let loadingPromise = null;
|
||||
let retryCount = 0;
|
||||
const MAX_RETRY = 3;
|
||||
|
||||
/**
|
||||
* 加载百度地图API
|
||||
* @param {string} apiKey - 百度地图API密钥
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
export const loadBaiduMapAPI = async (apiKey = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo') => {
|
||||
// 如果已经加载过,直接返回
|
||||
if (BMapLoaded && window.BMap) {
|
||||
console.log('百度地图API已加载');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 如果正在加载中,返回加载Promise
|
||||
if (loadingPromise) {
|
||||
console.log('百度地图API正在加载中...');
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
console.log('开始加载百度地图API...');
|
||||
|
||||
// 创建加载Promise
|
||||
loadingPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 检查API密钥
|
||||
if (!apiKey || apiKey === 'YOUR_VALID_BAIDU_MAP_API_KEY') {
|
||||
const error = new Error('百度地图API密钥未配置或无效');
|
||||
console.error('API密钥错误:', error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经存在BMap
|
||||
if (typeof window.BMap !== 'undefined' && window.BMap.Map) {
|
||||
console.log('BMap已存在,直接使用');
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建全局回调函数
|
||||
window.initBaiduMapCallback = () => {
|
||||
console.log('百度地图API脚本加载完成');
|
||||
|
||||
// 等待BMap对象完全初始化
|
||||
const checkBMap = () => {
|
||||
if (window.BMap && typeof window.BMap.Map === 'function') {
|
||||
console.log('BMap对象初始化完成');
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
// 清理全局回调
|
||||
delete window.initBaiduMapCallback;
|
||||
} else {
|
||||
console.log('等待BMap对象初始化...');
|
||||
setTimeout(checkBMap, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 开始检查BMap对象
|
||||
setTimeout(checkBMap, 50);
|
||||
};
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${apiKey}&callback=initBaiduMapCallback`;
|
||||
|
||||
console.log('百度地图API URL:', script.src);
|
||||
|
||||
script.onerror = (error) => {
|
||||
console.error('百度地图脚本加载失败:', error);
|
||||
reject(new Error('百度地图脚本加载失败'));
|
||||
};
|
||||
|
||||
script.onload = () => {
|
||||
console.log('百度地图脚本加载成功');
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
const timeout = setTimeout(() => {
|
||||
if (!BMapLoaded) {
|
||||
console.error('百度地图API加载超时');
|
||||
reject(new Error('百度地图API加载超时'));
|
||||
}
|
||||
}, 20000);
|
||||
|
||||
// 成功加载后清除超时
|
||||
const originalResolve = resolve;
|
||||
resolve = () => {
|
||||
clearTimeout(timeout);
|
||||
originalResolve();
|
||||
};
|
||||
|
||||
// 添加到文档中
|
||||
document.head.appendChild(script);
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载百度地图API时出错:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* 重试加载百度地图API
|
||||
* @param {string} apiKey - 百度地图API密钥
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
export const retryLoadBaiduMapAPI = async (apiKey = 'SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo') => {
|
||||
if (retryCount >= MAX_RETRY) {
|
||||
throw new Error('百度地图API加载重试次数已达上限');
|
||||
}
|
||||
|
||||
retryCount++;
|
||||
console.log(`第 ${retryCount} 次尝试加载百度地图API...`);
|
||||
|
||||
// 重置状态
|
||||
BMapLoaded = false;
|
||||
loadingPromise = null;
|
||||
|
||||
// 清理可能存在的旧脚本
|
||||
const existingScript = document.querySelector('script[src*="api.map.baidu.com"]');
|
||||
if (existingScript) {
|
||||
existingScript.remove();
|
||||
}
|
||||
|
||||
// 清理全局回调
|
||||
if (window.initBaiduMapCallback) {
|
||||
delete window.initBaiduMapCallback;
|
||||
}
|
||||
|
||||
return loadBaiduMapAPI(apiKey);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查百度地图API是否可用
|
||||
* @returns {boolean} 是否可用
|
||||
*/
|
||||
export const isBaiduMapAvailable = () => {
|
||||
return BMapLoaded && window.BMap && typeof window.BMap.Map === 'function';
|
||||
};
|
||||
|
||||
/**
|
||||
* 等待百度地图API加载完成
|
||||
* @param {number} timeout - 超时时间(毫秒)
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
export const waitForBaiduMap = (timeout = 10000) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isBaiduMapAvailable()) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const checkInterval = setInterval(() => {
|
||||
if (isBaiduMapAvailable()) {
|
||||
clearInterval(checkInterval);
|
||||
resolve();
|
||||
} else if (Date.now() - startTime > timeout) {
|
||||
clearInterval(checkInterval);
|
||||
reject(new Error('等待百度地图API加载超时'));
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建百度地图实例
|
||||
* @param {string|HTMLElement} container - 地图容器ID或元素
|
||||
* @param {Object} options - 地图配置选项
|
||||
* @returns {Promise<BMap.Map>} 地图实例
|
||||
*/
|
||||
export const createBaiduMap = async (container, options = {}) => {
|
||||
try {
|
||||
// 确保百度地图API已加载
|
||||
await loadBaiduMapAPI();
|
||||
|
||||
// 等待BMap完全可用
|
||||
await waitForBaiduMap();
|
||||
|
||||
// 获取容器元素
|
||||
let mapContainer = typeof container === 'string'
|
||||
? document.getElementById(container)
|
||||
: container;
|
||||
|
||||
// 如果容器不存在,等待一段时间后重试
|
||||
if (!mapContainer) {
|
||||
console.log('地图容器不存在,等待DOM渲染...');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
mapContainer = typeof container === 'string'
|
||||
? document.getElementById(container)
|
||||
: container;
|
||||
}
|
||||
|
||||
if (!mapContainer) {
|
||||
throw new Error('地图容器不存在,请确保DOM已正确渲染');
|
||||
}
|
||||
|
||||
// 检查容器是否有尺寸
|
||||
if (mapContainer.offsetWidth === 0 || mapContainer.offsetHeight === 0) {
|
||||
console.warn('地图容器尺寸为0,等待尺寸计算...');
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
center: new window.BMap.Point(106.27, 38.47), // 宁夏中心点
|
||||
zoom: 8,
|
||||
enableMapClick: true,
|
||||
enableScrollWheelZoom: true,
|
||||
enableDragging: true,
|
||||
enableDoubleClickZoom: true,
|
||||
enableKeyboard: true
|
||||
};
|
||||
|
||||
// 合并配置
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
// 创建地图实例
|
||||
const map = new window.BMap.Map(mapContainer);
|
||||
|
||||
// 设置中心点和缩放级别
|
||||
map.centerAndZoom(mergedOptions.center, mergedOptions.zoom);
|
||||
|
||||
// 配置地图功能
|
||||
if (mergedOptions.enableScrollWheelZoom) {
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDragging) {
|
||||
map.enableDragging();
|
||||
} else {
|
||||
map.disableDragging();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDoubleClickZoom) {
|
||||
map.enableDoubleClickZoom();
|
||||
} else {
|
||||
map.disableDoubleClickZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableKeyboard) {
|
||||
map.enableKeyboard();
|
||||
} else {
|
||||
map.disableKeyboard();
|
||||
}
|
||||
|
||||
// 添加地图控件
|
||||
map.addControl(new window.BMap.NavigationControl());
|
||||
map.addControl(new window.BMap.ScaleControl());
|
||||
map.addControl(new window.BMap.OverviewMapControl());
|
||||
|
||||
console.log('百度地图创建成功');
|
||||
return map;
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建百度地图失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
337
admin-system/frontend/src/utils/baiduMapTest.js
Normal file
337
admin-system/frontend/src/utils/baiduMapTest.js
Normal file
@@ -0,0 +1,337 @@
|
||||
/**
|
||||
* 百度地图API测试工具
|
||||
* 用于诊断和修复百度地图API加载问题
|
||||
*/
|
||||
|
||||
export class BaiduMapTester {
|
||||
constructor() {
|
||||
this.testResults = [];
|
||||
this.isLoading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行完整的API测试
|
||||
*/
|
||||
async runFullTest() {
|
||||
console.log('🔍 开始百度地图API完整测试...');
|
||||
this.testResults = [];
|
||||
|
||||
try {
|
||||
// 测试1: 检查当前BMap状态
|
||||
await this.testCurrentBMapStatus();
|
||||
|
||||
// 测试2: 测试API加载
|
||||
await this.testApiLoading();
|
||||
|
||||
// 测试3: 测试BMap对象功能
|
||||
await this.testBMapFunctionality();
|
||||
|
||||
// 测试4: 测试地图创建
|
||||
await this.testMapCreation();
|
||||
|
||||
// 输出测试结果
|
||||
this.outputTestResults();
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 测试过程中出现错误:', error);
|
||||
this.testResults.push({
|
||||
name: '测试过程错误',
|
||||
status: 'error',
|
||||
message: error.message
|
||||
});
|
||||
}
|
||||
|
||||
return this.testResults;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试当前BMap状态
|
||||
*/
|
||||
async testCurrentBMapStatus() {
|
||||
console.log('📋 测试1: 检查当前BMap状态');
|
||||
|
||||
const result = {
|
||||
name: 'BMap状态检查',
|
||||
status: 'info',
|
||||
details: {}
|
||||
};
|
||||
|
||||
result.details.bMapExists = typeof window.BMap !== 'undefined';
|
||||
result.details.bMapType = typeof window.BMap;
|
||||
result.details.bMapValue = window.BMap;
|
||||
|
||||
if (window.BMap) {
|
||||
result.details.bMapVersion = window.BMap.version || '未知';
|
||||
result.details.mapConstructor = typeof window.BMap.Map;
|
||||
result.details.pointConstructor = typeof window.BMap.Point;
|
||||
result.details.markerConstructor = typeof window.BMap.Marker;
|
||||
result.details.infoWindowConstructor = typeof window.BMap.InfoWindow;
|
||||
|
||||
if (typeof window.BMap.Map === 'function') {
|
||||
result.status = 'success';
|
||||
result.message = 'BMap对象已存在且功能完整';
|
||||
} else {
|
||||
result.status = 'warning';
|
||||
result.message = 'BMap对象存在但功能不完整';
|
||||
}
|
||||
} else {
|
||||
result.status = 'error';
|
||||
result.message = 'BMap对象不存在,需要加载API';
|
||||
}
|
||||
|
||||
this.testResults.push(result);
|
||||
console.log(`✅ 测试1完成: ${result.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试API加载
|
||||
*/
|
||||
async testApiLoading() {
|
||||
console.log('📋 测试2: 测试API加载');
|
||||
|
||||
const result = {
|
||||
name: 'API加载测试',
|
||||
status: 'info',
|
||||
details: {}
|
||||
};
|
||||
|
||||
try {
|
||||
// 检查是否已有BMap
|
||||
if (typeof window.BMap !== 'undefined') {
|
||||
result.status = 'success';
|
||||
result.message = 'BMap已存在,跳过API加载测试';
|
||||
this.testResults.push(result);
|
||||
return;
|
||||
}
|
||||
|
||||
// 尝试加载API
|
||||
result.details.loadingStarted = true;
|
||||
const loadPromise = this.loadBMapApi();
|
||||
const timeoutPromise = new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('API加载超时')), 10000)
|
||||
);
|
||||
|
||||
await Promise.race([loadPromise, timeoutPromise]);
|
||||
|
||||
result.status = 'success';
|
||||
result.message = 'API加载成功';
|
||||
result.details.loadingCompleted = true;
|
||||
|
||||
} catch (error) {
|
||||
result.status = 'error';
|
||||
result.message = `API加载失败: ${error.message}`;
|
||||
result.details.error = error.message;
|
||||
}
|
||||
|
||||
this.testResults.push(result);
|
||||
console.log(`✅ 测试2完成: ${result.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试BMap对象功能
|
||||
*/
|
||||
async testBMapFunctionality() {
|
||||
console.log('📋 测试3: 测试BMap对象功能');
|
||||
|
||||
const result = {
|
||||
name: 'BMap功能测试',
|
||||
status: 'info',
|
||||
details: {}
|
||||
};
|
||||
|
||||
try {
|
||||
if (typeof window.BMap === 'undefined') {
|
||||
throw new Error('BMap对象不存在');
|
||||
}
|
||||
|
||||
// 测试Point创建
|
||||
const point = new window.BMap.Point(106.27, 38.47);
|
||||
result.details.pointTest = {
|
||||
success: true,
|
||||
lng: point.lng,
|
||||
lat: point.lat
|
||||
};
|
||||
|
||||
// 测试Marker创建
|
||||
const marker = new window.BMap.Marker(point);
|
||||
result.details.markerTest = {
|
||||
success: true,
|
||||
position: marker.getPosition()
|
||||
};
|
||||
|
||||
// 测试InfoWindow创建
|
||||
const infoWindow = new window.BMap.InfoWindow('<div>测试</div>');
|
||||
result.details.infoWindowTest = {
|
||||
success: true,
|
||||
type: typeof infoWindow
|
||||
};
|
||||
|
||||
result.status = 'success';
|
||||
result.message = 'BMap对象功能测试通过';
|
||||
|
||||
} catch (error) {
|
||||
result.status = 'error';
|
||||
result.message = `BMap功能测试失败: ${error.message}`;
|
||||
result.details.error = error.message;
|
||||
}
|
||||
|
||||
this.testResults.push(result);
|
||||
console.log(`✅ 测试3完成: ${result.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试地图创建
|
||||
*/
|
||||
async testMapCreation() {
|
||||
console.log('📋 测试4: 测试地图创建');
|
||||
|
||||
const result = {
|
||||
name: '地图创建测试',
|
||||
status: 'info',
|
||||
details: {}
|
||||
};
|
||||
|
||||
try {
|
||||
if (typeof window.BMap === 'undefined') {
|
||||
throw new Error('BMap对象不存在');
|
||||
}
|
||||
|
||||
// 创建测试容器
|
||||
const testContainer = document.createElement('div');
|
||||
testContainer.style.width = '100px';
|
||||
testContainer.style.height = '100px';
|
||||
testContainer.style.position = 'absolute';
|
||||
testContainer.style.top = '-1000px';
|
||||
testContainer.style.left = '-1000px';
|
||||
document.body.appendChild(testContainer);
|
||||
|
||||
// 创建地图
|
||||
const map = new window.BMap.Map(testContainer);
|
||||
result.details.mapCreated = true;
|
||||
result.details.mapType = typeof map;
|
||||
|
||||
// 测试地图基本功能
|
||||
const center = new window.BMap.Point(106.27, 38.47);
|
||||
map.centerAndZoom(center, 10);
|
||||
result.details.mapConfigured = true;
|
||||
|
||||
// 清理测试容器
|
||||
document.body.removeChild(testContainer);
|
||||
|
||||
result.status = 'success';
|
||||
result.message = '地图创建测试通过';
|
||||
|
||||
} catch (error) {
|
||||
result.status = 'error';
|
||||
result.message = `地图创建测试失败: ${error.message}`;
|
||||
result.details.error = error.message;
|
||||
}
|
||||
|
||||
this.testResults.push(result);
|
||||
console.log(`✅ 测试4完成: ${result.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载百度地图API
|
||||
*/
|
||||
loadBMapApi() {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.isLoading) {
|
||||
reject(new Error('API正在加载中'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://api.map.baidu.com/api?v=3.0&ak=SOawZTeQbxdgrKYYx0o2hn34G0DyU2uo&callback=initBMapTest';
|
||||
|
||||
window.initBMapTest = () => {
|
||||
this.isLoading = false;
|
||||
delete window.initBMapTest;
|
||||
resolve();
|
||||
};
|
||||
|
||||
script.onerror = () => {
|
||||
this.isLoading = false;
|
||||
delete window.initBMapTest;
|
||||
reject(new Error('API脚本加载失败'));
|
||||
};
|
||||
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出测试结果
|
||||
*/
|
||||
outputTestResults() {
|
||||
console.log('\n📊 百度地图API测试结果:');
|
||||
console.log('='.repeat(50));
|
||||
|
||||
this.testResults.forEach((result, index) => {
|
||||
const statusIcon = {
|
||||
success: '✅',
|
||||
error: '❌',
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️'
|
||||
}[result.status] || '❓';
|
||||
|
||||
console.log(`${index + 1}. ${statusIcon} ${result.name}: ${result.message}`);
|
||||
|
||||
if (result.details && Object.keys(result.details).length > 0) {
|
||||
console.log(' 详细信息:', result.details);
|
||||
}
|
||||
});
|
||||
|
||||
console.log('='.repeat(50));
|
||||
|
||||
const successCount = this.testResults.filter(r => r.status === 'success').length;
|
||||
const totalCount = this.testResults.length;
|
||||
|
||||
console.log(`📈 测试总结: ${successCount}/${totalCount} 项测试通过`);
|
||||
|
||||
if (successCount === totalCount) {
|
||||
console.log('🎉 所有测试通过!百度地图API工作正常。');
|
||||
} else {
|
||||
console.log('⚠️ 部分测试失败,请检查上述错误信息。');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取修复建议
|
||||
*/
|
||||
getFixSuggestions() {
|
||||
const suggestions = [];
|
||||
|
||||
this.testResults.forEach(result => {
|
||||
if (result.status === 'error') {
|
||||
switch (result.name) {
|
||||
case 'BMap状态检查':
|
||||
suggestions.push('1. 检查网络连接是否正常');
|
||||
suggestions.push('2. 检查百度地图API密钥是否有效');
|
||||
suggestions.push('3. 检查域名白名单配置');
|
||||
break;
|
||||
case 'API加载测试':
|
||||
suggestions.push('1. 尝试刷新页面');
|
||||
suggestions.push('2. 检查浏览器控制台是否有CORS错误');
|
||||
suggestions.push('3. 尝试使用备用API密钥');
|
||||
break;
|
||||
case 'BMap功能测试':
|
||||
suggestions.push('1. 等待API完全加载后再使用');
|
||||
suggestions.push('2. 检查API版本兼容性');
|
||||
break;
|
||||
case '地图创建测试':
|
||||
suggestions.push('1. 确保容器元素存在且有尺寸');
|
||||
suggestions.push('2. 检查容器是否被隐藏');
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return suggestions;
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例实例
|
||||
export const baiduMapTester = new BaiduMapTester();
|
||||
@@ -14,7 +14,8 @@ export const farmService = {
|
||||
* @returns {Promise<Array>} 养殖场列表
|
||||
*/
|
||||
async getAllFarms() {
|
||||
const farms = await api.get('/farms/public');
|
||||
const response = await api.get('/farms/public');
|
||||
const farms = response && response.success ? response.data : response;
|
||||
|
||||
// 标准化location字段格式
|
||||
return farms.map(farm => {
|
||||
@@ -28,10 +29,19 @@ export const farmService = {
|
||||
farm.location = null;
|
||||
}
|
||||
}
|
||||
// 如果location是对象但缺少lat或lng,设为null
|
||||
if (farm.location && (typeof farm.location.lat !== 'number' || typeof farm.location.lng !== 'number')) {
|
||||
console.warn(`养殖场 ${farm.id} 的location字段格式不正确:`, farm.location);
|
||||
farm.location = null;
|
||||
|
||||
// 标准化location字段格式,支持latitude/longitude和lat/lng
|
||||
if (farm.location) {
|
||||
if (farm.location.latitude && farm.location.longitude) {
|
||||
// 将latitude/longitude转换为lat/lng
|
||||
farm.location.lat = farm.location.latitude;
|
||||
farm.location.lng = farm.location.longitude;
|
||||
} else if (farm.location.lat && farm.location.lng) {
|
||||
// 已经是lat/lng格式,保持不变
|
||||
} else {
|
||||
console.warn(`养殖场 ${farm.id} 的location字段格式不正确:`, farm.location);
|
||||
farm.location = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return farm;
|
||||
@@ -44,7 +54,8 @@ export const farmService = {
|
||||
* @returns {Promise<Object>} 养殖场详情
|
||||
*/
|
||||
async getFarmById(id) {
|
||||
return api.get(`/farms/${id}`);
|
||||
const response = await api.get(`/farms/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -85,7 +96,8 @@ export const animalService = {
|
||||
* @returns {Promise<Array>} 动物列表
|
||||
*/
|
||||
async getAllAnimals() {
|
||||
return api.get('/animals/public');
|
||||
const response = await api.get('/animals/public');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -94,7 +106,8 @@ export const animalService = {
|
||||
* @returns {Promise<Array>} 动物列表
|
||||
*/
|
||||
async getAnimalsByFarm(farmId) {
|
||||
return api.get(`/farms/${farmId}/animals`);
|
||||
const response = await api.get(`/farms/${farmId}/animals`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -103,7 +116,8 @@ export const animalService = {
|
||||
* @returns {Promise<Object>} 动物详情
|
||||
*/
|
||||
async getAnimalById(id) {
|
||||
return api.get(`/animals/${id}`);
|
||||
const response = await api.get(`/animals/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -144,7 +158,8 @@ export const deviceService = {
|
||||
* @returns {Promise<Array>} 设备列表
|
||||
*/
|
||||
async getAllDevices() {
|
||||
return api.get('/devices/public');
|
||||
const response = await api.get('/devices/public');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -153,7 +168,8 @@ export const deviceService = {
|
||||
* @returns {Promise<Array>} 设备列表
|
||||
*/
|
||||
async getDevicesByFarm(farmId) {
|
||||
return api.get(`/farms/${farmId}/devices`);
|
||||
const response = await api.get(`/farms/${farmId}/devices`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -162,7 +178,8 @@ export const deviceService = {
|
||||
* @returns {Promise<Object>} 设备详情
|
||||
*/
|
||||
async getDeviceById(id) {
|
||||
return api.get(`/devices/${id}`);
|
||||
const response = await api.get(`/devices/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -171,7 +188,8 @@ export const deviceService = {
|
||||
* @returns {Promise<Object>} 设备状态
|
||||
*/
|
||||
async getDeviceStatus(id) {
|
||||
return api.get(`/devices/${id}/status`);
|
||||
const response = await api.get(`/devices/${id}/status`);
|
||||
return response && response.success ? response.data : response;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -184,7 +202,8 @@ export const alertService = {
|
||||
* @returns {Promise<Array>} 预警列表
|
||||
*/
|
||||
async getAllAlerts() {
|
||||
return api.get('/alerts/public');
|
||||
const response = await api.get('/alerts/public');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -193,7 +212,8 @@ export const alertService = {
|
||||
* @returns {Promise<Array>} 预警列表
|
||||
*/
|
||||
async getAlertsByFarm(farmId) {
|
||||
return api.get(`/farms/${farmId}/alerts`);
|
||||
const response = await api.get(`/farms/${farmId}/alerts`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -202,7 +222,8 @@ export const alertService = {
|
||||
* @returns {Promise<Object>} 预警详情
|
||||
*/
|
||||
async getAlertById(id) {
|
||||
return api.get(`/alerts/${id}`);
|
||||
const response = await api.get(`/alerts/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -216,6 +237,299 @@ export const alertService = {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 菜单数据服务
|
||||
*/
|
||||
export const menuService = {
|
||||
/**
|
||||
* 获取所有菜单
|
||||
* @returns {Promise<Array>} 菜单列表
|
||||
*/
|
||||
async getAllMenus() {
|
||||
const response = await api.get('/menus', { params: { pageSize: 1000 } });
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取菜单详情
|
||||
* @param {string} id - 菜单ID
|
||||
* @returns {Promise<Object>} 菜单详情
|
||||
*/
|
||||
async getMenuById(id) {
|
||||
const response = await api.get(`/menus/public/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取角色的菜单权限
|
||||
* @param {string} roleId - 角色ID
|
||||
* @returns {Promise<Object>} 角色菜单权限
|
||||
*/
|
||||
async getRoleMenus(roleId) {
|
||||
const response = await api.get(`/menus/public/role/${roleId}`);
|
||||
return response && response.success ? response.data : response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 角色权限数据服务
|
||||
*/
|
||||
export const rolePermissionService = {
|
||||
/**
|
||||
* 获取角色列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Object>} 角色列表
|
||||
*/
|
||||
async getRoles(params = {}) {
|
||||
const response = await api.get('/role-permissions/public/roles', { params });
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取角色详情
|
||||
* @param {string} id - 角色ID
|
||||
* @returns {Promise<Object>} 角色详情
|
||||
*/
|
||||
async getRoleById(id) {
|
||||
const response = await api.get(`/role-permissions/public/roles/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建角色
|
||||
* @param {Object} roleData - 角色数据
|
||||
* @returns {Promise<Object>} 创建结果
|
||||
*/
|
||||
async createRole(roleData) {
|
||||
const response = await api.post('/role-permissions/roles', roleData);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新角色
|
||||
* @param {string} id - 角色ID
|
||||
* @param {Object} roleData - 角色数据
|
||||
* @returns {Promise<Object>} 更新结果
|
||||
*/
|
||||
async updateRole(id, roleData) {
|
||||
const response = await api.put(`/role-permissions/roles/${id}`, roleData);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除角色
|
||||
* @param {string} id - 角色ID
|
||||
* @returns {Promise<Object>} 删除结果
|
||||
*/
|
||||
async deleteRole(id) {
|
||||
const response = await api.delete(`/role-permissions/roles/${id}`);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取权限菜单树
|
||||
* @returns {Promise<Object>} 权限菜单树
|
||||
*/
|
||||
async getPermissionTree() {
|
||||
const response = await api.get('/role-permissions/public/menus');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取角色的菜单权限
|
||||
* @param {string} roleId - 角色ID
|
||||
* @returns {Promise<Object>} 角色菜单权限
|
||||
*/
|
||||
async getRoleMenuPermissions(roleId) {
|
||||
const response = await api.get(`/role-permissions/public/roles/${roleId}/menus`);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 设置角色菜单权限
|
||||
* @param {string} roleId - 角色ID
|
||||
* @param {Object} permissionData - 权限数据
|
||||
* @returns {Promise<Object>} 设置结果
|
||||
*/
|
||||
async setRoleMenuPermissions(roleId, permissionData) {
|
||||
const response = await api.post(`/role-permissions/roles/${roleId}/menus`, permissionData);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
// 获取所有功能权限
|
||||
async getAllPermissions(params = {}) {
|
||||
const response = await api.get('/role-permissions/public/permissions', { params });
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
// 获取权限模块列表
|
||||
async getPermissionModules() {
|
||||
const response = await api.get('/role-permissions/public/permissions/modules');
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
// 获取角色功能权限
|
||||
async getRoleFunctionPermissions(roleId) {
|
||||
const response = await api.get(`/role-permissions/public/roles/${roleId}/permissions`);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
// 设置角色功能权限
|
||||
async setRoleFunctionPermissions(roleId, data) {
|
||||
const response = await api.post(`/role-permissions/roles/${roleId}/permissions`, data);
|
||||
return response && response.success ? response : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 切换角色状态
|
||||
* @param {string} id - 角色ID
|
||||
* @param {Object} statusData - 状态数据
|
||||
* @returns {Promise<Object>} 切换结果
|
||||
*/
|
||||
async toggleRoleStatus(id, statusData) {
|
||||
const response = await api.put(`/role-permissions/roles/${id}/status`, statusData);
|
||||
return response && response.success ? response : response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 智能预警数据服务
|
||||
*/
|
||||
export const smartAlertService = {
|
||||
/**
|
||||
* 获取智能预警统计
|
||||
* @returns {Promise<Object>} 智能预警统计数据
|
||||
*/
|
||||
async getSmartAlertStats() {
|
||||
const response = await api.get('/smart-alerts/public/stats');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取智能耳标预警列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Object>} 智能耳标预警列表响应
|
||||
*/
|
||||
async getEartagAlerts(params = {}) {
|
||||
const response = await api.get('/smart-alerts/public/eartag', { params });
|
||||
return response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取智能项圈预警列表
|
||||
* @param {Object} params - 查询参数
|
||||
* @returns {Promise<Object>} 智能项圈预警列表响应
|
||||
*/
|
||||
async getCollarAlerts(params = {}) {
|
||||
const response = await api.get('/smart-alerts/public/collar', { params });
|
||||
return response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 智能设备数据服务(基于iot_jbq_client表)
|
||||
*/
|
||||
export const smartDeviceService = {
|
||||
/**
|
||||
* 获取所有智能设备
|
||||
* @returns {Promise<Array>} 智能设备列表
|
||||
*/
|
||||
async getAllSmartDevices() {
|
||||
const response = await api.get('/smart-devices/public/eartags?limit=1000');
|
||||
// 后端返回的数据结构是 { success: true, data: { list: [...] } }
|
||||
if (response && response.success && response.data && response.data.list) {
|
||||
return response.data.list;
|
||||
}
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取智能设备详情
|
||||
* @param {string} id - 设备ID
|
||||
* @returns {Promise<Object>} 设备详情
|
||||
*/
|
||||
async getSmartDeviceById(id) {
|
||||
const response = await api.get(`/smart-devices/eartags/search/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 牛只数据服务(基于iot_cattle表)
|
||||
*/
|
||||
export const cattleService = {
|
||||
/**
|
||||
* 获取所有牛只
|
||||
* @returns {Promise<Array>} 牛只列表
|
||||
*/
|
||||
async getAllCattle() {
|
||||
const response = await api.get('/iot-cattle/public?pageSize=1000');
|
||||
return response && response.success ? response.data.list : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取牛只详情
|
||||
* @param {string} id - 牛只ID
|
||||
* @returns {Promise<Object>} 牛只详情
|
||||
*/
|
||||
async getCattleById(id) {
|
||||
const response = await api.get(`/iot-cattle/public/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 栏舍数据服务(基于cattle_pens表)
|
||||
*/
|
||||
export const penService = {
|
||||
/**
|
||||
* 获取所有栏舍
|
||||
* @returns {Promise<Array>} 栏舍列表
|
||||
*/
|
||||
async getAllPens() {
|
||||
const response = await api.get('/cattle-pens/public?pageSize=1000');
|
||||
return response && response.success ? response.data.list : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取栏舍详情
|
||||
* @param {string} id - 栏舍ID
|
||||
* @returns {Promise<Object>} 栏舍详情
|
||||
*/
|
||||
async getPenById(id) {
|
||||
const response = await api.get(`/cattle-pens/public/${id}`);
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
* 创建栏舍
|
||||
* @param {Object} penData - 栏舍数据
|
||||
* @returns {Promise<Object>} 创建的栏舍
|
||||
*/
|
||||
async createPen(penData) {
|
||||
return api.post('/cattle-pens', penData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新栏舍
|
||||
* @param {string} id - 栏舍ID
|
||||
* @param {Object} penData - 栏舍数据
|
||||
* @returns {Promise<Object>} 更新后的栏舍
|
||||
*/
|
||||
async updatePen(id, penData) {
|
||||
return api.put(`/cattle-pens/${id}`, penData);
|
||||
},
|
||||
|
||||
/**
|
||||
* 删除栏舍
|
||||
* @param {string} id - 栏舍ID
|
||||
* @returns {Promise<Object>} 删除结果
|
||||
*/
|
||||
async deletePen(id) {
|
||||
return api.delete(`/cattle-pens/${id}`);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 统计数据服务
|
||||
*/
|
||||
@@ -225,7 +539,8 @@ export const statsService = {
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getDashboardStats() {
|
||||
return api.get('/stats/public/dashboard');
|
||||
const response = await api.get('/stats/public/dashboard');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -233,7 +548,8 @@ export const statsService = {
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getFarmStats() {
|
||||
return api.get('/stats/farms');
|
||||
const response = await api.get('/stats/farms');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -241,7 +557,8 @@ export const statsService = {
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getAnimalStats() {
|
||||
return api.get('/stats/animals');
|
||||
const response = await api.get('/stats/animals');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -249,7 +566,8 @@ export const statsService = {
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getDeviceStats() {
|
||||
return api.get('/stats/devices');
|
||||
const response = await api.get('/stats/devices');
|
||||
return response && response.success ? response.data : response;
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -257,6 +575,7 @@ export const statsService = {
|
||||
* @returns {Promise<Object>} 统计数据
|
||||
*/
|
||||
async getAlertStats() {
|
||||
return api.get('/stats/alerts');
|
||||
const response = await api.get('/stats/alerts');
|
||||
return response && response.success ? response.data : response;
|
||||
}
|
||||
};
|
||||
447
admin-system/frontend/src/utils/exportUtils.js
Normal file
447
admin-system/frontend/src/utils/exportUtils.js
Normal file
@@ -0,0 +1,447 @@
|
||||
import * as XLSX from 'xlsx'
|
||||
import { saveAs } from 'file-saver'
|
||||
|
||||
/**
|
||||
* 通用Excel导出工具类
|
||||
*/
|
||||
export class ExportUtils {
|
||||
/**
|
||||
* 导出数据到Excel文件
|
||||
* @param {Array} data - 要导出的数据数组
|
||||
* @param {Array} columns - 列配置数组
|
||||
* @param {String} filename - 文件名(不包含扩展名)
|
||||
* @param {String} sheetName - 工作表名称,默认为'Sheet1'
|
||||
*/
|
||||
static exportToExcel(data, columns, filename, sheetName = 'Sheet1') {
|
||||
try {
|
||||
// 验证参数
|
||||
if (!Array.isArray(data)) {
|
||||
throw new Error('数据必须是数组格式')
|
||||
}
|
||||
if (!Array.isArray(columns)) {
|
||||
throw new Error('列配置必须是数组格式')
|
||||
}
|
||||
if (!filename) {
|
||||
throw new Error('文件名不能为空')
|
||||
}
|
||||
|
||||
// 准备Excel数据
|
||||
const excelData = this.prepareExcelData(data, columns)
|
||||
|
||||
// 创建工作簿
|
||||
const workbook = XLSX.utils.book_new()
|
||||
|
||||
// 创建工作表
|
||||
const worksheet = XLSX.utils.aoa_to_sheet(excelData)
|
||||
|
||||
// 设置列宽
|
||||
const colWidths = this.calculateColumnWidths(excelData)
|
||||
worksheet['!cols'] = colWidths
|
||||
|
||||
// 添加工作表到工作簿
|
||||
XLSX.utils.book_append_sheet(workbook, worksheet, sheetName)
|
||||
|
||||
// 生成Excel文件并下载
|
||||
const excelBuffer = XLSX.write(workbook, { bookType: 'xlsx', type: 'array' })
|
||||
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
|
||||
|
||||
// 添加时间戳到文件名
|
||||
const timestamp = new Date().toISOString().slice(0, 19).replace(/[-:]/g, '').replace('T', '_')
|
||||
const finalFilename = `${filename}_${timestamp}.xlsx`
|
||||
|
||||
saveAs(blob, finalFilename)
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: '导出成功',
|
||||
filename: finalFilename
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('导出Excel失败:', error)
|
||||
return {
|
||||
success: false,
|
||||
message: `导出失败: ${error.message}`,
|
||||
error: error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 准备Excel数据格式
|
||||
*/
|
||||
static prepareExcelData(data, columns) {
|
||||
// 第一行:列标题
|
||||
const headers = columns.map(col => col.title || col.dataIndex || col.key)
|
||||
const excelData = [headers]
|
||||
|
||||
// 数据行
|
||||
data.forEach(item => {
|
||||
const row = columns.map(col => {
|
||||
const fieldName = col.dataIndex || col.key
|
||||
let value = item[fieldName]
|
||||
|
||||
// 处理特殊数据类型
|
||||
if (value === null || value === undefined) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// 处理日期时间
|
||||
if (col.dataType === 'datetime' && value) {
|
||||
return new Date(value).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 处理布尔值
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? '是' : '否'
|
||||
}
|
||||
|
||||
// 处理数组
|
||||
if (Array.isArray(value)) {
|
||||
return value.join(', ')
|
||||
}
|
||||
|
||||
// 处理对象
|
||||
if (typeof value === 'object') {
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
return String(value)
|
||||
})
|
||||
excelData.push(row)
|
||||
})
|
||||
|
||||
return excelData
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算列宽
|
||||
*/
|
||||
static calculateColumnWidths(excelData) {
|
||||
if (!excelData || excelData.length === 0) return []
|
||||
|
||||
const colCount = excelData[0].length
|
||||
const colWidths = []
|
||||
|
||||
for (let i = 0; i < colCount; i++) {
|
||||
let maxWidth = 10 // 最小宽度
|
||||
|
||||
excelData.forEach(row => {
|
||||
if (row[i]) {
|
||||
const cellWidth = String(row[i]).length
|
||||
maxWidth = Math.max(maxWidth, cellWidth)
|
||||
}
|
||||
})
|
||||
|
||||
// 限制最大宽度
|
||||
colWidths.push({ wch: Math.min(maxWidth + 2, 50) })
|
||||
}
|
||||
|
||||
return colWidths
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出智能设备数据
|
||||
*/
|
||||
static exportDeviceData(data, deviceType) {
|
||||
const deviceTypeMap = {
|
||||
collar: { name: '智能项圈', columns: this.getCollarColumns() },
|
||||
eartag: { name: '智能耳标', columns: this.getEartagColumns() },
|
||||
host: { name: '智能主机', columns: this.getHostColumns() }
|
||||
}
|
||||
|
||||
const config = deviceTypeMap[deviceType]
|
||||
if (!config) {
|
||||
throw new Error(`不支持的设备类型: ${deviceType}`)
|
||||
}
|
||||
|
||||
return this.exportToExcel(data, config.columns, config.name + '数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出预警数据
|
||||
*/
|
||||
static exportAlertData(data, alertType) {
|
||||
const alertTypeMap = {
|
||||
collar: { name: '智能项圈预警', columns: this.getCollarAlertColumns() },
|
||||
eartag: { name: '智能耳标预警', columns: this.getEartagAlertColumns() }
|
||||
}
|
||||
|
||||
const config = alertTypeMap[alertType]
|
||||
if (!config) {
|
||||
throw new Error(`不支持的预警类型: ${alertType}`)
|
||||
}
|
||||
|
||||
return this.exportToExcel(data, config.columns, config.name + '数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出牛只档案数据
|
||||
*/
|
||||
static exportCattleData(data) {
|
||||
return this.exportToExcel(data, this.getCattleColumns(), '牛只档案数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出动物数据(别名方法,与exportCattleData相同)
|
||||
*/
|
||||
static exportAnimalsData(data) {
|
||||
return this.exportCattleData(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出栏舍数据
|
||||
*/
|
||||
static exportPenData(data) {
|
||||
return this.exportToExcel(data, this.getPenColumns(), '栏舍数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出批次数据
|
||||
*/
|
||||
static exportBatchData(data) {
|
||||
return this.exportToExcel(data, this.getBatchColumns(), '批次数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出转栏记录数据
|
||||
*/
|
||||
static exportTransferData(data) {
|
||||
return this.exportToExcel(data, this.getTransferColumns(), '转栏记录数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出离栏记录数据
|
||||
*/
|
||||
static exportExitData(data) {
|
||||
return this.exportToExcel(data, this.getExitColumns(), '离栏记录数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出养殖场数据
|
||||
*/
|
||||
static exportFarmData(data) {
|
||||
return this.exportToExcel(data, this.getFarmColumns(), '养殖场数据')
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用户数据
|
||||
*/
|
||||
static exportUserData(data) {
|
||||
return this.exportToExcel(data, this.getUserColumns(), '用户数据')
|
||||
}
|
||||
|
||||
// 列配置定义
|
||||
static getCollarColumns() {
|
||||
return [
|
||||
{ title: '设备ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '设备名称', dataIndex: 'device_name', key: 'device_name' },
|
||||
{ title: '设备编号', dataIndex: 'device_code', key: 'device_code' },
|
||||
{ title: '设备状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '电量', dataIndex: 'battery_level', key: 'battery_level' },
|
||||
{ title: '信号强度', dataIndex: 'signal_strength', key: 'signal_strength' },
|
||||
{ title: '最后在线时间', dataIndex: 'last_online', key: 'last_online', dataType: 'datetime' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
static getEartagColumns() {
|
||||
return [
|
||||
{ title: '设备ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '耳标编号', dataIndex: 'eartagNumber', key: 'eartagNumber' },
|
||||
{ title: '设备状态', dataIndex: 'deviceStatus', key: 'deviceStatus' },
|
||||
{ title: '电量/%', dataIndex: 'battery', key: 'battery' },
|
||||
{ title: '设备温度/°C', dataIndex: 'temperature', key: 'temperature' },
|
||||
{ title: '被采集主机', dataIndex: 'collectedHost', key: 'collectedHost' },
|
||||
{ title: '总运动量', dataIndex: 'totalMovement', key: 'totalMovement' },
|
||||
{ title: '当日运动量', dataIndex: 'dailyMovement', key: 'dailyMovement' },
|
||||
{ title: '定位信息', dataIndex: 'location', key: 'location' },
|
||||
{ title: '数据最后更新时间', dataIndex: 'lastUpdate', key: 'lastUpdate', dataType: 'datetime' },
|
||||
{ title: '绑定牲畜', dataIndex: 'bindingStatus', key: 'bindingStatus' },
|
||||
{ title: 'GPS信号强度', dataIndex: 'gpsSignal', key: 'gpsSignal' },
|
||||
{ title: '经度', dataIndex: 'longitude', key: 'longitude' },
|
||||
{ title: '纬度', dataIndex: 'latitude', key: 'latitude' }
|
||||
]
|
||||
}
|
||||
|
||||
static getHostColumns() {
|
||||
return [
|
||||
{ title: '设备ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '设备名称', dataIndex: 'device_name', key: 'device_name' },
|
||||
{ title: '设备编号', dataIndex: 'device_code', key: 'device_code' },
|
||||
{ title: '设备状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: 'IP地址', dataIndex: 'ip_address', key: 'ip_address' },
|
||||
{ title: '端口', dataIndex: 'port', key: 'port' },
|
||||
{ title: '最后在线时间', dataIndex: 'last_online', key: 'last_online', dataType: 'datetime' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
static getCollarAlertColumns() {
|
||||
return [
|
||||
{ title: '耳标编号', dataIndex: 'collarNumber', key: 'collarNumber' },
|
||||
{ title: '预警类型', dataIndex: 'alertType', key: 'alertType' },
|
||||
{ title: '预警级别', dataIndex: 'alertLevel', key: 'alertLevel' },
|
||||
{ title: '预警时间', dataIndex: 'alertTime', key: 'alertTime', dataType: 'datetime' },
|
||||
{ title: '设备电量', dataIndex: 'battery', key: 'battery' },
|
||||
{ title: '设备温度', dataIndex: 'temperature', key: 'temperature' },
|
||||
{ title: '当日步数', dataIndex: 'dailySteps', key: 'dailySteps' }
|
||||
]
|
||||
}
|
||||
|
||||
static getEartagAlertColumns() {
|
||||
return [
|
||||
{ title: '设备编号', dataIndex: 'device_name', key: 'device_name' },
|
||||
{ title: '预警类型', dataIndex: 'alert_type', key: 'alert_type' },
|
||||
{ title: '预警级别', dataIndex: 'alert_level', key: 'alert_level' },
|
||||
{ title: '预警内容', dataIndex: 'alert_content', key: 'alert_content' },
|
||||
{ title: '预警时间', dataIndex: 'alert_time', key: 'alert_time', dataType: 'datetime' },
|
||||
{ title: '处理状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '处理人', dataIndex: 'handler', key: 'handler' }
|
||||
]
|
||||
}
|
||||
|
||||
static getCattleColumns() {
|
||||
return [
|
||||
{ title: '牛只ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '耳标号', dataIndex: 'ear_tag', key: 'ear_tag' },
|
||||
{ title: '智能佩戴设备', dataIndex: 'device_id', key: 'device_id' },
|
||||
{ title: '出生日期', dataIndex: 'birth_date', key: 'birth_date', dataType: 'datetime' },
|
||||
{ title: '月龄', dataIndex: 'age_in_months', key: 'age_in_months' },
|
||||
{ title: '品类', dataIndex: 'category', key: 'category' },
|
||||
{ title: '品种', dataIndex: 'breed', key: 'breed' },
|
||||
{ title: '生理阶段', dataIndex: 'physiological_stage', key: 'physiological_stage' },
|
||||
{ title: '性别', dataIndex: 'gender', key: 'gender' },
|
||||
{ title: '出生体重(KG)', dataIndex: 'birth_weight', key: 'birth_weight' },
|
||||
{ title: '现估值(KG)', dataIndex: 'current_weight', key: 'current_weight' },
|
||||
{ title: '代数', dataIndex: 'generation', key: 'generation' },
|
||||
{ title: '血统纯度', dataIndex: 'bloodline_purity', key: 'bloodline_purity' },
|
||||
{ title: '入场日期', dataIndex: 'entry_date', key: 'entry_date', dataType: 'datetime' },
|
||||
{ title: '栏舍', dataIndex: 'pen_name', key: 'pen_name' },
|
||||
{ title: '已产胎次', dataIndex: 'calvings', key: 'calvings' },
|
||||
{ title: '来源', dataIndex: 'source', key: 'source' },
|
||||
{ title: '冻精编号', dataIndex: 'frozen_semen_number', key: 'frozen_semen_number' }
|
||||
]
|
||||
}
|
||||
|
||||
static getPenColumns() {
|
||||
return [
|
||||
{ title: '栏舍ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '栏舍名', dataIndex: 'name', key: 'name' },
|
||||
{ title: '动物类型', dataIndex: 'animal_type', key: 'animal_type' },
|
||||
{ title: '栏舍类型', dataIndex: 'pen_type', key: 'pen_type' },
|
||||
{ title: '负责人', dataIndex: 'responsible', key: 'responsible' },
|
||||
{ title: '容量', dataIndex: 'capacity', key: 'capacity' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '创建人', dataIndex: 'creator', key: 'creator' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
static getBatchColumns() {
|
||||
return [
|
||||
{ title: '批次ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '批次名称', dataIndex: 'batch_name', key: 'batch_name' },
|
||||
{ title: '批次类型', dataIndex: 'batch_type', key: 'batch_type' },
|
||||
{ title: '目标数量', dataIndex: 'target_count', key: 'target_count' },
|
||||
{ title: '当前数量', dataIndex: 'current_count', key: 'current_count' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' },
|
||||
{ title: '负责人', dataIndex: 'manager', key: 'manager' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' }
|
||||
]
|
||||
}
|
||||
|
||||
static getTransferColumns() {
|
||||
return [
|
||||
{ title: '记录ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '牛只耳标', dataIndex: 'ear_tag', key: 'ear_tag' },
|
||||
{ title: '原栏舍', dataIndex: 'from_pen', key: 'from_pen' },
|
||||
{ title: '目标栏舍', dataIndex: 'to_pen', key: 'to_pen' },
|
||||
{ title: '转栏时间', dataIndex: 'transfer_time', key: 'transfer_time', dataType: 'datetime' },
|
||||
{ title: '转栏原因', dataIndex: 'reason', key: 'reason' },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator' }
|
||||
]
|
||||
}
|
||||
|
||||
static getExitColumns() {
|
||||
return [
|
||||
{ title: '记录ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '牛只耳标', dataIndex: 'ear_tag', key: 'ear_tag' },
|
||||
{ title: '原栏舍', dataIndex: 'from_pen', key: 'from_pen' },
|
||||
{ title: '离栏时间', dataIndex: 'exit_time', key: 'exit_time', dataType: 'datetime' },
|
||||
{ title: '离栏原因', dataIndex: 'exit_reason', key: 'exit_reason' },
|
||||
{ title: '去向', dataIndex: 'destination', key: 'destination' },
|
||||
{ title: '操作人', dataIndex: 'operator', key: 'operator' }
|
||||
]
|
||||
}
|
||||
|
||||
static getFarmColumns() {
|
||||
return [
|
||||
{ title: '养殖场ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '养殖场名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '地址', dataIndex: 'address', key: 'address' },
|
||||
{ title: '联系电话', dataIndex: 'phone', key: 'phone' },
|
||||
{ title: '负责人', dataIndex: 'contact', key: 'contact' },
|
||||
{ title: '面积', dataIndex: 'area', key: 'area' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'createdAt', key: 'createdAt', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
static getUserColumns() {
|
||||
return [
|
||||
{ title: '用户ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
||||
{ title: '角色', dataIndex: 'roleName', key: 'roleName' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '最后登录', dataIndex: 'last_login', key: 'last_login', dataType: 'datetime' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
// 其他导出方法(为了兼容性)
|
||||
static exportFarmsData(data) {
|
||||
return this.exportFarmData(data)
|
||||
}
|
||||
|
||||
static exportAlertsData(data) {
|
||||
return this.exportAlertData(data, 'collar') // 默认使用collar类型
|
||||
}
|
||||
|
||||
static exportDevicesData(data) {
|
||||
return this.exportDeviceData(data, 'collar') // 默认使用collar类型
|
||||
}
|
||||
|
||||
static exportOrdersData(data) {
|
||||
return this.exportToExcel(data, this.getOrderColumns(), '订单数据')
|
||||
}
|
||||
|
||||
static exportProductsData(data) {
|
||||
return this.exportToExcel(data, this.getProductColumns(), '产品数据')
|
||||
}
|
||||
|
||||
static getOrderColumns() {
|
||||
return [
|
||||
{ title: '订单ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '订单号', dataIndex: 'order_number', key: 'order_number' },
|
||||
{ title: '客户名称', dataIndex: 'customer_name', key: 'customer_name' },
|
||||
{ title: '订单金额', dataIndex: 'total_amount', key: 'total_amount' },
|
||||
{ title: '订单状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
|
||||
static getProductColumns() {
|
||||
return [
|
||||
{ title: '产品ID', dataIndex: 'id', key: 'id' },
|
||||
{ title: '产品名称', dataIndex: 'name', key: 'name' },
|
||||
{ title: '产品类型', dataIndex: 'type', key: 'type' },
|
||||
{ title: '价格', dataIndex: 'price', key: 'price' },
|
||||
{ title: '库存', dataIndex: 'stock', key: 'stock' },
|
||||
{ title: '状态', dataIndex: 'status', key: 'status' },
|
||||
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', dataType: 'datetime' }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export default ExportUtils
|
||||
664
admin-system/frontend/src/utils/mapService.js
Normal file
664
admin-system/frontend/src/utils/mapService.js
Normal file
@@ -0,0 +1,664 @@
|
||||
/**
|
||||
* 百度地图服务工具
|
||||
* 封装了百度地图API的初始化和操作方法
|
||||
*/
|
||||
|
||||
// 百度地图API加载状态
|
||||
let BMapLoaded = false;
|
||||
let loadingPromise = null;
|
||||
|
||||
/**
|
||||
* 加载百度地图API
|
||||
* @param {number} retryCount 重试次数
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
export const loadBMapScript = async (retryCount = 0) => {
|
||||
// 如果已经加载过,直接返回
|
||||
if (BMapLoaded) {
|
||||
console.log('百度地图API已加载');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 如果正在加载中,返回加载Promise
|
||||
if (loadingPromise) {
|
||||
console.log('百度地图API正在加载中...');
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
console.log(`开始加载百度地图API... (重试次数: ${retryCount})`);
|
||||
|
||||
// 创建加载Promise
|
||||
loadingPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 导入环境配置
|
||||
const { BAIDU_MAP_CONFIG } = await import('../config/env');
|
||||
|
||||
console.log('使用API密钥:', BAIDU_MAP_CONFIG.apiKey);
|
||||
console.log('完整配置:', BAIDU_MAP_CONFIG);
|
||||
|
||||
// 检查API密钥是否有效
|
||||
if (!BAIDU_MAP_CONFIG.apiKey || BAIDU_MAP_CONFIG.apiKey === 'YOUR_VALID_BAIDU_MAP_API_KEY') {
|
||||
const error = new Error('百度地图API密钥未配置或无效');
|
||||
console.error('API密钥错误:', error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 构建API URL,包含Referer参数
|
||||
let apiUrl = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_CONFIG.apiKey}`;
|
||||
|
||||
// 如果启用Referer校验,添加当前域名
|
||||
if (BAIDU_MAP_CONFIG.enableRefererCheck) {
|
||||
const currentDomain = window.location.hostname;
|
||||
apiUrl += `&callback=initBMap&referer=${encodeURIComponent(currentDomain)}`;
|
||||
} else {
|
||||
apiUrl += '&callback=initBMap';
|
||||
}
|
||||
|
||||
console.log('百度地图API URL:', apiUrl);
|
||||
|
||||
// 检查是否已经存在BMap
|
||||
if (typeof window.BMap !== 'undefined') {
|
||||
console.log('BMap已存在,直接使用');
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建全局回调函数
|
||||
window.initBMap = () => {
|
||||
console.log('百度地图API加载成功,BMap对象类型:', typeof window.BMap);
|
||||
console.log('BMap对象详情:', window.BMap);
|
||||
console.log('BMap.Map是否存在:', typeof window.BMap?.Map);
|
||||
|
||||
// 清理超时定时器
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
// 等待BMap完全初始化
|
||||
setTimeout(() => {
|
||||
// 详细验证BMap对象
|
||||
if (!window.BMap) {
|
||||
console.error('BMap对象不存在');
|
||||
reject(new Error('BMap对象不存在'));
|
||||
delete window.initBMap;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.BMap.Map !== 'function') {
|
||||
console.error('BMap.Map构造函数不可用');
|
||||
reject(new Error('BMap.Map构造函数不可用'));
|
||||
delete window.initBMap;
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.BMap.Point !== 'function') {
|
||||
console.error('BMap.Point构造函数不可用');
|
||||
reject(new Error('BMap.Point构造函数不可用'));
|
||||
delete window.initBMap;
|
||||
return;
|
||||
}
|
||||
|
||||
// 测试创建Point对象
|
||||
try {
|
||||
const testPoint = new window.BMap.Point(0, 0);
|
||||
if (!testPoint || typeof testPoint.lng !== 'number') {
|
||||
throw new Error('Point对象创建失败');
|
||||
}
|
||||
console.log('BMap对象验证通过,版本:', window.BMap.version || '未知');
|
||||
} catch (error) {
|
||||
console.error('BMap对象功能测试失败:', error);
|
||||
reject(new Error(`BMap对象功能测试失败: ${error.message}`));
|
||||
delete window.initBMap;
|
||||
return;
|
||||
}
|
||||
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
// 清理全局回调
|
||||
delete window.initBMap;
|
||||
}, 200); // 增加等待时间
|
||||
};
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
// 使用构建的API URL
|
||||
script.src = apiUrl;
|
||||
|
||||
console.log('百度地图API URL:', script.src);
|
||||
|
||||
script.onerror = async (error) => {
|
||||
console.error('百度地图脚本加载失败:', error);
|
||||
console.error('失败的URL:', script.src);
|
||||
// 清理超时定时器
|
||||
clearTimeout(timeoutId);
|
||||
// 清理script标签
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
// 清理全局回调
|
||||
if (window.initBMap) {
|
||||
delete window.initBMap;
|
||||
}
|
||||
|
||||
// 尝试使用备用API密钥
|
||||
if (BAIDU_MAP_CONFIG.fallbackApiKey && BAIDU_MAP_CONFIG.fallbackApiKey !== 'YOUR_FALLBACK_API_KEY') {
|
||||
console.log('尝试使用备用API密钥...');
|
||||
try {
|
||||
const fallbackUrl = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_CONFIG.fallbackApiKey}&callback=initBMap`;
|
||||
const fallbackScript = document.createElement('script');
|
||||
fallbackScript.type = 'text/javascript';
|
||||
fallbackScript.src = fallbackUrl;
|
||||
|
||||
fallbackScript.onerror = () => {
|
||||
console.error('备用API密钥也加载失败');
|
||||
if (retryCount < BAIDU_MAP_CONFIG.maxRetries) {
|
||||
retryLoad();
|
||||
} else {
|
||||
reject(new Error('百度地图API加载失败,主密钥和备用密钥都无法使用'));
|
||||
}
|
||||
};
|
||||
|
||||
window.initBMap = () => {
|
||||
console.log('备用API密钥加载成功');
|
||||
BMapLoaded = true;
|
||||
loadingPromise = null;
|
||||
resolve();
|
||||
delete window.initBMap;
|
||||
};
|
||||
|
||||
document.head.appendChild(fallbackScript);
|
||||
return;
|
||||
} catch (fallbackError) {
|
||||
console.error('备用API密钥加载出错:', fallbackError);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否可以重试
|
||||
if (retryCount < BAIDU_MAP_CONFIG.maxRetries) {
|
||||
retryLoad();
|
||||
} else {
|
||||
reject(new Error('百度地图脚本加载失败,已达到最大重试次数'));
|
||||
}
|
||||
|
||||
function retryLoad() {
|
||||
console.log(`准备重试加载百度地图API (${retryCount + 1}/${BAIDU_MAP_CONFIG.maxRetries})`);
|
||||
loadingPromise = null; // 重置加载状态
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await loadBMapScript(retryCount + 1);
|
||||
resolve();
|
||||
} catch (retryError) {
|
||||
reject(retryError);
|
||||
}
|
||||
}, BAIDU_MAP_CONFIG.retryDelay);
|
||||
}
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(async () => {
|
||||
if (!BMapLoaded) {
|
||||
console.error('百度地图API加载超时');
|
||||
// 清理script标签
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
// 清理全局回调
|
||||
if (window.initBMap) {
|
||||
delete window.initBMap;
|
||||
}
|
||||
|
||||
// 检查是否可以重试
|
||||
if (retryCount < BAIDU_MAP_CONFIG.maxRetries) {
|
||||
console.log(`准备重试加载百度地图API (${retryCount + 1}/${BAIDU_MAP_CONFIG.maxRetries})`);
|
||||
loadingPromise = null; // 重置加载状态
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await loadBMapScript(retryCount + 1);
|
||||
resolve();
|
||||
} catch (retryError) {
|
||||
reject(retryError);
|
||||
}
|
||||
}, BAIDU_MAP_CONFIG.retryDelay);
|
||||
} else {
|
||||
reject(new Error('百度地图API加载超时,已达到最大重试次数'));
|
||||
}
|
||||
}
|
||||
}, 30000); // 增加超时时间到30秒
|
||||
|
||||
// 添加到文档中
|
||||
document.head.appendChild(script);
|
||||
} catch (error) {
|
||||
console.error('加载百度地图API时出错:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建百度地图实例
|
||||
* @param {HTMLElement} container - 地图容器元素
|
||||
* @param {Object} options - 地图配置选项
|
||||
* @returns {Promise<BMap.Map>} 地图实例
|
||||
*/
|
||||
export const createMap = async (container, options = {}) => {
|
||||
try {
|
||||
console.log('开始创建地图,容器:', container);
|
||||
console.log('容器尺寸:', container.offsetWidth, 'x', container.offsetHeight);
|
||||
|
||||
// 确保百度地图API已加载
|
||||
await loadBMapScript();
|
||||
|
||||
// 导入环境配置
|
||||
const { BAIDU_MAP_CONFIG } = await import('../config/env');
|
||||
|
||||
console.log('百度地图配置:', BAIDU_MAP_CONFIG);
|
||||
|
||||
// 检查BMap是否可用
|
||||
if (typeof window.BMap === 'undefined' || !window.BMap) {
|
||||
const error = new Error('百度地图API未正确加载,BMap对象不存在');
|
||||
console.error('BMap未定义:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 验证BMap对象的关键属性
|
||||
if (typeof window.BMap.Map !== 'function') {
|
||||
const error = new Error('BMap.Map构造函数不可用');
|
||||
console.error('BMap.Map不可用:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (typeof window.BMap.Point !== 'function') {
|
||||
const error = new Error('BMap.Point构造函数不可用');
|
||||
console.error('BMap.Point不可用:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('BMap对象可用,版本:', window.BMap.version || '未知');
|
||||
console.log('BMap对象详细信息:', {
|
||||
Map: typeof window.BMap.Map,
|
||||
Point: typeof window.BMap.Point,
|
||||
Marker: typeof window.BMap.Marker,
|
||||
InfoWindow: typeof window.BMap.InfoWindow
|
||||
});
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
center: new BMap.Point(BAIDU_MAP_CONFIG.defaultCenter.lng, BAIDU_MAP_CONFIG.defaultCenter.lat), // 宁夏中心点
|
||||
zoom: BAIDU_MAP_CONFIG.defaultZoom, // 默认缩放级别
|
||||
enableMapClick: true, // 启用地图点击
|
||||
enableScrollWheelZoom: true, // 启用滚轮缩放
|
||||
enableDragging: true, // 启用拖拽
|
||||
enableDoubleClickZoom: true, // 启用双击缩放
|
||||
enableKeyboard: true // 启用键盘控制
|
||||
};
|
||||
|
||||
// 合并配置
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('地图配置选项:', mergedOptions);
|
||||
|
||||
// 验证容器
|
||||
if (!container || !container.offsetWidth || !container.offsetHeight) {
|
||||
throw new Error('地图容器无效或没有设置尺寸');
|
||||
}
|
||||
|
||||
// 验证BMap对象
|
||||
if (!window.BMap || typeof window.BMap.Map !== 'function') {
|
||||
throw new Error('BMap对象未正确加载');
|
||||
}
|
||||
|
||||
// 检查容器及其父级的样式问题
|
||||
let currentElement = container;
|
||||
while (currentElement && currentElement !== document.body) {
|
||||
const computedStyle = window.getComputedStyle(currentElement);
|
||||
console.log('检查元素样式:', {
|
||||
tagName: currentElement.tagName,
|
||||
id: currentElement.id,
|
||||
className: currentElement.className,
|
||||
position: computedStyle.position,
|
||||
display: computedStyle.display,
|
||||
visibility: computedStyle.visibility
|
||||
});
|
||||
|
||||
// 检查是否有问题样式
|
||||
if (computedStyle.position === 'fixed') {
|
||||
console.warn('发现position: fixed的父级元素,可能导致地图初始化失败');
|
||||
}
|
||||
if (computedStyle.display === 'none') {
|
||||
console.warn('发现display: none的父级元素,可能导致地图初始化失败');
|
||||
}
|
||||
|
||||
currentElement = currentElement.parentElement;
|
||||
}
|
||||
|
||||
// 临时修复:确保容器有正确的样式
|
||||
const originalStyle = {
|
||||
position: container.style.position,
|
||||
display: container.style.display,
|
||||
visibility: container.style.visibility
|
||||
};
|
||||
|
||||
// 设置临时样式确保地图能正确初始化
|
||||
container.style.position = 'relative';
|
||||
container.style.display = 'block';
|
||||
container.style.visibility = 'visible';
|
||||
|
||||
console.log('创建BMap.Map实例...');
|
||||
console.log('容器信息:', {
|
||||
id: container.id,
|
||||
className: container.className,
|
||||
offsetWidth: container.offsetWidth,
|
||||
offsetHeight: container.offsetHeight,
|
||||
clientWidth: container.clientWidth,
|
||||
clientHeight: container.clientHeight,
|
||||
style: container.style.cssText
|
||||
});
|
||||
|
||||
// 最终验证容器状态
|
||||
if (!container || !container.parentNode) {
|
||||
throw new Error('地图容器无效或已从DOM中移除');
|
||||
}
|
||||
|
||||
if (container.offsetWidth === 0 || container.offsetHeight === 0) {
|
||||
throw new Error('地图容器尺寸为0,无法创建地图');
|
||||
}
|
||||
|
||||
let map;
|
||||
try {
|
||||
// 临时移除所有可能干扰的样式
|
||||
const tempStyles = {};
|
||||
const styleProps = ['position', 'display', 'visibility', 'transform', 'opacity', 'zIndex'];
|
||||
|
||||
styleProps.forEach(prop => {
|
||||
tempStyles[prop] = container.style[prop];
|
||||
container.style[prop] = '';
|
||||
});
|
||||
|
||||
// 确保容器有基本样式
|
||||
container.style.position = 'relative';
|
||||
container.style.display = 'block';
|
||||
container.style.visibility = 'visible';
|
||||
container.style.width = container.offsetWidth + 'px';
|
||||
container.style.height = container.offsetHeight + 'px';
|
||||
|
||||
console.log('创建地图前的容器状态:', {
|
||||
offsetWidth: container.offsetWidth,
|
||||
offsetHeight: container.offsetHeight,
|
||||
style: container.style.cssText
|
||||
});
|
||||
|
||||
// 再次验证BMap对象
|
||||
if (!window.BMap || typeof window.BMap.Map !== 'function') {
|
||||
throw new Error('BMap.Map构造函数不可用');
|
||||
}
|
||||
|
||||
// 创建地图实例,添加错误捕获
|
||||
try {
|
||||
map = new window.BMap.Map(container);
|
||||
console.log('地图实例创建成功:', map);
|
||||
} catch (mapError) {
|
||||
console.error('BMap.Map构造函数调用失败:', mapError);
|
||||
throw new Error(`地图实例创建失败: ${mapError.message}`);
|
||||
}
|
||||
|
||||
// 恢复临时移除的样式
|
||||
styleProps.forEach(prop => {
|
||||
if (tempStyles[prop]) {
|
||||
container.style[prop] = tempStyles[prop];
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('地图实例创建失败:', error);
|
||||
// 恢复所有原始样式
|
||||
container.style.position = originalStyle.position;
|
||||
container.style.display = originalStyle.display;
|
||||
container.style.visibility = originalStyle.visibility;
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 恢复原始样式(如果地图创建成功)
|
||||
container.style.position = originalStyle.position;
|
||||
container.style.display = originalStyle.display;
|
||||
container.style.visibility = originalStyle.visibility;
|
||||
|
||||
// 验证地图实例
|
||||
if (!map || typeof map.centerAndZoom !== 'function') {
|
||||
throw new Error('地图实例创建失败');
|
||||
}
|
||||
|
||||
// 设置中心点和缩放级别
|
||||
console.log('设置地图中心点和缩放级别:', mergedOptions.center, mergedOptions.zoom);
|
||||
map.centerAndZoom(mergedOptions.center, mergedOptions.zoom);
|
||||
|
||||
// 监听地图加载完成事件
|
||||
map.addEventListener('tilesloaded', function() {
|
||||
console.log('百度地图瓦片加载完成');
|
||||
});
|
||||
|
||||
map.addEventListener('load', function() {
|
||||
console.log('百度地图完全加载完成');
|
||||
});
|
||||
|
||||
// 强制刷新地图
|
||||
setTimeout(() => {
|
||||
console.log('强制刷新地图');
|
||||
map.reset();
|
||||
}, 100);
|
||||
|
||||
// 延迟确保地图完全渲染
|
||||
setTimeout(() => {
|
||||
console.log('地图渲染完成');
|
||||
// 移除map.reset()调用,避免缩放时重置地图
|
||||
}, 100);
|
||||
|
||||
// 添加地图类型控件
|
||||
console.log('添加地图控件...');
|
||||
map.addControl(new window.BMap.MapTypeControl());
|
||||
|
||||
// 添加增强的缩放控件
|
||||
const navigationControl = new window.BMap.NavigationControl({
|
||||
anchor: window.BMAP_ANCHOR_TOP_LEFT,
|
||||
type: window.BMAP_NAVIGATION_CONTROL_LARGE,
|
||||
enableGeolocation: false
|
||||
});
|
||||
map.addControl(navigationControl);
|
||||
|
||||
// 添加缩放控件(滑块式)
|
||||
const scaleControl = new window.BMap.ScaleControl({
|
||||
anchor: window.BMAP_ANCHOR_BOTTOM_LEFT
|
||||
});
|
||||
map.addControl(scaleControl);
|
||||
|
||||
// 添加概览地图控件
|
||||
const overviewMapControl = new window.BMap.OverviewMapControl({
|
||||
anchor: window.BMAP_ANCHOR_TOP_RIGHT,
|
||||
isOpen: false
|
||||
});
|
||||
map.addControl(overviewMapControl);
|
||||
|
||||
// 设置缩放范围
|
||||
map.setMinZoom(3);
|
||||
map.setMaxZoom(19);
|
||||
|
||||
// 配置地图功能
|
||||
if (mergedOptions.enableScrollWheelZoom) {
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDragging) {
|
||||
map.enableDragging();
|
||||
} else {
|
||||
map.disableDragging();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDoubleClickZoom) {
|
||||
map.enableDoubleClickZoom();
|
||||
} else {
|
||||
map.disableDoubleClickZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableKeyboard) {
|
||||
map.enableKeyboard();
|
||||
} else {
|
||||
map.disableKeyboard();
|
||||
}
|
||||
|
||||
console.log('百度地图创建成功:', map);
|
||||
|
||||
return map;
|
||||
} catch (error) {
|
||||
console.error('创建百度地图失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在地图上添加标记
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
* @param {Array} markers - 标记点数据数组
|
||||
* @param {Function} onClick - 点击标记的回调函数
|
||||
* @returns {Array<BMap.Marker>} 标记点实例数组
|
||||
*/
|
||||
export const addMarkers = (map, markers = [], onClick = null) => {
|
||||
// 验证地图实例是否有效
|
||||
if (!map || typeof map.addOverlay !== 'function') {
|
||||
console.error('addMarkers: 无效的地图实例', map);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 验证BMap对象是否可用
|
||||
if (!window.BMap || typeof window.BMap.Point !== 'function' || typeof window.BMap.Marker !== 'function') {
|
||||
console.error('addMarkers: BMap对象不可用');
|
||||
return [];
|
||||
}
|
||||
|
||||
return markers.map(markerData => {
|
||||
try {
|
||||
// 验证标记数据
|
||||
if (!markerData || !markerData.location || typeof markerData.location.lng !== 'number' || typeof markerData.location.lat !== 'number') {
|
||||
console.error('addMarkers: 无效的标记数据', markerData);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 创建标记点
|
||||
const point = new window.BMap.Point(markerData.location.lng, markerData.location.lat);
|
||||
const marker = new window.BMap.Marker(point);
|
||||
|
||||
// 添加标记到地图
|
||||
map.addOverlay(marker);
|
||||
|
||||
// 创建信息窗口
|
||||
if (markerData.title || markerData.content) {
|
||||
const infoWindow = new window.BMap.InfoWindow(
|
||||
`<div class="map-info-window">
|
||||
${markerData.content || ''}
|
||||
</div>`,
|
||||
{
|
||||
title: markerData.title || '',
|
||||
width: 250,
|
||||
height: 120,
|
||||
enableMessage: false
|
||||
}
|
||||
);
|
||||
|
||||
// 添加点击事件
|
||||
marker.addEventListener('click', () => {
|
||||
marker.openInfoWindow(infoWindow);
|
||||
|
||||
// 如果有自定义点击回调,则调用
|
||||
if (onClick && typeof onClick === 'function') {
|
||||
onClick(markerData, marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return marker;
|
||||
} catch (error) {
|
||||
console.error('addMarkers: 创建标记失败:', error, markerData);
|
||||
return null;
|
||||
}
|
||||
}).filter(marker => marker !== null);
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除地图上的所有覆盖物
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
*/
|
||||
export const clearOverlays = (map) => {
|
||||
if (!map || typeof map.clearOverlays !== 'function') {
|
||||
console.warn('clearOverlays: 无效的地图实例,已跳过清理');
|
||||
return;
|
||||
}
|
||||
map.clearOverlays();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置地图视图以包含所有标记点
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
* @param {Array<BMap.Point>} points - 点数组
|
||||
* @param {Number} padding - 边距,单位像素
|
||||
*/
|
||||
export const setViewToFitMarkers = (map, points, padding = 50) => {
|
||||
if (!map) {
|
||||
console.warn('setViewToFitMarkers: 无效的地图实例,已跳过设置视图');
|
||||
return;
|
||||
}
|
||||
if (!points || points.length === 0) return;
|
||||
|
||||
// 如果只有一个点,直接居中
|
||||
if (points.length === 1) {
|
||||
map.centerAndZoom(points[0], 15);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建视图范围
|
||||
const viewport = map.getViewport(points, { margins: [padding, padding, padding, padding] });
|
||||
map.setViewport(viewport);
|
||||
};
|
||||
|
||||
/**
|
||||
* 转换养殖场数据为地图标记数据
|
||||
* @param {Array} farms - 养殖场数据数组
|
||||
* @returns {Array} 地图标记数据数组
|
||||
*/
|
||||
export const convertFarmsToMarkers = (farms = []) => {
|
||||
return farms
|
||||
.filter(farm => {
|
||||
// 只保留有有效位置信息的农场
|
||||
return farm.location &&
|
||||
typeof farm.location.lat === 'number' &&
|
||||
typeof farm.location.lng === 'number';
|
||||
})
|
||||
.map(farm => {
|
||||
// 计算动物总数
|
||||
const animalCount = farm.animals ?
|
||||
farm.animals.reduce((total, animal) => total + (animal.count || 0), 0) : 0;
|
||||
|
||||
// 计算在线设备数
|
||||
const onlineDevices = farm.devices ?
|
||||
farm.devices.filter(device => device.status === 'online').length : 0;
|
||||
const totalDevices = farm.devices ? farm.devices.length : 0;
|
||||
|
||||
return {
|
||||
id: farm.id,
|
||||
title: farm.name,
|
||||
location: farm.location,
|
||||
content: `
|
||||
<div style="padding: 8px; font-size: 14px;">
|
||||
<h4 style="margin: 0 0 8px 0; color: #1890ff;">${farm.name}</h4>
|
||||
<p style="margin: 4px 0;"><strong>类型:</strong> ${farm.type || '未知'}</p>
|
||||
<p style="margin: 4px 0;"><strong>动物数量:</strong> ${animalCount} 只</p>
|
||||
<p style="margin: 4px 0;"><strong>设备状态:</strong> ${onlineDevices}/${totalDevices} 在线</p>
|
||||
<p style="margin: 4px 0;"><strong>联系人:</strong> ${farm.contact || '未知'}</p>
|
||||
<p style="margin: 4px 0;"><strong>地址:</strong> ${farm.address || '未知'}</p>
|
||||
<p style="margin: 4px 0; color: #666;"><strong>坐标:</strong> ${farm.location.lat.toFixed(4)}, ${farm.location.lng.toFixed(4)}</p>
|
||||
</div>
|
||||
`,
|
||||
originalData: farm
|
||||
};
|
||||
});
|
||||
};
|
||||
@@ -1,353 +0,0 @@
|
||||
/**
|
||||
* 百度地图服务工具
|
||||
* 封装了百度地图API的初始化和操作方法
|
||||
*/
|
||||
|
||||
// 百度地图API加载状态
|
||||
let BMapLoaded = false;
|
||||
let loadingPromise = null;
|
||||
|
||||
/**
|
||||
* 加载百度地图API
|
||||
* @returns {Promise} 加载完成的Promise
|
||||
*/
|
||||
export const loadBMapScript = async () => {
|
||||
// 如果已经加载过,直接返回
|
||||
if (BMapLoaded) {
|
||||
console.log('百度地图API已加载');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
// 如果正在加载中,返回加载Promise
|
||||
if (loadingPromise) {
|
||||
console.log('百度地图API正在加载中...');
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
console.log('开始加载百度地图API...');
|
||||
|
||||
// 创建加载Promise
|
||||
loadingPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
// 导入环境配置
|
||||
const { BAIDU_MAP_CONFIG } = await import('../config/env');
|
||||
|
||||
console.log('使用API密钥:', BAIDU_MAP_CONFIG.apiKey);
|
||||
console.log('完整配置:', BAIDU_MAP_CONFIG);
|
||||
|
||||
// 检查API密钥是否有效
|
||||
if (!BAIDU_MAP_CONFIG.apiKey || BAIDU_MAP_CONFIG.apiKey === 'YOUR_VALID_BAIDU_MAP_API_KEY') {
|
||||
const error = new Error('百度地图API密钥未配置或无效');
|
||||
console.error('API密钥错误:', error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经存在BMap
|
||||
if (typeof window.BMap !== 'undefined') {
|
||||
console.log('BMap已存在,直接使用');
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建全局回调函数
|
||||
window.initBMap = () => {
|
||||
console.log('百度地图API加载成功,BMap对象类型:', typeof window.BMap);
|
||||
console.log('BMap对象详情:', window.BMap);
|
||||
console.log('BMap.Map是否存在:', typeof window.BMap?.Map);
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
// 清理全局回调
|
||||
delete window.initBMap;
|
||||
};
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
// 使用配置文件中的API密钥
|
||||
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_CONFIG.apiKey}&callback=initBMap`;
|
||||
|
||||
console.log('百度地图API URL:', script.src);
|
||||
|
||||
script.onerror = (error) => {
|
||||
console.error('百度地图脚本加载失败:', error);
|
||||
reject(new Error('百度地图脚本加载失败'));
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
setTimeout(() => {
|
||||
if (!BMapLoaded) {
|
||||
reject(new Error('百度地图API加载超时'));
|
||||
}
|
||||
}, 10000);
|
||||
|
||||
// 添加到文档中
|
||||
document.head.appendChild(script);
|
||||
} catch (error) {
|
||||
console.error('加载百度地图API时出错:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建百度地图实例
|
||||
* @param {HTMLElement} container - 地图容器元素
|
||||
* @param {Object} options - 地图配置选项
|
||||
* @returns {Promise<BMap.Map>} 地图实例
|
||||
*/
|
||||
export const createMap = async (container, options = {}) => {
|
||||
try {
|
||||
console.log('开始创建地图,容器:', container);
|
||||
console.log('容器尺寸:', container.offsetWidth, 'x', container.offsetHeight);
|
||||
|
||||
// 确保百度地图API已加载
|
||||
await loadBMapScript();
|
||||
|
||||
// 导入环境配置
|
||||
const { BAIDU_MAP_CONFIG } = await import('../config/env');
|
||||
|
||||
console.log('百度地图配置:', BAIDU_MAP_CONFIG);
|
||||
|
||||
// 检查BMap是否可用
|
||||
if (typeof window.BMap === 'undefined') {
|
||||
const error = new Error('百度地图API未正确加载');
|
||||
console.error('BMap未定义:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log('BMap对象可用,版本:', window.BMap.version || '未知');
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
center: new BMap.Point(BAIDU_MAP_CONFIG.defaultCenter.lng, BAIDU_MAP_CONFIG.defaultCenter.lat), // 宁夏中心点
|
||||
zoom: BAIDU_MAP_CONFIG.defaultZoom, // 默认缩放级别
|
||||
enableMapClick: true, // 启用地图点击
|
||||
enableScrollWheelZoom: true, // 启用滚轮缩放
|
||||
enableDragging: true, // 启用拖拽
|
||||
enableDoubleClickZoom: true, // 启用双击缩放
|
||||
enableKeyboard: true // 启用键盘控制
|
||||
};
|
||||
|
||||
// 合并配置
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('地图配置选项:', mergedOptions);
|
||||
|
||||
// 创建地图实例
|
||||
console.log('创建BMap.Map实例...');
|
||||
const map = new window.BMap.Map(container);
|
||||
console.log('地图实例创建成功:', map);
|
||||
|
||||
// 监听地图加载完成事件
|
||||
map.addEventListener('tilesloaded', function() {
|
||||
console.log('百度地图瓦片加载完成');
|
||||
});
|
||||
|
||||
map.addEventListener('load', function() {
|
||||
console.log('百度地图完全加载完成');
|
||||
});
|
||||
|
||||
// 设置中心点和缩放级别
|
||||
console.log('设置地图中心点和缩放级别:', mergedOptions.center, mergedOptions.zoom);
|
||||
map.centerAndZoom(mergedOptions.center, mergedOptions.zoom);
|
||||
|
||||
// 延迟确保地图完全渲染
|
||||
setTimeout(() => {
|
||||
console.log('地图渲染完成');
|
||||
// 移除map.reset()调用,避免缩放时重置地图
|
||||
}, 100);
|
||||
|
||||
// 添加地图类型控件
|
||||
console.log('添加地图控件...');
|
||||
map.addControl(new window.BMap.MapTypeControl());
|
||||
|
||||
// 添加增强的缩放控件
|
||||
const navigationControl = new window.BMap.NavigationControl({
|
||||
anchor: window.BMAP_ANCHOR_TOP_LEFT,
|
||||
type: window.BMAP_NAVIGATION_CONTROL_LARGE,
|
||||
enableGeolocation: false
|
||||
});
|
||||
map.addControl(navigationControl);
|
||||
|
||||
// 添加缩放控件(滑块式)
|
||||
const scaleControl = new window.BMap.ScaleControl({
|
||||
anchor: window.BMAP_ANCHOR_BOTTOM_LEFT
|
||||
});
|
||||
map.addControl(scaleControl);
|
||||
|
||||
// 添加概览地图控件
|
||||
const overviewMapControl = new window.BMap.OverviewMapControl({
|
||||
anchor: window.BMAP_ANCHOR_TOP_RIGHT,
|
||||
isOpen: false
|
||||
});
|
||||
map.addControl(overviewMapControl);
|
||||
|
||||
// 设置缩放范围
|
||||
map.setMinZoom(3);
|
||||
map.setMaxZoom(19);
|
||||
|
||||
// 配置地图功能
|
||||
if (mergedOptions.enableScrollWheelZoom) {
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDragging) {
|
||||
map.enableDragging();
|
||||
} else {
|
||||
map.disableDragging();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableDoubleClickZoom) {
|
||||
map.enableDoubleClickZoom();
|
||||
} else {
|
||||
map.disableDoubleClickZoom();
|
||||
}
|
||||
|
||||
if (mergedOptions.enableKeyboard) {
|
||||
map.enableKeyboard();
|
||||
} else {
|
||||
map.disableKeyboard();
|
||||
}
|
||||
|
||||
console.log('百度地图创建成功:', map);
|
||||
|
||||
return map;
|
||||
} catch (error) {
|
||||
console.error('创建百度地图失败:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 在地图上添加标记
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
* @param {Array} markers - 标记点数据数组
|
||||
* @param {Function} onClick - 点击标记的回调函数
|
||||
* @returns {Array<BMap.Marker>} 标记点实例数组
|
||||
*/
|
||||
export const addMarkers = (map, markers = [], onClick = null) => {
|
||||
// 验证地图实例是否有效
|
||||
if (!map || typeof map.addOverlay !== 'function') {
|
||||
console.error('addMarkers: 无效的地图实例', map);
|
||||
return [];
|
||||
}
|
||||
|
||||
return markers.map(markerData => {
|
||||
// 创建标记点
|
||||
const point = new BMap.Point(markerData.location.lng, markerData.location.lat);
|
||||
const marker = new BMap.Marker(point);
|
||||
|
||||
// 添加标记到地图
|
||||
map.addOverlay(marker);
|
||||
|
||||
// 创建信息窗口
|
||||
if (markerData.title || markerData.content) {
|
||||
const infoWindow = new BMap.InfoWindow(
|
||||
`<div class="map-info-window">
|
||||
${markerData.content || ''}
|
||||
</div>`,
|
||||
{
|
||||
title: markerData.title || '',
|
||||
width: 250,
|
||||
height: 120,
|
||||
enableMessage: false
|
||||
}
|
||||
);
|
||||
|
||||
// 添加点击事件
|
||||
marker.addEventListener('click', () => {
|
||||
marker.openInfoWindow(infoWindow);
|
||||
|
||||
// 如果有自定义点击回调,则调用
|
||||
if (onClick && typeof onClick === 'function') {
|
||||
onClick(markerData, marker);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return marker;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除地图上的所有覆盖物
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
*/
|
||||
export const clearOverlays = (map) => {
|
||||
if (!map || typeof map.clearOverlays !== 'function') {
|
||||
console.warn('clearOverlays: 无效的地图实例,已跳过清理');
|
||||
return;
|
||||
}
|
||||
map.clearOverlays();
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置地图视图以包含所有标记点
|
||||
* @param {BMap.Map} map - 百度地图实例
|
||||
* @param {Array<BMap.Point>} points - 点数组
|
||||
* @param {Number} padding - 边距,单位像素
|
||||
*/
|
||||
export const setViewToFitMarkers = (map, points, padding = 50) => {
|
||||
if (!map) {
|
||||
console.warn('setViewToFitMarkers: 无效的地图实例,已跳过设置视图');
|
||||
return;
|
||||
}
|
||||
if (!points || points.length === 0) return;
|
||||
|
||||
// 如果只有一个点,直接居中
|
||||
if (points.length === 1) {
|
||||
map.centerAndZoom(points[0], 15);
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建视图范围
|
||||
const viewport = map.getViewport(points, { margins: [padding, padding, padding, padding] });
|
||||
map.setViewport(viewport);
|
||||
};
|
||||
|
||||
/**
|
||||
* 转换养殖场数据为地图标记数据
|
||||
* @param {Array} farms - 养殖场数据数组
|
||||
* @returns {Array} 地图标记数据数组
|
||||
*/
|
||||
export const convertFarmsToMarkers = (farms = []) => {
|
||||
return farms
|
||||
.filter(farm => {
|
||||
// 只保留有有效位置信息的农场
|
||||
return farm.location &&
|
||||
typeof farm.location.lat === 'number' &&
|
||||
typeof farm.location.lng === 'number';
|
||||
})
|
||||
.map(farm => {
|
||||
// 计算动物总数
|
||||
const animalCount = farm.animals ?
|
||||
farm.animals.reduce((total, animal) => total + (animal.count || 0), 0) : 0;
|
||||
|
||||
// 计算在线设备数
|
||||
const onlineDevices = farm.devices ?
|
||||
farm.devices.filter(device => device.status === 'online').length : 0;
|
||||
const totalDevices = farm.devices ? farm.devices.length : 0;
|
||||
|
||||
return {
|
||||
id: farm.id,
|
||||
title: farm.name,
|
||||
location: farm.location,
|
||||
content: `
|
||||
<div style="padding: 8px; font-size: 14px;">
|
||||
<h4 style="margin: 0 0 8px 0; color: #1890ff;">${farm.name}</h4>
|
||||
<p style="margin: 4px 0;"><strong>类型:</strong> ${farm.type || '未知'}</p>
|
||||
<p style="margin: 4px 0;"><strong>动物数量:</strong> ${animalCount} 只</p>
|
||||
<p style="margin: 4px 0;"><strong>设备状态:</strong> ${onlineDevices}/${totalDevices} 在线</p>
|
||||
<p style="margin: 4px 0;"><strong>联系人:</strong> ${farm.contact || '未知'}</p>
|
||||
<p style="margin: 4px 0;"><strong>地址:</strong> ${farm.address || '未知'}</p>
|
||||
<p style="margin: 4px 0; color: #666;"><strong>坐标:</strong> ${farm.location.lat.toFixed(4)}, ${farm.location.lng.toFixed(4)}</p>
|
||||
</div>
|
||||
`,
|
||||
originalData: farm
|
||||
};
|
||||
});
|
||||
};
|
||||
332
admin-system/frontend/src/utils/mapServiceFixed.js
Normal file
332
admin-system/frontend/src/utils/mapServiceFixed.js
Normal file
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* 修复版百度地图服务工具
|
||||
* 专门解决 coordType 错误问题
|
||||
*/
|
||||
|
||||
// 百度地图API加载状态
|
||||
let BMapLoaded = false;
|
||||
let loadingPromise = null;
|
||||
|
||||
/**
|
||||
* 加载百度地图API
|
||||
*/
|
||||
export const loadBMapScript = async (retryCount = 0) => {
|
||||
if (BMapLoaded) {
|
||||
console.log('百度地图API已加载');
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
if (loadingPromise) {
|
||||
console.log('百度地图API正在加载中...');
|
||||
return loadingPromise;
|
||||
}
|
||||
|
||||
console.log(`开始加载百度地图API... (重试次数: ${retryCount})`);
|
||||
|
||||
loadingPromise = new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
const { BAIDU_MAP_CONFIG } = await import('../config/env');
|
||||
|
||||
console.log('使用API密钥:', BAIDU_MAP_CONFIG.apiKey);
|
||||
|
||||
if (!BAIDU_MAP_CONFIG.apiKey || BAIDU_MAP_CONFIG.apiKey === 'YOUR_VALID_BAIDU_MAP_API_KEY') {
|
||||
const error = new Error('百度地图API密钥未配置或无效');
|
||||
console.error('API密钥错误:', error);
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof window.BMap !== 'undefined') {
|
||||
console.log('BMap已存在,直接使用');
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建全局回调函数
|
||||
window.initBMap = () => {
|
||||
console.log('百度地图API加载成功');
|
||||
clearTimeout(timeoutId);
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.BMap && typeof window.BMap.Map === 'function') {
|
||||
BMapLoaded = true;
|
||||
resolve();
|
||||
} else {
|
||||
console.error('BMap对象未正确初始化');
|
||||
reject(new Error('BMap对象未正确初始化'));
|
||||
}
|
||||
delete window.initBMap;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
// 创建script标签
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_CONFIG.apiKey}&callback=initBMap`;
|
||||
|
||||
console.log('百度地图API URL:', script.src);
|
||||
|
||||
script.onerror = async (error) => {
|
||||
console.error('百度地图脚本加载失败:', error);
|
||||
clearTimeout(timeoutId);
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
if (window.initBMap) {
|
||||
delete window.initBMap;
|
||||
}
|
||||
|
||||
if (retryCount < BAIDU_MAP_CONFIG.maxRetries) {
|
||||
console.log(`重试加载百度地图API (${retryCount + 1}/${BAIDU_MAP_CONFIG.maxRetries})`);
|
||||
setTimeout(() => {
|
||||
loadBMapScript(retryCount + 1).then(resolve).catch(reject);
|
||||
}, BAIDU_MAP_CONFIG.retryDelay);
|
||||
} else {
|
||||
reject(new Error('百度地图API加载失败,已达到最大重试次数'));
|
||||
}
|
||||
};
|
||||
|
||||
// 设置超时
|
||||
const timeoutId = setTimeout(() => {
|
||||
console.error('百度地图API加载超时');
|
||||
if (script.parentNode) {
|
||||
script.parentNode.removeChild(script);
|
||||
}
|
||||
if (window.initBMap) {
|
||||
delete window.initBMap;
|
||||
}
|
||||
reject(new Error('百度地图API加载超时'));
|
||||
}, 10000);
|
||||
|
||||
document.head.appendChild(script);
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载百度地图API时发生错误:', error);
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
return loadingPromise;
|
||||
};
|
||||
|
||||
/**
|
||||
* 修复版创建地图函数
|
||||
* 专门解决 coordType 错误
|
||||
*/
|
||||
export const createMap = async (container, options = {}) => {
|
||||
console.log('修复版createMap函数开始执行');
|
||||
|
||||
// 确保API已加载
|
||||
await loadBMapScript();
|
||||
|
||||
// 验证容器
|
||||
if (!container) {
|
||||
throw new Error('地图容器不能为空');
|
||||
}
|
||||
|
||||
// 检查容器是否在DOM中
|
||||
if (!document.contains(container)) {
|
||||
throw new Error('地图容器不在DOM中');
|
||||
}
|
||||
|
||||
// 强制设置容器样式,确保地图能正确初始化
|
||||
const originalStyles = {
|
||||
position: container.style.position,
|
||||
display: container.style.display,
|
||||
visibility: container.style.visibility,
|
||||
width: container.style.width,
|
||||
height: container.style.height,
|
||||
minHeight: container.style.minHeight
|
||||
};
|
||||
|
||||
// 设置临时样式
|
||||
container.style.position = 'relative';
|
||||
container.style.display = 'block';
|
||||
container.style.visibility = 'visible';
|
||||
container.style.width = container.style.width || '100%';
|
||||
container.style.height = container.style.height || '400px';
|
||||
container.style.minHeight = container.style.minHeight || '400px';
|
||||
|
||||
// 等待样式生效
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
|
||||
// 检查容器尺寸
|
||||
if (container.offsetWidth === 0 || container.offsetHeight === 0) {
|
||||
console.warn('容器尺寸为0,强制设置尺寸');
|
||||
container.style.width = '100%';
|
||||
container.style.height = '400px';
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
console.log('容器最终状态:', {
|
||||
offsetWidth: container.offsetWidth,
|
||||
offsetHeight: container.offsetHeight,
|
||||
clientWidth: container.clientWidth,
|
||||
clientHeight: container.clientHeight,
|
||||
computedStyle: {
|
||||
position: window.getComputedStyle(container).position,
|
||||
display: window.getComputedStyle(container).display,
|
||||
visibility: window.getComputedStyle(container).visibility
|
||||
}
|
||||
});
|
||||
|
||||
// 检查父级元素样式
|
||||
let parent = container.parentElement;
|
||||
while (parent && parent !== document.body) {
|
||||
const parentStyle = window.getComputedStyle(parent);
|
||||
if (parentStyle.position === 'fixed' || parentStyle.display === 'none') {
|
||||
console.warn('发现可能有问题的父级样式:', {
|
||||
tagName: parent.tagName,
|
||||
position: parentStyle.position,
|
||||
display: parentStyle.display
|
||||
});
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
|
||||
// 默认配置
|
||||
const defaultOptions = {
|
||||
center: { lng: 106.27, lat: 38.47 },
|
||||
zoom: 8,
|
||||
enableMapClick: true,
|
||||
enableScrollWheelZoom: true,
|
||||
enableDragging: true,
|
||||
enableDoubleClickZoom: true,
|
||||
enableKeyboard: true
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaultOptions, ...options };
|
||||
|
||||
console.log('地图配置选项:', mergedOptions);
|
||||
|
||||
// 创建地图实例
|
||||
let map;
|
||||
try {
|
||||
console.log('开始创建BMap.Map实例...');
|
||||
|
||||
// 确保BMap对象存在
|
||||
if (!window.BMap || typeof window.BMap.Map !== 'function') {
|
||||
throw new Error('BMap对象未正确加载');
|
||||
}
|
||||
|
||||
// 创建地图实例
|
||||
map = new window.BMap.Map(container);
|
||||
console.log('地图实例创建成功:', map);
|
||||
|
||||
// 验证地图实例
|
||||
if (!map || typeof map.centerAndZoom !== 'function') {
|
||||
throw new Error('地图实例创建失败');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('创建地图实例失败:', error);
|
||||
// 恢复原始样式
|
||||
Object.keys(originalStyles).forEach(key => {
|
||||
container.style[key] = originalStyles[key];
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
// 恢复原始样式
|
||||
Object.keys(originalStyles).forEach(key => {
|
||||
container.style[key] = originalStyles[key];
|
||||
});
|
||||
|
||||
// 设置中心点和缩放级别
|
||||
console.log('设置地图中心点和缩放级别:', mergedOptions.center, mergedOptions.zoom);
|
||||
map.centerAndZoom(mergedOptions.center, mergedOptions.zoom);
|
||||
|
||||
// 添加地图控件
|
||||
map.addControl(new window.BMap.NavigationControl());
|
||||
map.addControl(new window.BMap.ScaleControl());
|
||||
map.addControl(new window.BMap.OverviewMapControl());
|
||||
map.addControl(new window.BMap.MapTypeControl());
|
||||
|
||||
// 监听地图事件
|
||||
map.addEventListener('tilesloaded', function() {
|
||||
console.log('百度地图瓦片加载完成');
|
||||
});
|
||||
|
||||
map.addEventListener('load', function() {
|
||||
console.log('百度地图完全加载完成');
|
||||
});
|
||||
|
||||
// 设置地图功能
|
||||
if (mergedOptions.enableMapClick) {
|
||||
map.enableMapClick();
|
||||
}
|
||||
if (mergedOptions.enableScrollWheelZoom) {
|
||||
map.enableScrollWheelZoom();
|
||||
}
|
||||
if (mergedOptions.enableDragging) {
|
||||
map.enableDragging();
|
||||
}
|
||||
if (mergedOptions.enableDoubleClickZoom) {
|
||||
map.enableDoubleClickZoom();
|
||||
}
|
||||
if (mergedOptions.enableKeyboard) {
|
||||
map.enableKeyboard();
|
||||
}
|
||||
|
||||
console.log('修复版地图创建完成');
|
||||
return map;
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加标记点
|
||||
*/
|
||||
export const addMarkers = (map, markers) => {
|
||||
if (!map || !markers || !Array.isArray(markers)) {
|
||||
console.warn('addMarkers: 参数无效');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('添加标记点:', markers.length);
|
||||
|
||||
markers.forEach((markerData, index) => {
|
||||
try {
|
||||
const point = new window.BMap.Point(markerData.lng, markerData.lat);
|
||||
const marker = new window.BMap.Marker(point);
|
||||
|
||||
if (markerData.title) {
|
||||
const infoWindow = new window.BMap.InfoWindow(markerData.title);
|
||||
marker.addEventListener('click', function() {
|
||||
map.openInfoWindow(infoWindow, point);
|
||||
});
|
||||
}
|
||||
|
||||
map.addOverlay(marker);
|
||||
console.log(`标记点 ${index + 1} 添加成功`);
|
||||
} catch (error) {
|
||||
console.error(`添加标记点 ${index + 1} 失败:`, error);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除所有覆盖物
|
||||
*/
|
||||
export const clearOverlays = (map) => {
|
||||
if (!map) {
|
||||
console.warn('clearOverlays: 地图实例无效');
|
||||
return;
|
||||
}
|
||||
|
||||
map.clearOverlays();
|
||||
console.log('已清除所有覆盖物');
|
||||
};
|
||||
|
||||
/**
|
||||
* 调整视图以适应所有标记
|
||||
*/
|
||||
export const setViewToFitMarkers = (map, markers) => {
|
||||
if (!map || !markers || !Array.isArray(markers) || markers.length === 0) {
|
||||
console.warn('setViewToFitMarkers: 参数无效');
|
||||
return;
|
||||
}
|
||||
|
||||
const points = markers.map(marker => new window.BMap.Point(marker.lng, marker.lat));
|
||||
map.setViewport(points);
|
||||
console.log('已调整视图以适应标记点');
|
||||
};
|
||||
116
admin-system/frontend/src/utils/menuDebugger.js
Normal file
116
admin-system/frontend/src/utils/menuDebugger.js
Normal file
@@ -0,0 +1,116 @@
|
||||
/**
|
||||
* 菜单权限调试工具
|
||||
* @file menuDebugger.js
|
||||
* @description 用于调试菜单权限问题的工具函数
|
||||
*/
|
||||
|
||||
/**
|
||||
* 调试菜单权限问题
|
||||
* @param {Object} userData - 用户数据
|
||||
* @param {Array} routes - 路由配置
|
||||
*/
|
||||
export function debugMenuPermissions(userData, routes) {
|
||||
console.log('🔍 菜单权限调试开始...')
|
||||
console.log('📊 用户数据:', userData)
|
||||
console.log('📊 路由配置:', routes)
|
||||
|
||||
if (!userData) {
|
||||
console.error('❌ 用户数据为空')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('📋 用户权限:', userData.permissions || [])
|
||||
console.log('📋 用户角色:', userData.role)
|
||||
console.log('📋 可访问菜单:', userData.accessibleMenus || [])
|
||||
|
||||
// 检查每个路由的权限
|
||||
routes.forEach(route => {
|
||||
console.log(`\n🔍 检查路由: ${route.name}`)
|
||||
console.log(' - 路径:', route.path)
|
||||
console.log(' - 标题:', route.meta?.title)
|
||||
console.log(' - 图标:', route.meta?.icon)
|
||||
console.log(' - 权限要求:', route.meta?.permission)
|
||||
console.log(' - 角色要求:', route.meta?.roles)
|
||||
console.log(' - 菜单要求:', route.meta?.menu)
|
||||
|
||||
// 检查权限
|
||||
if (route.meta?.permission) {
|
||||
const hasPerm = userData.permissions?.includes(route.meta.permission)
|
||||
console.log(` - 权限检查: ${route.meta.permission} -> ${hasPerm ? '✅' : '❌'}`)
|
||||
}
|
||||
|
||||
// 检查角色
|
||||
if (route.meta?.roles) {
|
||||
const hasRole = route.meta.roles.includes(userData.role?.name)
|
||||
console.log(` - 角色检查: ${route.meta.roles} -> ${hasRole ? '✅' : '❌'}`)
|
||||
}
|
||||
|
||||
// 检查菜单
|
||||
if (route.meta?.menu) {
|
||||
const canAccess = userData.accessibleMenus?.includes(route.meta.menu)
|
||||
console.log(` - 菜单检查: ${route.meta.menu} -> ${canAccess ? '✅' : '❌'}`)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('🔍 菜单权限调试完成')
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定权限
|
||||
* @param {Object} userData - 用户数据
|
||||
* @param {string} permission - 权限名称
|
||||
*/
|
||||
export function checkPermission(userData, permission) {
|
||||
console.log(`🔍 检查权限: ${permission}`)
|
||||
console.log('用户权限列表:', userData.permissions || [])
|
||||
|
||||
const hasPerm = userData.permissions?.includes(permission)
|
||||
console.log(`权限检查结果: ${hasPerm ? '✅' : '❌'}`)
|
||||
|
||||
return hasPerm
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查特定角色
|
||||
* @param {Object} userData - 用户数据
|
||||
* @param {string} role - 角色名称
|
||||
*/
|
||||
export function checkRole(userData, role) {
|
||||
console.log(`🔍 检查角色: ${role}`)
|
||||
console.log('用户角色:', userData.role)
|
||||
|
||||
const hasRole = userData.role?.name === role
|
||||
console.log(`角色检查结果: ${hasRole ? '✅' : '❌'}`)
|
||||
|
||||
return hasRole
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查菜单访问
|
||||
* @param {Object} userData - 用户数据
|
||||
* @param {string} menuKey - 菜单键
|
||||
*/
|
||||
export function checkMenuAccess(userData, menuKey) {
|
||||
console.log(`🔍 检查菜单访问: ${menuKey}`)
|
||||
console.log('可访问菜单:', userData.accessibleMenus || [])
|
||||
|
||||
const canAccess = userData.accessibleMenus?.includes(menuKey)
|
||||
console.log(`菜单访问检查结果: ${canAccess ? '✅' : '❌'}`)
|
||||
|
||||
return canAccess
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出调试工具到全局
|
||||
*/
|
||||
export function setupGlobalDebugger() {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.menuDebugger = {
|
||||
debugMenuPermissions,
|
||||
checkPermission,
|
||||
checkRole,
|
||||
checkMenuAccess
|
||||
}
|
||||
console.log('🔧 菜单权限调试工具已添加到 window.menuDebugger')
|
||||
}
|
||||
}
|
||||
379
admin-system/frontend/src/utils/websocketService.js
Normal file
379
admin-system/frontend/src/utils/websocketService.js
Normal file
@@ -0,0 +1,379 @@
|
||||
/**
|
||||
* WebSocket实时通信服务
|
||||
* @file websocketService.js
|
||||
* @description 前端WebSocket客户端,处理实时数据接收
|
||||
*/
|
||||
import { io } from 'socket.io-client';
|
||||
import { useUserStore } from '../stores/user';
|
||||
import { useDataStore } from '../stores/data';
|
||||
import { message, notification } from 'ant-design-vue';
|
||||
|
||||
class WebSocketService {
|
||||
constructor() {
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
this.reconnectAttempts = 0;
|
||||
this.maxReconnectAttempts = 5;
|
||||
this.reconnectInterval = 3000; // 3秒重连间隔
|
||||
this.userStore = null;
|
||||
this.dataStore = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连接WebSocket服务器
|
||||
* @param {string} token JWT认证令牌
|
||||
*/
|
||||
connect(token) {
|
||||
if (this.socket && this.isConnected) {
|
||||
console.log('WebSocket已连接,无需重复连接');
|
||||
return;
|
||||
}
|
||||
|
||||
// 初始化store
|
||||
this.userStore = useUserStore();
|
||||
this.dataStore = useDataStore();
|
||||
|
||||
const serverUrl = import.meta.env.VITE_API_URL || 'http://localhost:5350';
|
||||
|
||||
console.log('正在连接WebSocket服务器:', serverUrl);
|
||||
|
||||
this.socket = io(serverUrl, {
|
||||
auth: {
|
||||
token: token
|
||||
},
|
||||
transports: ['websocket', 'polling'],
|
||||
timeout: 20000,
|
||||
reconnection: true,
|
||||
reconnectionAttempts: this.maxReconnectAttempts,
|
||||
reconnectionDelay: this.reconnectInterval
|
||||
});
|
||||
|
||||
this.setupEventListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置事件监听器
|
||||
*/
|
||||
setupEventListeners() {
|
||||
if (!this.socket) return;
|
||||
|
||||
// 连接成功
|
||||
this.socket.on('connect', () => {
|
||||
this.isConnected = true;
|
||||
this.reconnectAttempts = 0;
|
||||
console.log('WebSocket连接成功,连接ID:', this.socket.id);
|
||||
|
||||
message.success('实时数据连接已建立');
|
||||
});
|
||||
|
||||
// 连接确认
|
||||
this.socket.on('connected', (data) => {
|
||||
console.log('收到服务器连接确认:', data);
|
||||
});
|
||||
|
||||
// 设备状态更新
|
||||
this.socket.on('device_update', (data) => {
|
||||
console.log('收到设备状态更新:', data);
|
||||
this.handleDeviceUpdate(data);
|
||||
});
|
||||
|
||||
// 预警更新
|
||||
this.socket.on('alert_update', (data) => {
|
||||
console.log('收到预警更新:', data);
|
||||
this.handleAlertUpdate(data);
|
||||
});
|
||||
|
||||
// 紧急预警
|
||||
this.socket.on('urgent_alert', (data) => {
|
||||
console.log('收到紧急预警:', data);
|
||||
this.handleUrgentAlert(data);
|
||||
});
|
||||
|
||||
// 动物健康状态更新
|
||||
this.socket.on('animal_update', (data) => {
|
||||
console.log('收到动物健康状态更新:', data);
|
||||
this.handleAnimalUpdate(data);
|
||||
});
|
||||
|
||||
// 系统统计数据更新
|
||||
this.socket.on('stats_update', (data) => {
|
||||
console.log('收到系统统计数据更新:', data);
|
||||
this.handleStatsUpdate(data);
|
||||
});
|
||||
|
||||
// 性能监控数据(仅管理员)
|
||||
this.socket.on('performance_update', (data) => {
|
||||
console.log('收到性能监控数据:', data);
|
||||
this.handlePerformanceUpdate(data);
|
||||
});
|
||||
|
||||
// 连接断开
|
||||
this.socket.on('disconnect', (reason) => {
|
||||
this.isConnected = false;
|
||||
console.log('WebSocket连接断开:', reason);
|
||||
|
||||
if (reason === 'io server disconnect') {
|
||||
// 服务器主动断开,需要重新连接
|
||||
this.reconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 连接错误
|
||||
this.socket.on('connect_error', (error) => {
|
||||
console.error('WebSocket连接错误:', error);
|
||||
|
||||
if (error.message.includes('认证失败') || error.message.includes('未提供认证令牌')) {
|
||||
message.error('实时连接认证失败,请重新登录');
|
||||
this.userStore.logout();
|
||||
} else {
|
||||
this.handleReconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// 心跳响应
|
||||
this.socket.on('pong', (data) => {
|
||||
console.log('收到心跳响应:', data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理设备状态更新
|
||||
* @param {Object} data 设备数据
|
||||
*/
|
||||
handleDeviceUpdate(data) {
|
||||
// 更新数据存储中的设备状态
|
||||
if (this.dataStore) {
|
||||
this.dataStore.updateDeviceRealtime(data.data);
|
||||
}
|
||||
|
||||
// 如果设备状态异常,显示通知
|
||||
if (data.data.status === 'offline') {
|
||||
notification.warning({
|
||||
message: '设备状态变化',
|
||||
description: `设备 ${data.data.name} 已离线`,
|
||||
duration: 4.5,
|
||||
});
|
||||
} else if (data.data.status === 'maintenance') {
|
||||
notification.info({
|
||||
message: '设备状态变化',
|
||||
description: `设备 ${data.data.name} 进入维护模式`,
|
||||
duration: 4.5,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理预警更新
|
||||
* @param {Object} data 预警数据
|
||||
*/
|
||||
handleAlertUpdate(data) {
|
||||
// 更新数据存储中的预警数据
|
||||
if (this.dataStore) {
|
||||
this.dataStore.addNewAlert(data.data);
|
||||
}
|
||||
|
||||
// 显示预警通知
|
||||
const alertLevel = data.data.level;
|
||||
let notificationType = 'info';
|
||||
|
||||
if (alertLevel === 'critical') {
|
||||
notificationType = 'error';
|
||||
} else if (alertLevel === 'high') {
|
||||
notificationType = 'warning';
|
||||
}
|
||||
|
||||
notification[notificationType]({
|
||||
message: '新预警',
|
||||
description: `${data.data.farm_name}: ${data.data.message}`,
|
||||
duration: 6,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理紧急预警
|
||||
* @param {Object} data 紧急预警数据
|
||||
*/
|
||||
handleUrgentAlert(data) {
|
||||
// 紧急预警使用模态框显示
|
||||
notification.error({
|
||||
message: '🚨 紧急预警',
|
||||
description: `${data.alert.farm_name}: ${data.alert.message}`,
|
||||
duration: 0, // 不自动关闭
|
||||
style: {
|
||||
backgroundColor: '#fff2f0',
|
||||
border: '1px solid #ffccc7'
|
||||
}
|
||||
});
|
||||
|
||||
// 播放警报声音(如果浏览器支持)
|
||||
this.playAlertSound();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理动物健康状态更新
|
||||
* @param {Object} data 动物数据
|
||||
*/
|
||||
handleAnimalUpdate(data) {
|
||||
// 更新数据存储
|
||||
if (this.dataStore) {
|
||||
this.dataStore.updateAnimalRealtime(data.data);
|
||||
}
|
||||
|
||||
// 如果动物健康状态异常,显示通知
|
||||
if (data.data.health_status === 'sick') {
|
||||
notification.warning({
|
||||
message: '动物健康状态变化',
|
||||
description: `${data.data.farm_name}的${data.data.type}出现健康问题`,
|
||||
duration: 5,
|
||||
});
|
||||
} else if (data.data.health_status === 'quarantined') {
|
||||
notification.error({
|
||||
message: '动物健康状态变化',
|
||||
description: `${data.data.farm_name}的${data.data.type}已隔离`,
|
||||
duration: 6,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理系统统计数据更新
|
||||
* @param {Object} data 统计数据
|
||||
*/
|
||||
handleStatsUpdate(data) {
|
||||
// 更新数据存储中的统计信息
|
||||
if (this.dataStore) {
|
||||
this.dataStore.updateStatsRealtime(data.data);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理性能监控数据更新
|
||||
* @param {Object} data 性能数据
|
||||
*/
|
||||
handlePerformanceUpdate(data) {
|
||||
// 只有管理员才能看到性能数据
|
||||
if (this.userStore?.user?.roles?.includes('admin')) {
|
||||
console.log('收到性能监控数据:', data);
|
||||
// 可以通过事件总线通知性能监控组件更新
|
||||
window.dispatchEvent(new CustomEvent('performance_update', { detail: data }));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放警报声音
|
||||
*/
|
||||
playAlertSound() {
|
||||
try {
|
||||
// 创建音频上下文
|
||||
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
|
||||
|
||||
// 生成警报音
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
|
||||
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
|
||||
|
||||
oscillator.start();
|
||||
oscillator.stop(audioContext.currentTime + 0.5);
|
||||
} catch (error) {
|
||||
console.log('无法播放警报声音:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅农场数据
|
||||
* @param {number} farmId 农场ID
|
||||
*/
|
||||
subscribeFarm(farmId) {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('subscribe_farm', farmId);
|
||||
console.log(`已订阅农场 ${farmId} 的实时数据`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅农场数据
|
||||
* @param {number} farmId 农场ID
|
||||
*/
|
||||
unsubscribeFarm(farmId) {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('unsubscribe_farm', farmId);
|
||||
console.log(`已取消订阅农场 ${farmId} 的实时数据`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅设备数据
|
||||
* @param {number} deviceId 设备ID
|
||||
*/
|
||||
subscribeDevice(deviceId) {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('subscribe_device', deviceId);
|
||||
console.log(`已订阅设备 ${deviceId} 的实时数据`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送心跳
|
||||
*/
|
||||
sendHeartbeat() {
|
||||
if (this.socket && this.isConnected) {
|
||||
this.socket.emit('ping');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理重连
|
||||
*/
|
||||
handleReconnect() {
|
||||
this.reconnectAttempts++;
|
||||
|
||||
if (this.reconnectAttempts <= this.maxReconnectAttempts) {
|
||||
console.log(`尝试重连WebSocket (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);
|
||||
|
||||
setTimeout(() => {
|
||||
this.reconnect();
|
||||
}, this.reconnectInterval * this.reconnectAttempts);
|
||||
} else {
|
||||
message.error('实时连接已断开,请刷新页面重试');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新连接
|
||||
*/
|
||||
reconnect() {
|
||||
if (this.userStore?.token) {
|
||||
this.connect(this.userStore.token);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.disconnect();
|
||||
this.socket = null;
|
||||
this.isConnected = false;
|
||||
console.log('WebSocket连接已断开');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
* @returns {boolean} 连接状态
|
||||
*/
|
||||
getConnectionStatus() {
|
||||
return this.isConnected;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建单例实例
|
||||
const webSocketService = new WebSocketService();
|
||||
|
||||
export default webSocketService;
|
||||
@@ -2,9 +2,35 @@
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>预警管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加预警
|
||||
<a-space>
|
||||
<a-button @click="exportAlerts" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加预警
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-auto-complete
|
||||
v-model:value="searchFarmName"
|
||||
:options="farmNameOptions"
|
||||
placeholder="请选择或输入养殖场名称进行搜索"
|
||||
style="width: 300px;"
|
||||
:filter-option="false"
|
||||
@search="handleSearchInput"
|
||||
@press-enter="searchAlertsByFarm"
|
||||
/>
|
||||
<a-button type="primary" @click="searchAlertsByFarm" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -16,7 +42,10 @@
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'level'">
|
||||
<template v-if="column.dataIndex === 'type'">
|
||||
<a-tag color="blue">{{ getTypeText(record.type) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'level'">
|
||||
<a-tag :color="getLevelColor(record.level)">
|
||||
{{ getLevelText(record.level) }}
|
||||
</a-tag>
|
||||
@@ -172,8 +201,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 响应式数据
|
||||
@@ -192,6 +222,15 @@ const formRef = ref()
|
||||
const currentAlert = ref(null)
|
||||
const resolveNotes = ref('')
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchFarmName = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const farmNameOptions = ref([])
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
@@ -283,13 +322,9 @@ const fetchAlerts = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get('/api/alerts', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
alerts.value = response.data.data
|
||||
const response = await api.get('/alerts')
|
||||
if (response.success) {
|
||||
alerts.value = response.data
|
||||
} else {
|
||||
message.error('获取预警列表失败')
|
||||
}
|
||||
@@ -313,13 +348,9 @@ const fetchFarms = async () => {
|
||||
try {
|
||||
farmsLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get('/api/farms', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
farms.value = response.data.data
|
||||
const response = await api.get('/farms')
|
||||
if (response.success) {
|
||||
farms.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取农场列表失败:', error)
|
||||
@@ -333,13 +364,9 @@ const fetchDevices = async () => {
|
||||
try {
|
||||
devicesLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get('/api/devices', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
devices.value = response.data.data
|
||||
const response = await api.get('/devices')
|
||||
if (response.success) {
|
||||
devices.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取设备列表失败:', error)
|
||||
@@ -387,17 +414,13 @@ const handleResolve = async () => {
|
||||
try {
|
||||
resolveLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.put(`/api/alerts/${currentAlert.value.id}`, {
|
||||
const response = await api.put(`/alerts/${currentAlert.value.id}`, {
|
||||
status: 'resolved',
|
||||
resolved_at: new Date().toISOString(),
|
||||
resolution_notes: resolveNotes.value
|
||||
}, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
|
||||
if (response.data.success) {
|
||||
if (response.success) {
|
||||
message.success('预警已解决')
|
||||
resolveModalVisible.value = false
|
||||
fetchAlerts()
|
||||
@@ -416,12 +439,8 @@ const handleResolve = async () => {
|
||||
const deleteAlert = async (id) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.delete(`/api/alerts/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
const response = await api.delete(`/alerts/${id}`)
|
||||
if (response.success) {
|
||||
message.success('删除成功')
|
||||
fetchAlerts()
|
||||
} else {
|
||||
@@ -455,12 +474,12 @@ const handleSubmit = async () => {
|
||||
|
||||
let response
|
||||
if (isEdit.value) {
|
||||
response = await axios.put(`/api/alerts/${formData.id}`, submitData, config)
|
||||
response = await api.put(`/alerts/${formData.id}`, submitData)
|
||||
} else {
|
||||
response = await axios.post('/api/alerts', submitData, config)
|
||||
response = await api.post('/alerts', submitData)
|
||||
}
|
||||
|
||||
if (response.data.success) {
|
||||
if (response.success) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
modalVisible.value = false
|
||||
fetchAlerts()
|
||||
@@ -507,6 +526,25 @@ const getLevelColor = (level) => {
|
||||
return colors[level] || 'default'
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
temperature_alert: '温度异常',
|
||||
humidity_alert: '湿度异常',
|
||||
feed_alert: '饲料异常',
|
||||
health_alert: '健康异常',
|
||||
device_alert: '设备异常',
|
||||
temperature: '温度异常',
|
||||
humidity: '湿度异常',
|
||||
device_failure: '设备故障',
|
||||
animal_health: '动物健康',
|
||||
security: '安全警报',
|
||||
maintenance: '维护提醒',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 获取级别文本
|
||||
const getLevelText = (level) => {
|
||||
const texts = {
|
||||
@@ -563,10 +601,106 @@ const formatDateTime = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
// 根据养殖场搜索预警
|
||||
const searchAlertsByFarm = async () => {
|
||||
if (!searchFarmName.value.trim()) {
|
||||
message.warning('请输入养殖场名称进行搜索')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await api.get('/alerts/search', {
|
||||
params: { farmName: searchFarmName.value.trim() }
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
alerts.value = response.data || []
|
||||
isSearching.value = true
|
||||
message.success(response.message || `找到 ${alerts.value.length} 个匹配的预警`)
|
||||
} else {
|
||||
alerts.value = []
|
||||
message.info('未找到匹配的预警')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索预警失败:', error)
|
||||
message.error('搜索预警失败')
|
||||
alerts.value = []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入(为自动完成提供选项)
|
||||
const handleSearchInput = (value) => {
|
||||
if (!value) {
|
||||
farmNameOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 从现有农场列表中筛选匹配的农场名称
|
||||
const matchingFarms = farms.value.filter(farm =>
|
||||
farm.name.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
|
||||
farmNameOptions.value = matchingFarms.map(farm => ({
|
||||
value: farm.name,
|
||||
label: farm.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 更新农场名称选项(在数据加载后)
|
||||
const updateFarmNameOptions = () => {
|
||||
farmNameOptions.value = farms.value.map(farm => ({
|
||||
value: farm.name,
|
||||
label: farm.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 导出预警数据
|
||||
const exportAlerts = async () => {
|
||||
try {
|
||||
if (!alerts.value || alerts.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportAlertsData(alerts.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchFarmName.value = ''
|
||||
isSearching.value = false
|
||||
farmNameOptions.value = []
|
||||
fetchAlerts() // 重新加载全部预警
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchAlerts()
|
||||
fetchFarms()
|
||||
fetchFarms().then(() => {
|
||||
updateFarmNameOptions()
|
||||
})
|
||||
fetchDevices()
|
||||
})
|
||||
</script>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
665
admin-system/frontend/src/views/ApiTester.vue
Normal file
665
admin-system/frontend/src/views/ApiTester.vue
Normal file
@@ -0,0 +1,665 @@
|
||||
<template>
|
||||
<div class="api-tester-page">
|
||||
<a-page-header
|
||||
title="API测试工具"
|
||||
sub-title="快速测试和调试API接口"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="clearHistory">
|
||||
<template #icon><ClearOutlined /></template>
|
||||
清空历史
|
||||
</a-button>
|
||||
<a-button type="primary" @click="openSwaggerDocs" target="_blank">
|
||||
<template #icon><ApiOutlined /></template>
|
||||
查看API文档
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<div class="api-tester-content">
|
||||
<a-row :gutter="24">
|
||||
<!-- 左侧:API请求面板 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="API请求" :bordered="false">
|
||||
<!-- 快捷API选择 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-select
|
||||
v-model:value="selectedApi"
|
||||
placeholder="选择常用API"
|
||||
style="width: 100%;"
|
||||
@change="loadApiTemplate"
|
||||
show-search
|
||||
option-filter-prop="children"
|
||||
>
|
||||
<a-select-opt-group label="认证相关">
|
||||
<a-select-option value="auth-login">登录</a-select-option>
|
||||
<a-select-option value="auth-logout">登出</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="农场管理">
|
||||
<a-select-option value="farms-list">获取农场列表</a-select-option>
|
||||
<a-select-option value="farms-create">创建农场</a-select-option>
|
||||
<a-select-option value="farms-search">搜索农场</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="设备管理">
|
||||
<a-select-option value="devices-list">获取设备列表</a-select-option>
|
||||
<a-select-option value="devices-search">搜索设备</a-select-option>
|
||||
<a-select-option value="devices-stats">设备统计</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="牛只管理">
|
||||
<a-select-option value="animals-list">获取动物列表</a-select-option>
|
||||
<a-select-option value="animals-search">搜索动物</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="预警管理">
|
||||
<a-select-option value="alerts-list">获取预警列表</a-select-option>
|
||||
<a-select-option value="alerts-search">搜索预警</a-select-option>
|
||||
<a-select-option value="alerts-resolve">解决预警</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="用户管理">
|
||||
<a-select-option value="users-list">获取用户列表</a-select-option>
|
||||
<a-select-option value="users-search">搜索用户</a-select-option>
|
||||
<a-select-option value="users-create">创建用户</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="产品管理">
|
||||
<a-select-option value="products-list">获取产品列表</a-select-option>
|
||||
<a-select-option value="products-search">搜索产品</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="订单管理">
|
||||
<a-select-option value="orders-list">获取订单列表</a-select-option>
|
||||
<a-select-option value="orders-search">搜索订单</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="报表管理">
|
||||
<a-select-option value="reports-farm">生成农场报表</a-select-option>
|
||||
<a-select-option value="reports-sales">生成销售报表</a-select-option>
|
||||
<a-select-option value="reports-list">获取报表列表</a-select-option>
|
||||
</a-select-opt-group>
|
||||
<a-select-opt-group label="系统管理">
|
||||
<a-select-option value="system-configs">获取系统配置</a-select-option>
|
||||
<a-select-option value="system-menus">获取菜单权限</a-select-option>
|
||||
<a-select-option value="system-stats">获取系统统计</a-select-option>
|
||||
</a-select-opt-group>
|
||||
</a-select>
|
||||
</div>
|
||||
|
||||
<!-- HTTP方法和URL -->
|
||||
<a-row :gutter="8" style="margin-bottom: 16px;">
|
||||
<a-col :span="6">
|
||||
<a-select v-model:value="requestMethod" style="width: 100%;">
|
||||
<a-select-option value="GET">GET</a-select-option>
|
||||
<a-select-option value="POST">POST</a-select-option>
|
||||
<a-select-option value="PUT">PUT</a-select-option>
|
||||
<a-select-option value="DELETE">DELETE</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="18">
|
||||
<a-input
|
||||
v-model:value="requestUrl"
|
||||
placeholder="请输入API端点,例如: /api/farms"
|
||||
addonBefore="API"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 请求头 -->
|
||||
<div style="margin-bottom: 16px;">
|
||||
<a-typography-title :level="5">请求头</a-typography-title>
|
||||
<a-textarea
|
||||
v-model:value="requestHeaders"
|
||||
placeholder='{"Authorization": "Bearer your-token", "Content-Type": "application/json"}'
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 请求体 -->
|
||||
<div style="margin-bottom: 16px;" v-if="['POST', 'PUT'].includes(requestMethod)">
|
||||
<a-typography-title :level="5">请求体 (JSON)</a-typography-title>
|
||||
<a-textarea
|
||||
v-model:value="requestBody"
|
||||
placeholder='{"key": "value"}'
|
||||
:rows="6"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 查询参数 -->
|
||||
<div style="margin-bottom: 16px;" v-if="requestMethod === 'GET'">
|
||||
<a-typography-title :level="5">查询参数</a-typography-title>
|
||||
<a-textarea
|
||||
v-model:value="queryParams"
|
||||
placeholder='{"page": 1, "limit": 10}'
|
||||
:rows="3"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 发送请求按钮 -->
|
||||
<a-space style="width: 100%;">
|
||||
<a-button
|
||||
type="primary"
|
||||
@click="sendRequest"
|
||||
:loading="requesting"
|
||||
size="large"
|
||||
>
|
||||
<template #icon><SendOutlined /></template>
|
||||
发送请求
|
||||
</a-button>
|
||||
<a-button @click="saveToHistory" :disabled="!lastResponse">
|
||||
<template #icon><SaveOutlined /></template>
|
||||
保存到历史
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
</a-col>
|
||||
|
||||
<!-- 右侧:响应面板 -->
|
||||
<a-col :span="12">
|
||||
<a-card title="API响应" :bordered="false">
|
||||
<!-- 响应状态 -->
|
||||
<div v-if="lastResponse" style="margin-bottom: 16px;">
|
||||
<a-descriptions size="small" :column="2">
|
||||
<a-descriptions-item label="状态码">
|
||||
<a-tag
|
||||
:color="getStatusColor(lastResponse.status)"
|
||||
>
|
||||
{{ lastResponse.status }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="响应时间">
|
||||
{{ lastResponse.duration }}ms
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="响应大小">
|
||||
{{ formatResponseSize(lastResponse.size) }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="内容类型">
|
||||
{{ lastResponse.contentType }}
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 响应内容 -->
|
||||
<div v-if="lastResponse">
|
||||
<a-typography-title :level="5">响应数据</a-typography-title>
|
||||
<a-textarea
|
||||
:value="formatResponse(lastResponse.data)"
|
||||
:rows="15"
|
||||
readonly
|
||||
style="font-family: 'Courier New', monospace; font-size: 12px;"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<a-empty
|
||||
v-else
|
||||
description="发送请求后将显示响应结果"
|
||||
style="margin: 60px 0;"
|
||||
>
|
||||
<template #image>
|
||||
<ApiOutlined style="font-size: 64px; color: #d9d9d9;" />
|
||||
</template>
|
||||
</a-empty>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 请求历史 -->
|
||||
<a-row style="margin-top: 24px;">
|
||||
<a-col :span="24">
|
||||
<a-card title="请求历史" :bordered="false">
|
||||
<a-table
|
||||
:columns="historyColumns"
|
||||
:data-source="requestHistory"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="timestamp"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'method'">
|
||||
<a-tag :color="getMethodColor(record.method)">
|
||||
{{ record.method }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'timestamp'">
|
||||
{{ formatDate(record.timestamp) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space size="small">
|
||||
<a-button size="small" @click="loadFromHistory(record)">
|
||||
加载
|
||||
</a-button>
|
||||
<a-button size="small" @click="viewResponse(record)">
|
||||
查看响应
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 响应查看模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showResponseModal"
|
||||
title="查看响应详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<div v-if="selectedHistoryItem">
|
||||
<a-descriptions :column="2" size="small" style="margin-bottom: 16px;">
|
||||
<a-descriptions-item label="请求方法">
|
||||
<a-tag :color="getMethodColor(selectedHistoryItem.method)">
|
||||
{{ selectedHistoryItem.method }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="状态码">
|
||||
<a-tag :color="getStatusColor(selectedHistoryItem.status)">
|
||||
{{ selectedHistoryItem.status }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求URL">
|
||||
{{ selectedHistoryItem.url }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="响应时间">
|
||||
{{ selectedHistoryItem.duration }}ms
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
||||
<a-typography-title :level="5">响应数据</a-typography-title>
|
||||
<a-textarea
|
||||
:value="formatResponse(selectedHistoryItem.response)"
|
||||
:rows="12"
|
||||
readonly
|
||||
style="font-family: 'Courier New', monospace; font-size: 12px;"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ClearOutlined,
|
||||
ApiOutlined,
|
||||
SendOutlined,
|
||||
SaveOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import moment from 'moment'
|
||||
|
||||
// 响应式数据
|
||||
const requesting = ref(false)
|
||||
const selectedApi = ref('')
|
||||
const requestMethod = ref('GET')
|
||||
const requestUrl = ref('')
|
||||
const requestHeaders = ref('{"Authorization": "Bearer ' + localStorage.getItem('token') + '", "Content-Type": "application/json"}')
|
||||
const requestBody = ref('')
|
||||
const queryParams = ref('')
|
||||
const lastResponse = ref(null)
|
||||
const requestHistory = ref([])
|
||||
const showResponseModal = ref(false)
|
||||
const selectedHistoryItem = ref(null)
|
||||
|
||||
// API模板
|
||||
const apiTemplates = {
|
||||
'auth-login': {
|
||||
method: 'POST',
|
||||
url: '/api/auth/login',
|
||||
body: '{"username": "admin", "password": "admin123"}'
|
||||
},
|
||||
'farms-list': {
|
||||
method: 'GET',
|
||||
url: '/api/farms',
|
||||
params: '{"page": 1, "limit": 10}'
|
||||
},
|
||||
'farms-create': {
|
||||
method: 'POST',
|
||||
url: '/api/farms',
|
||||
body: '{"name": "测试农场", "type": "养牛场", "location": {"latitude": 38.4872, "longitude": 106.2309}, "address": "银川市测试地址", "contact": "张三", "phone": "13800138000"}'
|
||||
},
|
||||
'farms-search': {
|
||||
method: 'GET',
|
||||
url: '/api/farms/search',
|
||||
params: '{"farmName": "测试"}'
|
||||
},
|
||||
'devices-list': {
|
||||
method: 'GET',
|
||||
url: '/api/devices'
|
||||
},
|
||||
'devices-search': {
|
||||
method: 'GET',
|
||||
url: '/api/devices/search',
|
||||
params: '{"deviceName": "传感器"}'
|
||||
},
|
||||
'animals-list': {
|
||||
method: 'GET',
|
||||
url: '/api/animals'
|
||||
},
|
||||
'alerts-list': {
|
||||
method: 'GET',
|
||||
url: '/api/alerts'
|
||||
},
|
||||
'users-list': {
|
||||
method: 'GET',
|
||||
url: '/api/users'
|
||||
},
|
||||
'products-list': {
|
||||
method: 'GET',
|
||||
url: '/api/products'
|
||||
},
|
||||
'orders-list': {
|
||||
method: 'GET',
|
||||
url: '/api/orders'
|
||||
},
|
||||
'reports-farm': {
|
||||
method: 'POST',
|
||||
url: '/api/reports/farm',
|
||||
body: '{"startDate": "2025-01-01", "endDate": "2025-01-18", "format": "pdf"}'
|
||||
},
|
||||
'system-configs': {
|
||||
method: 'GET',
|
||||
url: '/api/system/configs'
|
||||
},
|
||||
'system-stats': {
|
||||
method: 'GET',
|
||||
url: '/api/system/stats'
|
||||
}
|
||||
}
|
||||
|
||||
// 历史记录表格列
|
||||
const historyColumns = [
|
||||
{
|
||||
title: '方法',
|
||||
dataIndex: 'method',
|
||||
key: 'method',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: 'URL',
|
||||
dataIndex: 'url',
|
||||
key: 'url',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 120
|
||||
}
|
||||
]
|
||||
|
||||
// 组件挂载时加载历史记录
|
||||
onMounted(() => {
|
||||
loadHistory()
|
||||
})
|
||||
|
||||
// 加载API模板
|
||||
function loadApiTemplate(value) {
|
||||
const template = apiTemplates[value]
|
||||
if (template) {
|
||||
requestMethod.value = template.method
|
||||
requestUrl.value = template.url
|
||||
requestBody.value = template.body || ''
|
||||
queryParams.value = template.params || ''
|
||||
}
|
||||
}
|
||||
|
||||
// 发送API请求
|
||||
async function sendRequest() {
|
||||
if (!requestUrl.value.trim()) {
|
||||
message.error('请输入API端点')
|
||||
return
|
||||
}
|
||||
|
||||
requesting.value = true
|
||||
const startTime = Date.now()
|
||||
|
||||
try {
|
||||
// 解析请求头
|
||||
let headers = {}
|
||||
try {
|
||||
headers = JSON.parse(requestHeaders.value || '{}')
|
||||
} catch (error) {
|
||||
message.error('请求头格式错误,请使用有效的JSON格式')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建请求配置
|
||||
const config = {
|
||||
method: requestMethod.value,
|
||||
url: requestUrl.value,
|
||||
headers
|
||||
}
|
||||
|
||||
// 添加请求体(POST/PUT)
|
||||
if (['POST', 'PUT'].includes(requestMethod.value) && requestBody.value.trim()) {
|
||||
try {
|
||||
config.data = JSON.parse(requestBody.value)
|
||||
} catch (error) {
|
||||
message.error('请求体格式错误,请使用有效的JSON格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 添加查询参数(GET)
|
||||
if (requestMethod.value === 'GET' && queryParams.value.trim()) {
|
||||
try {
|
||||
config.params = JSON.parse(queryParams.value)
|
||||
} catch (error) {
|
||||
message.error('查询参数格式错误,请使用有效的JSON格式')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 发送请求
|
||||
const response = await api.request(config)
|
||||
const endTime = Date.now()
|
||||
|
||||
lastResponse.value = {
|
||||
status: 200,
|
||||
data: response,
|
||||
duration: endTime - startTime,
|
||||
size: JSON.stringify(response).length,
|
||||
contentType: 'application/json'
|
||||
}
|
||||
|
||||
message.success('请求发送成功')
|
||||
} catch (error) {
|
||||
const endTime = Date.now()
|
||||
|
||||
lastResponse.value = {
|
||||
status: error.response?.status || 500,
|
||||
data: error.response?.data || { error: error.message },
|
||||
duration: endTime - startTime,
|
||||
size: JSON.stringify(error.response?.data || {}).length,
|
||||
contentType: 'application/json'
|
||||
}
|
||||
|
||||
message.error('请求失败: ' + error.message)
|
||||
} finally {
|
||||
requesting.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 保存到历史记录
|
||||
function saveToHistory() {
|
||||
if (!lastResponse.value) {
|
||||
message.warning('没有响应数据可保存')
|
||||
return
|
||||
}
|
||||
|
||||
const historyItem = {
|
||||
timestamp: Date.now(),
|
||||
method: requestMethod.value,
|
||||
url: requestUrl.value,
|
||||
headers: requestHeaders.value,
|
||||
body: requestBody.value,
|
||||
params: queryParams.value,
|
||||
status: lastResponse.value.status,
|
||||
response: lastResponse.value.data,
|
||||
duration: lastResponse.value.duration
|
||||
}
|
||||
|
||||
requestHistory.value.unshift(historyItem)
|
||||
|
||||
// 只保留最近50条记录
|
||||
if (requestHistory.value.length > 50) {
|
||||
requestHistory.value = requestHistory.value.slice(0, 50)
|
||||
}
|
||||
|
||||
// 保存到本地存储
|
||||
localStorage.setItem('api-test-history', JSON.stringify(requestHistory.value))
|
||||
|
||||
message.success('已保存到历史记录')
|
||||
}
|
||||
|
||||
// 从历史记录加载
|
||||
function loadFromHistory(record) {
|
||||
requestMethod.value = record.method
|
||||
requestUrl.value = record.url
|
||||
requestHeaders.value = record.headers
|
||||
requestBody.value = record.body
|
||||
queryParams.value = record.params
|
||||
|
||||
message.info('已从历史记录加载请求配置')
|
||||
}
|
||||
|
||||
// 查看历史响应
|
||||
function viewResponse(record) {
|
||||
selectedHistoryItem.value = record
|
||||
showResponseModal.value = true
|
||||
}
|
||||
|
||||
// 清空历史记录
|
||||
function clearHistory() {
|
||||
requestHistory.value = []
|
||||
localStorage.removeItem('api-test-history')
|
||||
message.success('历史记录已清空')
|
||||
}
|
||||
|
||||
// 加载历史记录
|
||||
function loadHistory() {
|
||||
try {
|
||||
const stored = localStorage.getItem('api-test-history')
|
||||
if (stored) {
|
||||
requestHistory.value = JSON.parse(stored)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载历史记录失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 打开Swagger文档
|
||||
function openSwaggerDocs() {
|
||||
const apiBaseUrl = import.meta.env.VITE_API_FULL_URL || 'http://localhost:5350/api'
|
||||
const docsUrl = apiBaseUrl.replace('/api', '/api-docs')
|
||||
window.open(docsUrl, '_blank')
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function getMethodColor(method) {
|
||||
const colors = {
|
||||
GET: 'green',
|
||||
POST: 'blue',
|
||||
PUT: 'orange',
|
||||
DELETE: 'red'
|
||||
}
|
||||
return colors[method] || 'default'
|
||||
}
|
||||
|
||||
function getStatusColor(status) {
|
||||
if (status >= 200 && status < 300) return 'green'
|
||||
if (status >= 400 && status < 500) return 'orange'
|
||||
if (status >= 500) return 'red'
|
||||
return 'default'
|
||||
}
|
||||
|
||||
function formatResponse(data) {
|
||||
try {
|
||||
return JSON.stringify(data, null, 2)
|
||||
} catch (error) {
|
||||
return String(data)
|
||||
}
|
||||
}
|
||||
|
||||
function formatResponseSize(size) {
|
||||
if (size < 1024) return `${size} B`
|
||||
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
|
||||
return `${(size / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
function formatDate(timestamp) {
|
||||
return moment(timestamp).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.api-tester-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.api-tester-content {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 8px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.ant-page-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-textarea) {
|
||||
font-family: 'Courier New', Monaco, monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
:deep(.ant-card-body) {
|
||||
height: calc(100% - 57px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.api-tester-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-col) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.ant-row {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-col {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1398
admin-system/frontend/src/views/CattleArchives.vue
Normal file
1398
admin-system/frontend/src/views/CattleArchives.vue
Normal file
File diff suppressed because it is too large
Load Diff
1163
admin-system/frontend/src/views/CattleBatches.vue
Normal file
1163
admin-system/frontend/src/views/CattleBatches.vue
Normal file
File diff suppressed because it is too large
Load Diff
1147
admin-system/frontend/src/views/CattleExitRecords.vue
Normal file
1147
admin-system/frontend/src/views/CattleExitRecords.vue
Normal file
File diff suppressed because it is too large
Load Diff
777
admin-system/frontend/src/views/CattlePens.vue
Normal file
777
admin-system/frontend/src/views/CattlePens.vue
Normal file
@@ -0,0 +1,777 @@
|
||||
<template>
|
||||
<div class="cattle-pens">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">栏舍设置</h1>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮区域 -->
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<a-button type="primary" @click="handleAdd">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增栏舍
|
||||
</a-button>
|
||||
<a-button @click="handleBatchDelete" :disabled="selectedRowKeys.length === 0">
|
||||
<template #icon>
|
||||
<DeleteOutlined />
|
||||
</template>
|
||||
批量删除
|
||||
</a-button>
|
||||
<a-button @click="handleExport">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 搜索框 -->
|
||||
<div class="search-container">
|
||||
<a-input
|
||||
v-model="searchValue"
|
||||
placeholder="请输入栏舍名称(精确匹配)"
|
||||
class="search-input"
|
||||
@pressEnter="handleSearch"
|
||||
@input="handleSearchInput"
|
||||
>
|
||||
<template #suffix>
|
||||
<SearchOutlined @click="handleSearch" />
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-selection="rowSelection"
|
||||
class="pens-table"
|
||||
size="middle"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="record.status === '启用' ? 'green' : 'red'">
|
||||
{{ record.status }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleEdit(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="handleViewAnimals(record)">
|
||||
查看牛只
|
||||
</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑弹窗 -->
|
||||
<a-modal
|
||||
:open="modalVisible"
|
||||
@update:open="modalVisible = $event"
|
||||
:title="modalTitle"
|
||||
width="600px"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="栏舍名称" name="name">
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入栏舍名称"
|
||||
@input="handleFieldChange('name', $event.target.value)"
|
||||
@change="handleFieldChange('name', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="栏舍编号" name="code">
|
||||
<a-input
|
||||
v-model="formData.code"
|
||||
placeholder="请输入栏舍编号"
|
||||
@input="handleFieldChange('code', $event.target.value)"
|
||||
@change="handleFieldChange('code', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="栏舍类型" name="type">
|
||||
<a-select
|
||||
v-model="formData.type"
|
||||
placeholder="请选择栏舍类型"
|
||||
@change="handleFieldChange('type', $event)"
|
||||
>
|
||||
<a-select-option value="育成栏">育成栏</a-select-option>
|
||||
<a-select-option value="产房">产房</a-select-option>
|
||||
<a-select-option value="配种栏">配种栏</a-select-option>
|
||||
<a-select-option value="隔离栏">隔离栏</a-select-option>
|
||||
<a-select-option value="治疗栏">治疗栏</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="容量" name="capacity">
|
||||
<a-input-number
|
||||
v-model="formData.capacity"
|
||||
:min="1"
|
||||
:max="1000"
|
||||
style="width: 100%"
|
||||
placeholder="请输入栏舍容量"
|
||||
@change="handleFieldChange('capacity', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="当前牛只数量" name="currentCount">
|
||||
<a-input-number
|
||||
v-model="formData.currentCount"
|
||||
:min="0"
|
||||
:max="formData.capacity || 1000"
|
||||
style="width: 100%"
|
||||
placeholder="当前牛只数量"
|
||||
@change="handleFieldChange('currentCount', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="面积(平方米)" name="area">
|
||||
<a-input-number
|
||||
v-model="formData.area"
|
||||
:min="0"
|
||||
:precision="2"
|
||||
style="width: 100%"
|
||||
placeholder="请输入栏舍面积"
|
||||
@change="handleFieldChange('area', $event)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="位置描述" name="location">
|
||||
<a-textarea
|
||||
v-model="formData.location"
|
||||
:rows="3"
|
||||
placeholder="请输入栏舍位置描述"
|
||||
@input="handleFieldChange('location', $event.target.value)"
|
||||
@change="handleFieldChange('location', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-radio-group
|
||||
v-model="formData.status"
|
||||
@change="handleFieldChange('status', $event.target.value)"
|
||||
>
|
||||
<a-radio value="启用">启用</a-radio>
|
||||
<a-radio value="停用">停用</a-radio>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="备注" name="remark">
|
||||
<a-textarea
|
||||
v-model="formData.remark"
|
||||
:rows="3"
|
||||
placeholder="请输入备注信息"
|
||||
@input="handleFieldChange('remark', $event.target.value)"
|
||||
@change="handleFieldChange('remark', $event.target.value)"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 查看牛只弹窗 -->
|
||||
<a-modal
|
||||
:open="animalsModalVisible"
|
||||
@update:open="animalsModalVisible = $event"
|
||||
title="栏舍牛只信息"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-table
|
||||
:columns="animalColumns"
|
||||
:data-source="currentPenAnimals"
|
||||
:pagination="{ pageSize: 5 }"
|
||||
size="small"
|
||||
/>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
PlusOutlined,
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import { api } from '../utils/api'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const searchValue = ref('')
|
||||
const modalVisible = ref(false)
|
||||
const animalsModalVisible = ref(false)
|
||||
const modalTitle = ref('新增栏舍')
|
||||
const formRef = ref()
|
||||
const selectedRowKeys = ref([])
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
|
||||
// 当前栏舍的牛只数据
|
||||
const currentPenAnimals = ref([])
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '栏舍名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '栏舍编号',
|
||||
dataIndex: 'code',
|
||||
key: 'code',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '栏舍类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '容量',
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '当前牛只数量',
|
||||
dataIndex: 'currentCount',
|
||||
key: 'currentCount',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '面积(平方米)',
|
||||
dataIndex: 'area',
|
||||
key: 'area',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '位置描述',
|
||||
dataIndex: 'location',
|
||||
key: 'location',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createTime',
|
||||
key: 'createTime',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 牛只表格列配置
|
||||
const animalColumns = [
|
||||
{
|
||||
title: '耳号',
|
||||
dataIndex: 'earTag',
|
||||
key: 'earTag'
|
||||
},
|
||||
{
|
||||
title: '品种',
|
||||
dataIndex: 'breed',
|
||||
key: 'breed'
|
||||
},
|
||||
{
|
||||
title: '性别',
|
||||
dataIndex: 'gender',
|
||||
key: 'gender'
|
||||
},
|
||||
{
|
||||
title: '月龄',
|
||||
dataIndex: 'ageInMonths',
|
||||
key: 'ageInMonths'
|
||||
},
|
||||
{
|
||||
title: '生理阶段',
|
||||
dataIndex: 'physiologicalStage',
|
||||
key: 'physiologicalStage'
|
||||
}
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
||||
})
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
name: '',
|
||||
code: '',
|
||||
type: '',
|
||||
capacity: null,
|
||||
currentCount: null,
|
||||
area: null,
|
||||
location: '',
|
||||
status: '启用',
|
||||
remark: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入栏舍名称', trigger: 'blur' }
|
||||
],
|
||||
code: [
|
||||
{ required: true, message: '请输入栏舍编号', trigger: 'blur' }
|
||||
],
|
||||
type: [
|
||||
{ required: true, message: '请选择栏舍类型', trigger: 'change' }
|
||||
],
|
||||
capacity: [
|
||||
{ required: true, message: '请输入栏舍容量', trigger: 'blur' }
|
||||
],
|
||||
area: [
|
||||
{ required: true, message: '请输入栏舍面积', trigger: 'blur' }
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: '请选择状态', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 方法
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize
|
||||
}
|
||||
|
||||
// 精确匹配栏舍名称
|
||||
if (searchValue.value) {
|
||||
params.name = searchValue.value
|
||||
}
|
||||
|
||||
console.log('📤 [栏舍设置] 发送请求参数:', params)
|
||||
|
||||
const response = await api.cattlePens.getList(params)
|
||||
console.log('📥 [栏舍设置] 接收到的响应数据:', response)
|
||||
|
||||
if (response.success) {
|
||||
console.log('📊 [栏舍设置] 原始列表数据:', response.data.list)
|
||||
|
||||
tableData.value = response.data.list.map(item => ({
|
||||
...item,
|
||||
key: item.id,
|
||||
createTime: item.created_at ? dayjs(item.created_at).format('YYYY-MM-DD HH:mm:ss') : '-'
|
||||
}))
|
||||
|
||||
console.log('📊 [栏舍设置] 格式化后的表格数据:', tableData.value)
|
||||
pagination.total = response.data.total
|
||||
console.log('📊 [栏舍设置] 总记录数:', response.data.total)
|
||||
} else {
|
||||
console.warn('⚠️ [栏舍设置] 请求成功但返回失败状态:', response.message)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [栏舍设置] 加载数据失败:', error)
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
modalTitle.value = '新增栏舍'
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
console.log('🔄 [栏舍设置] 开始编辑操作')
|
||||
console.log('📋 [栏舍设置] 原始记录数据:', {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
type: record.type,
|
||||
status: record.status,
|
||||
capacity: record.capacity,
|
||||
area: record.area,
|
||||
location: record.location,
|
||||
description: record.description,
|
||||
remark: record.remark,
|
||||
farmId: record.farmId,
|
||||
farmName: record.farm?.name
|
||||
})
|
||||
|
||||
modalTitle.value = '编辑栏舍'
|
||||
Object.assign(formData, record)
|
||||
|
||||
console.log('📝 [栏舍设置] 表单数据已填充:', formData)
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除栏舍"${record.name}"吗?`,
|
||||
async onOk() {
|
||||
try {
|
||||
await api.cattlePens.delete(record.id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleBatchDelete = () => {
|
||||
Modal.confirm({
|
||||
title: '确认批量删除',
|
||||
content: `确定要删除选中的 ${selectedRowKeys.value.length} 个栏舍吗?`,
|
||||
async onOk() {
|
||||
try {
|
||||
await api.cattlePens.batchDelete(selectedRowKeys.value)
|
||||
message.success('批量删除成功')
|
||||
selectedRowKeys.value = []
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('批量删除失败:', error)
|
||||
message.error('批量删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleViewAnimals = async (record) => {
|
||||
try {
|
||||
const response = await api.cattlePens.getAnimals(record.id)
|
||||
if (response.success) {
|
||||
currentPenAnimals.value = response.data.list.map(item => ({
|
||||
...item,
|
||||
key: item.id
|
||||
}))
|
||||
animalsModalVisible.value = true
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取栏舍牛只失败:', error)
|
||||
message.error('获取栏舍牛只失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
console.log('=== 开始导出栏舍数据 ===')
|
||||
console.log('tableData.value长度:', tableData.value.length)
|
||||
console.log('tableData.value示例:', tableData.value[0])
|
||||
|
||||
if (!tableData.value || tableData.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
// 转换数据格式以匹配导出工具类的列配置
|
||||
const exportData = tableData.value.map(item => {
|
||||
console.log('转换前栏舍数据项:', item)
|
||||
return {
|
||||
id: item.id,
|
||||
name: item.name || '',
|
||||
animal_type: item.animalType || item.animal_type || '牛', // 默认动物类型为牛
|
||||
pen_type: item.type || item.penType || item.pen_type || '', // 使用type字段
|
||||
responsible: item.responsible || item.manager || item.managerName || '',
|
||||
capacity: item.capacity || 0,
|
||||
status: item.status || '启用',
|
||||
creator: item.creator || item.created_by || item.creatorName || '',
|
||||
created_at: item.created_at || item.createTime || ''
|
||||
}
|
||||
})
|
||||
|
||||
console.log('转换后栏舍数据示例:', exportData[0])
|
||||
console.log('转换后栏舍数据总数:', exportData.length)
|
||||
|
||||
const result = ExportUtils.exportPenData(exportData)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听搜索输入变化
|
||||
const handleSearchInput = (e) => {
|
||||
console.log('🔍 [栏舍设置] 搜索输入变化:', e.target.value)
|
||||
// 确保searchValue被正确更新
|
||||
searchValue.value = e.target.value
|
||||
|
||||
// 实时过滤表格数据
|
||||
debounceSearch()
|
||||
}
|
||||
|
||||
// 使用防抖函数处理实时搜索,避免频繁请求
|
||||
const debounceSearch = (() => {
|
||||
let timer = null
|
||||
return () => {
|
||||
if (timer) clearTimeout(timer)
|
||||
timer = setTimeout(() => {
|
||||
console.log('🔍 [栏舍设置] 执行实时搜索,搜索值:', searchValue.value)
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}, 300) // 300ms防抖延迟
|
||||
}
|
||||
})()
|
||||
|
||||
const handleSearch = () => {
|
||||
console.log('🔍 [栏舍设置] 执行搜索操作,搜索值:', searchValue.value)
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
}
|
||||
|
||||
// 字段变化监听方法
|
||||
const handleFieldChange = (fieldName, value) => {
|
||||
console.log(`📝 [栏舍设置] 字段变化监听: ${fieldName} = ${value}`)
|
||||
console.log(`📝 [栏舍设置] 当前表单数据:`, formData)
|
||||
|
||||
// 确保数据同步到formData
|
||||
if (formData[fieldName] !== value) {
|
||||
formData[fieldName] = value
|
||||
console.log(`✅ [栏舍设置] 字段 ${fieldName} 已更新为: ${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
console.log('💾 [栏舍设置] 开始保存操作')
|
||||
console.log('📝 [栏舍设置] 用户输入的表单数据:', {
|
||||
id: formData.id,
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
type: formData.type,
|
||||
status: formData.status,
|
||||
capacity: formData.capacity,
|
||||
currentCount: formData.currentCount,
|
||||
area: formData.area,
|
||||
location: formData.location,
|
||||
remark: formData.remark,
|
||||
farmId: formData.farmId
|
||||
})
|
||||
|
||||
await formRef.value.validate()
|
||||
console.log('✅ [栏舍设置] 表单验证通过')
|
||||
|
||||
// 准备发送给后端的数据,转换数字格式
|
||||
const submitData = {
|
||||
name: formData.name,
|
||||
code: formData.code,
|
||||
type: formData.type,
|
||||
status: formData.status,
|
||||
capacity: formData.capacity ? Number(formData.capacity) : null,
|
||||
currentCount: formData.currentCount ? Number(formData.currentCount) : 0,
|
||||
area: formData.area ? Number(formData.area) : null,
|
||||
location: formData.location || '',
|
||||
remark: formData.remark || '',
|
||||
farmId: formData.farmId || 1
|
||||
}
|
||||
|
||||
console.log('📤 [栏舍设置] 准备发送的数据:', submitData)
|
||||
|
||||
if (modalTitle.value === '新增栏舍') {
|
||||
console.log('🆕 [栏舍设置] 执行创建操作')
|
||||
await api.cattlePens.create(submitData)
|
||||
console.log('✅ [栏舍设置] 创建成功')
|
||||
message.success('创建成功')
|
||||
} else {
|
||||
console.log('🔄 [栏舍设置] 执行更新操作,记录ID:', formData.id)
|
||||
const response = await api.cattlePens.update(formData.id, submitData)
|
||||
console.log('✅ [栏舍设置] 更新成功,服务器响应:', response)
|
||||
message.success('更新成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
loadData()
|
||||
} catch (error) {
|
||||
console.error('❌ [栏舍设置] 操作失败:', error)
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.keys(formData).forEach(key => {
|
||||
if (key === 'capacity' || key === 'currentCount' || key === 'area') {
|
||||
formData[key] = null
|
||||
} else {
|
||||
formData[key] = ''
|
||||
}
|
||||
})
|
||||
formData.status = '启用'
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.cattle-pens {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
height: 40px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pens-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.pens-table :deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.pens-table :deep(.ant-table-tbody > tr > td) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.pens-table :deep(.ant-table-tbody > tr:hover > td) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cattle-pens {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1098
admin-system/frontend/src/views/CattleTransferRecords.vue
Normal file
1098
admin-system/frontend/src/views/CattleTransferRecords.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,8 @@
|
||||
<template>
|
||||
<div class="dashboard-page">
|
||||
<a-page-header
|
||||
title="系统概览"
|
||||
sub-title="宁夏智慧养殖监管平台数据概览"
|
||||
title="宁夏智慧养殖监管平台数据概览"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button>导出报表</a-button>
|
||||
<a-button type="primary">刷新数据</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<Dashboard />
|
||||
|
||||
@@ -2,9 +2,35 @@
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>设备管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加设备
|
||||
<a-space>
|
||||
<a-button @click="exportDevices" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加设备
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-auto-complete
|
||||
v-model:value="searchDeviceName"
|
||||
:options="deviceNameOptions"
|
||||
placeholder="请选择或输入设备名称进行搜索"
|
||||
style="width: 300px;"
|
||||
:filter-option="false"
|
||||
@search="handleSearchInput"
|
||||
@press-enter="searchDevices"
|
||||
/>
|
||||
<a-button type="primary" @click="searchDevices" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -23,7 +49,7 @@
|
||||
{{ record.name }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'type'">
|
||||
{{ record.type }}
|
||||
<a-tag color="blue">{{ getTypeText(record.type) }}</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
@@ -130,8 +156,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
// 响应式数据
|
||||
@@ -144,6 +171,15 @@ const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchDeviceName = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const deviceNameOptions = ref([])
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
@@ -228,13 +264,9 @@ const fetchDevices = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get('/api/devices', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
devices.value = response.data.data
|
||||
const response = await api.get('/devices')
|
||||
if (response.success) {
|
||||
devices.value = response.data
|
||||
} else {
|
||||
message.error('获取设备列表失败')
|
||||
}
|
||||
@@ -259,13 +291,9 @@ const fetchFarms = async () => {
|
||||
try {
|
||||
farmsLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.get('/api/farms', {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
farms.value = response.data.data
|
||||
const response = await api.get('/farms')
|
||||
if (response.success) {
|
||||
farms.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取农场列表失败:', error)
|
||||
@@ -315,12 +343,8 @@ const editDevice = (record) => {
|
||||
const deleteDevice = async (id) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.delete(`/api/devices/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
const response = await api.delete(`/devices/${id}`)
|
||||
if (response.success) {
|
||||
message.success('删除成功')
|
||||
fetchDevices()
|
||||
} else {
|
||||
@@ -362,12 +386,12 @@ const handleSubmit = async () => {
|
||||
|
||||
let response
|
||||
if (isEdit.value) {
|
||||
response = await axios.put(`/api/devices/${formData.id}`, submitData, config)
|
||||
response = await api.put(`/devices/${formData.id}`, submitData)
|
||||
} else {
|
||||
response = await axios.post('/api/devices', submitData, config)
|
||||
response = await api.post('/devices', submitData)
|
||||
}
|
||||
|
||||
if (response.data.success) {
|
||||
if (response.success) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
modalVisible.value = false
|
||||
fetchDevices()
|
||||
@@ -424,6 +448,24 @@ const getStatusText = (status) => {
|
||||
return texts[status] || status
|
||||
}
|
||||
|
||||
// 获取类型文本
|
||||
const getTypeText = (type) => {
|
||||
const texts = {
|
||||
temperature_sensor: '温度传感器',
|
||||
humidity_sensor: '湿度传感器',
|
||||
feed_dispenser: '饲料分配器',
|
||||
water_system: '水系统',
|
||||
ventilation_system: '通风系统',
|
||||
sensor: '传感器',
|
||||
camera: '摄像头',
|
||||
feeder: '喂食器',
|
||||
monitor: '监控器',
|
||||
controller: '控制器',
|
||||
other: '其他'
|
||||
}
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 获取农场名称
|
||||
const getFarmName = (farmId) => {
|
||||
const farm = farms.value.find(f => f.id === farmId)
|
||||
@@ -436,9 +478,105 @@ const formatDate = (date) => {
|
||||
return dayjs(date).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
// 搜索设备
|
||||
const searchDevices = async () => {
|
||||
if (!searchDeviceName.value.trim()) {
|
||||
message.warning('请输入设备名称进行搜索')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await api.get('/devices/search', {
|
||||
params: { name: searchDeviceName.value.trim() }
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
devices.value = response.data || []
|
||||
isSearching.value = true
|
||||
message.success(response.message || `找到 ${devices.value.length} 个匹配的设备`)
|
||||
} else {
|
||||
devices.value = []
|
||||
message.info('未找到匹配的设备')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error)
|
||||
message.error('搜索设备失败')
|
||||
devices.value = []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入(为自动完成提供选项)
|
||||
const handleSearchInput = (value) => {
|
||||
if (!value) {
|
||||
deviceNameOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 从现有设备列表中筛选匹配的设备名称
|
||||
const matchingDevices = devices.value.filter(device =>
|
||||
device.name.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
|
||||
deviceNameOptions.value = matchingDevices.map(device => ({
|
||||
value: device.name,
|
||||
label: device.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchDeviceName.value = ''
|
||||
isSearching.value = false
|
||||
deviceNameOptions.value = []
|
||||
fetchDevices() // 重新加载全部设备
|
||||
}
|
||||
|
||||
// 更新设备名称选项(在数据加载后)
|
||||
const updateDeviceNameOptions = () => {
|
||||
deviceNameOptions.value = devices.value.map(device => ({
|
||||
value: device.name,
|
||||
label: device.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 导出设备数据
|
||||
const exportDevices = async () => {
|
||||
try {
|
||||
if (!devices.value || devices.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportDevicesData(devices.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchDevices()
|
||||
fetchDevices().then(() => {
|
||||
updateDeviceNameOptions()
|
||||
})
|
||||
fetchFarms()
|
||||
})
|
||||
</script>
|
||||
|
||||
1336
admin-system/frontend/src/views/ElectronicFence.vue
Normal file
1336
admin-system/frontend/src/views/ElectronicFence.vue
Normal file
File diff suppressed because it is too large
Load Diff
1411
admin-system/frontend/src/views/FarmInfoManagement.vue
Normal file
1411
admin-system/frontend/src/views/FarmInfoManagement.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,35 @@
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>养殖场管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加养殖场
|
||||
<a-space>
|
||||
<a-button @click="exportFarms" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加养殖场
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-auto-complete
|
||||
v-model:value="searchFarmName"
|
||||
:options="farmNameOptions"
|
||||
placeholder="请选择或输入养殖场名称进行搜索"
|
||||
style="width: 300px;"
|
||||
:filter-option="false"
|
||||
@search="handleSearchInput"
|
||||
@press-enter="searchFarms"
|
||||
/>
|
||||
<a-button type="primary" @click="searchFarms" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -184,7 +210,8 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
// 响应式数据
|
||||
const farms = ref([])
|
||||
@@ -194,6 +221,15 @@ const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchFarmName = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const farmNameOptions = ref([])
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
@@ -335,7 +371,13 @@ const fetchFarms = async () => {
|
||||
loading.value = true
|
||||
const { api } = await import('../utils/api')
|
||||
const response = await api.get('/farms')
|
||||
if (Array.isArray(response)) {
|
||||
console.log('养殖场API响应:', response)
|
||||
|
||||
// 检查响应格式
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
farms.value = response.data
|
||||
} else if (Array.isArray(response)) {
|
||||
// 兼容旧格式
|
||||
farms.value = response
|
||||
} else {
|
||||
farms.value = []
|
||||
@@ -437,9 +479,105 @@ const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 搜索养殖场
|
||||
const searchFarms = async () => {
|
||||
if (!searchFarmName.value.trim()) {
|
||||
message.warning('请输入养殖场名称进行搜索')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const { api } = await import('../utils/api')
|
||||
const response = await api.get('/farms/search', {
|
||||
params: { name: searchFarmName.value.trim() }
|
||||
})
|
||||
|
||||
if (Array.isArray(response)) {
|
||||
farms.value = response
|
||||
isSearching.value = true
|
||||
message.success(`找到 ${response.length} 个匹配的养殖场`)
|
||||
} else {
|
||||
farms.value = []
|
||||
message.info('未找到匹配的养殖场')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索养殖场失败:', error)
|
||||
message.error('搜索养殖场失败')
|
||||
farms.value = []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入(为自动完成提供选项)
|
||||
const handleSearchInput = (value) => {
|
||||
if (!value) {
|
||||
farmNameOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 从现有养殖场列表中筛选匹配的养殖场名称
|
||||
const matchingFarms = farms.value.filter(farm =>
|
||||
farm.name.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
|
||||
farmNameOptions.value = matchingFarms.map(farm => ({
|
||||
value: farm.name,
|
||||
label: farm.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 更新养殖场名称选项(在数据加载后)
|
||||
const updateFarmNameOptions = () => {
|
||||
farmNameOptions.value = farms.value.map(farm => ({
|
||||
value: farm.name,
|
||||
label: farm.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 导出农场数据
|
||||
const exportFarms = async () => {
|
||||
try {
|
||||
if (!farms.value || farms.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportFarmsData(farms.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchFarmName.value = ''
|
||||
isSearching.value = false
|
||||
farmNameOptions.value = []
|
||||
fetchFarms() // 重新加载全部养殖场
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchFarms()
|
||||
fetchFarms().then(() => {
|
||||
updateFarmNameOptions()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
599
admin-system/frontend/src/views/FormLogManagement.vue
Normal file
599
admin-system/frontend/src/views/FormLogManagement.vue
Normal file
@@ -0,0 +1,599 @@
|
||||
<template>
|
||||
<div class="form-log-management">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1 class="page-title">表单日志管理</h1>
|
||||
<div class="header-actions">
|
||||
<a-button @click="handleRefresh" :loading="loading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="handleExport" :disabled="selectedRowKeys.length === 0">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出日志
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 筛选区域 -->
|
||||
<div class="filter-bar">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
v-model="filters.module"
|
||||
placeholder="选择模块"
|
||||
allowClear
|
||||
style="width: 100%"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="farm-management">养殖场管理</a-select-option>
|
||||
<a-select-option value="animal-management">动物管理</a-select-option>
|
||||
<a-select-option value="device-management">设备管理</a-select-option>
|
||||
<a-select-option value="alert-management">预警管理</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
v-model="filters.action"
|
||||
placeholder="选择操作"
|
||||
allowClear
|
||||
style="width: 100%"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="form_submit_start">表单提交开始</a-select-option>
|
||||
<a-select-option value="form_validation_success">验证成功</a-select-option>
|
||||
<a-select-option value="form_validation_failed">验证失败</a-select-option>
|
||||
<a-select-option value="form_create_success">创建成功</a-select-option>
|
||||
<a-select-option value="form_edit_success">编辑成功</a-select-option>
|
||||
<a-select-option value="field_change">字段变化</a-select-option>
|
||||
<a-select-option value="search">搜索操作</a-select-option>
|
||||
<a-select-option value="modal_open">打开模态框</a-select-option>
|
||||
<a-select-option value="modal_cancel">取消操作</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-select
|
||||
v-model="filters.status"
|
||||
placeholder="选择状态"
|
||||
allowClear
|
||||
style="width: 100%"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="success">成功</a-select-option>
|
||||
<a-select-option value="error">错误</a-select-option>
|
||||
<a-select-option value="warning">警告</a-select-option>
|
||||
</a-select>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-range-picker
|
||||
v-model="filters.dateRange"
|
||||
style="width: 100%"
|
||||
@change="handleFilterChange"
|
||||
/>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16" style="margin-top: 16px;">
|
||||
<a-col :span="12">
|
||||
<a-input
|
||||
v-model="filters.keyword"
|
||||
placeholder="搜索用户名、操作或模块"
|
||||
@pressEnter="handleSearch"
|
||||
@change="handleSearch"
|
||||
>
|
||||
<template #suffix>
|
||||
<SearchOutlined @click="handleSearch" style="cursor: pointer;" />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button @click="handleSearch" type="primary">
|
||||
搜索
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button @click="handleResetFilters">
|
||||
重置筛选
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息卡片 -->
|
||||
<div class="stats-cards">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总日志数"
|
||||
:value="stats.totalLogs"
|
||||
:loading="statsLoading"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="成功操作"
|
||||
:value="stats.successCount"
|
||||
:loading="statsLoading"
|
||||
value-style="color: #52c41a"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="错误操作"
|
||||
:value="stats.errorCount"
|
||||
:loading="statsLoading"
|
||||
value-style="color: #ff4d4f"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="活跃用户"
|
||||
:value="stats.activeUsers"
|
||||
:loading="statsLoading"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="tableData"
|
||||
:pagination="pagination"
|
||||
:loading="loading"
|
||||
:row-selection="rowSelection"
|
||||
class="log-table"
|
||||
size="middle"
|
||||
:scroll="{ x: 1200 }"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="handleViewDetail(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
<a-button type="link" size="small" danger @click="handleDelete(record)">
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
<template v-if="column.key === 'formData'">
|
||||
<a-button type="link" size="small" @click="handleViewFormData(record)">
|
||||
查看数据
|
||||
</a-button>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 详情弹窗 -->
|
||||
<a-modal
|
||||
:open="detailModalVisible"
|
||||
@update:open="detailModalVisible = $event"
|
||||
title="日志详情"
|
||||
width="800px"
|
||||
:footer="null"
|
||||
>
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="日志ID">{{ currentRecord.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="模块">{{ currentRecord.module }}</a-descriptions-item>
|
||||
<a-descriptions-item label="操作">{{ currentRecord.action }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(currentRecord.status)">
|
||||
{{ getStatusText(currentRecord.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户">{{ currentRecord.username || '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户ID">{{ currentRecord.userId || '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="会话ID">{{ currentRecord.sessionId || '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="IP地址">{{ currentRecord.ipAddress || '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="屏幕分辨率">{{ currentRecord.screenResolution || '未知' }}</a-descriptions-item>
|
||||
<a-descriptions-item label="时间戳">{{ currentRecord.timestamp }}</a-descriptions-item>
|
||||
<a-descriptions-item label="当前URL" :span="2">
|
||||
<a :href="currentRecord.currentUrl" target="_blank" v-if="currentRecord.currentUrl">
|
||||
{{ currentRecord.currentUrl }}
|
||||
</a>
|
||||
<span v-else>未知</span>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="用户代理" :span="2">
|
||||
<div style="word-break: break-all; max-height: 100px; overflow-y: auto;">
|
||||
{{ currentRecord.userAgent || '未知' }}
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="错误信息" :span="2" v-if="currentRecord.errorMessage">
|
||||
<div style="color: #ff4d4f;">
|
||||
{{ currentRecord.errorMessage }}
|
||||
</div>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-modal>
|
||||
|
||||
<!-- 表单数据弹窗 -->
|
||||
<a-modal
|
||||
:open="formDataModalVisible"
|
||||
@update:open="formDataModalVisible = $event"
|
||||
title="表单数据"
|
||||
width="600px"
|
||||
:footer="null"
|
||||
>
|
||||
<pre style="background: #f5f5f5; padding: 16px; border-radius: 4px; max-height: 400px; overflow-y: auto;">{{ JSON.stringify(currentRecord.formData, null, 2) }}</pre>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
ExportOutlined,
|
||||
SearchOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const statsLoading = ref(false)
|
||||
const detailModalVisible = ref(false)
|
||||
const formDataModalVisible = ref(false)
|
||||
const selectedRowKeys = ref([])
|
||||
const currentRecord = ref({})
|
||||
|
||||
// 筛选条件
|
||||
const filters = reactive({
|
||||
module: null,
|
||||
action: null,
|
||||
status: null,
|
||||
dateRange: null,
|
||||
keyword: ''
|
||||
})
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
totalLogs: 0,
|
||||
successCount: 0,
|
||||
errorCount: 0,
|
||||
activeUsers: 0
|
||||
})
|
||||
|
||||
// 表格数据
|
||||
const tableData = ref([])
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '模块',
|
||||
dataIndex: 'module',
|
||||
key: 'module',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'timestamp',
|
||||
key: 'timestamp',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '表单数据',
|
||||
dataIndex: 'formData',
|
||||
key: 'formData',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 150,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`
|
||||
})
|
||||
|
||||
// 行选择配置
|
||||
const rowSelection = {
|
||||
selectedRowKeys: selectedRowKeys,
|
||||
onChange: (keys) => {
|
||||
selectedRowKeys.value = keys
|
||||
}
|
||||
}
|
||||
|
||||
// 方法
|
||||
const getStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'success': 'green',
|
||||
'error': 'red',
|
||||
'warning': 'orange'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
const getStatusText = (status) => {
|
||||
const textMap = {
|
||||
'success': '成功',
|
||||
'error': '错误',
|
||||
'warning': '警告'
|
||||
}
|
||||
return textMap[status] || status
|
||||
}
|
||||
|
||||
// 数据加载方法
|
||||
const loadData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...filters
|
||||
}
|
||||
|
||||
// 处理日期范围
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
params.startDate = filters.dateRange[0].format('YYYY-MM-DD')
|
||||
params.endDate = filters.dateRange[1].format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
console.group('📋 [日志管理] 加载数据')
|
||||
console.log('🕒 时间:', new Date().toLocaleString())
|
||||
console.log('📊 查询参数:', params)
|
||||
console.groupEnd()
|
||||
|
||||
const response = await api.formLogs.getList(params)
|
||||
|
||||
if (response.success) {
|
||||
tableData.value = response.data.list.map(item => ({
|
||||
...item,
|
||||
key: item.id
|
||||
}))
|
||||
pagination.total = response.data.total
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [日志管理] 加载数据失败:', error)
|
||||
message.error('加载数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载统计数据
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
statsLoading.value = true
|
||||
const params = {}
|
||||
|
||||
// 处理日期范围
|
||||
if (filters.dateRange && filters.dateRange.length === 2) {
|
||||
params.startDate = filters.dateRange[0].format('YYYY-MM-DD')
|
||||
params.endDate = filters.dateRange[1].format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const response = await api.formLogs.getStats(params)
|
||||
|
||||
if (response.success) {
|
||||
const data = response.data
|
||||
stats.totalLogs = data.totalLogs || 0
|
||||
stats.successCount = data.statusStats?.find(s => s.status === 'success')?.count || 0
|
||||
stats.errorCount = data.statusStats?.find(s => s.status === 'error')?.count || 0
|
||||
stats.activeUsers = data.userStats?.length || 0
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [日志管理] 加载统计失败:', error)
|
||||
} finally {
|
||||
statsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadData()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleFilterChange = () => {
|
||||
pagination.current = 1
|
||||
loadData()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleResetFilters = () => {
|
||||
Object.assign(filters, {
|
||||
module: null,
|
||||
action: null,
|
||||
status: null,
|
||||
dateRange: null,
|
||||
keyword: ''
|
||||
})
|
||||
handleFilterChange()
|
||||
}
|
||||
|
||||
const handleViewDetail = (record) => {
|
||||
currentRecord.value = record
|
||||
detailModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleViewFormData = (record) => {
|
||||
currentRecord.value = record
|
||||
formDataModalVisible.value = true
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除日志"${record.id}"吗?`,
|
||||
async onOk() {
|
||||
try {
|
||||
await api.formLogs.delete(record.id)
|
||||
message.success('删除成功')
|
||||
loadData()
|
||||
loadStats()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleExport = () => {
|
||||
message.success('导出功能开发中')
|
||||
// 实现导出逻辑
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadStats()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.form-log-management {
|
||||
padding: 24px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.filter-bar {
|
||||
margin-bottom: 24px;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-cards {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.log-table :deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.log-table :deep(.ant-table-tbody > tr > td) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.log-table :deep(.ant-table-tbody > tr:hover > td) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 1200px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.filter-bar .ant-row .ant-col {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.form-log-management {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,8 +1,5 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>欢迎使用宁夏智慧养殖监管平台</h1>
|
||||
<p>这是一个基于Vue 3和Ant Design Vue构建的现代化管理系统。</p>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="8">
|
||||
<a-card title="用户总数" style="text-align: center">
|
||||
|
||||
@@ -1,77 +1,84 @@
|
||||
<template>
|
||||
<div class="login-container">
|
||||
<a-card title="用户登录" style="width: 400px; margin: 100px auto;">
|
||||
<div class="login-hints">
|
||||
<p>测试账号:</p>
|
||||
<p>用户名: admin 或 john_doe</p>
|
||||
<p>密码: 123456</p>
|
||||
<!-- 左侧标题 -->
|
||||
<div class="left-title">
|
||||
<h1 class="main-title">智慧宁夏牧场</h1>
|
||||
</div>
|
||||
|
||||
<!-- 玻璃态登录卡片 -->
|
||||
<div class="glass-card">
|
||||
<div class="card-header">
|
||||
<h1 class="login-title">用户登录</h1>
|
||||
</div>
|
||||
|
||||
<a-alert
|
||||
v-if="error"
|
||||
:message="error"
|
||||
type="error"
|
||||
show-icon
|
||||
style="margin-bottom: 16px"
|
||||
/>
|
||||
<a-button
|
||||
:loading="loading"
|
||||
@click="handleLogin"
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
|
||||
<a-form
|
||||
:model="formState"
|
||||
name="login"
|
||||
autocomplete="off"
|
||||
@finish="handleLogin"
|
||||
>
|
||||
<a-form-item
|
||||
label="用户名"
|
||||
name="username"
|
||||
:rules="[{ required: true, message: '请输入用户名或邮箱!' }]"
|
||||
>
|
||||
<a-input v-model="formState.username">
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="密码"
|
||||
name="password"
|
||||
:rules="[
|
||||
{ required: true, message: '请输入密码!' },
|
||||
{ min: 6, message: '密码长度不能少于6位' }
|
||||
]"
|
||||
>
|
||||
<a-input-password v-model="formState.password">
|
||||
<template #prefix>
|
||||
<LockOutlined />
|
||||
</template>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<!-- 信息提示框 -->
|
||||
|
||||
<a-form-item>
|
||||
<a-button
|
||||
type="primary"
|
||||
html-type="submit"
|
||||
:loading="loading"
|
||||
block
|
||||
>
|
||||
登录
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<div v-if="error" class="error-alert">
|
||||
<span class="error-icon">⚠️</span>
|
||||
<span class="error-message">{{ error }}</span>
|
||||
</div>
|
||||
|
||||
<!-- 登录表单 -->
|
||||
<form @submit.prevent="handleLogin" class="login-form">
|
||||
<!-- 用户名输入框 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="required">*</span> 用户名:
|
||||
</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-icon">👨💼</span>
|
||||
<input
|
||||
v-model="formState.username"
|
||||
type="text"
|
||||
class="form-input"
|
||||
placeholder="请输入用户名"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<div class="input-group">
|
||||
<label class="input-label">
|
||||
<span class="required">*</span> 密码:
|
||||
</label>
|
||||
<div class="input-wrapper">
|
||||
<span class="input-icon">🔒</span>
|
||||
<input
|
||||
v-model="formState.password"
|
||||
:type="showPassword ? 'text' : 'password'"
|
||||
class="form-input"
|
||||
placeholder="请输入密码"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
class="password-toggle"
|
||||
@click="togglePassword"
|
||||
>
|
||||
{{ showPassword ? '👁️' : '🚫' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
type="submit"
|
||||
class="login-button"
|
||||
:disabled="loading"
|
||||
>
|
||||
<span v-if="loading" class="loading-spinner"></span>
|
||||
{{ loading ? '登录中...' : '登录' }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useUserStore } from '../stores';
|
||||
@@ -80,64 +87,58 @@ const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
const loading = ref(false);
|
||||
const error = ref('');
|
||||
const showPassword = ref(false);
|
||||
|
||||
const formState = reactive({
|
||||
username: 'admin',
|
||||
password: '123456'
|
||||
username: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
// 页面加载时检查是否可以自动登录
|
||||
// 切换密码显示/隐藏
|
||||
const togglePassword = () => {
|
||||
showPassword.value = !showPassword.value;
|
||||
};
|
||||
|
||||
// 页面加载时检查token状态
|
||||
onMounted(async () => {
|
||||
// 如果已有token,尝试验证并自动跳转
|
||||
// 如果已有有效token,直接跳转到仪表盘
|
||||
if (userStore.token) {
|
||||
try {
|
||||
const isValid = await userStore.validateToken();
|
||||
if (isValid) {
|
||||
const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
|
||||
message.success('自动登录成功');
|
||||
router.push(redirectPath);
|
||||
return;
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('自动登录失败,显示登录表单');
|
||||
console.log('Token验证失败,需要重新登录');
|
||||
userStore.logout(); // 清除无效token
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有token或验证失败,尝试使用默认账号自动登录
|
||||
if (!userStore.token) {
|
||||
await handleAutoLogin();
|
||||
}
|
||||
});
|
||||
|
||||
// 自动登录函数
|
||||
const handleAutoLogin = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const result = await userStore.login(formState.username, formState.password);
|
||||
if (result.success) {
|
||||
message.success('自动登录成功');
|
||||
const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
|
||||
router.push(redirectPath);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log('自动登录失败,需要手动登录');
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogin = async (values) => {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
|
||||
try {
|
||||
// 使用表单数据或默认数据进行登录
|
||||
// 使用表单数据进行登录
|
||||
const username = values?.username || formState.username;
|
||||
const password = values?.password || formState.password;
|
||||
|
||||
// 验证表单数据
|
||||
if (!username || !password) {
|
||||
error.value = '请输入用户名和密码';
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('开始登录,用户名:', username);
|
||||
|
||||
// 使用Pinia用户存储进行登录
|
||||
const result = await userStore.login(username, password);
|
||||
|
||||
console.log('登录结果:', result);
|
||||
|
||||
if (result.success) {
|
||||
// 登录成功提示
|
||||
message.success(`登录成功,欢迎 ${userStore.userData.username}`);
|
||||
@@ -160,28 +161,358 @@ const handleLogin = async (values) => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 主容器 - 图片背景 */
|
||||
.login-container {
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: url('/cows.jpg');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-attachment: fixed;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 20px 160px 20px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-hints {
|
||||
background: #e6f7ff;
|
||||
border: 1px solid #91d5ff;
|
||||
border-radius: 4px;
|
||||
padding: 12px 16px;
|
||||
/* 左侧标题 */
|
||||
.left-title {
|
||||
position: absolute;
|
||||
top: 60px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 3;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
color: #ffffff;
|
||||
font-size: 48px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 16px 0;
|
||||
text-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||
letter-spacing: 2px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
font-size: 20px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.5);
|
||||
letter-spacing: 1px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* 玻璃态卡片 */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
backdrop-filter: blur(25px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
box-shadow:
|
||||
0 8px 32px 0 rgba(0, 0, 0, 0.3),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.3);
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
max-width: 380px;
|
||||
min-width: 350px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.glass-card:hover {
|
||||
transform: translateY(-3px);
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
0 12px 40px 0 rgba(0, 0, 0, 0.4),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
/* 卡片头部 */
|
||||
.card-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
color: #fff;
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.6);
|
||||
}
|
||||
|
||||
/* 信息提示框 */
|
||||
.info-box {
|
||||
background: rgba(255, 255, 255, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.login-hints p {
|
||||
.info-text {
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.login-hints p:first-child {
|
||||
font-weight: bold;
|
||||
.info-text:first-child {
|
||||
font-weight: 600;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* 错误提示 */
|
||||
.error-alert {
|
||||
background: rgba(255, 77, 79, 0.2);
|
||||
border: 1px solid rgba(255, 77, 79, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.error-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 登录表单 */
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
/* 输入组 */
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.required {
|
||||
color: #ff4d4f;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 输入框包装器 */
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.input-icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
font-size: 16px;
|
||||
z-index: 1;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||
border-radius: 8px;
|
||||
padding: 0 16px 0 40px;
|
||||
color: #ffffff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
backdrop-filter: blur(20px);
|
||||
transition: all 0.3s ease;
|
||||
outline: none;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.form-input::placeholder {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
border-color: rgba(255, 255, 255, 0.7);
|
||||
background: rgba(255, 255, 255, 0.35);
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(255, 255, 255, 0.4),
|
||||
0 4px 16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 密码切换按钮 */
|
||||
.password-toggle {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
transition: color 0.3s ease;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.password-toggle:hover {
|
||||
color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-button {
|
||||
width: 100%;
|
||||
height: 48px;
|
||||
background: linear-gradient(90deg, #4CAF50 0%, #45a049 100%);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: #fff;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 16px rgba(76, 175, 80, 0.4);
|
||||
}
|
||||
|
||||
.login-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.login-button:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.login-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 25px rgba(76, 175, 80, 0.6);
|
||||
}
|
||||
|
||||
.login-button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.login-button:disabled {
|
||||
opacity: 0.7;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.loading-spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid rgba(255, 255, 255, 0.3);
|
||||
border-top: 2px solid #fff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.login-container {
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.left-title {
|
||||
position: relative;
|
||||
top: auto;
|
||||
left: auto;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.glass-card {
|
||||
max-width: 100%;
|
||||
min-width: auto;
|
||||
padding: 30px 20px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.glass-card {
|
||||
padding: 30px 20px;
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 深色模式支持 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.glass-card {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.form-input {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.form-input:focus {
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
border-color: rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -264,7 +264,7 @@ const alertTableData = computed(() => {
|
||||
return alerts.map(alert => ({
|
||||
key: alert.id,
|
||||
id: alert.id,
|
||||
type: alert.type,
|
||||
type: getAlertTypeText(alert.type),
|
||||
level: alert.level,
|
||||
farmId: alert.farm_id,
|
||||
farmName: dataStore.farms.find(f => f.id == alert.farm_id)?.name || '',
|
||||
@@ -328,23 +328,23 @@ function getAlertLevelText(level) {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预警类型中文
|
||||
// 获取预警类型文本(英文转中文)
|
||||
function getAlertTypeText(type) {
|
||||
const typeMap = {
|
||||
'温度异常': '温度异常',
|
||||
'湿度异常': '湿度异常',
|
||||
'设备离线': '设备离线',
|
||||
'temperature_high': '温度过高',
|
||||
'temperature_low': '温度过低',
|
||||
'humidity_high': '湿度过高',
|
||||
'humidity_low': '湿度过低',
|
||||
'device_offline': '设备离线',
|
||||
'device_error': '设备故障',
|
||||
'sensor_error': '传感器故障',
|
||||
'power_failure': '电源故障',
|
||||
'network_error': '网络异常'
|
||||
const texts = {
|
||||
temperature_alert: '温度异常',
|
||||
humidity_alert: '湿度异常',
|
||||
feed_alert: '饲料异常',
|
||||
health_alert: '健康异常',
|
||||
device_alert: '设备异常',
|
||||
temperature: '温度异常',
|
||||
humidity: '湿度异常',
|
||||
device_failure: '设备故障',
|
||||
animal_health: '动物健康',
|
||||
security: '安全警报',
|
||||
maintenance: '维护提醒',
|
||||
other: '其他'
|
||||
}
|
||||
return typeMap[type] || type
|
||||
return texts[type] || type
|
||||
}
|
||||
|
||||
// 格式化时间显示
|
||||
|
||||
516
admin-system/frontend/src/views/OperationLogs.vue
Normal file
516
admin-system/frontend/src/views/OperationLogs.vue
Normal file
@@ -0,0 +1,516 @@
|
||||
<template>
|
||||
<div class="operation-logs-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h1>操作日志管理</h1>
|
||||
<p>查看和管理系统用户的操作记录</p>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选区域 -->
|
||||
<div class="search-section">
|
||||
<a-card title="搜索条件" class="search-card">
|
||||
<a-form
|
||||
:model="searchForm"
|
||||
layout="inline"
|
||||
@finish="handleSearch"
|
||||
>
|
||||
<a-form-item label="用户名">
|
||||
<a-input
|
||||
v-model:value="searchForm.username"
|
||||
placeholder="请输入用户名"
|
||||
allow-clear
|
||||
style="width: 200px"
|
||||
/>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-space>
|
||||
<a-button type="primary" html-type="submit" :loading="loading">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleReset">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
重置
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片区域 -->
|
||||
<div class="stats-section">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<a-statistic
|
||||
title="总操作数"
|
||||
:value="stats.total"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<a-statistic
|
||||
title="新增操作"
|
||||
:value="stats.CREATE || 0"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<a-statistic
|
||||
title="编辑操作"
|
||||
:value="stats.UPDATE || 0"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card class="stats-card">
|
||||
<a-statistic
|
||||
title="删除操作"
|
||||
:value="stats.DELETE || 0"
|
||||
:value-style="{ color: '#f5222d' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
|
||||
<!-- 操作日志表格 -->
|
||||
<div class="table-section">
|
||||
<a-card title="操作日志列表" class="table-card">
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="handleExport" :loading="exportLoading">
|
||||
<template #icon>
|
||||
<DownloadOutlined />
|
||||
</template>
|
||||
导出
|
||||
</a-button>
|
||||
<a-button @click="handleRefresh" :loading="loading">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="operationLogList"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<!-- 操作描述列 -->
|
||||
<template #operationDesc="{ record }">
|
||||
<a-tooltip :title="record.operation_desc" placement="topLeft">
|
||||
<span class="operation-desc">{{ record.operation_desc }}</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { api } from '@/utils/api'
|
||||
|
||||
export default {
|
||||
name: 'OperationLogs',
|
||||
components: {
|
||||
SearchOutlined,
|
||||
ReloadOutlined,
|
||||
DownloadOutlined
|
||||
},
|
||||
setup() {
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const exportLoading = ref(false)
|
||||
const operationLogList = ref([])
|
||||
const stats = ref({})
|
||||
|
||||
// 搜索表单
|
||||
const searchForm = reactive({
|
||||
username: ''
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条,共 ${total} 条`
|
||||
})
|
||||
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 60,
|
||||
align: 'center'
|
||||
},
|
||||
{
|
||||
title: '操作用户',
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
width: 120,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '操作类型',
|
||||
dataIndex: 'operation_type',
|
||||
key: 'operation_type',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
customCell: (record) => {
|
||||
const typeMap = {
|
||||
'CREATE': '新增',
|
||||
'UPDATE': '编辑',
|
||||
'DELETE': '删除',
|
||||
'READ': '查看',
|
||||
'LOGIN': '登录',
|
||||
'LOGOUT': '登出'
|
||||
}
|
||||
const colorMap = {
|
||||
'CREATE': 'green',
|
||||
'UPDATE': 'blue',
|
||||
'DELETE': 'red',
|
||||
'READ': 'default',
|
||||
'LOGIN': 'cyan',
|
||||
'LOGOUT': 'orange'
|
||||
}
|
||||
return {
|
||||
style: { color: colorMap[record.operation_type] || 'default' }
|
||||
}
|
||||
},
|
||||
customRender: ({ record }) => {
|
||||
const typeMap = {
|
||||
'CREATE': '新增',
|
||||
'UPDATE': '编辑',
|
||||
'DELETE': '删除',
|
||||
'READ': '查看',
|
||||
'LOGIN': '登录',
|
||||
'LOGOUT': '登出'
|
||||
}
|
||||
return typeMap[record.operation_type] || record.operation_type
|
||||
}
|
||||
},
|
||||
{
|
||||
title: '模块名称',
|
||||
dataIndex: 'module_name',
|
||||
key: 'module_name',
|
||||
width: 140,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '操作描述',
|
||||
dataIndex: 'operation_desc',
|
||||
key: 'operation_desc',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180,
|
||||
align: 'center'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const searchParams = computed(() => {
|
||||
return { ...searchForm }
|
||||
})
|
||||
|
||||
// 方法
|
||||
|
||||
|
||||
const loadOperationLogs = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
...searchParams.value
|
||||
}
|
||||
|
||||
const response = await api.operationLogs.getOperationLogs(params)
|
||||
if (response.success) {
|
||||
operationLogList.value = response.data
|
||||
pagination.total = response.pagination.total
|
||||
} else {
|
||||
message.error(response.message || '获取操作日志失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取操作日志失败:', error)
|
||||
message.error('获取操作日志失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const params = {
|
||||
type: 'overall',
|
||||
...searchParams.value
|
||||
}
|
||||
|
||||
const response = await api.operationLogs.getOperationStats(params)
|
||||
if (response.success) {
|
||||
stats.value = response.data
|
||||
stats.value.total = (stats.value.CREATE || 0) +
|
||||
(stats.value.UPDATE || 0) +
|
||||
(stats.value.DELETE || 0)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取统计数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadOperationLogs()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleReset = () => {
|
||||
searchForm.username = ''
|
||||
pagination.current = 1
|
||||
loadOperationLogs()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadOperationLogs()
|
||||
}
|
||||
|
||||
const handleRefresh = () => {
|
||||
loadOperationLogs()
|
||||
loadStats()
|
||||
}
|
||||
|
||||
const handleExport = async () => {
|
||||
try {
|
||||
exportLoading.value = true
|
||||
const params = { ...searchParams.value }
|
||||
|
||||
const response = await api.operationLogs.exportOperationLogs(params)
|
||||
|
||||
// 创建下载链接
|
||||
const blob = new Blob([response], { type: 'text/csv;charset=utf-8' })
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = `operation_logs_${new Date().getTime()}.csv`
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
|
||||
message.success('导出成功')
|
||||
} catch (error) {
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
loadOperationLogs()
|
||||
loadStats()
|
||||
})
|
||||
|
||||
return {
|
||||
loading,
|
||||
exportLoading,
|
||||
operationLogList,
|
||||
stats,
|
||||
searchForm,
|
||||
pagination,
|
||||
columns,
|
||||
handleSearch,
|
||||
handleReset,
|
||||
handleTableChange,
|
||||
handleRefresh,
|
||||
handleExport
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.operation-logs-container {
|
||||
padding: 24px;
|
||||
background-color: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h1 {
|
||||
margin: 0 0 8px 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #8c8c8c;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.search-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.search-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stats-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stats-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-section {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 表格布局优化 */
|
||||
:deep(.ant-table) {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
padding: 16px 12px;
|
||||
font-size: 14px;
|
||||
border-bottom: 2px solid #e8e8e8;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 16px 12px;
|
||||
word-break: break-word;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr:hover > td) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 操作描述列样式 */
|
||||
.operation-desc {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
/* 表格行样式优化 */
|
||||
:deep(.ant-table-tbody > tr) {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr:last-child) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* 响应式布局 */
|
||||
@media (max-width: 1400px) {
|
||||
:deep(.ant-table-thead > tr > th),
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 8px;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
:deep(.ant-table-thead > tr > th),
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 10px 6px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
:deep(.ant-table-thead > tr > th),
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 8px 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-content {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.data-content {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background-color: #f5f5f5;
|
||||
padding: 16px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.data-content pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
color: #262626;
|
||||
}
|
||||
</style>
|
||||
@@ -2,9 +2,35 @@
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>订单管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加订单
|
||||
<a-space>
|
||||
<a-button @click="exportOrders" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加订单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-auto-complete
|
||||
v-model:value="searchUsername"
|
||||
:options="usernameOptions"
|
||||
placeholder="请选择或输入用户名进行搜索"
|
||||
style="width: 300px;"
|
||||
:filter-option="false"
|
||||
@search="handleSearchInput"
|
||||
@press-enter="searchOrdersByUsername"
|
||||
/>
|
||||
<a-button type="primary" @click="searchOrdersByUsername" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -136,7 +162,7 @@
|
||||
<div v-if="viewOrderData">
|
||||
<a-descriptions :column="2" bordered>
|
||||
<a-descriptions-item label="订单ID">{{ viewOrderData.id }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户">{{ getUserName(viewOrderData.user_id) }}</a-descriptions-item>
|
||||
<a-descriptions-item label="用户">{{ viewOrderData.user?.username || `用户ID: ${viewOrderData.user_id}` }}</a-descriptions-item>
|
||||
<a-descriptions-item label="总金额">¥{{ viewOrderData.total_amount }}</a-descriptions-item>
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(viewOrderData.status)">
|
||||
@@ -174,13 +200,17 @@
|
||||
<script>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { PlusOutlined, DeleteOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
// 移除axios导入,使用统一的api工具
|
||||
import { api } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlusOutlined,
|
||||
DeleteOutlined
|
||||
DeleteOutlined,
|
||||
SearchOutlined,
|
||||
ExportOutlined
|
||||
},
|
||||
setup() {
|
||||
const orders = ref([])
|
||||
@@ -195,6 +225,15 @@ export default {
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
const viewOrderData = ref(null)
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchUsername = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const usernameOptions = ref([])
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
user_id: undefined,
|
||||
@@ -220,9 +259,9 @@ export default {
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
dataIndex: 'user_id',
|
||||
key: 'user_id',
|
||||
customRender: ({ record }) => getUserName(record.user_id)
|
||||
dataIndex: 'username',
|
||||
key: 'username',
|
||||
customRender: ({ record }) => record.user?.username || `用户ID: ${record.user_id}`
|
||||
},
|
||||
{
|
||||
title: '总金额',
|
||||
@@ -274,9 +313,9 @@ export default {
|
||||
const fetchOrders = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await axios.get('/api/orders')
|
||||
if (response.data.success) {
|
||||
orders.value = response.data.data || []
|
||||
const response = await api.get('/orders')
|
||||
if (response.success) {
|
||||
orders.value = response.data || []
|
||||
} else {
|
||||
message.error('获取订单失败')
|
||||
}
|
||||
@@ -292,9 +331,9 @@ export default {
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
usersLoading.value = true
|
||||
const response = await axios.get('/api/users')
|
||||
if (response.data.success) {
|
||||
users.value = response.data.data || []
|
||||
const response = await api.get('/users')
|
||||
if (response.success) {
|
||||
users.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取用户失败:', error)
|
||||
@@ -307,9 +346,9 @@ export default {
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
productsLoading.value = true
|
||||
const response = await axios.get('/api/products')
|
||||
if (response.data.success) {
|
||||
products.value = response.data.data || []
|
||||
const response = await api.get('/products')
|
||||
if (response.success) {
|
||||
products.value = response.data || []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取产品失败:', error)
|
||||
@@ -352,7 +391,7 @@ export default {
|
||||
// 删除订单
|
||||
const deleteOrder = async (id) => {
|
||||
try {
|
||||
await axios.delete(`/api/orders/${id}`)
|
||||
await api.delete(`/orders/${id}`)
|
||||
message.success('删除成功')
|
||||
fetchOrders()
|
||||
} catch (error) {
|
||||
@@ -375,10 +414,10 @@ export default {
|
||||
}
|
||||
|
||||
if (isEdit.value) {
|
||||
await axios.put(`/api/orders/${formData.id}`, orderData)
|
||||
await api.put(`/orders/${formData.id}`, orderData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await axios.post('/api/orders', orderData)
|
||||
await api.post('/orders', orderData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
@@ -487,8 +526,105 @@ export default {
|
||||
return product ? product.name : `产品ID: ${productId}`
|
||||
}
|
||||
|
||||
// 搜索订单
|
||||
const searchOrdersByUsername = async () => {
|
||||
if (!searchUsername.value.trim()) {
|
||||
message.warning('请输入用户名进行搜索')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const response = await api.get('/orders/search', {
|
||||
params: { username: searchUsername.value.trim() }
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
orders.value = response.data || []
|
||||
isSearching.value = true
|
||||
message.success(response.message || `找到 ${orders.value.length} 个匹配的订单`)
|
||||
} else {
|
||||
orders.value = []
|
||||
message.info('未找到匹配的订单')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索订单失败:', error)
|
||||
message.error('搜索订单失败')
|
||||
orders.value = []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入(为自动完成提供选项)
|
||||
const handleSearchInput = (value) => {
|
||||
if (!value) {
|
||||
usernameOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 从现有用户列表中筛选匹配的用户名
|
||||
const matchingUsers = users.value.filter(user =>
|
||||
user.username && user.username.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
|
||||
usernameOptions.value = matchingUsers.map(user => ({
|
||||
value: user.username,
|
||||
label: user.username
|
||||
}))
|
||||
}
|
||||
|
||||
// 更新用户名选项(在数据加载后)
|
||||
const updateUsernameOptions = () => {
|
||||
const uniqueUsernames = [...new Set(users.value.map(user => user.username).filter(Boolean))]
|
||||
usernameOptions.value = uniqueUsernames.map(username => ({
|
||||
value: username,
|
||||
label: username
|
||||
}))
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchUsername.value = ''
|
||||
isSearching.value = false
|
||||
usernameOptions.value = []
|
||||
fetchOrders() // 重新加载全部订单
|
||||
}
|
||||
|
||||
// 导出订单数据
|
||||
const exportOrders = async () => {
|
||||
try {
|
||||
if (!orders.value || orders.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportOrdersData(orders.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
fetchOrders()
|
||||
fetchUsers().then(() => {
|
||||
updateUsernameOptions()
|
||||
})
|
||||
})
|
||||
|
||||
return {
|
||||
@@ -508,7 +644,24 @@ export default {
|
||||
columns,
|
||||
orderItemColumns,
|
||||
viewOrderData,
|
||||
|
||||
// 搜索相关
|
||||
searchUsername,
|
||||
searchLoading,
|
||||
isSearching,
|
||||
usernameOptions,
|
||||
handleSearchInput,
|
||||
updateUsernameOptions,
|
||||
searchOrdersByUsername,
|
||||
resetSearch,
|
||||
|
||||
// 导出相关
|
||||
exportLoading,
|
||||
exportOrders,
|
||||
|
||||
fetchOrders,
|
||||
fetchUsers,
|
||||
fetchProducts,
|
||||
showAddModal,
|
||||
viewOrder,
|
||||
editOrder,
|
||||
|
||||
717
admin-system/frontend/src/views/PenManagement.vue
Normal file
717
admin-system/frontend/src/views/PenManagement.vue
Normal file
@@ -0,0 +1,717 @@
|
||||
<template>
|
||||
<div class="pen-management-container">
|
||||
<!-- 页面标题和操作栏 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">栏舍设置</h2>
|
||||
<div class="header-actions">
|
||||
<a-button @click="exportData" class="add-button">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal" class="add-button">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
新增栏舍
|
||||
</a-button>
|
||||
<div class="search-container">
|
||||
<a-input
|
||||
v-model="searchKeyword"
|
||||
placeholder="请输入栏舍名、类型、负责人等关键词"
|
||||
class="search-input"
|
||||
@pressEnter="handleSearch"
|
||||
@input="(e) => { console.log('搜索输入框输入:', e.target.value); searchKeyword = e.target.value; }"
|
||||
@change="(e) => { console.log('搜索输入框变化:', e.target.value); searchKeyword = e.target.value; }"
|
||||
allowClear
|
||||
/>
|
||||
<a-button type="primary" @click="handleSearch" class="search-button">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button @click="handleClearSearch" class="clear-button">
|
||||
清空
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-container">
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="filteredData"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
class="pen-table"
|
||||
>
|
||||
<!-- 状态列 -->
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status"
|
||||
@change="(checked) => { record.status = checked; handleStatusChange(record) }"
|
||||
:checked-children="'开启'"
|
||||
:un-checked-children="'关闭'"
|
||||
/>
|
||||
</template>
|
||||
<!-- 操作列 -->
|
||||
<template v-else-if="column.key === 'actions'">
|
||||
<a-space>
|
||||
<a-button type="link" @click="handleEdit(record)" class="action-btn">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button type="link" danger @click="handleDelete(record)" class="action-btn">
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 新增/编辑栏舍模态框 -->
|
||||
<a-modal
|
||||
:open="modalVisible"
|
||||
@update:open="(val) => modalVisible = val"
|
||||
:title="isEdit ? '编辑栏舍' : '新增栏舍'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="submitting"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
ref="formRef"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="栏舍名" name="name" required>
|
||||
<a-input
|
||||
v-model="formData.name"
|
||||
placeholder="请输入栏舍名"
|
||||
@input="(e) => { console.log('栏舍名输入:', e.target.value); formData.name = e.target.value; }"
|
||||
@change="(e) => { console.log('栏舍名变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="动物类型" name="animal_type" required>
|
||||
<a-select
|
||||
v-model="formData.animal_type"
|
||||
placeholder="请选择动物类型"
|
||||
@change="(value) => { console.log('动物类型变化:', value); }"
|
||||
>
|
||||
<a-select-option value="马">马</a-select-option>
|
||||
<a-select-option value="牛">牛</a-select-option>
|
||||
<a-select-option value="羊">羊</a-select-option>
|
||||
<a-select-option value="家禽">家禽</a-select-option>
|
||||
<a-select-option value="猪">猪</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="栏舍类型" name="pen_type">
|
||||
<a-input
|
||||
v-model="formData.pen_type"
|
||||
placeholder="请输入栏舍类型"
|
||||
@input="(e) => { console.log('栏舍类型输入:', e.target.value); formData.pen_type = e.target.value; }"
|
||||
@change="(e) => { console.log('栏舍类型变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="负责人" name="responsible" required>
|
||||
<a-input
|
||||
v-model="formData.responsible"
|
||||
placeholder="请输入负责人"
|
||||
@input="(e) => { console.log('负责人输入:', e.target.value); formData.responsible = e.target.value; }"
|
||||
@change="(e) => { console.log('负责人变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="容量" name="capacity" required>
|
||||
<a-input-number
|
||||
v-model="formData.capacity"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
placeholder="请输入容量"
|
||||
style="width: 100%"
|
||||
@change="(value) => { console.log('容量变化:', value); formData.capacity = value; }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch
|
||||
:checked="formData.status"
|
||||
@change="(checked) => { console.log('状态变化:', checked); formData.status = checked; }"
|
||||
:checked-children="'开启'"
|
||||
:un-checked-children="'关闭'"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="备注" name="description">
|
||||
<a-textarea
|
||||
v-model="formData.description"
|
||||
placeholder="请输入备注信息"
|
||||
:rows="3"
|
||||
@input="(e) => { console.log('备注输入:', e.target.value); formData.description = e.target.value; }"
|
||||
@change="(e) => { console.log('备注变化:', e.target.value); }"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, computed, onMounted } from 'vue'
|
||||
import { message, Modal } from 'ant-design-vue'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const submitting = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const searchKeyword = ref('')
|
||||
const formRef = ref(null)
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '栏舍名',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '动物类型',
|
||||
dataIndex: 'animal_type',
|
||||
key: 'animal_type',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '栏舍类型',
|
||||
dataIndex: 'pen_type',
|
||||
key: 'pen_type',
|
||||
width: 120,
|
||||
},
|
||||
{
|
||||
title: '负责人',
|
||||
dataIndex: 'responsible',
|
||||
key: 'responsible',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '容量',
|
||||
dataIndex: 'capacity',
|
||||
key: 'capacity',
|
||||
width: 80,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建人',
|
||||
dataIndex: 'creator',
|
||||
key: 'creator',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 120,
|
||||
fixed: 'right',
|
||||
},
|
||||
]
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total, range) => `第 ${range[0]}-${range[1]} 条/共 ${total} 条`,
|
||||
onChange: async (page, pageSize) => {
|
||||
pagination.current = page
|
||||
pagination.pageSize = pageSize
|
||||
await loadPenData()
|
||||
},
|
||||
onShowSizeChange: async (current, size) => {
|
||||
pagination.current = 1
|
||||
pagination.pageSize = size
|
||||
await loadPenData()
|
||||
}
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
animal_type: '',
|
||||
pen_type: '',
|
||||
responsible: '',
|
||||
capacity: 1,
|
||||
status: true,
|
||||
description: '',
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入栏舍名', trigger: 'blur' },
|
||||
{ min: 1, max: 50, message: '栏舍名长度应在1-50个字符之间', trigger: 'blur' }
|
||||
],
|
||||
animal_type: [
|
||||
{ required: true, message: '请选择动物类型', trigger: 'change' }
|
||||
],
|
||||
responsible: [
|
||||
{ required: true, message: '请输入负责人', trigger: 'blur' },
|
||||
{ min: 1, max: 20, message: '负责人姓名长度应在1-20个字符之间', trigger: 'blur' }
|
||||
],
|
||||
capacity: [
|
||||
{ required: true, message: '请输入容量', trigger: 'blur' },
|
||||
{ type: 'number', min: 1, max: 10000, message: '容量应在1-10000之间', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 真实数据
|
||||
const penData = ref([])
|
||||
|
||||
// 计算属性 - 过滤后的数据(暂时不使用前端过滤,使用API搜索)
|
||||
const filteredData = computed(() => {
|
||||
return penData.value
|
||||
})
|
||||
|
||||
// 数据加载方法
|
||||
const loadPenData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
console.log('开始加载栏舍数据...', {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
search: searchKeyword.value
|
||||
})
|
||||
|
||||
const requestParams = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
search: searchKeyword.value
|
||||
}
|
||||
|
||||
console.log('请求参数:', requestParams)
|
||||
console.log('搜索参数详情:', {
|
||||
search: searchKeyword.value,
|
||||
searchType: typeof searchKeyword.value,
|
||||
searchEmpty: !searchKeyword.value,
|
||||
searchTrimmed: searchKeyword.value?.trim()
|
||||
})
|
||||
|
||||
const response = await api.pens.getList(requestParams)
|
||||
|
||||
console.log('API响应:', response)
|
||||
|
||||
if (response && response.success) {
|
||||
penData.value = response.data.list
|
||||
pagination.total = response.data.pagination.total
|
||||
console.log('栏舍数据加载成功:', penData.value)
|
||||
} else {
|
||||
console.error('API返回失败:', response)
|
||||
message.error('获取栏舍数据失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载栏舍数据失败:', error)
|
||||
message.error('加载栏舍数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 方法
|
||||
const showAddModal = () => {
|
||||
isEdit.value = false
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
const handleEdit = (record) => {
|
||||
console.log('=== 开始编辑栏舍 ===')
|
||||
console.log('点击编辑按钮,原始记录数据:', record)
|
||||
|
||||
isEdit.value = true
|
||||
Object.assign(formData, {
|
||||
id: record.id,
|
||||
name: record.name,
|
||||
animal_type: record.animal_type,
|
||||
pen_type: record.pen_type,
|
||||
responsible: record.responsible,
|
||||
capacity: record.capacity,
|
||||
status: record.status,
|
||||
description: record.description
|
||||
})
|
||||
|
||||
console.log('编辑模式:表单数据已填充')
|
||||
console.log('formData对象:', formData)
|
||||
console.log('formData.name:', formData.name)
|
||||
console.log('formData.animal_type:', formData.animal_type)
|
||||
console.log('formData.pen_type:', formData.pen_type)
|
||||
console.log('formData.responsible:', formData.responsible)
|
||||
console.log('formData.capacity:', formData.capacity)
|
||||
console.log('formData.status:', formData.status)
|
||||
console.log('formData.description:', formData.description)
|
||||
|
||||
modalVisible.value = true
|
||||
console.log('编辑模态框已打开')
|
||||
}
|
||||
|
||||
const handleDelete = (record) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: `确定要删除栏舍"${record.name}"吗?`,
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await api.pens.delete(record.id)
|
||||
if (response.success) {
|
||||
message.success('删除成功')
|
||||
await loadPenData() // 重新加载数据
|
||||
} else {
|
||||
message.error('删除失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除栏舍失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleStatusChange = async (record) => {
|
||||
try {
|
||||
const response = await api.pens.update(record.id, { status: record.status })
|
||||
if (response.success) {
|
||||
message.success(`栏舍"${record.name}"状态已${record.status ? '开启' : '关闭'}`)
|
||||
} else {
|
||||
message.error('状态更新失败')
|
||||
// 恢复原状态
|
||||
record.status = !record.status
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新状态失败:', error)
|
||||
message.error('状态更新失败')
|
||||
// 恢复原状态
|
||||
record.status = !record.status
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = async () => {
|
||||
console.log('=== 开始搜索栏舍 ===')
|
||||
console.log('搜索关键词:', searchKeyword.value)
|
||||
console.log('搜索关键词类型:', typeof searchKeyword.value)
|
||||
console.log('搜索关键词长度:', searchKeyword.value?.length)
|
||||
console.log('搜索关键词是否为空:', !searchKeyword.value)
|
||||
console.log('搜索关键词去除空格后:', searchKeyword.value?.trim())
|
||||
|
||||
// 确保搜索关键词正确传递
|
||||
const searchValue = searchKeyword.value?.trim() || ''
|
||||
console.log('实际使用的搜索值:', searchValue)
|
||||
|
||||
pagination.current = 1 // 重置到第一页
|
||||
await loadPenData()
|
||||
|
||||
console.log('=== 搜索完成 ===')
|
||||
}
|
||||
|
||||
const handleClearSearch = async () => {
|
||||
console.log('=== 清空搜索 ===')
|
||||
searchKeyword.value = ''
|
||||
pagination.current = 1 // 重置到第一页
|
||||
await loadPenData()
|
||||
console.log('=== 搜索已清空 ===')
|
||||
}
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
console.log('=== 开始提交栏舍数据 ===')
|
||||
console.log('当前表单数据:', formData)
|
||||
console.log('是否为编辑模式:', isEdit.value)
|
||||
|
||||
await formRef.value.validate()
|
||||
submitting.value = true
|
||||
|
||||
const submitData = {
|
||||
name: formData.name,
|
||||
animal_type: formData.animal_type,
|
||||
pen_type: formData.pen_type,
|
||||
responsible: formData.responsible,
|
||||
capacity: formData.capacity,
|
||||
status: formData.status,
|
||||
description: formData.description
|
||||
}
|
||||
|
||||
console.log('准备提交的数据:', submitData)
|
||||
console.log('提交的字段详情:')
|
||||
console.log('- 栏舍名 (name):', submitData.name)
|
||||
console.log('- 动物类型 (animal_type):', submitData.animal_type)
|
||||
console.log('- 栏舍类型 (pen_type):', submitData.pen_type)
|
||||
console.log('- 负责人 (responsible):', submitData.responsible)
|
||||
console.log('- 容量 (capacity):', submitData.capacity, typeof submitData.capacity)
|
||||
console.log('- 状态 (status):', submitData.status, typeof submitData.status)
|
||||
console.log('- 描述 (description):', submitData.description)
|
||||
|
||||
let response
|
||||
if (isEdit.value) {
|
||||
// 编辑
|
||||
console.log('执行编辑操作,栏舍ID:', formData.id)
|
||||
response = await api.pens.update(formData.id, submitData)
|
||||
console.log('编辑API响应:', response)
|
||||
} else {
|
||||
// 新增
|
||||
console.log('执行新增操作')
|
||||
response = await api.pens.create(submitData)
|
||||
console.log('新增API响应:', response)
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
console.log('✅ 操作成功:', response.message)
|
||||
console.log('返回的数据:', response.data)
|
||||
message.success(isEdit.value ? '编辑成功' : '新增成功')
|
||||
modalVisible.value = false
|
||||
await loadPenData() // 重新加载数据
|
||||
console.log('数据已重新加载')
|
||||
} else {
|
||||
console.log('❌ 操作失败:', response.message)
|
||||
message.error(isEdit.value ? '编辑失败' : '新增失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ 提交失败:', error)
|
||||
console.error('错误详情:', error.response?.data || error.message)
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
submitting.value = false
|
||||
console.log('=== 提交操作完成 ===')
|
||||
}
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
name: '',
|
||||
animal_type: '',
|
||||
pen_type: '',
|
||||
responsible: '',
|
||||
capacity: 1,
|
||||
status: true,
|
||||
description: '',
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 生命周期
|
||||
onMounted(async () => {
|
||||
console.log('PenManagement组件已挂载')
|
||||
// 初始化数据
|
||||
await loadPenData()
|
||||
})
|
||||
|
||||
// 导出数据
|
||||
const exportData = async () => {
|
||||
try {
|
||||
if (!penData.value || penData.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportPenData(penData.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pen-management-container {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-button {
|
||||
height: 40px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 300px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.clear-button {
|
||||
height: 40px;
|
||||
color: #666;
|
||||
border-color: #d9d9d9;
|
||||
}
|
||||
|
||||
.clear-button:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pen-table {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
padding: 4px 8px;
|
||||
height: auto;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.page-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
font-weight: 600;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr:hover > td) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
:deep(.ant-table-tbody > tr > td) {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 状态开关样式 */
|
||||
:deep(.ant-switch-checked) {
|
||||
background-color: #1890ff;
|
||||
}
|
||||
|
||||
/* 表单样式 */
|
||||
:deep(.ant-form-item-label > label) {
|
||||
font-weight: 500;
|
||||
color: #262626;
|
||||
}
|
||||
|
||||
:deep(.ant-input),
|
||||
:deep(.ant-select),
|
||||
:deep(.ant-input-number) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
:deep(.ant-btn) {
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
@@ -2,9 +2,35 @@
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>产品管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加产品
|
||||
<a-space>
|
||||
<a-button @click="exportProducts" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加产品
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-auto-complete
|
||||
v-model:value="searchProductName"
|
||||
:options="productNameOptions"
|
||||
placeholder="请选择或输入产品名称进行搜索"
|
||||
style="width: 300px;"
|
||||
:filter-option="false"
|
||||
@search="handleSearchInput"
|
||||
@press-enter="searchProducts"
|
||||
/>
|
||||
<a-button type="primary" @click="searchProducts" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@@ -89,8 +115,9 @@
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
// 响应式数据
|
||||
const products = ref([])
|
||||
@@ -100,6 +127,15 @@ const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchProductName = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const productNameOptions = ref([])
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
@@ -167,9 +203,9 @@ const columns = [
|
||||
const fetchProducts = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const response = await axios.get('/api/products')
|
||||
if (response.data.success) {
|
||||
products.value = response.data.data
|
||||
const response = await api.get('/products')
|
||||
if (response.success) {
|
||||
products.value = response.data
|
||||
} else {
|
||||
message.error('获取产品列表失败')
|
||||
}
|
||||
@@ -198,13 +234,8 @@ const editProduct = (record) => {
|
||||
// 删除产品
|
||||
const deleteProduct = async (id) => {
|
||||
try {
|
||||
const token = localStorage.getItem('token')
|
||||
const response = await axios.delete(`/api/products/${id}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
})
|
||||
if (response.data.success) {
|
||||
const response = await api.delete(`/products/${id}`)
|
||||
if (response.success) {
|
||||
message.success('删除成功')
|
||||
fetchProducts()
|
||||
} else {
|
||||
@@ -222,21 +253,14 @@ const handleSubmit = async () => {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
const token = localStorage.getItem('token')
|
||||
const config = {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
let response
|
||||
if (isEdit.value) {
|
||||
response = await axios.put(`/api/products/${formData.id}`, formData, config)
|
||||
response = await api.put(`/products/${formData.id}`, formData)
|
||||
} else {
|
||||
response = await axios.post('/api/products', formData, config)
|
||||
response = await api.post('/products', formData)
|
||||
}
|
||||
|
||||
if (response.data.success) {
|
||||
if (response.success) {
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
modalVisible.value = false
|
||||
fetchProducts()
|
||||
@@ -270,8 +294,105 @@ const resetForm = () => {
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 搜索产品
|
||||
const searchProducts = async () => {
|
||||
if (!searchProductName.value.trim()) {
|
||||
message.warning('请输入产品名称进行搜索')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
searchLoading.value = true
|
||||
const response = await api.get('/products/search', {
|
||||
params: { name: searchProductName.value.trim() }
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
products.value = response.data || []
|
||||
isSearching.value = true
|
||||
message.success(response.message || `找到 ${products.value.length} 个匹配的产品`)
|
||||
} else {
|
||||
products.value = []
|
||||
message.info('未找到匹配的产品')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('搜索产品失败:', error)
|
||||
message.error('搜索产品失败')
|
||||
products.value = []
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理搜索输入(为自动完成提供选项)
|
||||
const handleSearchInput = (value) => {
|
||||
if (!value) {
|
||||
productNameOptions.value = []
|
||||
return
|
||||
}
|
||||
|
||||
// 从现有产品列表中筛选匹配的产品名称
|
||||
const matchingProducts = products.value.filter(product =>
|
||||
product.name.toLowerCase().includes(value.toLowerCase())
|
||||
)
|
||||
|
||||
productNameOptions.value = matchingProducts.map(product => ({
|
||||
value: product.name,
|
||||
label: product.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 更新产品名称选项(在数据加载后)
|
||||
const updateProductNameOptions = () => {
|
||||
productNameOptions.value = products.value.map(product => ({
|
||||
value: product.name,
|
||||
label: product.name
|
||||
}))
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchProductName.value = ''
|
||||
isSearching.value = false
|
||||
productNameOptions.value = []
|
||||
fetchProducts() // 重新加载全部产品
|
||||
}
|
||||
|
||||
// 导出产品数据
|
||||
const exportProducts = async () => {
|
||||
try {
|
||||
if (!products.value || products.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportProductsData(products.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchProducts()
|
||||
fetchProducts().then(() => {
|
||||
updateProductNameOptions()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
485
admin-system/frontend/src/views/Reports.vue
Normal file
485
admin-system/frontend/src/views/Reports.vue
Normal file
@@ -0,0 +1,485 @@
|
||||
<template>
|
||||
<div class="reports-page">
|
||||
<a-page-header
|
||||
title="报表管理"
|
||||
sub-title="生成和管理系统报表"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="fetchReportList">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新列表
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showGenerateModal = true">
|
||||
<template #icon><FilePdfOutlined /></template>
|
||||
生成报表
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<div class="reports-content">
|
||||
<!-- 快捷导出区域 -->
|
||||
<a-card title="快捷数据导出" :bordered="false" style="margin-bottom: 24px;">
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="exportLoading.farms"
|
||||
@click="quickExport('farms', 'excel')"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出农场数据
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="exportLoading.devices"
|
||||
@click="quickExport('devices', 'excel')"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出设备数据
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="exportLoading.animals"
|
||||
@click="quickExport('animals', 'excel')"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出动物数据
|
||||
</a-button>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-button
|
||||
type="primary"
|
||||
block
|
||||
:loading="exportLoading.alerts"
|
||||
@click="quickExport('alerts', 'excel')"
|
||||
>
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出预警数据
|
||||
</a-button>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
|
||||
<!-- 报表文件列表 -->
|
||||
<a-card title="历史报表文件" :bordered="false">
|
||||
<a-table
|
||||
:columns="reportColumns"
|
||||
:data-source="reportFiles"
|
||||
:loading="loading"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
row-key="fileName"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'size'">
|
||||
{{ formatFileSize(record.size) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'createdAt'">
|
||||
{{ formatDate(record.createdAt) }}
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="downloadReport(record)"
|
||||
>
|
||||
<template #icon><DownloadOutlined /></template>
|
||||
下载
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
size="small"
|
||||
@click="deleteReport(record)"
|
||||
v-if="userStore.userData?.roles?.includes('admin')"
|
||||
>
|
||||
<template #icon><DeleteOutlined /></template>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
</div>
|
||||
|
||||
<!-- 生成报表模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showGenerateModal"
|
||||
title="生成报表"
|
||||
:confirm-loading="generateLoading"
|
||||
@ok="generateReport"
|
||||
@cancel="resetGenerateForm"
|
||||
width="600px"
|
||||
>
|
||||
<a-form
|
||||
ref="generateFormRef"
|
||||
:model="generateForm"
|
||||
:rules="generateRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="报表类型" name="reportType">
|
||||
<a-select v-model:value="generateForm.reportType" @change="onReportTypeChange">
|
||||
<a-select-option value="farm">养殖统计报表</a-select-option>
|
||||
<a-select-option value="sales">销售分析报表</a-select-option>
|
||||
<a-select-option value="compliance" v-if="userStore.userData?.roles?.includes('admin')">
|
||||
监管合规报表
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item label="开始日期" name="startDate">
|
||||
<a-date-picker
|
||||
v-model:value="generateForm.startDate"
|
||||
style="width: 100%;"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item label="结束日期" name="endDate">
|
||||
<a-date-picker
|
||||
v-model:value="generateForm.endDate"
|
||||
style="width: 100%;"
|
||||
format="YYYY-MM-DD"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="报表格式" name="format">
|
||||
<a-radio-group v-model:value="generateForm.format">
|
||||
<a-radio-button value="pdf">PDF</a-radio-button>
|
||||
<a-radio-button value="excel">Excel</a-radio-button>
|
||||
<a-radio-button value="csv" v-if="generateForm.reportType === 'farm'">CSV</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item
|
||||
label="选择农场"
|
||||
name="farmIds"
|
||||
v-if="generateForm.reportType === 'farm'"
|
||||
>
|
||||
<a-select
|
||||
v-model:value="generateForm.farmIds"
|
||||
mode="multiple"
|
||||
placeholder="不选择则包含所有农场"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<a-select-option
|
||||
v-for="farm in dataStore.farms"
|
||||
:key="farm.id"
|
||||
:value="farm.id"
|
||||
>
|
||||
{{ farm.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
FilePdfOutlined,
|
||||
ExportOutlined,
|
||||
DownloadOutlined,
|
||||
DeleteOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import { useUserStore } from '../stores/user'
|
||||
import { useDataStore } from '../stores/data'
|
||||
import moment from 'moment'
|
||||
|
||||
// Store
|
||||
const userStore = useUserStore()
|
||||
const dataStore = useDataStore()
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const generateLoading = ref(false)
|
||||
const showGenerateModal = ref(false)
|
||||
const reportFiles = ref([])
|
||||
|
||||
// 导出加载状态
|
||||
const exportLoading = reactive({
|
||||
farms: false,
|
||||
devices: false,
|
||||
animals: false,
|
||||
alerts: false
|
||||
})
|
||||
|
||||
// 生成报表表单
|
||||
const generateFormRef = ref()
|
||||
const generateForm = reactive({
|
||||
reportType: 'farm',
|
||||
startDate: moment().subtract(30, 'days'),
|
||||
endDate: moment(),
|
||||
format: 'pdf',
|
||||
farmIds: []
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const generateRules = {
|
||||
reportType: [{ required: true, message: '请选择报表类型' }],
|
||||
startDate: [{ required: true, message: '请选择开始日期' }],
|
||||
endDate: [{ required: true, message: '请选择结束日期' }],
|
||||
format: [{ required: true, message: '请选择报表格式' }]
|
||||
}
|
||||
|
||||
// 报表文件列表表格列定义
|
||||
const reportColumns = [
|
||||
{
|
||||
title: '文件名',
|
||||
dataIndex: 'fileName',
|
||||
key: 'fileName',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '文件大小',
|
||||
dataIndex: 'size',
|
||||
key: 'size',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 150
|
||||
}
|
||||
]
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(async () => {
|
||||
await dataStore.fetchAllData()
|
||||
await fetchReportList()
|
||||
})
|
||||
|
||||
// 获取报表文件列表
|
||||
async function fetchReportList() {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await api.get('/reports/list')
|
||||
if (response.success) {
|
||||
reportFiles.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取报表列表失败:', error)
|
||||
message.error('获取报表列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成报表
|
||||
async function generateReport() {
|
||||
try {
|
||||
await generateFormRef.value.validate()
|
||||
generateLoading.value = true
|
||||
|
||||
const params = {
|
||||
startDate: generateForm.startDate.format('YYYY-MM-DD'),
|
||||
endDate: generateForm.endDate.format('YYYY-MM-DD'),
|
||||
format: generateForm.format
|
||||
}
|
||||
|
||||
if (generateForm.reportType === 'farm' && generateForm.farmIds.length > 0) {
|
||||
params.farmIds = generateForm.farmIds
|
||||
}
|
||||
|
||||
let endpoint = ''
|
||||
if (generateForm.reportType === 'farm') {
|
||||
endpoint = '/reports/farm'
|
||||
} else if (generateForm.reportType === 'sales') {
|
||||
endpoint = '/reports/sales'
|
||||
} else if (generateForm.reportType === 'compliance') {
|
||||
endpoint = '/reports/compliance'
|
||||
}
|
||||
|
||||
const response = await api.post(endpoint, params)
|
||||
|
||||
if (response.success) {
|
||||
message.success('报表生成成功')
|
||||
showGenerateModal.value = false
|
||||
resetGenerateForm()
|
||||
await fetchReportList()
|
||||
|
||||
// 自动下载生成的报表
|
||||
if (response.data.downloadUrl) {
|
||||
window.open(`${api.baseURL}${response.data.downloadUrl}`, '_blank')
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('生成报表失败:', error)
|
||||
message.error('生成报表失败')
|
||||
} finally {
|
||||
generateLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置生成表单
|
||||
function resetGenerateForm() {
|
||||
generateForm.reportType = 'farm'
|
||||
generateForm.startDate = moment().subtract(30, 'days')
|
||||
generateForm.endDate = moment()
|
||||
generateForm.format = 'pdf'
|
||||
generateForm.farmIds = []
|
||||
}
|
||||
|
||||
// 报表类型改变时的处理
|
||||
function onReportTypeChange(value) {
|
||||
if (value !== 'farm') {
|
||||
generateForm.farmIds = []
|
||||
}
|
||||
}
|
||||
|
||||
// 快捷导出
|
||||
async function quickExport(dataType, format) {
|
||||
exportLoading[dataType] = true
|
||||
try {
|
||||
let endpoint = ''
|
||||
let fileName = ''
|
||||
|
||||
switch (dataType) {
|
||||
case 'farms':
|
||||
endpoint = '/reports/export/farms'
|
||||
fileName = `农场数据_${moment().format('YYYYMMDD_HHmmss')}.xlsx`
|
||||
break
|
||||
case 'devices':
|
||||
endpoint = '/reports/export/devices'
|
||||
fileName = `设备数据_${moment().format('YYYYMMDD_HHmmss')}.xlsx`
|
||||
break
|
||||
case 'animals':
|
||||
// 使用农场报表API导出动物数据
|
||||
endpoint = '/reports/farm'
|
||||
fileName = `动物数据_${moment().format('YYYYMMDD_HHmmss')}.xlsx`
|
||||
break
|
||||
case 'alerts':
|
||||
// 使用农场报表API导出预警数据
|
||||
endpoint = '/reports/farm'
|
||||
fileName = `预警数据_${moment().format('YYYYMMDD_HHmmss')}.xlsx`
|
||||
break
|
||||
}
|
||||
|
||||
if (dataType === 'animals' || dataType === 'alerts') {
|
||||
// 生成包含动物或预警数据的报表
|
||||
const response = await api.post(endpoint, { format: 'excel' })
|
||||
if (response.success && response.data.downloadUrl) {
|
||||
downloadFile(`${api.baseURL}${response.data.downloadUrl}`, response.data.fileName)
|
||||
message.success('数据导出成功')
|
||||
}
|
||||
} else {
|
||||
// 直接导出
|
||||
const url = `${api.baseURL}${endpoint}?format=${format}`
|
||||
downloadFile(url, fileName)
|
||||
message.success('数据导出成功')
|
||||
}
|
||||
|
||||
await fetchReportList()
|
||||
} catch (error) {
|
||||
console.error(`导出${dataType}数据失败:`, error)
|
||||
message.error('数据导出失败')
|
||||
} finally {
|
||||
exportLoading[dataType] = false
|
||||
}
|
||||
}
|
||||
|
||||
// 下载报表文件
|
||||
function downloadReport(record) {
|
||||
const url = `${api.baseURL}${record.downloadUrl}`
|
||||
downloadFile(url, record.fileName)
|
||||
}
|
||||
|
||||
// 下载文件辅助函数
|
||||
function downloadFile(url, fileName) {
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = fileName
|
||||
link.target = '_blank'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
// 删除报表文件
|
||||
async function deleteReport(record) {
|
||||
try {
|
||||
// 注意:这里需要在后端添加删除API
|
||||
message.success('删除功能将在后续版本实现')
|
||||
} catch (error) {
|
||||
console.error('删除报表失败:', error)
|
||||
message.error('删除报表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(date) {
|
||||
return moment(date).format('YYYY-MM-DD HH:mm:ss')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.reports-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.reports-content {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ant-page-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-btn-group) .ant-btn {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-col {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
660
admin-system/frontend/src/views/RolePermissions.vue
Normal file
660
admin-system/frontend/src/views/RolePermissions.vue
Normal file
@@ -0,0 +1,660 @@
|
||||
<template>
|
||||
<div class="role-permissions">
|
||||
<div class="page-header">
|
||||
<h1>角色权限管理</h1>
|
||||
<a-button type="primary" @click="showCreateModal">
|
||||
<PlusOutlined />
|
||||
新增角色
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 角色列表 -->
|
||||
<a-card title="角色列表" :bordered="false">
|
||||
<template #extra>
|
||||
<a-input-search
|
||||
v-model="searchText"
|
||||
placeholder="搜索角色名称"
|
||||
style="width: 200px"
|
||||
@search="handleSearch"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="roles"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'status'">
|
||||
<a-switch
|
||||
:checked="record.status"
|
||||
@change="(checked) => handleStatusChange(record, checked)"
|
||||
:checked-children="'启用'"
|
||||
:un-checked-children="'禁用'"
|
||||
:loading="record.statusChanging"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<template v-if="column.key === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="editRole(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-dropdown>
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="menu" @click="manageMenuPermissions(record)">
|
||||
菜单权限
|
||||
</a-menu-item>
|
||||
<a-menu-item key="function" @click="manageFunctionPermissions(record)">
|
||||
功能权限
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button type="link" size="small">
|
||||
权限管理 <DownOutlined />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个角色吗?"
|
||||
@confirm="deleteRole(record.id)"
|
||||
>
|
||||
<a-button type="link" size="small" danger>
|
||||
删除
|
||||
</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 创建/编辑角色模态框 -->
|
||||
<a-modal
|
||||
:open="modalVisible"
|
||||
:title="isEdit ? '编辑角色' : '新增角色'"
|
||||
@ok="handleModalOk"
|
||||
@cancel="handleModalCancel"
|
||||
@update:open="modalVisible = $event"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
<a-form-item label="角色名称" name="name">
|
||||
<a-input :value="formData.name" placeholder="请输入角色名称" @update:value="formData.name = $event" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="角色描述" name="description">
|
||||
<a-textarea :value="formData.description" placeholder="请输入角色描述" @update:value="formData.description = $event" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="状态" name="status">
|
||||
<a-switch :checked="formData.status" @update:checked="formData.status = $event" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 菜单权限管理模态框 -->
|
||||
<a-modal
|
||||
:open="menuPermissionModalVisible"
|
||||
title="菜单权限管理"
|
||||
width="800px"
|
||||
@ok="handleMenuPermissionOk"
|
||||
@cancel="handleMenuPermissionCancel"
|
||||
@update:open="menuPermissionModalVisible = $event"
|
||||
>
|
||||
<div class="permission-container">
|
||||
<div class="permission-header">
|
||||
<h3>角色:{{ currentRole?.name }}</h3>
|
||||
<a-space>
|
||||
<a-button @click="checkAllMenus">全选</a-button>
|
||||
<a-button @click="uncheckAllMenus">全不选</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-tree
|
||||
:checkedKeys="menuCheckedKeys"
|
||||
:tree-data="menuTree"
|
||||
:field-names="{ children: 'children', title: 'name', key: 'id' }"
|
||||
checkable
|
||||
:check-strictly="false"
|
||||
@check="handleMenuTreeCheck"
|
||||
@update:checkedKeys="menuCheckedKeys = $event"
|
||||
/>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 功能权限管理模态框 -->
|
||||
<a-modal
|
||||
:open="functionPermissionModalVisible"
|
||||
title="功能权限管理"
|
||||
width="1000px"
|
||||
@ok="handleFunctionPermissionOk"
|
||||
@cancel="handleFunctionPermissionCancel"
|
||||
@update:open="functionPermissionModalVisible = $event"
|
||||
>
|
||||
<div class="permission-container">
|
||||
<div class="permission-header">
|
||||
<h3>角色:{{ currentRole?.name }}</h3>
|
||||
<a-space>
|
||||
<a-select
|
||||
:value="selectedModule"
|
||||
placeholder="选择模块"
|
||||
style="width: 150px"
|
||||
@update:value="selectedModule = $event"
|
||||
@change="filterPermissions"
|
||||
>
|
||||
<a-select-option value="">全部模块</a-select-option>
|
||||
<a-select-option v-for="module in permissionModules" :key="module" :value="module">
|
||||
{{ module }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-button @click="checkAllFunctions">全选</a-button>
|
||||
<a-button @click="uncheckAllFunctions">全不选</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<div class="function-permissions">
|
||||
<a-collapse v-model="activeModules">
|
||||
<a-collapse-panel v-for="(permissions, module) in filteredPermissions" :key="module" :header="module">
|
||||
<a-checkbox-group
|
||||
:value="functionCheckedKeys"
|
||||
@update:value="functionCheckedKeys = $event"
|
||||
@change="handleFunctionCheck"
|
||||
>
|
||||
<a-row :gutter="[16, 8]">
|
||||
<a-col v-for="permission in permissions" :key="permission.id" :span="8">
|
||||
<a-checkbox :value="permission.id">
|
||||
{{ permission.permission_name }}
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-checkbox-group>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { PlusOutlined, DownOutlined } from '@ant-design/icons-vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { rolePermissionService } from '../utils/dataService'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const searchText = ref('')
|
||||
const roles = ref([])
|
||||
const menuTree = ref([])
|
||||
const menuCheckedKeys = ref([])
|
||||
const functionCheckedKeys = ref([])
|
||||
const allPermissions = ref([])
|
||||
const permissionModules = ref([])
|
||||
const selectedModule = ref('')
|
||||
const activeModules = ref([])
|
||||
|
||||
// 分页
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条记录`
|
||||
})
|
||||
|
||||
// 模态框状态
|
||||
const modalVisible = ref(false)
|
||||
const menuPermissionModalVisible = ref(false)
|
||||
const functionPermissionModalVisible = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const currentRole = ref(null)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
status: true
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const formRules = {
|
||||
name: [
|
||||
{ required: true, message: '请输入角色名称', trigger: 'blur' }
|
||||
]
|
||||
}
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'id',
|
||||
key: 'id',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '角色名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name'
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description'
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
width: 100
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'created_at',
|
||||
key: 'created_at',
|
||||
width: 180
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 计算属性
|
||||
const filteredPermissions = computed(() => {
|
||||
if (!selectedModule.value) {
|
||||
return allPermissions.value.reduce((acc, permission) => {
|
||||
const module = permission.module;
|
||||
if (!acc[module]) {
|
||||
acc[module] = [];
|
||||
}
|
||||
acc[module].push(permission);
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
|
||||
return allPermissions.value
|
||||
.filter(permission => permission.module === selectedModule.value)
|
||||
.reduce((acc, permission) => {
|
||||
const module = permission.module;
|
||||
if (!acc[module]) {
|
||||
acc[module] = [];
|
||||
}
|
||||
acc[module].push(permission);
|
||||
return acc;
|
||||
}, {});
|
||||
});
|
||||
|
||||
// 加载角色列表
|
||||
const loadRoles = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
page: pagination.current,
|
||||
pageSize: pagination.pageSize,
|
||||
search: searchText.value
|
||||
}
|
||||
|
||||
const response = await rolePermissionService.getRoles(params)
|
||||
roles.value = response.list || []
|
||||
pagination.total = response.pagination?.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载角色列表失败:', error)
|
||||
message.error('加载角色列表失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 加载菜单树
|
||||
const loadMenuTree = async () => {
|
||||
try {
|
||||
const response = await rolePermissionService.getPermissionTree()
|
||||
|
||||
// 转换字段名以匹配前端组件期望的格式
|
||||
const convertMenuFields = (menu) => {
|
||||
return {
|
||||
id: menu.id,
|
||||
name: menu.menu_name || menu.name, // 映射 menu_name 到 name
|
||||
key: menu.id,
|
||||
children: menu.children ? menu.children.map(convertMenuFields) : []
|
||||
}
|
||||
}
|
||||
|
||||
menuTree.value = response ? response.map(convertMenuFields) : []
|
||||
} catch (error) {
|
||||
console.error('加载菜单树失败:', error)
|
||||
message.error('加载菜单树失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载所有权限
|
||||
const loadAllPermissions = async () => {
|
||||
try {
|
||||
const response = await rolePermissionService.getAllPermissions()
|
||||
allPermissions.value = response.data?.permissions || []
|
||||
} catch (error) {
|
||||
console.error('加载权限列表失败:', error)
|
||||
message.error('加载权限列表失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 加载权限模块列表
|
||||
const loadPermissionModules = async () => {
|
||||
try {
|
||||
const response = await rolePermissionService.getPermissionModules()
|
||||
permissionModules.value = response.data || []
|
||||
// 默认展开所有模块
|
||||
activeModules.value = permissionModules.value
|
||||
} catch (error) {
|
||||
console.error('加载权限模块失败:', error)
|
||||
message.error('加载权限模块失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 搜索
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
loadRoles()
|
||||
}
|
||||
|
||||
// 表格变化
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
loadRoles()
|
||||
}
|
||||
|
||||
// 显示创建模态框
|
||||
const showCreateModal = () => {
|
||||
isEdit.value = false
|
||||
modalVisible.value = true
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 编辑角色
|
||||
const editRole = (record) => {
|
||||
isEdit.value = true
|
||||
modalVisible.value = true
|
||||
Object.assign(formData, record)
|
||||
}
|
||||
|
||||
// 管理菜单权限
|
||||
const manageMenuPermissions = async (record) => {
|
||||
currentRole.value = record
|
||||
menuPermissionModalVisible.value = true
|
||||
|
||||
try {
|
||||
// 加载角色的菜单权限
|
||||
console.log('🔍 [菜单权限加载] 角色ID:', record.id)
|
||||
console.log('🔍 [菜单权限加载] 调用API: /role-permissions/public/roles/' + record.id + '/menus')
|
||||
|
||||
const response = await rolePermissionService.getRoleMenuPermissions(record.id)
|
||||
console.log('🔍 [菜单权限加载] API响应:', response)
|
||||
console.log('🔍 [菜单权限加载] 权限数组:', response.data?.permissions)
|
||||
console.log('🔍 [菜单权限加载] 权限数组长度:', response.data?.permissions?.length)
|
||||
|
||||
const permissionIds = response.data?.permissions?.map(p => p.id) || []
|
||||
console.log('🔍 [菜单权限加载] 权限ID数组:', permissionIds)
|
||||
console.log('🔍 [菜单权限加载] 权限ID数组长度:', permissionIds.length)
|
||||
|
||||
menuCheckedKeys.value = permissionIds
|
||||
console.log('🔍 [菜单权限加载] 设置后的menuCheckedKeys:', menuCheckedKeys.value)
|
||||
} catch (error) {
|
||||
console.error('加载角色菜单权限失败:', error)
|
||||
message.error('加载角色菜单权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 管理功能权限
|
||||
const manageFunctionPermissions = async (record) => {
|
||||
currentRole.value = record
|
||||
functionPermissionModalVisible.value = true
|
||||
|
||||
try {
|
||||
// 加载角色的功能权限
|
||||
const response = await rolePermissionService.getRoleFunctionPermissions(record.id)
|
||||
functionCheckedKeys.value = response.data?.permissions?.map(p => p.id) || []
|
||||
} catch (error) {
|
||||
console.error('加载角色功能权限失败:', error)
|
||||
message.error('加载角色功能权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 删除角色
|
||||
const deleteRole = async (id) => {
|
||||
try {
|
||||
await rolePermissionService.deleteRole(id)
|
||||
message.success('删除成功')
|
||||
loadRoles()
|
||||
} catch (error) {
|
||||
console.error('删除角色失败:', error)
|
||||
message.error('删除角色失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框确定
|
||||
const handleModalOk = async () => {
|
||||
try {
|
||||
if (isEdit.value) {
|
||||
await rolePermissionService.updateRole(formData.id, formData)
|
||||
message.success('更新成功')
|
||||
} else {
|
||||
await rolePermissionService.createRole(formData)
|
||||
message.success('创建成功')
|
||||
}
|
||||
|
||||
modalVisible.value = false
|
||||
loadRoles()
|
||||
} catch (error) {
|
||||
console.error('保存角色失败:', error)
|
||||
message.error('保存角色失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框取消
|
||||
const handleModalCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 菜单权限管理确定
|
||||
const handleMenuPermissionOk = async () => {
|
||||
try {
|
||||
console.log('🔍 [菜单权限保存] 角色ID:', currentRole.value.id)
|
||||
console.log('🔍 [菜单权限保存] 要保存的权限ID:', menuCheckedKeys.value)
|
||||
|
||||
await rolePermissionService.setRoleMenuPermissions(currentRole.value.id, {
|
||||
menuIds: menuCheckedKeys.value
|
||||
})
|
||||
|
||||
console.log('🔍 [菜单权限保存] 保存成功')
|
||||
message.success('菜单权限设置成功')
|
||||
menuPermissionModalVisible.value = false
|
||||
loadRoles()
|
||||
} catch (error) {
|
||||
console.error('设置菜单权限失败:', error)
|
||||
message.error('设置菜单权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 菜单权限管理取消
|
||||
const handleMenuPermissionCancel = () => {
|
||||
menuPermissionModalVisible.value = false
|
||||
menuCheckedKeys.value = []
|
||||
}
|
||||
|
||||
// 功能权限管理确定
|
||||
const handleFunctionPermissionOk = async () => {
|
||||
try {
|
||||
await rolePermissionService.setRoleFunctionPermissions(currentRole.value.id, {
|
||||
permissionIds: functionCheckedKeys.value
|
||||
})
|
||||
message.success('功能权限设置成功')
|
||||
functionPermissionModalVisible.value = false
|
||||
loadRoles()
|
||||
} catch (error) {
|
||||
console.error('设置功能权限失败:', error)
|
||||
message.error('设置功能权限失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 功能权限管理取消
|
||||
const handleFunctionPermissionCancel = () => {
|
||||
functionPermissionModalVisible.value = false
|
||||
functionCheckedKeys.value = []
|
||||
}
|
||||
|
||||
// 菜单树形选择变化
|
||||
const handleMenuTreeCheck = (checkedKeysValue) => {
|
||||
menuCheckedKeys.value = checkedKeysValue
|
||||
}
|
||||
|
||||
// 功能权限选择变化
|
||||
const handleFunctionCheck = (checkedValues) => {
|
||||
functionCheckedKeys.value = checkedValues
|
||||
}
|
||||
|
||||
// 过滤权限
|
||||
const filterPermissions = () => {
|
||||
// 重新计算过滤后的权限
|
||||
}
|
||||
|
||||
// 菜单权限全选
|
||||
const checkAllMenus = () => {
|
||||
const getAllKeys = (tree) => {
|
||||
let keys = []
|
||||
tree.forEach(node => {
|
||||
keys.push(node.id)
|
||||
if (node.children) {
|
||||
keys = keys.concat(getAllKeys(node.children))
|
||||
}
|
||||
})
|
||||
return keys
|
||||
}
|
||||
menuCheckedKeys.value = getAllKeys(menuTree.value)
|
||||
}
|
||||
|
||||
// 菜单权限全不选
|
||||
const uncheckAllMenus = () => {
|
||||
menuCheckedKeys.value = []
|
||||
}
|
||||
|
||||
// 功能权限全选
|
||||
const checkAllFunctions = () => {
|
||||
functionCheckedKeys.value = allPermissions.value.map(p => p.id)
|
||||
}
|
||||
|
||||
// 功能权限全不选
|
||||
const uncheckAllFunctions = () => {
|
||||
functionCheckedKeys.value = []
|
||||
}
|
||||
|
||||
// 处理角色状态切换
|
||||
const handleStatusChange = async (record, checked) => {
|
||||
try {
|
||||
// 设置加载状态
|
||||
record.statusChanging = true
|
||||
|
||||
await rolePermissionService.toggleRoleStatus(record.id, { status: checked })
|
||||
|
||||
// 更新本地状态
|
||||
record.status = checked
|
||||
|
||||
message.success(`角色${checked ? '启用' : '禁用'}成功`)
|
||||
} catch (error) {
|
||||
console.error('切换角色状态失败:', error)
|
||||
message.error('切换角色状态失败')
|
||||
|
||||
// 恢复原状态
|
||||
record.status = !checked
|
||||
} finally {
|
||||
record.statusChanging = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
name: '',
|
||||
description: '',
|
||||
status: true
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载时加载数据
|
||||
onMounted(() => {
|
||||
loadRoles()
|
||||
loadMenuTree()
|
||||
loadAllPermissions()
|
||||
loadPermissionModules()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.role-permissions {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.permission-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.permission-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.permission-header h3 {
|
||||
margin: 0;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.function-permissions {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.function-permissions .ant-collapse {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.function-permissions .ant-collapse-item {
|
||||
margin-bottom: 8px;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.function-permissions .ant-collapse-header {
|
||||
background: #fafafa;
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
358
admin-system/frontend/src/views/SearchMonitor.vue
Normal file
358
admin-system/frontend/src/views/SearchMonitor.vue
Normal file
@@ -0,0 +1,358 @@
|
||||
<template>
|
||||
<div class="search-monitor">
|
||||
<div class="page-header">
|
||||
<h2>搜索监听数据查询</h2>
|
||||
<p>查看前端和后端接收到的搜索相关信息数据</p>
|
||||
</div>
|
||||
|
||||
<div class="monitor-content">
|
||||
<!-- 搜索测试区域 -->
|
||||
<div class="test-section">
|
||||
<h3>搜索测试</h3>
|
||||
<div class="test-controls">
|
||||
<a-input
|
||||
v-model="testKeyword"
|
||||
placeholder="输入测试关键词"
|
||||
@input="handleTestInput"
|
||||
@focus="handleTestFocus"
|
||||
@blur="handleTestBlur"
|
||||
@change="handleTestChange"
|
||||
style="width: 300px; margin-right: 10px"
|
||||
/>
|
||||
<a-button type="primary" @click="handleTestSearch">测试搜索</a-button>
|
||||
<a-button @click="clearLogs">清空日志</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 实时日志显示 -->
|
||||
<div class="logs-section">
|
||||
<h3>实时监听日志</h3>
|
||||
<div class="logs-container">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
:class="['log-item', log.type]"
|
||||
>
|
||||
<div class="log-header">
|
||||
<span class="log-time">{{ log.timestamp }}</span>
|
||||
<span class="log-type">{{ log.typeLabel }}</span>
|
||||
<span class="log-module">{{ log.module }}</span>
|
||||
</div>
|
||||
<div class="log-content">
|
||||
<pre>{{ JSON.stringify(log.data, null, 2) }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-section">
|
||||
<h3>统计信息</h3>
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="总请求数" :value="stats.totalRequests" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="成功请求" :value="stats.successRequests" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="失败请求" :value="stats.failedRequests" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic title="平均响应时间" :value="stats.avgResponseTime" suffix="ms" />
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { api, directApi } from '../utils/api'
|
||||
|
||||
// 测试关键词
|
||||
const testKeyword = ref('')
|
||||
|
||||
// 日志数据
|
||||
const logs = ref([])
|
||||
|
||||
// 统计信息
|
||||
const stats = reactive({
|
||||
totalRequests: 0,
|
||||
successRequests: 0,
|
||||
failedRequests: 0,
|
||||
avgResponseTime: 0
|
||||
})
|
||||
|
||||
// 测试输入处理
|
||||
const handleTestInput = (e) => {
|
||||
addLog('input', '前端输入监听', {
|
||||
event: 'input',
|
||||
value: e.target.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
const handleTestFocus = (e) => {
|
||||
addLog('focus', '前端焦点监听', {
|
||||
event: 'focus',
|
||||
value: e.target.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
const handleTestBlur = (e) => {
|
||||
addLog('blur', '前端失焦监听', {
|
||||
event: 'blur',
|
||||
value: e.target.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
const handleTestChange = (e) => {
|
||||
addLog('change', '前端值改变监听', {
|
||||
event: 'change',
|
||||
value: e.target.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// 测试搜索
|
||||
const handleTestSearch = async () => {
|
||||
if (!testKeyword.value.trim()) {
|
||||
message.warning('请输入测试关键词')
|
||||
return
|
||||
}
|
||||
|
||||
addLog('search_start', '前端搜索开始', {
|
||||
keyword: testKeyword.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
try {
|
||||
const startTime = Date.now()
|
||||
const response = await api.get(`/farms/search?name=${encodeURIComponent(testKeyword.value)}`)
|
||||
const responseTime = Date.now() - startTime
|
||||
|
||||
const result = response
|
||||
|
||||
addLog('search_success', '前端搜索成功', {
|
||||
keyword: testKeyword.value,
|
||||
resultCount: result.data ? result.data.length : 0,
|
||||
responseTime: responseTime,
|
||||
backendData: result.data,
|
||||
meta: result.meta
|
||||
})
|
||||
|
||||
// 更新统计
|
||||
stats.totalRequests++
|
||||
stats.successRequests++
|
||||
stats.avgResponseTime = Math.round((stats.avgResponseTime * (stats.totalRequests - 1) + responseTime) / stats.totalRequests)
|
||||
|
||||
message.success(`搜索成功,找到 ${result.data ? result.data.length : 0} 条记录`)
|
||||
} catch (error) {
|
||||
addLog('search_error', '前端搜索失败', {
|
||||
keyword: testKeyword.value,
|
||||
error: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
stats.totalRequests++
|
||||
stats.failedRequests++
|
||||
|
||||
message.error('搜索失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
const addLog = (type, typeLabel, data) => {
|
||||
const log = {
|
||||
type,
|
||||
typeLabel,
|
||||
module: 'search_monitor',
|
||||
timestamp: new Date().toLocaleTimeString(),
|
||||
data
|
||||
}
|
||||
|
||||
logs.value.unshift(log)
|
||||
|
||||
// 限制日志数量
|
||||
if (logs.value.length > 100) {
|
||||
logs.value = logs.value.slice(0, 100)
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
logs.value = []
|
||||
stats.totalRequests = 0
|
||||
stats.successRequests = 0
|
||||
stats.failedRequests = 0
|
||||
stats.avgResponseTime = 0
|
||||
message.info('日志已清空')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
addLog('system', '系统启动', {
|
||||
message: '搜索监听系统已启动',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.search-monitor {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.monitor-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.test-section {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.test-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.test-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.logs-section {
|
||||
background: #fff;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.logs-section h3 {
|
||||
margin: 0;
|
||||
padding: 15px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid #d9d9d9;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.logs-container {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #e8e8e8;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.log-item.input {
|
||||
border-left: 4px solid #1890ff;
|
||||
}
|
||||
|
||||
.log-item.focus {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
.log-item.blur {
|
||||
border-left: 4px solid #faad14;
|
||||
}
|
||||
|
||||
.log-item.change {
|
||||
border-left: 4px solid #722ed1;
|
||||
}
|
||||
|
||||
.log-item.search_start {
|
||||
border-left: 4px solid #13c2c2;
|
||||
}
|
||||
|
||||
.log-item.search_success {
|
||||
border-left: 4px solid #52c41a;
|
||||
}
|
||||
|
||||
.log-item.search_error {
|
||||
border-left: 4px solid #ff4d4f;
|
||||
}
|
||||
|
||||
.log-item.system {
|
||||
border-left: 4px solid #8c8c8c;
|
||||
}
|
||||
|
||||
.log-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: #fafafa;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #666;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.log-type {
|
||||
background: #1890ff;
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.log-module {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.log-content {
|
||||
padding: 10px 12px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.log-content pre {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.stats-section h3 {
|
||||
margin: 0 0 15px 0;
|
||||
color: #333;
|
||||
}
|
||||
</style>
|
||||
509
admin-system/frontend/src/views/SmartAnklet.vue
Normal file
509
admin-system/frontend/src/views/SmartAnklet.vue
Normal file
@@ -0,0 +1,509 @@
|
||||
<template>
|
||||
<div class="smart-anklet-container">
|
||||
<div class="page-header">
|
||||
<h2>
|
||||
<a-icon type="radar-chart" style="margin-right: 8px;" />
|
||||
智能脚环管理
|
||||
</h2>
|
||||
<p>管理和监控智能脚环设备,实时监测动物运动和健康数据</p>
|
||||
</div>
|
||||
|
||||
<!-- 操作栏 -->
|
||||
<div class="action-bar">
|
||||
<a-space>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon>
|
||||
<PlusOutlined />
|
||||
</template>
|
||||
添加脚环
|
||||
</a-button>
|
||||
<a-button @click="refreshData">
|
||||
<template #icon>
|
||||
<ReloadOutlined />
|
||||
</template>
|
||||
刷新
|
||||
</a-button>
|
||||
<a-button @click="exportData">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<!-- <a-row :gutter="16" class="stats-row">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="总数量"
|
||||
:value="stats.total"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="活跃设备"
|
||||
:value="stats.active"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="待机设备"
|
||||
:value="stats.standby"
|
||||
:value-style="{ color: '#faad14' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="故障设备"
|
||||
:value="stats.fault"
|
||||
:value-style="{ color: '#f5222d' }"
|
||||
/>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row> -->
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<a-card title="脚环设备列表" class="table-card">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="anklets"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'status'">
|
||||
<a-tag :color="getStatusColor(record.status)">
|
||||
{{ getStatusText(record.status) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'stepCount'">
|
||||
<a-statistic
|
||||
:value="record.stepCount"
|
||||
suffix="步"
|
||||
:value-style="{ fontSize: '14px' }"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'heartRate'">
|
||||
<span :style="{ color: getHeartRateColor(record.heartRate) }">
|
||||
{{ record.heartRate }} BPM
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'temperature'">
|
||||
<span :style="{ color: getTemperatureColor(record.temperature) }">
|
||||
{{ record.temperature }}°C
|
||||
</span>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space>
|
||||
<a-button type="link" size="small" @click="viewDetail(record)">
|
||||
详情
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="viewChart(record)">
|
||||
图表
|
||||
</a-button>
|
||||
<a-button type="link" size="small" @click="editAnklet(record)">
|
||||
编辑
|
||||
</a-button>
|
||||
<a-popconfirm
|
||||
title="确定要删除这个脚环吗?"
|
||||
@confirm="deleteAnklet(record.id)"
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a-button type="link" size="small" danger>删除</a-button>
|
||||
</a-popconfirm>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
|
||||
<!-- 添加/编辑模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:title="isEdit ? '编辑脚环' : '添加脚环'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
:confirm-loading="submitLoading"
|
||||
>
|
||||
<a-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="rules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="设备编号" name="deviceId">
|
||||
<a-input v-model:value="formData.deviceId" placeholder="请输入设备编号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="动物ID" name="animalId">
|
||||
<a-select v-model:value="formData.animalId" placeholder="请选择关联动物">
|
||||
<a-select-option value="1">动物001</a-select-option>
|
||||
<a-select-option value="2">动物002</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="设备型号" name="model">
|
||||
<a-input v-model:value="formData.model" placeholder="请输入设备型号" />
|
||||
</a-form-item>
|
||||
<a-form-item label="监测频率" name="frequency">
|
||||
<a-select v-model:value="formData.frequency" placeholder="请选择监测频率">
|
||||
<a-select-option value="1">每分钟</a-select-option>
|
||||
<a-select-option value="5">每5分钟</a-select-option>
|
||||
<a-select-option value="15">每15分钟</a-select-option>
|
||||
<a-select-option value="60">每小时</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="安装日期" name="installDate">
|
||||
<a-date-picker v-model:value="formData.installDate" style="width: 100%" />
|
||||
</a-form-item>
|
||||
<a-form-item label="备注" name="notes">
|
||||
<a-textarea v-model:value="formData.notes" placeholder="请输入备注信息" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined, ReloadOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const anklets = ref([])
|
||||
const loading = ref(false)
|
||||
const modalVisible = ref(false)
|
||||
const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
total: 0,
|
||||
active: 0,
|
||||
standby: 0,
|
||||
fault: 0
|
||||
})
|
||||
|
||||
// 表格配置
|
||||
const columns = [
|
||||
{
|
||||
title: '设备编号',
|
||||
dataIndex: 'deviceId',
|
||||
key: 'deviceId',
|
||||
},
|
||||
{
|
||||
title: '关联动物',
|
||||
dataIndex: 'animalName',
|
||||
key: 'animalName',
|
||||
},
|
||||
{
|
||||
title: '设备型号',
|
||||
dataIndex: 'model',
|
||||
key: 'model',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
},
|
||||
{
|
||||
title: '步数',
|
||||
dataIndex: 'stepCount',
|
||||
key: 'stepCount',
|
||||
},
|
||||
{
|
||||
title: '心率',
|
||||
dataIndex: 'heartRate',
|
||||
key: 'heartRate',
|
||||
},
|
||||
{
|
||||
title: '体温',
|
||||
dataIndex: 'temperature',
|
||||
key: 'temperature',
|
||||
},
|
||||
{
|
||||
title: '最后更新',
|
||||
dataIndex: 'lastUpdate',
|
||||
key: 'lastUpdate',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
}
|
||||
]
|
||||
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
deviceId: '',
|
||||
animalId: '',
|
||||
model: '',
|
||||
frequency: '',
|
||||
installDate: null,
|
||||
notes: ''
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const rules = {
|
||||
deviceId: [
|
||||
{ required: true, message: '请输入设备编号', trigger: 'blur' }
|
||||
],
|
||||
animalId: [
|
||||
{ required: true, message: '请选择关联动物', trigger: 'change' }
|
||||
],
|
||||
model: [
|
||||
{ required: true, message: '请输入设备型号', trigger: 'blur' }
|
||||
],
|
||||
frequency: [
|
||||
{ required: true, message: '请选择监测频率', trigger: 'change' }
|
||||
]
|
||||
}
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status) => {
|
||||
const colors = {
|
||||
'active': 'green',
|
||||
'standby': 'orange',
|
||||
'fault': 'red'
|
||||
}
|
||||
return colors[status] || 'default'
|
||||
}
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status) => {
|
||||
const texts = {
|
||||
'active': '活跃',
|
||||
'standby': '待机',
|
||||
'fault': '故障'
|
||||
}
|
||||
return texts[status] || '未知'
|
||||
}
|
||||
|
||||
// 获取心率颜色
|
||||
const getHeartRateColor = (heartRate) => {
|
||||
if (heartRate >= 60 && heartRate <= 100) return '#52c41a'
|
||||
if (heartRate > 100 || heartRate < 60) return '#faad14'
|
||||
return '#f5222d'
|
||||
}
|
||||
|
||||
// 获取体温颜色
|
||||
const getTemperatureColor = (temperature) => {
|
||||
if (temperature >= 38.0 && temperature <= 39.5) return '#52c41a'
|
||||
if (temperature > 39.5 || temperature < 38.0) return '#faad14'
|
||||
return '#f5222d'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 模拟数据
|
||||
const mockData = [
|
||||
{
|
||||
id: 1,
|
||||
deviceId: 'AN001',
|
||||
animalName: '牛001',
|
||||
model: 'SmartAnklet-V1',
|
||||
status: 'active',
|
||||
stepCount: 2456,
|
||||
heartRate: 75,
|
||||
temperature: 38.5,
|
||||
lastUpdate: '2025-01-18 10:30:00'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
deviceId: 'AN002',
|
||||
animalName: '牛002',
|
||||
model: 'SmartAnklet-V1',
|
||||
status: 'standby',
|
||||
stepCount: 1823,
|
||||
heartRate: 68,
|
||||
temperature: 38.2,
|
||||
lastUpdate: '2025-01-18 09:15:00'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
deviceId: 'AN003',
|
||||
animalName: '羊001',
|
||||
model: 'SmartAnklet-V2',
|
||||
status: 'active',
|
||||
stepCount: 3124,
|
||||
heartRate: 82,
|
||||
temperature: 39.1,
|
||||
lastUpdate: '2025-01-18 10:25:00'
|
||||
}
|
||||
]
|
||||
|
||||
anklets.value = mockData
|
||||
pagination.total = mockData.length
|
||||
|
||||
// 更新统计数据
|
||||
stats.total = mockData.length
|
||||
stats.active = mockData.filter(item => item.status === 'active').length
|
||||
stats.standby = mockData.filter(item => item.status === 'standby').length
|
||||
stats.fault = mockData.filter(item => item.status === 'fault').length
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
message.error('获取数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新数据
|
||||
const refreshData = () => {
|
||||
fetchData()
|
||||
message.success('数据已刷新')
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = () => {
|
||||
message.info('导出功能开发中...')
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = () => {
|
||||
isEdit.value = false
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 编辑脚环
|
||||
const editAnklet = (record) => {
|
||||
isEdit.value = true
|
||||
Object.assign(formData, {
|
||||
...record,
|
||||
installDate: null
|
||||
})
|
||||
modalVisible.value = true
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetail = (record) => {
|
||||
message.info(`查看 ${record.deviceId} 的详细信息`)
|
||||
}
|
||||
|
||||
// 查看图表
|
||||
const viewChart = (record) => {
|
||||
message.info(`查看 ${record.deviceId} 的运动图表`)
|
||||
}
|
||||
|
||||
// 删除脚环
|
||||
const deleteAnklet = async (id) => {
|
||||
try {
|
||||
message.success('删除成功')
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('删除失败:', error)
|
||||
message.error('删除失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
await formRef.value.validate()
|
||||
submitLoading.value = true
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
message.success(isEdit.value ? '更新成功' : '创建成功')
|
||||
modalVisible.value = false
|
||||
fetchData()
|
||||
} catch (error) {
|
||||
console.error('提交失败:', error)
|
||||
message.error('操作失败')
|
||||
} finally {
|
||||
submitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 取消操作
|
||||
const handleCancel = () => {
|
||||
modalVisible.value = false
|
||||
resetForm()
|
||||
}
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
Object.assign(formData, {
|
||||
id: null,
|
||||
deviceId: '',
|
||||
animalId: '',
|
||||
model: '',
|
||||
frequency: '',
|
||||
installDate: null,
|
||||
notes: ''
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-anklet-container {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-header h2 {
|
||||
margin: 0 0 8px 0;
|
||||
color: #1890ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.page-header p {
|
||||
color: #666;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.table-card {
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
1776
admin-system/frontend/src/views/SmartCollar.vue
Normal file
1776
admin-system/frontend/src/views/SmartCollar.vue
Normal file
File diff suppressed because it is too large
Load Diff
1141
admin-system/frontend/src/views/SmartCollarAlert.vue
Normal file
1141
admin-system/frontend/src/views/SmartCollarAlert.vue
Normal file
File diff suppressed because it is too large
Load Diff
1252
admin-system/frontend/src/views/SmartEartag.vue
Normal file
1252
admin-system/frontend/src/views/SmartEartag.vue
Normal file
File diff suppressed because it is too large
Load Diff
935
admin-system/frontend/src/views/SmartEartagAlert.vue
Normal file
935
admin-system/frontend/src/views/SmartEartagAlert.vue
Normal file
@@ -0,0 +1,935 @@
|
||||
<template>
|
||||
<div class="smart-eartag-alert-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">智能耳标预警</h2>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-cards">
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon battery">
|
||||
<PoweroffOutlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.lowBattery }}</div>
|
||||
<div class="stat-label">低电量预警</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon offline">
|
||||
<DisconnectOutlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.offline }}</div>
|
||||
<div class="stat-label">离线预警</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon temperature">
|
||||
<FireOutlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.highTemperature }}</div>
|
||||
<div class="stat-label">温度预警</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-icon movement">
|
||||
<ThunderboltOutlined />
|
||||
</div>
|
||||
<div class="stat-content">
|
||||
<div class="stat-value">{{ stats.abnormalMovement }}</div>
|
||||
<div class="stat-label">异常运动预警</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 搜索和筛选栏 -->
|
||||
<div class="search-bar">
|
||||
<div class="search-group">
|
||||
<a-input
|
||||
:value="searchValue"
|
||||
@input="updateSearchValue"
|
||||
placeholder="请输入耳标编号"
|
||||
class="search-input"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
<a-select
|
||||
v-model="alertTypeFilter"
|
||||
placeholder="预警类型"
|
||||
class="filter-select"
|
||||
@change="handleFilterChange"
|
||||
>
|
||||
<a-select-option value="">全部预警</a-select-option>
|
||||
<a-select-option value="battery">低电量预警</a-select-option>
|
||||
<a-select-option value="offline">离线预警</a-select-option>
|
||||
<a-select-option value="temperature">温度预警</a-select-option>
|
||||
<a-select-option value="movement">异常运动预警</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" class="search-button" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button class="search-button" @click="handleClearSearch" v-if="searchValue.trim() || alertTypeFilter">
|
||||
清除
|
||||
</a-button>
|
||||
</div>
|
||||
<a-button @click="exportData" class="export-button">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 预警列表表格 -->
|
||||
<div class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="alerts"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
class="alert-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 耳标编号 -->
|
||||
<template v-if="column.dataIndex === 'eartagNumber'">
|
||||
<span class="eartag-number">{{ record.eartagNumber }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 预警类型 -->
|
||||
<template v-else-if="column.dataIndex === 'alertType'">
|
||||
<a-tag
|
||||
:color="getAlertTypeColor(record.alertType)"
|
||||
class="alert-type-tag"
|
||||
>
|
||||
{{ getAlertTypeText(record.alertType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 预警级别 -->
|
||||
<template v-else-if="column.dataIndex === 'alertLevel'">
|
||||
<a-tag
|
||||
:color="getAlertLevelColor(record.alertLevel)"
|
||||
class="alert-level-tag"
|
||||
>
|
||||
{{ getAlertLevelText(record.alertLevel) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<!-- 预警时间 -->
|
||||
<template v-else-if="column.dataIndex === 'alertTime'">
|
||||
<div class="alert-time-cell">
|
||||
<span class="alert-time-value">{{ record.alertTime }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<div class="action-cell">
|
||||
<a-button type="link" class="action-link" @click="viewDetails(record)">
|
||||
查看详情
|
||||
</a-button>
|
||||
<a-button type="link" class="action-link" @click="handleAlert(record)">
|
||||
处理预警
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 预警详情模态框 -->
|
||||
<a-modal
|
||||
:open="detailVisible"
|
||||
title="预警详情"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
@cancel="handleDetailCancel"
|
||||
>
|
||||
<div class="alert-detail-modal">
|
||||
<div class="detail-grid">
|
||||
<div class="detail-item">
|
||||
<span class="label">耳标编号:</span>
|
||||
<span class="value">{{ currentAlert?.eartagNumber }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警类型:</span>
|
||||
<span class="value">
|
||||
<a-tag :color="getAlertTypeColor(currentAlert?.alertType)">
|
||||
{{ getAlertTypeText(currentAlert?.alertType) }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警级别:</span>
|
||||
<span class="value">
|
||||
<a-tag :color="getAlertLevelColor(currentAlert?.alertLevel)">
|
||||
{{ getAlertLevelText(currentAlert?.alertLevel) }}
|
||||
</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警时间:</span>
|
||||
<span class="value">{{ currentAlert?.alertTime }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">设备电量:</span>
|
||||
<span class="value">{{ currentAlert?.battery }}%</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">设备温度:</span>
|
||||
<span class="value">{{ currentAlert?.temperature }}°C</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">运动量:</span>
|
||||
<span class="value">{{ currentAlert?.movement }}</span>
|
||||
</div>
|
||||
<div class="detail-item">
|
||||
<span class="label">预警描述:</span>
|
||||
<span class="value">{{ currentAlert?.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="modal-footer">
|
||||
<a-button @click="handleDetailCancel">关闭</a-button>
|
||||
<a-button type="primary" @click="handleAlert(currentAlert)">处理预警</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
SearchOutlined,
|
||||
PoweroffOutlined,
|
||||
DisconnectOutlined,
|
||||
FireOutlined,
|
||||
ThunderboltOutlined,
|
||||
ExportOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
// 响应式数据
|
||||
const alerts = ref([])
|
||||
const loading = ref(false)
|
||||
const searchValue = ref('')
|
||||
const alertTypeFilter = ref('')
|
||||
const detailVisible = ref(false)
|
||||
const currentAlert = ref(null)
|
||||
|
||||
// 统计数据
|
||||
const stats = reactive({
|
||||
lowBattery: 0,
|
||||
offline: 0,
|
||||
highTemperature: 0,
|
||||
abnormalMovement: 0
|
||||
})
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
// 表格列配置
|
||||
const columns = [
|
||||
{
|
||||
title: '耳标编号',
|
||||
dataIndex: 'eartagNumber',
|
||||
key: 'eartagNumber',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '预警类型',
|
||||
dataIndex: 'alertType',
|
||||
key: 'alertType',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '预警级别',
|
||||
dataIndex: 'alertLevel',
|
||||
key: 'alertLevel',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '预警时间',
|
||||
dataIndex: 'alertTime',
|
||||
key: 'alertTime',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备电量',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备温度',
|
||||
dataIndex: 'temperature',
|
||||
key: 'temperature',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '当日步数',
|
||||
dataIndex: 'dailySteps',
|
||||
key: 'dailySteps',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
}
|
||||
]
|
||||
|
||||
// 获取预警类型文本
|
||||
const getAlertTypeText = (type) => {
|
||||
const typeMap = {
|
||||
'battery': '低电量预警',
|
||||
'offline': '离线预警',
|
||||
'temperature': '温度预警',
|
||||
'movement': '异常运动预警'
|
||||
}
|
||||
return typeMap[type] || '未知预警'
|
||||
}
|
||||
|
||||
// 获取预警类型颜色
|
||||
const getAlertTypeColor = (type) => {
|
||||
const colorMap = {
|
||||
'battery': 'orange',
|
||||
'offline': 'red',
|
||||
'temperature': 'red',
|
||||
'movement': 'purple'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
// 获取预警级别文本
|
||||
const getAlertLevelText = (level) => {
|
||||
const levelMap = {
|
||||
'high': '高级',
|
||||
'medium': '中级',
|
||||
'low': '低级'
|
||||
}
|
||||
return levelMap[level] || '未知'
|
||||
}
|
||||
|
||||
// 获取预警级别颜色
|
||||
const getAlertLevelColor = (level) => {
|
||||
const colorMap = {
|
||||
'high': 'red',
|
||||
'medium': 'orange',
|
||||
'low': 'green'
|
||||
}
|
||||
return colorMap[level] || 'default'
|
||||
}
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async (showMessage = false, customAlertType = null) => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams({
|
||||
page: pagination.current.toString(),
|
||||
limit: pagination.pageSize.toString(),
|
||||
_t: Date.now().toString(),
|
||||
refresh: 'true'
|
||||
})
|
||||
|
||||
// 添加搜索条件
|
||||
if (searchValue.value.trim()) {
|
||||
params.append('search', searchValue.value.trim())
|
||||
}
|
||||
|
||||
// 添加预警类型筛选 - 使用传入的参数或默认值
|
||||
const alertType = customAlertType !== null ? customAlertType : alertTypeFilter.value
|
||||
if (alertType && alertType.trim() !== '') {
|
||||
params.append('alertType', alertType)
|
||||
}
|
||||
|
||||
// 调用API获取预警数据
|
||||
const { smartAlertService } = await import('../utils/dataService')
|
||||
console.log('使用smartAlertService获取耳标预警数据')
|
||||
console.log('搜索参数:', {
|
||||
search: searchValue.value.trim(),
|
||||
alertType: alertTypeFilter.value,
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize
|
||||
})
|
||||
|
||||
const requestParams = {
|
||||
search: searchValue.value.trim(),
|
||||
alertType: alertType,
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize
|
||||
}
|
||||
console.log('发送API请求,参数:', requestParams)
|
||||
|
||||
const result = await smartAlertService.getEartagAlerts(requestParams)
|
||||
console.log('API响应结果:', result)
|
||||
console.log('API返回的数据类型:', typeof result.data)
|
||||
console.log('API返回的数据长度:', result.data ? result.data.length : 'undefined')
|
||||
console.log('API返回的数据内容:', result.data)
|
||||
console.log('响应类型:', typeof result)
|
||||
console.log('是否有success字段:', 'success' in result)
|
||||
console.log('success值:', result.success)
|
||||
|
||||
if (result.success) {
|
||||
// 更新预警列表数据
|
||||
alerts.value = result.data || []
|
||||
pagination.total = result.total || 0
|
||||
|
||||
// 更新统计数据
|
||||
if (result.stats) {
|
||||
stats.lowBattery = result.stats.lowBattery || 0
|
||||
stats.offline = result.stats.offline || 0
|
||||
stats.highTemperature = result.stats.highTemperature || 0
|
||||
stats.abnormalMovement = result.stats.abnormalMovement || 0
|
||||
}
|
||||
|
||||
console.log('更新后的预警列表:', alerts.value)
|
||||
console.log('总数:', pagination.total)
|
||||
|
||||
if (showMessage) {
|
||||
const searchText = searchValue.value.trim() ? `搜索"${searchValue.value.trim()}"` : '加载'
|
||||
message.success(`${searchText}完成,共找到 ${pagination.total} 条预警数据`)
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(result.message || '获取数据失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
console.error('错误详情:', {
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
name: error.name
|
||||
})
|
||||
if (showMessage) {
|
||||
message.error('获取数据失败: ' + error.message)
|
||||
}
|
||||
|
||||
// 清空数据而不是显示模拟数据
|
||||
alerts.value = []
|
||||
pagination.total = 0
|
||||
stats.lowBattery = 0
|
||||
stats.offline = 0
|
||||
stats.highTemperature = 0
|
||||
stats.abnormalMovement = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 生成模拟数据
|
||||
const generateMockData = () => {
|
||||
const mockAlerts = [
|
||||
{
|
||||
id: 1,
|
||||
eartagNumber: 'EARTAG001',
|
||||
alertType: 'battery',
|
||||
alertLevel: 'high',
|
||||
alertTime: '2025-01-18 10:30:00',
|
||||
battery: 15,
|
||||
temperature: 38.5,
|
||||
gpsSignal: '强',
|
||||
movementStatus: '正常',
|
||||
description: '设备电量低于20%,需要及时充电',
|
||||
longitude: 116.3974,
|
||||
latitude: 39.9093
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
eartagNumber: 'EARTAG002',
|
||||
alertType: 'offline',
|
||||
alertLevel: 'high',
|
||||
alertTime: '2025-01-18 09:15:00',
|
||||
battery: 0,
|
||||
temperature: 0,
|
||||
gpsSignal: '无',
|
||||
movementStatus: '静止',
|
||||
description: '设备已离线超过30分钟',
|
||||
longitude: 0,
|
||||
latitude: 0
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
eartagNumber: 'EARTAG003',
|
||||
alertType: 'temperature',
|
||||
alertLevel: 'medium',
|
||||
alertTime: '2025-01-18 08:45:00',
|
||||
battery: 85,
|
||||
temperature: 42.3,
|
||||
gpsSignal: '强',
|
||||
movementStatus: '正常',
|
||||
description: '设备温度异常,超过正常范围',
|
||||
longitude: 116.4074,
|
||||
latitude: 39.9193
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
eartagNumber: 'EARTAG004',
|
||||
alertType: 'movement',
|
||||
alertLevel: 'low',
|
||||
alertTime: '2025-01-18 07:20:00',
|
||||
battery: 92,
|
||||
temperature: 39.1,
|
||||
gpsSignal: '强',
|
||||
movementStatus: '异常',
|
||||
description: '运动量异常,可能发生异常行为',
|
||||
longitude: 116.4174,
|
||||
latitude: 39.9293
|
||||
}
|
||||
]
|
||||
|
||||
alerts.value = mockAlerts
|
||||
pagination.total = mockAlerts.length
|
||||
|
||||
// 更新统计数据
|
||||
stats.lowBattery = mockAlerts.filter(alert => alert.alertType === 'battery').length
|
||||
stats.offline = mockAlerts.filter(alert => alert.alertType === 'offline').length
|
||||
stats.highTemperature = mockAlerts.filter(alert => alert.alertType === 'temperature').length
|
||||
stats.abnormalMovement = mockAlerts.filter(alert => alert.alertType === 'movement').length
|
||||
}
|
||||
|
||||
// 更新搜索值
|
||||
const updateSearchValue = (e) => {
|
||||
searchValue.value = e.target.value
|
||||
console.log('搜索输入框值变化:', searchValue.value)
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
pagination.current = 1
|
||||
fetchData(true)
|
||||
}
|
||||
|
||||
// 筛选变化处理
|
||||
const handleFilterChange = (value) => {
|
||||
console.log('=== 智能耳标预警类型筛选变化 ===')
|
||||
console.log('传入的 value 参数:', value)
|
||||
console.log('传入的 value 参数类型:', typeof value)
|
||||
console.log('传入的 value 参数长度:', value !== undefined ? value.length : 'undefined')
|
||||
console.log('传入的 value 参数是否为空:', value === '')
|
||||
console.log('传入的 value 参数是否为undefined:', value === undefined)
|
||||
console.log('传入的 value 参数是否为null:', value === null)
|
||||
console.log('传入的 value 参数是否有效:', value && value.trim() !== '')
|
||||
|
||||
// 使用传入的 value 参数而不是 alertTypeFilter.value
|
||||
const alertType = value || ''
|
||||
console.log('使用的 alertType:', alertType)
|
||||
|
||||
pagination.current = 1
|
||||
console.log('准备调用 fetchData,参数:', {
|
||||
search: searchValue.value,
|
||||
alertType: alertType,
|
||||
page: pagination.current,
|
||||
limit: pagination.pageSize
|
||||
})
|
||||
|
||||
// 直接调用 fetchData 并传递 alertType 参数
|
||||
fetchData(true, alertType)
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
const handleClearSearch = () => {
|
||||
searchValue.value = ''
|
||||
alertTypeFilter.value = ''
|
||||
pagination.current = 1
|
||||
fetchData(true)
|
||||
}
|
||||
|
||||
// 查看详情
|
||||
const viewDetails = (record) => {
|
||||
currentAlert.value = record
|
||||
detailVisible.value = true
|
||||
}
|
||||
|
||||
// 处理预警
|
||||
const handleAlert = (record) => {
|
||||
message.success(`正在处理预警: ${record.eartagNumber}`)
|
||||
// 这里可以添加处理预警的逻辑
|
||||
}
|
||||
|
||||
// 取消详情
|
||||
const handleDetailCancel = () => {
|
||||
detailVisible.value = false
|
||||
currentAlert.value = null
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = async () => {
|
||||
try {
|
||||
console.log('=== 开始导出智能耳标预警数据 ===')
|
||||
|
||||
message.loading('正在获取所有预警数据...', 0)
|
||||
|
||||
// 使用smartAlertService获取所有预警数据,不受分页限制
|
||||
const { smartAlertService } = await import('../utils/dataService')
|
||||
|
||||
const requestParams = {
|
||||
search: searchValue.value.trim(),
|
||||
alertType: alertTypeFilter.value,
|
||||
page: 1,
|
||||
limit: 1000 // 获取大量数据
|
||||
}
|
||||
|
||||
console.log('导出请求参数:', requestParams)
|
||||
|
||||
const apiResult = await smartAlertService.getEartagAlerts(requestParams)
|
||||
console.log('预警API响应:', apiResult)
|
||||
|
||||
if (!apiResult.success || !apiResult.data) {
|
||||
message.destroy()
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
const allAlerts = apiResult.data || []
|
||||
console.log('获取到所有预警数据:', allAlerts.length, '条记录')
|
||||
console.log('原始数据示例:', allAlerts[0])
|
||||
|
||||
// 预警类型中文映射
|
||||
const alertTypeMap = {
|
||||
'battery': '低电量预警',
|
||||
'offline': '离线预警',
|
||||
'temperature': '温度异常预警',
|
||||
'movement': '运动异常预警',
|
||||
'location': '位置异常预警'
|
||||
}
|
||||
|
||||
// 预警级别中文映射
|
||||
const alertLevelMap = {
|
||||
'high': '高',
|
||||
'medium': '中',
|
||||
'low': '低',
|
||||
'critical': '紧急'
|
||||
}
|
||||
|
||||
// 转换数据格式以匹配导出工具类的列配置
|
||||
const exportData = allAlerts.map(item => {
|
||||
console.log('转换前预警数据项:', item)
|
||||
|
||||
// 格式化时间
|
||||
let alertTime = ''
|
||||
if (item.alertTime || item.alert_time || item.created_at) {
|
||||
const timeValue = item.alertTime || item.alert_time || item.created_at
|
||||
if (typeof timeValue === 'number') {
|
||||
// Unix时间戳转换
|
||||
alertTime = new Date(timeValue * 1000).toLocaleString('zh-CN')
|
||||
} else if (typeof timeValue === 'string') {
|
||||
// 字符串时间转换
|
||||
const date = new Date(timeValue)
|
||||
if (!isNaN(date.getTime())) {
|
||||
alertTime = date.toLocaleString('zh-CN')
|
||||
} else {
|
||||
alertTime = timeValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
device_name: item.deviceName || item.eartagNumber || item.device_name || item.deviceId || '',
|
||||
alert_type: alertTypeMap[item.alertType || item.alert_type] || item.alertType || item.alert_type || '',
|
||||
alert_level: alertLevelMap[item.alertLevel || item.alert_level] || item.alertLevel || item.alert_level || '',
|
||||
alert_content: item.alertContent || item.alert_content || item.message || item.description || '系统预警',
|
||||
alert_time: alertTime,
|
||||
status: item.status || (item.processed ? '已处理' : '未处理'),
|
||||
handler: item.handler || item.processor || item.handler_name || item.operator || ''
|
||||
}
|
||||
})
|
||||
|
||||
console.log('转换后预警数据示例:', exportData[0])
|
||||
console.log('转换后预警数据总数:', exportData.length)
|
||||
|
||||
message.destroy()
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportAlertData(exportData, 'eartag')
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchData(true)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-eartag-alert-container {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 页面标题样式 */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #52c41a;
|
||||
margin: 0;
|
||||
padding: 12px 20px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 统计卡片样式 */
|
||||
.stats-cards {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: #fff;
|
||||
border: 1px solid #f0f0f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.stat-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
color: #fff;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.stat-icon.battery {
|
||||
background: linear-gradient(135deg, #ff9a56, #ff6b6b);
|
||||
}
|
||||
|
||||
.stat-icon.offline {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ee5a52);
|
||||
}
|
||||
|
||||
.stat-icon.temperature {
|
||||
background: linear-gradient(135deg, #ff6b6b, #ff4757);
|
||||
}
|
||||
|
||||
.stat-icon.movement {
|
||||
background: linear-gradient(135deg, #a55eea, #8b5cf6);
|
||||
}
|
||||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
color: #262626;
|
||||
line-height: 1;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: #8c8c8c;
|
||||
}
|
||||
|
||||
/* 搜索栏样式 */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.filter-select {
|
||||
width: 150px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
/* 表格容器样式 */
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.alert-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 表格单元格样式 */
|
||||
.eartag-number {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.alert-type-tag,
|
||||
.alert-level-tag {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.alert-time-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.alert-time-value {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: #1890ff;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 预警详情模态框样式 */
|
||||
.alert-detail-modal {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.detail-item .label {
|
||||
min-width: 100px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.detail-item .value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.alert-table :deep(.ant-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert-table :deep(.ant-table-thead > tr > th) {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.alert-table :deep(.ant-table-tbody > tr > td) {
|
||||
padding: 6px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.alert-table :deep(.ant-table-tbody > tr:hover > td) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
890
admin-system/frontend/src/views/SmartHost.vue
Normal file
890
admin-system/frontend/src/views/SmartHost.vue
Normal file
@@ -0,0 +1,890 @@
|
||||
<template>
|
||||
<div class="smart-host-container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<h2 class="page-title">主机定位总览</h2>
|
||||
</div>
|
||||
|
||||
<!-- 搜索栏 -->
|
||||
<div class="search-bar">
|
||||
<a-button @click="exportData" class="export-button">
|
||||
<template #icon>
|
||||
<ExportOutlined />
|
||||
</template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<div class="search-group">
|
||||
<a-input
|
||||
:value="searchValue"
|
||||
@input="updateSearchValue"
|
||||
placeholder="请输入主机编号"
|
||||
class="search-input"
|
||||
@pressEnter="handleSearch"
|
||||
/>
|
||||
<a-button type="primary" class="search-button" @click="handleSearch">
|
||||
<template #icon>
|
||||
<SearchOutlined />
|
||||
</template>
|
||||
搜索
|
||||
</a-button>
|
||||
<a-button class="search-button" @click="handleClearSearch" v-if="searchValue.trim()">
|
||||
清除
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 数据表格 -->
|
||||
<div class="table-container">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="hosts"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
row-key="id"
|
||||
@change="handleTableChange"
|
||||
class="host-table"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<!-- 设备编号 -->
|
||||
<template v-if="column.dataIndex === 'deviceNumber'">
|
||||
<span class="device-number">{{ record.deviceNumber }}</span>
|
||||
</template>
|
||||
|
||||
<!-- 设备电量% -->
|
||||
<template v-else-if="column.dataIndex === 'battery'">
|
||||
<div class="battery-cell">
|
||||
<span class="battery-value">{{ record.battery }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 设备信号值 -->
|
||||
<template v-else-if="column.dataIndex === 'signalValue'">
|
||||
<div class="signal-cell">
|
||||
<span class="signal-value">{{ record.signalValue }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 设备温度/°C -->
|
||||
<template v-else-if="column.dataIndex === 'temperature'">
|
||||
<div class="temperature-cell">
|
||||
<span class="temperature-value">{{ record.temperature }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 更新时间 -->
|
||||
<template v-else-if="column.dataIndex === 'updateTime'">
|
||||
<div class="update-time-cell">
|
||||
<span class="update-time-value">{{ record.updateTime }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 联网状态 -->
|
||||
<template v-else-if="column.dataIndex === 'networkStatus'">
|
||||
<div class="network-status-cell">
|
||||
<a-tag
|
||||
:color="getNetworkStatusColor(record.networkStatus)"
|
||||
class="network-status-tag"
|
||||
>
|
||||
{{ record.networkStatus }}
|
||||
</a-tag>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作 -->
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<div class="action-cell">
|
||||
<a-button type="link" class="action-link" @click="viewLocation(record)">
|
||||
查看定位
|
||||
</a-button>
|
||||
<a-button type="link" class="action-link" @click="viewCollectionInfo(record)">
|
||||
查看采集信息
|
||||
</a-button>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
|
||||
<!-- 定位信息模态框 -->
|
||||
<a-modal
|
||||
:open="locationVisible"
|
||||
title="查看定位"
|
||||
:footer="null"
|
||||
width="90%"
|
||||
:style="{ top: '20px' }"
|
||||
@cancel="handleLocationCancel"
|
||||
>
|
||||
<div class="location-modal">
|
||||
<!-- 地图容器 -->
|
||||
<div class="map-container">
|
||||
<div id="locationMap" class="baidu-map"></div>
|
||||
|
||||
<!-- 地图样式切换按钮 - 严格按照图片样式 -->
|
||||
<div class="map-style-controls">
|
||||
<div
|
||||
:class="['style-btn', { active: mapStyle === 'normal' }]"
|
||||
@click="switchMapStyle('normal')"
|
||||
>
|
||||
地图
|
||||
</div>
|
||||
<div
|
||||
:class="['style-btn', { active: mapStyle === 'hybrid' }]"
|
||||
@click="switchMapStyle('hybrid')"
|
||||
>
|
||||
混合
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 底部取消按钮 -->
|
||||
<div class="location-footer">
|
||||
<a-button class="cancel-btn" @click="handleLocationCancel">取消</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
|
||||
<!-- 采集信息模态框 -->
|
||||
<a-modal
|
||||
:open="collectionInfoVisible"
|
||||
title="查看采集信息"
|
||||
:footer="null"
|
||||
width="800px"
|
||||
@cancel="handleCollectionInfoCancel"
|
||||
>
|
||||
<div class="collection-info-modal">
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<span class="label">设备编号:</span>
|
||||
<span class="value">{{ currentHost?.deviceNumber }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">设备电量:</span>
|
||||
<span class="value">{{ currentHost?.battery }}%</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">信号强度:</span>
|
||||
<span class="value">{{ currentHost?.signalValue }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">设备温度:</span>
|
||||
<span class="value">{{ currentHost?.temperature }}°C</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">GPS状态:</span>
|
||||
<span class="value">{{ currentHost?.gpsStatus }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">联网状态:</span>
|
||||
<span class="value">{{ currentHost?.networkStatus }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">最后更新:</span>
|
||||
<span class="value">{{ currentHost?.updateTime }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">设备标题:</span>
|
||||
<span class="value">{{ currentHost?.title || '未设置' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部按钮 -->
|
||||
<div class="modal-footer">
|
||||
<a-button @click="handleCollectionInfoCancel">关闭</a-button>
|
||||
</div>
|
||||
</div>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
import { loadBMapScript, createMap } from '@/utils/mapService'
|
||||
|
||||
// 响应式数据
|
||||
const hosts = ref([])
|
||||
const loading = ref(false)
|
||||
const searchValue = ref('')
|
||||
const autoRefresh = ref(true)
|
||||
const refreshInterval = ref(null)
|
||||
|
||||
// 模态框相关数据
|
||||
const locationVisible = ref(false)
|
||||
const collectionInfoVisible = ref(false)
|
||||
const mapStyle = ref('normal')
|
||||
const currentLocation = ref(null)
|
||||
const currentHost = ref(null)
|
||||
const baiduMap = ref(null)
|
||||
const locationMarker = ref(null)
|
||||
|
||||
// 分页配置
|
||||
const pagination = reactive({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
})
|
||||
|
||||
// 表格列配置 - 严格按照图片样式
|
||||
const columns = [
|
||||
{
|
||||
title: '设备编号',
|
||||
dataIndex: 'deviceNumber',
|
||||
key: 'deviceNumber',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备电量/%',
|
||||
dataIndex: 'battery',
|
||||
key: 'battery',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备信号值',
|
||||
dataIndex: 'signalValue',
|
||||
key: 'signalValue',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '设备温度/°C',
|
||||
dataIndex: 'temperature',
|
||||
key: 'temperature',
|
||||
width: 120,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '更新时间',
|
||||
dataIndex: 'updateTime',
|
||||
key: 'updateTime',
|
||||
width: 180,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '联网状态',
|
||||
dataIndex: 'networkStatus',
|
||||
key: 'networkStatus',
|
||||
width: 100,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 200,
|
||||
align: 'center',
|
||||
}
|
||||
]
|
||||
|
||||
// 获取联网状态颜色
|
||||
const getNetworkStatusColor = (status) => {
|
||||
const colorMap = {
|
||||
'已联网': 'green',
|
||||
'未联网': 'red'
|
||||
}
|
||||
return colorMap[status] || 'default'
|
||||
}
|
||||
|
||||
|
||||
// 获取数据
|
||||
const fetchData = async (showMessage = false) => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 构建查询参数
|
||||
const params = new URLSearchParams({
|
||||
page: pagination.current.toString(),
|
||||
limit: pagination.pageSize.toString(),
|
||||
_t: Date.now().toString(), // 防止缓存
|
||||
refresh: 'true'
|
||||
})
|
||||
|
||||
// 如果有搜索条件,添加到参数中
|
||||
if (searchValue.value.trim()) {
|
||||
params.append('search', searchValue.value.trim())
|
||||
console.log('搜索条件:', searchValue.value.trim())
|
||||
}
|
||||
|
||||
// 调用API获取智能主机数据
|
||||
const apiUrl = `/api/smart-devices/hosts?${params}`
|
||||
console.log('API请求URL:', apiUrl)
|
||||
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${localStorage.getItem('token')}`
|
||||
}
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
||||
const result = await response.json()
|
||||
console.log('API响应结果:', result)
|
||||
|
||||
if (result.success) {
|
||||
// 更新设备列表数据(后端已计算联网状态)
|
||||
hosts.value = result.data || []
|
||||
pagination.total = result.total || 0
|
||||
|
||||
console.log('更新后的设备列表:', hosts.value)
|
||||
console.log('总数:', pagination.total)
|
||||
|
||||
if (showMessage) {
|
||||
const searchText = searchValue.value.trim() ? `搜索"${searchValue.value.trim()}"` : '加载'
|
||||
message.success(`${searchText}完成,共找到 ${pagination.total} 条主机数据`)
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(result.message || '获取数据失败')
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取数据失败:', error)
|
||||
if (showMessage) {
|
||||
message.error('获取数据失败: ' + error.message)
|
||||
}
|
||||
|
||||
// 如果API失败,显示空数据
|
||||
hosts.value = []
|
||||
pagination.total = 0
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 更新搜索值
|
||||
const updateSearchValue = (e) => {
|
||||
const newValue = e.target.value
|
||||
console.log('输入框值变化:', newValue)
|
||||
console.log('更新前searchValue:', searchValue.value)
|
||||
searchValue.value = newValue
|
||||
console.log('更新后searchValue:', searchValue.value)
|
||||
}
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = () => {
|
||||
console.log('执行搜索,当前搜索值:', searchValue.value)
|
||||
pagination.current = 1
|
||||
fetchData(true)
|
||||
}
|
||||
|
||||
// 清除搜索
|
||||
const handleClearSearch = () => {
|
||||
searchValue.value = ''
|
||||
pagination.current = 1
|
||||
fetchData(true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 自动刷新功能
|
||||
const startAutoRefresh = () => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
}
|
||||
|
||||
if (autoRefresh.value) {
|
||||
refreshInterval.value = setInterval(() => {
|
||||
fetchData(false) // 自动刷新时不显示成功消息
|
||||
}, 30000) // 每30秒自动刷新一次
|
||||
console.log('自动刷新已启动,每30秒更新一次数据')
|
||||
}
|
||||
}
|
||||
|
||||
// 停止自动刷新
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshInterval.value) {
|
||||
clearInterval(refreshInterval.value)
|
||||
refreshInterval.value = null
|
||||
console.log('自动刷新已停止')
|
||||
}
|
||||
}
|
||||
|
||||
// 查看定位
|
||||
const viewLocation = (record) => {
|
||||
if (record.longitude && record.latitude && record.longitude !== '0' && record.latitude !== '90') {
|
||||
currentLocation.value = {
|
||||
longitude: parseFloat(record.longitude),
|
||||
latitude: parseFloat(record.latitude),
|
||||
deviceNumber: record.deviceNumber,
|
||||
updateTime: record.updateTime
|
||||
}
|
||||
locationVisible.value = true
|
||||
|
||||
// 延迟初始化地图,确保DOM已渲染
|
||||
setTimeout(() => {
|
||||
initBaiduMap()
|
||||
}, 100)
|
||||
} else {
|
||||
message.warning('该设备暂无有效定位信息')
|
||||
}
|
||||
}
|
||||
|
||||
// 查看采集信息
|
||||
const viewCollectionInfo = (record) => {
|
||||
currentHost.value = record
|
||||
collectionInfoVisible.value = true
|
||||
}
|
||||
|
||||
// 取消定位信息
|
||||
const handleLocationCancel = () => {
|
||||
locationVisible.value = false
|
||||
currentLocation.value = null
|
||||
if (baiduMap.value) {
|
||||
baiduMap.value = null
|
||||
}
|
||||
if (locationMarker.value) {
|
||||
locationMarker.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 取消采集信息
|
||||
const handleCollectionInfoCancel = () => {
|
||||
collectionInfoVisible.value = false
|
||||
currentHost.value = null
|
||||
}
|
||||
|
||||
// 初始化百度地图
|
||||
const initBaiduMap = async () => {
|
||||
if (!currentLocation.value) return
|
||||
|
||||
try {
|
||||
// 确保百度地图API已加载
|
||||
await loadBMapScript()
|
||||
|
||||
// 获取地图容器
|
||||
const mapContainer = document.getElementById('locationMap')
|
||||
if (!mapContainer) {
|
||||
message.error('地图容器不存在')
|
||||
return
|
||||
}
|
||||
|
||||
// 创建地图实例
|
||||
const map = await createMap(mapContainer, {
|
||||
center: new window.BMap.Point(currentLocation.value.longitude, currentLocation.value.latitude),
|
||||
zoom: 15
|
||||
})
|
||||
|
||||
baiduMap.value = map
|
||||
|
||||
// 添加设备位置标记 - 使用红色大头针样式
|
||||
const point = new window.BMap.Point(currentLocation.value.longitude, currentLocation.value.latitude)
|
||||
|
||||
// 创建自定义标记图标
|
||||
const icon = new window.BMap.Icon(
|
||||
'data:image/svg+xml;base64,' + btoa(`
|
||||
<svg width="32" height="48" viewBox="0 0 32 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16 0C7.163 0 0 7.163 0 16c0 16 16 32 16 32s16-16 16-32C32 7.163 24.837 0 16 0z" fill="#ff0000"/>
|
||||
<circle cx="16" cy="16" r="8" fill="#ffffff"/>
|
||||
</svg>
|
||||
`),
|
||||
new window.BMap.Size(32, 48),
|
||||
{
|
||||
anchor: new window.BMap.Size(16, 48)
|
||||
}
|
||||
)
|
||||
|
||||
const marker = new window.BMap.Marker(point, { icon: icon })
|
||||
map.addOverlay(marker)
|
||||
locationMarker.value = marker
|
||||
|
||||
// 创建时间戳标签,固定在红色大头针下方中间
|
||||
const label = new window.BMap.Label(currentLocation.value.updateTime, {
|
||||
position: point,
|
||||
offset: new window.BMap.Size(-50, 30) // 水平居中偏移-50像素,向下偏移30像素
|
||||
})
|
||||
|
||||
// 设置标签样式
|
||||
label.setStyle({
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.7)',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
padding: '4px 8px',
|
||||
fontSize: '12px',
|
||||
fontWeight: 'normal',
|
||||
whiteSpace: 'nowrap',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.3)',
|
||||
zIndex: 999,
|
||||
textAlign: 'center',
|
||||
width: 'auto',
|
||||
minWidth: '120px'
|
||||
})
|
||||
|
||||
map.addOverlay(label)
|
||||
|
||||
// 创建信息窗口
|
||||
const infoWindow = new window.BMap.InfoWindow(`
|
||||
<div style="padding: 10px; font-size: 14px;">
|
||||
<div style="font-weight: bold; margin-bottom: 5px;">主机位置</div>
|
||||
<div>设备编号: ${currentLocation.value.deviceNumber}</div>
|
||||
<div>经度: ${currentLocation.value.longitude}</div>
|
||||
<div>纬度: ${currentLocation.value.latitude}</div>
|
||||
<div style="margin-top: 5px; color: #666;">
|
||||
最后定位时间: ${currentLocation.value.updateTime}
|
||||
</div>
|
||||
</div>
|
||||
`, {
|
||||
width: 200,
|
||||
height: 120
|
||||
})
|
||||
|
||||
// 点击标记显示信息窗口
|
||||
marker.addEventListener('click', () => {
|
||||
map.openInfoWindow(infoWindow, point)
|
||||
})
|
||||
|
||||
// 启用地图控件
|
||||
map.addControl(new window.BMap.NavigationControl())
|
||||
map.addControl(new window.BMap.ScaleControl())
|
||||
map.addControl(new window.BMap.OverviewMapControl())
|
||||
map.addControl(new window.BMap.MapTypeControl())
|
||||
|
||||
// 启用滚轮缩放
|
||||
map.enableScrollWheelZoom(true)
|
||||
|
||||
// 设置地图样式
|
||||
switchMapStyle(mapStyle.value)
|
||||
|
||||
console.log('定位地图初始化成功')
|
||||
|
||||
} catch (error) {
|
||||
console.error('初始化百度地图失败:', error)
|
||||
message.error('地图初始化失败: ' + error.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 切换地图样式
|
||||
const switchMapStyle = (style) => {
|
||||
mapStyle.value = style
|
||||
|
||||
if (!baiduMap.value) return
|
||||
|
||||
try {
|
||||
if (style === 'normal') {
|
||||
// 普通地图
|
||||
baiduMap.value.setMapType(window.BMAP_NORMAL_MAP)
|
||||
} else if (style === 'hybrid') {
|
||||
// 混合地图(卫星图+路网)
|
||||
baiduMap.value.setMapType(window.BMAP_HYBRID_MAP)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('切换地图样式失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 表格变化处理
|
||||
const handleTableChange = (pag) => {
|
||||
pagination.current = pag.current
|
||||
pagination.pageSize = pag.pageSize
|
||||
fetchData()
|
||||
}
|
||||
|
||||
// 导出数据
|
||||
const exportData = async () => {
|
||||
try {
|
||||
if (!hosts.value || hosts.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportDeviceData(hosts.value, 'host')
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载时获取数据并启动自动刷新
|
||||
onMounted(() => {
|
||||
fetchData(true)
|
||||
startAutoRefresh()
|
||||
})
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
stopAutoRefresh()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.smart-host-container {
|
||||
padding: 24px;
|
||||
background: #fff;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* 页面标题样式 */
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #52c41a;
|
||||
margin: 0;
|
||||
padding: 12px 20px;
|
||||
background: #f6ffed;
|
||||
border: 1px solid #b7eb8f;
|
||||
border-radius: 6px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 搜索栏样式 */
|
||||
.search-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.search-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 200px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.search-button {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.export-button {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* 表格容器样式 */
|
||||
.table-container {
|
||||
background: #fff;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.host-table {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 表格单元格样式 */
|
||||
.device-number {
|
||||
color: #1890ff;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.battery-cell,
|
||||
.signal-cell,
|
||||
.temperature-cell,
|
||||
.update-time-cell,
|
||||
.network-status-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.battery-value,
|
||||
.signal-value,
|
||||
.temperature-value,
|
||||
.update-time-value {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.network-status-tag {
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
.action-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-link {
|
||||
color: #1890ff;
|
||||
font-size: 12px;
|
||||
padding: 0;
|
||||
height: auto;
|
||||
line-height: 1.2;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.action-link:hover {
|
||||
color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 定位信息模态框样式 */
|
||||
.location-modal {
|
||||
height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
}
|
||||
|
||||
.baidu-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.map-style-controls {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
display: flex;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
overflow: hidden;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.style-btn {
|
||||
padding: 8px 16px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
border: 1px solid #d9d9d9;
|
||||
background: white;
|
||||
transition: all 0.3s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.style-btn:first-child {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.style-btn:hover {
|
||||
color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
.style-btn.active {
|
||||
color: #fff;
|
||||
background-color: #1890ff;
|
||||
border-color: #1890ff;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.location-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background-color: #f5f5f5;
|
||||
border-color: #d9d9d9;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.cancel-btn:hover {
|
||||
background-color: #e6e6e6;
|
||||
border-color: #bfbfbf;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* 采集信息模态框样式 */
|
||||
.collection-info-modal {
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px 24px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item .label {
|
||||
min-width: 100px;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
.info-item .value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* 表格样式优化 */
|
||||
.host-table :deep(.ant-table) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.host-table :deep(.ant-table-thead > tr > th) {
|
||||
padding: 8px 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.host-table :deep(.ant-table-tbody > tr > td) {
|
||||
padding: 6px 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.host-table :deep(.ant-table-tbody > tr:hover > td) {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
/* 确保表格不会超出容器 */
|
||||
.host-table :deep(.ant-table-wrapper) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.host-table :deep(.ant-table-container) {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
||||
924
admin-system/frontend/src/views/System.vue
Normal file
924
admin-system/frontend/src/views/System.vue
Normal file
@@ -0,0 +1,924 @@
|
||||
<template>
|
||||
<div class="system-page">
|
||||
<a-page-header
|
||||
title="系统管理"
|
||||
sub-title="系统配置和权限管理"
|
||||
>
|
||||
<template #extra>
|
||||
<a-space>
|
||||
<a-button @click="fetchData">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="initSystem" :loading="initLoading">
|
||||
<template #icon><SettingOutlined /></template>
|
||||
初始化系统
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<div class="system-content">
|
||||
<!-- 系统统计卡片 -->
|
||||
<a-row :gutter="16" style="margin-bottom: 24px;">
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="系统配置"
|
||||
:value="systemStats.configs?.total || 0"
|
||||
:value-style="{ color: '#1890ff' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<SettingOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="菜单权限"
|
||||
:value="systemStats.menus?.total || 0"
|
||||
:value-style="{ color: '#52c41a' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<MenuOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="系统用户"
|
||||
:value="systemStats.users?.total || 0"
|
||||
:value-style="{ color: '#fa8c16' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :span="6">
|
||||
<a-card>
|
||||
<a-statistic
|
||||
title="用户角色"
|
||||
:value="systemStats.roles?.total || 0"
|
||||
:value-style="{ color: '#722ed1' }"
|
||||
>
|
||||
<template #prefix>
|
||||
<TeamOutlined />
|
||||
</template>
|
||||
</a-statistic>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<!-- 功能标签页 -->
|
||||
<a-tabs v-model:activeKey="activeTab" type="card">
|
||||
<!-- 系统配置管理 -->
|
||||
<a-tab-pane key="configs" tab="系统配置">
|
||||
<div class="config-section">
|
||||
<div class="section-header">
|
||||
<a-space>
|
||||
<a-select
|
||||
v-model:value="selectedCategory"
|
||||
placeholder="选择配置分类"
|
||||
style="width: 200px;"
|
||||
@change="filterConfigs"
|
||||
allow-clear
|
||||
>
|
||||
<a-select-option value="">全部分类</a-select-option>
|
||||
<a-select-option
|
||||
v-for="category in categories"
|
||||
:key="category"
|
||||
:value="category"
|
||||
>
|
||||
{{ getCategoryName(category) }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
<a-button type="primary" @click="showAddConfigModal = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加配置
|
||||
</a-button>
|
||||
<a-button @click="batchSaveConfigs" :loading="batchSaveLoading">
|
||||
<template #icon><SaveOutlined /></template>
|
||||
批量保存
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="configColumns"
|
||||
:data-source="filteredConfigs"
|
||||
:loading="configLoading"
|
||||
:pagination="{ pageSize: 15 }"
|
||||
row-key="id"
|
||||
size="small"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'config_value'">
|
||||
<div v-if="record.config_type === 'boolean'">
|
||||
<a-switch
|
||||
:checked="record.parsed_value"
|
||||
@change="updateConfigValue(record, $event)"
|
||||
:disabled="!record.is_editable"
|
||||
/>
|
||||
</div>
|
||||
<div v-else-if="record.config_type === 'number'">
|
||||
<a-input-number
|
||||
:value="record.parsed_value"
|
||||
@change="updateConfigValue(record, $event)"
|
||||
:disabled="!record.is_editable"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</div>
|
||||
<div v-else>
|
||||
<a-input
|
||||
:value="record.config_value"
|
||||
@change="updateConfigValue(record, $event.target.value)"
|
||||
:disabled="!record.is_editable"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'category'">
|
||||
<a-tag :color="getCategoryColor(record.category)">
|
||||
{{ getCategoryName(record.category) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'is_public'">
|
||||
<a-tag :color="record.is_public ? 'green' : 'orange'">
|
||||
{{ record.is_public ? '公开' : '私有' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'is_editable'">
|
||||
<a-tag :color="record.is_editable ? 'blue' : 'red'">
|
||||
{{ record.is_editable ? '可编辑' : '只读' }}
|
||||
</a-tag>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space size="small">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editConfig(record)"
|
||||
:disabled="!record.is_editable"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
size="small"
|
||||
@click="resetConfig(record)"
|
||||
:disabled="!record.is_editable"
|
||||
>
|
||||
重置
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
size="small"
|
||||
@click="deleteConfig(record)"
|
||||
:disabled="!record.is_editable"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
|
||||
<!-- 菜单权限管理 -->
|
||||
<a-tab-pane key="menus" tab="菜单权限">
|
||||
<div class="menu-section">
|
||||
<div class="section-header">
|
||||
<a-space>
|
||||
<a-button @click="fetchMenus">
|
||||
<template #icon><ReloadOutlined /></template>
|
||||
刷新菜单
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddMenuModal = true">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加菜单
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
:columns="menuColumns"
|
||||
:data-source="menus"
|
||||
:loading="menuLoading"
|
||||
:pagination="false"
|
||||
row-key="id"
|
||||
size="small"
|
||||
:default-expand-all-rows="true"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'menu_name'">
|
||||
<a-space>
|
||||
<component
|
||||
:is="getIconComponent(record.icon)"
|
||||
v-if="record.icon"
|
||||
style="color: #1890ff;"
|
||||
/>
|
||||
{{ record.menu_name }}
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'required_roles'">
|
||||
<a-space wrap>
|
||||
<a-tag
|
||||
v-for="role in parseRoles(record.required_roles)"
|
||||
:key="role"
|
||||
:color="getRoleColor(role)"
|
||||
>
|
||||
{{ getRoleName(role) }}
|
||||
</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'is_visible'">
|
||||
<a-switch
|
||||
:checked="record.is_visible"
|
||||
@change="updateMenuVisible(record, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'is_enabled'">
|
||||
<a-switch
|
||||
:checked="record.is_enabled"
|
||||
@change="updateMenuEnabled(record, $event)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-else-if="column.dataIndex === 'action'">
|
||||
<a-space size="small">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
@click="editMenu(record)"
|
||||
>
|
||||
编辑
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
danger
|
||||
size="small"
|
||||
@click="deleteMenu(record)"
|
||||
>
|
||||
删除
|
||||
</a-button>
|
||||
</a-space>
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</a-tab-pane>
|
||||
</a-tabs>
|
||||
</div>
|
||||
|
||||
<!-- 添加配置模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showAddConfigModal"
|
||||
title="添加系统配置"
|
||||
:confirm-loading="configSubmitLoading"
|
||||
@ok="saveConfig"
|
||||
@cancel="resetConfigForm"
|
||||
>
|
||||
<a-form
|
||||
ref="configFormRef"
|
||||
:model="configForm"
|
||||
:rules="configRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="配置键名" name="config_key">
|
||||
<a-input v-model:value="configForm.config_key" placeholder="例如: system.name" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置值" name="config_value">
|
||||
<a-textarea v-model:value="configForm.config_value" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置分类" name="category">
|
||||
<a-select v-model:value="configForm.category">
|
||||
<a-select-option value="general">通用配置</a-select-option>
|
||||
<a-select-option value="ui">界面配置</a-select-option>
|
||||
<a-select-option value="security">安全配置</a-select-option>
|
||||
<a-select-option value="notification">通知配置</a-select-option>
|
||||
<a-select-option value="monitoring">监控配置</a-select-option>
|
||||
<a-select-option value="report">报表配置</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置描述" name="description">
|
||||
<a-input v-model:value="configForm.description" />
|
||||
</a-form-item>
|
||||
|
||||
<a-row :gutter="16">
|
||||
<a-col :span="12">
|
||||
<a-form-item name="is_public">
|
||||
<a-checkbox v-model:checked="configForm.is_public">
|
||||
公开配置(前端可访问)
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item name="is_editable">
|
||||
<a-checkbox v-model:checked="configForm.is_editable">
|
||||
允许编辑
|
||||
</a-checkbox>
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
</a-row>
|
||||
|
||||
<a-form-item label="排序顺序" name="sort_order">
|
||||
<a-input-number
|
||||
v-model:value="configForm.sort_order"
|
||||
:min="0"
|
||||
style="width: 100%;"
|
||||
/>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
|
||||
<!-- 编辑配置模态框 -->
|
||||
<a-modal
|
||||
v-model:open="showEditConfigModal"
|
||||
title="编辑系统配置"
|
||||
:confirm-loading="configSubmitLoading"
|
||||
@ok="saveConfig"
|
||||
@cancel="resetConfigForm"
|
||||
>
|
||||
<a-form
|
||||
ref="configFormRef"
|
||||
:model="configForm"
|
||||
:rules="configRules"
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="配置键名">
|
||||
<a-input :value="configForm.config_key" disabled />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置值" name="config_value">
|
||||
<a-textarea v-model:value="configForm.config_value" :rows="3" />
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item label="配置描述" name="description">
|
||||
<a-input v-model:value="configForm.description" />
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, computed } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import {
|
||||
ReloadOutlined,
|
||||
SettingOutlined,
|
||||
PlusOutlined,
|
||||
SaveOutlined,
|
||||
MenuOutlined,
|
||||
UserOutlined,
|
||||
TeamOutlined
|
||||
} from '@ant-design/icons-vue'
|
||||
import { api } from '../utils/api'
|
||||
import * as Icons from '@ant-design/icons-vue'
|
||||
|
||||
// 响应式数据
|
||||
const loading = ref(false)
|
||||
const configLoading = ref(false)
|
||||
const menuLoading = ref(false)
|
||||
const initLoading = ref(false)
|
||||
const configSubmitLoading = ref(false)
|
||||
const batchSaveLoading = ref(false)
|
||||
|
||||
const activeTab = ref('configs')
|
||||
const systemStats = ref({})
|
||||
const configs = ref([])
|
||||
const menus = ref([])
|
||||
const categories = ref([])
|
||||
const selectedCategory = ref('')
|
||||
|
||||
// 模态框状态
|
||||
const showAddConfigModal = ref(false)
|
||||
const showEditConfigModal = ref(false)
|
||||
const showAddMenuModal = ref(false)
|
||||
|
||||
// 表单引用
|
||||
const configFormRef = ref()
|
||||
|
||||
// 配置表单数据
|
||||
const configForm = reactive({
|
||||
id: null,
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
category: 'general',
|
||||
description: '',
|
||||
is_public: false,
|
||||
is_editable: true,
|
||||
sort_order: 0
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
const configRules = {
|
||||
config_key: [{ required: true, message: '请输入配置键名' }],
|
||||
config_value: [{ required: true, message: '请输入配置值' }],
|
||||
category: [{ required: true, message: '请选择配置分类' }]
|
||||
}
|
||||
|
||||
// 过滤后的配置列表
|
||||
const filteredConfigs = computed(() => {
|
||||
if (!selectedCategory.value) {
|
||||
return configs.value
|
||||
}
|
||||
return configs.value.filter(config => config.category === selectedCategory.value)
|
||||
})
|
||||
|
||||
// 配置表格列定义
|
||||
const configColumns = [
|
||||
{
|
||||
title: '配置键名',
|
||||
dataIndex: 'config_key',
|
||||
key: 'config_key',
|
||||
width: 200,
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '配置值',
|
||||
dataIndex: 'config_value',
|
||||
key: 'config_value',
|
||||
width: 250
|
||||
},
|
||||
{
|
||||
title: '分类',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
key: 'description',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '公开',
|
||||
dataIndex: 'is_public',
|
||||
key: 'is_public',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '可编辑',
|
||||
dataIndex: 'is_editable',
|
||||
key: 'is_editable',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 180,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 菜单表格列定义
|
||||
const menuColumns = [
|
||||
{
|
||||
title: '菜单名称',
|
||||
dataIndex: 'menu_name',
|
||||
key: 'menu_name',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '菜单路径',
|
||||
dataIndex: 'menu_path',
|
||||
key: 'menu_path',
|
||||
width: 150
|
||||
},
|
||||
{
|
||||
title: '所需角色',
|
||||
dataIndex: 'required_roles',
|
||||
key: 'required_roles',
|
||||
width: 200
|
||||
},
|
||||
{
|
||||
title: '排序',
|
||||
dataIndex: 'sort_order',
|
||||
key: 'sort_order',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '可见',
|
||||
dataIndex: 'is_visible',
|
||||
key: 'is_visible',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '启用',
|
||||
dataIndex: 'is_enabled',
|
||||
key: 'is_enabled',
|
||||
width: 80
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
dataIndex: 'action',
|
||||
key: 'action',
|
||||
width: 120,
|
||||
fixed: 'right'
|
||||
}
|
||||
]
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
// 获取所有数据
|
||||
async function fetchData() {
|
||||
await Promise.all([
|
||||
fetchSystemStats(),
|
||||
fetchConfigs(),
|
||||
fetchMenus(),
|
||||
fetchCategories()
|
||||
])
|
||||
}
|
||||
|
||||
// 获取系统统计信息
|
||||
async function fetchSystemStats() {
|
||||
try {
|
||||
const response = await api.get('/system/stats')
|
||||
if (response.success) {
|
||||
systemStats.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统统计失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 获取系统配置
|
||||
async function fetchConfigs() {
|
||||
configLoading.value = true
|
||||
try {
|
||||
const response = await api.get('/system/configs')
|
||||
if (response.success) {
|
||||
configs.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取系统配置失败:', error)
|
||||
message.error('获取系统配置失败')
|
||||
} finally {
|
||||
configLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取菜单权限
|
||||
async function fetchMenus() {
|
||||
menuLoading.value = true
|
||||
try {
|
||||
const response = await api.get('/system/menus')
|
||||
if (response.success) {
|
||||
menus.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取菜单权限失败:', error)
|
||||
message.error('获取菜单权限失败')
|
||||
} finally {
|
||||
menuLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取配置分类
|
||||
async function fetchCategories() {
|
||||
try {
|
||||
const response = await api.get('/system/configs/categories')
|
||||
if (response.success) {
|
||||
categories.value = response.data
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取配置分类失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化系统
|
||||
async function initSystem() {
|
||||
initLoading.value = true
|
||||
try {
|
||||
const response = await api.post('/system/init')
|
||||
if (response.success) {
|
||||
message.success('系统初始化成功')
|
||||
await fetchData()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('系统初始化失败:', error)
|
||||
message.error('系统初始化失败')
|
||||
} finally {
|
||||
initLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 过滤配置
|
||||
function filterConfigs() {
|
||||
// 响应式计算属性会自动处理过滤
|
||||
}
|
||||
|
||||
// 更新配置值
|
||||
function updateConfigValue(config, value) {
|
||||
const index = configs.value.findIndex(c => c.id === config.id)
|
||||
if (index !== -1) {
|
||||
configs.value[index].config_value = String(value)
|
||||
configs.value[index].parsed_value = value
|
||||
configs.value[index]._changed = true // 标记为已修改
|
||||
}
|
||||
}
|
||||
|
||||
// 批量保存配置
|
||||
async function batchSaveConfigs() {
|
||||
const changedConfigs = configs.value.filter(config => config._changed)
|
||||
|
||||
if (changedConfigs.length === 0) {
|
||||
message.info('没有配置需要保存')
|
||||
return
|
||||
}
|
||||
|
||||
batchSaveLoading.value = true
|
||||
try {
|
||||
const configsToUpdate = changedConfigs.map(config => ({
|
||||
config_key: config.config_key,
|
||||
config_value: config.parsed_value
|
||||
}))
|
||||
|
||||
const response = await api.put('/system/configs/batch', {
|
||||
configs: configsToUpdate
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success(`成功保存 ${changedConfigs.length} 个配置`)
|
||||
await fetchConfigs() // 重新获取数据
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('批量保存配置失败:', error)
|
||||
message.error('批量保存配置失败')
|
||||
} finally {
|
||||
batchSaveLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑配置
|
||||
function editConfig(record) {
|
||||
Object.assign(configForm, {
|
||||
id: record.id,
|
||||
config_key: record.config_key,
|
||||
config_value: record.config_value,
|
||||
category: record.category,
|
||||
description: record.description,
|
||||
is_public: record.is_public,
|
||||
is_editable: record.is_editable,
|
||||
sort_order: record.sort_order
|
||||
})
|
||||
showEditConfigModal.value = true
|
||||
}
|
||||
|
||||
// 保存配置
|
||||
async function saveConfig() {
|
||||
try {
|
||||
await configFormRef.value.validate()
|
||||
configSubmitLoading.value = true
|
||||
|
||||
if (configForm.id) {
|
||||
// 更新配置
|
||||
const response = await api.put(`/system/configs/${configForm.id}`, {
|
||||
config_value: configForm.config_value,
|
||||
description: configForm.description
|
||||
})
|
||||
|
||||
if (response.success) {
|
||||
message.success('配置更新成功')
|
||||
showEditConfigModal.value = false
|
||||
await fetchConfigs()
|
||||
}
|
||||
} else {
|
||||
// 创建配置
|
||||
const response = await api.post('/system/configs', configForm)
|
||||
|
||||
if (response.success) {
|
||||
message.success('配置创建成功')
|
||||
showAddConfigModal.value = false
|
||||
await fetchConfigs()
|
||||
}
|
||||
}
|
||||
|
||||
resetConfigForm()
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error)
|
||||
message.error('保存配置失败')
|
||||
} finally {
|
||||
configSubmitLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置表单
|
||||
function resetConfigForm() {
|
||||
Object.assign(configForm, {
|
||||
id: null,
|
||||
config_key: '',
|
||||
config_value: '',
|
||||
category: 'general',
|
||||
description: '',
|
||||
is_public: false,
|
||||
is_editable: true,
|
||||
sort_order: 0
|
||||
})
|
||||
configFormRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 删除配置
|
||||
async function deleteConfig(record) {
|
||||
try {
|
||||
const response = await api.delete(`/system/configs/${record.id}`)
|
||||
if (response.success) {
|
||||
message.success('配置删除成功')
|
||||
await fetchConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('删除配置失败:', error)
|
||||
message.error('删除配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 重置配置
|
||||
async function resetConfig(record) {
|
||||
try {
|
||||
const response = await api.post(`/system/configs/${record.id}/reset`)
|
||||
if (response.success) {
|
||||
message.success('配置重置成功')
|
||||
await fetchConfigs()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('重置配置失败:', error)
|
||||
message.error('重置配置失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 工具函数
|
||||
function getCategoryName(category) {
|
||||
const categoryMap = {
|
||||
general: '通用配置',
|
||||
ui: '界面配置',
|
||||
security: '安全配置',
|
||||
notification: '通知配置',
|
||||
monitoring: '监控配置',
|
||||
report: '报表配置'
|
||||
}
|
||||
return categoryMap[category] || category
|
||||
}
|
||||
|
||||
function getCategoryColor(category) {
|
||||
const colorMap = {
|
||||
general: 'blue',
|
||||
ui: 'green',
|
||||
security: 'red',
|
||||
notification: 'orange',
|
||||
monitoring: 'purple',
|
||||
report: 'cyan'
|
||||
}
|
||||
return colorMap[category] || 'default'
|
||||
}
|
||||
|
||||
function getIconComponent(iconName) {
|
||||
if (!iconName) return null
|
||||
const iconKey = iconName.split('-').map(part =>
|
||||
part.charAt(0).toUpperCase() + part.slice(1)
|
||||
).join('')
|
||||
return Icons[iconKey] || Icons.SettingOutlined
|
||||
}
|
||||
|
||||
function parseRoles(rolesStr) {
|
||||
try {
|
||||
return rolesStr ? JSON.parse(rolesStr) : []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
function getRoleColor(role) {
|
||||
const colorMap = {
|
||||
admin: 'red',
|
||||
manager: 'orange',
|
||||
user: 'blue'
|
||||
}
|
||||
return colorMap[role] || 'default'
|
||||
}
|
||||
|
||||
function getRoleName(role) {
|
||||
const nameMap = {
|
||||
admin: '管理员',
|
||||
manager: '管理者',
|
||||
user: '普通用户'
|
||||
}
|
||||
return nameMap[role] || role
|
||||
}
|
||||
|
||||
// 菜单相关方法(占位符)
|
||||
function editMenu(record) {
|
||||
message.info('菜单编辑功能开发中')
|
||||
}
|
||||
|
||||
function deleteMenu(record) {
|
||||
message.info('菜单删除功能开发中')
|
||||
}
|
||||
|
||||
function updateMenuVisible(record, visible) {
|
||||
message.info('菜单可见性更新功能开发中')
|
||||
}
|
||||
|
||||
function updateMenuEnabled(record, enabled) {
|
||||
message.info('菜单启用状态更新功能开发中')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.system-page {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.system-content {
|
||||
padding: 24px;
|
||||
background: #f0f2f5;
|
||||
min-height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.section-header {
|
||||
margin-bottom: 16px;
|
||||
padding: 16px;
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ant-card {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.ant-page-header {
|
||||
background: #fff;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
:deep(.ant-table-thead > tr > th) {
|
||||
background: #fafafa;
|
||||
color: #262626;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-tab) {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
:deep(.ant-tabs-card .ant-tabs-content) {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.config-section,
|
||||
.menu-section {
|
||||
background: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 移动端优化 */
|
||||
@media (max-width: 768px) {
|
||||
.system-content {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
:deep(.ant-col) {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-title) {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
:deep(.ant-statistic-content-value) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -33,7 +33,7 @@
|
||||
<script setup>
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import api from '@/utils/api'
|
||||
import { api } from '@/utils/api'
|
||||
|
||||
// 响应式数据
|
||||
const tableCanvas = ref(null)
|
||||
|
||||
13
admin-system/frontend/src/views/TestImport.vue
Normal file
13
admin-system/frontend/src/views/TestImport.vue
Normal file
@@ -0,0 +1,13 @@
|
||||
<template>
|
||||
<div>
|
||||
<h1>测试导入</h1>
|
||||
<p>API 对象: {{ api ? '已加载' : '未加载' }}</p>
|
||||
<p>电子围栏方法: {{ api?.electronicFence ? '已加载' : '未加载' }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { api } from '@/utils/api'
|
||||
|
||||
console.log('API 对象:', api)
|
||||
</script>
|
||||
@@ -1,11 +1,42 @@
|
||||
<template>
|
||||
<div>
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<div class="page-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
|
||||
<h1>用户管理</h1>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加用户
|
||||
</a-button>
|
||||
<a-space class="page-actions">
|
||||
<a-button @click="exportUsers" :loading="exportLoading">
|
||||
<template #icon><ExportOutlined /></template>
|
||||
导出数据
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddModal">
|
||||
<template #icon><PlusOutlined /></template>
|
||||
添加用户
|
||||
</a-button>
|
||||
</a-space>
|
||||
</div>
|
||||
|
||||
<!-- 搜索区域 -->
|
||||
<div class="search-area" style="margin-bottom: 16px; display: flex; gap: 8px; align-items: center;">
|
||||
<a-input
|
||||
v-model="searchUsername"
|
||||
placeholder="请输入用户名进行搜索"
|
||||
class="search-input"
|
||||
style="width: 300px;"
|
||||
@input="handleSearchInput"
|
||||
@focus="handleSearchFocus"
|
||||
@blur="handleSearchBlur"
|
||||
@change="handleSearchChange"
|
||||
@press-enter="searchUsers"
|
||||
/>
|
||||
<div class="search-buttons">
|
||||
<a-button type="primary" @click="searchUsers" :loading="searchLoading">
|
||||
<template #icon><SearchOutlined /></template>
|
||||
查询
|
||||
</a-button>
|
||||
<a-button @click="resetSearch" :disabled="!isSearching">
|
||||
重置
|
||||
</a-button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<a-table
|
||||
@@ -16,9 +47,9 @@
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.dataIndex === 'role'">
|
||||
<a-tag :color="record.role === 'admin' ? 'red' : 'blue'">
|
||||
{{ record.role === 'admin' ? '管理员' : '普通用户' }}
|
||||
<template v-if="column.dataIndex === 'roles'">
|
||||
<a-tag :color="getRoleColor(record.roleName || record.role?.name)">
|
||||
{{ getRoleDisplayName(record.roleName || record.role?.name) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-else-if="column.dataIndex === 'created_at'">
|
||||
@@ -40,7 +71,8 @@
|
||||
|
||||
<!-- 添加/编辑用户模态框 -->
|
||||
<a-modal
|
||||
v-model:open="modalVisible"
|
||||
:open="modalVisible"
|
||||
@update:open="(val) => modalVisible = val"
|
||||
:title="isEdit ? '编辑用户' : '添加用户'"
|
||||
@ok="handleSubmit"
|
||||
@cancel="handleCancel"
|
||||
@@ -53,18 +85,23 @@
|
||||
layout="vertical"
|
||||
>
|
||||
<a-form-item label="用户名" name="username">
|
||||
<a-input v-model:value="formData.username" placeholder="请输入用户名" :disabled="isEdit" />
|
||||
<a-input v-model="formData.username" placeholder="请输入用户名" :disabled="isEdit" />
|
||||
</a-form-item>
|
||||
<a-form-item label="邮箱" name="email">
|
||||
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
|
||||
<a-input v-model="formData.email" placeholder="请输入邮箱" />
|
||||
</a-form-item>
|
||||
<a-form-item label="密码" name="password" v-if="!isEdit">
|
||||
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
|
||||
<a-input-password v-model="formData.password" placeholder="请输入密码" />
|
||||
</a-form-item>
|
||||
<a-form-item label="角色" name="role">
|
||||
<a-select v-model:value="formData.role" placeholder="请选择角色">
|
||||
<a-select-option value="user">普通用户</a-select-option>
|
||||
<a-select-option value="admin">管理员</a-select-option>
|
||||
<a-form-item label="角色" name="roles">
|
||||
<a-select v-model="formData.roles" placeholder="请选择角色" :loading="rolesLoading">
|
||||
<a-select-option
|
||||
v-for="role in availableRoles"
|
||||
:key="role.id"
|
||||
:value="role.id"
|
||||
>
|
||||
{{ role.description || role.name }}
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -73,10 +110,11 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { PlusOutlined } from '@ant-design/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { PlusOutlined, SearchOutlined, ExportOutlined } from '@ant-design/icons-vue'
|
||||
import { api, directApi } from '../utils/api'
|
||||
import { ExportUtils } from '../utils/exportUtils'
|
||||
|
||||
// 响应式数据
|
||||
const users = ref([])
|
||||
@@ -86,13 +124,30 @@ const submitLoading = ref(false)
|
||||
const isEdit = ref(false)
|
||||
const formRef = ref()
|
||||
|
||||
// 搜索相关的响应式数据
|
||||
const searchUsername = ref('')
|
||||
const searchLoading = ref(false)
|
||||
const isSearching = ref(false)
|
||||
const usernameOptions = ref([])
|
||||
|
||||
// 搜索监听相关
|
||||
let searchTimeout = null
|
||||
let searchFocusTime = null
|
||||
|
||||
// 导出相关
|
||||
const exportLoading = ref(false)
|
||||
|
||||
// 角色相关
|
||||
const availableRoles = ref([])
|
||||
const rolesLoading = ref(false)
|
||||
|
||||
// 表单数据
|
||||
const formData = reactive({
|
||||
id: null,
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
roles: 2 // 默认为普通用户ID
|
||||
})
|
||||
|
||||
// 表单验证规则
|
||||
@@ -103,7 +158,7 @@ const rules = {
|
||||
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
|
||||
],
|
||||
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
|
||||
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
roles: [{ required: true, message: '请选择角色', trigger: 'change' }]
|
||||
}
|
||||
|
||||
// 表格列配置
|
||||
@@ -126,8 +181,8 @@ const columns = [
|
||||
},
|
||||
{
|
||||
title: '角色',
|
||||
dataIndex: 'role',
|
||||
key: 'role',
|
||||
dataIndex: 'roles',
|
||||
key: 'roles',
|
||||
width: 120
|
||||
},
|
||||
{
|
||||
@@ -150,14 +205,108 @@ const formatDate = (dateString) => {
|
||||
return new Date(dateString).toLocaleString('zh-CN')
|
||||
}
|
||||
|
||||
// 获取角色颜色
|
||||
const getRoleColor = (roleName) => {
|
||||
const colorMap = {
|
||||
'admin': 'red',
|
||||
'farm_manager': 'green',
|
||||
'inspector': 'orange',
|
||||
'user': 'blue'
|
||||
}
|
||||
return colorMap[roleName] || 'default'
|
||||
}
|
||||
|
||||
// 获取角色显示名称
|
||||
const getRoleDisplayName = (roleName) => {
|
||||
const role = availableRoles.value.find(r => r.name === roleName)
|
||||
if (role) {
|
||||
return role.description || role.name
|
||||
}
|
||||
|
||||
// 如果没有找到角色,使用默认映射
|
||||
const nameMap = {
|
||||
'admin': '系统管理员',
|
||||
'farm_manager': '养殖场管理员',
|
||||
'inspector': '监管人员',
|
||||
'user': '普通用户'
|
||||
}
|
||||
return nameMap[roleName] || roleName || '未知角色'
|
||||
}
|
||||
|
||||
// 获取角色列表
|
||||
const fetchRoles = async () => {
|
||||
try {
|
||||
rolesLoading.value = true
|
||||
console.log('开始获取角色列表...')
|
||||
|
||||
const { api } = await import('../utils/api')
|
||||
const response = await api.get('/auth/roles')
|
||||
|
||||
console.log('角色API响应:', response)
|
||||
|
||||
// 处理API响应格式
|
||||
// api.get() 已经处理了响应格式,直接返回 result.data
|
||||
let roles = []
|
||||
if (Array.isArray(response)) {
|
||||
// 直接是角色数组
|
||||
roles = response
|
||||
} else if (response && Array.isArray(response.data)) {
|
||||
roles = response.data
|
||||
} else if (response && Array.isArray(response.roles)) {
|
||||
roles = response.roles
|
||||
} else {
|
||||
console.warn('API返回角色数据格式异常:', response)
|
||||
// 如果是对象,尝试提取角色数据
|
||||
if (response && typeof response === 'object') {
|
||||
// 可能的字段名
|
||||
const possibleFields = ['data', 'roles', 'items', 'list']
|
||||
for (const field of possibleFields) {
|
||||
if (response[field] && Array.isArray(response[field])) {
|
||||
roles = response[field]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('处理后的角色列表:', roles)
|
||||
|
||||
if (roles.length > 0) {
|
||||
availableRoles.value = roles
|
||||
console.log('✅ 角色列表加载成功,数量:', roles.length)
|
||||
} else {
|
||||
throw new Error('角色列表为空')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取角色列表失败:', error)
|
||||
message.error('获取角色列表失败,使用默认角色')
|
||||
|
||||
// 如果API失败,使用默认角色
|
||||
availableRoles.value = [
|
||||
{ id: 1, name: 'admin', description: '系统管理员' },
|
||||
{ id: 2, name: 'user', description: '普通用户' },
|
||||
{ id: 32, name: 'farm_manager', description: '养殖场管理员' },
|
||||
{ id: 33, name: 'inspector', description: '监管人员' }
|
||||
]
|
||||
console.log('使用默认角色列表')
|
||||
} finally {
|
||||
rolesLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取用户列表
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
const { api } = await import('../utils/api')
|
||||
const response = await api.get('/users')
|
||||
// api.get已经处理了响应格式,直接返回data部分
|
||||
if (Array.isArray(response)) {
|
||||
console.log('用户API响应:', response)
|
||||
|
||||
// 检查响应格式
|
||||
if (response && response.success && Array.isArray(response.data)) {
|
||||
users.value = response.data
|
||||
} else if (Array.isArray(response)) {
|
||||
// 兼容旧格式
|
||||
users.value = response
|
||||
} else {
|
||||
users.value = []
|
||||
@@ -172,21 +321,58 @@ const fetchUsers = async () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 导出用户数据
|
||||
const exportUsers = async () => {
|
||||
try {
|
||||
if (!users.value || users.value.length === 0) {
|
||||
message.warning('没有数据可导出')
|
||||
return
|
||||
}
|
||||
|
||||
exportLoading.value = true
|
||||
message.loading('正在导出数据...', 0)
|
||||
|
||||
const result = ExportUtils.exportUserData(users.value)
|
||||
|
||||
if (result.success) {
|
||||
message.destroy()
|
||||
message.success(`导出成功!文件:${result.filename}`)
|
||||
} else {
|
||||
message.destroy()
|
||||
message.error(result.message)
|
||||
}
|
||||
} catch (error) {
|
||||
message.destroy()
|
||||
console.error('导出失败:', error)
|
||||
message.error('导出失败,请重试')
|
||||
} finally {
|
||||
exportLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加模态框
|
||||
const showAddModal = () => {
|
||||
const showAddModal = async () => {
|
||||
isEdit.value = false
|
||||
resetForm()
|
||||
modalVisible.value = true
|
||||
// 确保角色数据已加载
|
||||
if (availableRoles.value.length === 0) {
|
||||
await fetchRoles()
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑用户
|
||||
const editUser = (record) => {
|
||||
const editUser = async (record) => {
|
||||
isEdit.value = true
|
||||
Object.assign(formData, {
|
||||
...record,
|
||||
password: '' // 编辑时不显示密码
|
||||
})
|
||||
modalVisible.value = true
|
||||
// 确保角色数据已加载
|
||||
if (availableRoles.value.length === 0) {
|
||||
await fetchRoles()
|
||||
}
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
@@ -245,13 +431,325 @@ const resetForm = () => {
|
||||
username: '',
|
||||
email: '',
|
||||
password: '',
|
||||
role: 'user'
|
||||
roles: 2 // 默认为普通用户ID
|
||||
})
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
|
||||
// 搜索用户
|
||||
const searchUsers = async () => {
|
||||
const searchKeywordValue = searchUsername.value
|
||||
const searchStartTime = Date.now()
|
||||
|
||||
console.log('🔍 [用户搜索监听] 开始搜索:', {
|
||||
keyword: searchKeywordValue,
|
||||
keywordType: typeof searchKeywordValue,
|
||||
keywordLength: searchKeywordValue ? searchKeywordValue.length : 0,
|
||||
keywordTrimmed: searchKeywordValue ? searchKeywordValue.trim() : '',
|
||||
timestamp: new Date().toISOString(),
|
||||
searchStartTime: searchStartTime
|
||||
})
|
||||
|
||||
// 记录搜索开始日志
|
||||
await logUserAction('search_start', {
|
||||
keyword: searchKeywordValue,
|
||||
searchMethod: 'fetch_direct',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
if (!searchKeywordValue || searchKeywordValue.trim() === '') {
|
||||
console.log('🔄 [用户搜索监听] 搜索关键词为空,重新加载所有数据', {
|
||||
searchKeywordValue: searchKeywordValue,
|
||||
isFalsy: !searchKeywordValue,
|
||||
isEmpty: searchKeywordValue === '',
|
||||
isWhitespace: searchKeywordValue && searchKeywordValue.trim() === ''
|
||||
})
|
||||
await fetchUsers()
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
console.log('🔄 [用户搜索监听] 发送搜索请求到后端:', searchKeywordValue)
|
||||
searchLoading.value = true
|
||||
|
||||
const searchUrl = `/users/search?username=${encodeURIComponent(searchKeywordValue.trim())}`
|
||||
console.log('🌐 [用户搜索监听] 请求URL:', searchUrl)
|
||||
|
||||
// 获取认证token
|
||||
const userInfo = JSON.parse(localStorage.getItem('user') || '{}')
|
||||
const token = userInfo.token || localStorage.getItem('token')
|
||||
|
||||
console.log('🔐 [用户搜索监听] 认证信息:', {
|
||||
hasUserInfo: !!userInfo,
|
||||
hasToken: !!token,
|
||||
tokenLength: token ? token.length : 0,
|
||||
tokenPreview: token ? token.substring(0, 20) + '...' : 'none'
|
||||
})
|
||||
|
||||
if (!token) {
|
||||
console.error('❌ [用户搜索监听] 未找到认证token')
|
||||
message.error('未找到认证token,请重新登录')
|
||||
throw new Error('未找到认证token,请重新登录')
|
||||
}
|
||||
|
||||
// 检查token是否过期(简单检查)
|
||||
try {
|
||||
const tokenPayload = JSON.parse(atob(token.split('.')[1]))
|
||||
const currentTime = Math.floor(Date.now() / 1000)
|
||||
if (tokenPayload.exp && tokenPayload.exp < currentTime) {
|
||||
console.error('❌ [用户搜索监听] Token已过期')
|
||||
message.error('登录已过期,请重新登录')
|
||||
throw new Error('Token已过期,请重新登录')
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('⚠️ [用户搜索监听] Token解析失败:', e.message)
|
||||
}
|
||||
|
||||
// 使用API工具查询后端API
|
||||
const result = await api.get(searchUrl)
|
||||
|
||||
const responseTime = Date.now() - searchStartTime
|
||||
console.log('⏱️ [用户搜索监听] 后端响应时间:', responseTime + 'ms')
|
||||
console.log('📊 [用户搜索监听] 后端返回数据:', {
|
||||
success: result.success,
|
||||
dataCount: result.data ? result.data.length : 0,
|
||||
message: result.message
|
||||
})
|
||||
|
||||
if (result.success) {
|
||||
users.value = result.data
|
||||
isSearching.value = true
|
||||
|
||||
console.log(`✅ [用户搜索监听] 搜索成功,找到 ${users.value.length} 条记录`)
|
||||
message.success(`搜索完成,找到 ${users.value.length} 个用户`)
|
||||
|
||||
// 记录搜索成功日志
|
||||
await logUserAction('search_success', {
|
||||
keyword: searchKeywordValue,
|
||||
resultCount: users.value.length,
|
||||
searchMethod: 'fetch_direct',
|
||||
responseTime: responseTime,
|
||||
backendData: result.data
|
||||
})
|
||||
} else {
|
||||
throw new Error(result.message || '搜索失败')
|
||||
}
|
||||
} catch (error) {
|
||||
const errorTime = Date.now() - searchStartTime
|
||||
console.error('❌ [用户搜索监听] 搜索失败:', {
|
||||
error: error.message,
|
||||
keyword: searchKeywordValue,
|
||||
errorTime: errorTime
|
||||
})
|
||||
message.error('搜索失败: ' + (error.message || '网络错误'))
|
||||
|
||||
// 记录搜索失败日志
|
||||
await logUserAction('search_failed', {
|
||||
keyword: searchKeywordValue,
|
||||
error: error.message,
|
||||
errorTime: errorTime,
|
||||
searchMethod: 'fetch_direct'
|
||||
})
|
||||
} finally {
|
||||
searchLoading.value = false
|
||||
const totalTime = Date.now() - searchStartTime
|
||||
console.log('⏱️ [用户搜索监听] 搜索总耗时:', totalTime + 'ms')
|
||||
}
|
||||
}
|
||||
|
||||
// 监听searchUsername变化
|
||||
watch(searchUsername, (newValue, oldValue) => {
|
||||
console.log('👀 [用户搜索监听] searchUsername变化:', {
|
||||
oldValue: oldValue,
|
||||
newValue: newValue,
|
||||
timestamp: new Date().toISOString(),
|
||||
type: typeof newValue,
|
||||
length: newValue ? newValue.length : 0
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
// 输入框获得焦点
|
||||
const handleSearchFocus = async (e) => {
|
||||
searchFocusTime = Date.now()
|
||||
console.log('🎯 [用户搜索监听] 搜索框获得焦点:', {
|
||||
timestamp: new Date().toISOString(),
|
||||
currentValue: searchUsername.value,
|
||||
eventValue: e.target.value,
|
||||
focusTime: searchFocusTime,
|
||||
valuesMatch: searchUsername.value === e.target.value
|
||||
})
|
||||
|
||||
await logUserAction('search_focus', {
|
||||
field: 'searchUsername',
|
||||
currentValue: searchUsername.value,
|
||||
eventValue: e.target.value,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// 输入框失去焦点
|
||||
const handleSearchBlur = async (e) => {
|
||||
const blurTime = Date.now()
|
||||
const focusDuration = searchFocusTime ? blurTime - searchFocusTime : 0
|
||||
|
||||
console.log('👋 [用户搜索监听] 搜索框失去焦点:', {
|
||||
timestamp: new Date().toISOString(),
|
||||
finalValue: searchUsername.value,
|
||||
focusDuration: focusDuration + 'ms',
|
||||
blurTime: blurTime
|
||||
})
|
||||
|
||||
await logUserAction('search_blur', {
|
||||
field: 'searchUsername',
|
||||
finalValue: searchUsername.value,
|
||||
focusDuration: focusDuration,
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
searchFocusTime = null
|
||||
}
|
||||
|
||||
// 输入框值改变(与input不同,change在失焦时触发)
|
||||
const handleSearchChange = async (e) => {
|
||||
const value = e.target.value
|
||||
console.log('🔄 [用户搜索监听] 搜索框值改变:', {
|
||||
newValue: value,
|
||||
timestamp: new Date().toISOString(),
|
||||
eventType: 'change'
|
||||
})
|
||||
|
||||
await logUserAction('search_change', {
|
||||
field: 'searchUsername',
|
||||
newValue: value,
|
||||
eventType: 'change',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
}
|
||||
|
||||
// 实时搜索输入处理
|
||||
const handleSearchInput = async (e) => {
|
||||
const value = e.target.value
|
||||
const oldValue = searchUsername.value
|
||||
|
||||
// 更新searchUsername的值
|
||||
searchUsername.value = value
|
||||
|
||||
console.log('🔍 [用户搜索监听] 搜索输入变化:', {
|
||||
oldValue: oldValue,
|
||||
newValue: value,
|
||||
timestamp: new Date().toISOString(),
|
||||
inputLength: value ? value.length : 0,
|
||||
searchUsernameValue: searchUsername.value
|
||||
})
|
||||
|
||||
// 记录输入变化日志
|
||||
await logUserAction('search_input_change', {
|
||||
field: 'searchUsername',
|
||||
oldValue: oldValue,
|
||||
newValue: value,
|
||||
inputLength: value ? value.length : 0,
|
||||
isEmpty: !value || value.trim() === ''
|
||||
})
|
||||
|
||||
// 清除之前的定时器
|
||||
if (searchTimeout) {
|
||||
clearTimeout(searchTimeout)
|
||||
console.log('⏰ [用户搜索监听] 清除之前的搜索定时器')
|
||||
}
|
||||
|
||||
// 如果输入为空,立即重新加载所有数据
|
||||
if (!value || value.trim() === '') {
|
||||
console.log('🔄 [用户搜索监听] 输入为空,重新加载所有数据')
|
||||
await fetchUsers()
|
||||
return
|
||||
}
|
||||
|
||||
// 延迟500ms执行搜索,避免频繁请求
|
||||
searchTimeout = setTimeout(async () => {
|
||||
console.log('⏰ [用户搜索监听] 延迟搜索触发,关键词:', value, 'searchUsername.value:', searchUsername.value)
|
||||
await searchUsers()
|
||||
}, 500)
|
||||
}
|
||||
|
||||
// 更新用户名选项(在数据加载后)
|
||||
const updateUsernameOptions = () => {
|
||||
usernameOptions.value = users.value.map(user => ({
|
||||
value: user.username,
|
||||
label: user.username
|
||||
}))
|
||||
}
|
||||
|
||||
// 重置搜索
|
||||
const resetSearch = () => {
|
||||
searchUsername.value = ''
|
||||
isSearching.value = false
|
||||
usernameOptions.value = []
|
||||
fetchUsers() // 重新加载全部用户
|
||||
}
|
||||
|
||||
// 调试搜索关键词
|
||||
const debugSearchKeyword = () => {
|
||||
console.log('🐛 [用户搜索调试] 搜索关键词调试信息:', {
|
||||
searchUsername: searchUsername.value,
|
||||
searchUsernameType: typeof searchUsername.value,
|
||||
searchUsernameLength: searchUsername.value ? searchUsername.value.length : 0,
|
||||
searchUsernameTrimmed: searchUsername.value ? searchUsername.value.trim() : '',
|
||||
searchUsernameIsEmpty: !searchUsername.value,
|
||||
searchUsernameIsWhitespace: searchUsername.value && searchUsername.value.trim() === '',
|
||||
timestamp: new Date().toISOString()
|
||||
})
|
||||
|
||||
message.info(`搜索关键词: "${searchUsername.value}" (长度: ${searchUsername.value ? searchUsername.value.length : 0})`)
|
||||
}
|
||||
|
||||
// 用户操作日志记录
|
||||
const logUserAction = async (action, data = {}) => {
|
||||
try {
|
||||
const userInfo = JSON.parse(localStorage.getItem('user') || '{}')
|
||||
|
||||
const logData = {
|
||||
module: 'user-management',
|
||||
action: action,
|
||||
timestamp: new Date().toISOString(),
|
||||
userId: userInfo.id || null,
|
||||
username: userInfo.username || 'anonymous',
|
||||
...data
|
||||
}
|
||||
|
||||
console.log('📝 [用户操作日志]', action, {
|
||||
time: new Date().toLocaleString(),
|
||||
user: userInfo.username || 'anonymous',
|
||||
action: action,
|
||||
data: logData
|
||||
})
|
||||
|
||||
// 发送到后端记录
|
||||
const { api } = await import('../utils/api')
|
||||
await api.formLogs.add({
|
||||
action: action,
|
||||
module: 'user-management',
|
||||
userId: userInfo.id || null,
|
||||
formData: JSON.stringify(logData),
|
||||
oldValues: null,
|
||||
newValues: JSON.stringify(data),
|
||||
success: true,
|
||||
errorMessage: null
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('❌ 记录用户操作日志失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 组件挂载时获取数据
|
||||
onMounted(() => {
|
||||
fetchUsers()
|
||||
onMounted(async () => {
|
||||
// 并行获取用户列表和角色列表
|
||||
await Promise.all([
|
||||
fetchUsers().then(() => {
|
||||
updateUsernameOptions()
|
||||
}),
|
||||
fetchRoles()
|
||||
])
|
||||
})
|
||||
</script>
|
||||
43
admin-system/frontend/test-download.js
Normal file
43
admin-system/frontend/test-download.js
Normal file
@@ -0,0 +1,43 @@
|
||||
// 测试下载模板功能
|
||||
async function testDownloadTemplate() {
|
||||
try {
|
||||
console.log('开始测试下载模板功能...');
|
||||
|
||||
// 模拟API调用
|
||||
const response = await fetch('http://localhost:5350/api/iot-cattle/public/import/template');
|
||||
|
||||
console.log('API响应状态:', response.status);
|
||||
console.log('Content-Type:', response.headers.get('content-type'));
|
||||
console.log('Content-Disposition:', response.headers.get('content-disposition'));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
// 获取blob
|
||||
const blob = await response.blob();
|
||||
console.log('Blob类型:', blob.type);
|
||||
console.log('Blob大小:', blob.size, 'bytes');
|
||||
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = '牛只档案导入模板.xlsx';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
window.URL.revokeObjectURL(url);
|
||||
|
||||
console.log('✅ 下载成功!');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ 下载失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 在浏览器控制台中运行
|
||||
if (typeof window !== 'undefined') {
|
||||
window.testDownloadTemplate = testDownloadTemplate;
|
||||
console.log('测试函数已加载,请在控制台运行: testDownloadTemplate()');
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>百度地图API测试</title>
|
||||
<style>
|
||||
#map {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#log {
|
||||
margin-top: 20px;
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
white-space: pre-wrap;
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>百度地图API测试</h1>
|
||||
<div id="map"></div>
|
||||
<div id="log"></div>
|
||||
|
||||
<script>
|
||||
const log = document.getElementById('log');
|
||||
function addLog(message) {
|
||||
log.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
|
||||
console.log(message);
|
||||
}
|
||||
|
||||
// 测试API密钥
|
||||
const API_KEY = '3AN3VahoqaXUs32U8luXD2Dwn86KK5B7';
|
||||
addLog('使用API密钥: ' + API_KEY);
|
||||
|
||||
// 创建全局回调函数
|
||||
window.initBMap = function() {
|
||||
addLog('百度地图API加载成功');
|
||||
addLog('BMap对象类型: ' + typeof window.BMap);
|
||||
addLog('BMap.Map是否存在: ' + typeof window.BMap.Map);
|
||||
|
||||
try {
|
||||
// 创建地图实例
|
||||
const map = new BMap.Map("map");
|
||||
addLog('地图实例创建成功');
|
||||
|
||||
// 设置中心点
|
||||
const point = new BMap.Point(106.27, 38.47); // 宁夏中心
|
||||
map.centerAndZoom(point, 8);
|
||||
addLog('地图中心点设置成功');
|
||||
|
||||
// 添加控件
|
||||
map.addControl(new BMap.MapTypeControl());
|
||||
map.addControl(new BMap.NavigationControl());
|
||||
addLog('地图控件添加成功');
|
||||
|
||||
addLog('地图初始化完成!');
|
||||
} catch (error) {
|
||||
addLog('地图创建失败: ' + error.message);
|
||||
}
|
||||
};
|
||||
|
||||
// 加载百度地图API
|
||||
addLog('开始加载百度地图API...');
|
||||
const script = document.createElement('script');
|
||||
script.type = 'text/javascript';
|
||||
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${API_KEY}&callback=initBMap`;
|
||||
|
||||
script.onerror = function(error) {
|
||||
addLog('脚本加载失败: ' + error);
|
||||
};
|
||||
|
||||
addLog('API URL: ' + script.src);
|
||||
document.head.appendChild(script);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,18 +1,44 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { resolve } from 'path'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
server: {
|
||||
port: 5300,
|
||||
host: '0.0.0.0',
|
||||
historyApiFallback: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:5350',
|
||||
changeOrigin: true
|
||||
export default defineConfig(({ mode }) => {
|
||||
// 加载环境变量
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
plugins: [vue()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src')
|
||||
}
|
||||
},
|
||||
server: {
|
||||
port: 5300,
|
||||
host: '0.0.0.0',
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_API_FULL_URL?.replace('/api', '') || 'http://localhost:5350',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api')
|
||||
}
|
||||
}
|
||||
},
|
||||
build: {
|
||||
outDir: 'dist',
|
||||
assetsDir: 'assets',
|
||||
rollupOptions: {
|
||||
output: {
|
||||
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||
chunkFileNames: 'assets/[name]-[hash].js',
|
||||
entryFileNames: 'assets/[name]-[hash].js'
|
||||
}
|
||||
}
|
||||
},
|
||||
define: {
|
||||
// 将环境变量注入到应用中
|
||||
__APP_ENV__: JSON.stringify(env)
|
||||
}
|
||||
}
|
||||
})
|
||||
76
backend/Dockerfile
Normal file
76
backend/Dockerfile
Normal file
@@ -0,0 +1,76 @@
|
||||
# 宁夏智慧养殖监管平台 - 后端服务容器
|
||||
# 基于Node.js 18 Alpine镜像构建
|
||||
FROM node:18-alpine
|
||||
|
||||
# 设置工作目录
|
||||
WORKDIR /app
|
||||
|
||||
# 安装系统依赖
|
||||
RUN apk add --no-cache \
|
||||
# 用于数据库备份的MySQL客户端
|
||||
mysql-client \
|
||||
# 用于PDF生成的Chrome依赖
|
||||
chromium \
|
||||
nss \
|
||||
freetype \
|
||||
freetype-dev \
|
||||
harfbuzz \
|
||||
ca-certificates \
|
||||
ttf-freefont \
|
||||
# 用于文件压缩的工具
|
||||
zip \
|
||||
unzip \
|
||||
# 开发工具
|
||||
git \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# 设置Puppeteer使用系统安装的Chromium
|
||||
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true \
|
||||
PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium-browser
|
||||
|
||||
# 复制package.json和package-lock.json
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装Node.js依赖
|
||||
RUN npm ci --only=production && npm cache clean --force
|
||||
|
||||
# 复制应用代码
|
||||
COPY . .
|
||||
|
||||
# 创建必要的目录
|
||||
RUN mkdir -p logs backups uploads temp
|
||||
|
||||
# 设置文件权限
|
||||
RUN chown -R node:node /app
|
||||
|
||||
# 切换到非root用户
|
||||
USER node
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5350
|
||||
|
||||
# 健康检查
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD node -e "const http = require('http'); \
|
||||
const options = { host: 'localhost', port: 5350, path: '/api/health', timeout: 5000 }; \
|
||||
const req = http.request(options, (res) => { \
|
||||
if (res.statusCode === 200) process.exit(0); \
|
||||
else process.exit(1); \
|
||||
}); \
|
||||
req.on('error', () => process.exit(1)); \
|
||||
req.end();"
|
||||
|
||||
# 设置环境变量
|
||||
ENV NODE_ENV=production \
|
||||
PORT=5350 \
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 启动命令
|
||||
CMD ["node", "server.js"]
|
||||
|
||||
# 元数据标签
|
||||
LABEL maintainer="宁夏智慧养殖监管平台 <support@nxxm.com>" \
|
||||
version="2.1.0" \
|
||||
description="宁夏智慧养殖监管平台后端API服务" \
|
||||
application="nxxm-farming-platform" \
|
||||
tier="backend"
|
||||
139
backend/NETWORK_ACCESS_SOLUTION.md
Normal file
139
backend/NETWORK_ACCESS_SOLUTION.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# 网络访问问题解决方案
|
||||
|
||||
## 问题描述
|
||||
您能访问 `http://172.28.112.1:5300/` 而别人访问不了,这是因为网络配置和防火墙设置的问题。
|
||||
|
||||
## 已完成的修复
|
||||
|
||||
### 1. ✅ 后端服务器配置修复
|
||||
- 修改了 `server.js` 文件
|
||||
- 服务器现在监听所有网络接口 (`0.0.0.0:5350`)
|
||||
- 不再只监听本地回环地址
|
||||
|
||||
### 2. ✅ 前端服务器配置检查
|
||||
- 前端已正确配置 `host: '0.0.0.0'`
|
||||
- 可以接受来自任何IP地址的连接
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 步骤1:重启服务器
|
||||
```bash
|
||||
# 停止当前服务器(Ctrl+C)
|
||||
# 重新启动后端服务器
|
||||
cd backend
|
||||
npm start
|
||||
```
|
||||
|
||||
### 步骤2:配置Windows防火墙
|
||||
|
||||
#### 方法A:使用批处理脚本(推荐)
|
||||
1. 右键点击 `configure-firewall.bat`
|
||||
2. 选择"以管理员身份运行"
|
||||
3. 等待配置完成
|
||||
|
||||
#### 方法B:使用PowerShell脚本
|
||||
1. 右键点击 `configure-firewall.ps1`
|
||||
2. 选择"以管理员身份运行"
|
||||
3. 等待配置完成
|
||||
|
||||
#### 方法C:手动配置
|
||||
以管理员身份运行PowerShell,执行以下命令:
|
||||
```powershell
|
||||
# 允许前端端口
|
||||
netsh advfirewall firewall add rule name="Node.js Frontend Port 5300" dir=in action=allow protocol=TCP localport=5300
|
||||
|
||||
# 允许后端端口
|
||||
netsh advfirewall firewall add rule name="Node.js Backend Port 5350" dir=in action=allow protocol=TCP localport=5350
|
||||
```
|
||||
|
||||
### 步骤3:验证配置
|
||||
运行网络诊断脚本:
|
||||
```bash
|
||||
node fix-network-access.js
|
||||
```
|
||||
|
||||
### 步骤4:测试访问
|
||||
让其他用户访问以下地址之一:
|
||||
- `http://172.28.112.1:5300` (前端)
|
||||
- `http://172.28.112.1:5350` (后端)
|
||||
- `http://192.168.0.48:5300` (如果使用以太网)
|
||||
- `http://192.168.0.48:5350` (如果使用以太网)
|
||||
|
||||
## 网络接口说明
|
||||
|
||||
根据诊断结果,您有以下可用的网络接口:
|
||||
- **Clash**: 198.18.0.1 (VPN接口)
|
||||
- **vEthernet (Default Switch)**: 172.28.112.1 (Hyper-V接口)
|
||||
- **以太网**: 192.168.0.48 (主要网络接口)
|
||||
- **VMware Network Adapter VMnet1**: 192.168.134.1
|
||||
- **VMware Network Adapter VMnet8**: 192.168.238.1
|
||||
|
||||
## 常见问题解决
|
||||
|
||||
### 问题1:其他用户仍然无法访问
|
||||
**解决方案**:
|
||||
1. 确认其他用户与您在同一个局域网内
|
||||
2. 使用正确的IP地址(不是localhost)
|
||||
3. 检查路由器是否阻止了设备间通信
|
||||
|
||||
### 问题2:端口被占用
|
||||
**解决方案**:
|
||||
```bash
|
||||
# 查看端口占用情况
|
||||
netstat -ano | findstr :5300
|
||||
netstat -ano | findstr :5350
|
||||
|
||||
# 终止占用端口的进程
|
||||
taskkill /PID [进程ID] /F
|
||||
```
|
||||
|
||||
### 问题3:防火墙配置失败
|
||||
**解决方案**:
|
||||
1. 确保以管理员身份运行脚本
|
||||
2. 检查Windows防火墙是否被禁用
|
||||
3. 手动在Windows安全中心添加规则
|
||||
|
||||
## 验证方法
|
||||
|
||||
### 1. 检查服务器状态
|
||||
```bash
|
||||
# 应该看到类似输出
|
||||
netstat -ano | findstr :5300
|
||||
# TCP 0.0.0.0:5300 0.0.0.0:0 LISTENING [PID]
|
||||
|
||||
netstat -ano | findstr :5350
|
||||
# TCP 0.0.0.0:5350 0.0.0.0:0 LISTENING [PID]
|
||||
```
|
||||
|
||||
### 2. 测试连接
|
||||
```bash
|
||||
# 测试本地连接
|
||||
telnet localhost 5300
|
||||
telnet localhost 5350
|
||||
|
||||
# 测试局域网连接
|
||||
telnet 172.28.112.1 5300
|
||||
telnet 172.28.112.1 5350
|
||||
```
|
||||
|
||||
### 3. 浏览器测试
|
||||
在浏览器中访问:
|
||||
- `http://172.28.112.1:5300` - 应该看到前端页面
|
||||
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全性**:开放端口到所有网络接口会降低安全性,仅用于开发环境
|
||||
2. **网络环境**:确保在受信任的局域网环境中使用
|
||||
3. **IP地址变化**:如果IP地址发生变化,需要更新访问地址
|
||||
4. **路由器设置**:某些路由器可能阻止设备间通信,需要检查路由器设置
|
||||
|
||||
## 成功标志
|
||||
|
||||
当配置成功后,您应该看到:
|
||||
- 服务器启动时显示"服务器监听所有网络接口 (0.0.0.0:5350)"
|
||||
- 其他用户可以通过您的IP地址访问服务
|
||||
- 防火墙规则已正确添加
|
||||
- 网络诊断脚本显示端口可以正常监听
|
||||
|
||||
如果按照以上步骤操作后仍有问题,请检查网络环境或联系网络管理员。
|
||||
186
backend/NGROK_SETUP_GUIDE.md
Normal file
186
backend/NGROK_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# ngrok外网访问配置指南
|
||||
|
||||
## 🎯 目标
|
||||
让不在同一局域网的用户能够访问您的开发服务器
|
||||
|
||||
## 📋 完整步骤
|
||||
|
||||
### 步骤1:注册ngrok账号
|
||||
1. 访问 https://ngrok.com/
|
||||
2. 点击 "Sign up" 注册免费账号
|
||||
3. 验证邮箱后登录
|
||||
|
||||
### 步骤2:获取认证令牌
|
||||
1. 登录后访问 https://dashboard.ngrok.com/get-started/your-authtoken
|
||||
2. 复制您的authtoken(类似:`2abc123def456ghi789jkl012mno345pqr678stu901vwx234yz`)
|
||||
|
||||
### 步骤3:配置ngrok认证
|
||||
```bash
|
||||
# 在backend目录下运行
|
||||
.\ngrok.exe authtoken YOUR_AUTH_TOKEN
|
||||
```
|
||||
|
||||
### 步骤4:启动服务穿透
|
||||
|
||||
#### 方法A:使用批处理脚本
|
||||
```bash
|
||||
# 双击运行
|
||||
start-ngrok.bat
|
||||
```
|
||||
|
||||
#### 方法B:使用PowerShell脚本
|
||||
```powershell
|
||||
# 右键以管理员身份运行
|
||||
.\start-ngrok.ps1
|
||||
```
|
||||
|
||||
#### 方法C:手动启动
|
||||
```bash
|
||||
# 启动前端穿透(新开一个终端窗口)
|
||||
.\ngrok.exe http 5300
|
||||
|
||||
# 启动后端穿透(再开一个终端窗口)
|
||||
.\ngrok.exe http 5350
|
||||
```
|
||||
|
||||
### 步骤5:获取访问地址
|
||||
|
||||
ngrok会显示类似这样的信息:
|
||||
```
|
||||
Session Status online
|
||||
Account your-email@example.com
|
||||
Version 3.27.0
|
||||
Region United States (us)
|
||||
Latency 45ms
|
||||
Web Interface http://127.0.0.1:4040
|
||||
Forwarding https://abc123.ngrok.io -> http://localhost:5300
|
||||
```
|
||||
|
||||
### 步骤6:分享访问地址
|
||||
|
||||
- **前端访问地址**:`https://abc123.ngrok.io`
|
||||
- **后端访问地址**:`https://def456.ngrok.io`
|
||||
- **API文档地址**:`https://def456.ngrok.io/api-docs`
|
||||
|
||||
## 🔧 高级配置
|
||||
|
||||
### 自定义子域名(付费功能)
|
||||
```bash
|
||||
# 使用自定义子域名
|
||||
.\ngrok.exe http 5300 --subdomain=myapp-frontend
|
||||
.\ngrok.exe http 5350 --subdomain=myapp-backend
|
||||
```
|
||||
|
||||
### 同时启动多个服务
|
||||
```bash
|
||||
# 使用配置文件
|
||||
.\ngrok.exe start --all --config=ngrok.yml
|
||||
```
|
||||
|
||||
### 配置文件示例 (ngrok.yml)
|
||||
```yaml
|
||||
version: "2"
|
||||
authtoken: YOUR_AUTH_TOKEN
|
||||
tunnels:
|
||||
frontend:
|
||||
proto: http
|
||||
addr: 5300
|
||||
subdomain: myapp-frontend
|
||||
backend:
|
||||
proto: http
|
||||
addr: 5350
|
||||
subdomain: myapp-backend
|
||||
```
|
||||
|
||||
## 📊 免费版限制
|
||||
|
||||
- 每次重启ngrok,URL会变化
|
||||
- 同时只能运行1个隧道
|
||||
- 有连接数限制
|
||||
- 有带宽限制
|
||||
|
||||
## 💰 付费版优势
|
||||
|
||||
- 固定子域名
|
||||
- 多个隧道
|
||||
- 更高带宽
|
||||
- 更多功能
|
||||
|
||||
## 🚨 注意事项
|
||||
|
||||
1. **安全性**:
|
||||
- 外网访问会暴露您的服务
|
||||
- 建议设置访问密码
|
||||
- 不要在生产环境使用
|
||||
|
||||
2. **性能**:
|
||||
- 外网访问比内网慢
|
||||
- 免费版有带宽限制
|
||||
|
||||
3. **稳定性**:
|
||||
- 免费版URL会变化
|
||||
- 付费版更稳定
|
||||
|
||||
## 🛠️ 故障排除
|
||||
|
||||
### 问题1:ngrok启动失败
|
||||
```bash
|
||||
# 检查网络连接
|
||||
ping ngrok.com
|
||||
|
||||
# 重新配置认证
|
||||
.\ngrok.exe authtoken YOUR_AUTH_TOKEN
|
||||
```
|
||||
|
||||
### 问题2:无法访问服务
|
||||
```bash
|
||||
# 检查本地服务是否运行
|
||||
netstat -ano | findstr :5300
|
||||
netstat -ano | findstr :5350
|
||||
|
||||
# 检查防火墙设置
|
||||
netsh advfirewall firewall show rule name="Node.js Frontend Port 5300"
|
||||
```
|
||||
|
||||
### 问题3:URL无法访问
|
||||
- 检查ngrok是否在线
|
||||
- 重新启动ngrok
|
||||
- 检查本地服务状态
|
||||
|
||||
## 📱 移动端访问
|
||||
|
||||
ngrok提供的HTTPS地址可以在移动设备上正常访问:
|
||||
- 手机浏览器访问:`https://abc123.ngrok.io`
|
||||
- 平板电脑访问:`https://abc123.ngrok.io`
|
||||
|
||||
## 🔄 自动重启脚本
|
||||
|
||||
创建自动重启脚本,当ngrok断开时自动重连:
|
||||
|
||||
```bash
|
||||
# auto-restart-ngrok.bat
|
||||
@echo off
|
||||
:start
|
||||
echo 启动ngrok...
|
||||
.\ngrok.exe http 5300
|
||||
echo ngrok断开,3秒后重新启动...
|
||||
timeout /t 3 /nobreak >nul
|
||||
goto start
|
||||
```
|
||||
|
||||
## 📈 监控和日志
|
||||
|
||||
ngrok提供Web界面监控:
|
||||
- 访问:http://127.0.0.1:4040
|
||||
- 查看请求日志
|
||||
- 监控连接状态
|
||||
- 查看流量统计
|
||||
|
||||
## 🎉 完成!
|
||||
|
||||
配置完成后,其他用户就可以通过ngrok提供的HTTPS地址访问您的开发服务器了!
|
||||
|
||||
记住:
|
||||
- 每次重启ngrok,URL会变化
|
||||
- 免费版有使用限制
|
||||
- 建议在开发测试时使用
|
||||
99
backend/VERIFICATION_COMPLETE.md
Normal file
99
backend/VERIFICATION_COMPLETE.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# ✅ 网络访问问题已解决
|
||||
|
||||
## 修复完成状态
|
||||
|
||||
### 1. ✅ 后端服务器配置
|
||||
- **状态**: 已修复
|
||||
- **配置**: 服务器现在监听 `0.0.0.0:5350`
|
||||
- **验证**: `netstat` 显示 `TCP 0.0.0.0:5350 LISTENING`
|
||||
|
||||
### 2. ✅ 前端服务器配置
|
||||
- **状态**: 已正确配置
|
||||
- **配置**: 服务器监听 `0.0.0.0:5300`
|
||||
- **验证**: `netstat` 显示 `TCP 0.0.0.0:5300 LISTENING`
|
||||
|
||||
### 3. ✅ Windows防火墙配置
|
||||
- **状态**: 已配置
|
||||
- **规则1**: Node.js Frontend Port 5300 (已启用)
|
||||
- **规则2**: Node.js Backend Port 5350 (已启用)
|
||||
- **验证**: 防火墙规则已确认添加
|
||||
|
||||
## 现在其他用户可以访问的地址
|
||||
|
||||
### 主要访问地址
|
||||
- **前端**: `http://172.28.112.1:5300`
|
||||
- **后端**: `http://172.28.112.1:5350`
|
||||
- **API文档**: `http://172.28.112.1:5350/api-docs`
|
||||
|
||||
### 备用访问地址(如果主要地址不可用)
|
||||
- **前端**: `http://192.168.0.48:5300`
|
||||
- **后端**: `http://192.168.0.48:5350`
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 1. 本地验证
|
||||
在您的浏览器中访问:
|
||||
- `http://172.28.112.1:5300` - 应该看到前端页面
|
||||
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
|
||||
|
||||
### 2. 外部用户验证
|
||||
让其他用户在他们的浏览器中访问:
|
||||
- `http://172.28.112.1:5300` - 应该看到前端页面
|
||||
- `http://172.28.112.1:5350/api-docs` - 应该看到API文档
|
||||
|
||||
### 3. 网络连接测试
|
||||
其他用户可以运行以下命令测试连接:
|
||||
```cmd
|
||||
# 测试前端端口
|
||||
telnet 172.28.112.1 5300
|
||||
|
||||
# 测试后端端口
|
||||
telnet 172.28.112.1 5350
|
||||
```
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 如果其他用户仍然无法访问:
|
||||
|
||||
1. **检查网络环境**
|
||||
- 确保其他用户与您在同一个局域网内
|
||||
- 确认没有使用VPN或代理
|
||||
|
||||
2. **检查IP地址**
|
||||
- 使用 `ipconfig` 确认当前IP地址
|
||||
- 如果IP地址发生变化,更新访问地址
|
||||
|
||||
3. **检查防火墙**
|
||||
- 确认Windows防火墙规则已启用
|
||||
- 检查是否有其他安全软件阻止连接
|
||||
|
||||
4. **检查路由器设置**
|
||||
- 某些路由器可能阻止设备间通信
|
||||
- 检查路由器的访问控制设置
|
||||
|
||||
## 成功标志
|
||||
|
||||
当配置完全成功时,您应该看到:
|
||||
- ✅ 服务器启动时显示"服务器监听所有网络接口"
|
||||
- ✅ 防火墙规则已正确添加
|
||||
- ✅ 其他用户可以通过IP地址访问服务
|
||||
- ✅ 网络诊断脚本显示端口可以正常监听
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **安全性**: 当前配置允许局域网内所有设备访问,仅适用于开发环境
|
||||
2. **IP地址**: 如果网络环境变化,IP地址可能会改变
|
||||
3. **端口占用**: 确保端口5300和5350没有被其他程序占用
|
||||
4. **服务器状态**: 确保服务器持续运行
|
||||
|
||||
## 维护建议
|
||||
|
||||
1. **定期检查**: 定期运行 `node fix-network-access.js` 检查网络状态
|
||||
2. **日志监控**: 查看服务器日志确认连接状态
|
||||
3. **备份配置**: 保存防火墙配置以便快速恢复
|
||||
|
||||
---
|
||||
|
||||
**问题已完全解决!** 🎉
|
||||
|
||||
现在其他用户应该能够正常访问您的开发服务器了。如果还有任何问题,请检查上述故障排除步骤。
|
||||
@@ -11,11 +11,11 @@ const ormConfig = require('./orm-config');
|
||||
// 从环境变量获取数据库连接参数
|
||||
const DB_DIALECT = process.env.DB_DIALECT || 'mysql';
|
||||
const DB_STORAGE = process.env.DB_STORAGE || './database.sqlite';
|
||||
const DB_NAME = process.env.DB_NAME || 'nxTest';
|
||||
const DB_NAME = process.env.DB_NAME || 'nxxmdata';
|
||||
const DB_USER = process.env.DB_USER || 'root';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || 'Aiotagro@741';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || 'aiotAiot123!';
|
||||
const DB_HOST = process.env.DB_HOST || '129.211.213.226';
|
||||
const DB_PORT = process.env.DB_PORT || 3306;
|
||||
const DB_PORT = process.env.DB_PORT || 9527;
|
||||
|
||||
// 数据库连接池事件发射器
|
||||
class DatabasePoolEmitter extends EventEmitter {}
|
||||
|
||||
@@ -4,10 +4,10 @@ require('dotenv').config();
|
||||
// 从环境变量获取数据库配置
|
||||
const DB_DIALECT = process.env.DB_DIALECT || 'mysql';
|
||||
const DB_HOST = process.env.DB_HOST || '129.211.213.226';
|
||||
const DB_PORT = process.env.DB_PORT || 3306;
|
||||
const DB_NAME = process.env.DB_NAME || 'nxTest';
|
||||
const DB_PORT = process.env.DB_PORT || 9527;
|
||||
const DB_NAME = process.env.DB_NAME || 'nxxmdata';
|
||||
const DB_USER = process.env.DB_USER || 'root';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || 'Aiotagro@741';
|
||||
const DB_PASSWORD = process.env.DB_PASSWORD || 'aiotAiot123!';
|
||||
|
||||
// 创建Sequelize实例
|
||||
const sequelize = new Sequelize(DB_NAME, DB_USER, DB_PASSWORD, {
|
||||
|
||||
@@ -15,7 +15,7 @@ if (dialect === 'sqlite') {
|
||||
config.dialect = 'sqlite';
|
||||
} else {
|
||||
config.host = process.env.DB_HOST || '129.211.213.226';
|
||||
config.port = process.env.DB_PORT || 3306;
|
||||
config.port = process.env.DB_PORT || 9527;
|
||||
config.dialect = 'mysql';
|
||||
config.timezone = '+08:00';
|
||||
config.define.charset = 'utf8mb4';
|
||||
@@ -33,9 +33,9 @@ if (dialect === 'sqlite') {
|
||||
sequelize = new Sequelize(config);
|
||||
} else {
|
||||
sequelize = new Sequelize(
|
||||
process.env.DB_NAME || 'nxTest',
|
||||
process.env.DB_NAME || 'nxxmdata',
|
||||
process.env.DB_USER || 'root',
|
||||
process.env.DB_PASSWORD || 'Aiotagro@741',
|
||||
process.env.DB_PASSWORD || 'aiotAiot123!',
|
||||
config
|
||||
);
|
||||
}
|
||||
|
||||
526
backend/config/permissions.js
Normal file
526
backend/config/permissions.js
Normal file
@@ -0,0 +1,526 @@
|
||||
/**
|
||||
* 权限配置
|
||||
* @file permissions.js
|
||||
* @description 定义系统权限和角色权限矩阵
|
||||
*/
|
||||
|
||||
// 系统权限定义
|
||||
const PERMISSIONS = {
|
||||
// 用户管理权限
|
||||
USER_VIEW: 'user:view', // 查看用户
|
||||
USER_CREATE: 'user:create', // 创建用户
|
||||
USER_UPDATE: 'user:update', // 更新用户
|
||||
USER_DELETE: 'user:delete', // 删除用户
|
||||
|
||||
// 养殖场管理权限
|
||||
FARM_VIEW: 'farm:view', // 查看养殖场
|
||||
FARM_CREATE: 'farm:create', // 创建养殖场
|
||||
FARM_UPDATE: 'farm:update', // 更新养殖场
|
||||
FARM_DELETE: 'farm:delete', // 删除养殖场
|
||||
|
||||
// 设备管理权限
|
||||
DEVICE_VIEW: 'device:view', // 查看设备
|
||||
DEVICE_CREATE: 'device:create', // 创建设备
|
||||
DEVICE_UPDATE: 'device:update', // 更新设备
|
||||
DEVICE_DELETE: 'device:delete', // 删除设备
|
||||
DEVICE_CONTROL: 'device:control', // 控制设备
|
||||
|
||||
// 智能设备权限
|
||||
SMART_DEVICE_VIEW: 'smart_device:view', // 查看智能设备
|
||||
SMART_DEVICE_MANAGE: 'smart_device:manage', // 管理智能设备
|
||||
|
||||
// 智能耳标权限
|
||||
SMART_EARTAG_VIEW: 'smart_eartag:view', // 查看智能耳标
|
||||
SMART_EARTAG_CREATE: 'smart_eartag:create', // 创建智能耳标
|
||||
SMART_EARTAG_UPDATE: 'smart_eartag:update', // 更新智能耳标
|
||||
SMART_EARTAG_DELETE: 'smart_eartag:delete', // 删除智能耳标
|
||||
|
||||
// 智能脚环权限
|
||||
SMART_ANKLET_VIEW: 'smart_anklet:view', // 查看智能脚环
|
||||
SMART_ANKLET_CREATE: 'smart_anklet:create', // 创建智能脚环
|
||||
SMART_ANKLET_UPDATE: 'smart_anklet:update', // 更新智能脚环
|
||||
SMART_ANKLET_DELETE: 'smart_anklet:delete', // 删除智能脚环
|
||||
|
||||
// 智能项圈权限
|
||||
SMART_COLLAR_VIEW: 'smart_collar:view', // 查看智能项圈
|
||||
SMART_COLLAR_CREATE: 'smart_collar:create', // 创建智能项圈
|
||||
SMART_COLLAR_UPDATE: 'smart_collar:update', // 更新智能项圈
|
||||
SMART_COLLAR_DELETE: 'smart_collar:delete',
|
||||
|
||||
// 智能主机权限
|
||||
SMART_HOST_VIEW: 'smart_host:view', // 查看智能主机
|
||||
SMART_HOST_CREATE: 'smart_host:create', // 创建智能主机
|
||||
SMART_HOST_UPDATE: 'smart_host:update', // 更新智能主机
|
||||
SMART_HOST_DELETE: 'smart_host:delete', // 删除智能主机
|
||||
|
||||
// 电子围栏权限
|
||||
SMART_FENCE_VIEW: 'smart_fence:view', // 查看电子围栏
|
||||
SMART_FENCE_CREATE: 'smart_fence:create', // 创建电子围栏
|
||||
SMART_FENCE_UPDATE: 'smart_fence:update', // 更新电子围栏
|
||||
SMART_FENCE_DELETE: 'smart_fence:delete', // 删除电子围栏
|
||||
|
||||
// 动物管理权限
|
||||
ANIMAL_VIEW: 'animal:view', // 查看动物
|
||||
ANIMAL_CREATE: 'animal:create', // 创建动物记录
|
||||
ANIMAL_UPDATE: 'animal:update', // 更新动物记录
|
||||
ANIMAL_DELETE: 'animal:delete', // 删除动物记录
|
||||
|
||||
// 牛只管理权限
|
||||
CATTLE_ARCHIVES_VIEW: 'cattle:archives:view', // 查看牛只档案
|
||||
CATTLE_ARCHIVES_CREATE: 'cattle:archives:create', // 创建牛只档案
|
||||
CATTLE_ARCHIVES_UPDATE: 'cattle:archives:update', // 更新牛只档案
|
||||
CATTLE_ARCHIVES_DELETE: 'cattle:archives:delete', // 删除牛只档案
|
||||
|
||||
CATTLE_PENS_VIEW: 'cattle:pens:view', // 查看栏舍设置
|
||||
CATTLE_PENS_CREATE: 'cattle:pens:create', // 创建栏舍设置
|
||||
CATTLE_PENS_UPDATE: 'cattle:pens:update', // 更新栏舍设置
|
||||
CATTLE_PENS_DELETE: 'cattle:pens:delete', // 删除栏舍设置
|
||||
|
||||
CATTLE_BATCHES_VIEW: 'cattle:batches:view', // 查看批次设置
|
||||
CATTLE_BATCHES_CREATE: 'cattle:batches:create', // 创建批次设置
|
||||
CATTLE_BATCHES_UPDATE: 'cattle:batches:update', // 更新批次设置
|
||||
CATTLE_BATCHES_DELETE: 'cattle:batches:delete', // 删除批次设置
|
||||
|
||||
CATTLE_TRANSFER_VIEW: 'cattle:transfer:view', // 查看转栏记录
|
||||
CATTLE_TRANSFER_CREATE: 'cattle:transfer:create', // 创建转栏记录
|
||||
CATTLE_TRANSFER_UPDATE: 'cattle:transfer:update', // 更新转栏记录
|
||||
CATTLE_TRANSFER_DELETE: 'cattle:transfer:delete', // 删除转栏记录
|
||||
|
||||
CATTLE_EXIT_VIEW: 'cattle:exit:view', // 查看离栏记录
|
||||
CATTLE_EXIT_CREATE: 'cattle:exit:create', // 创建离栏记录
|
||||
CATTLE_EXIT_UPDATE: 'cattle:exit:update', // 更新离栏记录
|
||||
CATTLE_EXIT_DELETE: 'cattle:exit:delete', // 删除离栏记录
|
||||
|
||||
// 预警管理权限
|
||||
ALERT_VIEW: 'alert:view', // 查看预警
|
||||
ALERT_CREATE: 'alert:create', // 创建预警
|
||||
ALERT_UPDATE: 'alert:update', // 更新预警
|
||||
ALERT_DELETE: 'alert:delete', // 删除预警
|
||||
ALERT_HANDLE: 'alert:handle', // 处理预警
|
||||
|
||||
// 智能预警权限
|
||||
SMART_ALERT_VIEW: 'smart_alert:view', // 查看智能预警总览
|
||||
SMART_EARTAG_ALERT_VIEW: 'smart_eartag_alert:view', // 查看智能耳标预警
|
||||
SMART_COLLAR_ALERT_VIEW: 'smart_collar_alert:view', // 查看智能项圈预警
|
||||
|
||||
// 数据分析权限
|
||||
ANALYTICS_VIEW: 'analytics:view', // 查看分析数据
|
||||
REPORT_GENERATE: 'report:generate', // 生成报表
|
||||
REPORT_EXPORT: 'report:export', // 导出报表
|
||||
|
||||
// 系统管理权限
|
||||
SYSTEM_CONFIG: 'system:config', // 系统配置
|
||||
SYSTEM_MONITOR: 'system:monitor', // 系统监控
|
||||
SYSTEM_BACKUP: 'system:backup', // 系统备份
|
||||
OPERATION_LOG_VIEW: 'operation_log:view', // 查看操作日志
|
||||
|
||||
// 实时监控权限
|
||||
MONITOR_VIEW: 'monitor:view', // 查看实时监控
|
||||
|
||||
// 地图权限
|
||||
MAP_VIEW: 'map:view', // 查看地图
|
||||
MAP_EDIT: 'map:edit', // 编辑地图标记
|
||||
|
||||
// 产品订单权限
|
||||
PRODUCT_VIEW: 'product:view', // 查看产品
|
||||
PRODUCT_MANAGE: 'product:manage', // 管理产品
|
||||
ORDER_VIEW: 'order:view', // 查看订单
|
||||
ORDER_MANAGE: 'order:manage', // 管理订单
|
||||
|
||||
// 角色管理权限
|
||||
ROLE_VIEW: 'role:view', // 查看角色
|
||||
ROLE_CREATE: 'role:create', // 创建角色
|
||||
ROLE_UPDATE: 'role:update', // 更新角色
|
||||
ROLE_DELETE: 'role:delete', // 删除角色
|
||||
ROLE_ASSIGN: 'role:assign', // 分配角色权限
|
||||
};
|
||||
|
||||
// 角色权限矩阵
|
||||
const ROLE_PERMISSIONS = {
|
||||
// 系统管理员 - 全系统权限
|
||||
admin: [
|
||||
// 用户管理
|
||||
PERMISSIONS.USER_VIEW,
|
||||
PERMISSIONS.USER_CREATE,
|
||||
PERMISSIONS.USER_UPDATE,
|
||||
PERMISSIONS.USER_DELETE,
|
||||
|
||||
// 养殖场管理
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.FARM_CREATE,
|
||||
PERMISSIONS.FARM_UPDATE,
|
||||
PERMISSIONS.FARM_DELETE,
|
||||
|
||||
// 设备管理
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.DEVICE_CREATE,
|
||||
PERMISSIONS.DEVICE_UPDATE,
|
||||
PERMISSIONS.DEVICE_DELETE,
|
||||
PERMISSIONS.DEVICE_CONTROL,
|
||||
|
||||
// 智能设备管理
|
||||
PERMISSIONS.SMART_DEVICE_VIEW,
|
||||
PERMISSIONS.SMART_DEVICE_MANAGE,
|
||||
PERMISSIONS.SMART_EARTAG_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_CREATE,
|
||||
PERMISSIONS.SMART_EARTAG_UPDATE,
|
||||
PERMISSIONS.SMART_EARTAG_DELETE,
|
||||
PERMISSIONS.SMART_ANKLET_VIEW,
|
||||
PERMISSIONS.SMART_ANKLET_CREATE,
|
||||
PERMISSIONS.SMART_ANKLET_UPDATE,
|
||||
PERMISSIONS.SMART_ANKLET_DELETE,
|
||||
PERMISSIONS.SMART_COLLAR_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_CREATE,
|
||||
PERMISSIONS.SMART_COLLAR_UPDATE,
|
||||
PERMISSIONS.SMART_COLLAR_DELETE,
|
||||
PERMISSIONS.SMART_HOST_VIEW,
|
||||
PERMISSIONS.SMART_HOST_CREATE,
|
||||
PERMISSIONS.SMART_HOST_UPDATE,
|
||||
PERMISSIONS.SMART_HOST_DELETE,
|
||||
PERMISSIONS.SMART_FENCE_VIEW,
|
||||
PERMISSIONS.SMART_FENCE_CREATE,
|
||||
PERMISSIONS.SMART_FENCE_UPDATE,
|
||||
PERMISSIONS.SMART_FENCE_DELETE,
|
||||
|
||||
// 动物管理
|
||||
PERMISSIONS.ANIMAL_VIEW,
|
||||
PERMISSIONS.ANIMAL_CREATE,
|
||||
PERMISSIONS.ANIMAL_UPDATE,
|
||||
PERMISSIONS.ANIMAL_DELETE,
|
||||
|
||||
// 牛只管理
|
||||
PERMISSIONS.CATTLE_ARCHIVES_VIEW,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_CREATE,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_UPDATE,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_DELETE,
|
||||
PERMISSIONS.CATTLE_PENS_VIEW,
|
||||
PERMISSIONS.CATTLE_PENS_CREATE,
|
||||
PERMISSIONS.CATTLE_PENS_UPDATE,
|
||||
PERMISSIONS.CATTLE_PENS_DELETE,
|
||||
PERMISSIONS.CATTLE_BATCHES_VIEW,
|
||||
PERMISSIONS.CATTLE_BATCHES_CREATE,
|
||||
PERMISSIONS.CATTLE_BATCHES_UPDATE,
|
||||
PERMISSIONS.CATTLE_BATCHES_DELETE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_VIEW,
|
||||
PERMISSIONS.CATTLE_TRANSFER_CREATE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_UPDATE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_DELETE,
|
||||
PERMISSIONS.CATTLE_EXIT_VIEW,
|
||||
PERMISSIONS.CATTLE_EXIT_CREATE,
|
||||
PERMISSIONS.CATTLE_EXIT_UPDATE,
|
||||
PERMISSIONS.CATTLE_EXIT_DELETE,
|
||||
|
||||
// 预警管理
|
||||
PERMISSIONS.ALERT_VIEW,
|
||||
PERMISSIONS.ALERT_CREATE,
|
||||
PERMISSIONS.ALERT_UPDATE,
|
||||
PERMISSIONS.ALERT_DELETE,
|
||||
PERMISSIONS.ALERT_HANDLE,
|
||||
|
||||
// 智能预警管理
|
||||
PERMISSIONS.SMART_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_ALERT_VIEW,
|
||||
|
||||
// 数据分析
|
||||
PERMISSIONS.ANALYTICS_VIEW,
|
||||
PERMISSIONS.REPORT_GENERATE,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
|
||||
// 系统管理
|
||||
PERMISSIONS.SYSTEM_CONFIG,
|
||||
PERMISSIONS.SYSTEM_MONITOR,
|
||||
PERMISSIONS.SYSTEM_BACKUP,
|
||||
PERMISSIONS.OPERATION_LOG_VIEW,
|
||||
|
||||
// 角色管理
|
||||
PERMISSIONS.ROLE_VIEW,
|
||||
PERMISSIONS.ROLE_CREATE,
|
||||
PERMISSIONS.ROLE_UPDATE,
|
||||
PERMISSIONS.ROLE_DELETE,
|
||||
PERMISSIONS.ROLE_ASSIGN,
|
||||
|
||||
// 实时监控
|
||||
PERMISSIONS.MONITOR_VIEW,
|
||||
|
||||
// 地图
|
||||
PERMISSIONS.MAP_VIEW,
|
||||
PERMISSIONS.MAP_EDIT,
|
||||
|
||||
// 产品订单
|
||||
PERMISSIONS.PRODUCT_VIEW,
|
||||
PERMISSIONS.PRODUCT_MANAGE,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
PERMISSIONS.ORDER_MANAGE,
|
||||
],
|
||||
|
||||
// 养殖场管理员 - 只有四个管理功能:养殖场管理、设备管理、实时监控、动物管理
|
||||
farm_manager: [
|
||||
// 养殖场管理
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.FARM_CREATE,
|
||||
PERMISSIONS.FARM_UPDATE,
|
||||
PERMISSIONS.FARM_DELETE,
|
||||
|
||||
// 设备管理(包含智能设备)
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.DEVICE_CREATE,
|
||||
PERMISSIONS.DEVICE_UPDATE,
|
||||
PERMISSIONS.DEVICE_DELETE,
|
||||
PERMISSIONS.DEVICE_CONTROL,
|
||||
|
||||
// 智能设备管理
|
||||
PERMISSIONS.SMART_DEVICE_VIEW,
|
||||
PERMISSIONS.SMART_DEVICE_MANAGE,
|
||||
PERMISSIONS.SMART_EARTAG_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_CREATE,
|
||||
PERMISSIONS.SMART_EARTAG_UPDATE,
|
||||
PERMISSIONS.SMART_EARTAG_DELETE,
|
||||
PERMISSIONS.SMART_ANKLET_VIEW,
|
||||
PERMISSIONS.SMART_ANKLET_CREATE,
|
||||
PERMISSIONS.SMART_ANKLET_UPDATE,
|
||||
PERMISSIONS.SMART_ANKLET_DELETE,
|
||||
PERMISSIONS.SMART_COLLAR_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_CREATE,
|
||||
PERMISSIONS.SMART_COLLAR_UPDATE,
|
||||
PERMISSIONS.SMART_COLLAR_DELETE,
|
||||
PERMISSIONS.SMART_HOST_VIEW,
|
||||
PERMISSIONS.SMART_HOST_CREATE,
|
||||
PERMISSIONS.SMART_HOST_UPDATE,
|
||||
PERMISSIONS.SMART_HOST_DELETE,
|
||||
PERMISSIONS.SMART_FENCE_VIEW,
|
||||
PERMISSIONS.SMART_FENCE_CREATE,
|
||||
PERMISSIONS.SMART_FENCE_UPDATE,
|
||||
PERMISSIONS.SMART_FENCE_DELETE,
|
||||
|
||||
// 动物管理
|
||||
PERMISSIONS.ANIMAL_VIEW,
|
||||
PERMISSIONS.ANIMAL_CREATE,
|
||||
PERMISSIONS.ANIMAL_UPDATE,
|
||||
PERMISSIONS.ANIMAL_DELETE,
|
||||
|
||||
// 牛只管理
|
||||
PERMISSIONS.CATTLE_ARCHIVES_VIEW,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_CREATE,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_UPDATE,
|
||||
PERMISSIONS.CATTLE_ARCHIVES_DELETE,
|
||||
PERMISSIONS.CATTLE_PENS_VIEW,
|
||||
PERMISSIONS.CATTLE_PENS_CREATE,
|
||||
PERMISSIONS.CATTLE_PENS_UPDATE,
|
||||
PERMISSIONS.CATTLE_PENS_DELETE,
|
||||
PERMISSIONS.CATTLE_BATCHES_VIEW,
|
||||
PERMISSIONS.CATTLE_BATCHES_CREATE,
|
||||
PERMISSIONS.CATTLE_BATCHES_UPDATE,
|
||||
PERMISSIONS.CATTLE_BATCHES_DELETE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_VIEW,
|
||||
PERMISSIONS.CATTLE_TRANSFER_CREATE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_UPDATE,
|
||||
PERMISSIONS.CATTLE_TRANSFER_DELETE,
|
||||
PERMISSIONS.CATTLE_EXIT_VIEW,
|
||||
PERMISSIONS.CATTLE_EXIT_CREATE,
|
||||
PERMISSIONS.CATTLE_EXIT_UPDATE,
|
||||
PERMISSIONS.CATTLE_EXIT_DELETE,
|
||||
|
||||
// 实时监控功能
|
||||
PERMISSIONS.MONITOR_VIEW, // 实时监控功能
|
||||
PERMISSIONS.MAP_VIEW, // 地图查看(监控功能的一部分)
|
||||
|
||||
// 智能预警管理
|
||||
PERMISSIONS.SMART_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_ALERT_VIEW,
|
||||
],
|
||||
|
||||
// 监管人员 - 四个功能:数据分析、实时监控、预警管理、设备管理
|
||||
inspector: [
|
||||
// 数据分析功能
|
||||
PERMISSIONS.ANALYTICS_VIEW,
|
||||
PERMISSIONS.REPORT_GENERATE,
|
||||
PERMISSIONS.REPORT_EXPORT,
|
||||
|
||||
// 实时监控功能
|
||||
PERMISSIONS.MONITOR_VIEW,
|
||||
PERMISSIONS.MAP_VIEW,
|
||||
|
||||
// 预警管理功能
|
||||
PERMISSIONS.ALERT_VIEW,
|
||||
PERMISSIONS.ALERT_CREATE,
|
||||
PERMISSIONS.ALERT_UPDATE,
|
||||
PERMISSIONS.ALERT_DELETE,
|
||||
PERMISSIONS.ALERT_HANDLE,
|
||||
|
||||
// 智能预警管理
|
||||
PERMISSIONS.SMART_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_ALERT_VIEW,
|
||||
|
||||
// 设备管理功能
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.DEVICE_CREATE,
|
||||
PERMISSIONS.DEVICE_UPDATE,
|
||||
PERMISSIONS.DEVICE_DELETE,
|
||||
PERMISSIONS.DEVICE_CONTROL,
|
||||
|
||||
// 牛只管理查看权限
|
||||
PERMISSIONS.CATTLE_ARCHIVES_VIEW,
|
||||
PERMISSIONS.CATTLE_PENS_VIEW,
|
||||
PERMISSIONS.CATTLE_BATCHES_VIEW,
|
||||
PERMISSIONS.CATTLE_TRANSFER_VIEW,
|
||||
PERMISSIONS.CATTLE_EXIT_VIEW,
|
||||
],
|
||||
|
||||
// 普通用户 - 基础权限
|
||||
user: [
|
||||
// 个人信息管理
|
||||
PERMISSIONS.USER_UPDATE, // 只能更新自己的信息
|
||||
|
||||
// 基础查看权限
|
||||
PERMISSIONS.FARM_VIEW,
|
||||
PERMISSIONS.DEVICE_VIEW,
|
||||
PERMISSIONS.ANIMAL_VIEW,
|
||||
PERMISSIONS.ALERT_VIEW,
|
||||
PERMISSIONS.ANALYTICS_VIEW,
|
||||
PERMISSIONS.MAP_VIEW,
|
||||
|
||||
// 牛只管理查看权限
|
||||
PERMISSIONS.CATTLE_ARCHIVES_VIEW,
|
||||
PERMISSIONS.CATTLE_PENS_VIEW,
|
||||
PERMISSIONS.CATTLE_BATCHES_VIEW,
|
||||
PERMISSIONS.CATTLE_TRANSFER_VIEW,
|
||||
PERMISSIONS.CATTLE_EXIT_VIEW,
|
||||
|
||||
// 智能预警查看权限
|
||||
PERMISSIONS.SMART_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_EARTAG_ALERT_VIEW,
|
||||
PERMISSIONS.SMART_COLLAR_ALERT_VIEW,
|
||||
|
||||
// 产品订单
|
||||
PERMISSIONS.PRODUCT_VIEW,
|
||||
PERMISSIONS.ORDER_VIEW,
|
||||
],
|
||||
};
|
||||
|
||||
// 菜单权限配置
|
||||
const MENU_PERMISSIONS = {
|
||||
// 系统管理菜单
|
||||
'system.users': [PERMISSIONS.USER_VIEW],
|
||||
'system.config': [PERMISSIONS.SYSTEM_CONFIG],
|
||||
'system.monitor': [PERMISSIONS.SYSTEM_MONITOR],
|
||||
'system.backup': [PERMISSIONS.SYSTEM_BACKUP],
|
||||
'system.operation_logs': [PERMISSIONS.OPERATION_LOG_VIEW],
|
||||
|
||||
// 实时监控菜单
|
||||
'monitor.view': [PERMISSIONS.MONITOR_VIEW],
|
||||
|
||||
// 养殖场管理菜单
|
||||
'farm.management': [PERMISSIONS.FARM_VIEW],
|
||||
'farm.create': [PERMISSIONS.FARM_CREATE],
|
||||
'farm.edit': [PERMISSIONS.FARM_UPDATE],
|
||||
'farm.delete': [PERMISSIONS.FARM_DELETE],
|
||||
|
||||
// 设备管理菜单
|
||||
'device.management': [PERMISSIONS.DEVICE_VIEW],
|
||||
'device.control': [PERMISSIONS.DEVICE_CONTROL],
|
||||
|
||||
// 智能设备菜单
|
||||
'smart_device.main': [PERMISSIONS.SMART_DEVICE_VIEW],
|
||||
'smart_device.eartag': [PERMISSIONS.SMART_EARTAG_VIEW],
|
||||
'smart_device.anklet': [PERMISSIONS.SMART_ANKLET_VIEW],
|
||||
'smart_device.collar': [PERMISSIONS.SMART_COLLAR_VIEW],
|
||||
'smart_device.host': [PERMISSIONS.SMART_HOST_VIEW],
|
||||
'smart_device.fence': [PERMISSIONS.SMART_FENCE_VIEW],
|
||||
|
||||
// 动物管理菜单
|
||||
'animal.management': [PERMISSIONS.ANIMAL_VIEW],
|
||||
'animal.create': [PERMISSIONS.ANIMAL_CREATE],
|
||||
'animal.edit': [PERMISSIONS.ANIMAL_UPDATE],
|
||||
|
||||
// 牛只管理菜单
|
||||
'cattle.archives': [PERMISSIONS.CATTLE_ARCHIVES_VIEW],
|
||||
'cattle.pens': [PERMISSIONS.CATTLE_PENS_VIEW],
|
||||
'cattle.batches': [PERMISSIONS.CATTLE_BATCHES_VIEW],
|
||||
'cattle.transfer': [PERMISSIONS.CATTLE_TRANSFER_VIEW],
|
||||
'cattle.exit': [PERMISSIONS.CATTLE_EXIT_VIEW],
|
||||
|
||||
// 预警管理菜单
|
||||
'alert.management': [PERMISSIONS.ALERT_VIEW],
|
||||
'alert.handle': [PERMISSIONS.ALERT_HANDLE],
|
||||
|
||||
// 智能预警菜单
|
||||
'smart_alert.main': [PERMISSIONS.SMART_ALERT_VIEW],
|
||||
'smart_alert.eartag': [PERMISSIONS.SMART_EARTAG_ALERT_VIEW],
|
||||
'smart_alert.collar': [PERMISSIONS.SMART_COLLAR_ALERT_VIEW],
|
||||
|
||||
// 数据分析菜单
|
||||
'analytics.dashboard': [PERMISSIONS.ANALYTICS_VIEW],
|
||||
'analytics.reports': [PERMISSIONS.REPORT_GENERATE],
|
||||
|
||||
// 地图菜单
|
||||
'map.view': [PERMISSIONS.MAP_VIEW],
|
||||
'map.edit': [PERMISSIONS.MAP_EDIT],
|
||||
|
||||
// 产品订单菜单
|
||||
'product.management': [PERMISSIONS.PRODUCT_VIEW],
|
||||
'order.management': [PERMISSIONS.ORDER_VIEW],
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取角色的所有权限
|
||||
* @param {string} roleName 角色名称
|
||||
* @returns {Array} 权限列表
|
||||
*/
|
||||
function getRolePermissions(roleName) {
|
||||
return ROLE_PERMISSIONS[roleName] || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否具有指定权限
|
||||
* @param {Array} userPermissions 用户权限列表
|
||||
* @param {string|Array} requiredPermissions 需要的权限
|
||||
* @returns {boolean} 是否有权限
|
||||
*/
|
||||
function hasPermission(userPermissions, requiredPermissions) {
|
||||
if (!userPermissions || !Array.isArray(userPermissions)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const required = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
|
||||
|
||||
return required.some(permission => userPermissions.includes(permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否可以访问指定菜单
|
||||
* @param {Array} userPermissions 用户权限列表
|
||||
* @param {string} menuKey 菜单键
|
||||
* @returns {boolean} 是否可以访问
|
||||
*/
|
||||
function canAccessMenu(userPermissions, menuKey) {
|
||||
const menuPermissions = MENU_PERMISSIONS[menuKey];
|
||||
if (!menuPermissions) {
|
||||
return true; // 没有权限要求的菜单默认可以访问
|
||||
}
|
||||
|
||||
return hasPermission(userPermissions, menuPermissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户可访问的菜单列表
|
||||
* @param {Array} userPermissions 用户权限列表
|
||||
* @returns {Array} 可访问的菜单键列表
|
||||
*/
|
||||
function getAccessibleMenus(userPermissions) {
|
||||
return Object.keys(MENU_PERMISSIONS).filter(menuKey =>
|
||||
canAccessMenu(userPermissions, menuKey)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
PERMISSIONS,
|
||||
ROLE_PERMISSIONS,
|
||||
MENU_PERMISSIONS,
|
||||
getRolePermissions,
|
||||
hasPermission,
|
||||
canAccessMenu,
|
||||
getAccessibleMenus,
|
||||
};
|
||||
@@ -5,8 +5,52 @@ const options = {
|
||||
openapi: '3.0.0',
|
||||
info: {
|
||||
title: '宁夏智慧养殖监管平台 API',
|
||||
version: '1.0.0',
|
||||
description: '宁夏智慧养殖监管平台后端 API 文档',
|
||||
version: '2.1.0',
|
||||
description: `
|
||||
## 宁夏智慧养殖监管平台 API 文档
|
||||
|
||||
### 概述
|
||||
本文档提供了智慧养殖监管平台的完整API接口说明,包括:
|
||||
|
||||
- **核心功能**: 农场管理、动物管理、设备监控、预警管理
|
||||
- **业务功能**: 产品管理、订单管理、报表生成
|
||||
- **系统功能**: 用户管理、权限控制、系统配置
|
||||
|
||||
### 认证机制
|
||||
- 采用JWT (JSON Web Token) 进行身份认证
|
||||
- 所有API请求需在Header中携带Authorization字段
|
||||
- 格式: \`Authorization: Bearer <token>\`
|
||||
|
||||
### 响应格式
|
||||
所有API响应均采用统一格式:
|
||||
\`\`\`json
|
||||
{
|
||||
"success": true,
|
||||
"data": {},
|
||||
"message": "操作成功",
|
||||
"timestamp": "2025-01-18T10:30:00Z"
|
||||
}
|
||||
\`\`\`
|
||||
|
||||
### 错误处理
|
||||
- HTTP状态码遵循RESTful标准
|
||||
- 详细错误信息在响应体中提供
|
||||
- 支持多语言错误消息
|
||||
|
||||
### 版本信息
|
||||
- **当前版本**: v2.1.0
|
||||
- **最后更新**: 2025-01-18
|
||||
- **维护状态**: 积极维护中
|
||||
`,
|
||||
contact: {
|
||||
name: '技术支持',
|
||||
email: 'support@nxxm.com',
|
||||
url: 'https://github.com/nxxm-platform'
|
||||
},
|
||||
license: {
|
||||
name: 'MIT',
|
||||
url: 'https://opensource.org/licenses/MIT'
|
||||
}
|
||||
},
|
||||
servers: [
|
||||
{
|
||||
@@ -355,6 +399,247 @@ const options = {
|
||||
default: 'active'
|
||||
}
|
||||
}
|
||||
},
|
||||
User: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '用户ID'
|
||||
},
|
||||
username: {
|
||||
type: 'string',
|
||||
description: '用户名'
|
||||
},
|
||||
email: {
|
||||
type: 'string',
|
||||
format: 'email',
|
||||
description: '邮箱地址'
|
||||
},
|
||||
roles: {
|
||||
type: 'integer',
|
||||
description: '角色ID'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive'],
|
||||
description: '用户状态'
|
||||
},
|
||||
last_login: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '最后登录时间'
|
||||
}
|
||||
}
|
||||
},
|
||||
Product: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '产品ID'
|
||||
},
|
||||
name: {
|
||||
type: 'string',
|
||||
description: '产品名称'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: '产品描述'
|
||||
},
|
||||
price: {
|
||||
type: 'number',
|
||||
format: 'decimal',
|
||||
description: '产品价格'
|
||||
},
|
||||
stock: {
|
||||
type: 'integer',
|
||||
description: '库存数量'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['active', 'inactive'],
|
||||
description: '产品状态'
|
||||
}
|
||||
}
|
||||
},
|
||||
Order: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '订单ID'
|
||||
},
|
||||
user_id: {
|
||||
type: 'integer',
|
||||
description: '用户ID'
|
||||
},
|
||||
total_amount: {
|
||||
type: 'number',
|
||||
format: 'decimal',
|
||||
description: '订单总金额'
|
||||
},
|
||||
status: {
|
||||
type: 'string',
|
||||
enum: ['pending', 'processing', 'shipped', 'delivered', 'cancelled'],
|
||||
description: '订单状态'
|
||||
},
|
||||
payment_status: {
|
||||
type: 'string',
|
||||
enum: ['unpaid', 'paid', 'refunded'],
|
||||
description: '支付状态'
|
||||
},
|
||||
shipping_address: {
|
||||
type: 'string',
|
||||
description: '收货地址'
|
||||
}
|
||||
}
|
||||
},
|
||||
SystemConfig: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '配置ID'
|
||||
},
|
||||
config_key: {
|
||||
type: 'string',
|
||||
description: '配置键名'
|
||||
},
|
||||
config_value: {
|
||||
type: 'string',
|
||||
description: '配置值'
|
||||
},
|
||||
config_type: {
|
||||
type: 'string',
|
||||
enum: ['string', 'number', 'boolean', 'json', 'array'],
|
||||
description: '配置类型'
|
||||
},
|
||||
category: {
|
||||
type: 'string',
|
||||
description: '配置分类'
|
||||
},
|
||||
description: {
|
||||
type: 'string',
|
||||
description: '配置描述'
|
||||
},
|
||||
is_public: {
|
||||
type: 'boolean',
|
||||
description: '是否公开'
|
||||
},
|
||||
is_editable: {
|
||||
type: 'boolean',
|
||||
description: '是否可编辑'
|
||||
}
|
||||
}
|
||||
},
|
||||
MenuPermission: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: {
|
||||
type: 'integer',
|
||||
description: '菜单ID'
|
||||
},
|
||||
menu_key: {
|
||||
type: 'string',
|
||||
description: '菜单标识'
|
||||
},
|
||||
menu_name: {
|
||||
type: 'string',
|
||||
description: '菜单名称'
|
||||
},
|
||||
menu_path: {
|
||||
type: 'string',
|
||||
description: '菜单路径'
|
||||
},
|
||||
parent_id: {
|
||||
type: 'integer',
|
||||
description: '父菜单ID'
|
||||
},
|
||||
menu_type: {
|
||||
type: 'string',
|
||||
enum: ['page', 'button', 'api'],
|
||||
description: '菜单类型'
|
||||
},
|
||||
required_roles: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string'
|
||||
},
|
||||
description: '所需角色'
|
||||
},
|
||||
icon: {
|
||||
type: 'string',
|
||||
description: '菜单图标'
|
||||
},
|
||||
sort_order: {
|
||||
type: 'integer',
|
||||
description: '排序顺序'
|
||||
},
|
||||
is_visible: {
|
||||
type: 'boolean',
|
||||
description: '是否可见'
|
||||
},
|
||||
is_enabled: {
|
||||
type: 'boolean',
|
||||
description: '是否启用'
|
||||
}
|
||||
}
|
||||
},
|
||||
ApiResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
description: '操作是否成功'
|
||||
},
|
||||
data: {
|
||||
description: '响应数据'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '响应消息'
|
||||
},
|
||||
timestamp: {
|
||||
type: 'string',
|
||||
format: 'date-time',
|
||||
description: '响应时间'
|
||||
}
|
||||
}
|
||||
},
|
||||
ErrorResponse: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: {
|
||||
type: 'boolean',
|
||||
example: false
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '错误消息'
|
||||
},
|
||||
error: {
|
||||
type: 'string',
|
||||
description: '错误详情'
|
||||
},
|
||||
errors: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
field: {
|
||||
type: 'string',
|
||||
description: '错误字段'
|
||||
},
|
||||
message: {
|
||||
type: 'string',
|
||||
description: '错误消息'
|
||||
}
|
||||
}
|
||||
},
|
||||
description: '详细错误列表'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
61
backend/configure-firewall.bat
Normal file
61
backend/configure-firewall.bat
Normal file
@@ -0,0 +1,61 @@
|
||||
@echo off
|
||||
echo 正在配置Windows防火墙以允许外部访问...
|
||||
echo.
|
||||
|
||||
REM 检查是否以管理员身份运行
|
||||
net session >nul 2>&1
|
||||
if %errorLevel% == 0 (
|
||||
echo 检测到管理员权限,继续配置...
|
||||
) else (
|
||||
echo 错误:请以管理员身份运行此脚本
|
||||
echo 右键点击此文件,选择"以管理员身份运行"
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 添加防火墙规则...
|
||||
|
||||
REM 允许前端端口5300
|
||||
netsh advfirewall firewall add rule name="Node.js Frontend Port 5300" dir=in action=allow protocol=TCP localport=5300
|
||||
if %errorLevel% == 0 (
|
||||
echo ✅ 前端端口5300规则添加成功
|
||||
) else (
|
||||
echo ⚠️ 前端端口5300规则可能已存在
|
||||
)
|
||||
|
||||
REM 允许后端端口5350
|
||||
netsh advfirewall firewall add rule name="Node.js Backend Port 5350" dir=in action=allow protocol=TCP localport=5350
|
||||
if %errorLevel% == 0 (
|
||||
echo ✅ 后端端口5350规则添加成功
|
||||
) else (
|
||||
echo ⚠️ 后端端口5350规则可能已存在
|
||||
)
|
||||
|
||||
REM 允许Node.js程序
|
||||
netsh advfirewall firewall add rule name="Node.js Program" dir=in action=allow program="C:\Program Files\nodejs\node.exe" enable=yes
|
||||
if %errorLevel% == 0 (
|
||||
echo ✅ Node.js程序规则添加成功
|
||||
) else (
|
||||
echo ⚠️ Node.js程序规则可能已存在
|
||||
)
|
||||
|
||||
echo.
|
||||
echo 检查已添加的规则...
|
||||
netsh advfirewall firewall show rule name="Node.js Frontend Port 5300"
|
||||
echo.
|
||||
netsh advfirewall firewall show rule name="Node.js Backend Port 5350"
|
||||
echo.
|
||||
|
||||
echo 🎉 防火墙配置完成!
|
||||
echo.
|
||||
echo 现在其他用户可以通过以下地址访问您的服务:
|
||||
echo 前端: http://172.28.112.1:5300
|
||||
echo 后端: http://172.28.112.1:5350
|
||||
echo.
|
||||
echo 请确保:
|
||||
echo 1. 服务器正在运行
|
||||
echo 2. 其他用户与您在同一个局域网内
|
||||
echo 3. 使用正确的IP地址(不是localhost)
|
||||
echo.
|
||||
pause
|
||||
78
backend/configure-firewall.ps1
Normal file
78
backend/configure-firewall.ps1
Normal file
@@ -0,0 +1,78 @@
|
||||
# PowerShell防火墙配置脚本
|
||||
# 解决外部用户无法访问开发服务器的问题
|
||||
|
||||
Write-Host "🔧 正在配置Windows防火墙以允许外部访问..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 检查是否以管理员身份运行
|
||||
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
|
||||
Write-Host "❌ 错误:请以管理员身份运行此脚本" -ForegroundColor Red
|
||||
Write-Host "右键点击此文件,选择'以管理员身份运行'" -ForegroundColor Yellow
|
||||
Read-Host "按任意键退出"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "✅ 检测到管理员权限,继续配置..." -ForegroundColor Green
|
||||
Write-Host ""
|
||||
|
||||
# 配置防火墙规则
|
||||
Write-Host "添加防火墙规则..." -ForegroundColor Cyan
|
||||
|
||||
# 允许前端端口5300
|
||||
try {
|
||||
New-NetFirewallRule -DisplayName "Node.js Frontend Port 5300" -Direction Inbound -Protocol TCP -LocalPort 5300 -Action Allow -ErrorAction SilentlyContinue
|
||||
Write-Host "✅ 前端端口5300规则添加成功" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ 前端端口5300规则可能已存在" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 允许后端端口5350
|
||||
try {
|
||||
New-NetFirewallRule -DisplayName "Node.js Backend Port 5350" -Direction Inbound -Protocol TCP -LocalPort 5350 -Action Allow -ErrorAction SilentlyContinue
|
||||
Write-Host "✅ 后端端口5350规则添加成功" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "⚠️ 后端端口5350规则可能已存在" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# 允许Node.js程序
|
||||
try {
|
||||
$nodePath = "C:\Program Files\nodejs\node.exe"
|
||||
if (Test-Path $nodePath) {
|
||||
New-NetFirewallRule -DisplayName "Node.js Program" -Direction Inbound -Program $nodePath -Action Allow -ErrorAction SilentlyContinue
|
||||
Write-Host "✅ Node.js程序规则添加成功" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "⚠️ 未找到Node.js程序路径,跳过程序规则" -ForegroundColor Yellow
|
||||
}
|
||||
} catch {
|
||||
Write-Host "⚠️ Node.js程序规则可能已存在" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "检查已添加的规则..." -ForegroundColor Cyan
|
||||
|
||||
# 显示规则
|
||||
Get-NetFirewallRule -DisplayName "*Node.js*" | Format-Table DisplayName, Direction, Action, Enabled -AutoSize
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "🎉 防火墙配置完成!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "现在其他用户可以通过以下地址访问您的服务:" -ForegroundColor Yellow
|
||||
Write-Host "前端: http://172.28.112.1:5300" -ForegroundColor White
|
||||
Write-Host "后端: http://172.28.112.1:5350" -ForegroundColor White
|
||||
Write-Host ""
|
||||
Write-Host "请确保:" -ForegroundColor Yellow
|
||||
Write-Host "1. 服务器正在运行" -ForegroundColor White
|
||||
Write-Host "2. 其他用户与您在同一个局域网内" -ForegroundColor White
|
||||
Write-Host "3. 使用正确的IP地址(不是localhost)" -ForegroundColor White
|
||||
Write-Host ""
|
||||
|
||||
# 获取所有可用的IP地址
|
||||
Write-Host "可用的访问地址:" -ForegroundColor Cyan
|
||||
$networkAdapters = Get-NetIPAddress -AddressFamily IPv4 | Where-Object { $_.IPAddress -notlike "127.*" -and $_.IPAddress -notlike "169.254.*" }
|
||||
foreach ($adapter in $networkAdapters) {
|
||||
Write-Host " 前端: http://$($adapter.IPAddress):5300" -ForegroundColor White
|
||||
Write-Host " 后端: http://$($adapter.IPAddress):5350" -ForegroundColor White
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Read-Host "按任意键退出"
|
||||
@@ -37,6 +37,75 @@ exports.getAllAlerts = async (req, res) => {
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 根据养殖场名称搜索预警
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
exports.searchAlertsByFarmName = async (req, res) => {
|
||||
try {
|
||||
const { farmName } = req.query;
|
||||
|
||||
if (!farmName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供养殖场名称参数'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`开始搜索养殖场名称包含 "${farmName}" 的预警...`);
|
||||
|
||||
// 首先找到匹配的养殖场
|
||||
const farms = await Farm.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[require('sequelize').Op.like]: `%${farmName}%`
|
||||
}
|
||||
},
|
||||
attributes: ['id', 'name']
|
||||
});
|
||||
|
||||
if (farms.length === 0) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: '未找到匹配的养殖场'
|
||||
});
|
||||
}
|
||||
|
||||
const farmIds = farms.map(farm => farm.id);
|
||||
|
||||
// 根据养殖场ID查找预警
|
||||
const alerts = await Alert.findAll({
|
||||
where: {
|
||||
farm_id: {
|
||||
[require('sequelize').Op.in]: farmIds
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{ model: Farm, as: 'farm', attributes: ['id', 'name', 'location'] },
|
||||
{ model: Device, as: 'device', attributes: ['id', 'name', 'type'] }
|
||||
],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
console.log(`找到 ${alerts.length} 个匹配的预警`);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: alerts,
|
||||
message: `找到 ${alerts.length} 个养殖场名称包含 "${farmName}" 的预警`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('根据养殖场名称搜索预警失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '搜索预警失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个预警
|
||||
* @param {Object} req - 请求对象
|
||||
|
||||
@@ -30,6 +30,72 @@ exports.getAllAnimals = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据养殖场名称搜索动物
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
exports.searchAnimalsByFarmName = async (req, res) => {
|
||||
try {
|
||||
const { farmName } = req.query;
|
||||
|
||||
if (!farmName) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供养殖场名称参数'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`开始搜索养殖场名称包含 "${farmName}" 的动物...`);
|
||||
|
||||
// 首先找到匹配的养殖场
|
||||
const farms = await Farm.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[require('sequelize').Op.like]: `%${farmName}%`
|
||||
}
|
||||
},
|
||||
attributes: ['id', 'name']
|
||||
});
|
||||
|
||||
if (farms.length === 0) {
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
data: [],
|
||||
message: '未找到匹配的养殖场'
|
||||
});
|
||||
}
|
||||
|
||||
const farmIds = farms.map(farm => farm.id);
|
||||
|
||||
// 根据养殖场ID查找动物
|
||||
const animals = await Animal.findAll({
|
||||
where: {
|
||||
farm_id: {
|
||||
[require('sequelize').Op.in]: farmIds
|
||||
}
|
||||
},
|
||||
include: [{ model: Farm, as: 'farm', attributes: ['id', 'name', 'location'] }],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
console.log(`找到 ${animals.length} 个匹配的动物`);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: animals,
|
||||
message: `找到 ${animals.length} 个养殖场名称包含 "${farmName}" 的动物`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('根据养殖场名称搜索动物失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '搜索动物失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个动物
|
||||
* @param {Object} req - 请求对象
|
||||
|
||||
448
backend/controllers/backupController.js
Normal file
448
backend/controllers/backupController.js
Normal file
@@ -0,0 +1,448 @@
|
||||
/**
|
||||
* 备份管理控制器
|
||||
* @file backupController.js
|
||||
* @description 处理数据备份和恢复相关业务逻辑
|
||||
*/
|
||||
const backupService = require('../services/backupService');
|
||||
const logger = require('../utils/logger');
|
||||
const { validationResult } = require('express-validator');
|
||||
const cron = require('node-cron');
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
* @route POST /api/backup/create
|
||||
*/
|
||||
const createBackup = async (req, res) => {
|
||||
try {
|
||||
const { type = 'full', description } = req.body;
|
||||
|
||||
logger.info(`用户 ${req.user.username} 开始创建备份,类型: ${type}`);
|
||||
|
||||
const backupResult = await backupService.createFullBackup({
|
||||
type,
|
||||
description,
|
||||
createdBy: req.user.id
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '备份创建成功',
|
||||
data: backupResult
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('创建备份失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || '创建备份失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取备份列表
|
||||
* @route GET /api/backup/list
|
||||
*/
|
||||
const getBackupList = async (req, res) => {
|
||||
try {
|
||||
const { page = 1, limit = 10, type } = req.query;
|
||||
|
||||
let backups = await backupService.getBackupList();
|
||||
|
||||
// 按类型过滤
|
||||
if (type) {
|
||||
backups = backups.filter(backup => backup.type === type);
|
||||
}
|
||||
|
||||
// 分页
|
||||
const startIndex = (page - 1) * limit;
|
||||
const endIndex = startIndex + parseInt(limit);
|
||||
const paginatedBackups = backups.slice(startIndex, endIndex);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: paginatedBackups,
|
||||
total: backups.length,
|
||||
page: parseInt(page),
|
||||
limit: parseInt(limit)
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取备份列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取备份列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取备份统计
|
||||
* @route GET /api/backup/stats
|
||||
*/
|
||||
const getBackupStats = async (req, res) => {
|
||||
try {
|
||||
const stats = await backupService.getBackupStats();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: stats
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取备份统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取备份统计失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 删除备份
|
||||
* @route DELETE /api/backup/:id
|
||||
*/
|
||||
const deleteBackup = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const result = await backupService.deleteBackup(id);
|
||||
|
||||
if (result) {
|
||||
logger.info(`用户 ${req.user.username} 删除备份: ${id}`);
|
||||
res.json({
|
||||
success: true,
|
||||
message: '备份删除成功'
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
message: '备份不存在或删除失败'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('删除备份失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除备份失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 恢复数据库
|
||||
* @route POST /api/backup/:id/restore
|
||||
*/
|
||||
const restoreBackup = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { confirm } = req.body;
|
||||
|
||||
if (!confirm) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请确认恢复操作'
|
||||
});
|
||||
}
|
||||
|
||||
logger.info(`用户 ${req.user.username} 开始恢复备份: ${id}`);
|
||||
|
||||
const result = await backupService.restoreDatabase(id);
|
||||
|
||||
if (result) {
|
||||
res.json({
|
||||
success: true,
|
||||
message: '数据恢复成功'
|
||||
});
|
||||
} else {
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '数据恢复失败'
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error('恢复备份失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '恢复备份失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 下载备份文件
|
||||
* @route GET /api/backup/:id/download
|
||||
*/
|
||||
const downloadBackup = async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const archivePath = path.join(__dirname, '../../backups', `${id}.zip`);
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!fs.existsSync(archivePath)) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '备份文件不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const stats = fs.statSync(archivePath);
|
||||
const filename = `backup_${id}.zip`;
|
||||
|
||||
res.setHeader('Content-Type', 'application/zip');
|
||||
res.setHeader('Content-Disposition', `attachment; filename="${filename}"`);
|
||||
res.setHeader('Content-Length', stats.size);
|
||||
|
||||
const readStream = fs.createReadStream(archivePath);
|
||||
readStream.pipe(res);
|
||||
|
||||
logger.info(`用户 ${req.user.username} 下载备份: ${id}`);
|
||||
} catch (error) {
|
||||
logger.error('下载备份失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '下载备份失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 清理过期备份
|
||||
* @route POST /api/backup/cleanup
|
||||
*/
|
||||
const cleanupBackups = async (req, res) => {
|
||||
try {
|
||||
const deletedCount = await backupService.cleanupExpiredBackups();
|
||||
|
||||
logger.info(`用户 ${req.user.username} 执行备份清理,删除了 ${deletedCount} 个过期备份`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `清理完成,删除了 ${deletedCount} 个过期备份`,
|
||||
data: { deletedCount }
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('清理备份失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '清理备份失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取备份健康状态
|
||||
* @route GET /api/backup/health
|
||||
*/
|
||||
const getBackupHealth = async (req, res) => {
|
||||
try {
|
||||
const health = await backupService.checkBackupHealth();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: health
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取备份健康状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取备份健康状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动备份任务调度器
|
||||
*/
|
||||
class BackupScheduler {
|
||||
constructor() {
|
||||
this.tasks = new Map();
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动自动备份调度
|
||||
*/
|
||||
start() {
|
||||
// 每日备份(凌晨2点)
|
||||
const dailyTask = cron.schedule('0 2 * * *', async () => {
|
||||
try {
|
||||
logger.info('开始执行自动日备份');
|
||||
await backupService.createFullBackup({ type: 'daily', description: '自动日备份' });
|
||||
logger.info('自动日备份完成');
|
||||
} catch (error) {
|
||||
logger.error('自动日备份失败:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: false
|
||||
});
|
||||
|
||||
// 每周备份(周日凌晨3点)
|
||||
const weeklyTask = cron.schedule('0 3 * * 0', async () => {
|
||||
try {
|
||||
logger.info('开始执行自动周备份');
|
||||
await backupService.createFullBackup({ type: 'weekly', description: '自动周备份' });
|
||||
logger.info('自动周备份完成');
|
||||
} catch (error) {
|
||||
logger.error('自动周备份失败:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: false
|
||||
});
|
||||
|
||||
// 每月备份(每月1号凌晨4点)
|
||||
const monthlyTask = cron.schedule('0 4 1 * *', async () => {
|
||||
try {
|
||||
logger.info('开始执行自动月备份');
|
||||
await backupService.createFullBackup({ type: 'monthly', description: '自动月备份' });
|
||||
logger.info('自动月备份完成');
|
||||
} catch (error) {
|
||||
logger.error('自动月备份失败:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: false
|
||||
});
|
||||
|
||||
// 自动清理任务(每天凌晨5点)
|
||||
const cleanupTask = cron.schedule('0 5 * * *', async () => {
|
||||
try {
|
||||
logger.info('开始执行自动备份清理');
|
||||
const deletedCount = await backupService.cleanupExpiredBackups();
|
||||
logger.info(`自动备份清理完成,删除了 ${deletedCount} 个过期备份`);
|
||||
} catch (error) {
|
||||
logger.error('自动备份清理失败:', error);
|
||||
}
|
||||
}, {
|
||||
scheduled: false
|
||||
});
|
||||
|
||||
this.tasks.set('daily', dailyTask);
|
||||
this.tasks.set('weekly', weeklyTask);
|
||||
this.tasks.set('monthly', monthlyTask);
|
||||
this.tasks.set('cleanup', cleanupTask);
|
||||
|
||||
// 启动所有任务
|
||||
this.tasks.forEach((task, name) => {
|
||||
task.start();
|
||||
logger.info(`备份调度任务已启动: ${name}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止自动备份调度
|
||||
*/
|
||||
stop() {
|
||||
this.tasks.forEach((task, name) => {
|
||||
task.stop();
|
||||
logger.info(`备份调度任务已停止: ${name}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调度状态
|
||||
*/
|
||||
getStatus() {
|
||||
const status = {};
|
||||
this.tasks.forEach((task, name) => {
|
||||
status[name] = {
|
||||
running: task.running,
|
||||
lastExecution: task.lastExecution,
|
||||
nextExecution: task.nextExecution
|
||||
};
|
||||
});
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建调度器实例
|
||||
const scheduler = new BackupScheduler();
|
||||
|
||||
/**
|
||||
* 启动自动备份调度
|
||||
* @route POST /api/backup/schedule/start
|
||||
*/
|
||||
const startScheduler = async (req, res) => {
|
||||
try {
|
||||
scheduler.start();
|
||||
logger.info(`用户 ${req.user.username} 启动自动备份调度`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '自动备份调度已启动'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('启动备份调度失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '启动备份调度失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止自动备份调度
|
||||
* @route POST /api/backup/schedule/stop
|
||||
*/
|
||||
const stopScheduler = async (req, res) => {
|
||||
try {
|
||||
scheduler.stop();
|
||||
logger.info(`用户 ${req.user.username} 停止自动备份调度`);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '自动备份调度已停止'
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('停止备份调度失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '停止备份调度失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取调度状态
|
||||
* @route GET /api/backup/schedule/status
|
||||
*/
|
||||
const getSchedulerStatus = async (req, res) => {
|
||||
try {
|
||||
const status = scheduler.getStatus();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: status
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('获取调度状态失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取调度状态失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createBackup,
|
||||
getBackupList,
|
||||
getBackupStats,
|
||||
deleteBackup,
|
||||
restoreBackup,
|
||||
downloadBackup,
|
||||
cleanupBackups,
|
||||
getBackupHealth,
|
||||
startScheduler,
|
||||
stopScheduler,
|
||||
getSchedulerStatus,
|
||||
scheduler
|
||||
};
|
||||
414
backend/controllers/bindingController.js
Normal file
414
backend/controllers/bindingController.js
Normal file
@@ -0,0 +1,414 @@
|
||||
/**
|
||||
* 绑定信息控制器
|
||||
* @file bindingController.js
|
||||
* @description 处理耳标与牛只档案的绑定信息查询
|
||||
*/
|
||||
|
||||
const { IotJbqClient, IotCattle, Farm, CattlePen, CattleBatch } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 获取耳标绑定信息
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const getBindingInfo = async (req, res) => {
|
||||
try {
|
||||
const { cid } = req.params;
|
||||
|
||||
if (!cid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '耳标编号不能为空',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 查询耳标信息
|
||||
const jbqDevice = await IotJbqClient.findOne({
|
||||
where: { cid: cid },
|
||||
attributes: [
|
||||
'id', 'cid', 'aaid', 'org_id', 'uid', 'time', 'uptime', 'sid',
|
||||
'walk', 'y_steps', 'r_walk', 'lat', 'lon', 'gps_state', 'voltage',
|
||||
'temperature', 'temperature_two', 'state', 'type', 'sort', 'ver',
|
||||
'weight', 'start_time', 'run_days', 'zenowalk', 'zenotime',
|
||||
'is_read', 'read_end_time', 'bank_userid', 'bank_item_id',
|
||||
'bank_house', 'bank_lanwei', 'bank_place', 'is_home',
|
||||
'distribute_time', 'bandge_status', 'is_wear', 'is_temperature',
|
||||
'source_id', 'expire_time'
|
||||
]
|
||||
});
|
||||
|
||||
if (!jbqDevice) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '未找到指定的耳标设备',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 查询绑定的牛只档案信息
|
||||
const cattleInfo = await IotCattle.findOne({
|
||||
where: { ear_number: cid },
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name', 'address', 'contact', 'phone']
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'pen',
|
||||
attributes: ['id', 'name', 'description']
|
||||
},
|
||||
{
|
||||
model: CattleBatch,
|
||||
as: 'batch',
|
||||
attributes: ['id', 'name', 'description', 'start_date', 'end_date']
|
||||
}
|
||||
],
|
||||
attributes: [
|
||||
'id', 'orgId', 'earNumber', 'sex', 'strain', 'varieties', 'cate',
|
||||
'birthWeight', 'birthday', 'penId', 'intoTime', 'parity', 'source',
|
||||
'sourceDay', 'sourceWeight', 'weight', 'event', 'eventTime',
|
||||
'lactationDay', 'semenNum', 'isWear', 'batchId', 'imgs',
|
||||
'isEleAuth', 'isQuaAuth', 'isDelete', 'isOut', 'createUid',
|
||||
'createTime', 'algebra', 'colour', 'infoWeight', 'descent',
|
||||
'isVaccin', 'isInsemination', 'isInsure', 'isMortgage',
|
||||
'updateTime', 'breedBullTime', 'level', 'sixWeight',
|
||||
'eighteenWeight', 'twelveDayWeight', 'eighteenDayWeight',
|
||||
'xxivDayWeight', 'semenBreedImgs', 'sellStatus',
|
||||
'weightCalculateTime', 'dayOfBirthday'
|
||||
]
|
||||
});
|
||||
|
||||
// 构建响应数据
|
||||
const bindingInfo = {
|
||||
device: {
|
||||
id: jbqDevice.id,
|
||||
cid: jbqDevice.cid,
|
||||
aaid: jbqDevice.aaid,
|
||||
orgId: jbqDevice.org_id,
|
||||
uid: jbqDevice.uid,
|
||||
time: jbqDevice.time,
|
||||
uptime: jbqDevice.uptime,
|
||||
sid: jbqDevice.sid,
|
||||
walk: jbqDevice.walk,
|
||||
ySteps: jbqDevice.y_steps,
|
||||
rWalk: jbqDevice.r_walk,
|
||||
lat: jbqDevice.lat,
|
||||
lon: jbqDevice.lon,
|
||||
gpsState: jbqDevice.gps_state,
|
||||
voltage: jbqDevice.voltage,
|
||||
temperature: jbqDevice.temperature,
|
||||
temperatureTwo: jbqDevice.temperature_two,
|
||||
state: jbqDevice.state,
|
||||
type: jbqDevice.type,
|
||||
sort: jbqDevice.sort,
|
||||
ver: jbqDevice.ver,
|
||||
weight: jbqDevice.weight,
|
||||
startTime: jbqDevice.start_time,
|
||||
runDays: jbqDevice.run_days,
|
||||
zenowalk: jbqDevice.zenowalk,
|
||||
zenotime: jbqDevice.zenotime,
|
||||
isRead: jbqDevice.is_read,
|
||||
readEndTime: jbqDevice.read_end_time,
|
||||
bankUserid: jbqDevice.bank_userid,
|
||||
bankItemId: jbqDevice.bank_item_id,
|
||||
bankHouse: jbqDevice.bank_house,
|
||||
bankLanwei: jbqDevice.bank_lanwei,
|
||||
bankPlace: jbqDevice.bank_place,
|
||||
isHome: jbqDevice.is_home,
|
||||
distributeTime: jbqDevice.distribute_time,
|
||||
bandgeStatus: jbqDevice.bandge_status,
|
||||
isWear: jbqDevice.is_wear,
|
||||
isTemperature: jbqDevice.is_temperature,
|
||||
sourceId: jbqDevice.source_id,
|
||||
expireTime: jbqDevice.expire_time
|
||||
},
|
||||
cattle: cattleInfo ? {
|
||||
id: cattleInfo.id,
|
||||
orgId: cattleInfo.orgId,
|
||||
earNumber: cattleInfo.earNumber,
|
||||
sex: cattleInfo.sex,
|
||||
strain: cattleInfo.strain,
|
||||
varieties: cattleInfo.varieties,
|
||||
cate: cattleInfo.cate,
|
||||
birthWeight: cattleInfo.birthWeight,
|
||||
birthday: cattleInfo.birthday,
|
||||
penId: cattleInfo.penId,
|
||||
intoTime: cattleInfo.intoTime,
|
||||
parity: cattleInfo.parity,
|
||||
source: cattleInfo.source,
|
||||
sourceDay: cattleInfo.sourceDay,
|
||||
sourceWeight: cattleInfo.sourceWeight,
|
||||
weight: cattleInfo.weight,
|
||||
event: cattleInfo.event,
|
||||
eventTime: cattleInfo.eventTime,
|
||||
lactationDay: cattleInfo.lactationDay,
|
||||
semenNum: cattleInfo.semenNum,
|
||||
isWear: cattleInfo.isWear,
|
||||
batchId: cattleInfo.batchId,
|
||||
imgs: cattleInfo.imgs,
|
||||
isEleAuth: cattleInfo.isEleAuth,
|
||||
isQuaAuth: cattleInfo.isQuaAuth,
|
||||
isDelete: cattleInfo.isDelete,
|
||||
isOut: cattleInfo.isOut,
|
||||
createUid: cattleInfo.createUid,
|
||||
createTime: cattleInfo.createTime,
|
||||
algebra: cattleInfo.algebra,
|
||||
colour: cattleInfo.colour,
|
||||
infoWeight: cattleInfo.infoWeight,
|
||||
descent: cattleInfo.descent,
|
||||
isVaccin: cattleInfo.isVaccin,
|
||||
isInsemination: cattleInfo.isInsemination,
|
||||
isInsure: cattleInfo.isInsure,
|
||||
isMortgage: cattleInfo.isMortgage,
|
||||
updateTime: cattleInfo.updateTime,
|
||||
breedBullTime: cattleInfo.breedBullTime,
|
||||
level: cattleInfo.level,
|
||||
sixWeight: cattleInfo.sixWeight,
|
||||
eighteenWeight: cattleInfo.eighteenWeight,
|
||||
twelveDayWeight: cattleInfo.twelveDayWeight,
|
||||
eighteenDayWeight: cattleInfo.eighteenDayWeight,
|
||||
xxivDayWeight: cattleInfo.xxivDayWeight,
|
||||
semenBreedImgs: cattleInfo.semenBreedImgs,
|
||||
sellStatus: cattleInfo.sellStatus,
|
||||
weightCalculateTime: cattleInfo.weightCalculateTime,
|
||||
dayOfBirthday: cattleInfo.dayOfBirthday,
|
||||
farm: cattleInfo.farm,
|
||||
pen: cattleInfo.pen,
|
||||
batch: cattleInfo.batch
|
||||
} : null,
|
||||
isBound: !!cattleInfo,
|
||||
bindingStatus: cattleInfo ? '已绑定' : '未绑定'
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取绑定信息成功',
|
||||
data: bindingInfo,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取绑定信息失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取绑定信息失败: ' + error.message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有绑定状态统计
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const getBindingStats = async (req, res) => {
|
||||
try {
|
||||
// 统计绑定状态
|
||||
const stats = await IotJbqClient.findAll({
|
||||
attributes: [
|
||||
'bandge_status',
|
||||
[IotJbqClient.sequelize.fn('COUNT', IotJbqClient.sequelize.col('id')), 'count']
|
||||
],
|
||||
group: ['bandge_status'],
|
||||
raw: true
|
||||
});
|
||||
|
||||
// 统计匹配情况
|
||||
const matchStats = await IotJbqClient.findAll({
|
||||
attributes: [
|
||||
[IotJbqClient.sequelize.fn('COUNT', IotJbqClient.sequelize.col('IotJbqClient.id')), 'total_jbq'],
|
||||
[IotJbqClient.sequelize.fn('COUNT', IotJbqClient.sequelize.col('cattle.id')), 'matched_count']
|
||||
],
|
||||
include: [
|
||||
{
|
||||
model: IotCattle,
|
||||
as: 'cattle',
|
||||
attributes: [],
|
||||
required: false,
|
||||
where: {
|
||||
earNumber: IotJbqClient.sequelize.col('IotJbqClient.cid')
|
||||
}
|
||||
}
|
||||
],
|
||||
raw: true
|
||||
});
|
||||
|
||||
const result = {
|
||||
bindingStats: stats.map(stat => ({
|
||||
status: stat.bandge_status === 1 ? '已绑定' : '未绑定',
|
||||
count: parseInt(stat.count)
|
||||
})),
|
||||
matchStats: {
|
||||
totalJbq: parseInt(matchStats[0]?.total_jbq || 0),
|
||||
matchedCount: parseInt(matchStats[0]?.matched_count || 0),
|
||||
matchRate: matchStats[0]?.total_jbq > 0
|
||||
? ((matchStats[0]?.matched_count / matchStats[0]?.total_jbq) * 100).toFixed(2) + '%'
|
||||
: '0%'
|
||||
}
|
||||
};
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '获取绑定统计成功',
|
||||
data: result,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('获取绑定统计失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取绑定统计失败: ' + error.message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 手动绑定耳标与牛只档案
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const bindCattle = async (req, res) => {
|
||||
try {
|
||||
const { cid, cattleId } = req.body;
|
||||
|
||||
if (!cid || !cattleId) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '耳标编号和牛只ID不能为空',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 检查耳标是否存在
|
||||
const jbqDevice = await IotJbqClient.findOne({
|
||||
where: { cid: cid }
|
||||
});
|
||||
|
||||
if (!jbqDevice) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '未找到指定的耳标设备',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 检查牛只档案是否存在
|
||||
const cattle = await IotCattle.findByPk(cattleId);
|
||||
if (!cattle) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '未找到指定的牛只档案',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 更新牛只档案的耳标号
|
||||
await cattle.update({
|
||||
earNumber: cid,
|
||||
updateTime: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
|
||||
// 更新耳标的绑定状态
|
||||
await jbqDevice.update({
|
||||
bandge_status: 1
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '绑定成功',
|
||||
data: {
|
||||
cid: cid,
|
||||
cattleId: cattleId,
|
||||
bindingStatus: '已绑定'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('绑定失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '绑定失败: ' + error.message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 解绑耳标与牛只档案
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
const unbindCattle = async (req, res) => {
|
||||
try {
|
||||
const { cid } = req.params;
|
||||
|
||||
if (!cid) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '耳标编号不能为空',
|
||||
data: null
|
||||
});
|
||||
}
|
||||
|
||||
// 查找绑定的牛只档案
|
||||
const cattle = await IotCattle.findOne({
|
||||
where: { earNumber: cid }
|
||||
});
|
||||
|
||||
if (cattle) {
|
||||
// 清除牛只档案的耳标号
|
||||
await cattle.update({
|
||||
earNumber: null,
|
||||
updateTime: Math.floor(Date.now() / 1000)
|
||||
});
|
||||
}
|
||||
|
||||
// 更新耳标的绑定状态
|
||||
const jbqDevice = await IotJbqClient.findOne({
|
||||
where: { cid: cid }
|
||||
});
|
||||
|
||||
if (jbqDevice) {
|
||||
await jbqDevice.update({
|
||||
bandge_status: 0
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '解绑成功',
|
||||
data: {
|
||||
cid: cid,
|
||||
bindingStatus: '未绑定'
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('解绑失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '解绑失败: ' + error.message,
|
||||
data: null,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getBindingInfo,
|
||||
getBindingStats,
|
||||
bindCattle,
|
||||
unbindCattle
|
||||
};
|
||||
565
backend/controllers/cattleBatchController.js
Normal file
565
backend/controllers/cattleBatchController.js
Normal file
@@ -0,0 +1,565 @@
|
||||
const { CattleBatch, IotCattle, Farm, CattleBatchAnimal, User } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 批次设置控制器
|
||||
*/
|
||||
class CattleBatchController {
|
||||
/**
|
||||
* 获取批次列表
|
||||
*/
|
||||
async getBatches(req, res) {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, search, type, status } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
console.log('🔍 [后端-批次设置] 搜索请求参数:', { page, pageSize, search, type, status });
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${search}%` } },
|
||||
{ code: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
console.log('🔍 [后端-批次设置] 搜索条件:', where[Op.or]);
|
||||
}
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
|
||||
const { count, rows } = await CattleBatch.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
console.log('🔍 [后端-批次设置] 查询结果:', {
|
||||
总数: count,
|
||||
当前页数据量: rows.length,
|
||||
搜索关键词: search,
|
||||
查询条件: where
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取批次列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取批次列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取批次列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批次详情
|
||||
*/
|
||||
async getBatchById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const batch = await CattleBatch.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!batch) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: batch,
|
||||
message: '获取批次详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取批次详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取批次详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建批次
|
||||
*/
|
||||
async createBatch(req, res) {
|
||||
try {
|
||||
console.log('🆕 [后端-批次设置] 开始创建操作');
|
||||
console.log('📋 [后端-批次设置] 请求数据:', req.body);
|
||||
|
||||
const {
|
||||
name,
|
||||
code,
|
||||
type,
|
||||
startDate,
|
||||
expectedEndDate,
|
||||
actualEndDate,
|
||||
targetCount,
|
||||
currentCount,
|
||||
manager,
|
||||
status,
|
||||
remark,
|
||||
farmId
|
||||
} = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!name || !code || !type || !startDate || !targetCount || !manager) {
|
||||
console.log('❌ [后端-批次设置] 必填字段验证失败:', {
|
||||
name: !!name,
|
||||
code: !!code,
|
||||
type: !!type,
|
||||
startDate: !!startDate,
|
||||
targetCount: !!targetCount,
|
||||
manager: !!manager
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请填写所有必填字段(批次名称、编号、类型、开始日期、目标数量、负责人)'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查批次编号是否已存在
|
||||
const existingBatch = await CattleBatch.findOne({
|
||||
where: { code }
|
||||
});
|
||||
|
||||
if (existingBatch) {
|
||||
console.log('❌ [后端-批次设置] 批次编号已存在:', code);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '批次编号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查农场是否存在
|
||||
const farm = await Farm.findByPk(farmId);
|
||||
if (!farm) {
|
||||
console.log('❌ [后端-批次设置] 农场不存在:', farmId);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '农场不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 准备创建数据
|
||||
const createData = {
|
||||
name,
|
||||
code,
|
||||
type,
|
||||
startDate: new Date(startDate),
|
||||
expectedEndDate: expectedEndDate ? new Date(expectedEndDate) : null,
|
||||
actualEndDate: actualEndDate ? new Date(actualEndDate) : null,
|
||||
targetCount: parseInt(targetCount),
|
||||
currentCount: currentCount ? parseInt(currentCount) : 0,
|
||||
manager,
|
||||
status: status || '进行中',
|
||||
remark: remark || '',
|
||||
farmId: farmId || 1
|
||||
};
|
||||
|
||||
console.log('📝 [后端-批次设置] 准备创建的数据:', createData);
|
||||
|
||||
const batch = await CattleBatch.create(createData);
|
||||
|
||||
console.log('✅ [后端-批次设置] 批次创建成功:', batch.id);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: batch,
|
||||
message: '创建批次成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-批次设置] 创建失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建批次失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新批次
|
||||
*/
|
||||
async updateBatch(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.log('🔄 [后端-批次设置] 开始更新操作');
|
||||
console.log('📋 [后端-批次设置] 请求参数:', {
|
||||
batchId: id,
|
||||
updateData: updateData
|
||||
});
|
||||
|
||||
const batch = await CattleBatch.findByPk(id);
|
||||
if (!batch) {
|
||||
console.log('❌ [后端-批次设置] 批次不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📝 [后端-批次设置] 原始批次数据:', {
|
||||
id: batch.id,
|
||||
name: batch.name,
|
||||
code: batch.code,
|
||||
type: batch.type,
|
||||
description: batch.description,
|
||||
status: batch.status,
|
||||
startDate: batch.startDate,
|
||||
expectedEndDate: batch.expectedEndDate,
|
||||
actualEndDate: batch.actualEndDate,
|
||||
targetCount: batch.targetCount,
|
||||
currentCount: batch.currentCount,
|
||||
manager: batch.manager,
|
||||
remark: batch.remark,
|
||||
farmId: batch.farmId
|
||||
});
|
||||
|
||||
// 如果更新编号,检查是否已存在
|
||||
if (updateData.code && updateData.code !== batch.code) {
|
||||
console.log('🔄 [后端-批次设置] 检测到编号变更,检查是否已存在');
|
||||
console.log('📝 [后端-批次设置] 编号变更详情:', {
|
||||
oldCode: batch.code,
|
||||
newCode: updateData.code
|
||||
});
|
||||
|
||||
const existingBatch = await CattleBatch.findOne({
|
||||
where: { code: updateData.code, id: { [Op.ne]: id } }
|
||||
});
|
||||
|
||||
if (existingBatch) {
|
||||
console.log('❌ [后端-批次设置] 批次编号已存在');
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '批次编号已存在'
|
||||
});
|
||||
}
|
||||
console.log('✅ [后端-批次设置] 批次编号可用');
|
||||
}
|
||||
|
||||
await batch.update(updateData);
|
||||
console.log('✅ [后端-批次设置] 批次更新成功');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: batch,
|
||||
message: '更新批次成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-批次设置] 更新失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新批次失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除批次
|
||||
*/
|
||||
async deleteBatch(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const batch = await CattleBatch.findByPk(id);
|
||||
if (!batch) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有牛只在批次中
|
||||
const animalCount = await CattleBatchAnimal.count({
|
||||
where: { batchId: id }
|
||||
});
|
||||
|
||||
if (animalCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '批次中还有牛只,无法删除'
|
||||
});
|
||||
}
|
||||
|
||||
await batch.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除批次成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除批次失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除批次失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除批次
|
||||
*/
|
||||
async batchDeleteBatches(req, res) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要删除的批次'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有批次包含牛只
|
||||
const animalCount = await CattleBatchAnimal.count({
|
||||
where: { batchId: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
if (animalCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '部分批次中还有牛只,无法删除'
|
||||
});
|
||||
}
|
||||
|
||||
await CattleBatch.destroy({
|
||||
where: { id: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 个批次`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除批次失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量删除批次失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取批次中的牛只
|
||||
*/
|
||||
async getBatchAnimals(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 检查批次是否存在
|
||||
const batch = await CattleBatch.findByPk(id);
|
||||
if (!batch) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取批次中的牛只
|
||||
const { count, rows } = await IotCattle.findAndCountAll({
|
||||
include: [
|
||||
{
|
||||
model: CattleBatchAnimal,
|
||||
as: 'batchAnimals',
|
||||
where: { batchId: id },
|
||||
attributes: ['id', 'joinDate', 'notes']
|
||||
},
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
attributes: ['id', 'earNumber', 'sex', 'strain', 'orgId'],
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取批次牛只成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取批次牛只失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取批次牛只失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加牛只到批次
|
||||
*/
|
||||
async addAnimalsToBatch(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { animalIds } = req.body;
|
||||
|
||||
if (!animalIds || !Array.isArray(animalIds) || animalIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要添加的牛只'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查批次是否存在
|
||||
const batch = await CattleBatch.findByPk(id);
|
||||
if (!batch) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查牛只是否存在
|
||||
const animals = await IotCattle.findAll({
|
||||
where: { id: { [Op.in]: animalIds } }
|
||||
});
|
||||
|
||||
if (animals.length !== animalIds.length) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '部分牛只不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查哪些牛只已经在批次中
|
||||
const existingAssociations = await CattleBatchAnimal.findAll({
|
||||
where: {
|
||||
batchId: id,
|
||||
animalId: { [Op.in]: animalIds }
|
||||
}
|
||||
});
|
||||
|
||||
const existingAnimalIds = existingAssociations.map(assoc => assoc.animalId);
|
||||
const newAnimalIds = animalIds.filter(id => !existingAnimalIds.includes(id));
|
||||
|
||||
if (newAnimalIds.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '所有牛只都已在该批次中'
|
||||
});
|
||||
}
|
||||
|
||||
// 添加新的关联
|
||||
const associations = newAnimalIds.map(animalId => ({
|
||||
batchId: id,
|
||||
animalId: animalId,
|
||||
joinDate: new Date()
|
||||
}));
|
||||
|
||||
await CattleBatchAnimal.bulkCreate(associations);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功添加 ${newAnimalIds.length} 头牛只到批次`,
|
||||
data: {
|
||||
addedCount: newAnimalIds.length,
|
||||
skippedCount: existingAnimalIds.length
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('添加牛只到批次失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '添加牛只到批次失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从批次中移除牛只
|
||||
*/
|
||||
async removeAnimalFromBatch(req, res) {
|
||||
try {
|
||||
const { id, animalId } = req.params;
|
||||
|
||||
// 检查批次是否存在
|
||||
const batch = await CattleBatch.findByPk(id);
|
||||
if (!batch) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '批次不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查关联是否存在
|
||||
const association = await CattleBatchAnimal.findOne({
|
||||
where: { batchId: id, animalId }
|
||||
});
|
||||
|
||||
if (!association) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '牛只不在该批次中'
|
||||
});
|
||||
}
|
||||
|
||||
await association.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '从批次中移除牛只成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('从批次中移除牛只失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '从批次中移除牛只失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CattleBatchController();
|
||||
540
backend/controllers/cattleExitRecordController.js
Normal file
540
backend/controllers/cattleExitRecordController.js
Normal file
@@ -0,0 +1,540 @@
|
||||
const { CattleExitRecord, IotCattle, CattlePen, Farm } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 离栏记录控制器
|
||||
*/
|
||||
class CattleExitRecordController {
|
||||
/**
|
||||
* 获取离栏记录列表
|
||||
*/
|
||||
async getExitRecords(req, res) {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, search, exitReason, status, dateRange } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
console.log('🔍 [后端-离栏记录] 搜索关键词:', search);
|
||||
where[Op.or] = [
|
||||
{ recordId: { [Op.like]: `%${search}%` } },
|
||||
{ earNumber: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
console.log('🔍 [后端-离栏记录] 搜索条件构建完成');
|
||||
}
|
||||
if (exitReason) {
|
||||
where.exitReason = exitReason;
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
if (dateRange && dateRange.length === 2) {
|
||||
where.exitDate = {
|
||||
[Op.between]: [dateRange[0], dateRange[1]]
|
||||
};
|
||||
}
|
||||
|
||||
console.log('🔍 [后端-离栏记录] 搜索请求参数:', {
|
||||
search,
|
||||
exitReason,
|
||||
status,
|
||||
dateRange,
|
||||
page,
|
||||
pageSize
|
||||
});
|
||||
|
||||
console.log('🔍 [后端-离栏记录] 构建的查询条件:', JSON.stringify(where, null, 2));
|
||||
|
||||
console.log('🔍 [后端-离栏记录] 开始执行查询...');
|
||||
const { count, rows } = await CattleExitRecord.findAndCountAll({
|
||||
where,
|
||||
attributes: ['id', 'recordId', 'animalId', 'earNumber', 'exitDate', 'exitReason', 'originalPenId', 'destination', 'disposalMethod', 'handler', 'status', 'remark', 'farmId', 'created_at', 'updated_at'],
|
||||
include: [
|
||||
{
|
||||
model: IotCattle,
|
||||
as: 'cattle',
|
||||
attributes: ['id', 'earNumber', 'strain', 'sex'],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'originalPen',
|
||||
attributes: ['id', 'name', 'code'],
|
||||
required: false
|
||||
},
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name'],
|
||||
required: false
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset),
|
||||
order: [['exit_date', 'DESC']]
|
||||
});
|
||||
|
||||
console.log('📊 [后端-离栏记录] 查询结果:', {
|
||||
总数: count,
|
||||
当前页记录数: rows.length,
|
||||
记录列表: rows.map(item => ({
|
||||
id: item.id,
|
||||
recordId: item.recordId,
|
||||
earNumber: item.earNumber,
|
||||
exitReason: item.exitReason,
|
||||
status: item.status
|
||||
}))
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取离栏记录列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取离栏记录列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取离栏记录列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个离栏记录详情
|
||||
*/
|
||||
async getExitRecordById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const record = await CattleExitRecord.findByPk(id, {
|
||||
attributes: ['id', 'recordId', 'animalId', 'earNumber', 'exitDate', 'exitReason', 'originalPenId', 'destination', 'disposalMethod', 'handler', 'status', 'remark', 'farmId', 'created_at', 'updated_at'],
|
||||
include: [
|
||||
{
|
||||
model: IotCattle,
|
||||
as: 'cattle',
|
||||
attributes: ['id', 'earNumber', 'strain', 'sex']
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'originalPen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '离栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '获取离栏记录详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取离栏记录详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取离栏记录详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建离栏记录
|
||||
*/
|
||||
async createExitRecord(req, res) {
|
||||
try {
|
||||
console.log('🆕 [后端-离栏记录] 开始创建操作');
|
||||
console.log('📋 [后端-离栏记录] 请求数据:', req.body);
|
||||
|
||||
const recordData = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!recordData.earNumber || !recordData.exitDate || !recordData.exitReason || !recordData.originalPenId || !recordData.handler) {
|
||||
console.log('❌ [后端-离栏记录] 必填字段验证失败:', {
|
||||
earNumber: !!recordData.earNumber,
|
||||
exitDate: !!recordData.exitDate,
|
||||
exitReason: !!recordData.exitReason,
|
||||
originalPenId: !!recordData.originalPenId,
|
||||
handler: !!recordData.handler
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请填写所有必填字段(牛只耳号、离栏日期、离栏原因、原栏舍、处理人员)'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成记录编号
|
||||
const recordId = `EX${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(Date.now()).slice(-3)}`;
|
||||
|
||||
// 通过耳号查找动物(支持数字和字符串类型)
|
||||
const earNumber = recordData.earNumber;
|
||||
const animal = await IotCattle.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ earNumber: earNumber },
|
||||
{ earNumber: parseInt(earNumber) },
|
||||
{ earNumber: earNumber.toString() }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!animal) {
|
||||
console.log('❌ [后端-离栏记录] 动物不存在,耳号:', recordData.earNumber);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '牛只不存在,请检查耳号是否正确'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ [后端-离栏记录] 找到动物:', {
|
||||
id: animal.id,
|
||||
earNumber: animal.earNumber,
|
||||
currentPenId: animal.penId
|
||||
});
|
||||
|
||||
// 检查原栏舍是否存在
|
||||
const originalPen = await CattlePen.findByPk(recordData.originalPenId);
|
||||
if (!originalPen) {
|
||||
console.log('❌ [后端-离栏记录] 原栏舍不存在:', recordData.originalPenId);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '原栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 准备创建数据
|
||||
const createData = {
|
||||
recordId,
|
||||
animalId: animal.id,
|
||||
earNumber: animal.earNumber,
|
||||
exitDate: new Date(recordData.exitDate),
|
||||
exitReason: recordData.exitReason,
|
||||
originalPenId: parseInt(recordData.originalPenId),
|
||||
destination: recordData.destination || '',
|
||||
disposalMethod: recordData.disposalMethod || '',
|
||||
handler: recordData.handler,
|
||||
status: recordData.status || '待确认',
|
||||
remark: recordData.remark || '',
|
||||
farmId: recordData.farmId || 1
|
||||
};
|
||||
|
||||
console.log('📝 [后端-离栏记录] 准备创建的数据:', createData);
|
||||
|
||||
const record = await CattleExitRecord.create(createData);
|
||||
|
||||
// 如果状态是已确认,将动物从栏舍中移除
|
||||
if (recordData.status === '已确认') {
|
||||
await animal.update({ penId: null });
|
||||
console.log('✅ [后端-离栏记录] 动物已从栏舍中移除');
|
||||
}
|
||||
|
||||
console.log('✅ [后端-离栏记录] 离栏记录创建成功:', record.id);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '创建离栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-离栏记录] 创建失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建离栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新离栏记录
|
||||
*/
|
||||
async updateExitRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.log('🔄 [后端-离栏记录] 开始更新操作');
|
||||
console.log('📋 [后端-离栏记录] 请求参数:', {
|
||||
recordId: id,
|
||||
updateData: updateData
|
||||
});
|
||||
|
||||
const record = await CattleExitRecord.findByPk(id);
|
||||
if (!record) {
|
||||
console.log('❌ [后端-离栏记录] 记录不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '离栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📝 [后端-离栏记录] 原始记录数据:', {
|
||||
id: record.id,
|
||||
animalId: record.animalId,
|
||||
earNumber: record.earNumber,
|
||||
exitDate: record.exitDate,
|
||||
exitReason: record.exitReason,
|
||||
originalPenId: record.originalPenId,
|
||||
destination: record.destination,
|
||||
disposalMethod: record.disposalMethod,
|
||||
handler: record.handler,
|
||||
status: record.status,
|
||||
remark: record.remark
|
||||
});
|
||||
|
||||
// 如果状态从非已确认变为已确认,将动物从栏舍中移除
|
||||
if (record.status !== '已确认' && updateData.status === '已确认') {
|
||||
console.log('🔄 [后端-离栏记录] 检测到状态变更为已确认,将动物从栏舍中移除');
|
||||
console.log('📝 [后端-离栏记录] 状态变更详情:', {
|
||||
oldStatus: record.status,
|
||||
newStatus: updateData.status,
|
||||
animalId: record.animalId
|
||||
});
|
||||
|
||||
const animal = await IotCattle.findByPk(record.animalId);
|
||||
if (animal) {
|
||||
await animal.update({ penId: null });
|
||||
console.log('✅ [后端-离栏记录] 动物栏舍清空成功');
|
||||
} else {
|
||||
console.log('⚠️ [后端-离栏记录] 未找到对应的动物记录');
|
||||
}
|
||||
}
|
||||
|
||||
await record.update(updateData);
|
||||
console.log('✅ [后端-离栏记录] 记录更新成功');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '更新离栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-离栏记录] 更新失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新离栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除离栏记录
|
||||
*/
|
||||
async deleteExitRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const record = await CattleExitRecord.findByPk(id);
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '离栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await record.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除离栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除离栏记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除离栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除离栏记录
|
||||
*/
|
||||
async batchDeleteExitRecords(req, res) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要删除的记录'
|
||||
});
|
||||
}
|
||||
|
||||
await CattleExitRecord.destroy({
|
||||
where: { id: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 条离栏记录`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除离栏记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量删除离栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认离栏记录
|
||||
*/
|
||||
async confirmExitRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const record = await CattleExitRecord.findByPk(id);
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '离栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
if (record.status === '已确认') {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '记录已确认,无需重复操作'
|
||||
});
|
||||
}
|
||||
|
||||
await record.update({ status: '已确认' });
|
||||
|
||||
// 将动物从栏舍中移除
|
||||
const animal = await IotCattle.findByPk(record.animalId);
|
||||
if (animal) {
|
||||
await animal.update({ penId: null });
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '确认离栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('确认离栏记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '确认离栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
*/
|
||||
async getAvailableIotCattles(req, res) {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
const where = {};
|
||||
|
||||
if (search) {
|
||||
where.earNumber = { [Op.like]: `%${search}%` };
|
||||
}
|
||||
|
||||
const animals = await IotCattle.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'pen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
}
|
||||
],
|
||||
attributes: ['id', 'earNumber', 'strain', 'sex', 'ageInMonths', 'physiologicalStage'],
|
||||
limit: 50,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: animals,
|
||||
message: '获取可用牛只列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取可用牛只列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取可用牛只列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
*/
|
||||
async getAvailableAnimals(req, res) {
|
||||
try {
|
||||
const { search, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ earNumber: { [Op.like]: `%${search}%` } },
|
||||
{ strain: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await IotCattle.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
attributes: ['id', 'earNumber', 'sex', 'strain', 'orgId'],
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取可用牛只列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取可用牛只列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取可用牛只列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CattleExitRecordController();
|
||||
415
backend/controllers/cattlePenController.js
Normal file
415
backend/controllers/cattlePenController.js
Normal file
@@ -0,0 +1,415 @@
|
||||
const { CattlePen, Farm, IotCattle } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 栏舍设置控制器
|
||||
*/
|
||||
class CattlePenController {
|
||||
/**
|
||||
* 获取栏舍列表
|
||||
*/
|
||||
async getPens(req, res) {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, search, status, type } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ name: { [Op.like]: `%${search}%` } },
|
||||
{ code: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
if (status) {
|
||||
where.status = status;
|
||||
}
|
||||
if (type) {
|
||||
where.type = type;
|
||||
}
|
||||
|
||||
const { count, rows } = await CattlePen.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取栏舍列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取栏舍列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取栏舍列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个栏舍详情
|
||||
*/
|
||||
async getPenById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const pen = await CattlePen.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!pen) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pen,
|
||||
message: '获取栏舍详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取栏舍详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取栏舍详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建栏舍
|
||||
*/
|
||||
async createPen(req, res) {
|
||||
try {
|
||||
console.log('🆕 [后端-栏舍设置] 开始创建操作');
|
||||
console.log('📋 [后端-栏舍设置] 请求数据:', req.body);
|
||||
|
||||
const penData = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!penData.name || !penData.code || !penData.type || !penData.capacity || !penData.area) {
|
||||
console.log('❌ [后端-栏舍设置] 必填字段验证失败:', {
|
||||
name: !!penData.name,
|
||||
code: !!penData.code,
|
||||
type: !!penData.type,
|
||||
capacity: !!penData.capacity,
|
||||
area: !!penData.area
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请填写所有必填字段(栏舍名称、编号、类型、容量、面积)'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查编号是否已存在
|
||||
const existingPen = await CattlePen.findOne({
|
||||
where: { code: penData.code }
|
||||
});
|
||||
|
||||
if (existingPen) {
|
||||
console.log('❌ [后端-栏舍设置] 栏舍编号已存在:', penData.code);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '栏舍编号已存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 准备创建数据
|
||||
const createData = {
|
||||
name: penData.name,
|
||||
code: penData.code,
|
||||
type: penData.type,
|
||||
capacity: parseInt(penData.capacity),
|
||||
currentCount: penData.currentCount ? parseInt(penData.currentCount) : 0,
|
||||
area: parseFloat(penData.area),
|
||||
location: penData.location || '',
|
||||
status: penData.status || '启用',
|
||||
remark: penData.remark || '',
|
||||
farmId: penData.farmId || 1
|
||||
};
|
||||
|
||||
console.log('📝 [后端-栏舍设置] 准备创建的数据:', createData);
|
||||
|
||||
const pen = await CattlePen.create(createData);
|
||||
|
||||
console.log('✅ [后端-栏舍设置] 栏舍创建成功:', pen.id);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: pen,
|
||||
message: '创建栏舍成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-栏舍设置] 创建失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建栏舍失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新栏舍
|
||||
*/
|
||||
async updatePen(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
const pen = await CattlePen.findByPk(id);
|
||||
if (!pen) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 如果更新编号,检查是否与其他栏舍冲突
|
||||
if (updateData.code && updateData.code !== pen.code) {
|
||||
const existingPen = await CattlePen.findOne({
|
||||
where: {
|
||||
code: updateData.code,
|
||||
id: { [Op.ne]: id }
|
||||
}
|
||||
});
|
||||
|
||||
if (existingPen) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '栏舍编号已存在'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await pen.update(updateData);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: pen,
|
||||
message: '更新栏舍成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('更新栏舍失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新栏舍失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除栏舍
|
||||
*/
|
||||
async deletePen(req, res) {
|
||||
try {
|
||||
console.log('🗑️ [后端-栏舍设置] 开始删除操作');
|
||||
console.log('📋 [后端-栏舍设置] 请求参数:', req.params);
|
||||
|
||||
const { id } = req.params;
|
||||
|
||||
const pen = await CattlePen.findByPk(id);
|
||||
if (!pen) {
|
||||
console.log('❌ [后端-栏舍设置] 栏舍不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ [后端-栏舍设置] 找到栏舍:', {
|
||||
id: pen.id,
|
||||
name: pen.name,
|
||||
code: pen.code
|
||||
});
|
||||
|
||||
// 注意:由于IotCattle表中没有penId字段,暂时跳过牛只检查
|
||||
// 在实际应用中,应该根据业务需求决定是否需要检查关联数据
|
||||
|
||||
await pen.destroy();
|
||||
console.log('✅ [后端-栏舍设置] 栏舍删除成功');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除栏舍成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-栏舍设置] 删除失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除栏舍失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除栏舍
|
||||
*/
|
||||
async batchDeletePens(req, res) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要删除的栏舍'
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否有栏舍包含牛只
|
||||
const animalCount = await IotCattle.count({
|
||||
where: { penId: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
if (animalCount > 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '选中的栏舍中还有牛只,无法删除'
|
||||
});
|
||||
}
|
||||
|
||||
await CattlePen.destroy({
|
||||
where: { id: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 个栏舍`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除栏舍失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量删除栏舍失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栏舍中的牛只
|
||||
*/
|
||||
async getPenIotCattles(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const pen = await CattlePen.findByPk(id);
|
||||
if (!pen) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
const { count, rows } = await IotCattle.findAndCountAll({
|
||||
where: { penId: id },
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset),
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取栏舍牛只成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取栏舍牛只失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取栏舍牛只失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取栏舍中的牛只
|
||||
*/
|
||||
async getPenAnimals(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 检查栏舍是否存在
|
||||
const pen = await CattlePen.findByPk(id);
|
||||
if (!pen) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 获取栏舍中的牛只
|
||||
const { count, rows } = await IotCattle.findAndCountAll({
|
||||
where: { penId: id },
|
||||
attributes: ['id', 'earNumber', 'sex', 'strain', 'orgId'],
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取栏舍牛只成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取栏舍牛只失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取栏舍牛只失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CattlePenController();
|
||||
540
backend/controllers/cattleTransferRecordController.js
Normal file
540
backend/controllers/cattleTransferRecordController.js
Normal file
@@ -0,0 +1,540 @@
|
||||
const { CattleTransferRecord, IotCattle, CattlePen, Farm } = require('../models');
|
||||
const { Op } = require('sequelize');
|
||||
|
||||
/**
|
||||
* 转栏记录控制器
|
||||
*/
|
||||
class CattleTransferRecordController {
|
||||
/**
|
||||
* 获取转栏记录列表
|
||||
*/
|
||||
async getTransferRecords(req, res) {
|
||||
try {
|
||||
const { page = 1, pageSize = 10, search, fromPen, toPen, dateRange } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 详细的搜索参数日志
|
||||
console.group('🔍 [后端-转栏记录] 搜索请求详情');
|
||||
console.log('🕒 请求时间:', new Date().toISOString());
|
||||
console.log('📋 接收到的查询参数:', {
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
search: search,
|
||||
fromPen: fromPen,
|
||||
toPen: toPen,
|
||||
dateRange: dateRange
|
||||
});
|
||||
console.log('🔍 搜索关键词:', search || '无');
|
||||
console.log('📊 分页参数:', { page, pageSize, offset });
|
||||
console.groupEnd();
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ recordId: { [Op.like]: `%${search}%` } },
|
||||
{ ear_number: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
console.log('🔍 [后端-转栏记录] 搜索条件构建:', {
|
||||
搜索关键词: search,
|
||||
搜索字段: ['recordId', 'ear_number'],
|
||||
搜索模式: 'LIKE %keyword%'
|
||||
});
|
||||
}
|
||||
if (fromPen) {
|
||||
where.fromPenId = fromPen;
|
||||
console.log('🏠 [后端-转栏记录] 转出栏舍筛选:', fromPen);
|
||||
}
|
||||
if (toPen) {
|
||||
where.toPenId = toPen;
|
||||
console.log('🏠 [后端-转栏记录] 转入栏舍筛选:', toPen);
|
||||
}
|
||||
if (dateRange && dateRange.length === 2) {
|
||||
where.transferDate = {
|
||||
[Op.between]: [dateRange[0], dateRange[1]]
|
||||
};
|
||||
console.log('📅 [后端-转栏记录] 日期范围筛选:', dateRange);
|
||||
}
|
||||
|
||||
const { count, rows } = await CattleTransferRecord.findAndCountAll({
|
||||
where,
|
||||
attributes: ['id', 'recordId', 'animalId', 'earNumber', 'fromPenId', 'toPenId', 'transferDate', 'reason', 'operator', 'status', 'remark', 'farmId', 'created_at', 'updated_at'],
|
||||
include: [
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'fromPen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'toPen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
limit: parseInt(pageSize),
|
||||
offset: parseInt(offset),
|
||||
order: [['transfer_date', 'DESC']]
|
||||
});
|
||||
|
||||
// 详细的查询结果日志
|
||||
console.group('📊 [后端-转栏记录] 查询结果详情');
|
||||
console.log('📈 查询统计:', {
|
||||
总记录数: count,
|
||||
当前页数据量: rows.length,
|
||||
当前页码: page,
|
||||
每页大小: pageSize,
|
||||
总页数: Math.ceil(count / pageSize)
|
||||
});
|
||||
console.log('🔍 查询条件:', where);
|
||||
console.log('⏱️ 查询耗时:', '已记录在数据库层面');
|
||||
console.groupEnd();
|
||||
|
||||
// 调试:检查关联数据
|
||||
if (rows.length > 0) {
|
||||
console.group('🔗 [后端-转栏记录] 关联数据检查');
|
||||
console.log('📋 第一条记录详情:', {
|
||||
id: rows[0].id,
|
||||
recordId: rows[0].recordId,
|
||||
earNumber: rows[0].earNumber,
|
||||
fromPenId: rows[0].fromPenId,
|
||||
toPenId: rows[0].toPenId,
|
||||
transferDate: rows[0].transferDate,
|
||||
reason: rows[0].reason,
|
||||
operator: rows[0].operator
|
||||
});
|
||||
console.log('🏠 栏舍关联信息:', {
|
||||
fromPen: rows[0].fromPen ? {
|
||||
id: rows[0].fromPen.id,
|
||||
name: rows[0].fromPen.name,
|
||||
code: rows[0].fromPen.code
|
||||
} : '无关联数据',
|
||||
toPen: rows[0].toPen ? {
|
||||
id: rows[0].toPen.id,
|
||||
name: rows[0].toPen.name,
|
||||
code: rows[0].toPen.code
|
||||
} : '无关联数据'
|
||||
});
|
||||
console.groupEnd();
|
||||
} else {
|
||||
console.log('📭 [后端-转栏记录] 查询结果为空');
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取转栏记录列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取转栏记录列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取转栏记录列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个转栏记录详情
|
||||
*/
|
||||
async getTransferRecordById(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const record = await CattleTransferRecord.findByPk(id, {
|
||||
include: [
|
||||
{
|
||||
model: IotCattle,
|
||||
as: 'cattle',
|
||||
attributes: ['id', 'earNumber', 'strain', 'sex']
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'fromPen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'toPen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
},
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '获取转栏记录详情成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取转栏记录详情失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取转栏记录详情失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建转栏记录
|
||||
*/
|
||||
async createTransferRecord(req, res) {
|
||||
try {
|
||||
console.log('🆕 [后端-转栏记录] 开始创建操作');
|
||||
console.log('📋 [后端-转栏记录] 请求数据:', req.body);
|
||||
|
||||
const recordData = req.body;
|
||||
|
||||
// 验证必填字段
|
||||
if (!recordData.earNumber || !recordData.fromPenId || !recordData.toPenId || !recordData.transferDate || !recordData.reason || !recordData.operator) {
|
||||
console.log('❌ [后端-转栏记录] 必填字段验证失败:', {
|
||||
earNumber: !!recordData.earNumber,
|
||||
fromPenId: !!recordData.fromPenId,
|
||||
toPenId: !!recordData.toPenId,
|
||||
transferDate: !!recordData.transferDate,
|
||||
reason: !!recordData.reason,
|
||||
operator: !!recordData.operator
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请填写所有必填字段(牛只耳号、转出栏舍、转入栏舍、转栏日期、转栏原因、操作人员)'
|
||||
});
|
||||
}
|
||||
|
||||
// 生成记录编号
|
||||
const recordId = `TR${new Date().getFullYear()}${String(new Date().getMonth() + 1).padStart(2, '0')}${String(new Date().getDate()).padStart(2, '0')}${String(Date.now()).slice(-3)}`;
|
||||
|
||||
// 通过耳号查找动物(支持数字和字符串类型)
|
||||
const earNumber = recordData.earNumber;
|
||||
const animal = await IotCattle.findOne({
|
||||
where: {
|
||||
[Op.or]: [
|
||||
{ earNumber: earNumber },
|
||||
{ earNumber: parseInt(earNumber) },
|
||||
{ earNumber: earNumber.toString() }
|
||||
]
|
||||
}
|
||||
});
|
||||
if (!animal) {
|
||||
console.log('❌ [后端-转栏记录] 动物不存在,耳号:', recordData.earNumber);
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '牛只不存在,请检查耳号是否正确'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('✅ [后端-转栏记录] 找到动物:', {
|
||||
id: animal.id,
|
||||
earNumber: animal.earNumber,
|
||||
currentPenId: animal.penId
|
||||
});
|
||||
|
||||
// 检查栏舍是否存在
|
||||
const fromPen = await CattlePen.findByPk(recordData.fromPenId);
|
||||
const toPen = await CattlePen.findByPk(recordData.toPenId);
|
||||
if (!fromPen || !toPen) {
|
||||
console.log('❌ [后端-转栏记录] 栏舍不存在:', {
|
||||
fromPenId: recordData.fromPenId,
|
||||
toPenId: recordData.toPenId,
|
||||
fromPenExists: !!fromPen,
|
||||
toPenExists: !!toPen
|
||||
});
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '栏舍不存在'
|
||||
});
|
||||
}
|
||||
|
||||
// 准备创建数据
|
||||
const createData = {
|
||||
recordId,
|
||||
animalId: animal.id,
|
||||
earNumber: animal.earNumber,
|
||||
fromPenId: parseInt(recordData.fromPenId),
|
||||
toPenId: parseInt(recordData.toPenId),
|
||||
transferDate: new Date(recordData.transferDate),
|
||||
reason: recordData.reason,
|
||||
operator: recordData.operator,
|
||||
status: recordData.status || '已完成',
|
||||
remark: recordData.remark || '',
|
||||
farmId: recordData.farmId || 1
|
||||
};
|
||||
|
||||
console.log('📝 [后端-转栏记录] 准备创建的数据:', createData);
|
||||
|
||||
const record = await CattleTransferRecord.create(createData);
|
||||
|
||||
// 更新动物的当前栏舍
|
||||
await animal.update({ penId: recordData.toPenId });
|
||||
console.log('✅ [后端-转栏记录] 动物栏舍更新成功');
|
||||
|
||||
console.log('✅ [后端-转栏记录] 转栏记录创建成功:', record.id);
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '创建转栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-转栏记录] 创建失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '创建转栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新转栏记录
|
||||
*/
|
||||
async updateTransferRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const updateData = req.body;
|
||||
|
||||
console.log('🔄 [后端-转栏记录] 开始更新操作');
|
||||
console.log('📋 [后端-转栏记录] 请求参数:', {
|
||||
recordId: id,
|
||||
updateData: updateData
|
||||
});
|
||||
|
||||
const record = await CattleTransferRecord.findByPk(id);
|
||||
if (!record) {
|
||||
console.log('❌ [后端-转栏记录] 记录不存在,ID:', id);
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
console.log('📝 [后端-转栏记录] 原始记录数据:', {
|
||||
id: record.id,
|
||||
animalId: record.animalId,
|
||||
earNumber: record.earNumber,
|
||||
fromPenId: record.fromPenId,
|
||||
toPenId: record.toPenId,
|
||||
transferDate: record.transferDate,
|
||||
reason: record.reason,
|
||||
operator: record.operator,
|
||||
status: record.status,
|
||||
remark: record.remark
|
||||
});
|
||||
|
||||
// 如果更新了转入栏舍,需要更新动物的当前栏舍
|
||||
if (updateData.toPenId && updateData.toPenId !== record.toPenId) {
|
||||
console.log('🔄 [后端-转栏记录] 检测到栏舍变更,更新动物当前栏舍');
|
||||
console.log('📝 [后端-转栏记录] 栏舍变更详情:', {
|
||||
oldPenId: record.toPenId,
|
||||
newPenId: updateData.toPenId,
|
||||
animalId: record.animalId
|
||||
});
|
||||
|
||||
const animal = await IotCattle.findByPk(record.animalId);
|
||||
if (animal) {
|
||||
await animal.update({ penId: updateData.toPenId });
|
||||
console.log('✅ [后端-转栏记录] 动物栏舍更新成功');
|
||||
} else {
|
||||
console.log('⚠️ [后端-转栏记录] 未找到对应的动物记录');
|
||||
}
|
||||
}
|
||||
|
||||
await record.update(updateData);
|
||||
console.log('✅ [后端-转栏记录] 记录更新成功');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: record,
|
||||
message: '更新转栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ [后端-转栏记录] 更新失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '更新转栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除转栏记录
|
||||
*/
|
||||
async deleteTransferRecord(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const record = await CattleTransferRecord.findByPk(id);
|
||||
if (!record) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: '转栏记录不存在'
|
||||
});
|
||||
}
|
||||
|
||||
await record.destroy();
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: '删除转栏记录成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('删除转栏记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '删除转栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量删除转栏记录
|
||||
*/
|
||||
async batchDeleteTransferRecords(req, res) {
|
||||
try {
|
||||
const { ids } = req.body;
|
||||
|
||||
if (!ids || !Array.isArray(ids) || ids.length === 0) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请选择要删除的记录'
|
||||
});
|
||||
}
|
||||
|
||||
await CattleTransferRecord.destroy({
|
||||
where: { id: { [Op.in]: ids } }
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `成功删除 ${ids.length} 条转栏记录`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('批量删除转栏记录失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '批量删除转栏记录失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
*/
|
||||
async getAvailableIotCattles(req, res) {
|
||||
try {
|
||||
const { search } = req.query;
|
||||
const where = {};
|
||||
|
||||
if (search) {
|
||||
where.earNumber = { [Op.like]: `%${search}%` };
|
||||
}
|
||||
|
||||
const animals = await IotCattle.findAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: CattlePen,
|
||||
as: 'pen',
|
||||
attributes: ['id', 'name', 'code']
|
||||
}
|
||||
],
|
||||
attributes: ['id', 'earNumber', 'strain', 'sex', 'ageInMonths', 'physiologicalStage'],
|
||||
limit: 50,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: animals,
|
||||
message: '获取可用牛只列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取可用牛只列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取可用牛只列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取可用的牛只列表
|
||||
*/
|
||||
async getAvailableAnimals(req, res) {
|
||||
try {
|
||||
const { search, page = 1, pageSize = 10 } = req.query;
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
// 构建查询条件
|
||||
const where = {};
|
||||
if (search) {
|
||||
where[Op.or] = [
|
||||
{ earNumber: { [Op.like]: `%${search}%` } },
|
||||
{ strain: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
}
|
||||
|
||||
const { count, rows } = await IotCattle.findAndCountAll({
|
||||
where,
|
||||
include: [
|
||||
{
|
||||
model: Farm,
|
||||
as: 'farm',
|
||||
attributes: ['id', 'name']
|
||||
}
|
||||
],
|
||||
attributes: ['id', 'earNumber', 'sex', 'strain', 'orgId'],
|
||||
limit: parseInt(pageSize),
|
||||
offset: offset,
|
||||
order: [['earNumber', 'ASC']]
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
list: rows,
|
||||
total: count,
|
||||
page: parseInt(page),
|
||||
pageSize: parseInt(pageSize)
|
||||
},
|
||||
message: '获取可用牛只列表成功'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('获取可用牛只列表失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '获取可用牛只列表失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new CattleTransferRecordController();
|
||||
@@ -31,6 +31,52 @@ exports.getAllDevices = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据设备名称搜索设备
|
||||
* @param {Object} req - 请求对象
|
||||
* @param {Object} res - 响应对象
|
||||
*/
|
||||
exports.searchDevicesByName = async (req, res) => {
|
||||
try {
|
||||
const { name } = req.query;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: '请提供设备名称参数'
|
||||
});
|
||||
}
|
||||
|
||||
console.log(`开始搜索设备名称包含: ${name}`);
|
||||
|
||||
// 使用模糊查询搜索设备名称
|
||||
const devices = await Device.findAll({
|
||||
where: {
|
||||
name: {
|
||||
[require('sequelize').Op.like]: `%${name}%`
|
||||
}
|
||||
},
|
||||
include: [{ model: Farm, as: 'farm', attributes: ['id', 'name', 'location'] }],
|
||||
order: [['created_at', 'DESC']]
|
||||
});
|
||||
|
||||
console.log(`找到 ${devices.length} 个匹配的设备`);
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
data: devices,
|
||||
message: `找到 ${devices.length} 个匹配的设备`
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('搜索设备失败:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
message: '搜索设备失败',
|
||||
error: error.message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取单个设备
|
||||
* @param {Object} req - 请求对象
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user