修改管理后台

This commit is contained in:
shenquanyi
2025-09-12 20:08:42 +08:00
parent 39d61c6f9b
commit 80a24c2d60
286 changed files with 75316 additions and 9452 deletions

View File

@@ -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

View 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"

View 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`
- 检查后端服务端口是否正确

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

View 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;
}
}

View File

@@ -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>

View 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;
}

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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)

View File

@@ -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)
})
// 组件卸载时清理资源

View File

@@ -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;

View File

@@ -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) {

View 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>

View File

@@ -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>

View 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>

View 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>

View File

@@ -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
};
// 其他环境配置

View File

@@ -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'
// 导入图标组件

View File

@@ -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
}
// 检查该路由是否需要登录权限

View File

@@ -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'
}
}
]
},
]
// 认证相关路由

View File

@@ -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
}
})

View File

@@ -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
}
})

View 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';
}
}

View File

@@ -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'
});
}
}
};
/**

View 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;
}
};

View 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();

View File

@@ -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;
}
};

View 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

View 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
};
});
};

View File

@@ -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
};
});
};

View 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('已调整视图以适应标记点');
};

View 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')
}
}

View 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;

View File

@@ -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

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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 />

View File

@@ -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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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>

View File

@@ -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">

View File

@@ -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>

View File

@@ -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
}
// 格式化时间显示

View 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>

View File

@@ -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,

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View 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>

View 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>

View 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>

View File

@@ -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)

View 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>

View File

@@ -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>

View 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()');
}

View File

@@ -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>

View File

@@ -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
View 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"

View 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地址访问服务
- 防火墙规则已正确添加
- 网络诊断脚本显示端口可以正常监听
如果按照以上步骤操作后仍有问题,请检查网络环境或联系网络管理员。

View 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
```
## 📊 免费版限制
- 每次重启ngrokURL会变化
- 同时只能运行1个隧道
- 有连接数限制
- 有带宽限制
## 💰 付费版优势
- 固定子域名
- 多个隧道
- 更高带宽
- 更多功能
## 🚨 注意事项
1. **安全性**
- 外网访问会暴露您的服务
- 建议设置访问密码
- 不要在生产环境使用
2. **性能**
- 外网访问比内网慢
- 免费版有带宽限制
3. **稳定性**
- 免费版URL会变化
- 付费版更稳定
## 🛠️ 故障排除
### 问题1ngrok启动失败
```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"
```
### 问题3URL无法访问
- 检查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地址访问您的开发服务器了
记住:
- 每次重启ngrokURL会变化
- 免费版有使用限制
- 建议在开发测试时使用

View 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. **备份配置**: 保存防火墙配置以便快速恢复
---
**问题已完全解决!** 🎉
现在其他用户应该能够正常访问您的开发服务器了。如果还有任何问题,请检查上述故障排除步骤。

View File

@@ -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 {}

View File

@@ -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, {

View File

@@ -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
);
}

View 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,
};

View File

@@ -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: '详细错误列表'
}
}
}
}
},

View 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

View 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 "按任意键退出"

View File

@@ -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 - 请求对象

View File

@@ -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 - 请求对象

View 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
};

View 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
};

View 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();

View 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();

View 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();

View 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();

View File

@@ -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