保险前后端,养殖端和保险端小程序

This commit is contained in:
xuqiuyun
2025-09-17 19:01:52 +08:00
parent e4287b83fe
commit 473891163c
218 changed files with 109331 additions and 14103 deletions

View File

@@ -0,0 +1,4 @@
# 生产环境配置
VITE_API_BASE_URL=/api
VITE_API_FULL_URL=https://ad.ningmuyun.com/api
VITE_USE_PROXY=false

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,7 @@
"admin-dashboard"
],
"engines": {
"node": ">=18.0.0",
"node": "16.20.2",
"npm": ">=8.0.0"
},
"scripts": {
@@ -49,22 +49,22 @@
"nprogress": "^0.2.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.4",
"vite": "^5.0.10",
"@types/node": "^20.10.5",
"@vitejs/plugin-vue": "^4.6.2",
"vite": "^4.5.3",
"@types/node": "^16.18.68",
"@types/lodash-es": "^4.17.12",
"@types/nprogress": "^0.2.3",
"@typescript-eslint/eslint-plugin": "^6.14.0",
"@typescript-eslint/parser": "^6.14.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@typescript-eslint/eslint-plugin": "^5.62.0",
"@typescript-eslint/parser": "^5.62.0",
"@vue/eslint-config-typescript": "^11.0.3",
"eslint": "^8.55.0",
"eslint-plugin-vue": "^9.19.2",
"rimraf": "^5.0.5",
"typescript": "^5.3.3",
"typescript": "^4.9.5",
"vite-bundle-analyzer": "^0.7.0",
"vitest": "^1.0.4",
"@vitest/ui": "^1.0.4",
"@vitest/coverage-v8": "^1.0.4",
"vitest": "^0.34.6",
"@vitest/ui": "^0.34.6",
"@vitest/coverage-v8": "^0.34.6",
"vue-tsc": "^1.8.25"
}
}

View File

