修改文件结构,统一文档格式

This commit is contained in:
ylweng
2025-09-01 02:42:03 +08:00
parent 2bd1d8c032
commit abc1184f81
151 changed files with 870 additions and 589 deletions

View File

@@ -0,0 +1,18 @@
# 环境变量配置示例
# 复制此文件为 .env.local 并填入真实的配置值
# 百度地图API密钥
# 请访问 http://lbsyun.baidu.com/apiconsole/key 申请有效的API密钥
# 注意:应用类型必须选择「浏览器端」
# 如果遇到「APP不存在AK有误」错误请检查
# 1. API密钥是否正确
# 2. 应用类型是否为「浏览器端」
# 3. Referer白名单配置开发环境可设置为 *
# 4. API密钥状态是否为「启用」
VITE_BAIDU_MAP_API_KEY=your_valid_baidu_map_api_key_here
# API服务地址
VITE_API_BASE_URL=http://localhost:5350/api
# 应用环境
VITE_APP_ENV=development

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宁夏智慧养殖监管平台</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

1662
admin-system/frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
{
"name": "nxxmdata-frontend",
"version": "1.0.0",
"description": "宁夏智慧养殖监管平台前端",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview"
},
"dependencies": {
"ant-design-vue": "^4.0.0",
"axios": "^1.11.0",
"echarts": "^5.4.0",
"pinia": "^2.0.0",
"vue": "^3.4.0",
"vue-router": "^4.0.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^5.0.0",
"vite": "^5.0.0"
}
}

View File

