删除前端废弃组件和示例文件

This commit is contained in:
ylweng
2025-09-12 21:53:14 +08:00
parent 7e093946a8
commit bc3b3d7b52
98 changed files with 14136 additions and 14931 deletions

View File

@@ -1,65 +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"
# 宁夏智慧养殖监管平台 - 前端管理系统容器
# 多阶段构建,优化镜像大小
# 阶段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

@@ -1,131 +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`
- 检查后端服务端口是否正确
# 环境变量配置说明
## 概述
本项目支持通过环境变量配置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`
- 检查后端服务端口是否正确

View File

Before

Width:  |  Height:  |  Size: 484 KiB

After

Width:  |  Height:  |  Size: 484 KiB

View File

@@ -1,114 +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;
}
}
# 宁夏智慧养殖监管平台 - 前端服务配置
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

@@ -1,66 +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;
}
# 宁夏智慧养殖监管平台 - 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;
}

View File

@@ -1,256 +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>
<!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,150 +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>
<!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,476 +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>
<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

@@ -1,75 +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>
<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

@@ -1,273 +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;
}
};
/**
* 百度地图加载器
* 提供更健壮的百度地图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

@@ -1,337 +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();
/**
* 百度地图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

@@ -1,447 +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' }
]
}
}
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

@@ -1,332 +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('已调整视图以适应标记点');
};
/**
* 修复版百度地图服务工具
* 专门解决 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

@@ -1,116 +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')
}
}
/**
* 菜单权限调试工具
* @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

@@ -1,379 +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;
/**
* 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

@@ -1,485 +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>
<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

@@ -1,358 +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>
<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

@@ -1,13 +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>
<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,43 +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()');
}
// 测试下载模板功能
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()');
}

File diff suppressed because it is too large Load Diff

View File

@@ -56,7 +56,6 @@
"mysql2": "^3.6.5",
"node-cron": "^3.0.3",
"nodemailer": "^6.9.8",
"puppeteer": "^21.6.1",
"redis": "^4.6.12",
"sequelize": "^6.35.2",
"sharp": "^0.33.2",