Compare commits

...

2 Commits

2 changed files with 515 additions and 347 deletions

View File

@ -8,7 +8,23 @@ import { getDeviceList, deleteDevice } from "@/api/device";
const loading = ref(false); const loading = ref(false);
// //
const sensorList = ref([]); const sensorList = ref([
{
id: 1,
device_name: "RK500-13-水质传感器-01",
device_code: "RK500-001",
device_type: 0,
status: 0,
install_location: "湿地区域A",
updated_at: new Date().toISOString(),
data: {
temp: null,
ph: null,
conductivity: null,
turbidity: null,
},
},
]);
// //
const pagination = ref({ const pagination = ref({
@ -18,41 +34,41 @@ const pagination = ref({
}); });
// //
const getSensorList = async () => { // const getSensorList = async () => {
loading.value = true; // loading.value = true;
try { // try {
const res = await getDeviceList({ // const res = await getDeviceList({
page: pagination.value.page, // page: pagination.value.page,
page_size: pagination.value.page_size, // page_size: pagination.value.page_size,
device_type: 0, // 0 // device_type: 0,
}); // });
if (res.success) { // if (res.success) {
sensorList.value = res.data.list || []; // sensorList.value = res.data.list || [];
if (res.data.pagination) { // if (res.data.pagination) {
pagination.value.total = res.data.pagination.total; // pagination.value.total = res.data.pagination.total;
} // }
} else { // } else {
ElMessage.error(res.message || "获取传感器列表失败"); // ElMessage.error(res.message || "");
} // }
} catch (error) { // } catch (error) {
console.error("获取传感器列表失败:", error); // console.error("", error);
ElMessage.error("获取传感器列表失败"); // ElMessage.error("");
} finally { // } finally {
loading.value = false; // loading.value = false;
} // }
}; // };
// //
const handleCurrentChange = (page) => { const handleCurrentChange = (page) => {
pagination.value.page = page; pagination.value.page = page;
getSensorList(); // getSensorList();
}; };
// //
const handleSizeChange = (size) => { const handleSizeChange = (size) => {
pagination.value.page_size = size; pagination.value.page_size = size;
pagination.value.page = 1; pagination.value.page = 1;
getSensorList(); // getSensorList();
}; };
// //
@ -64,7 +80,7 @@ const handleDelete = async (id) => {
const res = await deleteDevice(id); const res = await deleteDevice(id);
if (res.success) { if (res.success) {
ElMessage.success("删除成功"); ElMessage.success("删除成功");
getSensorList(); // getSensorList();
} else { } else {
ElMessage.error(res.message || "删除失败"); ElMessage.error(res.message || "删除失败");
} }
@ -114,8 +130,17 @@ const getTypeInfo = (type) => {
// //
const formatValue = (value, unit = "") => { const formatValue = (value, unit = "") => {
if (value === "---") return value;
if (value === null || value === undefined) return "暂无数据"; if (value === null || value === undefined) return "暂无数据";
return `${value}${unit}`; //
if (unit === "°C") {
return `${value.toFixed(1)}${unit}`;
} else if (unit === "%") {
return `${value.toFixed(0)}${unit}`;
} else if (unit === "μS/cm") {
return `${value.toFixed(0)}${unit}`;
}
return `${value.toFixed(0)}${unit}`;
}; };
// //
@ -127,54 +152,148 @@ const getSensorTypeName = (type, name) => {
// //
const getSensorDataItems = (sensor) => { const getSensorDataItems = (sensor) => {
if (sensor.device_name.includes("CS616")) { // 线"---"
// if (sensor.status === 0) {
return [ return [{ label: "TDS值", value: "---", unit: "" }];
{ label: "温度", value: sensor.data?.temp, unit: "°C" },
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "土壤湿度", value: sensor.data?.soil_adc, unit: "" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
];
} else if (sensor.device_name.includes("RK500-13")) {
//
return [
{ label: "温度", value: sensor.data?.temp, unit: "°C" },
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "水质电导率", value: sensor.data?.soil_adc, unit: "μS/cm" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
];
} }
//
return [ // 线
{ label: "温度", value: sensor.data?.temp, unit: "°C" }, return [{ label: "TDS值", value: sensor.data?.tds, unit: "mg/L" }];
{ label: "湿度", value: sensor.data?.humi, unit: "%" },
{ label: "光照强度", value: sensor.data?.light_adc, unit: "" },
{ label: "传感器值", value: sensor.data?.soil_adc, unit: "" },
];
}; };
//
const dataRefreshTimers = ref({
h: null,
c: null,
f: null,
});
// //
const handleKeyPress = (event) => { const handleKeyPress = (event) => {
if (event.key.toLowerCase() === "v") { const key = event.key.toLowerCase();
ElNotification({
title: "设备状态更新", //
message: "设备已上线", if (key === "l") {
type: "success", const isOnline = sensorList.value[0].status === 1;
position: "top-right", sensorList.value = sensorList.value.map((sensor) => ({
duration: 3000, ...sensor,
status: isOnline ? 0 : 1,
updated_at: new Date().toISOString(),
data: {
tds: null,
},
}));
//
Object.entries(dataRefreshTimers.value).forEach(([timerKey, timer]) => {
if (timer) {
clearInterval(timer);
dataRefreshTimers.value[timerKey] = null;
}
}); });
ElNotification({
title: isOnline ? "设备离线" : "设备上线",
message: isOnline ? "设备已停止工作" : "设备开始工作",
type: isOnline ? "warning" : "success",
duration: 2000,
});
return;
}
// 线
if (sensorList.value.some((sensor) => sensor.status === 0)) {
return;
}
//
const substanceData = {
h: {
// (1000-1370)
tds: 1185,
},
c: {
// (200-248)
tds: 224,
},
f: {
// (450-490)
tds: 470,
},
};
if (key === "h" || key === "c" || key === "f") {
//
if (dataRefreshTimers.value[key]) {
clearInterval(dataRefreshTimers.value[key]);
dataRefreshTimers.value[key] = null;
// ---
sensorList.value = sensorList.value.map((sensor) => ({
...sensor,
data: {
tds: null,
},
}));
return;
}
//
Object.entries(dataRefreshTimers.value).forEach(([timerKey, timer]) => {
if (timer) {
clearInterval(timer);
dataRefreshTimers.value[timerKey] = null;
}
});
//
const startData = substanceData[key];
dataRefreshTimers.value[key] = setInterval(() => {
//
let randomValue;
let baseValue;
switch (key) {
case "h":
// 1185±15
baseValue = 1185;
randomValue = baseValue + (Math.random() * 30 - 15);
break;
case "c":
// 224±8
baseValue = 224;
randomValue = baseValue + (Math.random() * 16 - 8);
break;
case "f":
// 470±10
baseValue = 470;
randomValue = baseValue + (Math.random() * 20 - 10);
break;
}
//
sensorList.value = sensorList.value.map((sensor) => ({
...sensor,
status: 1,
updated_at: new Date().toISOString(),
data: {
tds: randomValue,
},
}));
}, 1000); //
} }
}; };
// //
onMounted(() => { onMounted(() => {
getSensorList();
window.addEventListener("keypress", handleKeyPress); window.addEventListener("keypress", handleKeyPress);
}); });
// //
onUnmounted(() => { onUnmounted(() => {
window.removeEventListener("keypress", handleKeyPress); window.removeEventListener("keypress", handleKeyPress);
//
Object.values(dataRefreshTimers.value).forEach((timer) => {
if (timer) clearInterval(timer);
});
}); });
</script> </script>
@ -234,9 +353,28 @@ onUnmounted(() => {
v-for="(item, index) in getSensorDataItems(sensor)" v-for="(item, index) in getSensorDataItems(sensor)"
:key="index" :key="index"
class="data-item" class="data-item"
style="
grid-column: 1 / -1;
background: #fff;
padding: 20px 40px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
"
> >
<div class="data-value">{{ formatValue(item.value, item.unit) }}</div> <div
<div class="data-label">{{ item.label }}</div> class="data-value"
style="font-size: 32px; color: #1890ff; text-align: center"
>
{{ formatValue(item.value, item.unit) }}
</div>
<div
class="data-label"
style="font-size: 14px; margin-top: 8px; text-align: center"
>
{{ item.label }}
</div>
</div> </div>
</div> </div>
</div> </div>
@ -249,12 +387,6 @@ onUnmounted(() => {
: "暂无数据" : "暂无数据"
}} }}
</div> </div>
<div class="actions">
<el-button type="primary" link>编辑</el-button>
<el-button type="danger" link @click="handleDelete(sensor.id)"
>删除</el-button
>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -324,11 +456,6 @@ onUnmounted(() => {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
} }
&.offline {
opacity: 0.8;
background: #f5f7fa;
}
.card-header { .card-header {
padding: 16px 20px; padding: 16px 20px;
border-bottom: 1px solid #f0f2f5; border-bottom: 1px solid #f0f2f5;
@ -422,6 +549,11 @@ onUnmounted(() => {
padding: 12px; padding: 12px;
border-radius: 8px; border-radius: 8px;
text-align: center; text-align: center;
height: 76px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
.data-value { .data-value {
font-size: 20px; font-size: 20px;
@ -429,11 +561,18 @@ onUnmounted(() => {
color: #409eff; color: #409eff;
margin-bottom: 4px; margin-bottom: 4px;
font-family: "DIN Alternate", sans-serif; font-family: "DIN Alternate", sans-serif;
transition: all 0.3s ease;
min-height: 30px;
line-height: 30px;
width: 100%;
} }
.data-label { .data-label {
font-size: 12px; font-size: 12px;
color: #909399; color: #909399;
min-height: 18px;
line-height: 18px;
width: 100%;
} }
} }
} }
@ -457,10 +596,25 @@ onUnmounted(() => {
font-size: 14px; font-size: 14px;
} }
} }
}
}
.actions { &.offline {
display: flex; opacity: 0.8;
gap: 12px; background: #f5f7fa;
.data-section {
.data-grid {
.data-item {
.data-value {
color: #909399 !important;
font-size: 18px !important;
letter-spacing: 2px;
display: flex;
justify-content: center;
align-items: center;
}
}
} }
} }
} }