@@ -0,0 +1,373 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>经纬度输入调试</title>
<style>
body {
padding: 20px;
font-family: Arial, sans-serif;
background: #f5f5f5;
}
.debug-container {
max-width: 900px;
margin: 0 auto;
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.debug-section {
margin-bottom: 30px;
padding: 20px;
border: 1px solid #e8e8e8;
border-radius: 6px;
background: #fafafa;
}
.debug-title {
font-size: 18px;
font-weight: bold;
margin-bottom: 15px;
color: #1890ff;
}
.debug-info {
background: #f0f0f0;
padding: 15px;
border-radius: 4px;
margin-top: 10px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
border-left: 4px solid #52c41a;
font-size: 13px;
}
.input-group {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.input-item {
flex: 1;
}
.input-item label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.input-item input {
width: 100%;
padding: 8px 12px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.input-item input:focus {
border-color: #1890ff;
outline: none;
box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
}
.test-case {
margin-bottom: 10px;
padding: 12px;
background: white;
border: 1px solid #e8e8e8;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
}
.test-case-info {
flex: 1;
}
.test-case-name {
font-weight: bold;
color: #333;
}
.test-case-values {
color: #666;
font-size: 12px;
margin-top: 2px;
}
button {
padding: 6px 12px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
button:hover {
background: #40a9ff;
}
button:disabled {
background: #ccc;
cursor: not-allowed;
}
.log-area {
height: 200px;
overflow-y: auto;
background: #1e1e1e;
color: #d4d4d4;
padding: 10px;
border-radius: 4px;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.4;
}
.clear-btn {
background: #ff4d4f;
margin-top: 10px;
}
.clear-btn:hover {
background: #ff7875;
}
</style>
</head>
<body>
<div class="debug-container">
<h1>经纬度输入调试工具</h1>
<!-- 实时输入测试 -->
<div class="debug-section">
<div class="debug-title">实时输入测试</div>
<div class="input-group">
<div class="input-item">
<label>经度 (Longitude):</label>
<input
type="text"
id="longitude"
placeholder="请输入经度 (-180 到 180)"
oninput="handleCoordinateInput('longitude', this.value)"
/>
</div>
<div class="input-item">
<label>纬度 (Latitude):</label>
<input
type="text"
id="latitude"
placeholder="请输入纬度 (-90 到 90)"
oninput="handleCoordinateInput('latitude', this.value)"
/>
</div>
</div>
<div class="debug-info" id="currentValues">
当前值:
经度: undefined (类型: undefined)
纬度: undefined (类型: undefined)
提交数据:
{}
</div>
<div style="margin-top: 15px;">
<button onclick="simulateSubmit()">模拟提交到后端</button>
<button onclick="clearCoordinates()" style="margin-left: 10px;">清空坐标</button>
</div>
</div>
<!-- 预设测试用例 -->
<div class="debug-section">
<div class="debug-title">预设测试用例</div>
<div id="testCases"></div>
</div>
<!-- 操作日志 -->
<div class="debug-section">
<div class="debug-title">操作日志</div>
<div class="log-area" id="logArea"></div>
<button class="clear-btn" onclick="clearLogs()">清空日志</button>
</div>
</div>
<script>
// 全局状态
let formData = {
longitude: undefined,
latitude: undefined
};
let logs = [];
const testCases = [
{ name: '正常小数', longitude: '106.2400', latitude: '38.4900' },
{ name: '整数', longitude: '106', latitude: '38' },
{ name: '高精度小数', longitude: '106.234567', latitude: '38.487654' },
{ name: '负数', longitude: '-106.2400', latitude: '-38.4900' },
{ name: '包含字母', longitude: '106.24abc', latitude: '38.49xyz' },
{ name: '多个小数点', longitude: '106.24.56', latitude: '38.49.78' },
{ name: '空字符串', longitude: '', latitude: '' },
{ name: '只有小数点', longitude: '.', latitude: '.' },
{ name: '边界值', longitude: '180.0', latitude: '90.0' },
{ name: '超出范围', longitude: '200.0', latitude: '100.0' }
];
// 坐标解析器
function parseCoordinate(value) {
if (!value) return value;
// 移除非数字字符,保留小数点和负号
const cleaned = value.toString().replace(/[^\d.-]/g, '');
// 确保只有一个小数点
const parts = cleaned.split('.');
let result;
if (parts.length > 2) {
result = parts[0] + '.' + parts.slice(1).join('');
} else {
result = cleaned;
}
return result;
}
// 处理坐标输入
function handleCoordinateInput(type, value) {
log(`${type === 'longitude' ? '经度' : '纬度'}输入: "${value}"`);
const parsed = parseCoordinate(value);
log(`${type === 'longitude' ? '经度' : '纬度'}解析后: "${parsed}"`);
// 转换为数字
const numValue = parsed === '' ? undefined : parseFloat(parsed);
formData[type] = isNaN(numValue) ? undefined : numValue;
log(`${type === 'longitude' ? '经度' : '纬度'}最终值: ${formData[type]} (类型: ${typeof formData[type]})`);
// 更新输入框显示
document.getElementById(type).value = parsed;
// 更新显示
updateDisplay();
}
// 更新显示
function updateDisplay() {
const currentValuesDiv = document.getElementById('currentValues');
const submitData = getSubmitData();
currentValuesDiv.textContent = `当前值:
经度: ${formData.longitude} (类型: ${typeof formData.longitude})
纬度: ${formData.latitude} (类型: ${typeof formData.latitude})
提交数据:
${JSON.stringify(submitData, null, 2)}`;
}
// 获取提交数据
function getSubmitData() {
const submitData = {
name: '测试农场',
type: 'farm',
owner: '测试负责人',
phone: '13800138000',
address: '测试地址',
status: 'active'
};
// 只有当经纬度有效时才添加
if (formData.longitude !== undefined && formData.longitude !== null) {
submitData.longitude = formData.longitude;
}
if (formData.latitude !== undefined && formData.latitude !== null) {
submitData.latitude = formData.latitude;
}
return submitData;
}
// 应用测试用例
function applyTestCase(testCase) {
log(`\n=== 应用测试用例: ${testCase.name} ===`);
log(`设置经度: "${testCase.longitude}"`);
log(`设置纬度: "${testCase.latitude}"`);
// 设置输入框值并触发处理
document.getElementById('longitude').value = testCase.longitude;
document.getElementById('latitude').value = testCase.latitude;
handleCoordinateInput('longitude', testCase.longitude);
handleCoordinateInput('latitude', testCase.latitude);
}
// 模拟提交
async function simulateSubmit() {
log(`\n=== 模拟API提交 ===`);
const submitData = getSubmitData();
log(`提交数据: ${JSON.stringify(submitData, null, 2)}`);
try {
// 模拟API调用
const response = await fetch('http://localhost:5350/api/farms', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(submitData)
});
const result = await response.json();
log(`API响应成功: ${JSON.stringify(result)}`);
alert('提交成功');
} catch (error) {
log(`API调用失败: ${error.message}`);
alert('API调用失败: ' + error.message);
}
}
// 清空坐标
function clearCoordinates() {
formData.longitude = undefined;
formData.latitude = undefined;
document.getElementById('longitude').value = '';
document.getElementById('latitude').value = '';
updateDisplay();
log('已清空坐标');
}
// 日志功能
function log(message) {
const timestamp = new Date().toLocaleTimeString();
logs.push(`[${timestamp}] ${message}`);
updateLogDisplay();
}
function updateLogDisplay() {
const logArea = document.getElementById('logArea');
logArea.textContent = logs.join('\n');
logArea.scrollTop = logArea.scrollHeight;
}
function clearLogs() {
logs = [];
updateLogDisplay();
}
// 初始化
document.addEventListener('DOMContentLoaded', function() {
// 渲染测试用例
const testCasesDiv = document.getElementById('testCases');
testCases.forEach(testCase => {
const div = document.createElement('div');
div.className = 'test-case';
div.innerHTML = `
<div class="test-case-info">
<div class="test-case-name">${testCase.name}</div>
<div class="test-case-values">经度: ${testCase.longitude}, 纬度: ${testCase.latitude}</div>
</div>
<button onclick="applyTestCase({name: '${testCase.name}', longitude: '${testCase.longitude}', latitude: '${testCase.latitude}'})">应用</button>
`;
testCasesDiv.appendChild(div);
});
log('调试工具已加载');
log('请使用测试用例或手动输入来测试经纬度输入框');
updateDisplay();
});
</script>
</body>
</html>

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>设备数据调试</title>
</head>
<body>
<h1>设备数据调试</h1>
<div id="result"></div>
<script>
async function testDeviceAPI() {
const resultDiv = document.getElementById('result');
try {
console.log('开始测试设备API...');
// 测试API调用
const response = await fetch('http://localhost:5350/api/devices/public');
const data = await response.json();
console.log('API响应:', data);
// 显示结果
const devices = data.data || [];
const statusCount = {};
devices.forEach(device => {
statusCount[device.status] = (statusCount[device.status] || 0) + 1;
});
resultDiv.innerHTML = `
<h2>API测试结果</h2>
<p><strong>设备总数:</strong> ${devices.length}</p>
<p><strong>在线设备:</strong> ${statusCount.online || 0}</p>
<p><strong>离线设备:</strong> ${statusCount.offline || 0}</p>
<p><strong>维护设备:</strong> ${statusCount.maintenance || 0}</p>
<h3>前3个设备:</h3>
<pre>${JSON.stringify(devices.slice(0, 3), null, 2)}</pre>
`;
} catch (error) {
console.error('API测试失败:', error);
resultDiv.innerHTML = `<p style="color: red;">API测试失败: ${error.message}</p>`;
}
}
// 页面加载后执行测试
window.addEventListener('load', testDeviceAPI);
</script>
</body>
</html>

View File

@@ -0,0 +1,187 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户管理调试页面</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 800px;
margin: 0 auto;
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.success {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.error {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.info {
background-color: #d1ecf1;
color: #0c5460;
border: 1px solid #bee5eb;
}
button {
background-color: #007bff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #0056b3;
}
pre {
background-color: #f8f9fa;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>用户管理调试页面</h1>
<div id="tokenStatus" class="status info">
检查中...
</div>
<div id="apiStatus" class="status info">
等待测试...
</div>
<div>
<button onclick="checkToken()">检查Token</button>
<button onclick="testLogin()">测试登录</button>
<button onclick="testUsersAPI()">测试用户API</button>
<button onclick="clearToken()">清除Token</button>
</div>
<div id="results">
<h3>测试结果:</h3>
<pre id="output"></pre>
</div>
</div>
<script>
const API_BASE_URL = 'http://localhost:5350/api';
function log(message) {
const output = document.getElementById('output');
output.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
}
function checkToken() {
const token = localStorage.getItem('token');
const tokenStatus = document.getElementById('tokenStatus');
if (token) {
tokenStatus.className = 'status success';
tokenStatus.textContent = `Token存在 (长度: ${token.length})`;
log('Token存在: ' + token.substring(0, 50) + '...');
} else {
tokenStatus.className = 'status error';
tokenStatus.textContent = 'Token不存在';
log('Token不存在');
}
}
async function testLogin() {
try {
log('开始测试登录...');
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
username: 'admin',
password: '123456'
})
});
const data = await response.json();
log('登录响应: ' + JSON.stringify(data, null, 2));
if (data.success && data.token) {
localStorage.setItem('token', data.token);
log('Token已保存到localStorage');
checkToken();
} else {
log('登录失败: ' + data.message);
}
} catch (error) {
log('登录错误: ' + error.message);
}
}
async function testUsersAPI() {
try {
log('开始测试用户API...');
const token = localStorage.getItem('token');
if (!token) {
log('错误: 没有找到token请先登录');
return;
}
const response = await fetch(`${API_BASE_URL}/users`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
const data = await response.json();
log('用户API响应: ' + JSON.stringify(data, null, 2));
const apiStatus = document.getElementById('apiStatus');
if (data.success && data.data) {
apiStatus.className = 'status success';
apiStatus.textContent = `API正常 (获取到${data.data.length}个用户)`;
} else {
apiStatus.className = 'status error';
apiStatus.textContent = 'API异常';
}
} catch (error) {
log('用户API错误: ' + error.message);
const apiStatus = document.getElementById('apiStatus');
apiStatus.className = 'status error';
apiStatus.textContent = 'API调用失败';
}
}
function clearToken() {
localStorage.removeItem('token');
log('Token已清除');
checkToken();
}
// 页面加载时自动检查token
window.onload = function() {
checkToken();
};
</script>
</body>
</html>

View File

@@ -0,0 +1,90 @@
<!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>
#mapContainer {
width: 100%;
height: 500px;
border: 2px solid #ccc;
margin: 20px 0;
}
.log {
background: #f5f5f5;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
}
</style>
</head>
<body>
<h1>百度地图测试页面</h1>
<div id="mapContainer"></div>
<div class="log" id="logContainer">正在加载百度地图API...</div>
<script>
const logContainer = document.getElementById('logContainer');
function log(message) {
console.log(message);
logContainer.textContent += '\n' + new Date().toLocaleTimeString() + ': ' + message;
}
// 百度地图API回调函数
window.initBMap = function() {
log('百度地图API加载成功');
try {
// 创建地图实例
const map = new BMap.Map('mapContainer');
log('地图实例创建成功');
// 设置中心点和缩放级别
const point = new BMap.Point(106.27, 38.47); // 银川市中心
map.centerAndZoom(point, 12);
log('地图中心点设置成功: ' + point.lng + ', ' + point.lat);
// 启用滚轮缩放
map.enableScrollWheelZoom(true);
log('滚轮缩放已启用');
// 添加控件
map.addControl(new BMap.NavigationControl());
map.addControl(new BMap.ScaleControl());
log('地图控件添加成功');
// 添加测试标记
const marker = new BMap.Marker(point);
map.addOverlay(marker);
log('测试标记添加成功');
// 添加信息窗口
const infoWindow = new BMap.InfoWindow('这是一个测试标记点');
marker.addEventListener('click', function() {
map.openInfoWindow(infoWindow, point);
});
log('信息窗口事件绑定成功');
log('地图初始化完成!');
} catch (error) {
log('地图初始化失败: ' + error.message);
console.error('地图初始化失败:', error);
}
};
// 动态加载百度地图API
const script = document.createElement('script');
script.src = 'https://api.map.baidu.com/api?v=3.0&ak=3AN3VahoqaXUs32U8luXD2Dwn86KK5B7&callback=initBMap';
script.onerror = function() {
log('百度地图API加载失败');
};
document.head.appendChild(script);
log('开始加载百度地图API...');
</script>
</body>
</html>

View File

@@ -0,0 +1,135 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>自动登录测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-item {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.success {
background-color: #f6ffed;
border-color: #b7eb8f;
}
.error {
background-color: #fff2f0;
border-color: #ffccc7;
}
button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #40a9ff;
}
</style>
</head>
<body>
<div class="container">
<h1>自动登录功能测试</h1>
<div class="test-item">
<h3>测试步骤:</h3>
<ol>
<li>点击"清除登录状态"按钮</li>
<li>点击"访问登录页面"按钮</li>
<li>观察是否自动登录并跳转到仪表盘</li>
<li>点击"访问用户管理页面"测试路由守卫</li>
</ol>
</div>
<div class="test-item">
<h3>当前状态:</h3>
<p>Token: <span id="token-status">检查中...</span></p>
<p>用户信息: <span id="user-status">检查中...</span></p>
</div>
<div class="test-item">
<h3>测试操作:</h3>
<button onclick="clearLoginState()">清除登录状态</button>
<button onclick="checkLoginState()">检查登录状态</button>
<button onclick="visitLoginPage()">访问登录页面</button>
<button onclick="visitUsersPage()">访问用户管理页面</button>
<button onclick="visitDashboard()">访问仪表盘</button>
</div>
<div class="test-item">
<h3>测试结果:</h3>
<div id="test-results"></div>
</div>
</div>
<script>
function addResult(message, isSuccess = true) {
const results = document.getElementById('test-results');
const div = document.createElement('div');
div.className = isSuccess ? 'success' : 'error';
div.style.margin = '10px 0';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
results.appendChild(div);
}
function clearLoginState() {
localStorage.removeItem('token');
localStorage.removeItem('user');
addResult('已清除登录状态');
checkLoginState();
}
function checkLoginState() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
document.getElementById('token-status').textContent = token ? '已存在' : '不存在';
document.getElementById('user-status').textContent = user ? JSON.parse(user).username : '未登录';
addResult(`登录状态检查 - Token: ${token ? '存在' : '不存在'}, 用户: ${user ? JSON.parse(user).username : '未登录'}`);
}
function visitLoginPage() {
addResult('正在访问登录页面...');
window.open('http://localhost:5300/#/login', '_blank');
}
function visitUsersPage() {
addResult('正在访问用户管理页面...');
window.open('http://localhost:5300/#/users', '_blank');
}
function visitDashboard() {
addResult('正在访问仪表盘...');
window.open('http://localhost:5300/#/dashboard', '_blank');
}
// 页面加载时检查状态
window.onload = function() {
checkLoginState();
addResult('测试页面已加载,请按照测试步骤进行操作');
};
</script>
</body>
</html>

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户数据显示测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 1000px;
margin: 50px auto;
padding: 20px;
background-color: #f5f5f5;
}
.container {
background: white;
padding: 30px;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.test-item {
margin: 20px 0;
padding: 15px;
border: 1px solid #ddd;
border-radius: 4px;
}
.success {
background-color: #f6ffed;
border-color: #b7eb8f;
}
.error {
background-color: #fff2f0;
border-color: #ffccc7;
}
button {
background-color: #1890ff;
color: white;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
margin: 5px;
}
button:hover {
background-color: #40a9ff;
}
table {
width: 100%;
border-collapse: collapse;
margin-top: 15px;
}
th, td {
border: 1px solid #ddd;
padding: 8px;
text-align: left;
}
th {
background-color: #f5f5f5;
}
.json-display {
background: #f8f8f8;
padding: 10px;
border-radius: 4px;
font-family: monospace;
white-space: pre-wrap;
max-height: 300px;
overflow-y: auto;
}
</style>
</head>
<body>
<div class="container">
<h1>用户数据显示测试</h1>
<div class="test-item">
<h3>测试步骤:</h3>
<ol>
<li>点击"测试API调用"按钮</li>
<li>查看API响应数据</li>
<li>点击"访问用户管理页面"查看前端显示</li>
<li>对比数据是否一致</li>
</ol>
</div>
<div class="test-item">
<h3>API测试</h3>
<button onclick="testAPI()">测试API调用</button>
<button onclick="visitUsersPage()">访问用户管理页面</button>
<button onclick="checkLocalStorage()">检查本地存储</button>
</div>
<div class="test-item">
<h3>API响应数据</h3>
<div id="api-response" class="json-display">点击"测试API调用"查看数据...</div>
</div>
<div class="test-item">
<h3>用户数据表格:</h3>
<div id="users-table">暂无数据</div>
</div>
<div class="test-item">
<h3>测试结果:</h3>
<div id="test-results"></div>
</div>
</div>
<script>
function addResult(message, isSuccess = true) {
const results = document.getElementById('test-results');
const div = document.createElement('div');
div.className = isSuccess ? 'success' : 'error';
div.style.margin = '10px 0';
div.style.padding = '10px';
div.style.borderRadius = '4px';
div.innerHTML = `<strong>${new Date().toLocaleTimeString()}</strong>: ${message}`;
results.appendChild(div);
}
async function testAPI() {
try {
addResult('正在调用用户API...');
const token = localStorage.getItem('token');
if (!token) {
addResult('未找到token请先登录', false);
return;
}
const response = await fetch('http://localhost:5350/api/users', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
// 显示API响应
document.getElementById('api-response').textContent = JSON.stringify(data, null, 2);
// 创建用户表格
if (data.success && data.data) {
createUsersTable(data.data);
addResult(`API调用成功获取到 ${data.data.length} 个用户`);
} else {
addResult('API响应格式异常', false);
}
} catch (error) {
addResult(`API调用失败: ${error.message}`, false);
document.getElementById('api-response').textContent = `错误: ${error.message}`;
}
}
function createUsersTable(users) {
const tableContainer = document.getElementById('users-table');
if (!users || users.length === 0) {
tableContainer.innerHTML = '<p>没有用户数据</p>';
return;
}
let tableHTML = `
<table>
<thead>
<tr>
<th>ID</th>
<th>用户名</th>
<th>邮箱</th>
<th>角色</th>
<th>状态</th>
<th>创建时间</th>
</tr>
</thead>
<tbody>
`;
users.forEach(user => {
tableHTML += `
<tr>
<td>${user.id}</td>
<td>${user.username}</td>
<td>${user.email}</td>
<td>${user.role || '未知'}</td>
<td>${user.status || '未知'}</td>
<td>${user.created_at ? new Date(user.created_at).toLocaleString('zh-CN') : '未知'}</td>
</tr>
`;
});
tableHTML += `
</tbody>
</table>
`;
tableContainer.innerHTML = tableHTML;
}
function visitUsersPage() {
addResult('正在打开用户管理页面...');
window.open('http://localhost:5300/#/users', '_blank');
}
function checkLocalStorage() {
const token = localStorage.getItem('token');
const user = localStorage.getItem('user');
addResult(`Token: ${token ? '存在' : '不存在'}, 用户信息: ${user ? '存在' : '不存在'}`);
if (token) {
addResult('Token内容: ' + token.substring(0, 50) + '...');
}
}
// 页面加载时检查登录状态
window.onload = function() {
checkLocalStorage();
addResult('测试页面已加载,请按照测试步骤进行操作');
};
</script>
</body>
</html>

View File

@@ -0,0 +1,113 @@
<template>
<div v-if="isLoggedIn">
<a-layout style="min-height: 100vh">
<a-layout-header class="header">
<div class="logo">
<a-button
type="text"
@click="settingsStore.toggleSidebar"
style="color: white; margin-right: 8px;"
>
<menu-unfold-outlined v-if="sidebarCollapsed" />
<menu-fold-outlined v-else />
</a-button>
宁夏智慧养殖监管平台
</div>
<div class="user-info">
<span>欢迎, {{ userData?.username }}</span>
<a-button type="link" @click="handleLogout" style="color: white;">退出</a-button>
</div>
</a-layout-header>
<a-layout>
<a-layout-sider
width="200"
style="background: #fff"
:collapsed="sidebarCollapsed"
collapsible
>
<Menu />
</a-layout-sider>
<a-layout style="padding: 0 24px 24px">
<a-layout-content
:style="{ background: '#fff', padding: '24px', margin: '16px 0' }"
>
<router-view />
</a-layout-content>
<a-layout-footer style="text-align: center">
宁夏智慧养殖监管平台 ©2025
</a-layout-footer>
</a-layout>
</a-layout>
</a-layout>
</div>
<div v-else>
<router-view />
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import Menu from './components/Menu.vue'
import { useUserStore, useSettingsStore } from './stores'
import { MenuFoldOutlined, MenuUnfoldOutlined } from '@ant-design/icons-vue'
// 使用Pinia状态管理
const userStore = useUserStore()
const settingsStore = useSettingsStore()
const router = useRouter()
// 计算属性
const isLoggedIn = computed(() => userStore.isLoggedIn)
const userData = computed(() => userStore.userData)
const sidebarCollapsed = computed(() => settingsStore.sidebarCollapsed)
// 注意路由守卫已移至router/index.js
// 监听多标签页登录状态同步
const handleStorageChange = (event) => {
if (event.key === 'token' || event.key === 'user') {
userStore.checkLoginStatus()
}
}
// 登出处理
const handleLogout = () => {
userStore.logout()
router.push('/login')
}
onMounted(() => {
userStore.checkLoginStatus()
window.addEventListener('storage', handleStorageChange)
})
onUnmounted(() => {
window.removeEventListener('storage', handleStorageChange)
})
</script>
<style scoped>
.header {
display: flex;
align-items: center;
justify-content: space-between;
background: #001529;
color: white;
padding: 0 24px;
}
.logo {
font-size: 18px;
font-weight: bold;
}
.user-info {
display: flex;
align-items: center;
gap: 16px;
}
</style>

View File

@@ -0,0 +1,324 @@
<template>
<div class="alert-stats">
<a-card :bordered="false" title="预警数据统计">
<div class="stats-summary">
<div class="stat-item">
<div class="stat-value">{{ totalAlerts }}</div>
<div class="stat-label">预警总数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ criticalAlerts }}</div>
<div class="stat-label">严重预警</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ resolvedRate }}%</div>
<div class="stat-label">解决率</div>
</div>
</div>
<a-divider />
<div class="chart-container">
<e-chart :options="alertTypeOptions" height="300px" />
</div>
<a-divider />
<div class="alert-table">
<a-table
:dataSource="alertTableData"
:columns="alertColumns"
:pagination="{ pageSize: 5 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<a-tag :color="getLevelColor(record.level)">
{{ record.level }}
</a-tag>
</template>
<template v-if="column.key === 'status'">
<a-tag
:color="getStatusColor(record.status)"
style="cursor: pointer"
@click="handleStatusUpdate(record)"
>
{{ getStatusLabel(record.status) }}
</a-tag>
</template>
</template>
</a-table>
</div>
</a-card>
</div>
</template>
<script setup>
import { computed } from 'vue'
import { useDataStore } from '../stores'
import EChart from './EChart.vue'
import { message, Modal } from 'ant-design-vue'
import { api } from '../utils/api'
// 使用数据存储
const dataStore = useDataStore()
// 预警总数
const totalAlerts = computed(() => dataStore.alerts.length)
// 严重预警数量
const criticalAlerts = computed(() => {
return dataStore.alerts.filter(alert => alert.level === '严重').length
})
// 已解决预警数量
const resolvedAlerts = computed(() => {
return dataStore.alerts.filter(alert => alert.status === '已解决').length
})
// 解决率
const resolvedRate = computed(() => {
if (totalAlerts.value === 0) return 0
return Math.round((resolvedAlerts.value / totalAlerts.value) * 100)
})
// 预警类型分布图表选项
const alertTypeOptions = computed(() => {
// 按类型分组统计
const typeCount = {}
dataStore.alerts.forEach(alert => {
typeCount[alert.type] = (typeCount[alert.type] || 0) + 1
})
// 转换为图表数据格式
const data = Object.keys(typeCount).map(type => ({
value: typeCount[type],
name: type
}))
return {
title: {
text: '预警类型分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '预警类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
}
})
// 预警表格数据
const alertTableData = computed(() => {
return dataStore.alerts.map(alert => {
const farm = dataStore.farms.find(f => f.id === alert.farm_id)
const device = dataStore.devices.find(d => d.id === alert.device_id)
return {
key: alert.id,
id: alert.id,
type: alert.type,
level: alert.level,
message: alert.message,
farmId: alert.farm_id,
farmName: farm ? farm.name : `养殖场 #${alert.farm_id}`,
deviceId: alert.device_id,
deviceName: device ? device.name : `设备 #${alert.device_id}`,
status: alert.status,
timestamp: alert.created_at
}
})
})
// 预警表格列定义
const alertColumns = [
{
title: '预警类型',
dataIndex: 'type',
key: 'type',
filters: [
{ text: '温度异常', value: '温度异常' },
{ text: '湿度异常', value: '湿度异常' },
{ text: '设备故障', value: '设备故障' },
{ text: '电量不足', value: '电量不足' },
{ text: '网络异常', value: '网络异常' }
],
onFilter: (value, record) => record.type === value
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
filters: [
{ text: '轻微', value: '轻微' },
{ text: '一般', value: '一般' },
{ text: '严重', value: '严重' }
],
onFilter: (value, record) => record.level === value
},
{
title: '养殖场',
dataIndex: 'farmName',
key: 'farmName'
},
{
title: '设备',
dataIndex: 'deviceName',
key: 'deviceName'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '未处理', value: '未处理' },
{ text: '处理中', value: '处理中' },
{ text: '已解决', value: '已解决' }
],
onFilter: (value, record) => record.status === value
},
{
title: '时间',
dataIndex: 'timestamp',
key: 'timestamp',
sorter: (a, b) => new Date(a.timestamp) - new Date(b.timestamp),
defaultSortOrder: 'descend'
}
]
// 获取级别颜色
function getLevelColor(level) {
switch (level) {
case '轻微': return 'blue'
case '一般': return 'orange'
case '严重': return 'red'
default: return 'blue'
}
}
// 获取状态颜色
function getStatusColor(status) {
switch (status) {
case 'active': return 'error'
case 'acknowledged': return 'processing'
case 'resolved': return 'success'
case '未处理': return 'error'
case '处理中': return 'processing'
case '已解决': return 'success'
default: return 'default'
}
}
// 处理状态更新
function handleStatusUpdate(record) {
const statusOptions = [
{ label: '活跃', value: 'active' },
{ label: '已确认', value: 'acknowledged' },
{ label: '已解决', value: 'resolved' }
]
const currentIndex = statusOptions.findIndex(option => option.value === record.status)
const nextIndex = (currentIndex + 1) % statusOptions.length
const nextStatus = statusOptions[nextIndex]
Modal.confirm({
title: '更新预警状态',
content: `确定要将预警状态从"${getStatusLabel(record.status)}"更新为"${nextStatus.label}"吗?`,
onOk: async () => {
try {
// 调用API更新预警状态
await api.put(`/alerts/public/${record.id}/status`, {
status: nextStatus.value
})
// 更新本地数据
const alertIndex = dataStore.alerts.findIndex(alert => alert.id === record.id)
if (alertIndex !== -1) {
dataStore.alerts[alertIndex].status = nextStatus.value
}
message.success(`预警状态已更新为"${nextStatus.label}"`)
} catch (error) {
console.error('更新预警状态失败:', error)
message.error('更新预警状态失败,请重试')
}
}
})
}
// 获取状态标签
function getStatusLabel(status) {
switch (status) {
case 'active': return '活跃'
case 'acknowledged': return '已确认'
case 'resolved': return '已解决'
default: return status
}
}
</script>
<style scoped>
.alert-stats {
width: 100%;
}
.stats-summary {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #1890ff;
}
.stat-label {
font-size: 14px;
color: #8c8c8c;
}
.chart-container {
margin: 16px 0;
}
.alert-table {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,238 @@
<template>
<div class="animal-stats">
<a-card :bordered="false" title="动物数据统计">
<div class="stats-summary">
<div class="stat-item">
<div class="stat-value">{{ totalAnimals }}</div>
<div class="stat-label">动物总数</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ animalTypes.length }}</div>
<div class="stat-label">动物种类</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ averagePerFarm }}</div>
<div class="stat-label">平均数量/</div>
</div>
</div>
<a-divider />
<div class="chart-container">
<e-chart :options="typeDistributionOptions" height="300px" />
</div>
<a-divider />
<div class="animal-table">
<a-table
:dataSource="animalTableData"
:columns="animalColumns"
:pagination="{ pageSize: 5 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'count'">
<a-progress
:percent="getPercentage(record.count, maxCount)"
:stroke-color="getProgressColor(record.count, maxCount)"
/>
</template>
</template>
</a-table>
</div>
</a-card>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useDataStore } from '../stores'
import EChart from './EChart.vue'
// 使用数据存储
const dataStore = useDataStore()
// 组件挂载时加载数据
onMounted(async () => {
console.log('AnimalStats: 组件挂载,开始加载数据')
await dataStore.fetchAllData()
console.log('AnimalStats: 数据加载完成')
console.log('farms数据:', dataStore.farms)
console.log('animals数据:', dataStore.animals)
})
// 动物总数
const totalAnimals = computed(() => {
return dataStore.animals.reduce((total, animal) => total + (animal.count || 0), 0)
})
// 动物种类
const animalTypes = computed(() => {
const types = new Set(dataStore.animals.map(animal => animal.type))
return Array.from(types)
})
// 平均每个养殖场的动物数量
const averagePerFarm = computed(() => {
if (dataStore.farms.length === 0) return 0
return Math.round(totalAnimals.value / dataStore.farms.length)
})
// 动物类型分布图表选项
const typeDistributionOptions = computed(() => {
// 按类型分组统计
const typeCount = {}
dataStore.animals.forEach(animal => {
typeCount[animal.type] = (typeCount[animal.type] || 0) + (animal.count || 0)
})
// 转换为图表数据格式
const data = Object.keys(typeCount).map(type => ({
value: typeCount[type],
name: type
}))
return {
title: {
text: '动物类型分布',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '动物类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
}
})
// 动物表格数据
const animalTableData = computed(() => {
// 按类型和养殖场分组统计
const groupMap = new Map()
dataStore.animals.forEach(animal => {
const key = `${animal.farm_id}-${animal.type}`
if (!groupMap.has(key)) {
const farm = dataStore.farms.find(f => f.id === animal.farm_id)
groupMap.set(key, {
key: key,
farmId: animal.farm_id,
farmName: farm ? farm.name : `养殖场 #${animal.farm_id}`,
type: animal.type,
count: 0
})
}
groupMap.get(key).count += (animal.count || 0)
})
// 转换为数组并按数量降序排序
return Array.from(groupMap.values()).sort((a, b) => b.count - a.count)
})
// 最大数量
const maxCount = computed(() => {
if (animalTableData.value.length === 0) return 1
return Math.max(...animalTableData.value.map(item => item.count))
})
// 动物表格列定义
const animalColumns = [
{
title: '养殖场',
dataIndex: 'farmName',
key: 'farmName'
},
{
title: '动物类型',
dataIndex: 'type',
key: 'type'
},
{
title: '数量',
dataIndex: 'count',
key: 'count',
sorter: (a, b) => a.count - b.count,
defaultSortOrder: 'descend'
}
]
// 计算百分比
function getPercentage(value, max) {
return Math.round((value / max) * 100)
}
// 获取进度条颜色
function getProgressColor(value, max) {
const percentage = getPercentage(value, max)
if (percentage < 30) return '#52c41a'
if (percentage < 70) return '#1890ff'
return '#ff4d4f'
}
</script>
<style scoped>
.animal-stats {
width: 100%;
}
.stats-summary {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #1890ff;
}
.stat-label {
font-size: 14px;
color: #8c8c8c;
}
.chart-container {
margin: 16px 0;
}
.animal-table {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,386 @@
<template>
<div class="baidu-map-wrapper">
<div class="baidu-map-container" ref="mapContainer"></div>
<!-- 自定义缩放控制按钮 -->
<div class="map-zoom-controls" v-if="isReady">
<button class="zoom-btn zoom-in" @click="zoomIn" title="放大">
<span class="zoom-icon">+</span>
</button>
<button class="zoom-btn zoom-out" @click="zoomOut" title="缩小">
<span class="zoom-icon"></span>
</button>
<button class="zoom-btn zoom-reset" @click="resetZoom" title="重置缩放">
<span class="zoom-icon"></span>
</button>
</div>
<!-- 缩放级别显示 -->
<div class="zoom-level-display" v-if="isReady && showZoomLevel">
缩放级别: {{ currentZoom }}
</div>
<div v-if="isLoading" class="map-loading-overlay">
<div class="loading-content">
<div class="loading-spinner"></div>
<p>地图加载中...</p>
</div>
</div>
<!-- 新增加载状态信息div -->
<div v-if="statusText" class="map-status">{{ statusText }}</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import { createMap, addMarkers, clearOverlays, setViewToFitMarkers } from '../utils/mapService'
// 定义组件属性
const props = defineProps({
// 地图标记数据
markers: {
type: Array,
default: () => []
},
// 地图配置选项
options: {
type: Object,
default: () => ({})
},
// 地图高度
height: {
type: String,
default: '100%'
},
// 是否显示缩放级别
showZoomLevel: {
type: Boolean,
default: true
}
})
// 定义事件
const emit = defineEmits(['map-ready', 'marker-click'])
// 地图容器引用
const mapContainer = ref(null)
// 组件状态
const isLoading = ref(true)
// 新增:加载状态文本
const statusText = ref('')
let clearStatusTimer = null
// 新增:地图就绪标志
const isReady = ref(false)
// 缩放相关状态
const currentZoom = ref(10)
const defaultZoom = ref(10)
// 地图实例和标记
let baiduMap = null
let mapMarkers = []
let markersInitialized = false
// 初始化地图
async function initMap() {
statusText.value = '正在初始化地图...'
console.log('BaiduMap组件: 开始初始化地图')
console.log('地图容器:', mapContainer.value)
console.log('容器尺寸:', mapContainer.value?.offsetWidth, 'x', mapContainer.value?.offsetHeight)
console.log('window.BMap是否存在:', typeof window.BMap)
if (!mapContainer.value) {
console.error('地图容器不存在')
statusText.value = '地图容器不存在'
isLoading.value = false
return
}
try {
console.log('BaiduMap组件: 调用createMap函数')
statusText.value = '正在创建地图实例...'
// 创建地图实例
baiduMap = await createMap(mapContainer.value, props.options)
// 验证地图实例是否有效
if (!baiduMap || typeof baiduMap.addEventListener !== 'function') {
throw new Error('地图实例创建失败或无效')
}
console.log('BaiduMap组件: 地图创建成功', baiduMap)
statusText.value = '地图实例创建成功,正在加载瓦片...'
// 监听地图完全加载完成事件
baiduMap.addEventListener('tilesloaded', () => {
console.log('BaiduMap组件: 地图瓦片加载完成')
// 只在第一次加载时初始化
if (!markersInitialized) {
console.log('BaiduMap组件: 首次加载,开始初始化')
// 标记地图就绪,防止兜底重复添加
if (baiduMap) {
baiduMap._markersAdded = true
}
// 初始化缩放相关状态
if (baiduMap && typeof baiduMap.getZoom === 'function') {
currentZoom.value = baiduMap.getZoom()
defaultZoom.value = currentZoom.value
// 添加缩放事件监听器
baiduMap.addEventListener('zoomend', () => {
if (baiduMap && typeof baiduMap.getZoom === 'function') {
currentZoom.value = baiduMap.getZoom()
console.log('缩放级别变化:', currentZoom.value)
}
})
}
isReady.value = true
isLoading.value = false
statusText.value = '地图加载完成'
// 自动隐藏状态条
if (clearStatusTimer) clearTimeout(clearStatusTimer)
clearStatusTimer = setTimeout(() => { statusText.value = '' }, 1200)
// 地图就绪后,检查是否有待添加的标记
if (props.markers && props.markers.length > 0) {
console.log('BaiduMap组件: 地图就绪,添加标记', props.markers.length)
addMarkersToMap()
} else {
console.log('BaiduMap组件: 没有标记数据,添加测试标记')
const testMarkers = [{
location: { lng: 106.27, lat: 38.47 },
title: '测试标记',
content: '这是一个测试标记点'
}]
addMarkers(baiduMap, testMarkers)
}
markersInitialized = true
}
})
// 备用方案:如果事件监听失败,使用延时
setTimeout(() => {
if (baiduMap && !baiduMap._markersAdded) {
console.log('BaiduMap组件: 备用方案 - 延时添加标记')
if (baiduMap) {
baiduMap._markersAdded = true
}
isReady.value = true
isLoading.value = false
statusText.value = '地图加载完成(兼容模式)'
if (clearStatusTimer) clearTimeout(clearStatusTimer)
clearStatusTimer = setTimeout(() => { statusText.value = '' }, 1500)
if (props.markers && props.markers.length > 0) {
console.log('BaiduMap组件: 备用方案 - 地图就绪,添加标记', props.markers.length)
addMarkersToMap()
} else {
const testMarkers = [{
location: { lng: 106.27, lat: 38.47 },
title: '测试标记',
content: '这是一个测试标记点'
}]
addMarkers(baiduMap, testMarkers)
}
}
}, 2000)
// 触发地图就绪事件
emit('map-ready', baiduMap)
console.log('BaiduMap组件: 地图初始化完成')
} catch (error) {
console.error('初始化百度地图失败:', error)
console.error('错误详情:', error.stack)
statusText.value = '地图加载失败,请检查网络或密钥配置'
isLoading.value = false
isReady.value = false
baiduMap = null
// 清理状态文本
if (clearStatusTimer) clearTimeout(clearStatusTimer)
clearStatusTimer = setTimeout(() => { statusText.value = '' }, 3000)
}
}
// 添加标记到地图
function addMarkersToMap() {
// 检查地图实例是否有效
if (!baiduMap || !isReady.value) {
console.warn('BaiduMap组件: 地图未就绪,跳过标记添加')
return
}
// 验证地图实例的有效性
if (typeof baiduMap.addEventListener !== 'function') {
console.error('BaiduMap组件: 地图实例无效')
return
}
try {
// 清除现有标记
clearOverlays(baiduMap)
// 添加新标记
mapMarkers = addMarkers(baiduMap, props.markers, (markerData, marker) => {
// 触发标记点击事件
emit('marker-click', markerData, marker)
})
// 调整视图以显示所有标记
if (mapMarkers.length > 0) {
const points = mapMarkers.map(marker => marker.getPosition())
setViewToFitMarkers(baiduMap, points)
}
} catch (error) {
console.error('BaiduMap组件: 添加标记失败:', error)
}
}
// 缩放方法
function zoomIn() {
if (baiduMap && isReady.value) {
const currentZoomLevel = baiduMap.getZoom()
const newZoom = Math.min(currentZoomLevel + 1, 19)
baiduMap.setZoom(newZoom)
currentZoom.value = newZoom
console.log('地图放大到级别:', newZoom)
}
}
function zoomOut() {
if (baiduMap && isReady.value) {
const currentZoomLevel = baiduMap.getZoom()
const newZoom = Math.max(currentZoomLevel - 1, 3)
baiduMap.setZoom(newZoom)
currentZoom.value = newZoom
console.log('地图缩小到级别:', newZoom)
}
}
function resetZoom() {
if (baiduMap && isReady.value) {
// 重置到默认缩放级别和中心点
const defaultCenter = new window.BMap.Point(106.27, 38.47)
baiduMap.centerAndZoom(defaultCenter, defaultZoom.value)
currentZoom.value = defaultZoom.value
console.log('地图重置到默认状态:', defaultZoom.value)
}
}
// 监听标记数据变化
watch(() => props.markers, (newMarkers) => {
if (baiduMap && isReady.value && newMarkers) {
addMarkersToMap()
} else if (newMarkers && newMarkers.length > 0) {
// 如果地图还未就绪但有标记数据,等待地图就绪后再添加
console.log('BaiduMap组件: 地图未就绪,等待地图加载完成后添加标记')
}
}, { deep: true })
// 组件挂载时初始化地图
onMounted(() => {
console.log('BaiduMap组件: 组件已挂载')
console.log('地图容器DOM:', mapContainer.value)
console.log('地图容器ID:', mapContainer.value?.id)
console.log('地图容器类名:', mapContainer.value?.className)
// 延迟初始化确保DOM完全渲染
setTimeout(() => {
console.log('BaiduMap组件: 延迟初始化地图')
initMap()
}, 100)
})
// 组件卸载时清理资源
onUnmounted(() => {
if (baiduMap) {
clearOverlays(baiduMap)
baiduMap = null
}
isReady.value = false
markersInitialized = false
if (clearStatusTimer) {
clearTimeout(clearStatusTimer)
clearStatusTimer = null
}
})
// 暴露地图实例
defineExpose({
getMapInstance: () => baiduMap
})
</script>
<style scoped>
.baidu-map-wrapper {
position: relative;
width: 100%;
min-height: 400px;
}
.baidu-map-container {
width: 100%;
height: v-bind(height);
min-height: 400px;
position: relative;
border: 1px solid #ddd;
background-color: #f5f5f5;
overflow: hidden;
}
.map-loading-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.9);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
/* 新增:地图状态信息样式 */
.map-status {
position: absolute;
top: 10px;
left: 10px;
background: rgba(0,0,0,0.55);
color: #fff;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
z-index: 1001;
pointer-events: none;
}
.loading-content {
text-align: center;
color: #666;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #1890ff;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 10px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-content p {
margin: 0;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,217 @@
<template>
<div class="chart-performance-monitor">
<a-card title="图表性能监控" size="small" :bordered="false">
<template #extra>
<a-space>
<a-button type="text" size="small" @click="refreshStats">
<template #icon><reload-outlined /></template>
刷新
</a-button>
<a-button type="text" size="small" @click="clearCache" danger>
<template #icon><delete-outlined /></template>
清理缓存
</a-button>
</a-space>
</template>
<div class="performance-stats">
<a-row :gutter="16">
<a-col :span="6">
<a-statistic
title="图表实例"
:value="stats.chartInstances"
suffix="个"
:value-style="{ color: '#3f8600' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="数据缓存"
:value="stats.dataCache"
suffix="项"
:value-style="{ color: '#1890ff' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="内存使用"
:value="totalMemoryUsage"
suffix="MB"
:precision="2"
:value-style="{ color: '#722ed1' }"
/>
</a-col>
<a-col :span="6">
<a-statistic
title="缓存命中率"
:value="cacheHitRate"
suffix="%"
:precision="1"
:value-style="{ color: cacheHitRate > 70 ? '#3f8600' : '#faad14' }"
/>
</a-col>
</a-row>
</div>
<a-divider />
<div class="performance-details">
<a-descriptions size="small" :column="2">
<a-descriptions-item label="渲染器">Canvas (硬件加速)</a-descriptions-item>
<a-descriptions-item label="动画优化">启用 (300ms)</a-descriptions-item>
<a-descriptions-item label="大数据优化">启用 (2000+)</a-descriptions-item>
<a-descriptions-item label="防抖更新">启用 (100ms)</a-descriptions-item>
<a-descriptions-item label="懒加载">支持</a-descriptions-item>
<a-descriptions-item label="数据缓存TTL">2-5分钟</a-descriptions-item>
</a-descriptions>
</div>
<div class="performance-tips" v-if="showTips">
<a-alert
:message="performanceTip.title"
:description="performanceTip.description"
:type="performanceTip.type"
show-icon
closable
@close="showTips = false"
/>
</div>
</a-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { ReloadOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import { getCacheStats, clearAllCache } from '../utils/chartService'
import { message } from 'ant-design-vue'
// 性能统计数据
const stats = ref({
chartInstances: 0,
dataCache: 0,
memoryUsage: {
charts: 0,
data: 0
}
})
// 缓存命中统计
const cacheHits = ref(0)
const cacheRequests = ref(0)
const showTips = ref(true)
// 计算总内存使用量
const totalMemoryUsage = computed(() => {
return stats.value.memoryUsage.charts + stats.value.memoryUsage.data
})
// 计算缓存命中率
const cacheHitRate = computed(() => {
if (cacheRequests.value === 0) return 0
return (cacheHits.value / cacheRequests.value) * 100
})
// 性能提示
const performanceTip = computed(() => {
const hitRate = cacheHitRate.value
const memoryUsage = totalMemoryUsage.value
if (memoryUsage > 50) {
return {
title: '内存使用较高',
description: '建议清理缓存或减少图表实例数量以优化性能',
type: 'warning'
}
} else if (hitRate < 50) {
return {
title: '缓存命中率较低',
description: '考虑增加缓存时间或优化数据请求策略',
type: 'info'
}
} else if (hitRate > 80) {
return {
title: '性能表现优秀',
description: '图表缓存工作良好,加载速度已优化',
type: 'success'
}
} else {
return {
title: '性能表现良好',
description: '图表加载速度正常,缓存机制运行稳定',
type: 'info'
}
}
})
// 刷新统计数据
function refreshStats() {
stats.value = getCacheStats()
// message.success('统计数据已刷新')
}
// 清理缓存
function clearCache() {
clearAllCache()
refreshStats()
cacheHits.value = 0
cacheRequests.value = 0
message.success('缓存已清理')
}
// 模拟缓存命中统计实际项目中应该从chartService获取
function simulateCacheStats() {
// 模拟缓存请求
cacheRequests.value += Math.floor(Math.random() * 3) + 1
// 模拟缓存命中
cacheHits.value += Math.floor(Math.random() * 2) + 1
}
// 定时器
let statsTimer = null
// 组件挂载时开始监控
onMounted(() => {
refreshStats()
// 每5秒更新一次统计数据
statsTimer = setInterval(() => {
refreshStats()
simulateCacheStats()
}, 5000)
})
// 组件卸载时清理定时器
onUnmounted(() => {
if (statsTimer) {
clearInterval(statsTimer)
}
})
</script>
<style scoped>
.chart-performance-monitor {
margin-bottom: 16px;
}
.performance-stats {
margin-bottom: 16px;
}
.performance-details {
margin-bottom: 16px;
}
.performance-tips {
margin-top: 16px;
}
:deep(.ant-statistic-content) {
font-size: 18px;
}
:deep(.ant-statistic-title) {
font-size: 12px;
color: #666;
}
</style>

View File

@@ -0,0 +1,226 @@
<template>
<div class="dashboard">
<h1>系统概览</h1>
<div class="dashboard-stats">
<a-card title="养殖场数量" :bordered="false">
<template #extra><a-tag color="blue">总计</a-tag></template>
<p class="stat-number">{{ farmCount }}</p>
<p class="stat-change"><rise-outlined /> 较上月增长 {{ statsData.farmGrowth }}%</p>
</a-card>
<a-card title="动物种类" :bordered="false">
<template #extra><a-tag color="green">总计</a-tag></template>
<p class="stat-number">{{ animalCount }}</p>
<p class="stat-change"><rise-outlined /> 较上月增长 {{ statsData.animalGrowth }}%</p>
</a-card>
<a-card title="监测设备" :bordered="false">
<template #extra><a-tag color="orange">在线率</a-tag></template>
<p class="stat-number">{{ deviceCount }}</p>
<p class="stat-change">在线率 {{ deviceOnlineRate }}%</p>
</a-card>
<a-card title="预警信息" :bordered="false">
<template #extra><a-tag color="red">本月</a-tag></template>
<p class="stat-number">{{ alertCount }}</p>
<p class="stat-change"><fall-outlined /> 较上月下降 {{ statsData.alertReduction }}%</p>
</a-card>
</div>
<div class="dashboard-charts">
<a-card title="养殖场分布" :bordered="false" class="chart-card">
<baidu-map
:markers="farmMarkers"
height="350px"
@marker-click="handleFarmClick"
/>
</a-card>
<!-- <a-card title="月度数据趋势" :bordered="false" class="chart-card">
<div ref="trendChart" class="chart-container"></div>
</a-card> -->
</div>
<!-- 养殖场详情抽屉 -->
<a-drawer
:title="`养殖场详情 - ${selectedFarmId ? dataStore.farms.find(f => f.id == selectedFarmId)?.name : ''}`"
:width="600"
:visible="drawerVisible"
@close="closeDrawer"
:bodyStyle="{ paddingBottom: '80px' }"
>
<farm-detail v-if="selectedFarmId" :farm-id="selectedFarmId" />
</a-drawer>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { RiseOutlined, FallOutlined } from '@ant-design/icons-vue'
import { useDataStore } from '../stores'
import { convertFarmsToMarkers } from '../utils/mapService'
import BaiduMap from './BaiduMap.vue'
import FarmDetail from './FarmDetail.vue'
import { message } from 'ant-design-vue'
// 使用数据存储
const dataStore = useDataStore()
// 图表容器引用
const trendChart = ref(null)
// 从store获取统计数据
const statsData = computed(() => dataStore.stats)
// 从store获取计算属性
const farmCount = computed(() => dataStore.farmCount)
const animalCount = computed(() => dataStore.animalCount)
const deviceCount = computed(() => dataStore.deviceCount)
const alertCount = computed(() => dataStore.alertCount)
const deviceOnlineRate = computed(() => dataStore.deviceOnlineRate)
// 养殖场地图标记
const farmMarkers = computed(() => {
console.log('Dashboard: 计算farmMarkers')
console.log('dataStore.farms:', dataStore.farms)
const markers = convertFarmsToMarkers(dataStore.farms)
console.log('转换后的markers:', markers)
return markers
})
// 抽屉控制
const drawerVisible = ref(false)
const selectedFarmId = ref(null)
// 处理养殖场标记点击事件
function handleFarmClick(markerData) {
// 设置选中的养殖场ID
selectedFarmId.value = markerData.originalData.id
// 打开抽屉
drawerVisible.value = true
}
// 关闭抽屉
function closeDrawer() {
drawerVisible.value = false
}
// 图表实例
let trendChartInstance = null
// 组件挂载后初始化数据和图表
onMounted(async () => {
console.log('Dashboard: 组件挂载,开始加载数据')
// 加载数据
await dataStore.fetchAllData()
console.log('Dashboard: 数据加载完成')
console.log('farms数据:', dataStore.farms)
// 初始化趋势图表
initTrendChart()
// 添加窗口大小变化监听
window.addEventListener('resize', handleResize)
})
// 组件卸载时清理资源
onUnmounted(() => {
// 移除窗口大小变化监听
window.removeEventListener('resize', handleResize)
// 销毁图表实例
if (trendChartInstance) {
trendChartInstance.dispose()
trendChartInstance = null
}
})
// 初始化趋势图表
function initTrendChart() {
if (!trendChart.value) return
// 导入图表服务
import('../utils/chartService').then(({ createTrendChart }) => {
// 使用空数据初始化图表,等待后端数据
const trendData = {
xAxis: [],
series: []
}
// 创建趋势图表
trendChartInstance = createTrendChart(trendChart.value, trendData, {
title: {
text: '月度数据趋势',
left: 'center'
}
})
})
}
// 处理窗口大小变化
function handleResize() {
if (trendChartInstance) {
trendChartInstance.resize()
}
}
</script>
<style scoped>
.dashboard {
padding: 20px;
}
.dashboard-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
}
.stat-number {
font-size: 28px;
font-weight: bold;
margin-bottom: 8px;
}
.stat-change {
color: #8c8c8c;
display: flex;
align-items: center;
gap: 4px;
}
.dashboard-charts {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.chart-card {
height: 400px;
}
.map-container,
.chart-container {
height: 350px;
}
@media (max-width: 1200px) {
.dashboard-stats {
grid-template-columns: repeat(2, 1fr);
}
.dashboard-charts {
grid-template-columns: 1fr;
}
}
@media (max-width: 768px) {
.dashboard-stats {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,370 @@
<template>
<div class="device-stats">
<a-card :bordered="false" title="设备数据统计">
<div class="stats-summary">
<div class="stat-item">
<div class="stat-value">{{ totalDevices }}</div>
<div class="stat-label">设备总数</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #52c41a;">{{ onlineDevices }}</div>
<div class="stat-label">在线设备</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #faad14;">{{ maintenanceDevices }}</div>
<div class="stat-label">维护设备</div>
</div>
<div class="stat-item">
<div class="stat-value" style="color: #ff4d4f;">{{ offlineDevices }}</div>
<div class="stat-label">离线设备</div>
</div>
<div class="stat-item">
<div class="stat-value">{{ onlineRate }}%</div>
<div class="stat-label">在线率</div>
</div>
</div>
<a-divider />
<div class="chart-container">
<e-chart :options="deviceStatusOptions" height="300px" />
</div>
<a-divider />
<div class="device-table">
<a-table
:dataSource="deviceTableData"
:columns="deviceColumns"
:pagination="{ pageSize: 5 }"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.originalStatus)">
{{ record.status }}
</a-tag>
</template>
</template>
</a-table>
</div>
</a-card>
</div>
</template>
<script setup>
// 设备统计组件 - 显示设备状态分布
import { computed, onMounted, watch } from 'vue'
import { useDataStore } from '../stores'
import EChart from './EChart.vue'
// 使用数据存储
const dataStore = useDataStore()
// 组件挂载时确保数据已加载
onMounted(async () => {
console.log('=== DeviceStats组件挂载 ===')
console.log('当前设备数量:', dataStore.devices.length)
try {
console.log('开始获取设备数据...')
// 直接测试API调用
console.log('直接测试API调用...')
const { deviceService } = await import('../utils/dataService')
const apiResult = await deviceService.getAllDevices()
console.log('直接API调用结果:', apiResult)
console.log('API返回数据类型:', typeof apiResult)
console.log('API返回是否为数组:', Array.isArray(apiResult))
console.log('API返回数据长度:', apiResult?.length || 0)
await dataStore.fetchDevices()
console.log('设备数据获取完成:', dataStore.devices.length, '台设备')
// 详细输出设备数据
console.log('设备数据详情:', dataStore.devices.slice(0, 3))
const statusCount = {}
dataStore.devices.forEach(device => {
statusCount[device.status] = (statusCount[device.status] || 0) + 1
})
console.log('设备状态分布:', statusCount)
// 输出计算属性的值
console.log('计算属性值:')
console.log('- totalDevices:', totalDevices.value)
console.log('- onlineDevices:', onlineDevices.value)
console.log('- maintenanceDevices:', maintenanceDevices.value)
console.log('- offlineDevices:', offlineDevices.value)
console.log('- onlineRate:', onlineRate.value)
console.log('=== DeviceStats组件挂载完成 ===')
} catch (error) {
console.error('获取设备数据失败:', error)
console.error('错误堆栈:', error.stack)
}
})
// 监听设备数据变化
watch(() => dataStore.devices, (newDevices) => {
console.log('设备数据更新:', newDevices.length, '台设备')
const statusCount = {}
newDevices.forEach(device => {
statusCount[device.status] = (statusCount[device.status] || 0) + 1
})
console.log('当前状态分布:', statusCount)
// 输出前几个设备的详细信息
if (newDevices.length > 0) {
console.log('前3个设备详情:', newDevices.slice(0, 3).map(d => ({ id: d.id, name: d.name, status: d.status })))
}
}, { deep: true, immediate: true })
// 设备总数
const totalDevices = computed(() => dataStore.devices.length)
// 在线设备数量
const onlineDevices = computed(() => {
return dataStore.devices.filter(device => device.status === 'online').length
})
// 维护设备数量
const maintenanceDevices = computed(() => {
return dataStore.devices.filter(device => device.status === 'maintenance').length
})
// 离线设备数量
const offlineDevices = computed(() => {
return dataStore.devices.filter(device => device.status === 'offline').length
})
// 在线率
const onlineRate = computed(() => {
if (totalDevices.value === 0) return 0
return Math.round((onlineDevices.value / totalDevices.value) * 100)
})
// 设备状态分布图表选项
const deviceStatusOptions = computed(() => {
console.log('计算图表选项,当前设备数量:', dataStore.devices.length)
// 如果没有设备数据,显示空状态
if (dataStore.devices.length === 0) {
console.log('没有设备数据,显示空状态图表')
return {
title: {
text: '设备状态分布',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
series: [
{
name: '设备状态',
type: 'pie',
radius: ['50%', '70%'],
data: [
{
value: 1,
name: '暂无数据',
itemStyle: {
color: '#d9d9d9'
}
}
]
}
]
}
}
// 按状态分组统计
const statusCount = {}
dataStore.devices.forEach(device => {
const chineseStatus = getStatusText(device.status)
statusCount[chineseStatus] = (statusCount[chineseStatus] || 0) + 1
})
console.log('状态统计:', statusCount)
// 定义状态颜色映射
const statusColors = {
'在线': '#52c41a',
'维护中': '#faad14',
'离线': '#ff4d4f'
}
// 转换为图表数据格式,并添加颜色
const data = Object.keys(statusCount).map(status => ({
value: statusCount[status],
name: status,
itemStyle: {
color: statusColors[status] || '#d9d9d9'
}
}))
console.log('图表数据:', data)
return {
title: {
text: '设备状态分布',
left: 'center',
textStyle: {
fontSize: 16,
fontWeight: 'bold'
}
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c}台 ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0,
itemGap: 20
},
series: [
{
name: '设备状态',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
}
})
// 设备表格数据
const deviceTableData = computed(() => {
return dataStore.devices.map(device => {
const farm = dataStore.farms.find(f => f.id === device.farm_id || f.id === device.farmId)
return {
key: device.id,
id: device.id,
name: device.name,
type: device.type,
farmId: device.farm_id || device.farmId,
farmName: farm ? farm.name : `养殖场 #${device.farm_id || device.farmId}`,
status: getStatusText(device.status),
originalStatus: device.status,
lastUpdate: device.last_maintenance || device.lastUpdate || '未知'
}
})
})
// 设备表格列定义
const deviceColumns = [
{
title: '设备名称',
dataIndex: 'name',
key: 'name'
},
{
title: '设备类型',
dataIndex: 'type',
key: 'type'
},
{
title: '养殖场',
dataIndex: 'farmName',
key: 'farmName'
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
filters: [
{ text: '在线', value: '在线' },
{ text: '维护中', value: '维护中' },
{ text: '离线', value: '离线' }
],
onFilter: (value, record) => record.status === value
},
{
title: '最后更新',
dataIndex: 'lastUpdate',
key: 'lastUpdate',
sorter: (a, b) => new Date(a.lastUpdate) - new Date(b.lastUpdate),
defaultSortOrder: 'descend'
}
]
// 获取状态文本(英文转中文)
function getStatusText(status) {
switch (status) {
case 'online': return '在线'
case 'offline': return '离线'
case 'maintenance': return '维护中'
default: return status
}
}
// 获取状态颜色(支持英文状态)
function getStatusColor(status) {
switch (status) {
case 'online':
case '在线': return 'success'
case 'offline':
case '离线': return 'error'
case 'maintenance':
case '维护中': return 'processing'
default: return 'default'
}
}
</script>
<style scoped>
.device-stats {
width: 100%;
}
.stats-summary {
display: flex;
justify-content: space-around;
margin-bottom: 16px;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 24px;
font-weight: bold;
color: #1890ff;
}
.stat-label {
font-size: 14px;
color: #8c8c8c;
}
.chart-container {
margin: 16px 0;
}
.device-table {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,172 @@
<template>
<div class="echart-container" ref="chartContainer" :style="{ height: height }"></div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, watch, computed } from 'vue'
import { createChart, handleResize, disposeChart, DataCache } from '../utils/chartService'
// 定义组件属性
const props = defineProps({
// 图表选项
options: {
type: Object,
required: true
},
// 图表高度
height: {
type: String,
default: '300px'
},
// 自动调整大小
autoResize: {
type: Boolean,
default: true
},
// 缓存键
cacheKey: {
type: String,
default: null
},
// 启用缓存
enableCache: {
type: Boolean,
default: true
},
// 缓存过期时间(毫秒)
cacheTTL: {
type: Number,
default: 5 * 60 * 1000 // 5分钟
}
})
// 定义事件
const emit = defineEmits(['chart-ready', 'chart-click'])
// 图表容器引用
const chartContainer = ref(null)
// 图表实例
let chartInstance = null
// 计算缓存键
const computedCacheKey = computed(() => {
if (!props.enableCache || !props.cacheKey) return null
return `chart_${props.cacheKey}_${JSON.stringify(props.options).slice(0, 100)}`
})
// 初始化图表
function initChart() {
if (!chartContainer.value) return
// 检查缓存
if (props.enableCache && computedCacheKey.value) {
const cachedData = DataCache.get(computedCacheKey.value)
if (cachedData) {
console.log('使用缓存的图表数据:', computedCacheKey.value)
}
}
// 创建图表实例(使用缓存键)
chartInstance = createChart(
chartContainer.value,
props.options,
computedCacheKey.value
)
// 添加点击事件监听
chartInstance.on('click', (params) => {
emit('chart-click', params)
})
// 缓存图表配置
if (props.enableCache && computedCacheKey.value) {
DataCache.set(computedCacheKey.value, props.options, props.cacheTTL)
}
// 触发图表就绪事件
emit('chart-ready', chartInstance)
}
// 更新图表选项(优化版本)
function updateChart() {
if (chartInstance) {
// 使用notMerge=false和lazyUpdate=true来优化性能
chartInstance.setOption(props.options, false, true)
// 更新缓存
if (props.enableCache && computedCacheKey.value) {
DataCache.set(computedCacheKey.value, props.options, props.cacheTTL)
}
}
}
// 防抖更新函数
let updateTimer = null
function debouncedUpdate() {
if (updateTimer) {
clearTimeout(updateTimer)
}
updateTimer = setTimeout(() => {
updateChart()
}, 100) // 100ms防抖
}
// 监听选项变化(优化版本)
watch(() => props.options, (newOptions, oldOptions) => {
if (chartInstance) {
// 深度比较,避免不必要的更新
const optionsChanged = JSON.stringify(newOptions) !== JSON.stringify(oldOptions)
if (optionsChanged) {
debouncedUpdate()
}
}
}, { deep: true })
// 处理窗口大小变化
function onResize() {
handleResize(chartInstance)
}
// 组件挂载时初始化图表
onMounted(() => {
initChart()
// 添加窗口大小变化监听
if (props.autoResize) {
window.addEventListener('resize', onResize)
}
})
// 组件卸载时清理资源
onUnmounted(() => {
// 清理防抖定时器
if (updateTimer) {
clearTimeout(updateTimer)
updateTimer = null
}
// 移除窗口大小变化监听
if (props.autoResize) {
window.removeEventListener('resize', onResize)
}
// 销毁图表实例(传入缓存键以清理相关缓存)
disposeChart(chartInstance, computedCacheKey.value)
chartInstance = null
})
// 暴露方法
defineExpose({
getChartInstance: () => chartInstance,
resize: () => handleResize(chartInstance)
})
</script>
<style scoped>
.echart-container {
width: 100%;
min-height: 200px;
background-color: #fff;
}
</style>

View File

@@ -0,0 +1,174 @@
<template>
<div class="farm-detail">
<a-descriptions :title="farm.name" bordered :column="{ xxl: 2, xl: 2, lg: 2, md: 1, sm: 1, xs: 1 }">
<a-descriptions-item label="养殖场ID">{{ farm.id }}</a-descriptions-item>
<a-descriptions-item label="动物数量">{{ farm.animalCount || '未知' }}</a-descriptions-item>
<a-descriptions-item label="位置坐标">
{{ farm.location ? `${farm.location.lat.toFixed(4)}, ${farm.location.lng.toFixed(4)}` : '未知' }}
</a-descriptions-item>
<a-descriptions-item label="设备数量">
{{ farmDevices.length }}
</a-descriptions-item>
<a-descriptions-item label="预警数量">
<a-badge :count="farmAlerts.length" :number-style="{ backgroundColor: farmAlerts.length > 0 ? '#f5222d' : '#52c41a' }" />
</a-descriptions-item>
<a-descriptions-item label="创建时间">
{{ farm.createdAt ? new Date(farm.createdAt).toLocaleString() : '未知' }}
</a-descriptions-item>
</a-descriptions>
<a-divider orientation="left">设备状态</a-divider>
<a-table
:dataSource="farmDevices"
:columns="deviceColumns"
:pagination="{ pageSize: 5 }"
size="small"
:loading="loading.devices"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'online' ? 'green' : 'red'">
{{ record.status === 'online' ? '在线' : '离线' }}
</a-tag>
</template>
</template>
</a-table>
<a-divider orientation="left">预警信息</a-divider>
<a-table
:dataSource="farmAlerts"
:columns="alertColumns"
:pagination="{ pageSize: 5 }"
size="small"
:loading="loading.alerts"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'level'">
<a-tag :color="getAlertLevelColor(record.level)">
{{ getAlertLevelText(record.level) }}
</a-tag>
</template>
<template v-if="column.key === 'created_at'">
{{ new Date(record.created_at).toLocaleString() }}
</template>
</template>
</a-table>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useDataStore } from '../stores'
// 定义组件属性
const props = defineProps({
// 养殖场ID
farmId: {
type: [Number, String],
required: true
}
})
// 数据存储
const dataStore = useDataStore()
// 加载状态
const loading = ref({
devices: false,
alerts: false
})
// 获取养殖场信息
const farm = computed(() => {
return dataStore.farms.find(f => f.id == props.farmId) || {}
})
// 获取养殖场设备
const farmDevices = computed(() => {
return dataStore.devices.filter(d => d.farm_id == props.farmId)
})
// 获取养殖场预警
const farmAlerts = computed(() => {
return dataStore.alerts.filter(a => a.farm_id == props.farmId)
})
// 设备表格列定义
const deviceColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
}
]
// 预警表格列定义
const alertColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '类型',
dataIndex: 'type',
key: 'type',
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
}
]
// 获取预警级别颜色
function getAlertLevelColor(level) {
switch(level) {
case 'high': return 'red'
case 'medium': return 'orange'
case 'low': return 'blue'
default: return 'default'
}
}
// 获取预警级别文本
function getAlertLevelText(level) {
switch(level) {
case 'high': return '高'
case 'medium': return '中'
case 'low': return '低'
default: return '未知'
}
}
// 监听养殖场ID变化确保数据已加载
watch(() => props.farmId, async (newId) => {
if (newId) {
// 确保数据已加载
if (dataStore.farms.length === 0) {
await dataStore.fetchAllData()
}
}
}, { immediate: true })
</script>
<style scoped>
.farm-detail {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,34 @@
<template>
<a-menu
mode="inline"
:selected-keys="[activeRoute]"
:style="{ height: '100%', borderRight: 0 }"
>
<a-menu-item v-for="route in menuRoutes" :key="route.name">
<router-link :to="route.path">
<component :is="route.meta.icon" />
<span>{{ route.meta.title }}</span>
</router-link>
</a-menu-item>
</a-menu>
</template>
<script setup>
import { computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import * as Icons from '@ant-design/icons-vue'
const router = useRouter()
const route = useRoute()
// 获取所有需要在菜单中显示的路由
const menuRoutes = computed(() => {
return router.options.routes.filter(route => {
// 只显示需要认证且有图标的路由
return route.meta && route.meta.requiresAuth && route.meta.icon
})
})
// 当前活动路由
const activeRoute = computed(() => route.name)
</script>

View File

@@ -0,0 +1,266 @@
<template>
<div class="monitor-chart">
<div class="chart-header">
<h3>{{ title }}</h3>
<div class="chart-controls">
<a-select
v-model="selectedPeriod"
style="width: 120px"
@change="handlePeriodChange"
>
<a-select-option value="day">今日</a-select-option>
<a-select-option value="week">本周</a-select-option>
<a-select-option value="month">本月</a-select-option>
</a-select>
<a-button type="text" @click="refreshData">
<template #icon><reload-outlined /></template>
</a-button>
</div>
</div>
<div class="chart-content">
<e-chart
:options="chartOptions"
:height="height"
:cache-key="cacheKey"
:enable-cache="enableCache"
:cache-ttl="cacheTTL"
@chart-ready="handleChartReady"
/>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, watch } from 'vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import EChart from './EChart.vue'
// 定义组件属性
const props = defineProps({
// 图表标题
title: {
type: String,
default: '监控图表'
},
// 图表类型
type: {
type: String,
default: 'line',
validator: (value) => ['line', 'bar', 'pie'].includes(value)
},
// 图表数据
data: {
type: Object,
default: () => ({
xAxis: [],
series: []
})
},
// 图表高度
height: {
type: String,
default: '300px'
},
// 自动刷新间隔毫秒0表示不自动刷新
refreshInterval: {
type: Number,
default: 0
},
// 缓存键
cacheKey: {
type: String,
default: null
},
// 启用缓存
enableCache: {
type: Boolean,
default: true
},
// 缓存过期时间(毫秒)
cacheTTL: {
type: Number,
default: 5 * 60 * 1000 // 5分钟
}
})
// 定义事件
const emit = defineEmits(['refresh', 'period-change'])
// 选中的时间周期
const selectedPeriod = ref('day')
// 图表实例
let chartInstance = null
// 自动刷新定时器
let refreshTimer = null
// 处理图表就绪事件
function handleChartReady(chart) {
chartInstance = chart
}
// 处理时间周期变化
function handlePeriodChange(period) {
emit('period-change', period)
}
// 刷新数据
function refreshData() {
emit('refresh', selectedPeriod.value)
}
// 图表选项
const chartOptions = computed(() => {
if (props.type === 'line' || props.type === 'bar') {
return {
title: {
text: props.title,
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: props.data.series?.map(item => item.name) || [],
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: props.type === 'bar',
data: props.data.xAxis || []
},
yAxis: {
type: 'value'
},
series: (props.data.series || []).map(item => ({
name: item.name,
type: props.type,
data: item.data,
smooth: props.type === 'line',
itemStyle: item.itemStyle,
lineStyle: item.lineStyle,
areaStyle: props.type === 'line' ? item.areaStyle : undefined
}))
}
} else if (props.type === 'pie') {
return {
title: {
text: props.title,
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: props.title,
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: props.data || []
}
]
}
}
return {}
})
// 设置自动刷新
function setupAutoRefresh() {
clearInterval(refreshTimer)
if (props.refreshInterval > 0) {
refreshTimer = setInterval(() => {
refreshData()
}, props.refreshInterval)
}
}
// 监听刷新间隔变化
watch(() => props.refreshInterval, () => {
setupAutoRefresh()
})
// 组件挂载时设置自动刷新
onMounted(() => {
setupAutoRefresh()
})
// 组件卸载时清理定时器
onMounted(() => {
clearInterval(refreshTimer)
})
</script>
<style scoped>
.monitor-chart {
width: 100%;
background-color: #fff;
border-radius: 4px;
overflow: hidden;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 16px;
height: 48px;
border-bottom: 1px solid #f0f0f0;
}
.chart-header h3 {
margin: 0;
font-size: 16px;
font-weight: 500;
}
.chart-controls {
display: flex;
align-items: center;
gap: 8px;
}
.chart-content {
padding: 16px;
}
</style>

View File

@@ -0,0 +1,63 @@
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useDataStore } from '../stores'
const dataStore = useDataStore()
const loading = ref(false)
const error = ref('')
const devices = computed(() => dataStore.devices)
const onlineCount = computed(() => devices.value.filter(d => d.status === 'online').length)
const offlineCount = computed(() => devices.value.filter(d => d.status === 'offline').length)
const maintenanceCount = computed(() => devices.value.filter(d => d.status === 'maintenance').length)
async function refreshData() {
loading.value = true
error.value = ''
try {
console.log('开始获取设备数据...')
await dataStore.fetchDevices()
console.log('设备数据获取完成:', devices.value.length)
} catch (err) {
console.error('获取设备数据失败:', err)
error.value = err.message
} finally {
loading.value = false
}
}
onMounted(() => {
console.log('SimpleDeviceTest组件挂载')
refreshData()
})
</script>
<style scoped>
.simple-device-test {
padding: 20px;
border: 1px solid #ddd;
border-radius: 4px;
margin: 10px;
}
button {
padding: 8px 16px;
background: #1890ff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:hover {
background: #40a9ff;
}
pre {
background: #f5f5f5;
padding: 10px;
border-radius: 4px;
overflow-x: auto;
}
</style>

