保险前后端,养殖端和保险端小程序
This commit is contained in:
4
admin-system/.env.production
Normal file
4
admin-system/.env.production
Normal file
@@ -0,0 +1,4 @@
|
||||
# 生产环境配置
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_API_FULL_URL=https://ad.ningmuyun.com/api
|
||||
VITE_USE_PROXY=false
|
||||
5684
admin-system/package-lock.json
generated
5684
admin-system/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
|
||||
417
admin-system/src/utils/apiClient.js
Normal file
417
admin-system/src/utils/apiClient.js
Normal 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;
|
||||
211
admin-system/src/utils/apiResponseFormat.js
Normal file
211
admin-system/src/utils/apiResponseFormat.js
Normal 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: '数据导入成功'
|
||||
};
|
||||
@@ -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' }
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
271
admin-system/src/utils/filterManager.js
Normal file
271
admin-system/src/utils/filterManager.js
Normal 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;
|
||||
}
|
||||
};
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 更新搜索值
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
// 更新搜索值
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user