@@ -4,6 +4,7 @@
*/
import { API_CONFIG } from '../config/env.js';
import { createSuccessResponse, createErrorResponse, extractData, isValidApiResponse } from './apiResponseFormat.js';
// API基础URL - 支持环境变量配置
const API_BASE_URL = API_CONFIG.baseUrl;
@@ -17,6 +18,8 @@ const createHeaders = (headers = {}) => {
const token = localStorage.getItem('token');
const defaultHeaders = {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
};
if (token) {
@@ -44,33 +47,10 @@ const handleResponse = async (response) => {
// 清除无效的认证信息
localStorage.removeItem('token');
localStorage.removeItem('user');
// 显示友好的错误提示而不是直接重定向
console.warn('认证token已过期请重新登录');
throw new Error('认证已过期,请重新登录');
}
if (response.status === 404) {
throw new Error('请求的资源不存在');
}
if (response.status === 500) {
const errorData = await response.json();
console.error('API 500错误详情:', errorData);
throw new Error(
errorData.message ||
(errorData.details ? `${errorData.message}\n${errorData.details}` : '服务暂时不可用,请稍后重试')
);
}
if (response.status === 401) {
// 清除无效的认证信息
localStorage.removeItem('token');
localStorage.removeItem('user');
// 可以在这里添加重定向到登录页的逻辑
window.location.href = '/login';
throw new Error('认证失败,请重新登录');
}
if (response.status === 403) {
console.error('API 403错误详情:', {
url: response.url,
@@ -88,13 +68,13 @@ const handleResponse = async (response) => {
try {
const errorData = await response.json();
console.error('API 500错误详情:', errorData);
// 优先使用后端返回的 message否则使用默认提示
throw new Error(errorData.message || '服务器繁忙,请稍后重试');
} catch (e) {
console.error('API处理错误:', e.stack);
// 区分网络错误和服务端错误
console.error('API处理错误:', e.message);
if (e.message.includes('Failed to fetch')) {
throw new Error('网络连接失败,请检查网络');
} else if (e.message.includes('Unexpected end of JSON input') || e.message.includes('JSON')) {
throw new Error('服务器返回非法响应,请联系管理员');
} else {
throw new Error('服务器内部错误,请联系管理员');
}
@@ -136,13 +116,29 @@ const handleResponse = async (response) => {
}
// 返回JSON数据
const result = await response.json();
let result;
try {
result = await response.json();
} catch (e) {
console.error('JSON解析失败:', e.message);
if (e.message.includes('Unexpected end of JSON input')) {
throw new Error('服务器返回空响应,请联系管理员');
} else {
throw new Error('服务器返回数据格式错误');
}
}
// 兼容数组响应
if (Array.isArray(result)) {
return result;
}
// 验证响应格式
if (!isValidApiResponse(result)) {
console.warn('API响应格式不符合标准:', result);
return result;
}
if (!result.success) {
console.error(`API业务逻辑失败:`, {
url: response.url,
@@ -151,6 +147,7 @@ const handleResponse = async (response) => {
});
throw new Error(result.message || 'API请求失败');
}
return result;
};
@@ -270,6 +267,8 @@ export const api = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'Pragma': 'no-cache'
},
body: JSON.stringify({ username, password }),
});

View File

@@ -0,0 +1,417 @@
/**
* 统一API客户端
* 基于fetch方法的API调用工具确保所有数据都从数据库动态获取
*/
import { createSuccessResponse, createErrorResponse, extractData } from './apiResponseFormat.js';
/**
* API客户端配置
*/
const API_CONFIG = {
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 30000,
retryCount: 3,
retryDelay: 1000
};
/**
* 创建请求头
* @param {Object} customHeaders - 自定义请求头
* @returns {Object} 请求头对象
*/
const createHeaders = (customHeaders = {}) => {
const token = localStorage.getItem('token');
const defaultHeaders = {
'Content-Type': 'application/json',
'Accept': 'application/json'
};
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
return { ...defaultHeaders, ...customHeaders };
};
/**
* 处理API响应
* @param {Response} response - Fetch响应对象
* @returns {Promise} 处理后的响应数据
*/
const handleResponse = async (response) => {
// 检查HTTP状态
if (!response.ok) {
let errorMessage = `HTTP ${response.status}: ${response.statusText}`;
try {
const errorData = await response.json();
errorMessage = errorData.message || errorMessage;
} catch (e) {
// 如果无法解析错误响应,使用默认错误消息
}
// 处理认证错误
if (response.status === 401) {
localStorage.removeItem('token');
localStorage.removeItem('user');
window.location.href = '/login';
throw new Error('认证已过期,请重新登录');
}
throw new Error(errorMessage);
}
// 检查响应类型
const contentType = response.headers.get('content-type');
// 如果是文件下载返回blob
if (contentType && (
contentType.includes('application/octet-stream') ||
contentType.includes('application/vnd.ms-excel') ||
contentType.includes('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet') ||
contentType.includes('text/csv')
)) {
return await response.blob();
}
// 返回JSON数据
const result = await response.json();
// 兼容数组响应
if (Array.isArray(result)) {
return result;
}
return result;
};
/**
* 重试机制
* @param {Function} requestFn - 请求函数
* @param {number} retryCount - 重试次数
* @param {number} delay - 重试延迟
* @returns {Promise} 请求结果
*/
const retryRequest = async (requestFn, retryCount = API_CONFIG.retryCount, delay = API_CONFIG.retryDelay) => {
try {
return await requestFn();
} catch (error) {
if (retryCount > 0 && !error.message.includes('认证')) {
console.warn(`请求失败,${delay}ms后重试剩余重试次数: ${retryCount - 1}`, error.message);
await new Promise(resolve => setTimeout(resolve, delay));
return retryRequest(requestFn, retryCount - 1, delay * 2);
}
throw error;
}
};
/**
* 创建完整URL
* @param {string} endpoint - API端点
* @returns {string} 完整URL
*/
const createURL = (endpoint) => {
const baseURL = API_CONFIG.baseURL.replace(/\/$/, '');
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${baseURL}${cleanEndpoint}`;
};
/**
* 构建查询参数
* @param {Object} params - 查询参数对象
* @returns {string} 查询字符串
*/
const buildQueryString = (params) => {
const searchParams = new URLSearchParams();
Object.keys(params).forEach(key => {
const value = params[key];
if (value !== null && value !== undefined && value !== '') {
if (Array.isArray(value)) {
value.forEach(item => {
if (item !== null && item !== undefined && item !== '') {
searchParams.append(key, item);
}
});
} else if (typeof value === 'object' && value !== null) {
// 处理对象类型的参数(如日期范围)
if (value.start && value.end) {
searchParams.append(`${key}Start`, value.start);
searchParams.append(`${key}End`, value.end);
}
} else {
searchParams.append(key, value);
}
}
});
// 添加时间戳防止缓存
searchParams.append('_t', Date.now().toString());
return searchParams.toString();
};
/**
* 基础请求方法
* @param {string} method - HTTP方法
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 请求结果
*/
const request = async (method, endpoint, options = {}) => {
const { data, params, headers = {}, responseType = 'json' } = options;
// 构建URL
let url = createURL(endpoint);
if (params && Object.keys(params).length > 0) {
const queryString = buildQueryString(params);
url += `?${queryString}`;
}
// 构建请求配置
const requestConfig = {
method: method.toUpperCase(),
headers: createHeaders(headers),
...options
};
// 添加请求体
if (data && method.toUpperCase() !== 'GET') {
if (data instanceof FormData) {
// 文件上传
requestConfig.body = data;
delete requestConfig.headers['Content-Type']; // 让浏览器自动设置
} else {
requestConfig.body = JSON.stringify(data);
}
}
// 执行请求
const requestFn = async () => {
const response = await fetch(url, requestConfig);
return await handleResponse(response);
};
// 使用重试机制
return await retryRequest(requestFn);
};
/**
* API客户端类
*/
export class ApiClient {
/**
* GET请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async get(endpoint, options = {}) {
return await request('GET', endpoint, options);
}
/**
* POST请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async post(endpoint, data = null, options = {}) {
return await request('POST', endpoint, { ...options, data });
}
/**
* PUT请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async put(endpoint, data = null, options = {}) {
return await request('PUT', endpoint, { ...options, data });
}
/**
* DELETE请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async delete(endpoint, options = {}) {
return await request('DELETE', endpoint, options);
}
/**
* PATCH请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async patch(endpoint, data = null, options = {}) {
return await request('PATCH', endpoint, { ...options, data });
}
/**
* 文件上传
* @param {string} endpoint - API端点
* @param {FormData} formData - 表单数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
static async upload(endpoint, formData, options = {}) {
return await request('POST', endpoint, {
...options,
data: formData,
headers: {} // 让浏览器自动设置Content-Type
});
}
/**
* 文件下载
* @param {string} endpoint - API端点
* @param {Object} params - 查询参数
* @param {string} filename - 文件名
* @returns {Promise} 下载结果
*/
static async download(endpoint, params = {}, filename = 'download') {
const response = await this.get(endpoint, {
params,
responseType: 'blob'
});
// 创建下载链接
const url = window.URL.createObjectURL(response);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(url);
return response;
}
}
/**
* 便捷的API调用方法
*/
export const api = {
// 基础CRUD操作
get: ApiClient.get,
post: ApiClient.post,
put: ApiClient.put,
delete: ApiClient.delete,
patch: ApiClient.patch,
upload: ApiClient.upload,
download: ApiClient.download,
// 智能设备相关API
smartDevices: {
// 智能耳标
async getEartags(params = {}) {
return await ApiClient.get('/smart-devices/public/eartags', { params });
},
async getEartagById(id) {
return await ApiClient.get(`/smart-devices/public/eartags/${id}`);
},
async exportEartags(params = {}) {
return await ApiClient.download('/smart-devices/public/eartags/export', params, '智能耳标数据.xlsx');
},
// 智能项圈
async getCollars(params = {}) {
return await ApiClient.get('/smart-devices/public/collars', { params });
},
async getCollarById(id) {
return await ApiClient.get(`/smart-devices/public/collars/${id}`);
},
// 智能脚环
async getAnklets(params = {}) {
return await ApiClient.get('/smart-devices/public/anklets', { params });
},
async getAnkletById(id) {
return await ApiClient.get(`/smart-devices/public/anklets/${id}`);
},
// 智能主机
async getHosts(params = {}) {
return await ApiClient.get('/smart-devices/public/hosts', { params });
},
async getHostById(id) {
return await ApiClient.get(`/smart-devices/public/hosts/${id}`);
}
},
// 预警相关API
smartAlerts: {
async getEartagAlerts(params = {}) {
return await ApiClient.get('/smart-alerts/public/eartag', { params });
},
async getCollarAlerts(params = {}) {
return await ApiClient.get('/smart-alerts/public/collar', { params });
},
async getAnkletAlerts(params = {}) {
return await ApiClient.get('/smart-alerts/public/anklet', { params });
},
async getAlertStats() {
return await ApiClient.get('/smart-alerts/public/stats');
}
},
// 养殖场相关API
farms: {
async getFarms(params = {}) {
return await ApiClient.get('/farms/public', { params });
},
async getFarmById(id) {
return await ApiClient.get(`/farms/public/${id}`);
},
async createFarm(data) {
return await ApiClient.post('/farms', data);
},
async updateFarm(id, data) {
return await ApiClient.put(`/farms/${id}`, data);
},
async deleteFarm(id) {
return await ApiClient.delete(`/farms/${id}`);
}
},
// 统计数据相关API
stats: {
async getDashboardStats() {
return await ApiClient.get('/stats/public/dashboard');
},
async getFarmStats() {
return await ApiClient.get('/stats/public/farms');
},
async getDeviceStats() {
return await ApiClient.get('/stats/public/devices');
},
async getAlertStats() {
return await ApiClient.get('/stats/public/alerts');
}
}
};
export default api;

View File

@@ -0,0 +1,211 @@
/**
* API响应格式标准化工具
* 统一前后端接口返回格式
*/
/**
* 标准API响应格式
* @typedef {Object} ApiResponse
* @property {boolean} success - 请求是否成功
* @property {string} message - 响应消息
* @property {*} data - 响应数据
* @property {number} total - 总记录数(分页时使用)
* @property {number} page - 当前页码(分页时使用)
* @property {number} limit - 每页记录数(分页时使用)
* @property {Object} meta - 元数据(可选)
* @property {string} timestamp - 响应时间戳
* @property {string} requestId - 请求ID用于追踪
*/
/**
* 创建标准成功响应
* @param {*} data - 响应数据
* @param {string} message - 响应消息
* @param {Object} options - 其他选项
* @returns {ApiResponse} 标准响应格式
*/
export const createSuccessResponse = (data = null, message = '操作成功', options = {}) => {
const response = {
success: true,
message,
data,
timestamp: new Date().toISOString(),
requestId: options.requestId || generateRequestId()
};
// 添加分页信息
if (options.total !== undefined) {
response.total = options.total;
}
if (options.page !== undefined) {
response.page = options.page;
}
if (options.limit !== undefined) {
response.limit = options.limit;
}
// 添加元数据
if (options.meta) {
response.meta = options.meta;
}
return response;
};
/**
* 创建标准错误响应
* @param {string} message - 错误消息
* @param {string} code - 错误代码
* @param {*} details - 错误详情
* @param {Object} options - 其他选项
* @returns {ApiResponse} 标准错误响应格式
*/
export const createErrorResponse = (message = '操作失败', code = 'UNKNOWN_ERROR', details = null, options = {}) => {
return {
success: false,
message,
code,
details,
timestamp: new Date().toISOString(),
requestId: options.requestId || generateRequestId()
};
};
/**
* 创建分页响应
* @param {Array} data - 数据列表
* @param {number} total - 总记录数
* @param {number} page - 当前页码
* @param {number} limit - 每页记录数
* @param {string} message - 响应消息
* @param {Object} options - 其他选项
* @returns {ApiResponse} 分页响应格式
*/
export const createPaginatedResponse = (data, total, page, limit, message = '获取数据成功', options = {}) => {
return createSuccessResponse(data, message, {
total,
page,
limit,
...options
});
};
/**
* 生成请求ID
* @returns {string} 请求ID
*/
const generateRequestId = () => {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
};
/**
* 验证API响应格式
* @param {*} response - 响应对象
* @returns {boolean} 是否为标准格式
*/
export const isValidApiResponse = (response) => {
return response &&
typeof response === 'object' &&
typeof response.success === 'boolean' &&
typeof response.message === 'string' &&
typeof response.timestamp === 'string';
};
/**
* 提取响应数据
* @param {ApiResponse} response - API响应
* @returns {*} 响应数据
*/
export const extractData = (response) => {
if (!isValidApiResponse(response)) {
console.warn('Invalid API response format:', response);
return response;
}
if (!response.success) {
throw new Error(response.message);
}
return response.data;
};
/**
* 提取分页信息
* @param {ApiResponse} response - API响应
* @returns {Object} 分页信息
*/
export const extractPagination = (response) => {
if (!isValidApiResponse(response)) {
return null;
}
return {
total: response.total || 0,
page: response.page || 1,
limit: response.limit || 10,
totalPages: response.total ? Math.ceil(response.total / (response.limit || 10)) : 0
};
};
/**
* 错误代码常量
*/
export const ERROR_CODES = {
// 认证相关
UNAUTHORIZED: 'UNAUTHORIZED',
TOKEN_EXPIRED: 'TOKEN_EXPIRED',
INVALID_TOKEN: 'INVALID_TOKEN',
// 权限相关
FORBIDDEN: 'FORBIDDEN',
INSUFFICIENT_PERMISSIONS: 'INSUFFICIENT_PERMISSIONS',
// 资源相关
NOT_FOUND: 'NOT_FOUND',
RESOURCE_CONFLICT: 'RESOURCE_CONFLICT',
RESOURCE_LOCKED: 'RESOURCE_LOCKED',
// 验证相关
VALIDATION_ERROR: 'VALIDATION_ERROR',
INVALID_INPUT: 'INVALID_INPUT',
MISSING_REQUIRED_FIELD: 'MISSING_REQUIRED_FIELD',
// 业务相关
BUSINESS_ERROR: 'BUSINESS_ERROR',
OPERATION_FAILED: 'OPERATION_FAILED',
DUPLICATE_ENTRY: 'DUPLICATE_ENTRY',
// 系统相关
INTERNAL_ERROR: 'INTERNAL_ERROR',
SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',
DATABASE_ERROR: 'DATABASE_ERROR',
NETWORK_ERROR: 'NETWORK_ERROR',
// 未知错误
UNKNOWN_ERROR: 'UNKNOWN_ERROR'
};
/**
* 成功消息常量
*/
export const SUCCESS_MESSAGES = {
// 通用操作
OPERATION_SUCCESS: '操作成功',
DATA_SAVED: '数据保存成功',
DATA_UPDATED: '数据更新成功',
DATA_DELETED: '数据删除成功',
DATA_RETRIEVED: '数据获取成功',
// 认证相关
LOGIN_SUCCESS: '登录成功',
LOGOUT_SUCCESS: '登出成功',
PASSWORD_CHANGED: '密码修改成功',
// 文件相关
FILE_UPLOADED: '文件上传成功',
FILE_DELETED: '文件删除成功',
// 导出相关
EXPORT_SUCCESS: '数据导出成功',
IMPORT_SUCCESS: '数据导入成功'
};

View File

@@ -266,13 +266,18 @@ export class ExportUtils {
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' }
{ title: '设备名称', dataIndex: 'title', key: 'title' },
{ title: '设备编号', dataIndex: 'deviceNumber', key: 'deviceNumber' },
{ title: '设备状态', dataIndex: 'networkStatus', key: 'networkStatus' },
{ title: 'IP地址', dataIndex: 'simId', key: 'simId' },
{ title: '端口', dataIndex: 'signalValue', key: 'signalValue' },
{ title: '最后在线时间', dataIndex: 'updateTime', key: 'updateTime', dataType: 'datetime' },
{ title: '创建时间', dataIndex: 'voltage', key: 'voltage' },
{ title: '设备电量', dataIndex: 'battery', key: 'battery' },
{ title: '设备温度', dataIndex: 'temperature', key: 'temperature' },
{ title: 'GPS状态', dataIndex: 'gpsStatus', key: 'gpsStatus' },
{ title: '经度', dataIndex: 'longitude', key: 'longitude' },
{ title: '纬度', dataIndex: 'latitude', key: 'latitude' }
]
}

View File

@@ -0,0 +1,271 @@
/**
* 筛选条件管理工具
* 统一管理前端筛选条件避免v-model绑定问题
*/
import { reactive, watch } from 'vue';
/**
* 创建筛选条件管理器
* @param {Object} initialFilters - 初始筛选条件
* @param {Function} onFilterChange - 筛选条件变化回调
* @returns {Object} 筛选条件管理器
*/
export const createFilterManager = (initialFilters = {}, onFilterChange = null) => {
// 创建响应式筛选条件对象
const filters = reactive({
// 通用筛选条件
search: '',
page: 1,
limit: 10,
sortBy: '',
sortOrder: 'desc',
dateRange: null,
status: '',
// 合并初始筛选条件
...initialFilters
});
// 创建筛选条件更新函数
const updateFilter = (key, value) => {
console.log(`更新筛选条件: ${key} = ${value}`);
filters[key] = value;
// 如果是分页相关,不触发搜索
if (key === 'page') {
return;
}
// 其他筛选条件变化时重置页码
if (key !== 'page' && key !== 'limit') {
filters.page = 1;
}
// 触发筛选条件变化回调
if (onFilterChange && typeof onFilterChange === 'function') {
onFilterChange(filters);
}
};
// 批量更新筛选条件
const updateFilters = (newFilters) => {
console.log('批量更新筛选条件:', newFilters);
Object.assign(filters, newFilters);
// 触发筛选条件变化回调
if (onFilterChange && typeof onFilterChange === 'function') {
onFilterChange(filters);
}
};
// 重置筛选条件
const resetFilters = () => {
console.log('重置筛选条件');
Object.keys(filters).forEach(key => {
if (key === 'page') {
filters[key] = 1;
} else if (key === 'limit') {
filters[key] = 10;
} else {
filters[key] = initialFilters[key] || '';
}
});
// 触发筛选条件变化回调
if (onFilterChange && typeof onFilterChange === 'function') {
onFilterChange(filters);
}
};
// 获取查询参数
const getQueryParams = () => {
const params = new URLSearchParams();
Object.keys(filters).forEach(key => {
const value = filters[key];
if (value !== null && value !== undefined && value !== '') {
if (Array.isArray(value)) {
// 处理数组类型的筛选条件
value.forEach(item => {
if (item !== null && item !== undefined && item !== '') {
params.append(key, item);
}
});
} else if (typeof value === 'object' && value !== null) {
// 处理对象类型的筛选条件(如日期范围)
if (value.start && value.end) {
params.append(`${key}Start`, value.start);
params.append(`${key}End`, value.end);
}
} else {
params.append(key, value);
}
}
});
// 添加时间戳防止缓存
params.append('_t', Date.now().toString());
return params;
};
// 获取URL查询字符串
const getQueryString = () => {
return getQueryParams().toString();
};
// 从URL参数初始化筛选条件
const initFromUrl = (urlParams) => {
console.log('从URL参数初始化筛选条件:', urlParams);
Object.keys(urlParams).forEach(key => {
if (filters.hasOwnProperty(key)) {
filters[key] = urlParams[key];
}
});
};
// 监听筛选条件变化
const watchFilters = (callback) => {
return watch(filters, (newFilters, oldFilters) => {
console.log('筛选条件变化:', { newFilters, oldFilters });
if (callback && typeof callback === 'function') {
callback(newFilters, oldFilters);
}
}, { deep: true });
};
return {
filters,
updateFilter,
updateFilters,
resetFilters,
getQueryParams,
getQueryString,
initFromUrl,
watchFilters
};
};
/**
* 防抖筛选条件更新
* @param {Function} updateFunction - 更新函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 防抖后的更新函数
*/
export const debounceFilter = (updateFunction, delay = 300) => {
let timeoutId;
return (...args) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
updateFunction(...args);
}, delay);
};
};
/**
* 节流筛选条件更新
* @param {Function} updateFunction - 更新函数
* @param {number} delay - 延迟时间(毫秒)
* @returns {Function} 节流后的更新函数
*/
export const throttleFilter = (updateFunction, delay = 300) => {
let lastCall = 0;
return (...args) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
updateFunction(...args);
}
};
};
/**
* 筛选条件预设
*/
export const FILTER_PRESETS = {
// 日期范围预设
DATE_RANGES: {
TODAY: 'today',
YESTERDAY: 'yesterday',
LAST_7_DAYS: 'last7days',
LAST_30_DAYS: 'last30days',
THIS_MONTH: 'thisMonth',
LAST_MONTH: 'lastMonth',
THIS_YEAR: 'thisYear',
LAST_YEAR: 'lastYear',
CUSTOM: 'custom'
},
// 状态预设
STATUS: {
ALL: '',
ACTIVE: 'active',
INACTIVE: 'inactive',
PENDING: 'pending',
COMPLETED: 'completed',
CANCELLED: 'cancelled'
},
// 排序预设
SORT_ORDERS: {
ASC: 'asc',
DESC: 'desc'
}
};
/**
* 获取日期范围
* @param {string} preset - 预设类型
* @returns {Object} 日期范围对象
*/
export const getDateRange = (preset) => {
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
switch (preset) {
case FILTER_PRESETS.DATE_RANGES.TODAY:
return {
start: today,
end: new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1)
};
case FILTER_PRESETS.DATE_RANGES.YESTERDAY:
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000);
return {
start: yesterday,
end: new Date(yesterday.getTime() + 24 * 60 * 60 * 1000 - 1)
};
case FILTER_PRESETS.DATE_RANGES.LAST_7_DAYS:
return {
start: new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000),
end: new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1)
};
case FILTER_PRESETS.DATE_RANGES.LAST_30_DAYS:
return {
start: new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000),
end: new Date(today.getTime() + 24 * 60 * 60 * 1000 - 1)
};
case FILTER_PRESETS.DATE_RANGES.THIS_MONTH:
return {
start: new Date(now.getFullYear(), now.getMonth(), 1),
end: new Date(now.getFullYear(), now.getMonth() + 1, 0, 23, 59, 59, 999)
};
case FILTER_PRESETS.DATE_RANGES.LAST_MONTH:
return {
start: new Date(now.getFullYear(), now.getMonth() - 1, 1),
end: new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999)
};
case FILTER_PRESETS.DATE_RANGES.THIS_YEAR:
return {
start: new Date(now.getFullYear(), 0, 1),
end: new Date(now.getFullYear(), 11, 31, 23, 59, 59, 999)
};
case FILTER_PRESETS.DATE_RANGES.LAST_YEAR:
return {
start: new Date(now.getFullYear() - 1, 0, 1),
end: new Date(now.getFullYear() - 1, 11, 31, 23, 59, 59, 999)
};
default:
return null;
}
};

View File

@@ -319,60 +319,73 @@ const fetchData = async () => {
try {
loading.value = true
// 模拟数据
const mockData = [
{
id: 1,
deviceId: 'AN001',
animalName: '牛001',
model: 'SmartAnklet-V1',
status: 'active',
stepCount: 2456,
heartRate: 75,
temperature: 38.5,
lastUpdate: '2025-01-18 10:30:00'
},
{
id: 2,
deviceId: 'AN002',
animalName: '牛002',
model: 'SmartAnklet-V1',
status: 'standby',
stepCount: 1823,
heartRate: 68,
temperature: 38.2,
lastUpdate: '2025-01-18 09:15:00'
},
{
id: 3,
deviceId: 'AN003',
animalName: '羊001',
model: 'SmartAnklet-V2',
status: 'active',
stepCount: 3124,
heartRate: 82,
temperature: 39.1,
lastUpdate: '2025-01-18 10:25:00'
// 构建查询参数
const params = new URLSearchParams({
page: pagination.current.toString(),
limit: pagination.pageSize.toString(),
_t: Date.now().toString()
})
// 添加搜索条件
if (searchValue.value.trim()) {
params.append('search', searchValue.value.trim())
}
// 调用API获取脚环数据
const response = await fetch(`/api/smart-devices/public/anklets?${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
]
})
anklets.value = mockData
pagination.total = mockData.length
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
// 更新统计数据
stats.total = mockData.length
stats.active = mockData.filter(item => item.status === 'active').length
stats.standby = mockData.filter(item => item.status === 'standby').length
stats.fault = mockData.filter(item => item.status === 'fault').length
const result = await response.json()
if (result.success) {
anklets.value = result.data.list || []
pagination.total = result.data.pagination?.total || 0
// 更新统计数据
if (result.data.stats) {
stats.total = result.data.stats.total || 0
stats.active = result.data.stats.active || 0
stats.standby = result.data.stats.standby || 0
stats.fault = result.data.stats.fault || 0
} else {
// 如果没有统计数据,从数据中计算
stats.total = anklets.value.length
stats.active = anklets.value.filter(item => item.status === 'active').length
stats.standby = anklets.value.filter(item => item.status === 'standby').length
stats.fault = anklets.value.filter(item => item.status === 'fault').length
}
} else {
throw new Error(result.message || '获取脚环数据失败')
}
} catch (error) {
console.error('获取数据失败:', error)
message.error('获取数据失败')
message.error('获取数据失败: ' + error.message)
anklets.value = []
pagination.total = 0
resetStats()
} finally {
loading.value = false
}
}
// 重置统计信息
const resetStats = () => {
stats.total = 0
stats.active = 0
stats.standby = 0
stats.fault = 0
}
// 刷新数据
const refreshData = () => {
fetchData()

View File

@@ -537,90 +537,81 @@ const fetchData = async (showMessage = false, customAlertType = null) => {
}
}
// 生成模拟数据
const generateMockData = () => {
const mockAlerts = [
{
id: 1,
collarNumber: 'COLLAR001',
alertType: 'battery',
alertLevel: 'high',
alertTime: '2025-01-18 10:30:00',
battery: 12,
temperature: 38.5,
gpsSignal: '弱',
wearStatus: '已佩戴',
description: '设备电量低于20%,需要及时充电',
longitude: 116.3974,
latitude: 39.9093
},
{
id: 2,
collarNumber: 'COLLAR002',
alertType: 'offline',
alertLevel: 'high',
alertTime: '2025-01-18 09:15:00',
battery: 0,
temperature: 0,
gpsSignal: '无',
wearStatus: '未知',
description: '设备已离线超过30分钟',
longitude: 0,
latitude: 0
},
{
id: 3,
collarNumber: 'COLLAR003',
alertType: 'temperature',
alertLevel: 'medium',
alertTime: '2025-01-18 08:45:00',
battery: 85,
temperature: 42.3,
gpsSignal: '强',
wearStatus: '已佩戴',
description: '设备温度异常,超过正常范围',
longitude: 116.4074,
latitude: 39.9193
},
{
id: 4,
collarNumber: 'COLLAR004',
alertType: 'location',
alertLevel: 'high',
alertTime: '2025-01-18 07:20:00',
battery: 92,
temperature: 39.1,
gpsSignal: '无',
wearStatus: '已佩戴',
description: 'GPS信号丢失无法获取位置信息',
longitude: 0,
latitude: 0
},
{
id: 5,
collarNumber: 'COLLAR005',
alertType: 'wear',
alertLevel: 'medium',
alertTime: '2025-01-18 06:10:00',
battery: 78,
temperature: 37.8,
gpsSignal: '中',
wearStatus: '未佩戴',
description: '设备佩戴状态异常,可能已脱落',
longitude: 116.4174,
latitude: 39.9293
// 从API获取预警数据
const fetchAlertData = async () => {
try {
loading.value = true
// 构建查询参数
const params = new URLSearchParams({
page: pagination.current.toString(),
limit: pagination.pageSize.toString(),
_t: Date.now().toString()
})
// 添加筛选条件
if (searchValue.value.trim()) {
params.append('search', searchValue.value.trim())
}
]
alerts.value = mockAlerts
pagination.total = mockAlerts.length
// 更新统计数据
stats.lowBattery = mockAlerts.filter(alert => alert.alertType === 'battery').length
stats.offline = mockAlerts.filter(alert => alert.alertType === 'offline').length
stats.highTemperature = mockAlerts.filter(alert => alert.alertType === 'temperature').length
stats.abnormalMovement = mockAlerts.filter(alert => alert.alertType === 'movement').length
stats.wearOff = mockAlerts.filter(alert => alert.alertType === 'wear').length
if (alertTypeFilter.value) {
params.append('alertType', alertTypeFilter.value)
}
// 调用API获取预警数据
const response = await fetch(`/api/smart-alerts/public/collar?${params}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
if (result.success) {
alerts.value = result.data.list || []
pagination.total = result.data.pagination?.total || 0
// 更新统计数据
if (result.data.stats) {
stats.lowBattery = result.data.stats.lowBattery || 0
stats.offline = result.data.stats.offline || 0
stats.highTemperature = result.data.stats.highTemperature || 0
stats.abnormalMovement = result.data.stats.abnormalMovement || 0
stats.wearOff = result.data.stats.wearOff || 0
} else {
// 如果没有统计数据,从数据中计算
stats.lowBattery = alerts.value.filter(alert => alert.alertType === 'battery').length
stats.offline = alerts.value.filter(alert => alert.alertType === 'offline').length
stats.highTemperature = alerts.value.filter(alert => alert.alertType === 'temperature').length
stats.abnormalMovement = alerts.value.filter(alert => alert.alertType === 'movement').length
stats.wearOff = alerts.value.filter(alert => alert.alertType === 'wear').length
}
} else {
throw new Error(result.message || '获取预警数据失败')
}
} catch (error) {
console.error('获取预警数据失败:', error)
message.error('获取预警数据失败: ' + error.message)
alerts.value = []
pagination.total = 0
resetStats()
} finally {
loading.value = false
}
}
// 重置统计信息
const resetStats = () => {
stats.lowBattery = 0
stats.offline = 0
stats.highTemperature = 0
stats.abnormalMovement = 0
stats.wearOff = 0
}
// 更新搜索值

View File

@@ -445,75 +445,59 @@ const fetchData = async (showMessage = false, customAlertType = null) => {
}
}
// 生成模拟数据
const generateMockData = () => {
const mockAlerts = [
{
id: 1,
eartagNumber: 'EARTAG001',
alertType: 'battery',
alertLevel: 'high',
alertTime: '2025-01-18 10:30:00',
battery: 15,
temperature: 38.5,
gpsSignal: '强',
movementStatus: '正常',
description: '设备电量低于20%,需要及时充电',
longitude: 116.3974,
latitude: 39.9093
},
{
id: 2,
eartagNumber: 'EARTAG002',
alertType: 'offline',
alertLevel: 'high',
alertTime: '2025-01-18 09:15:00',
battery: 0,
temperature: 0,
gpsSignal: '无',
movementStatus: '静止',
description: '设备已离线超过30分钟',
longitude: 0,
latitude: 0
},
{
id: 3,
eartagNumber: 'EARTAG003',
alertType: 'temperature',
alertLevel: 'medium',
alertTime: '2025-01-18 08:45:00',
battery: 85,
temperature: 42.3,
gpsSignal: '强',
movementStatus: '正常',
description: '设备温度异常,超过正常范围',
longitude: 116.4074,
latitude: 39.9193
},
{
id: 4,
eartagNumber: 'EARTAG004',
alertType: 'movement',
alertLevel: 'low',
alertTime: '2025-01-18 07:20:00',
battery: 92,
temperature: 39.1,
gpsSignal: '强',
movementStatus: '异常',
description: '运动量异常,可能发生异常行为',
longitude: 116.4174,
latitude: 39.9293
// 从API获取预警数据
const fetchAlertData = async () => {
try {
loading.value = true
// 构建查询参数
const params = {
page: pagination.current,
limit: pagination.pageSize,
alertType: alertTypeFilter.value,
search: searchValue.value.trim()
}
]
alerts.value = mockAlerts
pagination.total = mockAlerts.length
// 更新统计数据
stats.lowBattery = mockAlerts.filter(alert => alert.alertType === 'battery').length
stats.offline = mockAlerts.filter(alert => alert.alertType === 'offline').length
stats.highTemperature = mockAlerts.filter(alert => alert.alertType === 'temperature').length
stats.abnormalMovement = mockAlerts.filter(alert => alert.alertType === 'movement').length
// 调用API获取预警数据
const response = await api.get('/smart-alerts/public/eartag', { params })
if (response && response.success) {
alerts.value = response.data || []
pagination.total = response.total || 0
// 更新统计数据
updateStatsFromData(alerts.value)
} else {
console.error('获取预警数据失败:', response)
alerts.value = []
pagination.total = 0
resetStats()
}
} catch (error) {
console.error('获取预警数据失败:', error)
message.error('获取预警数据失败: ' + error.message)
alerts.value = []
pagination.total = 0
resetStats()
} finally {
loading.value = false
}
}
// 从数据更新统计信息
const updateStatsFromData = (data) => {
stats.lowBattery = data.filter(alert => alert.alertType === 'battery').length
stats.offline = data.filter(alert => alert.alertType === 'offline').length
stats.highTemperature = data.filter(alert => alert.alertType === 'temperature').length
stats.abnormalMovement = data.filter(alert => alert.alertType === 'movement').length
}
// 重置统计信息
const resetStats = () => {
stats.lowBattery = 0
stats.offline = 0
stats.highTemperature = 0
stats.abnormalMovement = 0
}
// 更新搜索值

View File

@@ -593,21 +593,69 @@ const handleTableChange = (pag) => {
// 导出数据
const exportData = async () => {
try {
if (!hosts.value || hosts.value.length === 0) {
console.log('=== 开始导出智能主机数据 ===')
message.loading('正在获取所有设备数据...', 0)
// 构建查询参数,获取所有数据
const params = new URLSearchParams({
page: '1',
limit: '10000', // 设置一个很大的限制值来获取所有数据
_t: Date.now().toString()
})
// 如果有搜索条件,添加到参数中
if (searchValue.value.trim()) {
params.append('search', searchValue.value.trim())
console.log('导出搜索条件:', searchValue.value.trim())
}
// 调用API获取所有智能主机数据
const apiUrl = `/api/smart-devices/hosts?${params}`
console.log('导出API请求URL:', apiUrl)
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${localStorage.getItem('token')}`
}
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
const result = await response.json()
console.log('API响应结果:', result)
if (!result.success || !result.data || result.data.length === 0) {
message.destroy()
message.warning('没有数据可导出')
return
}
const allHosts = result.data || []
console.log('获取到所有设备数据:', allHosts.length, '条记录')
console.log('原始数据示例:', allHosts[0])
// 后端已经处理了大部分格式化,直接使用
const exportData = allHosts
console.log('导出设备数据示例:', exportData[0])
console.log('导出设备数据总数:', exportData.length)
message.destroy()
message.loading('正在导出数据...', 0)
const result = ExportUtils.exportDeviceData(hosts.value, 'host')
const result_export = ExportUtils.exportDeviceData(exportData, 'host')
if (result.success) {
if (result_export.success) {
message.destroy()
message.success(`导出成功!文件:${result.filename}`)
message.success(`导出成功!文件:${result_export.filename}`)
} else {
message.destroy()
message.error(result.message)
message.error(result_export.message)
}
} catch (error) {
message.destroy()