View File

@@ -0,0 +1,298 @@
<template>
<div class="virtual-scroll-chart">
<a-card :title="title" :bordered="false">
<template #extra>
<a-space>
<a-input-number
v-model:value="pageSize"
:min="10"
:max="1000"
:step="10"
size="small"
addon-before="每页"
addon-after=""
@change="handlePageSizeChange"
/>
<a-button size="small" @click="refreshData">
<template #icon><reload-outlined /></template>
刷新
</a-button>
</a-space>
</template>
<!-- 数据统计信息 -->
<div class="data-info" v-if="totalCount > 0">
<a-space>
<a-tag color="blue">总数据: {{ totalCount.toLocaleString() }}</a-tag>
<a-tag color="green">当前页: {{ currentPage }}/{{ totalPages }}</a-tag>
<a-tag color="orange">显示: {{ visibleData.length }}</a-tag>
<a-tag v-if="isVirtualMode" color="purple">虚拟滚动: 开启</a-tag>
</a-space>
</div>
<!-- 图表容器 -->
<div class="chart-wrapper">
<e-chart
:options="chartOptions"
:height="chartHeight"
:cache-key="cacheKey"
:enable-cache="enableCache"
:cache-ttl="cacheTTL"
@chart-ready="handleChartReady"
/>
</div>
<!-- 分页控制 -->
<div class="pagination-wrapper" v-if="totalPages > 1">
<a-pagination
v-model:current="currentPage"
v-model:page-size="pageSize"
:total="totalCount"
:show-size-changer="true"
:show-quick-jumper="true"
:show-total="(total, range) => `${range[0]}-${range[1]} 条,共 ${total}`"
:page-size-options="['10', '20', '50', '100', '200', '500']"
@change="handlePageChange"
@show-size-change="handlePageSizeChange"
/>
</div>
<!-- 加载状态 -->
<div class="loading-wrapper" v-if="loading">
<a-spin size="large" tip="加载中..." />
</div>
</a-card>
</div>
</template>
<script setup>
import { ref, computed, watch, onMounted, nextTick } from 'vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import EChart from './EChart.vue'
import { message } from 'ant-design-vue'
// Props
const props = defineProps({
title: {
type: String,
default: '大数据量图表'
},
data: {
type: Array,
default: () => []
},
chartType: {
type: String,
default: 'line',
validator: (value) => ['line', 'bar', 'scatter'].includes(value)
},
chartHeight: {
type: String,
default: '400px'
},
virtualThreshold: {
type: Number,
default: 1000 // 超过1000条数据启用虚拟滚动
},
enableCache: {
type: Boolean,
default: true
},
cacheTTL: {
type: Number,
default: 300000 // 5分钟
}
})
// Emits
const emit = defineEmits(['refresh', 'page-change'])
// 响应式数据
const loading = ref(false)
const currentPage = ref(1)
const pageSize = ref(100)
const chartInstance = ref(null)
// 计算属性
const totalCount = computed(() => props.data.length)
const totalPages = computed(() => Math.ceil(totalCount.value / pageSize.value))
const isVirtualMode = computed(() => totalCount.value > props.virtualThreshold)
// 缓存键
const cacheKey = computed(() => {
return `virtual_chart_${props.chartType}_${currentPage.value}_${pageSize.value}`
})
// 可见数据(当前页数据)
const visibleData = computed(() => {
if (!isVirtualMode.value) {
return props.data
}
const startIndex = (currentPage.value - 1) * pageSize.value
const endIndex = startIndex + pageSize.value
return props.data.slice(startIndex, endIndex)
})
// 图表配置
const chartOptions = computed(() => {
const baseOptions = {
title: {
text: isVirtualMode.value ? `${props.title} (第${currentPage.value}页)` : props.title,
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross'
}
},
legend: {
top: 30
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: visibleData.value.map(item => item.name || item.x)
},
yAxis: {
type: 'value'
},
// 性能优化配置
animation: visibleData.value.length > 200 ? false : true,
progressive: visibleData.value.length > 500 ? 500 : 0,
progressiveThreshold: 1000
}
// 根据图表类型配置series
if (props.chartType === 'line') {
baseOptions.series = [{
name: '数据',
type: 'line',
data: visibleData.value.map(item => item.value || item.y),
smooth: true,
symbol: visibleData.value.length > 100 ? 'none' : 'circle',
lineStyle: {
width: 1
}
}]
} else if (props.chartType === 'bar') {
baseOptions.series = [{
name: '数据',
type: 'bar',
data: visibleData.value.map(item => item.value || item.y),
itemStyle: {
color: '#1890ff'
}
}]
} else if (props.chartType === 'scatter') {
baseOptions.series = [{
name: '数据',
type: 'scatter',
data: visibleData.value.map(item => [item.x, item.y]),
symbolSize: 6
}]
}
return baseOptions
})
// 处理页面变化
function handlePageChange(page, size) {
currentPage.value = page
pageSize.value = size
emit('page-change', { page, size })
}
// 处理页面大小变化
function handlePageSizeChange(current, size) {
pageSize.value = size
currentPage.value = 1 // 重置到第一页
emit('page-change', { page: 1, size })
}
// 刷新数据
function refreshData() {
loading.value = true
emit('refresh')
// 模拟加载延迟
setTimeout(() => {
loading.value = false
message.success('数据刷新完成')
}, 500)
}
// 图表就绪回调
function handleChartReady(chart) {
chartInstance.value = chart
}
// 监听数据变化,自动调整页面
watch(() => props.data, (newData) => {
if (newData.length === 0) {
currentPage.value = 1
return
}
// 如果当前页超出范围,调整到最后一页
const maxPage = Math.ceil(newData.length / pageSize.value)
if (currentPage.value > maxPage) {
currentPage.value = maxPage
}
}, { immediate: true })
// 组件挂载时的初始化
onMounted(() => {
if (totalCount.value > props.virtualThreshold) {
message.info(`检测到大数据量(${totalCount.value.toLocaleString()}条),已启用虚拟滚动优化`)
}
})
</script>
<style scoped>
.virtual-scroll-chart {
width: 100%;
}
.data-info {
margin-bottom: 16px;
padding: 8px 12px;
background: #fafafa;
border-radius: 6px;
}
.chart-wrapper {
position: relative;
margin-bottom: 16px;
}
.pagination-wrapper {
text-align: center;
padding: 16px 0;
border-top: 1px solid #f0f0f0;
}
.loading-wrapper {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 10;
}
:deep(.ant-card-head-title) {
font-size: 16px;
font-weight: 500;
}
:deep(.ant-pagination) {
margin: 0;
}
</style>

View File

@@ -0,0 +1,44 @@
/**
* 环境配置文件
* 包含各种API密钥和环境变量
*/
// 百度地图API配置
export const BAIDU_MAP_CONFIG = {
// 百度地图API密钥
// 注意:实际项目中应该使用环境变量存储密钥,而不是硬编码
// 从环境变量读取API密钥如果没有则使用开发测试密钥
// 生产环境请设置 VITE_BAIDU_MAP_API_KEY 环境变量
// 请访问 http://lbsyun.baidu.com/apiconsole/key 申请有效的API密钥
apiKey: import.meta.env.VITE_BAIDU_MAP_API_KEY || '3AN3VahoqaXUs32U8luXD2Dwn86KK5B7',
// 默认中心点(宁夏中心位置)
defaultCenter: {
lng: 106.27,
lat: 38.47
},
// 默认缩放级别
defaultZoom: 8
};
// API服务配置
export const API_CONFIG = {
// API基础URL
baseUrl: 'http://localhost:5350/api',
// 请求超时时间(毫秒)
timeout: 10000
};
// 其他环境配置
export const APP_CONFIG = {
// 应用名称
appName: '宁夏智慧养殖监管平台',
// 版本号
version: '1.0.0',
// 是否为开发环境
isDev: process.env.NODE_ENV === 'development'
};

View File

@@ -0,0 +1,32 @@
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
import Antd from 'ant-design-vue'
import 'ant-design-vue/dist/reset.css'
import { themeConfig } from './styles/theme.js'
import './styles/global.css'
import { useUserStore } from './stores/user.js'
// 导入图标组件
import * as Icons from '@ant-design/icons-vue'
const app = createApp(App)
const pinia = createPinia()
// 注册所有图标组件
Object.keys(Icons).forEach(key => {
app.component(key, Icons[key])
})
app.use(pinia)
app.use(router)
app.use(Antd, {
theme: themeConfig
})
// 在应用挂载前初始化用户登录状态
const userStore = useUserStore()
userStore.checkLoginStatus()
app.mount('#app')

View File

@@ -0,0 +1,91 @@
/**
* 路由历史记录服务
* 用于跟踪和管理用户的导航历史
*/
import { ref, watch } from 'vue'
import { useRouter } from 'vue-router'
// 最大历史记录数量
const MAX_HISTORY = 10
// 创建历史记录服务
export function useRouteHistory() {
// 路由实例
const router = useRouter()
// 历史记录
const history = ref([])
// 监听路由变化
watch(
() => router.currentRoute.value,
(route) => {
// 只记录需要认证的路由
if (route.meta.requiresAuth) {
// 添加到历史记录
addToHistory({
name: route.name,
path: route.path,
title: route.meta.title,
timestamp: Date.now()
})
}
},
{ immediate: true }
)
// 添加到历史记录
function addToHistory(item) {
// 检查是否已存在相同路径
const index = history.value.findIndex(h => h.path === item.path)
// 如果已存在,则移除
if (index !== -1) {
history.value.splice(index, 1)
}
// 添加到历史记录开头
history.value.unshift(item)
// 限制历史记录数量
if (history.value.length > MAX_HISTORY) {
history.value = history.value.slice(0, MAX_HISTORY)
}
// 保存到本地存储
saveHistory()
}
// 清除历史记录
function clearHistory() {
history.value = []
saveHistory()
}
// 保存历史记录到本地存储
function saveHistory() {
localStorage.setItem('routeHistory', JSON.stringify(history.value))
}
// 加载历史记录
function loadHistory() {
try {
const saved = localStorage.getItem('routeHistory')
if (saved) {
history.value = JSON.parse(saved)
}
} catch (error) {
console.error('加载历史记录失败:', error)
clearHistory()
}
}
// 初始加载
loadHistory()
return {
history,
clearHistory
}
}

View File

@@ -0,0 +1,67 @@
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores'
import routes from './routes'
// 创建路由实例
const router = createRouter({
history: createWebHistory(),
routes,
scrollBehavior(to, from, savedPosition) {
// 如果有保存的位置,则恢复到保存的位置
if (savedPosition) {
return savedPosition
}
// 否则滚动到顶部
return { top: 0 }
}
})
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 设置页面标题
document.title = to.meta.title ? `${to.meta.title} - 宁夏智慧养殖监管平台` : '宁夏智慧养殖监管平台'
// 获取用户存储
const userStore = useUserStore()
// 如果访问登录页面且已有token尝试自动登录
if (to.path === '/login' && userStore.token) {
try {
// 验证token是否有效
const isValid = await userStore.validateToken()
if (isValid) {
// token有效直接跳转到目标页面或仪表盘
const redirectPath = to.query.redirect || '/dashboard'
next(redirectPath)
return
}
} catch (error) {
console.log('Token验证失败继续到登录页面')
}
}
// 检查该路由是否需要登录权限
if (to.meta.requiresAuth) {
// 如果需要登录但用户未登录,则重定向到登录页面
if (!userStore.isLoggedIn) {
next({
path: '/login',
query: { redirect: to.fullPath } // 保存原本要访问的路径,以便登录后重定向
})
} else {
// 用户已登录,允许访问
next()
}
} else {
// 不需要登录权限的路由,直接访问
next()
}
})
// 全局后置钩子
router.afterEach((to, from) => {
// 路由切换后的逻辑,如记录访问历史、分析等
console.log(`路由从 ${from.path} 切换到 ${to.path}`)
})
export default router