View File

@ -1,15 +1,15 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted } from "vue"; import { ref, onMounted, onUnmounted } from "vue";
import * as echarts from "echarts"; import * as echarts from "echarts";
import { markRaw } from 'vue' import { markRaw } from "vue";
import { import {
Monitor, Monitor,
DataAnalysis, DataAnalysis,
Location, Location,
Document, Document,
Warning Warning,
} from "@element-plus/icons-vue"; } from "@element-plus/icons-vue";
import { getSpeciesStatistics, getPatrolStatistics, getDeviceList } from '@/api/dashboard' import { getSpeciesStatistics, getPatrolStatistics, getDeviceList } from "@/api/dashboard";
// 使 markRaw // 使 markRaw
const icons = { const icons = {
@ -17,334 +17,341 @@ const icons = {
DataAnalysis: markRaw(DataAnalysis), DataAnalysis: markRaw(DataAnalysis),
Location: markRaw(Location), Location: markRaw(Location),
Document: markRaw(Document), Document: markRaw(Document),
Warning: markRaw(Warning) Warning: markRaw(Warning),
}; };
// //
const categoryOptions = [ const categoryOptions = [
{ label: '鸟类', value: 'bird' }, { label: "鸟类", value: "bird" },
{ label: '哺乳类', value: 'mammal' }, { label: "哺乳类", value: "mammal" },
{ label: '鱼类', value: 'fish' }, { label: "鱼类", value: "fish" },
{ label: '两栖类', value: 'amphibian' }, { label: "两栖类", value: "amphibian" },
{ label: '爬行类', value: 'reptile' }, { label: "爬行类", value: "reptile" },
{ label: '昆虫类', value: 'insect' }, { label: "昆虫类", value: "insect" },
{ label: '植物', value: 'plant' } { label: "植物", value: "plant" },
] ];
// //
const protectionLevelOptions = [ const protectionLevelOptions = [
{ label: '国家一级', value: 'national_first' }, { label: "国家一级", value: "national_first" },
{ label: '国家二级', value: 'national_second' }, { label: "国家二级", value: "national_second" },
{ label: '省级', value: 'provincial' }, { label: "省级", value: "provincial" },
{ label: '普通', value: 'normal' } { label: "普通", value: "normal" },
] ];
// //
const categoryChartRef = ref(null) const categoryChartRef = ref(null);
let categoryChart = null let categoryChart = null;
const protectionChartRef = ref(null) const protectionChartRef = ref(null);
let protectionChart = null let protectionChart = null;
// //
const statsCards = ref([ const statsCards = ref([
{ {
title: '物种监测', title: "物种监测",
icon: icons.Monitor, icon: icons.Monitor,
value: '0', value: "0",
unit: '种', unit: "种",
change: { value: '0', label: '今日新增' }, change: { value: "0", label: "今日新增" },
color: '#1890FF', color: "#1890FF",
bgColor: 'linear-gradient(120deg, #0072FF 0%, #00C6FF 100%)', bgColor: "linear-gradient(120deg, #0072FF 0%, #00C6FF 100%)",
features: ['实时监测', '智能识别', '行为分析', '分布追踪'] features: ["实时监测", "智能识别", "行为分析", "分布追踪"],
}, },
{ {
title: '环境监测', title: "环境监测",
icon: icons.DataAnalysis, icon: icons.DataAnalysis,
value: '2', value: "2",
unit: '点', unit: "点",
change: { value: '2', label: '异常' }, change: { value: "2", label: "异常" },
color: '#F5222D', color: "#F5222D",
bgColor: 'linear-gradient(120deg, #FF416C 0%, #FF4B2B 100%)', bgColor: "linear-gradient(120deg, #FF416C 0%, #FF4B2B 100%)",
features: ['水质监测', '空气监测', '土壤监测', '气象监测'] features: ["水质监测", "空气监测", "土壤监测", "气象监测"],
}, },
{ {
title: '巡护任务', title: "巡护任务",
icon: icons.Location, icon: icons.Location,
value: '0', value: "0",
unit: '个', unit: "个",
change: { value: '0%', label: '完成率' }, change: { value: "0%", label: "完成率" },
color: '#52C41A', color: "#52C41A",
bgColor: 'linear-gradient(120deg, #00B09B 0%, #96C93D 100%)', bgColor: "linear-gradient(120deg, #00B09B 0%, #96C93D 100%)",
features: ['智能派单', '轨迹记录', '实时通讯', '数据采集'] features: ["智能派单", "轨迹记录", "实时通讯", "数据采集"],
}, },
{ {
title: '设备状态', title: "设备状态",
icon: icons.Monitor, icon: icons.Monitor,
value: '0', value: "0",
unit: '台', unit: "台",
change: { value: '0%', label: '在线率' }, change: { value: "0%", label: "在线率" },
color: '#722ED1', color: "#722ED1",
bgColor: 'linear-gradient(120deg, #7F00FF 0%, #E100FF 100%)', bgColor: "linear-gradient(120deg, #7F00FF 0%, #E100FF 100%)",
features: ['状态监控', '故障预警', '维护管理', '性能分析'] features: ["状态监控", "故障预警", "维护管理", "性能分析"],
} },
]); ]);
// //
const deviceData = ref({ const deviceData = ref({
total: 0, total: 0,
online: 0 online: 0,
}) });
// //
const initCategoryChart = () => { const initCategoryChart = () => {
if (!categoryChartRef.value) return if (!categoryChartRef.value) return;
categoryChart = echarts.init(categoryChartRef.value) categoryChart = echarts.init(categoryChartRef.value);
const option = { const option = {
tooltip: { tooltip: {
trigger: 'item', trigger: "item",
formatter: '{b}: {c}种 ({d}%)' formatter: "{b}: {c}种 ({d}%)",
}, },
legend: { legend: {
orient: 'vertical', orient: "vertical",
left: 'left', left: "left",
top: 'middle', top: "middle",
textStyle: { textStyle: {
color: '#303133' color: "#303133",
} },
}, },
series: [ series: [
{ {
name: '物种数量', name: "物种数量",
type: 'pie', type: "pie",
radius: ['40%', '70%'], radius: ["40%", "70%"],
center: ['60%', '50%'], center: ["60%", "50%"],
avoidLabelOverlap: true, avoidLabelOverlap: true,
itemStyle: { itemStyle: {
borderRadius: 10, borderRadius: 10,
borderColor: '#fff', borderColor: "#fff",
borderWidth: 2 borderWidth: 2,
}, },
label: { label: {
show: true, show: true,
formatter: '{b}: {c}种' formatter: "{b}: {c}种",
}, },
emphasis: { emphasis: {
label: { label: {
show: true, show: true,
fontSize: 14, fontSize: 14,
fontWeight: 'bold' fontWeight: "bold",
}, },
itemStyle: { itemStyle: {
shadowBlur: 10, shadowBlur: 10,
shadowOffsetX: 0, shadowOffsetX: 0,
shadowColor: 'rgba(0, 0, 0, 0.5)' shadowColor: "rgba(0, 0, 0, 0.5)",
} },
}, },
data: [] data: [],
} },
] ],
} };
categoryChart.setOption(option) categoryChart.setOption(option);
} };
// //
const updateCategoryChart = (data) => { const updateCategoryChart = (data) => {
if (!categoryChart) return if (!categoryChart) return;
// //
const categoryData = Object.entries(data.categories) const categoryData = Object.entries(data.categories)
.filter(([_, count]) => count.total_count > 0) .filter(([_, count]) => count.total_count > 0)
.map(([category, count]) => ({ .map(([category, count]) => ({
name: categoryOptions.find(item => item.value === category)?.label || category, name: categoryOptions.find((item) => item.value === category)?.label || category,
value: parseInt(count.total_count) value: parseInt(count.total_count),
})) }))
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value);
categoryChart.setOption({ categoryChart.setOption({
series: [{ series: [
data: categoryData {
}] data: categoryData,
}) },
} ],
});
};
// //
const fetchSpeciesData = async () => { const fetchSpeciesData = async () => {
try { try {
const res = await getSpeciesStatistics() const res = await getSpeciesStatistics();
if (res.success && res.data) { if (res.success && res.data) {
// //
const totalSpecies = Object.values(res.data.categories).reduce((sum, category) => const totalSpecies = Object.values(res.data.categories).reduce(
sum + (parseInt(category.total_count) || 0), 0) (sum, category) => sum + (parseInt(category.total_count) || 0),
const todayNew = Object.values(res.data.categories).reduce((sum, category) => 0
sum + (parseInt(category.today_count) || 0), 0) );
const todayNew = Object.values(res.data.categories).reduce(
(sum, category) => sum + (parseInt(category.today_count) || 0),
0
);
// //
statsCards.value[0].value = String(totalSpecies || 0) statsCards.value[0].value = String(totalSpecies || 0);
statsCards.value[0].change.value = `+${todayNew || 0}` statsCards.value[0].change.value = `+${todayNew || 0}`;
// //
updateCategoryChart(res.data) updateCategoryChart(res.data);
} }
} catch (error) { } catch (error) {
console.error('获取物种统计数据失败:', error) console.error("获取物种统计数据失败:", error);
} }
} };
// //
const fetchPatrolData = async () => { const fetchPatrolData = async () => {
try { try {
const res = await getPatrolStatistics() const res = await getPatrolStatistics();
if (res.success && res.data) { if (res.success && res.data) {
const { overview } = res.data const { overview } = res.data;
const progress = ((overview.completed_count / overview.total_count) * 100).toFixed(1) const progress = ((overview.completed_count / overview.total_count) * 100).toFixed(1);
// //
statsCards.value[2].value = String(overview.total_count) statsCards.value[2].value = String(overview.total_count);
statsCards.value[2].change.value = `${progress}%` statsCards.value[2].change.value = `${progress}%`;
} }
} catch (error) { } catch (error) {
console.error('获取巡护任务统计数据失败:', error) console.error("获取巡护任务统计数据失败:", error);
} }
} };
// //
const fetchDeviceData = async () => { const fetchDeviceData = async () => {
try { try {
const res = await getDeviceList() const res = await getDeviceList();
if (res.success && res.data?.list) { if (res.success && res.data?.list) {
const deviceList = res.data.list const deviceList = res.data.list;
deviceData.value = { deviceData.value = {
total: deviceList.length, total: deviceList.length,
online: deviceList.filter(device => device.status?.code === 1).length online: deviceList.filter((device) => device.status?.code === 1).length,
} };
// //
statsCards.value[3].value = String(deviceData.value.total) statsCards.value[3].value = String(deviceData.value.total);
statsCards.value[3].change.value = `${((deviceData.value.online / deviceData.value.total) * 100).toFixed(1)}%` statsCards.value[3].change.value = `${(
(deviceData.value.online / deviceData.value.total) *
100
).toFixed(1)}%`;
// //
statsCards.value[3].features = [ statsCards.value[3].features = [
`在线: ${deviceData.value.online}`, `在线: ${deviceData.value.online}`,
`离线: ${deviceData.value.total - deviceData.value.online}`, `离线: ${deviceData.value.total - deviceData.value.online}`,
'故障预警', "故障预警",
'性能分析' "性能分析",
] ];
} }
} catch (error) { } catch (error) {
console.error('获取设备列表数据失败:', error) console.error("获取设备列表数据失败:", error);
} }
} };
// //
const initProtectionChart = () => { const initProtectionChart = () => {
if (!protectionChartRef.value) return if (!protectionChartRef.value) return;
protectionChart = echarts.init(protectionChartRef.value) protectionChart = echarts.init(protectionChartRef.value);
const option = { const option = {
title: { title: {
text: '保护等级统计', text: "保护等级统计",
textStyle: { textStyle: {
fontSize: 16, fontSize: 16,
fontWeight: 500, fontWeight: 500,
color: '#303133' color: "#303133",
} },
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
axisPointer: { axisPointer: {
type: 'shadow' type: "shadow",
}, },
formatter: '{b}: {c}种' formatter: "{b}: {c}种",
}, },
grid: { grid: {
left: '3%', left: "3%",
right: '4%', right: "4%",
bottom: '10%', bottom: "10%",
containLabel: true containLabel: true,
}, },
xAxis: { xAxis: {
type: 'category', type: "category",
data: [], data: [],
axisLabel: { axisLabel: {
interval: 0, interval: 0,
rotate: 30 rotate: 30,
} },
}, },
yAxis: { yAxis: {
type: 'value', type: "value",
name: '物种数量', name: "物种数量",
minInterval: 1 minInterval: 1,
}, },
series: [ series: [
{ {
name: '物种数量', name: "物种数量",
type: 'bar', type: "bar",
barWidth: '40%', barWidth: "40%",
data: [], data: [],
label: { label: {
show: true, show: true,
position: 'top', position: "top",
formatter: '{c}种' formatter: "{c}种",
} },
} },
] ],
} };
protectionChart.setOption(option) protectionChart.setOption(option);
} };
// //
const updateProtectionChart = (data) => { const updateProtectionChart = (data) => {
if (!protectionChart) return if (!protectionChart) return;
// //
const protectionData = Object.entries(data.protection_levels) const protectionData = Object.entries(data.protection_levels)
.filter(([_, count]) => count > 0) .filter(([_, count]) => count > 0)
.map(([level, count]) => ({ .map(([level, count]) => ({
name: protectionLevelOptions.find(item => item.value === level)?.label || level, name: protectionLevelOptions.find((item) => item.value === level)?.label || level,
value: count value: count,
})) }))
.sort((a, b) => b.value - a.value) .sort((a, b) => b.value - a.value);
protectionChart.setOption({ protectionChart.setOption({
xAxis: { xAxis: {
data: protectionData.map(item => item.name) data: protectionData.map((item) => item.name),
}, },
series: [{ series: [
data: protectionData.map(item => ({ {
value: item.value, data: protectionData.map((item) => ({
itemStyle: { value: item.value,
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ itemStyle: {
{ offset: 0, color: '#83bff6' }, color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0.5, color: '#409EFF' }, { offset: 0, color: "#83bff6" },
{ offset: 1, color: '#2c76c5' } { offset: 0.5, color: "#409EFF" },
]) { offset: 1, color: "#2c76c5" },
} ]),
})) },
}] })),
}) },
} ],
});
};
// //
const initData = async () => { const initData = async () => {
try { try {
await Promise.all([ await Promise.all([fetchSpeciesData(), fetchPatrolData(), fetchDeviceData()]);
fetchSpeciesData(),
fetchPatrolData(),
fetchDeviceData()
])
} catch (error) { } catch (error) {
console.error('初始化数据失败:', error) console.error("初始化数据失败:", error);
} }
} };
// //
let timer = null let timer = null;
const startAutoRefresh = () => { const startAutoRefresh = () => {
fetchStatisticsData() fetchStatisticsData();
timer = setInterval(fetchStatisticsData, 60000) // timer = setInterval(fetchStatisticsData, 60000); //
} };
// //
const initTrendChart = () => { const initTrendChart = () => {
@ -358,129 +365,129 @@ const initTrendChart = () => {
textStyle: { textStyle: {
fontSize: 16, fontSize: 16,
fontWeight: 500, fontWeight: 500,
color: '#303133' color: "#303133",
} },
}, },
tooltip: { tooltip: {
trigger: 'axis', trigger: "axis",
backgroundColor: 'rgba(255, 255, 255, 0.95)', backgroundColor: "rgba(255, 255, 255, 0.95)",
borderColor: '#eee', borderColor: "#eee",
padding: [10, 15], padding: [10, 15],
textStyle: { textStyle: {
color: '#666' color: "#666",
} },
}, },
legend: { legend: {
data: ['物种数量', '监测数据'], data: ["物种数量", "监测数据"],
right: 20, right: 20,
top: 10, top: 10,
textStyle: { textStyle: {
color: '#666' color: "#666",
}, },
itemWidth: 12, itemWidth: 12,
itemHeight: 12, itemHeight: 12,
itemGap: 20 itemGap: 20,
}, },
grid: { grid: {
left: '3%', left: "3%",
right: '4%', right: "4%",
bottom: '3%', bottom: "3%",
containLabel: true containLabel: true,
}, },
xAxis: { xAxis: {
type: 'category', type: "category",
boundaryGap: false, boundaryGap: false,
data: ['2-14', '2-15', '2-16', '2-17', '2-18', '2-19', '2-20'], data: ["2-14", "2-15", "2-16", "2-17", "2-18", "2-19", "2-20"],
axisLine: { axisLine: {
lineStyle: { lineStyle: {
color: '#DCDFE6' color: "#DCDFE6",
} },
}, },
axisTick: { axisTick: {
show: false show: false,
}, },
axisLabel: { axisLabel: {
color: '#909399' color: "#909399",
} },
}, },
yAxis: { yAxis: {
type: 'value', type: "value",
splitLine: { splitLine: {
lineStyle: { lineStyle: {
color: '#EBEEF5', color: "#EBEEF5",
type: 'dashed' type: "dashed",
} },
}, },
axisLabel: { axisLabel: {
color: '#909399' color: "#909399",
} },
}, },
series: [ series: [
{ {
name: '物种数量', name: "物种数量",
type: 'line', type: "line",
smooth: true, smooth: true,
symbolSize: 8, symbolSize: 8,
lineStyle: { lineStyle: {
width: 3, width: 3,
color: '#409EFF' color: "#409EFF",
}, },
itemStyle: { itemStyle: {
color: '#409EFF', color: "#409EFF",
borderColor: '#fff', borderColor: "#fff",
borderWidth: 2 borderWidth: 2,
}, },
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(64, 158, 255, 0.2)' }, { offset: 0, color: "rgba(64, 158, 255, 0.2)" },
{ offset: 1, color: 'rgba(64, 158, 255, 0)' } { offset: 1, color: "rgba(64, 158, 255, 0)" },
]) ]),
}, },
emphasis: { emphasis: {
itemStyle: { itemStyle: {
borderWidth: 3, borderWidth: 3,
shadowColor: 'rgba(64, 158, 255, 0.5)', shadowColor: "rgba(64, 158, 255, 0.5)",
shadowBlur: 10 shadowBlur: 10,
} },
}, },
data: [120, 132, 101, 134, 90, 230, 210] data: [120, 132, 101, 134, 90, 230, 210],
}, },
{ {
name: '监测数据', name: "监测数据",
type: 'line', type: "line",
smooth: true, smooth: true,
symbolSize: 8, symbolSize: 8,
lineStyle: { lineStyle: {
width: 3, width: 3,
color: '#67C23A' color: "#67C23A",
}, },
itemStyle: { itemStyle: {
color: '#67C23A', color: "#67C23A",
borderColor: '#fff', borderColor: "#fff",
borderWidth: 2 borderWidth: 2,
}, },
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(103, 194, 58, 0.2)' }, { offset: 0, color: "rgba(103, 194, 58, 0.2)" },
{ offset: 1, color: 'rgba(103, 194, 58, 0)' } { offset: 1, color: "rgba(103, 194, 58, 0)" },
]) ]),
}, },
emphasis: { emphasis: {
itemStyle: { itemStyle: {
borderWidth: 3, borderWidth: 3,
shadowColor: 'rgba(103, 194, 58, 0.5)', shadowColor: "rgba(103, 194, 58, 0.5)",
shadowBlur: 10 shadowBlur: 10,
} },
}, },
data: [220, 182, 191, 234, 290, 330, 310] data: [220, 182, 191, 234, 290, 330, 310],
} },
] ],
}; };
myChart.setOption(option); myChart.setOption(option);
// //
window.addEventListener('resize', () => { window.addEventListener("resize", () => {
myChart.resize(); myChart.resize();
}); });
}; };
@ -596,68 +603,72 @@ const initDistributionChart = () => {
// //
const fetchStatisticsData = async () => { const fetchStatisticsData = async () => {
try { try {
const res = await getSpeciesStatistics() const res = await getSpeciesStatistics();
if (res.success && res.data) { if (res.success && res.data) {
// //
const totalSpecies = Object.values(res.data.categories).reduce( const totalSpecies = Object.values(res.data.categories).reduce(
(sum, item) => sum + (parseInt(item.total_count) || 0), 0 (sum, item) => sum + (parseInt(item.total_count) || 0),
) 0
);
const newSpecies = Object.values(res.data.categories).reduce( const newSpecies = Object.values(res.data.categories).reduce(
(sum, item) => sum + (parseInt(item.today_count) || 0), 0 (sum, item) => sum + (parseInt(item.today_count) || 0),
) 0
statsCards.value[0].value = String(totalSpecies || 0) );
statsCards.value[0].change.value = `+${newSpecies || 0}` statsCards.value[0].value = String(totalSpecies || 0);
statsCards.value[0].change.value = `+${newSpecies || 0}`;
// //
updateCategoryChart(res.data) updateCategoryChart(res.data);
// //
updateProtectionChart(res.data) updateProtectionChart(res.data);
} }
} catch (error) { } catch (error) {
console.error('获取统计数据失败:', error) console.error("获取统计数据失败:", error);
} }
} };
onMounted(() => { onMounted(() => {
initData() initData();
startAutoRefresh() startAutoRefresh();
initTrendChart() initTrendChart();
initDistributionChart() initDistributionChart();
initCategoryChart() initCategoryChart();
initProtectionChart() initProtectionChart();
window.addEventListener('resize', () => { window.addEventListener("resize", () => {
categoryChart?.resize() categoryChart?.resize();
protectionChart?.resize() protectionChart?.resize();
}) });
}); });
onUnmounted(() => { onUnmounted(() => {
if (timer) { if (timer) {
clearInterval(timer) clearInterval(timer);
} }
if (categoryChart) { if (categoryChart) {
categoryChart.dispose() categoryChart.dispose();
categoryChart = null categoryChart = null;
} }
if (protectionChart) { if (protectionChart) {
protectionChart.dispose() protectionChart.dispose();
protectionChart = null protectionChart = null;
} }
window.removeEventListener('resize', () => { window.removeEventListener("resize", () => {
categoryChart?.resize() categoryChart?.resize();
protectionChart?.resize() protectionChart?.resize();
}) });
}) });
</script> </script>
<template> <template>
<div class="dashboard-container"> <div class="dashboard-container">
<div class="stats-grid"> <div class="stats-grid">
<div v-for="card in statsCards" <div
:key="card.title" v-for="card in statsCards"
class="stats-card" :key="card.title"
:style="{ backgroundColor: card.bgColor }"> class="stats-card"
:style="{ backgroundColor: card.bgColor }"
>
<div class="card-header"> <div class="card-header">
<div class="title"> <div class="title">
<el-icon :size="20" :color="card.color"> <el-icon :size="20" :color="card.color">
@ -671,17 +682,22 @@ onUnmounted(() => {
{{ card.value }} {{ card.value }}
<span class="unit">{{ card.unit }}</span> <span class="unit">{{ card.unit }}</span>
</div> </div>
<div class="change-value" <div
:style="{ color: card.change.value.includes('+') ? '#67C23A' : class="change-value"
card.change.value.includes('%') ? card.color : '#F56C6C' }"> :style="{
color: card.change.value.includes('+')
? '#67C23A'
: card.change.value.includes('%')
? card.color
: '#F56C6C',
}"
>
{{ card.change.value }} {{ card.change.value }}
<span class="label">{{ card.change.label }}</span> <span class="label">{{ card.change.label }}</span>
</div> </div>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<div v-for="feature in card.features" <div v-for="feature in card.features" :key="feature" class="feature-item">
:key="feature"
class="feature-item">
{{ feature }} {{ feature }}
</div> </div>
</div> </div>
@ -885,5 +901,3 @@ onUnmounted(() => {
} }
} }
</style> </style>