删除前端废弃组件和示例文件
This commit is contained in:
@@ -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"
|
||||
@@ -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`
|
||||
- 检查后端服务端口是否正确
|
||||
|
Before Width: | Height: | Size: 484 KiB After Width: | Height: | Size: 484 KiB |
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
@@ -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
|
||||
@@ -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('已调整视图以适应标记点');
|
||||
};
|
||||
@@ -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')
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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()');
|
||||
}
|
||||
860
backend/package-lock.json
generated
860
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user