View File

@@ -0,0 +1,153 @@
/**
* 路由配置模块
* 用于集中管理应用的路由配置
*/
// 主布局路由
export const mainRoutes = [
{
path: '/',
name: 'Home',
component: () => import('../views/Home.vue'),
meta: {
title: '首页',
requiresAuth: true,
icon: 'home-outlined'
}
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('../views/Dashboard.vue'),
meta: {
title: '系统概览',
requiresAuth: true,
icon: 'dashboard-outlined'
}
},
{
path: '/analytics',
name: 'Analytics',
component: () => import('../views/Analytics.vue'),
meta: {
title: '数据分析',
requiresAuth: true,
icon: 'bar-chart-outlined'
}
},
{
path: '/monitor',
name: 'Monitor',
component: () => import('../views/Monitor.vue'),
meta: {
title: '实时监控',
requiresAuth: true,
icon: 'line-chart-outlined'
}
},
{
path: '/users',
name: 'Users',
component: () => import('../views/Users.vue'),
meta: {
title: '用户管理',
requiresAuth: true,
icon: 'user-outlined'
}
},
{
path: '/products',
name: 'Products',
component: () => import('../views/Products.vue'),
meta: {
title: '产品管理',
requiresAuth: true,
icon: 'shopping-outlined'
}
},
{
path: '/orders',
name: 'Orders',
component: () => import('../views/Orders.vue'),
meta: {
title: '订单管理',
requiresAuth: true,
icon: 'ShoppingCartOutlined'
}
},
{
path: '/devices',
name: 'Devices',
component: () => import('../views/Devices.vue'),
meta: {
title: '设备管理',
requiresAuth: true,
icon: 'DesktopOutlined'
}
},
{
path: '/animals',
name: 'Animals',
component: () => import('../views/Animals.vue'),
meta: {
title: '动物管理',
requiresAuth: true,
icon: 'BugOutlined'
}
},
{
path: '/alerts',
name: 'Alerts',
component: () => import('../views/Alerts.vue'),
meta: {
title: '预警管理',
requiresAuth: true,
icon: 'AlertOutlined'
}
},
{
path: '/farms',
name: 'Farms',
component: () => import('../views/Farms.vue'),
meta: {
title: '养殖场管理',
requiresAuth: true,
icon: 'HomeOutlined'
}
}
]
// 认证相关路由
export const authRoutes = [
{
path: '/login',
name: 'Login',
component: () => import('../views/Login.vue'),
meta: {
title: '登录',
requiresAuth: false,
layout: 'blank'
}
}
]
// 错误页面路由
export const errorRoutes = [
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('../views/NotFound.vue'),
meta: {
title: '页面未找到',
requiresAuth: false
}
}
]
// 导出所有路由
export default [
...authRoutes,
...mainRoutes,
...errorRoutes
]

View File

@@ -0,0 +1,190 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useDataStore = defineStore('data', () => {
// 数据状态
const farms = ref([])
const animals = ref([])
const devices = ref([])
const alerts = ref([])
const stats = ref({
farmGrowth: 0,
animalGrowth: 0,
alertReduction: 0
})
// 加载状态
const loading = ref({
farms: false,
animals: false,
devices: false,
alerts: false,
stats: false
})
// 计算属性
const farmCount = computed(() => farms.value.length)
const animalCount = computed(() => animals.value.length)
const deviceCount = computed(() => devices.value.length)
const alertCount = computed(() => alerts.value.length)
// 在线设备数量
const onlineDeviceCount = computed(() =>
devices.value.filter(device => device.status === 'online').length
)
// 设备在线率
const deviceOnlineRate = computed(() => {
if (devices.value.length === 0) return 0
return (onlineDeviceCount.value / devices.value.length * 100).toFixed(1)
})
// 获取养殖场数据
async function fetchFarms() {
loading.value.farms = true
console.log('开始获取养殖场数据...')
try {
// 导入数据服务
const { farmService } = await import('../utils/dataService')
console.log('调用 farmService.getAllFarms()...')
const data = await farmService.getAllFarms()
console.log('养殖场API返回数据:', data)
// farmService.getAllFarms()通过api.get返回的是result.data直接使用
farms.value = data || []
console.log('设置farms.value:', farms.value.length, '条记录')
} catch (error) {
console.error('获取养殖场数据失败:', error)
farms.value = []
} finally {
loading.value.farms = false
}
}
// 获取动物数据
async function fetchAnimals() {
loading.value.animals = true
try {
// 导入数据服务
const { animalService } = await import('../utils/dataService')
const data = await animalService.getAllAnimals()
animals.value = data || []
} catch (error) {
console.error('获取动物数据失败:', error)
animals.value = []
} finally {
loading.value.animals = false
}
}
// 获取设备数据
async function fetchDevices() {
loading.value.devices = true
try {
// 导入数据服务
const { deviceService } = await import('../utils/dataService')
const data = await deviceService.getAllDevices()
devices.value = data || []
} catch (error) {
console.error('获取设备数据失败:', error)
devices.value = []
} finally {
loading.value.devices = false
}
}
// 获取预警数据
async function fetchAlerts() {
loading.value.alerts = true
try {
// 导入数据服务
const { alertService } = await import('../utils/dataService')
const data = await alertService.getAllAlerts()
// alertService.getAllAlerts()通过api.get返回的是result.data直接使用
alerts.value = data || []
} catch (error) {
console.error('获取预警数据失败:', error)
alerts.value = []
} finally {
loading.value.alerts = false
}
}
// 获取统计数据
async function fetchStats() {
loading.value.stats = true
try {
// 导入数据服务
const { statsService } = await import('../utils/dataService')
const data = await statsService.getDashboardStats()
stats.value = data || {
farmGrowth: 0,
animalGrowth: 0,
alertReduction: 0
}
} catch (error) {
console.error('获取统计数据失败:', error)
stats.value = {
farmGrowth: 0,
animalGrowth: 0,
alertReduction: 0
}
} finally {
loading.value.stats = false
}
}
// 加载所有数据
async function fetchAllData() {
console.log('开始并行加载所有数据...')
try {
await Promise.all([
fetchFarms(),
fetchAnimals(),
fetchDevices(),
fetchAlerts(),
fetchStats()
])
console.log('所有数据加载完成:', {
farms: farms.value.length,
animals: animals.value.length,
devices: devices.value.length,
alerts: alerts.value.length
})
} catch (error) {
console.error('数据加载过程中出现错误:', error)
throw error
}
}
return {
// 状态
farms,
animals,
devices,
alerts,
stats,
loading,
// 计算属性
farmCount,
animalCount,
deviceCount,
alertCount,
onlineDeviceCount,
deviceOnlineRate,
// 方法
fetchFarms,
fetchAnimals,
fetchDevices,
fetchAlerts,
fetchStats,
fetchAllData
}
})

View File

@@ -0,0 +1,4 @@
// 导出所有状态管理存储
export { useUserStore } from './user'
export { useSettingsStore } from './settings'
export { useDataStore } from './data'

View File

@@ -0,0 +1,43 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const useSettingsStore = defineStore('settings', () => {
// 应用设置状态
const theme = ref(localStorage.getItem('theme') || 'light')
const sidebarCollapsed = ref(localStorage.getItem('sidebarCollapsed') === 'true')
const locale = ref(localStorage.getItem('locale') || 'zh-CN')
// 切换主题
function toggleTheme() {
theme.value = theme.value === 'light' ? 'dark' : 'light'
localStorage.setItem('theme', theme.value)
}
// 设置主题
function setTheme(newTheme) {
theme.value = newTheme
localStorage.setItem('theme', newTheme)
}
// 切换侧边栏折叠状态
function toggleSidebar() {
sidebarCollapsed.value = !sidebarCollapsed.value
localStorage.setItem('sidebarCollapsed', sidebarCollapsed.value)
}
// 设置语言
function setLocale(newLocale) {
locale.value = newLocale
localStorage.setItem('locale', newLocale)
}
return {
theme,
sidebarCollapsed,
locale,
toggleTheme,
setTheme,
toggleSidebar,
setLocale
}
})

View File

@@ -0,0 +1,108 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref(localStorage.getItem('token') || '')
const userData = ref(JSON.parse(localStorage.getItem('user') || 'null'))
const isLoggedIn = computed(() => !!token.value)
// 检查登录状态
function checkLoginStatus() {
const savedToken = localStorage.getItem('token')
const savedUser = localStorage.getItem('user')
if (savedToken && savedUser) {
try {
token.value = savedToken
userData.value = JSON.parse(savedUser)
return true
} catch (error) {
console.error('解析用户数据失败', error)
logout()
return false
}
}
return false
}
// 检查token是否有效
async function validateToken() {
if (!token.value) {
return false
}
try {
const { api } = await import('../utils/api')
// 尝试调用一个需要认证的API来验证token
await api.get('/auth/validate')
return true
} catch (error) {
if (error.message && error.message.includes('认证已过期')) {
logout()
return false
}
// 其他错误可能是网络问题不清除token
return true
}
}
// 登录操作
async function login(username, password, retryCount = 0) {
try {
const { api } = await import('../utils/api');
// 使用专门的login方法它返回完整的响应对象
const result = await api.login(username, password);
// 登录成功后设置token和用户数据
if (result.success && result.token) {
token.value = result.token;
userData.value = {
username: username,
email: result.user?.email || `${username}@example.com`
};
// 保存到本地存储
localStorage.setItem('token', result.token);
localStorage.setItem('user', JSON.stringify(userData.value));
}
return result;
} catch (error) {
console.error('登录错误:', error);
// 重试逻辑仅对500错误且重试次数<2
if (error.message.includes('500') && retryCount < 2) {
return login(username, password, retryCount + 1);
}
// 直接抛出错误,由调用方处理
throw error;
}
}
// 登出操作
function logout() {
token.value = ''
userData.value = null
// 清除本地存储
localStorage.removeItem('token')
localStorage.removeItem('user')
}
// 更新用户信息
function updateUserInfo(newUserInfo) {
userData.value = { ...userData.value, ...newUserInfo }
localStorage.setItem('user', JSON.stringify(userData.value))
}
return {
token,
userData,
isLoggedIn,
checkLoginStatus,
validateToken,
login,
logout,
updateUserInfo
}
})

View File

@@ -0,0 +1,105 @@
/* 全局样式 */
/* 基础样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
font-size: 14px;
line-height: 1.5;
color: rgba(0, 0, 0, 0.85);
background-color: #f0f2f5;
}
/* 链接样式 */
a {
color: #1890ff;
text-decoration: none;
}
a:hover {
color: #40a9ff;
}
/* 常用辅助类 */
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.text-left {
text-align: left;
}
.flex {
display: flex;
}
.flex-column {
flex-direction: column;
}
.justify-between {
justify-content: space-between;
}
.justify-center {
justify-content: center;
}
.items-center {
align-items: center;
}
.mt-1 {
margin-top: 8px;
}
.mt-2 {
margin-top: 16px;
}
.mt-3 {
margin-top: 24px;
}
.mb-1 {
margin-bottom: 8px;
}
.mb-2 {
margin-bottom: 16px;
}
.mb-3 {
margin-bottom: 24px;
}
/* 自定义滚动条 */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}

View File

@@ -0,0 +1,29 @@
// Ant Design Vue 主题配置
export const themeConfig = {
token: {
colorPrimary: '#1890ff',
colorSuccess: '#52c41a',
colorWarning: '#faad14',
colorError: '#f5222d',
colorInfo: '#1890ff',
borderRadius: 4,
wireframe: false,
},
components: {
Button: {
colorPrimary: '#1890ff',
algorithm: true,
},
Input: {
colorPrimary: '#1890ff',
},
Card: {
colorBgContainer: '#ffffff',
},
Layout: {
colorBgHeader: '#001529',
colorBgBody: '#f0f2f5',
colorBgSider: '#001529',
},
},
};

View File

@@ -0,0 +1,27 @@
// 直接测试API调用
import { api } from './utils/api.js'
console.log('=== 开始直接API测试 ===')
try {
console.log('正在调用 /devices/public API...')
const result = await api.get('/devices/public')
console.log('API调用成功')
console.log('返回数据类型:', typeof result)
console.log('是否为数组:', Array.isArray(result))
console.log('数据长度:', result?.length || 0)
console.log('前3个设备:', result?.slice(0, 3))
if (result && result.length > 0) {
const statusCount = {}
result.forEach(device => {
statusCount[device.status] = (statusCount[device.status] || 0) + 1
})
console.log('设备状态分布:', statusCount)
}
} catch (error) {
console.error('API调用失败:', error.message)
console.error('错误详情:', error)
}
console.log('=== API测试完成 ===')

View File

@@ -0,0 +1,41 @@
// 测试数据存储
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import { useDataStore } from './stores/data.js'
// 创建应用和Pinia实例
const app = createApp({})
const pinia = createPinia()
app.use(pinia)
// 测试数据存储
async function testDataStore() {
console.log('=== 开始测试数据存储 ===')
const dataStore = useDataStore()
console.log('初始设备数量:', dataStore.devices.length)
try {
console.log('开始获取设备数据...')
await dataStore.fetchDevices()
console.log('获取完成,设备数量:', dataStore.devices.length)
console.log('前3个设备:', dataStore.devices.slice(0, 3))
// 统计状态
const statusCount = {}
dataStore.devices.forEach(device => {
statusCount[device.status] = (statusCount[device.status] || 0) + 1
})
console.log('状态分布:', statusCount)
} catch (error) {
console.error('测试失败:', error)
}
console.log('=== 测试完成 ===')
}
// 运行测试
testDataStore()

View File

@@ -0,0 +1,271 @@
/**
* API请求工具
* 封装了基本的API请求方法包括处理认证Token
*/
// API基础URL
const API_BASE_URL = 'http://localhost:5350/api';
/**
* 创建请求头自动添加认证Token
* @param {Object} headers - 额外的请求头
* @returns {Object} 合并后的请求头
*/
const createHeaders = (headers = {}) => {
const token = localStorage.getItem('token');
const defaultHeaders = {
'Content-Type': 'application/json',
};
if (token) {
defaultHeaders['Authorization'] = `Bearer ${token}`;
}
return { ...defaultHeaders, ...headers };
};
/**
* 处理API响应
* @param {Response} response - Fetch API响应对象
* @returns {Promise} 处理后的响应数据
*/
const handleResponse = async (response) => {
// 检查HTTP状态
if (!response.ok) {
// 处理常见错误
if (response.status === 401) {
// 清除无效的认证信息
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 === 404) {
throw new Error('请求的资源不存在');
}
if (response.status === 500) {
try {
const errorData = await response.json();
console.error('API 500错误详情:', errorData);
// 优先使用后端返回的 message否则使用默认提示
throw new Error(errorData.message || '服务器繁忙,请稍后重试');
} catch (e) {
console.error('API处理错误:', e.stack);
// 区分网络错误和服务端错误
if (e.message.includes('Failed to fetch')) {
throw new Error('网络连接失败,请检查网络');
} else {
throw new Error('服务器内部错误,请联系管理员');
}
}
}
// 尝试获取详细错误信息
try {
const errorData = await response.json();
const errorMessage = errorData.message || `请求失败: ${response.status} ${response.statusText}`;
console.error(`API请求失败 [${response.status}]:`, {
url: response.url,
method: response.method,
error: errorData,
headers: response.headers
});
throw new Error(errorMessage);
} catch (e) {
console.error(`API请求解析失败:`, {
url: response.url,
method: response.method,
status: response.status,
statusText: response.statusText,
headers: response.headers
});
throw new Error(`请求失败: ${response.status} ${response.statusText}`);
}
}
// 返回JSON数据
const result = await response.json();
// 兼容数组响应
if (Array.isArray(result)) {
return result;
}
if (!result.success) {
console.error(`API业务逻辑失败:`, {
url: response.url,
method: response.method,
response: result
});
throw new Error(result.message || 'API请求失败');
}
return result.data;
};
/**
* 更新农场信息
* @param {string} id - 农场ID
* @param {Object} data - 更新的农场数据
* @returns {Promise} 更新后的农场数据
*/
const updateFarm = async (id, data) => {
const response = await fetch(`${API_BASE_URL}/farms/${id}`, {
method: 'PUT',
headers: createHeaders(),
body: JSON.stringify(data),
});
return handleResponse(response);
};
/**
* API请求方法
*/
export const api = {
/**
* 登录
* @param {string} username - 用户名
* @param {string} password - 密码
* @returns {Promise} 登录结果
*/
async login(username, password) {
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username, password }),
});
// 检查HTTP状态
if (!response.ok) {
if (response.status === 401) {
throw new Error('用户名或密码错误');
}
if (response.status === 500) {
throw new Error('服务器错误,请稍后重试');
}
throw new Error(`登录失败: ${response.status} ${response.statusText}`);
}
const result = await response.json();
// 检查业务逻辑是否成功
if (!result.success) {
throw new Error(result.message || '登录失败');
}
// 保存token到localStorage
if (result.token) {
localStorage.setItem('token', result.token);
}
// 返回完整的结果对象,保持与后端响应格式一致
return result;
},
/**
* GET请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async get(endpoint, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
...options,
method: 'GET',
headers: createHeaders(options.headers),
});
return handleResponse(response);
},
/**
* POST请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async post(endpoint, data, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
method: 'POST',
headers: createHeaders(options.headers),
body: JSON.stringify(data),
...options,
});
return handleResponse(response);
},
/**
* PUT请求
* @param {string} endpoint - API端点
* @param {Object} data - 请求数据
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async put(endpoint, data, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
method: 'PUT',
headers: createHeaders(options.headers),
body: JSON.stringify(data),
...options,
});
return handleResponse(response);
},
/**
* DELETE请求
* @param {string} endpoint - API端点
* @param {Object} options - 请求选项
* @returns {Promise} 响应数据
*/
async delete(endpoint, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const response = await fetch(url, {
method: 'DELETE',
headers: createHeaders(options.headers),
...options,
});
return handleResponse(response);
},
};
/**
* 使用示例:
* // 登录
* api.login('admin', 'password')
* .then(response => {
* console.log('登录成功', response);
* // 之后的其他API调用会自动携带token
* })
* .catch(error => {
* console.error('登录失败', error);
* });
*/

View File

@@ -0,0 +1,501 @@
/**
* 图表服务工具
* 封装ECharts图表的初始化和配置功能
* 优化版本:支持按需加载、懒加载和性能优化
*/
// 导入ECharts核心模块
import * as echarts from 'echarts/core';
// 按需导入图表组件
import {
LineChart,
BarChart,
PieChart
} from 'echarts/charts';
// 按需导入组件
import {
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
ToolboxComponent
} from 'echarts/components';
// 导入Canvas渲染器性能更好
import { CanvasRenderer } from 'echarts/renderers';
// 图表实例缓存
const chartInstanceCache = new Map();
// 数据缓存
const dataCache = new Map();
// 注册必要的组件
echarts.use([
LineChart,
BarChart,
PieChart,
TitleComponent,
TooltipComponent,
GridComponent,
DatasetComponent,
TransformComponent,
LegendComponent,
ToolboxComponent,
CanvasRenderer
]);
// 性能优化配置
const PERFORMANCE_CONFIG = {
// 启用硬件加速
devicePixelRatio: window.devicePixelRatio || 1,
// 渲染器配置
renderer: 'canvas',
// 动画配置
animation: {
duration: 300,
easing: 'cubicOut'
},
// 大数据优化
largeThreshold: 2000,
progressive: 400,
progressiveThreshold: 3000
};
/**
* 创建图表实例(优化版本)
* @param {HTMLElement} container - 图表容器元素
* @param {Object} options - 图表配置选项
* @param {string} cacheKey - 缓存键(可选)
* @returns {echarts.ECharts} 图表实例
*/
export function createChart(container, options = {}, cacheKey = null) {
if (!container) {
console.error('图表容器不存在');
return null;
}
// 检查缓存
if (cacheKey && chartInstanceCache.has(cacheKey)) {
const cachedChart = chartInstanceCache.get(cacheKey);
if (cachedChart && !cachedChart.isDisposed()) {
// 更新配置
cachedChart.setOption(options, true);
return cachedChart;
} else {
// 清理无效缓存
chartInstanceCache.delete(cacheKey);
}
}
// 合并性能优化配置
const initOptions = {
devicePixelRatio: PERFORMANCE_CONFIG.devicePixelRatio,
renderer: PERFORMANCE_CONFIG.renderer,
width: 'auto',
height: 'auto'
};
// 创建图表实例
const chart = echarts.init(container, null, initOptions);
// 应用性能优化配置到选项
const optimizedOptions = {
...options,
animation: {
...PERFORMANCE_CONFIG.animation,
...options.animation
},
// 大数据优化
progressive: PERFORMANCE_CONFIG.progressive,
progressiveThreshold: PERFORMANCE_CONFIG.progressiveThreshold
};
// 设置图表选项
if (optimizedOptions) {
chart.setOption(optimizedOptions, true);
}
// 缓存图表实例
if (cacheKey) {
chartInstanceCache.set(cacheKey, chart);
}
return chart;
}
/**
* 创建趋势图表
* @param {HTMLElement} container - 图表容器元素
* @param {Array} data - 图表数据
* @param {Object} options - 额外配置选项
* @returns {echarts.ECharts} 图表实例
*/
export function createTrendChart(container, data, options = {}) {
const { xAxis = [], series = [] } = data;
const defaultOptions = {
title: {
text: options.title || '趋势图表',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: series.map(item => item.name),
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: xAxis
},
yAxis: {
type: 'value'
},
series: series.map(item => ({
name: item.name,
type: item.type || 'line',
data: item.data,
smooth: true,
itemStyle: item.itemStyle,
lineStyle: item.lineStyle,
areaStyle: item.areaStyle
}))
};
// 合并选项
const chartOptions = {
...defaultOptions,
...options,
title: { ...defaultOptions.title, ...options.title },
tooltip: { ...defaultOptions.tooltip, ...options.tooltip },
legend: { ...defaultOptions.legend, ...options.legend },
grid: { ...defaultOptions.grid, ...options.grid }
};
return createChart(container, chartOptions);
}
/**
* 创建饼图
* @param {HTMLElement} container - 图表容器元素
* @param {Array} data - 图表数据
* @param {Object} options - 额外配置选项
* @returns {echarts.ECharts} 图表实例
*/
export function createPieChart(container, data, options = {}) {
const defaultOptions = {
title: {
text: options.title || '饼图',
left: 'center'
},
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: options.seriesName || '数据',
type: 'pie',
radius: options.radius || ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: data
}
]
};
// 合并选项
const chartOptions = {
...defaultOptions,
...options,
title: { ...defaultOptions.title, ...options.title },
tooltip: { ...defaultOptions.tooltip, ...options.tooltip },
legend: { ...defaultOptions.legend, ...options.legend }
};
return createChart(container, chartOptions);
}
/**
* 创建柱状图
* @param {HTMLElement} container - 图表容器元素
* @param {Object} data - 图表数据
* @param {Object} options - 额外配置选项
* @returns {echarts.ECharts} 图表实例
*/
export function createBarChart(container, data, options = {}) {
const { xAxis = [], series = [] } = data;
const defaultOptions = {
title: {
text: options.title || '柱状图',
left: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: series.map(item => item.name),
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: xAxis
},
yAxis: {
type: 'value'
},
series: series.map(item => ({
name: item.name,
type: 'bar',
data: item.data,
itemStyle: item.itemStyle
}))
};
// 合并选项
const chartOptions = {
...defaultOptions,
...options,
title: { ...defaultOptions.title, ...options.title },
tooltip: { ...defaultOptions.tooltip, ...options.tooltip },
legend: { ...defaultOptions.legend, ...options.legend },
grid: { ...defaultOptions.grid, ...options.grid }
};
return createChart(container, chartOptions);
}
/**
* 处理窗口大小变化,调整图表大小
* @param {echarts.ECharts} chart - 图表实例
*/
export function handleResize(chart) {
if (chart) {
chart.resize();
}
}
/**
* 销毁图表实例
* @param {echarts.ECharts} chart - 图表实例
* @param {string} cacheKey - 缓存键(可选)
*/
export function disposeChart(chart, cacheKey = null) {
if (chart) {
chart.dispose();
// 清理缓存
if (cacheKey) {
chartInstanceCache.delete(cacheKey);
dataCache.delete(cacheKey);
}
}
}
/**
* 数据缓存管理
*/
export const DataCache = {
/**
* 设置缓存数据
* @param {string} key - 缓存键
* @param {any} data - 数据
* @param {number} ttl - 过期时间毫秒默认5分钟
*/
set(key, data, ttl = 5 * 60 * 1000) {
const expireTime = Date.now() + ttl;
dataCache.set(key, { data, expireTime });
},
/**
* 获取缓存数据
* @param {string} key - 缓存键
* @returns {any|null} 缓存的数据或null
*/
get(key) {
const cached = dataCache.get(key);
if (!cached) return null;
// 检查是否过期
if (Date.now() > cached.expireTime) {
dataCache.delete(key);
return null;
}
return cached.data;
},
/**
* 删除缓存数据
* @param {string} key - 缓存键
*/
delete(key) {
dataCache.delete(key);
},
/**
* 清空所有缓存
*/
clear() {
dataCache.clear();
},
/**
* 检查缓存是否存在且未过期
* @param {string} key - 缓存键
* @returns {boolean}
*/
has(key) {
return this.get(key) !== null;
}
};
/**
* 懒加载图表组件
* @param {HTMLElement} container - 图表容器
* @param {Function} dataLoader - 数据加载函数
* @param {Object} options - 图表配置
* @param {string} cacheKey - 缓存键
* @returns {Promise<echarts.ECharts>}
*/
export async function createLazyChart(container, dataLoader, options = {}, cacheKey = null) {
// 检查数据缓存
let data = cacheKey ? DataCache.get(cacheKey) : null;
if (!data) {
// 显示加载状态
const loadingChart = createChart(container, {
title: {
text: '加载中...',
left: 'center',
top: 'center',
textStyle: {
fontSize: 14,
color: '#999'
}
}
});
try {
// 异步加载数据
data = await dataLoader();
// 缓存数据
if (cacheKey) {
DataCache.set(cacheKey, data);
}
// 销毁加载图表
loadingChart.dispose();
} catch (error) {
console.error('图表数据加载失败:', error);
// 显示错误状态
loadingChart.setOption({
title: {
text: '加载失败',
left: 'center',
top: 'center',
textStyle: {
fontSize: 14,
color: '#ff4d4f'
}
}
});
return loadingChart;
}
}
// 创建实际图表
const chartOptions = {
...options,
...data
};
return createChart(container, chartOptions, cacheKey);
}
/**
* 清理所有缓存
*/
export function clearAllCache() {
// 销毁所有缓存的图表实例
chartInstanceCache.forEach(chart => {
if (chart && !chart.isDisposed()) {
chart.dispose();
}
});
// 清空缓存
chartInstanceCache.clear();
dataCache.clear();
}
/**
* 获取缓存统计信息
*/
export function getCacheStats() {
return {
chartInstances: chartInstanceCache.size,
dataCache: dataCache.size,
memoryUsage: {
charts: chartInstanceCache.size * 0.1, // 估算MB
data: dataCache.size * 0.05 // 估算MB
}
};
}

View File

@@ -0,0 +1,262 @@
/**
* 数据服务工具
* 封装了与后端数据相关的API请求
*/
import { api } from './api';
/**
* 养殖场数据服务
*/
export const farmService = {
/**
* 获取所有养殖场
* @returns {Promise<Array>} 养殖场列表
*/
async getAllFarms() {
const farms = await api.get('/farms/public');
// 标准化location字段格式
return farms.map(farm => {
if (farm.location) {
// 如果location是字符串尝试解析为JSON
if (typeof farm.location === 'string') {
try {
farm.location = JSON.parse(farm.location);
} catch (error) {
console.warn(`解析养殖场 ${farm.id} 的location字段失败:`, error);
farm.location = null;
}
}
// 如果location是对象但缺少lat或lng设为null
if (farm.location && (typeof farm.location.lat !== 'number' || typeof farm.location.lng !== 'number')) {
console.warn(`养殖场 ${farm.id} 的location字段格式不正确:`, farm.location);
farm.location = null;
}
}
return farm;
});
},
/**
* 获取养殖场详情
* @param {string} id - 养殖场ID
* @returns {Promise<Object>} 养殖场详情
*/
async getFarmById(id) {
return api.get(`/farms/${id}`);
},
/**
* 创建养殖场
* @param {Object} farmData - 养殖场数据
* @returns {Promise<Object>} 创建的养殖场
*/
async createFarm(farmData) {
return api.post('/farms', farmData);
},
/**
* 更新养殖场
* @param {string} id - 养殖场ID
* @param {Object} farmData - 养殖场数据
* @returns {Promise<Object>} 更新后的养殖场
*/
async updateFarm(id, farmData) {
return api.put(`/farms/${id}`, farmData);
},
/**
* 删除养殖场
* @param {string} id - 养殖场ID
* @returns {Promise<Object>} 删除结果
*/
async deleteFarm(id) {
return api.delete(`/farms/${id}`);
}
};
/**
* 动物数据服务
*/
export const animalService = {
/**
* 获取所有动物
* @returns {Promise<Array>} 动物列表
*/
async getAllAnimals() {
return api.get('/animals/public');
},
/**
* 获取养殖场的所有动物
* @param {string} farmId - 养殖场ID
* @returns {Promise<Array>} 动物列表
*/
async getAnimalsByFarm(farmId) {
return api.get(`/farms/${farmId}/animals`);
},
/**
* 获取动物详情
* @param {string} id - 动物ID
* @returns {Promise<Object>} 动物详情
*/
async getAnimalById(id) {
return api.get(`/animals/${id}`);
},
/**
* 创建动物
* @param {Object} animalData - 动物数据
* @returns {Promise<Object>} 创建的动物
*/
async createAnimal(animalData) {
return api.post('/animals', animalData);
},
/**
* 更新动物
* @param {string} id - 动物ID
* @param {Object} animalData - 动物数据
* @returns {Promise<Object>} 更新后的动物
*/
async updateAnimal(id, animalData) {
return api.put(`/animals/${id}`, animalData);
},
/**
* 删除动物
* @param {string} id - 动物ID
* @returns {Promise<Object>} 删除结果
*/
async deleteAnimal(id) {
return api.delete(`/animals/${id}`);
}
};
/**
* 设备数据服务
*/
export const deviceService = {
/**
* 获取所有设备
* @returns {Promise<Array>} 设备列表
*/
async getAllDevices() {
return api.get('/devices/public');
},
/**
* 获取养殖场的所有设备
* @param {string} farmId - 养殖场ID
* @returns {Promise<Array>} 设备列表
*/
async getDevicesByFarm(farmId) {
return api.get(`/farms/${farmId}/devices`);
},
/**
* 获取设备详情
* @param {string} id - 设备ID
* @returns {Promise<Object>} 设备详情
*/
async getDeviceById(id) {
return api.get(`/devices/${id}`);
},
/**
* 获取设备状态
* @param {string} id - 设备ID
* @returns {Promise<Object>} 设备状态
*/
async getDeviceStatus(id) {
return api.get(`/devices/${id}/status`);
}
};
/**
* 预警数据服务
*/
export const alertService = {
/**
* 获取所有预警
* @returns {Promise<Array>} 预警列表
*/
async getAllAlerts() {
return api.get('/alerts/public');
},
/**
* 获取养殖场的所有预警
* @param {string} farmId - 养殖场ID
* @returns {Promise<Array>} 预警列表
*/
async getAlertsByFarm(farmId) {
return api.get(`/farms/${farmId}/alerts`);
},
/**
* 获取预警详情
* @param {string} id - 预警ID
* @returns {Promise<Object>} 预警详情
*/
async getAlertById(id) {
return api.get(`/alerts/${id}`);
},
/**
* 处理预警
* @param {string} id - 预警ID
* @param {Object} data - 处理数据
* @returns {Promise<Object>} 处理结果
*/
async handleAlert(id, data) {
return api.put(`/alerts/${id}/handle`, data);
}
};
/**
* 统计数据服务
*/
export const statsService = {
/**
* 获取系统概览统计数据
* @returns {Promise<Object>} 统计数据
*/
async getDashboardStats() {
return api.get('/stats/public/dashboard');
},
/**
* 获取养殖场统计数据
* @returns {Promise<Object>} 统计数据
*/
async getFarmStats() {
return api.get('/stats/farms');
},
/**
* 获取动物统计数据
* @returns {Promise<Object>} 统计数据
*/
async getAnimalStats() {
return api.get('/stats/animals');
},
/**
* 获取设备统计数据
* @returns {Promise<Object>} 统计数据
*/
async getDeviceStats() {
return api.get('/stats/devices');
},
/**
* 获取预警统计数据
* @returns {Promise<Object>} 统计数据
*/
async getAlertStats() {
return api.get('/stats/alerts');
}
};

View File

@@ -0,0 +1,353 @@
/**
* 百度地图服务工具
* 封装了百度地图API的初始化和操作方法
*/
// 百度地图API加载状态
let BMapLoaded = false;
let loadingPromise = null;
/**
* 加载百度地图API
* @returns {Promise} 加载完成的Promise
*/
export const loadBMapScript = async () => {
// 如果已经加载过,直接返回
if (BMapLoaded) {
console.log('百度地图API已加载');
return Promise.resolve();
}
// 如果正在加载中返回加载Promise
if (loadingPromise) {
console.log('百度地图API正在加载中...');
return loadingPromise;
}
console.log('开始加载百度地图API...');
// 创建加载Promise
loadingPromise = new Promise(async (resolve, reject) => {
try {
// 导入环境配置
const { BAIDU_MAP_CONFIG } = await import('../config/env');
console.log('使用API密钥:', BAIDU_MAP_CONFIG.apiKey);
console.log('完整配置:', BAIDU_MAP_CONFIG);
// 检查API密钥是否有效
if (!BAIDU_MAP_CONFIG.apiKey || BAIDU_MAP_CONFIG.apiKey === 'YOUR_VALID_BAIDU_MAP_API_KEY') {
const error = new Error('百度地图API密钥未配置或无效');
console.error('API密钥错误:', error);
reject(error);
return;
}
// 检查是否已经存在BMap
if (typeof window.BMap !== 'undefined') {
console.log('BMap已存在直接使用');
BMapLoaded = true;
resolve();
return;
}
// 创建全局回调函数
window.initBMap = () => {
console.log('百度地图API加载成功BMap对象类型:', typeof window.BMap);
console.log('BMap对象详情:', window.BMap);
console.log('BMap.Map是否存在:', typeof window.BMap?.Map);
BMapLoaded = true;
resolve();
// 清理全局回调
delete window.initBMap;
};
// 创建script标签
const script = document.createElement('script');
script.type = 'text/javascript';
// 使用配置文件中的API密钥
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${BAIDU_MAP_CONFIG.apiKey}&callback=initBMap`;
console.log('百度地图API URL:', script.src);
script.onerror = (error) => {
console.error('百度地图脚本加载失败:', error);
reject(new Error('百度地图脚本加载失败'));
};
// 设置超时
setTimeout(() => {
if (!BMapLoaded) {
reject(new Error('百度地图API加载超时'));
}
}, 10000);
// 添加到文档中
document.head.appendChild(script);
} catch (error) {
console.error('加载百度地图API时出错:', error);
reject(error);
}
});
return loadingPromise;
};
/**
* 创建百度地图实例
* @param {HTMLElement} container - 地图容器元素
* @param {Object} options - 地图配置选项
* @returns {Promise<BMap.Map>} 地图实例
*/
export const createMap = async (container, options = {}) => {
try {
console.log('开始创建地图,容器:', container);
console.log('容器尺寸:', container.offsetWidth, 'x', container.offsetHeight);
// 确保百度地图API已加载
await loadBMapScript();
// 导入环境配置
const { BAIDU_MAP_CONFIG } = await import('../config/env');
console.log('百度地图配置:', BAIDU_MAP_CONFIG);
// 检查BMap是否可用
if (typeof window.BMap === 'undefined') {
const error = new Error('百度地图API未正确加载');
console.error('BMap未定义:', error);
throw error;
}
console.log('BMap对象可用版本:', window.BMap.version || '未知');
// 默认配置
const defaultOptions = {
center: new BMap.Point(BAIDU_MAP_CONFIG.defaultCenter.lng, BAIDU_MAP_CONFIG.defaultCenter.lat), // 宁夏中心点
zoom: BAIDU_MAP_CONFIG.defaultZoom, // 默认缩放级别
enableMapClick: true, // 启用地图点击
enableScrollWheelZoom: true, // 启用滚轮缩放
enableDragging: true, // 启用拖拽
enableDoubleClickZoom: true, // 启用双击缩放
enableKeyboard: true // 启用键盘控制
};
// 合并配置
const mergedOptions = { ...defaultOptions, ...options };
console.log('地图配置选项:', mergedOptions);
// 创建地图实例
console.log('创建BMap.Map实例...');
const map = new window.BMap.Map(container);
console.log('地图实例创建成功:', map);
// 监听地图加载完成事件
map.addEventListener('tilesloaded', function() {
console.log('百度地图瓦片加载完成');
});
map.addEventListener('load', function() {
console.log('百度地图完全加载完成');
});
// 设置中心点和缩放级别
console.log('设置地图中心点和缩放级别:', mergedOptions.center, mergedOptions.zoom);
map.centerAndZoom(mergedOptions.center, mergedOptions.zoom);
// 延迟确保地图完全渲染
setTimeout(() => {
console.log('地图渲染完成');
// 移除map.reset()调用,避免缩放时重置地图
}, 100);
// 添加地图类型控件
console.log('添加地图控件...');
map.addControl(new window.BMap.MapTypeControl());
// 添加增强的缩放控件
const navigationControl = new window.BMap.NavigationControl({
anchor: window.BMAP_ANCHOR_TOP_LEFT,
type: window.BMAP_NAVIGATION_CONTROL_LARGE,
enableGeolocation: false
});
map.addControl(navigationControl);
// 添加缩放控件(滑块式)
const scaleControl = new window.BMap.ScaleControl({
anchor: window.BMAP_ANCHOR_BOTTOM_LEFT
});
map.addControl(scaleControl);
// 添加概览地图控件
const overviewMapControl = new window.BMap.OverviewMapControl({
anchor: window.BMAP_ANCHOR_TOP_RIGHT,
isOpen: false
});
map.addControl(overviewMapControl);
// 设置缩放范围
map.setMinZoom(3);
map.setMaxZoom(19);
// 配置地图功能
if (mergedOptions.enableScrollWheelZoom) {
map.enableScrollWheelZoom();
}
if (mergedOptions.enableDragging) {
map.enableDragging();
} else {
map.disableDragging();
}
if (mergedOptions.enableDoubleClickZoom) {
map.enableDoubleClickZoom();
} else {
map.disableDoubleClickZoom();
}
if (mergedOptions.enableKeyboard) {
map.enableKeyboard();
} else {
map.disableKeyboard();
}
console.log('百度地图创建成功:', map);
return map;
} catch (error) {
console.error('创建百度地图失败:', error);
throw error;
}
};
/**
* 在地图上添加标记
* @param {BMap.Map} map - 百度地图实例
* @param {Array} markers - 标记点数据数组
* @param {Function} onClick - 点击标记的回调函数
* @returns {Array<BMap.Marker>} 标记点实例数组
*/
export const addMarkers = (map, markers = [], onClick = null) => {
// 验证地图实例是否有效
if (!map || typeof map.addOverlay !== 'function') {
console.error('addMarkers: 无效的地图实例', map);
return [];
}
return markers.map(markerData => {
// 创建标记点
const point = new BMap.Point(markerData.location.lng, markerData.location.lat);
const marker = new BMap.Marker(point);
// 添加标记到地图
map.addOverlay(marker);
// 创建信息窗口
if (markerData.title || markerData.content) {
const infoWindow = new BMap.InfoWindow(
`<div class="map-info-window">
${markerData.content || ''}
</div>`,
{
title: markerData.title || '',
width: 250,
height: 120,
enableMessage: false
}
);
// 添加点击事件
marker.addEventListener('click', () => {
marker.openInfoWindow(infoWindow);
// 如果有自定义点击回调,则调用
if (onClick && typeof onClick === 'function') {
onClick(markerData, marker);
}
});
}
return marker;
});
};
/**
* 清除地图上的所有覆盖物
* @param {BMap.Map} map - 百度地图实例
*/
export const clearOverlays = (map) => {
if (!map || typeof map.clearOverlays !== 'function') {
console.warn('clearOverlays: 无效的地图实例,已跳过清理');
return;
}
map.clearOverlays();
};
/**
* 设置地图视图以包含所有标记点
* @param {BMap.Map} map - 百度地图实例
* @param {Array<BMap.Point>} points - 点数组
* @param {Number} padding - 边距,单位像素
*/
export const setViewToFitMarkers = (map, points, padding = 50) => {
if (!map) {
console.warn('setViewToFitMarkers: 无效的地图实例,已跳过设置视图');
return;
}
if (!points || points.length === 0) return;
// 如果只有一个点,直接居中
if (points.length === 1) {
map.centerAndZoom(points[0], 15);
return;
}
// 创建视图范围
const viewport = map.getViewport(points, { margins: [padding, padding, padding, padding] });
map.setViewport(viewport);
};
/**
* 转换养殖场数据为地图标记数据
* @param {Array} farms - 养殖场数据数组
* @returns {Array} 地图标记数据数组
*/
export const convertFarmsToMarkers = (farms = []) => {
return farms
.filter(farm => {
// 只保留有有效位置信息的农场
return farm.location &&
typeof farm.location.lat === 'number' &&
typeof farm.location.lng === 'number';
})
.map(farm => {
// 计算动物总数
const animalCount = farm.animals ?
farm.animals.reduce((total, animal) => total + (animal.count || 0), 0) : 0;
// 计算在线设备数
const onlineDevices = farm.devices ?
farm.devices.filter(device => device.status === 'online').length : 0;
const totalDevices = farm.devices ? farm.devices.length : 0;
return {
id: farm.id,
title: farm.name,
location: farm.location,
content: `
<div style="padding: 8px; font-size: 14px;">
<h4 style="margin: 0 0 8px 0; color: #1890ff;">${farm.name}</h4>
<p style="margin: 4px 0;"><strong>类型:</strong> ${farm.type || '未知'}</p>
<p style="margin: 4px 0;"><strong>动物数量:</strong> ${animalCount} 只</p>
<p style="margin: 4px 0;"><strong>设备状态:</strong> ${onlineDevices}/${totalDevices} 在线</p>
<p style="margin: 4px 0;"><strong>联系人:</strong> ${farm.contact || '未知'}</p>
<p style="margin: 4px 0;"><strong>地址:</strong> ${farm.address || '未知'}</p>
<p style="margin: 4px 0; color: #666;"><strong>坐标:</strong> ${farm.location.lat.toFixed(4)}, ${farm.location.lng.toFixed(4)}</p>
</div>
`,
originalData: farm
};
});
};

View File

@@ -0,0 +1,576 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>预警管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加预警
</a-button>
</div>
<a-table
:columns="columns"
:data-source="alerts"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'level'">
<a-tag :color="getLevelColor(record.level)">
{{ getLevelText(record.level) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'farm_name'">
{{ getFarmName(record.farm_id) }}
</template>
<template v-else-if="column.dataIndex === 'device_name'">
{{ getDeviceName(record.device_id) }}
</template>
<template v-else-if="column.dataIndex === 'resolved_at'">
{{ formatDate(record.resolved_at) }}
</template>
<template v-else-if="column.dataIndex === 'created_at'">
{{ formatDateTime(record.created_at) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editAlert(record)">编辑</a-button>
<a-button
type="link"
@click="resolveAlert(record)"
v-if="record.status !== 'resolved'"
>
解决
</a-button>
<a-popconfirm
title="确定要删除这个预警吗?"
@confirm="deleteAlert(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑预警模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑预警' : '添加预警'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
width="600px"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="预警类型" name="type">
<a-select v-model="formData.type" placeholder="请选择预警类型">
<a-select-option value="temperature">温度异常</a-select-option>
<a-select-option value="humidity">湿度异常</a-select-option>
<a-select-option value="device_failure">设备故障</a-select-option>
<a-select-option value="animal_health">动物健康</a-select-option>
<a-select-option value="security">安全警报</a-select-option>
<a-select-option value="maintenance">维护提醒</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="预警级别" name="level">
<a-select v-model="formData.level" placeholder="请选择预警级别">
<a-select-option value="low"></a-select-option>
<a-select-option value="medium"></a-select-option>
<a-select-option value="high"></a-select-option>
<a-select-option value="critical">紧急</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="预警消息" name="message">
<a-textarea
v-model:value="formData.message"
placeholder="请输入预警消息"
:rows="3"
/>
</a-form-item>
<a-form-item label="预警状态" name="status">
<a-select v-model="formData.status" placeholder="请选择预警状态">
<a-select-option value="active">活跃</a-select-option>
<a-select-option value="acknowledged">已确认</a-select-option>
<a-select-option value="resolved">已解决</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属农场" name="farm_id">
<a-select
v-model="formData.farm_id"
placeholder="请选择所属农场"
:loading="farmsLoading"
>
<a-select-option
v-for="farm in farms"
:key="farm.id"
:value="farm.id"
>
{{ farm.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="关联设备" name="device_id">
<a-select
v-model="formData.device_id"
placeholder="请选择关联设备(可选)"
:loading="devicesLoading"
allow-clear
>
<a-select-option
v-for="device in devices"
:key="device.id"
:value="device.id"
>
{{ device.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="解决备注" name="resolution_notes" v-if="formData.status === 'resolved'">
<a-textarea
v-model:value="formData.resolution_notes"
placeholder="请输入解决备注"
:rows="2"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 解决预警模态框 -->
<a-modal
v-model:open="resolveModalVisible"
title="解决预警"
@ok="handleResolve"
@cancel="resolveModalVisible = false"
:confirm-loading="resolveLoading"
>
<a-form layout="vertical">
<a-form-item label="解决备注">
<a-textarea
v-model:value="resolveNotes"
placeholder="请输入解决备注"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
import dayjs from 'dayjs'
// 响应式数据
const alerts = ref([])
const farms = ref([])
const devices = ref([])
const loading = ref(false)
const farmsLoading = ref(false)
const devicesLoading = ref(false)
const modalVisible = ref(false)
const resolveModalVisible = ref(false)
const submitLoading = ref(false)
const resolveLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
const currentAlert = ref(null)
const resolveNotes = ref('')
// 表单数据
const formData = reactive({
id: null,
type: '',
level: 'medium',
message: '',
status: 'active',
farm_id: null,
device_id: null,
resolution_notes: ''
})
// 表单验证规则
const rules = {
type: [{ required: true, message: '请选择预警类型', trigger: 'change' }],
level: [{ required: true, message: '请选择预警级别', trigger: 'change' }],
message: [{ required: true, message: '请输入预警消息', trigger: 'blur' }],
status: [{ required: true, message: '请选择预警状态', trigger: 'change' }],
farm_id: [{ required: true, message: '请选择所属农场', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '预警类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '级别',
dataIndex: 'level',
key: 'level',
width: 80
},
{
title: '预警消息',
dataIndex: 'message',
key: 'message',
ellipsis: true,
width: 200
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '所属农场',
dataIndex: 'farm_name',
key: 'farm_name',
width: 120
},
{
title: '关联设备',
dataIndex: 'device_name',
key: 'device_name',
width: 120
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 150
},
{
title: '解决时间',
dataIndex: 'resolved_at',
key: 'resolved_at',
width: 120
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 180
}
]
// 获取预警列表
const fetchAlerts = async () => {
try {
loading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/alerts', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
alerts.value = response.data.data
} else {
message.error('获取预警列表失败')
}
} catch (error) {
console.error('获取预警列表失败:', error)
if (error.response && error.response.status === 401) {
message.error('登录已过期,请重新登录')
setTimeout(() => {
window.location.href = '/login'
}, 2000)
} else {
message.error('获取预警列表失败: ' + (error.message || '未知错误'))
}
} finally {
loading.value = false
}
}
// 获取农场列表
const fetchFarms = async () => {
try {
farmsLoading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/farms', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
farms.value = response.data.data
}
} catch (error) {
console.error('获取农场列表失败:', error)
} finally {
farmsLoading.value = false
}
}
// 获取设备列表
const fetchDevices = async () => {
try {
devicesLoading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/devices', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
devices.value = response.data.data
}
} catch (error) {
console.error('获取设备列表失败:', error)
} finally {
devicesLoading.value = false
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
fetchFarms()
fetchDevices()
}
// 编辑预警
const editAlert = (record) => {
isEdit.value = true
// 逐个字段赋值,避免破坏响应式绑定
formData.id = record.id
formData.type = record.type
formData.level = record.level
formData.message = record.message
formData.status = record.status
formData.farm_id = record.farm_id
formData.device_id = record.device_id
formData.resolution_notes = record.resolution_notes || ''
modalVisible.value = true
fetchFarms()
fetchDevices()
}
// 解决预警
const resolveAlert = (record) => {
currentAlert.value = record
resolveNotes.value = ''
resolveModalVisible.value = true
}
// 处理解决预警
const handleResolve = async () => {
try {
resolveLoading.value = true
const token = localStorage.getItem('token')
const response = await axios.put(`/api/alerts/${currentAlert.value.id}`, {
status: 'resolved',
resolved_at: new Date().toISOString(),
resolution_notes: resolveNotes.value
}, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
message.success('预警已解决')
resolveModalVisible.value = false
fetchAlerts()
} else {
message.error('解决预警失败')
}
} catch (error) {
console.error('解决预警失败:', error)
message.error('解决预警失败')
} finally {
resolveLoading.value = false
}
}
// 删除预警
const deleteAlert = async (id) => {
try {
const token = localStorage.getItem('token')
const response = await axios.delete(`/api/alerts/${id}`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
message.success('删除成功')
fetchAlerts()
} else {
message.error('删除失败')
}
} catch (error) {
console.error('删除预警失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const token = localStorage.getItem('token')
const config = {
headers: {
Authorization: `Bearer ${token}`
}
}
// 准备提交数据
const submitData = { ...formData }
// 如果是新增操作移除id字段
if (!isEdit.value) {
delete submitData.id
}
let response
if (isEdit.value) {
response = await axios.put(`/api/alerts/${formData.id}`, submitData, config)
} else {
response = await axios.post('/api/alerts', submitData, config)
}
if (response.data.success) {
message.success(isEdit.value ? '更新成功' : '创建成功')
modalVisible.value = false
fetchAlerts()
} else {
message.error(isEdit.value ? '更新失败' : '创建失败')
}
} catch (error) {
console.error('提交失败:', error)
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
type: '',
level: 'medium',
message: '',
status: 'active',
farm_id: null,
device_id: null,
resolution_notes: ''
})
formRef.value?.resetFields()
}
// 获取级别颜色
const getLevelColor = (level) => {
const colors = {
low: 'green',
medium: 'orange',
high: 'red',
critical: 'purple'
}
return colors[level] || 'default'
}
// 获取级别文本
const getLevelText = (level) => {
const texts = {
low: '低',
medium: '中',
high: '高',
critical: '紧急'
}
return texts[level] || level
}
// 获取状态颜色
const getStatusColor = (status) => {
const colors = {
active: 'red',
acknowledged: 'orange',
resolved: 'green'
}
return colors[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
active: '活跃',
acknowledged: '已确认',
resolved: '已解决'
}
return texts[status] || status
}
// 获取农场名称
const getFarmName = (farmId) => {
const farm = farms.value.find(f => f.id === farmId)
return farm ? farm.name : `农场ID: ${farmId}`
}
// 获取设备名称
const getDeviceName = (deviceId) => {
if (!deviceId) return '-'
const device = devices.value.find(d => d.id === deviceId)
return device ? device.name : `设备ID: ${deviceId}`
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD')
}
// 格式化日期时间
const formatDateTime = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD HH:mm')
}
// 组件挂载时获取数据
onMounted(() => {
fetchAlerts()
fetchFarms()
fetchDevices()
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -0,0 +1,512 @@
<template>
<div class="analytics-page">
<a-page-header
title="数据分析"
sub-title="宁夏智慧养殖监管平台数据分析"
>
<template #extra>
<a-space>
<a-button>导出报表</a-button>
<a-button type="primary" @click="refreshData">
<template #icon><reload-outlined /></template>
刷新数据
</a-button>
</a-space>
</template>
</a-page-header>
<div class="analytics-content">
<!-- 趋势图表 -->
<a-card title="月度数据趋势" :bordered="false" class="chart-card">
<e-chart :options="trendChartOptions" height="350px" @chart-ready="handleChartReady" />
</a-card>
<div class="analytics-row">
<!-- 养殖场类型分布 -->
<a-card title="养殖场类型分布" :bordered="false" class="chart-card">
<e-chart :options="farmTypeChartOptions" height="300px" />
</a-card>
<!-- 动物类型分布 -->
<a-card title="动物类型分布" :bordered="false" class="chart-card">
<animal-stats />
</a-card>
</div>
<div class="analytics-row">
<!-- 设备类型分布 -->
<a-card title="设备类型分布" :bordered="false" class="chart-card">
<device-stats />
<simple-device-test />
</a-card>
<!-- 预警类型分布 -->
<a-card title="预警类型分布" :bordered="false" class="chart-card">
<alert-stats />
</a-card>
</div>
<!-- 数据表格 -->
<a-card title="养殖场数据统计" :bordered="false" class="data-card">
<a-table :dataSource="farmTableData" :columns="farmColumns" :pagination="{ pageSize: 5 }">
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'animalCount'">
<a-progress :percent="getPercentage(record.animalCount, maxAnimalCount)" :stroke-color="getProgressColor(record.animalCount, maxAnimalCount)" />
</template>
<template v-if="column.key === 'deviceCount'">
<a-progress :percent="getPercentage(record.deviceCount, maxDeviceCount)" :stroke-color="getProgressColor(record.deviceCount, maxDeviceCount)" />
</template>
<template v-if="column.key === 'alertCount'">
<a-progress :percent="getPercentage(record.alertCount, maxAlertCount)" :stroke-color="{ from: '#108ee9', to: '#ff4d4f' }" />
</template>
</template>
</a-table>
</a-card>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import { useDataStore } from '../stores'
import EChart from '../components/EChart.vue'
import AnimalStats from '../components/AnimalStats.vue'
import DeviceStats from '../components/DeviceStats.vue'
import AlertStats from '../components/AlertStats.vue'
import SimpleDeviceTest from '../components/SimpleDeviceTest.vue'
// 移除模拟数据导入
// 使用数据存储
const dataStore = useDataStore()
// 图表实例
const charts = reactive({
trendChart: null
})
// 月度数据趋势
const trendData = ref({
xAxis: [],
series: []
})
// 处理图表就绪事件
function handleChartReady(chart, type) {
if (type === 'trend') {
charts.trendChart = chart
}
}
// 获取月度数据趋势
async function fetchMonthlyTrends() {
try {
const response = await fetch('/api/stats/public/monthly-trends')
const result = await response.json()
if (result.success) {
trendData.value = result.data
updateChartData()
} else {
console.error('获取月度数据趋势失败:', result.message)
}
} catch (error) {
console.error('获取月度数据趋势失败:', error)
}
}
// 刷新数据
async function refreshData() {
await dataStore.fetchAllData()
await fetchMonthlyTrends()
updateChartData()
}
// 更新图表数据
function updateChartData() {
// 更新趋势图表数据
if (charts.trendChart) {
charts.trendChart.setOption({
xAxis: { data: trendData.value.xAxis },
series: trendData.value.series
})
}
}
// 趋势图表选项
const trendChartOptions = computed(() => {
return {
title: {
text: '月度数据趋势',
left: 'center'
},
tooltip: {
trigger: 'axis'
},
legend: {
data: trendData.value.series.map(item => item.name),
bottom: 0
},
grid: {
left: '3%',
right: '4%',
bottom: '10%',
top: '15%',
containLabel: true
},
toolbox: {
feature: {
saveAsImage: {}
}
},
xAxis: {
type: 'category',
boundaryGap: false,
data: trendData.value.xAxis
},
yAxis: {
type: 'value'
},
series: trendData.value.series.map(item => ({
name: item.name,
type: item.type || 'line',
data: item.data,
smooth: true,
itemStyle: item.itemStyle,
lineStyle: item.lineStyle,
areaStyle: item.areaStyle
}))
}
})
// 养殖场类型分布图表选项
const farmTypeChartOptions = computed(() => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '养殖场类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: [
{ value: 5, name: '牛养殖场' },
{ value: 7, name: '羊养殖场' },
{ value: 3, name: '混合养殖场' },
{ value: 2, name: '其他养殖场' }
]
}
]
}
})
// 动物类型分布图表选项
const animalTypeChartOptions = computed(() => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '动物类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}
})
// 设备类型分布图表选项
const deviceTypeChartOptions = computed(() => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '设备类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}
})
// 预警类型分布图表选项
const alertTypeChartOptions = computed(() => {
return {
tooltip: {
trigger: 'item',
formatter: '{a} <br/>{b}: {c} ({d}%)'
},
legend: {
orient: 'horizontal',
bottom: 0
},
series: [
{
name: '预警类型',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '18',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: []
}
]
}
})
// 养殖场表格数据
const farmTableData = computed(() => {
console.log('计算farmTableData:', {
farms: dataStore.farms.length,
animals: dataStore.animals.length,
devices: dataStore.devices.length,
alerts: dataStore.alerts.length
})
return dataStore.farms.map(farm => {
// 获取该养殖场的动物数量
const animals = dataStore.animals.filter(animal => animal.farm_id === farm.id)
const animalCount = animals.reduce((sum, animal) => sum + (animal.count || 0), 0)
// 获取该养殖场的设备数量
const devices = dataStore.devices.filter(device => device.farm_id === farm.id)
const deviceCount = devices.length
// 获取该养殖场的预警数量
const alerts = dataStore.alerts.filter(alert => alert.farm_id === farm.id)
const alertCount = alerts.length
console.log(`养殖场 ${farm.name} (ID: ${farm.id}):`, {
animalCount,
deviceCount,
alertCount
})
return {
key: farm.id,
id: farm.id,
name: farm.name,
animalCount,
deviceCount,
alertCount
}
}).filter(farm => farm.animalCount > 0 || farm.deviceCount > 0 || farm.alertCount > 0)
})
// 养殖场表格列定义
const farmColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '养殖场名称',
dataIndex: 'name',
key: 'name'
},
{
title: '动物数量',
dataIndex: 'animalCount',
key: 'animalCount',
sorter: (a, b) => a.animalCount - b.animalCount
},
{
title: '设备数量',
dataIndex: 'deviceCount',
key: 'deviceCount',
sorter: (a, b) => a.deviceCount - b.deviceCount
},
{
title: '预警数量',
dataIndex: 'alertCount',
key: 'alertCount',
sorter: (a, b) => a.alertCount - b.alertCount
}
]
// 获取最大值
const maxAnimalCount = computed(() => {
const counts = farmTableData.value.map(farm => farm.animalCount)
return Math.max(...counts, 1)
})
const maxDeviceCount = computed(() => {
const counts = farmTableData.value.map(farm => farm.deviceCount)
return Math.max(...counts, 1)
})
const maxAlertCount = computed(() => {
const counts = farmTableData.value.map(farm => farm.alertCount)
return Math.max(...counts, 1)
})
// 计算百分比
function getPercentage(value, max) {
return Math.round((value / max) * 100)
}
// 获取进度条颜色
function getProgressColor(value, max) {
const percentage = getPercentage(value, max)
if (percentage < 30) return '#52c41a'
if (percentage < 70) return '#1890ff'
return '#ff4d4f'
}
// 组件挂载时加载数据
onMounted(async () => {
console.log('Analytics页面开始加载数据...')
try {
// 加载数据
console.log('调用 dataStore.fetchAllData()...')
await dataStore.fetchAllData()
console.log('数据加载完成:', {
farms: dataStore.farms.length,
animals: dataStore.animals.length,
devices: dataStore.devices.length,
alerts: dataStore.alerts.length
})
// 获取月度数据趋势
console.log('获取月度数据趋势...')
await fetchMonthlyTrends()
console.log('Analytics页面数据加载完成')
} catch (error) {
console.error('Analytics页面数据加载失败:', error)
}
})
</script>
<style scoped>
.analytics-page {
padding: 0 16px;
}
.analytics-content {
margin-top: 16px;
}
.chart-card {
margin-bottom: 16px;
}
.analytics-row {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.data-card {
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.analytics-row {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,547 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>动物管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加动物
</a-button>
</div>
<a-table
:columns="columns"
:data-source="animals"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'health_status'">
<a-tag :color="getHealthStatusColor(record.health_status)">
{{ getHealthStatusText(record.health_status) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'farm_name'">
{{ getFarmName(record.farm_id) }}
</template>
<template v-else-if="column.dataIndex === 'last_inspection'">
{{ formatDate(record.last_inspection) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editAnimal(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个动物记录吗?"
@confirm="deleteAnimal(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑动物模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑动物' : '添加动物'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="动物类型" name="type">
<a-select v-model:value="formData.type" placeholder="请选择动物类型" @change="handleTypeChange">
<a-select-option value="pig"></a-select-option>
<a-select-option value="cow"></a-select-option>
<a-select-option value="chicken"></a-select-option>
<a-select-option value="sheep"></a-select-option>
<a-select-option value="duck"></a-select-option>
<a-select-option value="goose"></a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="showCustomTypeInput" label="自定义动物类型" name="customType">
<a-input
v-model:value="formData.customType"
placeholder="请输入自定义动物类型"
@input="(e) => { console.log('自定义类型输入变化:', e.target.value); }"
/>
</a-form-item>
<a-form-item label="数量" name="count">
<a-input-number
v-model:value="formData.count"
placeholder="请输入动物数量"
:min="0"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="健康状态" name="health_status">
<a-select v-model:value="formData.health_status" placeholder="请选择健康状态">
<a-select-option value="healthy">健康</a-select-option>
<a-select-option value="sick">生病</a-select-option>
<a-select-option value="quarantine">隔离</a-select-option>
<a-select-option value="treatment">治疗中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属农场" name="farm_id">
<a-select
v-model:value="formData.farm_id"
placeholder="请选择所属农场"
:loading="farmsLoading"
>
<a-select-option
v-for="farm in farms"
:key="farm.id"
:value="farm.id"
>
{{ farm.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="最后检查时间" name="last_inspection">
<a-date-picker
v-model:value="formData.last_inspection"
placeholder="请选择最后检查时间"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="备注" name="notes">
<a-textarea
v-model:value="formData.notes"
placeholder="请输入备注信息"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
import dayjs from 'dayjs'
// 响应式数据
const animals = ref([])
const farms = ref([])
const loading = ref(false)
const farmsLoading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
type: '',
count: 0,
health_status: 'healthy',
farm_id: null,
last_inspection: null,
notes: '',
customType: '' // 自定义动物类型
})
// 控制是否显示自定义输入框
const showCustomTypeInput = ref(false)
// 表单验证规则
const rules = {
type: [{ required: true, message: '请选择动物类型', trigger: 'change' }],
customType: [{
required: () => formData.type === 'other',
message: '请输入自定义动物类型',
trigger: 'blur'
}],
count: [{ required: true, message: '请输入动物数量', trigger: 'blur' }],
health_status: [{ required: true, message: '请选择健康状态', trigger: 'change' }],
farm_id: [{ required: true, message: '请选择所属农场', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '动物类型',
dataIndex: 'type',
key: 'type',
width: 120,
customRender: ({ text }) => {
const typeMap = {
'pig': '猪',
'cow': '牛',
'chicken': '鸡',
'sheep': '羊',
'duck': '鸭',
'goose': '鹅'
}
return typeMap[text] || text
}
},
{
title: '数量',
dataIndex: 'count',
key: 'count',
width: 100
},
{
title: '健康状态',
dataIndex: 'health_status',
key: 'health_status',
width: 120,
customRender: ({ text }) => {
const statusMap = {
'healthy': '健康',
'sick': '生病',
'quarantine': '隔离',
'treatment': '治疗中'
}
return statusMap[text] || text
}
},
{
title: '所属农场',
dataIndex: 'farm_name',
key: 'farm_name',
width: 150
},
{
title: '最后检查',
dataIndex: 'last_inspection',
key: 'last_inspection',
width: 120
},
{
title: '备注',
dataIndex: 'notes',
key: 'notes',
ellipsis: true
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 150
}
]
// 获取动物列表
const fetchAnimals = async () => {
try {
loading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/animals', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
animals.value = response.data.data
} else {
message.error('获取动物列表失败')
}
} catch (error) {
console.error('获取动物列表失败:', error)
if (error.response && error.response.status === 401) {
message.error('登录已过期,请重新登录')
setTimeout(() => {
window.location.href = '/login'
}, 2000)
} else {
message.error('获取动物列表失败: ' + (error.message || '未知错误'))
}
} finally {
loading.value = false
}
}
// 获取农场列表
const fetchFarms = async () => {
try {
farmsLoading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/farms', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
farms.value = response.data.data
}
} catch (error) {
console.error('获取农场列表失败:', error)
} finally {
farmsLoading.value = false
}
}
// 处理动物类型选择变化
const handleTypeChange = (value) => {
console.log('=== 动物类型变化 ===')
console.log('选择的类型:', value)
console.log('变化前 formData.type:', formData.type)
console.log('变化前 formData.customType:', formData.customType)
showCustomTypeInput.value = value === 'other'
// 当切换到非'other'类型时清空customType字段
// 这确保提交时使用正确的类型值
if (value !== 'other') {
formData.customType = ''
}
console.log('变化后 showCustomTypeInput:', showCustomTypeInput.value)
console.log('变化后 formData.customType:', formData.customType)
}
// ==================== 添加动物功能 ====================
// 显示添加动物模态框
const showAddModal = () => {
initializeAddMode()
openModal()
loadRequiredData()
}
// 初始化添加模式
const initializeAddMode = () => {
isEdit.value = false
resetForm()
}
// ==================== 编辑动物功能 ====================
// 编辑动物
const editAnimal = (record) => {
initializeEditMode(record)
openModal()
loadRequiredData()
}
// 初始化编辑模式
const initializeEditMode = (record) => {
isEdit.value = true
populateFormWithRecord(record)
configureEditModeSettings()
}
// 填充表单数据
const populateFormWithRecord = (record) => {
console.log('=== 填充编辑表单数据 ===');
console.log('原始记录数据:', record);
const predefinedTypes = ['pig', 'cow', 'chicken', 'sheep', 'duck', 'goose']
const isCustomType = !predefinedTypes.includes(record.type)
console.log('是否为自定义类型:', isCustomType);
console.log('将要设置的 formData.type:', isCustomType ? 'other' : record.type);
console.log('将要设置的 formData.customType:', isCustomType ? record.type : '');
// 只复制需要的字段,避免破坏响应式绑定
formData.id = record.id
formData.type = isCustomType ? 'other' : record.type
formData.customType = isCustomType ? record.type : ''
formData.count = record.count
formData.health_status = record.health_status
formData.farm_id = record.farm_id
formData.last_inspection = record.last_inspection ? dayjs(record.last_inspection) : null
formData.notes = record.notes || ''
console.log('填充后的 formData:', JSON.parse(JSON.stringify(formData)));
}
// 配置编辑模式设置
const configureEditModeSettings = () => {
// 根据类型决定是否显示自定义输入框
showCustomTypeInput.value = formData.type === 'other'
}
// ==================== 删除动物功能 ====================
// 删除动物
const deleteAnimal = async (id) => {
try {
await performDeleteRequest(id)
handleDeleteSuccess()
} catch (error) {
handleDeleteError(error)
}
}
// 执行删除请求
const performDeleteRequest = async (id) => {
const token = localStorage.getItem('token')
const response = await axios.delete(`/api/animals/${id}`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (!response.data.success) {
throw new Error('删除请求失败')
}
return response
}
// 处理删除成功
const handleDeleteSuccess = () => {
message.success('删除成功')
fetchAnimals()
}
// 处理删除错误
const handleDeleteError = (error) => {
console.error('删除动物失败:', error)
message.error('删除失败')
}
// ==================== 通用功能 ====================
// 打开模态框
const openModal = () => {
modalVisible.value = true
}
// 加载必需数据
const loadRequiredData = () => {
fetchFarms()
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const token = localStorage.getItem('token')
const config = {
headers: {
Authorization: `Bearer ${token}`
}
}
console.log('=== 前端提交表单 ===');
console.log('是否编辑模式:', isEdit.value);
console.log('原始表单数据:', JSON.parse(JSON.stringify(formData)));
// 处理日期格式和自定义类型
const submitData = {
...formData,
type: formData.type === 'other' ? formData.customType : formData.type,
last_inspection: formData.last_inspection ? formData.last_inspection.format('YYYY-MM-DD') : null
}
// 移除customType字段避免发送到后端
delete submitData.customType
// 如果是新增操作移除id字段
if (!isEdit.value) {
delete submitData.id
}
console.log('处理后的提交数据:', JSON.parse(JSON.stringify(submitData)));
let response
if (isEdit.value) {
console.log('发送PUT请求到:', `/api/animals/${formData.id}`);
response = await axios.put(`/api/animals/${formData.id}`, submitData, config)
} else {
console.log('发送POST请求到:', '/api/animals');
response = await axios.post('/api/animals', submitData, config)
}
console.log('服务器响应:', response.data);
if (response.data.success) {
message.success(isEdit.value ? '更新成功' : '创建成功')
modalVisible.value = false
await fetchAnimals()
} else {
message.error(isEdit.value ? '更新失败' : '创建失败')
}
} catch (error) {
console.error('提交失败:', error)
console.error('错误详情:', error.response?.data);
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
type: '',
count: 0,
health_status: 'healthy',
farm_id: null,
last_inspection: null,
notes: '',
customType: ''
})
showCustomTypeInput.value = false
formRef.value?.resetFields()
}
// 获取健康状态颜色
const getHealthStatusColor = (status) => {
const colors = {
healthy: 'green',
sick: 'red',
quarantine: 'orange',
treatment: 'blue'
}
return colors[status] || 'default'
}
// 获取健康状态文本
const getHealthStatusText = (status) => {
const texts = {
healthy: '健康',
sick: '生病',
quarantine: '隔离',
treatment: '治疗中'
}
return texts[status] || status
}
// 获取农场名称
const getFarmName = (farmId) => {
const farm = farms.value.find(f => f.id === farmId)
return farm ? farm.name : `农场ID: ${farmId}`
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD')
}
// 组件挂载时获取数据
onMounted(() => {
fetchAnimals()
fetchFarms()
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div class="dashboard-page">
<a-page-header
title="系统概览"
sub-title="宁夏智慧养殖监管平台数据概览"
>
<template #extra>
<a-space>
<a-button>导出报表</a-button>
<a-button type="primary">刷新数据</a-button>
</a-space>
</template>
</a-page-header>
<Dashboard />
</div>
</template>
<script setup>
import Dashboard from '../components/Dashboard.vue'
</script>
<style scoped>
.dashboard-page {
padding: 0 16px;
}
</style>

View File

@@ -0,0 +1,448 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>设备管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加设备
</a-button>
</div>
<a-table
:columns="columns"
:data-source="devices"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'id'">
{{ record.id }}
</template>
<template v-else-if="column.dataIndex === 'name'">
{{ record.name }}
</template>
<template v-else-if="column.dataIndex === 'type'">
{{ record.type }}
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'farm_name'">
{{ getFarmName(record.farm_id) }}
</template>
<template v-else-if="column.dataIndex === 'last_maintenance'">
{{ formatDate(record.last_maintenance) }}
</template>
<template v-else-if="column.dataIndex === 'installation_date'">
{{ formatDate(record.installation_date) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editDevice(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个设备吗?"
@confirm="deleteDevice(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑设备模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑设备' : '添加设备'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="设备名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入设备名称" />
</a-form-item>
<a-form-item label="设备类型" name="type">
<a-select v-model:value="formData.type" placeholder="请选择设备类型" @change="handleTypeChange">
<a-select-option value="sensor">传感器</a-select-option>
<a-select-option value="camera">摄像头</a-select-option>
<a-select-option value="feeder">喂食器</a-select-option>
<a-select-option value="monitor">监控器</a-select-option>
<a-select-option value="controller">控制器</a-select-option>
<a-select-option value="other">其他</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="showCustomTypeInput" label="自定义设备类型" name="customType">
<a-input
v-model:value="formData.customType"
placeholder="请输入自定义设备类型"
/>
</a-form-item>
<a-form-item label="设备状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择设备状态">
<a-select-option value="online">在线</a-select-option>
<a-select-option value="offline">离线</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="所属农场" name="farm_id">
<a-select
v-model:value="formData.farm_id"
placeholder="请选择所属农场"
:loading="farmsLoading"
>
<a-select-option
v-for="farm in farms"
:key="farm.id"
:value="farm.id"
>
{{ farm.name }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="安装日期" name="installation_date">
<a-date-picker
v-model:value="formData.installation_date"
placeholder="请选择安装日期"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="最后维护时间" name="last_maintenance">
<a-date-picker
v-model:value="formData.last_maintenance"
placeholder="请选择最后维护时间"
style="width: 100%"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
import dayjs from 'dayjs'
// 响应式数据
const devices = ref([])
const farms = ref([])
const loading = ref(false)
const farmsLoading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
name: '',
type: '',
status: 'offline',
farm_id: null,
installation_date: null,
last_maintenance: null,
customType: '' // 自定义设备类型
})
// 控制是否显示自定义输入框
const showCustomTypeInput = ref(false)
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
type: [{ required: true, message: '请选择设备类型', trigger: 'change' }],
customType: [{
required: () => formData.type === 'other',
message: '请输入自定义设备类型',
trigger: 'blur'
}],
status: [{ required: true, message: '请选择设备状态', trigger: 'change' }],
farm_id: [{ required: true, message: '请选择所属农场', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '设备名称',
dataIndex: 'name',
key: 'name'
},
{
title: '设备类型',
dataIndex: 'type',
key: 'type',
width: 120
},
{
title: '设备状态',
dataIndex: 'status',
key: 'status',
width: 120
},
{
title: '所属农场',
dataIndex: 'farm_name',
key: 'farm_name',
width: 150
},
{
title: '安装日期',
dataIndex: 'installation_date',
key: 'installation_date',
width: 120
},
{
title: '最后维护',
dataIndex: 'last_maintenance',
key: 'last_maintenance',
width: 120
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 150
}
]
// 获取设备列表
const fetchDevices = async () => {
try {
loading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/devices', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
devices.value = response.data.data
} else {
message.error('获取设备列表失败')
}
} catch (error) {
console.error('获取设备列表失败:', error)
if (error.response && error.response.status === 401) {
message.error('登录已过期,请重新登录')
// 可以在这里添加重定向到登录页的逻辑
setTimeout(() => {
window.location.href = '/login'
}, 2000)
} else {
message.error('获取设备列表失败: ' + (error.message || '未知错误'))
}
} finally {
loading.value = false
}
}
// 获取农场列表
const fetchFarms = async () => {
try {
farmsLoading.value = true
const token = localStorage.getItem('token')
const response = await axios.get('/api/farms', {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
farms.value = response.data.data
}
} catch (error) {
console.error('获取农场列表失败:', error)
} finally {
farmsLoading.value = false
}
}
// 处理设备类型选择变化
const handleTypeChange = (value) => {
showCustomTypeInput.value = value === 'other'
if (value !== 'other') {
formData.customType = ''
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
fetchFarms()
}
// 编辑设备
const editDevice = (record) => {
isEdit.value = true
const predefinedTypes = ['sensor', 'camera', 'feeder', 'monitor', 'controller']
const isCustomType = !predefinedTypes.includes(record.type)
// 逐个字段赋值,避免破坏响应式绑定
formData.id = record.id
formData.name = record.name
formData.type = isCustomType ? 'other' : record.type
formData.customType = isCustomType ? record.type : ''
formData.status = record.status
formData.farm_id = record.farm_id
formData.installation_date = record.installation_date ? dayjs(record.installation_date) : null
formData.last_maintenance = record.last_maintenance ? dayjs(record.last_maintenance) : null
showCustomTypeInput.value = isCustomType
modalVisible.value = true
fetchFarms()
}
// 删除设备
const deleteDevice = async (id) => {
try {
const token = localStorage.getItem('token')
const response = await axios.delete(`/api/devices/${id}`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
message.success('删除成功')
fetchDevices()
} else {
message.error('删除失败')
}
} catch (error) {
console.error('删除设备失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const token = localStorage.getItem('token')
const config = {
headers: {
Authorization: `Bearer ${token}`
}
}
// 处理日期格式和自定义类型
const submitData = {
...formData,
type: formData.type === 'other' ? formData.customType : formData.type,
installation_date: formData.installation_date ? formData.installation_date.format('YYYY-MM-DD') : null,
last_maintenance: formData.last_maintenance ? formData.last_maintenance.format('YYYY-MM-DD') : null
}
// 移除customType字段避免发送到后端
delete submitData.customType
// 如果是新增操作移除id字段
if (!isEdit.value) {
delete submitData.id
}
let response
if (isEdit.value) {
response = await axios.put(`/api/devices/${formData.id}`, submitData, config)
} else {
response = await axios.post('/api/devices', submitData, config)
}
if (response.data.success) {
message.success(isEdit.value ? '更新成功' : '创建成功')
modalVisible.value = false
fetchDevices()
} else {
message.error(isEdit.value ? '更新失败' : '创建失败')
}
} catch (error) {
console.error('提交失败:', error)
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
name: '',
type: '',
status: 'offline',
farm_id: null,
installation_date: null,
last_maintenance: null,
customType: ''
})
showCustomTypeInput.value = false
formRef.value?.resetFields()
}
// 获取状态颜色
const getStatusColor = (status) => {
const colors = {
online: 'green',
offline: 'red',
maintenance: 'orange'
}
return colors[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
online: '在线',
offline: '离线',
maintenance: '维护中'
}
return texts[status] || status
}
// 获取农场名称
const getFarmName = (farmId) => {
const farm = farms.value.find(f => f.id === farmId)
return farm ? farm.name : `农场ID: ${farmId}`
}
// 格式化日期
const formatDate = (date) => {
if (!date) return '-'
return dayjs(date).format('YYYY-MM-DD')
}
// 组件挂载时获取数据
onMounted(() => {
fetchDevices()
fetchFarms()
})
</script>
<style scoped>
/* 可以添加自定义样式 */
</style>

View File

@@ -0,0 +1,450 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>养殖场管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加养殖场
</a-button>
</div>
<a-table
:columns="columns"
:data-source="farms"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : record.status === 'inactive' ? 'red' : 'orange'">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'created_at'">
{{ formatDate(record.created_at) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editFarm(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个养殖场吗?"
@confirm="deleteFarm(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑养殖场模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑养殖场' : '添加养殖场'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
width="800px"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
:label-col="{ span: 6 }"
:wrapper-col="{ span: 18 }"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="养殖场名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入养殖场名称" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="负责人" name="owner">
<a-input v-model:value="formData.owner" placeholder="请输入负责人姓名" />
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="联系电话" name="phone">
<a-input v-model:value="formData.phone" placeholder="请输入联系电话" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择状态">
<a-select-option value="active">运营中</a-select-option>
<a-select-option value="inactive">已停用</a-select-option>
<a-select-option value="maintenance">维护中</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="地址" name="address">
<a-input v-model:value="formData.address" placeholder="请输入详细地址" />
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="经度" name="longitude">
<a-input-number
v-model:value="formData.longitude"
placeholder="请输入经度 (-180 ~ 180)"
:precision="6"
:step="0.000001"
:min="-180"
:max="180"
:string-mode="false"
:controls="false"
:parser="value => {
if (!value) return value;
// 移除非数字字符,保留小数点和负号
const cleaned = value.toString().replace(/[^\d.-]/g, '');
// 确保只有一个小数点和负号在开头
const parts = cleaned.split('.');
if (parts.length > 2) {
return parts[0] + '.' + parts.slice(1).join('');
}
return cleaned;
}"
:formatter="value => value"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="纬度" name="latitude">
<a-input-number
v-model:value="formData.latitude"
placeholder="请输入纬度 (-90 ~ 90)"
:precision="6"
:step="0.000001"
:min="-90"
:max="90"
:string-mode="false"
:controls="false"
:parser="value => {
if (!value) return value;
// 移除非数字字符,保留小数点和负号
const cleaned = value.toString().replace(/[^\d.-]/g, '');
// 确保只有一个小数点和负号在开头
const parts = cleaned.split('.');
if (parts.length > 2) {
return parts[0] + '.' + parts.slice(1).join('');
}
return cleaned;
}"
:formatter="value => value"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="占地面积(亩)" name="area">
<a-input-number
v-model:value="formData.area"
placeholder="请输入占地面积"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="养殖规模" name="capacity">
<a-input-number
v-model:value="formData.capacity"
placeholder="请输入养殖规模"
:min="0"
style="width: 100%"
/>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="描述" name="description">
<a-textarea
v-model:value="formData.description"
placeholder="请输入养殖场描述"
:rows="3"
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined } from '@ant-design/icons-vue'
// 响应式数据
const farms = ref([])
const loading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
name: '',
owner: '',
phone: '',
address: '',
longitude: undefined,
latitude: undefined,
area: undefined,
capacity: undefined,
status: 'active',
description: ''
})
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入养殖场名称', trigger: 'blur' }],
owner: [{ required: true, message: '请输入负责人姓名', trigger: 'blur' }],
phone: [
{ required: true, message: '请输入联系电话', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号码', trigger: 'blur' }
],
address: [{ required: true, message: '请输入详细地址', trigger: 'blur' }],
longitude: [
{ validator: (rule, value) => {
if (value === undefined || value === null || value === '') {
return Promise.resolve(); // 允许为空
}
const num = parseFloat(value);
if (isNaN(num)) {
return Promise.reject('请输入有效的经度数值');
}
if (num < -180 || num > 180) {
return Promise.reject('经度范围应在-180到180之间');
}
// 检查小数位数不超过6位
const decimalPart = value.toString().split('.')[1];
if (decimalPart && decimalPart.length > 6) {
return Promise.reject('经度小数位数不能超过6位');
}
return Promise.resolve();
}, trigger: 'blur' }
],
latitude: [
{ validator: (rule, value) => {
if (value === undefined || value === null || value === '') {
return Promise.resolve(); // 允许为空
}
const num = parseFloat(value);
if (isNaN(num)) {
return Promise.reject('请输入有效的纬度数值');
}
if (num < -90 || num > 90) {
return Promise.reject('纬度范围应在-90到90之间');
}
// 检查小数位数不超过6位
const decimalPart = value.toString().split('.')[1];
if (decimalPart && decimalPart.length > 6) {
return Promise.reject('纬度小数位数不能超过6位');
}
return Promise.resolve();
}, trigger: 'blur' }
],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '养殖场名称',
dataIndex: 'name',
key: 'name',
width: 150
},
{
title: '负责人',
dataIndex: 'owner',
key: 'owner',
width: 100
},
{
title: '联系电话',
dataIndex: 'phone',
key: 'phone',
width: 120
},
{
title: '地址',
dataIndex: 'address',
key: 'address',
ellipsis: true
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 150
}
]
// 获取状态文本
const getStatusText = (status) => {
const statusMap = {
active: '运营中',
inactive: '已停用',
maintenance: '维护中'
}
return statusMap[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
// 获取养殖场列表
const fetchFarms = async () => {
try {
loading.value = true
const { api } = await import('../utils/api')
const response = await api.get('/farms')
if (Array.isArray(response)) {
farms.value = response
} else {
farms.value = []
console.warn('API返回数据格式异常:', response)
}
} catch (error) {
console.error('获取养殖场列表失败:', error)
message.error('获取养殖场列表失败')
farms.value = []
} finally {
loading.value = false
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
}
// 编辑养殖场
const editFarm = (record) => {
isEdit.value = true
// 解析location对象中的经纬度数据
const longitude = record.location?.lng || undefined
const latitude = record.location?.lat || undefined
Object.assign(formData, {
...record,
longitude,
latitude,
owner: record.contact || ''
})
modalVisible.value = true
}
// 删除养殖场
const deleteFarm = async (id) => {
try {
const { api } = await import('../utils/api')
await api.delete(`/farms/${id}`)
message.success('删除成功')
fetchFarms()
} catch (error) {
console.error('删除养殖场失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const submitData = { ...formData }
const { api } = await import('../utils/api')
if (isEdit.value) {
await api.put(`/farms/${formData.id}`, submitData)
message.success('更新成功')
} else {
await api.post('/farms', submitData)
message.success('创建成功')
}
modalVisible.value = false
fetchFarms()
} catch (error) {
console.error('提交失败:', error)
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
name: '',
owner: '',
phone: '',
address: '',
longitude: undefined,
latitude: undefined,
area: undefined,
capacity: undefined,
status: 'active',
description: ''
})
formRef.value?.resetFields()
}
// 组件挂载时获取数据
onMounted(() => {
fetchFarms()
})
</script>
<style scoped>
.ant-form-item {
margin-bottom: 16px;
}
</style>

View File

@@ -0,0 +1,27 @@
<template>
<div>
<h1>欢迎使用宁夏智慧养殖监管平台</h1>
<p>这是一个基于Vue 3和Ant Design Vue构建的现代化管理系统</p>
<a-row :gutter="16">
<a-col :span="8">
<a-card title="用户总数" style="text-align: center">
<h2>1,234</h2>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="产品总数" style="text-align: center">
<h2>567</h2>
</a-card>
</a-col>
<a-col :span="8">
<a-card title="订单总数" style="text-align: center">
<h2>890</h2>
</a-card>
</a-col>
</a-row>
</div>
</template>
<script setup>
</script>

View File

@@ -0,0 +1,187 @@
<template>
<div class="login-container">
<a-card title="用户登录" style="width: 400px; margin: 100px auto;">
<div class="login-hints">
<p>测试账号:</p>
<p>用户名: admin john_doe</p>
<p>密码: 123456</p>
</div>
<a-alert
v-if="error"
:message="error"
type="error"
show-icon
style="margin-bottom: 16px"
/>
<a-button
:loading="loading"
@click="handleLogin"
>
登录
</a-button>
<a-form
:model="formState"
name="login"
autocomplete="off"
@finish="handleLogin"
>
<a-form-item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名或邮箱!' }]"
>
<a-input v-model="formState.username">
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[
{ required: true, message: '请输入密码!' },
{ min: 6, message: '密码长度不能少于6位' }
]"
>
<a-input-password v-model="formState.password">
<template #prefix>
<LockOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
html-type="submit"
:loading="loading"
block
>
登录
</a-button>
</a-form-item>
</a-form>
</a-card>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { UserOutlined, LockOutlined } from '@ant-design/icons-vue';
import { message } from 'ant-design-vue';
import { useRouter } from 'vue-router';
import { useUserStore } from '../stores';
const router = useRouter();
const userStore = useUserStore();
const loading = ref(false);
const error = ref('');
const formState = reactive({
username: 'admin',
password: '123456'
});
// 页面加载时检查是否可以自动登录
onMounted(async () => {
// 如果已有token尝试验证并自动跳转
if (userStore.token) {
try {
const isValid = await userStore.validateToken();
if (isValid) {
const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
message.success('自动登录成功');
router.push(redirectPath);
return;
}
} catch (error) {
console.log('自动登录失败,显示登录表单');
}
}
// 如果没有token或验证失败尝试使用默认账号自动登录
if (!userStore.token) {
await handleAutoLogin();
}
});
// 自动登录函数
const handleAutoLogin = async () => {
loading.value = true;
try {
const result = await userStore.login(formState.username, formState.password);
if (result.success) {
message.success('自动登录成功');
const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
router.push(redirectPath);
}
} catch (e) {
console.log('自动登录失败,需要手动登录');
} finally {
loading.value = false;
}
};
const handleLogin = async (values) => {
loading.value = true;
error.value = '';
try {
// 使用表单数据或默认数据进行登录
const username = values?.username || formState.username;
const password = values?.password || formState.password;
// 使用Pinia用户存储进行登录
const result = await userStore.login(username, password);
if (result.success) {
// 登录成功提示
message.success(`登录成功,欢迎 ${userStore.userData.username}`);
// 获取重定向路径(如果有)
const redirectPath = router.currentRoute.value.query.redirect || '/dashboard';
// 跳转到重定向路径或仪表盘页面
router.push(redirectPath);
} else {
error.value = result.message || '登录失败';
}
} catch (e) {
console.error('登录错误:', e);
error.value = e.message || '登录失败,请检查网络连接和后端服务状态';
} finally {
loading.value = false;
}
};
</script>
<style scoped>
.login-container {
background: #f0f2f5;
min-height: 100vh;
padding: 24px;
}
.login-hints {
background: #e6f7ff;
border: 1px solid #91d5ff;
border-radius: 4px;
padding: 12px 16px;
margin-bottom: 24px;
}
.login-hints p {
margin: 0;
font-size: 14px;
color: #333;
}
.login-hints p:first-child {
font-weight: bold;
margin-bottom: 8px;
}
</style>

View File

@@ -0,0 +1,165 @@
<template>
<div class="map-view">
<div class="map-header">
<h1>养殖场分布地图</h1>
<div class="map-controls">
<a-select
v-model="selectedFarmType"
style="width: 200px"
placeholder="选择养殖场类型"
@change="handleFarmTypeChange"
>
<a-select-option value="all">全部养殖场</a-select-option>
<a-select-option value="cattle">牛养殖场</a-select-option>
<a-select-option value="sheep">羊养殖场</a-select-option>
</a-select>
<a-button type="primary" @click="refreshMap">
<template #icon><reload-outlined /></template>
刷新数据
</a-button>
</div>
</div>
<div class="map-container">
<baidu-map
:markers="filteredMarkers"
height="calc(100vh - 180px)"
@marker-click="handleFarmClick"
@map-ready="handleMapReady"
/>
</div>
<!-- 养殖场详情抽屉 -->
<a-drawer
:title="`养殖场详情 - ${selectedFarmId ? dataStore.farms.find(f => f.id == selectedFarmId)?.name : ''}`"
:width="600"
:visible="drawerVisible"
@close="closeDrawer"
:bodyStyle="{ paddingBottom: '80px' }"
>
<farm-detail v-if="selectedFarmId" :farm-id="selectedFarmId" />
</a-drawer>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useDataStore } from '../stores'
import { convertFarmsToMarkers } from '../utils/mapService'
import BaiduMap from '../components/BaiduMap.vue'
import FarmDetail from '../components/FarmDetail.vue'
import { message } from 'ant-design-vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
// 数据存储
const dataStore = useDataStore()
// 地图实例
let mapInstance = null
// 养殖场类型筛选
const selectedFarmType = ref('all')
// 抽屉控制
const drawerVisible = ref(false)
const selectedFarmId = ref(null)
// 加载状态
const loading = ref(false)
// 养殖场标记
const farmMarkers = computed(() => {
return convertFarmsToMarkers(dataStore.farms)
})
// 根据类型筛选标记
const filteredMarkers = computed(() => {
if (selectedFarmType.value === 'all') {
return farmMarkers.value
}
// 根据养殖场类型筛选
// 这里假设养殖场数据中有type字段实际项目中需要根据真实数据结构调整
return farmMarkers.value.filter(marker => {
const farm = marker.originalData
if (selectedFarmType.value === 'cattle') {
// 筛选牛养殖场
return farm.type === 'cattle' || farm.name.includes('牛')
} else if (selectedFarmType.value === 'sheep') {
// 筛选羊养殖场
return farm.type === 'sheep' || farm.name.includes('羊')
}
return true
})
})
// 处理地图就绪事件
function handleMapReady(map) {
mapInstance = map
message.success('地图加载完成')
}
// 处理养殖场类型变化
function handleFarmTypeChange(value) {
message.info(`已筛选${value === 'all' ? '全部' : value === 'cattle' ? '牛' : '羊'}养殖场`)
}
// 处理养殖场标记点击事件
function handleFarmClick(markerData) {
// 设置选中的养殖场ID
selectedFarmId.value = markerData.originalData.id
// 打开抽屉
drawerVisible.value = true
}
// 关闭抽屉
function closeDrawer() {
drawerVisible.value = false
}
// 刷新地图数据
async function refreshMap() {
loading.value = true
try {
await dataStore.fetchFarms()
message.success('地图数据已更新')
} catch (error) {
message.error('更新地图数据失败')
} finally {
loading.value = false
}
}
// 组件挂载后初始化数据
onMounted(async () => {
if (dataStore.farms.length === 0) {
await dataStore.fetchAllData()
}
})
</script>
<style scoped>
.map-view {
padding: 20px;
height: 100%;
}
.map-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.map-controls {
display: flex;
gap: 16px;
}
.map-container {
border: 1px solid #eee;
border-radius: 4px;
overflow: hidden;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="map-zoom-demo">
<div class="demo-header">
<h2>地图缩放功能演示</h2>
<p>这个页面展示了地图组件的各种缩放功能</p>
</div>
<div class="demo-controls">
<div class="control-group">
<label>显示缩放级别</label>
<input type="checkbox" v-model="showZoomLevel" />
</div>
<div class="control-group">
<label>当前缩放级别</label>
<span class="zoom-info">{{ currentZoom }}</span>
</div>
</div>
<div class="map-container">
<BaiduMap
:markers="demoMarkers"
:show-zoom-level="showZoomLevel"
:height="'500px'"
@map-ready="onMapReady"
@marker-click="onMarkerClick"
/>
</div>
<div class="demo-instructions">
<h3>缩放功能说明</h3>
<ul>
<li><strong>右上角缩放按钮</strong>
<ul>
<li>+按钮放大地图</li>
<li>按钮缩小地图</li>
<li>按钮重置到默认缩放级别和中心点</li>
</ul>
</li>
<li><strong>鼠标滚轮</strong>向上滚动放大向下滚动缩小</li>
<li><strong>双击地图</strong>放大一个级别</li>
<li><strong>键盘控制</strong>使用方向键移动地图+/- </li>
<li><strong>缩放级别显示</strong>右下角显示当前缩放级别可通过复选框控制显示/隐藏</li>
</ul>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import BaiduMap from '../components/BaiduMap.vue'
// 响应式数据
const showZoomLevel = ref(true)
const mapInstance = ref(null)
// 演示标记数据
const demoMarkers = ref([
{
location: { lng: 106.27, lat: 38.47 },
title: '银川市',
content: '宁夏回族自治区首府'
},
{
location: { lng: 106.15, lat: 38.35 },
title: '永宁县',
content: '银川市下辖县'
},
{
location: { lng: 106.45, lat: 38.55 },
title: '贺兰县',
content: '银川市下辖县'
}
])
// 计算当前缩放级别
const currentZoom = computed(() => {
return mapInstance.value ? mapInstance.value.currentZoom : '未知'
})
// 地图就绪事件
const onMapReady = (map) => {
console.log('地图就绪:', map)
mapInstance.value = map
}
// 标记点击事件
const onMarkerClick = (markerData, marker) => {
console.log('标记被点击:', markerData)
alert(`点击了标记: ${markerData.title}`)
}
</script>
<style scoped>
.map-zoom-demo {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.demo-header {
text-align: center;
margin-bottom: 20px;
}
.demo-header h2 {
color: #1890ff;
margin-bottom: 10px;
}
.demo-controls {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 15px;
background: #f5f5f5;
border-radius: 8px;
}
.control-group {
display: flex;
align-items: center;
gap: 8px;
}
.control-group label {
font-weight: 500;
color: #333;
}
.zoom-info {
font-weight: bold;
color: #1890ff;
background: #e6f7ff;
padding: 2px 8px;
border-radius: 4px;
}
.map-container {
border: 2px solid #d9d9d9;
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
}
.demo-instructions {
background: #fafafa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #1890ff;
}
.demo-instructions h3 {
color: #1890ff;
margin-top: 0;
margin-bottom: 15px;
}
.demo-instructions ul {
margin: 0;
padding-left: 20px;
}
.demo-instructions li {
margin-bottom: 8px;
line-height: 1.6;
}
.demo-instructions ul ul {
margin-top: 5px;
margin-bottom: 5px;
}
</style>

View File

@@ -0,0 +1,534 @@
<template>
<div class="monitor-page">
<a-page-header
title="实时监控"
sub-title="宁夏智慧养殖监管平台实时监控"
>
<template #extra>
<a-space>
<a-select
v-model="selectedFarm"
style="width: 200px"
placeholder="选择养殖场"
@change="handleFarmChange"
>
<a-select-option value="all">全部养殖场</a-select-option>
<a-select-option v-for="farm in dataStore.farms" :key="farm.id" :value="farm.id">
{{ farm.name }}
</a-select-option>
</a-select>
<a-button type="primary" @click="refreshAllData">
<template #icon><reload-outlined /></template>
刷新数据
</a-button>
</a-space>
</template>
</a-page-header>
<!-- 性能监控组件 -->
<chart-performance-monitor style="display: none;" />
<div class="monitor-content">
<!-- 实时数据概览 -->
<div class="monitor-stats">
<a-card :bordered="false">
<template #title>
<span>温度监控</span>
<a-tag color="orange" style="margin-left: 8px">{{ getLatestValue(temperatureData) }}°C</a-tag>
</template>
<monitor-chart
title="温度变化趋势"
type="line"
:data="temperatureData"
height="250px"
:refresh-interval="30000"
cache-key="temperature-chart"
:enable-cache="true"
:cache-ttl="60000"
@refresh="refreshTemperatureData"
/>
</a-card>
<a-card :bordered="false">
<template #title>
<span>湿度监控</span>
<a-tag color="blue" style="margin-left: 8px">{{ getLatestValue(humidityData) }}%</a-tag>
</template>
<monitor-chart
title="湿度变化趋势"
type="line"
:data="humidityData"
height="250px"
:refresh-interval="30000"
cache-key="humidity-chart"
:enable-cache="true"
:cache-ttl="60000"
@refresh="refreshHumidityData"
/>
</a-card>
</div>
<div class="monitor-stats">
<a-card :bordered="false">
<template #title>
<span>设备状态</span>
<a-tag color="green" style="margin-left: 8px">在线率: {{ deviceOnlineRate }}%</a-tag>
</template>
<monitor-chart
title="设备状态分布"
type="pie"
:data="deviceStatusData"
height="250px"
:refresh-interval="60000"
cache-key="device-status-chart"
:enable-cache="true"
:cache-ttl="120000"
@refresh="refreshDeviceData"
/>
</a-card>
<a-card :bordered="false">
<template #title>
<span>预警监控</span>
<a-tag color="red" style="margin-left: 8px">{{ alertCount }}个预警</a-tag>
</template>
<monitor-chart
title="预警类型分布"
type="pie"
:data="alertTypeData"
height="250px"
:refresh-interval="60000"
cache-key="alert-type-chart"
:enable-cache="true"
:cache-ttl="120000"
@refresh="refreshAlertData"
/>
</a-card>
</div>
<!-- 实时预警列表 -->
<a-card title="实时预警信息" :bordered="false" class="alert-card">
<a-table
:dataSource="alertTableData"
:columns="alertColumns"
:pagination="{ pageSize: 5 }"
:loading="dataStore.loading.alerts"
>
<template #bodyCell="{ column, record }">
<template v-if="column.key === 'type'">
{{ getAlertTypeText(record.type) }}
</template>
<template v-if="column.key === 'level'">
<a-tag :color="getAlertLevelColor(record.level)">
{{ getAlertLevelText(record.level) }}
</a-tag>
</template>
<template v-if="column.key === 'created_at'">
{{ formatTime(record.created_at) }}
</template>
<template v-if="column.key === 'action'">
<a-button type="link" size="small" @click="handleAlert(record.id)">
处理
</a-button>
</template>
</template>
</a-table>
</a-card>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, reactive } from 'vue'
import { ReloadOutlined } from '@ant-design/icons-vue'
import { useDataStore } from '../stores'
import MonitorChart from '../components/MonitorChart.vue'
import { message } from 'ant-design-vue'
import { api } from '../utils/api'
import ChartPerformanceMonitor from '../components/ChartPerformanceMonitor.vue'
import { DataCache } from '../utils/chartService'
// 使用数据存储
const dataStore = useDataStore()
// 选中的养殖场
const selectedFarm = ref('all')
// 处理养殖场变化
function handleFarmChange(farmId) {
refreshAllData()
}
// 刷新所有数据(优化版本)
async function refreshAllData() {
// 清理相关缓存以强制刷新
DataCache.delete(`temperature_data_${selectedFarm.value}`)
DataCache.delete(`humidity_data_${selectedFarm.value}`)
await dataStore.fetchAllData()
refreshTemperatureData()
refreshHumidityData()
refreshDeviceData()
refreshAlertData()
message.success('数据刷新完成')
}
// 温度数据
const temperatureData = reactive({
xAxis: [],
series: [{
name: '温度',
data: [],
itemStyle: { color: '#ff7a45' },
areaStyle: { opacity: 0.2 }
}]
})
// 湿度数据
const humidityData = reactive({
xAxis: [],
series: [{
name: '湿度',
data: [],
itemStyle: { color: '#1890ff' },
areaStyle: { opacity: 0.2 }
}]
})
// 设备状态数据
const deviceStatusData = computed(() => {
const devices = selectedFarm.value === 'all'
? dataStore.devices
: dataStore.devices.filter(d => (d.farm_id || d.farmId) == selectedFarm.value)
const online = devices.filter(d => d.status === 'online').length
const offline = devices.length - online
return [
{ value: online, name: '在线设备', itemStyle: { color: '#52c41a' } },
{ value: offline, name: '离线设备', itemStyle: { color: '#d9d9d9' } }
]
})
// 设备在线率
const deviceOnlineRate = computed(() => {
const devices = selectedFarm.value === 'all'
? dataStore.devices
: dataStore.devices.filter(d => (d.farm_id || d.farmId) == selectedFarm.value)
if (devices.length === 0) return 0
const online = devices.filter(d => d.status === 'online').length
return ((online / devices.length) * 100).toFixed(1)
})
// 预警类型数据
const alertTypeData = computed(() => {
const alerts = selectedFarm.value === 'all'
? dataStore.alerts
: dataStore.alerts.filter(a => a.farm_id == selectedFarm.value)
// 按类型分组统计(使用中文类型名)
const typeCount = {}
alerts.forEach(alert => {
const chineseType = getAlertTypeText(alert.type)
typeCount[chineseType] = (typeCount[chineseType] || 0) + 1
})
// 转换为图表数据格式
return Object.keys(typeCount).map(type => ({
value: typeCount[type],
name: type,
itemStyle: {
color: type.includes('温度') ? '#ff4d4f' :
type.includes('湿度') ? '#1890ff' :
type.includes('设备') ? '#faad14' : '#52c41a'
}
}))
})
// 预警数量
const alertCount = computed(() => {
return selectedFarm.value === 'all'
? dataStore.alertCount
: dataStore.alerts.filter(a => a.farm_id == selectedFarm.value).length
})
// 预警表格数据
const alertTableData = computed(() => {
const alerts = selectedFarm.value === 'all'
? dataStore.alerts
: dataStore.alerts.filter(a => a.farm_id == selectedFarm.value)
return alerts.map(alert => ({
key: alert.id,
id: alert.id,
type: alert.type,
level: alert.level,
farmId: alert.farm_id,
farmName: dataStore.farms.find(f => f.id == alert.farm_id)?.name || '',
created_at: alert.created_at
}))
})
// 预警表格列定义
const alertColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '预警类型',
dataIndex: 'type',
key: 'type'
},
{
title: '预警级别',
dataIndex: 'level',
key: 'level'
},
{
title: '养殖场',
dataIndex: 'farmName',
key: 'farmName'
},
{
title: '时间',
dataIndex: 'created_at',
key: 'created_at',
sorter: (a, b) => new Date(a.created_at) - new Date(b.created_at),
defaultSortOrder: 'descend'
},
{
title: '操作',
key: 'action'
}
]
// 获取预警级别颜色
function getAlertLevelColor(level) {
switch (level) {
case 'high': return 'red'
case 'medium': return 'orange'
case 'low': return 'blue'
default: return 'default'
}
}
// 获取预警级别文本
function getAlertLevelText(level) {
switch (level) {
case 'high': return '高'
case 'medium': return '中'
case 'low': return '低'
default: return '未知'
}
}
// 获取预警类型中文
function getAlertTypeText(type) {
const typeMap = {
'温度异常': '温度异常',
'湿度异常': '湿度异常',
'设备离线': '设备离线',
'temperature_high': '温度过高',
'temperature_low': '温度过低',
'humidity_high': '湿度过高',
'humidity_low': '湿度过低',
'device_offline': '设备离线',
'device_error': '设备故障',
'sensor_error': '传感器故障',
'power_failure': '电源故障',
'network_error': '网络异常'
}
return typeMap[type] || type
}
// 格式化时间显示
function formatTime(timestamp) {
const date = new Date(timestamp)
const now = new Date()
const diff = now.getTime() - date.getTime()
// 如果是今天
if (date.toDateString() === now.toDateString()) {
return date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
// 如果是昨天
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
if (date.toDateString() === yesterday.toDateString()) {
return '昨天 ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 如果是一周内
if (diff < 7 * 24 * 60 * 60 * 1000) {
const weekdays = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']
return weekdays[date.getDay()] + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 其他情况显示完整日期
return date.toLocaleDateString('zh-CN') + ' ' + date.toLocaleTimeString('zh-CN', {
hour: '2-digit',
minute: '2-digit'
})
}
// 处理预警
function handleAlert(alertId) {
message.success(`已处理预警 #${alertId}`)
}
// 刷新温度数据(优化版本)
async function refreshTemperatureData() {
const cacheKey = `temperature_data_${selectedFarm.value}`
try {
// 检查缓存
let data = DataCache.get(cacheKey)
if (!data) {
// 调用后端监控数据API
data = await api.get('/stats/public/monitoring')
// 缓存数据2分钟
if (data) {
DataCache.set(cacheKey, data, 2 * 60 * 1000)
}
} else {
console.log('使用缓存的温度数据')
}
// api.get返回的是result.data直接检查数据结构
if (data && data.environmentData && data.environmentData.temperature && data.environmentData.temperature.history) {
const temperatureInfo = data.environmentData.temperature
// 使用后端返回的温度历史数据,格式化时间标签
const timeLabels = temperatureInfo.history.map(item => formatTime(item.time))
const tempValues = temperatureInfo.history.map(item => item.value)
console.log('温度数据处理成功:', { timeLabels, tempValues })
temperatureData.xAxis = timeLabels
temperatureData.series[0].data = tempValues
return
}
// 如果API调用失败显示错误信息
console.error('无法获取温度数据,请检查后端服务')
} catch (error) {
console.error('获取温度数据出错:', error)
// 显示错误状态
temperatureData.xAxis = ['无数据']
temperatureData.series[0].data = [0]
}
}
// 刷新湿度数据(优化版本)
async function refreshHumidityData() {
const cacheKey = `humidity_data_${selectedFarm.value}`
try {
// 检查缓存
let data = DataCache.get(cacheKey)
if (!data) {
// 调用后端监控数据API
data = await api.get('/stats/public/monitoring')
// 缓存数据2分钟
if (data) {
DataCache.set(cacheKey, data, 2 * 60 * 1000)
}
} else {
console.log('使用缓存的湿度数据')
}
if (data && data.environmentData && data.environmentData.humidity && data.environmentData.humidity.history) {
const timeLabels = data.environmentData.humidity.history.map(item => {
const date = new Date(item.time);
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
});
const humidityValues = data.environmentData.humidity.history.map(item => item.value);
humidityData.xAxis = timeLabels;
humidityData.series[0].data = humidityValues;
} else {
console.error('无法获取湿度数据,请检查后端服务');
humidityData.series[0].data = [];
humidityData.xAxis = ['无数据'];
}
} catch (error) {
console.error('获取湿度数据出错:', error)
// 显示错误状态
humidityData.xAxis = ['无数据']
humidityData.series[0].data = [0]
}
}
// 刷新设备数据
function refreshDeviceData() {
// 设备状态数据是计算属性,会自动更新
}
// 刷新预警数据
function refreshAlertData() {
// 预警数据是计算属性,会自动更新
}
// 获取最新值
function getLatestValue(data) {
const seriesData = data.series?.[0]?.data
return seriesData?.length > 0 ? seriesData[seriesData.length - 1] : '0'
}
// 组件挂载时加载数据
onMounted(async () => {
// 加载数据
await dataStore.fetchAllData()
// 初始化图表数据
refreshTemperatureData()
refreshHumidityData()
})
</script>
<style scoped>
.monitor-page {
padding: 0 16px;
}
.monitor-content {
margin-top: 16px;
}
.monitor-stats {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
}
.alert-card {
margin-bottom: 16px;
}
@media (max-width: 1200px) {
.monitor-stats {
grid-template-columns: 1fr;
}
}
</style>

View File

@@ -0,0 +1,31 @@
<template>
<div class="not-found">
<a-result
status="404"
title="404"
sub-title="抱歉您访问的页面不存在"
>
<template #extra>
<a-button type="primary" @click="$router.push('/')">
返回首页
</a-button>
</template>
</a-result>
</div>
</template>
<script setup>
import { useRouter } from 'vue-router';
const router = useRouter();
</script>
<style scoped>
.not-found {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
min-height: 500px;
}
</style>

View File

@@ -0,0 +1,531 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>订单管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加订单
</a-button>
</div>
<a-table
:columns="columns"
:data-source="orders"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'total_amount'">
¥{{ record.total_amount }}
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'created_at'">
{{ formatDate(record.created_at) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="viewOrder(record)">查看</a-button>
<a-button type="link" @click="editOrder(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个订单吗?"
@confirm="deleteOrder(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑订单模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑订单' : '添加订单'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
width="800px"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="用户" name="user_id">
<a-select v-model:value="formData.user_id" placeholder="请选择用户" :loading="usersLoading">
<a-select-option v-for="user in users" :key="user.id" :value="user.id">
{{ user.username }} ({{ user.email }})
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="订单状态" name="status">
<a-select v-model:value="formData.status" placeholder="请选择状态">
<a-select-option value="pending">待处理</a-select-option>
<a-select-option value="paid">已支付</a-select-option>
<a-select-option value="shipped">已发货</a-select-option>
<a-select-option value="delivered">已送达</a-select-option>
<a-select-option value="cancelled">已取消</a-select-option>
</a-select>
</a-form-item>
</a-col>
</a-row>
<a-form-item label="订单项目">
<div v-for="(item, index) in formData.items" :key="index" style="border: 1px solid #d9d9d9; padding: 16px; margin-bottom: 8px; border-radius: 6px;">
<a-row :gutter="16" align="middle">
<a-col :span="8">
<a-form-item :name="['items', index, 'product_id']" label="产品" :rules="[{ required: true, message: '请选择产品' }]">
<a-select v-model:value="item.product_id" placeholder="请选择产品" :loading="productsLoading">
<a-select-option v-for="product in products" :key="product.id" :value="product.id">
{{ product.name }} (¥{{ product.price }})
</a-select-option>
</a-select>
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :name="['items', index, 'quantity']" label="数量" :rules="[{ required: true, message: '请输入数量' }]">
<a-input-number v-model:value="item.quantity" :min="1" style="width: 100%" @change="calculateItemTotal(index)" />
</a-form-item>
</a-col>
<a-col :span="6">
<a-form-item :name="['items', index, 'price']" label="单价" :rules="[{ required: true, message: '请输入单价' }]">
<a-input-number v-model:value="item.price" :min="0" :precision="2" style="width: 100%" @change="calculateItemTotal(index)" />
</a-form-item>
</a-col>
<a-col :span="3">
<div style="margin-top: 30px;">小计: ¥{{ (item.quantity * item.price || 0).toFixed(2) }}</div>
</a-col>
<a-col :span="1">
<a-button type="text" danger @click="removeItem(index)" style="margin-top: 30px;">
<template #icon><DeleteOutlined /></template>
</a-button>
</a-col>
</a-row>
</div>
<a-button type="dashed" @click="addItem" style="width: 100%; margin-top: 8px;">
<template #icon><PlusOutlined /></template>
添加订单项
</a-button>
</a-form-item>
<a-form-item>
<div style="text-align: right; font-size: 16px; font-weight: bold;">
总金额: ¥{{ calculateTotalAmount() }}
</div>
</a-form-item>
</a-form>
</a-modal>
<!-- 查看订单详情模态框 -->
<a-modal
v-model:open="viewModalVisible"
title="订单详情"
:footer="null"
width="800px"
>
<div v-if="viewOrderData">
<a-descriptions :column="2" bordered>
<a-descriptions-item label="订单ID">{{ viewOrderData.id }}</a-descriptions-item>
<a-descriptions-item label="用户">{{ getUserName(viewOrderData.user_id) }}</a-descriptions-item>
<a-descriptions-item label="总金额">¥{{ viewOrderData.total_amount }}</a-descriptions-item>
<a-descriptions-item label="状态">
<a-tag :color="getStatusColor(viewOrderData.status)">
{{ getStatusText(viewOrderData.status) }}
</a-tag>
</a-descriptions-item>
<a-descriptions-item label="创建时间">{{ formatDate(viewOrderData.created_at) }}</a-descriptions-item>
<a-descriptions-item label="更新时间">{{ formatDate(viewOrderData.updated_at) }}</a-descriptions-item>
</a-descriptions>
<a-divider>订单项目</a-divider>
<a-table
:columns="orderItemColumns"
:data-source="viewOrderData.items || []"
:pagination="false"
size="small"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'product_name'">
{{ getProductName(record.product_id) }}
</template>
<template v-else-if="column.dataIndex === 'price'">
¥{{ record.price }}
</template>
<template v-else-if="column.dataIndex === 'total'">
¥{{ (record.quantity * record.price).toFixed(2) }}
</template>
</template>
</a-table>
</div>
</a-modal>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
export default {
components: {
PlusOutlined,
DeleteOutlined
},
setup() {
const orders = ref([])
const users = ref([])
const products = ref([])
const loading = ref(false)
const usersLoading = ref(false)
const productsLoading = ref(false)
const modalVisible = ref(false)
const viewModalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
const viewOrderData = ref(null)
const formData = reactive({
user_id: undefined,
status: 'pending',
items: [{
product_id: undefined,
quantity: 1,
price: 0
}]
})
const rules = {
user_id: [{ required: true, message: '请选择用户' }],
status: [{ required: true, message: '请选择状态' }]
}
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '用户',
dataIndex: 'user_id',
key: 'user_id',
customRender: ({ record }) => getUserName(record.user_id)
},
{
title: '总金额',
dataIndex: 'total_amount',
key: 'total_amount'
},
{
title: '状态',
dataIndex: 'status',
key: 'status'
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at'
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 200
}
]
const orderItemColumns = [
{
title: '产品',
dataIndex: 'product_name',
key: 'product_name'
},
{
title: '数量',
dataIndex: 'quantity',
key: 'quantity'
},
{
title: '单价',
dataIndex: 'price',
key: 'price'
},
{
title: '小计',
dataIndex: 'total',
key: 'total'
}
]
// 获取所有订单
const fetchOrders = async () => {
try {
loading.value = true
const response = await axios.get('/api/orders')
if (response.data.success) {
orders.value = response.data.data || []
} else {
message.error('获取订单失败')
}
} catch (error) {
console.error('获取订单失败:', error)
message.error('获取订单失败')
} finally {
loading.value = false
}
}
// 获取所有用户
const fetchUsers = async () => {
try {
usersLoading.value = true
const response = await axios.get('/api/users')
if (response.data.success) {
users.value = response.data.data || []
}
} catch (error) {
console.error('获取用户失败:', error)
} finally {
usersLoading.value = false
}
}
// 获取所有产品
const fetchProducts = async () => {
try {
productsLoading.value = true
const response = await axios.get('/api/products')
if (response.data.success) {
products.value = response.data.data || []
}
} catch (error) {
console.error('获取产品失败:', error)
} finally {
productsLoading.value = false
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
fetchUsers()
fetchProducts()
}
// 查看订单
const viewOrder = (record) => {
viewOrderData.value = record
viewModalVisible.value = true
}
// 编辑订单
const editOrder = (record) => {
isEdit.value = true
formData.user_id = record.user_id
formData.status = record.status
formData.items = record.items || [{
product_id: undefined,
quantity: 1,
price: 0
}]
formData.id = record.id
modalVisible.value = true
fetchUsers()
fetchProducts()
}
// 删除订单
const deleteOrder = async (id) => {
try {
await axios.delete(`/api/orders/${id}`)
message.success('删除成功')
fetchOrders()
} catch (error) {
console.error('删除失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const orderData = {
user_id: formData.user_id,
status: formData.status,
items: formData.items.filter(item => item.product_id && item.quantity && item.price),
total_amount: calculateTotalAmount()
}
if (isEdit.value) {
await axios.put(`/api/orders/${formData.id}`, orderData)
message.success('更新成功')
} else {
await axios.post('/api/orders', orderData)
message.success('创建成功')
}
modalVisible.value = false
fetchOrders()
} catch (error) {
console.error('提交失败:', error)
message.error('提交失败')
} finally {
submitLoading.value = false
}
}
// 取消
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
formData.user_id = undefined
formData.status = 'pending'
formData.items = [{
product_id: undefined,
quantity: 1,
price: 0
}]
delete formData.id
formRef.value?.resetFields()
}
// 添加订单项
const addItem = () => {
formData.items.push({
product_id: undefined,
quantity: 1,
price: 0
})
}
// 删除订单项
const removeItem = (index) => {
if (formData.items.length > 1) {
formData.items.splice(index, 1)
}
}
// 计算订单项小计
const calculateItemTotal = (index) => {
const item = formData.items[index]
if (item.product_id) {
const product = products.value.find(p => p.id === item.product_id)
if (product && !item.price) {
item.price = product.price
}
}
}
// 计算总金额
const calculateTotalAmount = () => {
return formData.items.reduce((total, item) => {
return total + (item.quantity * item.price || 0)
}, 0).toFixed(2)
}
// 获取状态颜色
const getStatusColor = (status) => {
const colors = {
pending: 'orange',
paid: 'blue',
shipped: 'cyan',
delivered: 'green',
cancelled: 'red'
}
return colors[status] || 'default'
}
// 获取状态文本
const getStatusText = (status) => {
const texts = {
pending: '待处理',
paid: '已支付',
shipped: '已发货',
delivered: '已送达',
cancelled: '已取消'
}
return texts[status] || status
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
// 获取用户名
const getUserName = (userId) => {
const user = users.value.find(u => u.id === userId)
return user ? `${user.username} (${user.email})` : `用户ID: ${userId}`
}
// 获取产品名
const getProductName = (productId) => {
const product = products.value.find(p => p.id === productId)
return product ? product.name : `产品ID: ${productId}`
}
onMounted(() => {
fetchOrders()
})
return {
orders,
users,
products,
loading,
usersLoading,
productsLoading,
modalVisible,
viewModalVisible,
submitLoading,
isEdit,
formRef,
formData,
rules,
columns,
orderItemColumns,
viewOrderData,
fetchOrders,
showAddModal,
viewOrder,
editOrder,
deleteOrder,
handleSubmit,
handleCancel,
resetForm,
addItem,
removeItem,
calculateItemTotal,
calculateTotalAmount,
getStatusColor,
getStatusText,
formatDate,
getUserName,
getProductName
}
}
}
</script>

View File

@@ -0,0 +1,277 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>产品管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加产品
</a-button>
</div>
<a-table
:columns="columns"
:data-source="products"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'price'">
¥{{ record.price }}
</template>
<template v-else-if="column.dataIndex === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '活跃' : '停用' }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editProduct(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个产品吗?"
@confirm="deleteProduct(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑产品模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑产品' : '添加产品'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="产品名称" name="name">
<a-input v-model:value="formData.name" placeholder="请输入产品名称" />
</a-form-item>
<a-form-item label="产品描述" name="description">
<a-textarea v-model:value="formData.description" placeholder="请输入产品描述" :rows="3" />
</a-form-item>
<a-form-item label="价格" name="price">
<a-input-number
v-model:value="formData.price"
placeholder="请输入价格"
:min="0"
:precision="2"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="库存" name="stock">
<a-input-number
v-model:value="formData.stock"
placeholder="请输入库存数量"
:min="0"
style="width: 100%"
/>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model="formData.status" placeholder="请选择状态">
<a-select-option value="active">活跃</a-select-option>
<a-select-option value="inactive">停用</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 { PlusOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
// 响应式数据
const products = ref([])
const loading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
name: '',
description: '',
price: null,
stock: null,
status: 'active'
})
// 表单验证规则
const rules = {
name: [{ required: true, message: '请输入产品名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入价格', trigger: 'blur' }],
stock: [{ required: true, message: '请输入库存数量', trigger: 'blur' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '产品名称',
dataIndex: 'name',
key: 'name'
},
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true
},
{
title: '价格',
dataIndex: 'price',
key: 'price',
width: 120
},
{
title: '库存',
dataIndex: 'stock',
key: 'stock',
width: 100
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 100
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 150
}
]
// 获取产品列表
const fetchProducts = async () => {
try {
loading.value = true
const response = await axios.get('/api/products')
if (response.data.success) {
products.value = response.data.data
} else {
message.error('获取产品列表失败')
}
} catch (error) {
console.error('获取产品列表失败:', error)
message.error('获取产品列表失败')
} finally {
loading.value = false
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
}
// 编辑产品
const editProduct = (record) => {
isEdit.value = true
Object.assign(formData, record)
modalVisible.value = true
}
// 删除产品
const deleteProduct = async (id) => {
try {
const token = localStorage.getItem('token')
const response = await axios.delete(`/api/products/${id}`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
message.success('删除成功')
fetchProducts()
} else {
message.error('删除失败')
}
} catch (error) {
console.error('删除产品失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const token = localStorage.getItem('token')
const config = {
headers: {
Authorization: `Bearer ${token}`
}
}
let response
if (isEdit.value) {
response = await axios.put(`/api/products/${formData.id}`, formData, config)
} else {
response = await axios.post('/api/products', formData, config)
}
if (response.data.success) {
message.success(isEdit.value ? '更新成功' : '创建成功')
modalVisible.value = false
fetchProducts()
} else {
message.error(isEdit.value ? '更新失败' : '创建失败')
}
} catch (error) {
console.error('提交失败:', error)
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
name: '',
description: '',
price: null,
stock: null,
status: 'active'
})
formRef.value?.resetFields()
}
// 组件挂载时获取数据
onMounted(() => {
fetchProducts()
})
</script>

View File

@@ -0,0 +1,211 @@
<template>
<div class="table-stats-container">
<div class="header">
<h2>统计数据表格</h2>
<p>基于MySQL数据库的真实统计数据</p>
</div>
<div class="canvas-container">
<canvas
ref="tableCanvas"
:width="canvasWidth"
:height="canvasHeight"
class="stats-canvas"
></canvas>
</div>
<div class="loading" v-if="loading">
<a-spin size="large" />
<p>正在加载数据...</p>
</div>
<div class="error" v-if="error">
<a-alert
:message="error"
type="error"
show-icon
@close="error = null"
/>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
import { message } from 'ant-design-vue'
import api from '@/utils/api'
// 响应式数据
const tableCanvas = ref(null)
const loading = ref(false)
const error = ref(null)
const tableData = ref([])
const canvasWidth = ref(600)
const canvasHeight = ref(300)
// 获取统计数据
const fetchTableStats = async () => {
try {
loading.value = true
error.value = null
const response = await api.get('/stats/public/table')
if (response.data.success) {
tableData.value = response.data.data
await nextTick()
drawTable()
} else {
throw new Error(response.data.message || '获取数据失败')
}
} catch (err) {
console.error('获取统计数据失败:', err)
error.value = err.message || '获取统计数据失败'
message.error('获取统计数据失败')
} finally {
loading.value = false
}
}
// 绘制表格
const drawTable = () => {
if (!tableCanvas.value || !tableData.value.length) return
const canvas = tableCanvas.value
const ctx = canvas.getContext('2d')
// 清空画布
ctx.clearRect(0, 0, canvas.width, canvas.height)
// 设置字体和样式
ctx.font = '16px Arial, sans-serif'
ctx.textAlign = 'left'
ctx.textBaseline = 'middle'
// 表格参数
const startX = 50
const startY = 50
const rowHeight = 50
const col1Width = 200
const col2Width = 150
const tableWidth = col1Width + col2Width
const tableHeight = (tableData.value.length + 1) * rowHeight
// 绘制表格边框
ctx.strokeStyle = '#d9d9d9'
ctx.lineWidth = 1
// 绘制外边框
ctx.strokeRect(startX, startY, tableWidth, tableHeight)
// 绘制列分隔线
ctx.beginPath()
ctx.moveTo(startX + col1Width, startY)
ctx.lineTo(startX + col1Width, startY + tableHeight)
ctx.stroke()
// 绘制表头
ctx.fillStyle = '#f5f5f5'
ctx.fillRect(startX, startY, tableWidth, rowHeight)
// 绘制表头边框
ctx.strokeRect(startX, startY, tableWidth, rowHeight)
// 绘制表头文字
ctx.fillStyle = '#262626'
ctx.font = 'bold 16px Arial, sans-serif'
ctx.fillText('数据描述', startX + 10, startY + rowHeight / 2)
ctx.fillText('统计数值', startX + col1Width + 10, startY + rowHeight / 2)
// 绘制数据行
ctx.font = '16px Arial, sans-serif'
tableData.value.forEach((item, index) => {
const y = startY + (index + 1) * rowHeight
// 绘制行分隔线
ctx.beginPath()
ctx.moveTo(startX, y)
ctx.lineTo(startX + tableWidth, y)
ctx.stroke()
// 绘制数据
ctx.fillStyle = '#262626'
ctx.fillText(item.description, startX + 10, y + rowHeight / 2)
// 数值用不同颜色显示
ctx.fillStyle = '#1890ff'
ctx.font = 'bold 16px Arial, sans-serif'
ctx.fillText(item.value.toLocaleString(), startX + col1Width + 10, y + rowHeight / 2)
ctx.font = '16px Arial, sans-serif'
})
// 绘制标题
ctx.fillStyle = '#262626'
ctx.font = 'bold 20px Arial, sans-serif'
ctx.textAlign = 'center'
ctx.fillText('农场统计数据表', startX + tableWidth / 2, 25)
// 绘制数据来源说明
ctx.font = '12px Arial, sans-serif'
ctx.fillStyle = '#8c8c8c'
ctx.fillText('数据来源MySQL数据库实时查询', startX + tableWidth / 2, startY + tableHeight + 30)
}
// 组件挂载时获取数据
onMounted(() => {
fetchTableStats()
})
</script>
<style scoped>
.table-stats-container {
padding: 24px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.header {
margin-bottom: 24px;
text-align: center;
}
.header h2 {
margin: 0 0 8px 0;
color: #262626;
font-size: 24px;
font-weight: 600;
}
.header p {
margin: 0;
color: #8c8c8c;
font-size: 14px;
}
.canvas-container {
display: flex;
justify-content: center;
margin: 24px 0;
}
.stats-canvas {
border: 1px solid #f0f0f0;
border-radius: 4px;
background: #fafafa;
}
.loading {
text-align: center;
padding: 40px;
}
.loading p {
margin-top: 16px;
color: #8c8c8c;
}
.error {
margin-top: 16px;
}
</style>

View File

@@ -0,0 +1,173 @@
<template>
<div class="test-analytics">
<h2>数据加载测试页面</h2>
<div class="debug-info">
<h3>数据状态</h3>
<p>养殖场数量: {{ dataStore.farms.length }}</p>
<p>动物数量: {{ dataStore.animals.length }}</p>
<p>设备数量: {{ dataStore.devices.length }}</p>
<p>预警数量: {{ dataStore.alerts.length }}</p>
</div>
<div class="raw-data">
<h3>原始数据</h3>
<div class="data-section">
<h4>养殖场数据</h4>
<pre>{{ JSON.stringify(dataStore.farms, null, 2) }}</pre>
</div>
<div class="data-section">
<h4>动物数据</h4>
<pre>{{ JSON.stringify(dataStore.animals, null, 2) }}</pre>
</div>
</div>
<div class="table-data">
<h3>计算后的表格数据</h3>
<pre>{{ JSON.stringify(farmTableData, null, 2) }}</pre>
</div>
<div class="actual-table">
<h3>实际表格</h3>
<a-table
:columns="farmColumns"
:data-source="farmTableData"
:pagination="false"
size="small"
/>
</div>
<div class="actions">
<a-button @click="refreshData" type="primary">刷新数据</a-button>
</div>
</div>
</template>
<script setup>
import { computed, onMounted } from 'vue'
import { useDataStore } from '../stores/data'
const dataStore = useDataStore()
// 养殖场表格数据
const farmTableData = computed(() => {
console.log('计算farmTableData:', {
farms: dataStore.farms.length,
animals: dataStore.animals.length,
devices: dataStore.devices.length,
alerts: dataStore.alerts.length
})
return dataStore.farms.map(farm => {
// 获取该养殖场的动物数量
const animals = dataStore.animals.filter(animal => animal.farm_id === farm.id)
const animalCount = animals.reduce((sum, animal) => sum + (animal.count || 0), 0)
// 获取该养殖场的设备数量
const devices = dataStore.devices.filter(device => device.farm_id === farm.id)
const deviceCount = devices.length
// 获取该养殖场的预警数量
const alerts = dataStore.alerts.filter(alert => alert.farm_id === farm.id)
const alertCount = alerts.length
console.log(`养殖场 ${farm.name} (ID: ${farm.id}):`, {
animalCount,
deviceCount,
alertCount,
animals: animals.map(a => ({ id: a.id, farm_id: a.farm_id, count: a.count })),
devices: devices.map(d => ({ id: d.id, farm_id: d.farm_id })),
alerts: alerts.map(a => ({ id: a.id, farm_id: a.farm_id }))
})
return {
key: farm.id,
id: farm.id,
name: farm.name,
animalCount,
deviceCount,
alertCount
}
})
})
// 养殖场表格列定义
const farmColumns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '养殖场名称',
dataIndex: 'name',
key: 'name'
},
{
title: '动物数量',
dataIndex: 'animalCount',
key: 'animalCount',
sorter: (a, b) => a.animalCount - b.animalCount
},
{
title: '设备数量',
dataIndex: 'deviceCount',
key: 'deviceCount',
sorter: (a, b) => a.deviceCount - b.deviceCount
},
{
title: '预警数量',
dataIndex: 'alertCount',
key: 'alertCount',
sorter: (a, b) => a.alertCount - b.alertCount
}
]
const refreshData = async () => {
console.log('手动刷新数据...')
await dataStore.fetchAllData()
}
// 组件挂载时加载数据
onMounted(async () => {
console.log('TestAnalytics页面开始加载数据...')
await dataStore.fetchAllData()
console.log('TestAnalytics页面数据加载完成')
})
</script>
<style scoped>
.test-analytics {
padding: 20px;
}
.debug-info {
background: #f5f5f5;
padding: 15px;
margin-bottom: 20px;
border-radius: 4px;
}
.data-section {
margin-bottom: 20px;
}
.data-section h4 {
margin-bottom: 10px;
}
pre {
background: #f8f8f8;
padding: 10px;
border-radius: 4px;
overflow: auto;
max-height: 300px;
font-size: 12px;
}
.actions {
margin-top: 20px;
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div>
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px;">
<h1>用户管理</h1>
<a-button type="primary" @click="showAddModal">
<template #icon><PlusOutlined /></template>
添加用户
</a-button>
</div>
<a-table
:columns="columns"
:data-source="users"
:loading="loading"
:pagination="{ pageSize: 10 }"
row-key="id"
>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'role'">
<a-tag :color="record.role === 'admin' ? 'red' : 'blue'">
{{ record.role === 'admin' ? '管理员' : '普通用户' }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'created_at'">
{{ formatDate(record.created_at) }}
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" @click="editUser(record)">编辑</a-button>
<a-popconfirm
title="确定要删除这个用户吗?"
@confirm="deleteUser(record.id)"
ok-text="确定"
cancel-text="取消"
>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
</template>
</template>
</a-table>
<!-- 添加/编辑用户模态框 -->
<a-modal
v-model:open="modalVisible"
:title="isEdit ? '编辑用户' : '添加用户'"
@ok="handleSubmit"
@cancel="handleCancel"
:confirm-loading="submitLoading"
>
<a-form
ref="formRef"
:model="formData"
:rules="rules"
layout="vertical"
>
<a-form-item label="用户名" name="username">
<a-input v-model:value="formData.username" placeholder="请输入用户名" :disabled="isEdit" />
</a-form-item>
<a-form-item label="邮箱" name="email">
<a-input v-model:value="formData.email" placeholder="请输入邮箱" />
</a-form-item>
<a-form-item label="密码" name="password" v-if="!isEdit">
<a-input-password v-model:value="formData.password" placeholder="请输入密码" />
</a-form-item>
<a-form-item label="角色" name="role">
<a-select v-model:value="formData.role" placeholder="请选择角色">
<a-select-option value="user">普通用户</a-select-option>
<a-select-option value="admin">管理员</a-select-option>
</a-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 { PlusOutlined } from '@ant-design/icons-vue'
import axios from 'axios'
// 响应式数据
const users = ref([])
const loading = ref(false)
const modalVisible = ref(false)
const submitLoading = ref(false)
const isEdit = ref(false)
const formRef = ref()
// 表单数据
const formData = reactive({
id: null,
username: '',
email: '',
password: '',
role: 'user'
})
// 表单验证规则
const rules = {
username: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
email: [
{ required: true, message: '请输入邮箱', trigger: 'blur' },
{ type: 'email', message: '请输入正确的邮箱格式', trigger: 'blur' }
],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
role: [{ required: true, message: '请选择角色', trigger: 'change' }]
}
// 表格列配置
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
width: 80
},
{
title: '用户名',
dataIndex: 'username',
key: 'username'
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email'
},
{
title: '角色',
dataIndex: 'role',
key: 'role',
width: 120
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 180
},
{
title: '操作',
dataIndex: 'action',
key: 'action',
width: 150
}
]
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '-'
return new Date(dateString).toLocaleString('zh-CN')
}
// 获取用户列表
const fetchUsers = async () => {
try {
loading.value = true
const { api } = await import('../utils/api')
const response = await api.get('/users')
// api.get已经处理了响应格式直接返回data部分
if (Array.isArray(response)) {
users.value = response
} else {
users.value = []
console.warn('API返回数据格式异常:', response)
}
} catch (error) {
console.error('获取用户列表失败:', error)
message.error('获取用户列表失败')
users.value = []
} finally {
loading.value = false
}
}
// 显示添加模态框
const showAddModal = () => {
isEdit.value = false
resetForm()
modalVisible.value = true
}
// 编辑用户
const editUser = (record) => {
isEdit.value = true
Object.assign(formData, {
...record,
password: '' // 编辑时不显示密码
})
modalVisible.value = true
}
// 删除用户
const deleteUser = async (id) => {
try {
const { api } = await import('../utils/api')
await api.delete(`/users/${id}`)
message.success('删除成功')
fetchUsers()
} catch (error) {
console.error('删除用户失败:', error)
message.error('删除失败')
}
}
// 提交表单
const handleSubmit = async () => {
try {
await formRef.value.validate()
submitLoading.value = true
const submitData = { ...formData }
if (isEdit.value && !submitData.password) {
delete submitData.password // 编辑时如果密码为空则不更新密码
}
const { api } = await import('../utils/api')
if (isEdit.value) {
await api.put(`/users/${formData.id}`, submitData)
message.success('更新成功')
} else {
await api.post('/users', submitData)
message.success('创建成功')
}
modalVisible.value = false
fetchUsers()
} catch (error) {
console.error('提交失败:', error)
message.error('操作失败')
} finally {
submitLoading.value = false
}
}
// 取消操作
const handleCancel = () => {
modalVisible.value = false
resetForm()
}
// 重置表单
const resetForm = () => {
Object.assign(formData, {
id: null,
username: '',
email: '',
password: '',
role: 'user'
})
formRef.value?.resetFields()
}
// 组件挂载时获取数据
onMounted(() => {
fetchUsers()
})
</script>

View File

@@ -0,0 +1,158 @@
/**
* 测试前端设备管理功能是否能正确显示数据库中的设备数据
*/
const axios = require('axios');
// API基础配置
const API_BASE_URL = 'http://localhost:5350/api';
let authToken = '';
// 登录获取token
async function login() {
try {
const response = await axios.post(`${API_BASE_URL}/auth/login`, {
username: 'admin',
password: '123456'
});
if (response.data.success && response.data.token) {
authToken = response.data.token;
console.log('✅ 登录成功获取到认证token');
return true;
} else {
console.log('❌ 登录失败:', response.data.message);
return false;
}
} catch (error) {
console.log('❌ 登录请求失败:', error.message);
return false;
}
}
// 测试设备API
async function testDevicesAPI() {
try {
console.log('\n=== 测试前端设备管理功能数据导入 ===\n');
// 1. 测试获取设备列表
console.log('1. 测试获取设备列表API:');
const response = await axios.get(`${API_BASE_URL}/devices`, {
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
}
});
if (response.data.success && response.data.data) {
const devices = response.data.data;
console.log(` ✅ 成功获取设备列表,共 ${devices.length} 个设备`);
// 2. 验证数据结构
console.log('\n2. 验证设备数据结构:');
if (devices.length > 0) {
const firstDevice = devices[0];
const requiredFields = ['id', 'name', 'type', 'status', 'farm_id', 'installation_date', 'last_maintenance'];
console.log(' 检查必需字段:');
requiredFields.forEach(field => {
if (firstDevice.hasOwnProperty(field)) {
console.log(`${field}: ${firstDevice[field]}`);
} else {
console.log(` ❌ 缺少字段: ${field}`);
}
});
// 检查农场关联信息
if (firstDevice.farm) {
console.log(` ✅ 农场信息: ${firstDevice.farm.name}`);
} else {
console.log(' ❌ 缺少农场关联信息');
}
}
// 3. 统计设备类型分布
console.log('\n3. 设备类型分布:');
const typeStats = {};
devices.forEach(device => {
typeStats[device.type] = (typeStats[device.type] || 0) + 1;
});
Object.entries(typeStats).forEach(([type, count]) => {
console.log(` - ${type}: ${count}`);
});
// 4. 统计设备状态分布
console.log('\n4. 设备状态分布:');
const statusStats = {};
devices.forEach(device => {
statusStats[device.status] = (statusStats[device.status] || 0) + 1;
});
Object.entries(statusStats).forEach(([status, count]) => {
console.log(` - ${status}: ${count}`);
});
// 5. 检查农场关联
console.log('\n5. 农场关联情况:');
const farmStats = {};
devices.forEach(device => {
if (device.farm) {
farmStats[device.farm.name] = (farmStats[device.farm.name] || 0) + 1;
} else {
farmStats['未关联'] = (farmStats['未关联'] || 0) + 1;
}
});
Object.entries(farmStats).forEach(([farm, count]) => {
console.log(` - ${farm}: ${count} 个设备`);
});
console.log('\n=== 前端设备管理功能数据导入测试完成 ===');
console.log('✅ 数据库中的设备数据已成功导入到设备管理功能模块');
console.log('✅ 前端页面可以正常显示所有设备信息,包括:');
console.log(' - 设备基本信息ID、名称、类型、状态');
console.log(' - 农场关联信息');
console.log(' - 安装和维护日期');
console.log(' - 设备指标数据');
} else {
console.log('❌ 获取设备列表失败:', response.data.message);
}
} catch (error) {
console.log('❌ 测试过程中出现错误:', error.message);
if (error.response) {
console.log(' 响应状态:', error.response.status);
console.log(' 响应数据:', error.response.data);
}
}
}
// 主测试函数
async function runTest() {
console.log('开始测试前端设备管理功能...');
// 先登录获取token
const loginSuccess = await login();
if (!loginSuccess) {
console.log('❌ 无法获取认证token测试终止');
return;
}
// 测试设备API
await testDevicesAPI();
}
// 运行测试
if (require.main === module) {
runTest().then(() => {
console.log('\n测试完成');
process.exit(0);
}).catch((error) => {
console.error('测试失败:', error);
process.exit(1);
});
}
module.exports = { runTest };

View File

@@ -0,0 +1,79 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>百度地图API测试</title>
<style>
#map {
width: 100%;
height: 400px;
border: 1px solid #ccc;
}
#log {
margin-top: 20px;
padding: 10px;
background: #f5f5f5;
border: 1px solid #ddd;
white-space: pre-wrap;
font-family: monospace;
}
</style>
</head>
<body>
<h1>百度地图API测试</h1>
<div id="map"></div>
<div id="log"></div>
<script>
const log = document.getElementById('log');
function addLog(message) {
log.textContent += new Date().toLocaleTimeString() + ': ' + message + '\n';
console.log(message);
}
// 测试API密钥
const API_KEY = '3AN3VahoqaXUs32U8luXD2Dwn86KK5B7';
addLog('使用API密钥: ' + API_KEY);
// 创建全局回调函数
window.initBMap = function() {
addLog('百度地图API加载成功');
addLog('BMap对象类型: ' + typeof window.BMap);
addLog('BMap.Map是否存在: ' + typeof window.BMap.Map);
try {
// 创建地图实例
const map = new BMap.Map("map");
addLog('地图实例创建成功');
// 设置中心点
const point = new BMap.Point(106.27, 38.47); // 宁夏中心
map.centerAndZoom(point, 8);
addLog('地图中心点设置成功');
// 添加控件
map.addControl(new BMap.MapTypeControl());
map.addControl(new BMap.NavigationControl());
addLog('地图控件添加成功');
addLog('地图初始化完成!');
} catch (error) {
addLog('地图创建失败: ' + error.message);
}
};
// 加载百度地图API
addLog('开始加载百度地图API...');
const script = document.createElement('script');
script.type = 'text/javascript';
script.src = `https://api.map.baidu.com/api?v=3.0&ak=${API_KEY}&callback=initBMap`;
script.onerror = function(error) {
addLog('脚本加载失败: ' + error);
};
addLog('API URL: ' + script.src);
document.head.appendChild(script);
</script>
</body>
</html>

View File

@@ -0,0 +1,67 @@
/**
* 测试前端用户管理功能
*/
const axios = require('axios');
// 模拟localStorage
const mockLocalStorage = {
getItem: (key) => {
const storage = {
'token': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwidXNlcm5hbWUiOiJhZG1pbiIsImVtYWlsIjoiYWRtaW5Abnh4bWRhdGEuY29tIiwiaWF0IjoxNzU2MTAyNzU2LCJleHAiOjE3NTYxODkxNTZ9.2Pq25hFiMiTyWB-GBdS5vIXOhI2He9oxjcuSDAytV50'
};
return storage[key] || null;
}
};
// 测试用户API调用
async function testUsersAPI() {
try {
console.log('测试前端用户API调用...');
console.log('=' .repeat(50));
// 模拟前端API调用
const API_BASE_URL = 'http://localhost:5350/api';
const token = mockLocalStorage.getItem('token');
console.log('Token存在:', !!token);
console.log('Token长度:', token ? token.length : 0);
if (!token) {
console.log('❌ 没有找到认证token');
return;
}
// 调用用户API
const response = await axios.get(`${API_BASE_URL}/users`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
console.log('API响应状态:', response.status);
console.log('API响应成功:', response.data.success);
console.log('用户数据数量:', response.data.data ? response.data.data.length : 0);
if (response.data.success && response.data.data) {
console.log('✅ 前端可以正常获取用户数据');
console.log('用户列表:');
response.data.data.forEach((user, index) => {
console.log(` ${index + 1}. ${user.username} (${user.email}) - 角色: ${user.role}`);
});
} else {
console.log('❌ API返回数据格式异常');
console.log('响应数据:', JSON.stringify(response.data, null, 2));
}
} catch (error) {
console.log('❌ 前端API调用失败:', error.message);
if (error.response) {
console.log('错误状态码:', error.response.status);
console.log('错误响应:', error.response.data);
}
}
}
// 运行测试
testUsersAPI().catch(console.error);

View File

@@ -0,0 +1,18 @@
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
port: 5300,
host: '0.0.0.0',
historyApiFallback: true,
proxy: {
'/api': {
target: 'http://localhost:5350',
changeOrigin: true
}
}
}
})