Update .gitignore and add files

This commit is contained in:
jrhlh
2025-07-17 23:13:04 +08:00
commit 39cedd4073
257 changed files with 34603 additions and 0 deletions

View File

@ -0,0 +1,993 @@
<template>
<div class="ai-alert-system">
<div class="alert-list">
<div class="list-header">
<h3 class="list-title">预警列表</h3>
<div class="list-actions">
<button class="action-btn" @click="refreshAlerts">
<RefreshCw :size="16" />
<span>刷新</span>
</button>
</div>
</div>
<div class="alerts-container">
<div
v-for="alert in alerts"
:key="alert.id"
class="alert-card"
:class="`alert-severity-${alert.severity}`"
@click="showAlertDetail(alert)"
>
<div class="alert-header">
<div class="severity-badge" :class="severityClass(alert.severity)">
<div class="severity-dot"></div>
<span class="severity-text">{{ severityText(alert.severity) }}</span>
</div>
<div class="alert-time">{{ formatDate(alert.time) }}</div>
</div>
<div class="alert-body">
<div class="alert-type">
<span class="type-icon">
<Thermometer v-if="alert.type === '温度'" :size="16" />
<Droplets v-else-if="alert.type === '土壤湿度'" :size="16" />
<Zap v-else :size="16" />
</span>
<span class="type-text">{{ alert.type }}</span>
<span class="current-value">{{ alert.currentValue }}</span>
<span class="separator">/</span>
<span class="threshold-value">阈值: {{ alert.threshold }}</span>
</div>
<!-- <div class="alert-values">-->
<!--&lt;!&ndash; <span class="current-value">{{ alert.currentValue }}</span>&ndash;&gt;-->
<!--&lt;!&ndash; <span class="separator">/</span>&ndash;&gt;-->
<!--&lt;!&ndash; <span class="threshold-value">阈值: {{ alert.threshold }}</span>&ndash;&gt;-->
<!-- </div>-->
<div class="alert-device">
<span class="device-label">设备:</span>
<span class="device-id">{{ alert.deviceId }}</span>
</div>
</div>
<div class="alert-footer">
<div class="status-badge" :class="`status-${getStatusClass(alert.status)}`">
<div class="status-dot"></div>
<span class="status-text">{{ alert.status }}</span>
</div>
<button class="detail-btn">
<ChevronRight :size="14" />
</button>
</div>
</div>
</div>
</div>
<!-- 预警详情模态框 -->
<div v-if="showDetail" class="modal-overlay" @click.self="closeModal">
<div class="modal-content">
<div class="modal-header">
<div class="modal-title">
<h3>预警详情</h3>
<div class="severity-badge" :class="severityClass(currentAlert?.severity || 1)">
<div class="severity-dot"></div>
<span class="severity-text">{{ severityText(currentAlert?.severity || 1) }}</span>
</div>
</div>
<button class="close-btn" @click="closeModal">
<X :size="20" />
</button>
</div>
<div class="modal-body">
<div class="detail-section">
<h4 class="section-title">基本信息</h4>
<div class="detail-grid">
<div class="detail-item">
<span class="detail-label">预警类型</span>
<span class="detail-value">{{ currentAlert?.type }}</span>
</div>
<div class="detail-item">
<span class="detail-label">设备编号</span>
<span class="detail-value">{{ currentAlert?.deviceId }}</span>
</div>
<div class="detail-item">
<span class="detail-label">当前值</span>
<span class="detail-value">{{ currentAlert?.currentValue }}</span>
</div>
<div class="detail-item">
<span class="detail-label">阈值</span>
<span class="detail-value">{{ currentAlert?.threshold }}</span>
</div>
<div class="detail-item">
<span class="detail-label">状态</span>
<span class="detail-value status-badge" :class="`status-${getStatusClass(currentAlert?.status || '')}`">
{{ currentAlert?.status }}
</span>
</div>
<div class="detail-item">
<span class="detail-label">发生时间</span>
<span class="detail-value">{{ formatDate(currentAlert?.time || new Date()) }}</span>
</div>
</div>
</div>
<div class="detail-section">
<h4 class="section-title">预警分析</h4>
<div class="analysis-content">
<div class="analysis-item">
<span class="analysis-label">预警原因</span>
<p class="analysis-text">{{ currentAlert?.reason }}</p>
</div>
<div class="analysis-item">
<span class="analysis-label">影响说明</span>
<p class="analysis-text">{{ currentAlert?.impact }}</p>
</div>
</div>
</div>
<div class="detail-section">
<h4 class="section-title">处理建议</h4>
<div class="suggestion-content">
<p class="suggestion-text">{{ currentAlert?.suggestion }}</p>
</div>
</div>
</div>
<div class="modal-footer">
<button class="btn-secondary" @click="closeModal">关闭</button>
<button
class="btn-primary"
@click="notifyResponsible"
:disabled="notifyLoading"
>
<Send v-if="!notifyLoading" :size="16" />
<Loader2 v-else :size="16" class="spinning" />
<span>{{ notifyLoading ? '发送中...' : '通知负责人' }}</span>
</button>
</div>
<div v-if="notifyMessage" class="notify-message" :class="{ success: notifySuccess, error: !notifySuccess }">
{{ notifyMessage }}
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import {
RefreshCw,
Thermometer,
Droplets,
Zap,
ChevronRight,
X,
Send,
Loader2
} from 'lucide-vue-next'
import axios from 'axios';
interface Alert {
id: number
type: string
severity: number // 1: 轻微, 2: 中等, 3: 严重
currentValue: string
threshold: string
status: string // '待处理' | '处理中' | '已解决'
time: Date
deviceId: string
reason: string
impact: string
suggestion: string
}
// 预警数据
const alerts = ref<Alert[]>([
{
id: 1,
type: '土壤湿度',
severity: 3,
currentValue: '91%',
threshold: '75%',
status: '待处理',
time: new Date(),
deviceId: 'DEV-2023-001',
reason: '土壤湿度过高,可能导致植物根部腐烂',
impact: '影响植物正常生长,可能导致作物减产',
suggestion: '1. 立即停止灌溉系统运行,检查灌溉设备阀门是否完全关闭\n2. 检查排水泵是否正常工作,清理排水管道堵塞物\n3. 启用土壤湿度监测设备每30分钟记录一次数据'
},
{
id: 2,
type: '温度',
severity: 2,
currentValue: '30°C',
threshold: '28°C',
status: '已解决',
time: new Date('2025-05-27 14:30'),
deviceId: 'DEV-2023-002',
reason: '环境温度超过阈值',
impact: '高温可能影响设备性能和作物生长',
suggestion: '1. 检查温度传感器是否正常工作,清洁传感器表面\n2. 开启智能通风系统,设置为自动模式\n3. 检查温控设备制冷模块,确认压缩机工作状态'
},
{
id: 3,
type: '温度',
severity: 1,
currentValue: '32°C',
threshold: '28°C',
status: '处理中',
time: new Date('2025-08-5 13:40'),
deviceId: 'DEV-2023-003',
reason: '温度略高于设定阈值',
impact: '轻微影响,需关注温度变化趋势',
suggestion: '1. 检查温度传感器校准状态,对比相邻设备数据\n2. 检查温控设备风扇是否正常运转,清理散热孔\n3. 调整智能遮阳设备角度,减少阳光直射'
}
])
// 弹窗相关状态
const showDetail = ref(false)
const currentAlert = ref<Alert | null>(null)
const notifyLoading = ref(false)
const notifyMessage = ref('')
const notifySuccess = ref(false)
const formatDate = (date: Date) => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
const hours = String(date.getHours()).padStart(2, '0')
const minutes = String(date.getMinutes()).padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
// 根据严重程度返回对应的样式类
const severityClass = (severity: number) => {
switch (severity) {
case 1: return 'severity-low'
case 2: return 'severity-medium'
case 3: return 'severity-high'
default: return 'severity-low'
}
}
// 根据严重程度返回对应的文本
const severityText = (severity: number) => {
switch (severity) {
case 1: return '轻微'
case 2: return '中等'
case 3: return '严重'
default: return '轻微'
}
}
// 获取状态样式类
const getStatusClass = (status: string) => {
switch (status) {
case '已解决': return 'resolved'
case '处理中': return 'processing'
case '待处理': return 'pending'
default: return 'pending'
}
}
// 显示预警详情
const showAlertDetail = (alert: Alert) => {
currentAlert.value = alert
showDetail.value = true
notifyMessage.value = ''
}
// 关闭弹窗
const closeModal = () => {
showDetail.value = false
currentAlert.value = null
}
// 刷新预警列表
const refreshAlerts = () => {
// 模拟刷新数据
console.log('刷新预警数据')
}
// 通知负责人
const notifyResponsible = async () => {
if (!currentAlert.value) return
notifyLoading.value = true
notifyMessage.value = ''
try {
const response = await axios.post(`http://localhost:5000/api/fault/notify/${currentAlert.value.id}`)
if (response.data.success) {
notifyMessage.value = '通知已成功发送给负责人'
notifySuccess.value = true
} else {
notifyMessage.value = response.data.message || '通知发送失败'
notifySuccess.value = false
}
} catch (error) {
console.error('通知失败:', error)
notifyMessage.value = '通知过程中发生错误'
notifySuccess.value = false
} finally {
notifyLoading.value = false
}
}
// 定时器引用
let timer: number | null = null
// 组件挂载时启动定时器
onMounted(() => {
// 每秒更新一次时间
timer = setInterval(() => {
alerts.value = alerts.value.map(alert => {
if (alert.status === '待处理') {
return {
...alert,
time: new Date(alert.time.getTime() + 1000)
}
}
return alert
})
}, 1000) as unknown as number
})
// 组件卸载时清除定时器
onUnmounted(() => {
if (timer) {
clearInterval(timer)
}
})
</script>
<style scoped>
.ai-alert-system {
display: flex;
flex-direction: column;
gap: var(--spacing-xl);
height: 100%;
}
/* 预警统计 */
.alert-stats {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: var(--spacing-md);
padding: var(--spacing-lg);
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid rgba(0, 0, 0, 0.06);
}
.stat-item {
display: flex;
flex-direction: column;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-md);
border-radius: var(--radius-md);
background: rgba(255, 255, 255, 0.5);
transition: var(--transition-fast);
}
.stat-item:hover {
background: rgba(255, 255, 255, 0.8);
transform: translateY(-2px);
}
.stat-number {
font-size: var(--font-size-2xl);
font-weight: 700;
color: var(--text-primary);
}
.stat-number.pending {
color: var(--error-color);
}
.stat-number.processing {
color: var(--warning-color);
}
.stat-number.resolved {
color: var(--success-color);
}
.stat-label {
font-size: var(--font-size-xs);
color: var(--text-secondary);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 预警列表 */
.alert-list {
flex: 1;
display: flex;
flex-direction: column;
background: var(--bg-card);
border-radius: var(--radius-lg);
border: 1px solid rgba(0, 0, 0, 0.06);
overflow: hidden;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-lg) var(--spacing-xl);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
background: rgba(255, 255, 255, 0.5);
}
.list-title {
font-size: 21.5px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.list-actions {
display: flex;
gap: var(--spacing-sm);
}
.action-btn {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-md);
font-size: var(--font-size-sm);
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition-fast);
font-weight: 500;
}
.action-btn:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}
/* 预警卡片 */
.alerts-container {
flex: 1;
padding: var(--spacing-lg);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
overflow-y: auto;
}
.alert-card {
background: rgba(240, 244, 248, 0.1);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid rgba(0, 0, 0, 0.06);
cursor: pointer;
transition: var(--transition-base);
position: relative;
overflow: hidden;
}
.alert-card::before {
content: '';
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 4px;
background: var(--warning-color);
transition: var(--transition-fast);
}
.alert-card.alert-severity-1::before {
background: var(--warning-color);
}
.alert-card.alert-severity-2::before {
background: var(--error-color);
}
.alert-card.alert-severity-3::before {
background: #ff1744;
}
.alert-card:hover {
transform: translateX(4px);
box-shadow: var(--shadow-md);
}
.alert-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-md);
}
.severity-badge {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-2xl);
font-size: var(--font-size-xs);
font-weight: 600;
}
.severity-badge.severity-low {
background: rgba(250, 173, 20, 0.1);
color: var(--warning-color);
}
.severity-badge.severity-medium {
background: rgba(255, 77, 79, 0.1);
color: var(--error-color);
}
.severity-badge.severity-high {
background: rgba(255, 23, 68, 0.1);
color: #ff1744;
}
.severity-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.severity-text {
text-transform: uppercase;
letter-spacing: 0.5px;
}
.alert-time {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.alert-body {
margin-bottom: var(--spacing-md);
}
.alert-type {
display: flex;
align-items: center;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-sm);
}
.type-icon {
color: var(--primary-color);
}
.type-text {
font-weight: 600;
color: var(--text-primary);
}
.alert-values {
display: flex;
align-items: center;
gap: var(--spacing-xs);
margin-bottom: var(--spacing-sm);
}
.current-value {
font-size: var(--font-size-lg);
font-weight: 700;
color: var(--text-primary);
}
.separator {
color: var(--text-tertiary);
}
.threshold-value {
font-size: 16px;
color: var(--text-secondary);
}
.alert-device {
display: flex;
align-items: center;
gap: var(--spacing-xs);
}
.device-label {
font-size: var(--font-size-xs);
color: var(--text-tertiary);
}
.device-id {
font-size: var(--font-size-xs);
font-weight: 500;
color: var(--text-secondary);
}
.alert-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.status-badge {
display: flex;
align-items: center;
gap: var(--spacing-xs);
padding: var(--spacing-xs) var(--spacing-sm);
border-radius: var(--radius-2xl);
font-size: var(--font-size-xs);
font-weight: 500;
}
.status-badge.status-pending {
background: rgba(255, 77, 79, 0.1);
color: var(--error-color);
}
.status-badge.status-processing {
background: rgba(250, 173, 20, 0.1);
color: var(--warning-color);
}
.status-badge.status-resolved {
background: rgba(82, 196, 26, 0.1);
color: var(--success-color);
}
.status-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: currentColor;
}
.detail-btn {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: var(--radius-sm);
background: rgba(0, 0, 0, 0.05);
border: none;
color: var(--text-tertiary);
cursor: pointer;
transition: var(--transition-fast);
}
.detail-btn:hover {
background: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
/* 模态框 */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: var(--z-index-modal);
backdrop-filter: blur(4px);
animation: fadeIn 0.3s ease;
}
.modal-content {
background: var(--bg-card);
border-radius: var(--radius-xl);
box-shadow: var(--shadow-xl);
max-width: 600px;
width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
animation: slideUp 0.3s ease;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--spacing-xl) var(--spacing-2xl);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
}
.modal-title {
display: flex;
align-items: center;
gap: var(--spacing-lg);
}
.modal-title h3 {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.close-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: var(--radius-md);
background: rgba(0, 0, 0, 0.05);
border: none;
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition-fast);
}
.close-btn:hover {
background: rgba(0, 0, 0, 0.1);
color: var(--text-primary);
}
.modal-body {
flex: 1;
padding: var(--spacing-2xl);
overflow-y: auto;
}
.detail-section {
margin-bottom: var(--spacing-2xl);
}
.section-title {
font-size: var(--font-size-lg);
font-weight: 600;
color: var(--text-primary);
margin: 0 0 var(--spacing-lg) 0;
}
.detail-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-lg);
}
.detail-item {
display: flex;
flex-direction: column;
gap: var(--spacing-xs);
}
.detail-label {
font-size: var(--font-size-sm);
color: var(--text-tertiary);
font-weight: 500;
}
.detail-value {
font-size: var(--font-size-sm);
color: var(--text-primary);
font-weight: 500;
}
.analysis-content {
display: flex;
flex-direction: column;
gap: var(--spacing-lg);
}
.analysis-item {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
.analysis-label {
font-size: var(--font-size-sm);
color: var(--text-secondary);
font-weight: 500;
}
.analysis-text {
font-size: var(--font-size-sm);
color: var(--text-primary);
line-height: 1.5;
margin: 0;
}
.suggestion-content {
background: rgba(24, 144, 255, 0.05);
border-radius: var(--radius-lg);
padding: var(--spacing-lg);
border: 1px solid rgba(24, 144, 255, 0.1);
}
.suggestion-text {
font-size: var(--font-size-sm);
color: var(--text-primary);
line-height: 1.6;
margin: 0;
white-space: pre-line;
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: var(--spacing-md);
padding: var(--spacing-xl) var(--spacing-2xl);
border-top: 1px solid rgba(0, 0, 0, 0.06);
}
.btn-secondary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: rgba(255, 255, 255, 0.8);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-secondary);
cursor: pointer;
transition: var(--transition-fast);
font-weight: 500;
}
.btn-secondary:hover {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(0, 0, 0, 0.15);
}
.btn-primary {
display: flex;
align-items: center;
gap: var(--spacing-xs);
background: var(--gradient-primary);
border: none;
border-radius: var(--radius-md);
padding: var(--spacing-sm) var(--spacing-lg);
font-size: var(--font-size-sm);
color: var(--text-white);
cursor: pointer;
transition: var(--transition-fast);
font-weight: 500;
}
.btn-primary:hover:not(:disabled) {
background: var(--gradient-primary-hover);
transform: translateY(-1px);
}
.btn-primary:disabled {
opacity: 0.7;
cursor: not-allowed;
}
.spinning {
animation: spin 1s linear infinite;
}
.notify-message {
position: absolute;
bottom: var(--spacing-lg);
left: 50%;
transform: translateX(-50%);
padding: var(--spacing-sm) var(--spacing-lg);
border-radius: var(--radius-lg);
font-size: var(--font-size-sm);
font-weight: 500;
animation: slideUp 0.3s ease;
}
.notify-message.success {
background: rgba(82, 196, 26, 0.1);
color: var(--success-color);
border: 1px solid rgba(82, 196, 26, 0.2);
}
.notify-message.error {
background: rgba(255, 77, 79, 0.1);
color: var(--error-color);
border: 1px solid rgba(255, 77, 79, 0.2);
}
/* 动画 */
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px) scale(0.95);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 响应式设计 */
@media (max-width: 768px) {
.alert-stats {
grid-template-columns: repeat(2, 1fr);
gap: var(--spacing-sm);
padding: var(--spacing-md);
}
.stat-number {
font-size: var(--font-size-xl);
}
.list-header {
flex-direction: column;
gap: var(--spacing-md);
align-items: stretch;
}
.detail-grid {
grid-template-columns: 1fr;
gap: var(--spacing-md);
}
.modal-content {
width: 95vw;
max-height: 95vh;
}
.modal-header {
padding: var(--spacing-lg) var(--spacing-xl);
}
.modal-body {
padding: var(--spacing-xl);
}
.modal-footer {
padding: var(--spacing-lg) var(--spacing-xl);
}
}
@media (max-width: 480px) {
.alert-stats {
grid-template-columns: 1fr;
}
.alert-card {
padding: var(--spacing-md);
}
.alert-header {
flex-direction: column;
gap: var(--spacing-sm);
align-items: flex-start;
}
.alert-footer {
flex-direction: column;
gap: var(--spacing-sm);
align-items: flex-start;
}
}
</style>

View File

@ -0,0 +1,531 @@
<template>
<div class="dashboard-page">
<!-- 统计卡片区域 -->
<div class="stats-section">
<StatCards
:user-count="0"
:device-count="0"
:temperature="0"
:fault-count="0"
:stats="statistics"
:loading="isStatsLoading"
/>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 环境监控区域 - 左列1 -->
<div class="monitoring-section">
<div class="section-header">
<h2 class="section-title">环境监控</h2>
<span class="section-badge">实时</span>
</div>
<div class="monitoring-grid">
<TemperatureMonitor
:device-id="'temp-001'"
:threshold="{ warning: 30, critical: 35 }"
@alert="handleTemperatureAlert"
@data-update="handleTemperatureUpdate"
/>
</div>
</div>
<!-- 设备运行状态区域 - 右列1 -->
<div class="device-status-section">
<div class="section-header">
<h2 class="section-title">设备运行状态</h2>
<span class="section-badge">监控</span>
</div>
<DeviceStatusCard />
</div>
<!-- 地图区域 - 跨两列 -->
<div class="map-section">
<div class="section-header">
<h2 class="section-title">设备地理分布</h2>
<span class="section-badge">地图</span>
</div>
<div class="under">
<iframe
src="/map_with_random_points.html"
width="100%"
height="100%"
></iframe>
</div>
</div>
<!-- 2:3比例区域容器 -->
<div class="ratio-section">
<!-- AI预警系统区域 - 占2份 -->
<div class="growth-section">
<div class="section-header">
<h2 class="section-title">AI预警系统</h2>
<span class="section-badge">分析</span>
</div>
<div class="body-foot">
<div class="body-under-box">
<div class="box002">
<AIyujing />
</div>
</div>
</div>
</div>
<!-- 生长趋势分析区域 - 占3份 -->
<div class="personnel-section">
<div class="section-header">
<h2 class="section-title">作物生长分析</h2>
<span class="section-badge">分析</span>
</div>
<div class="xx">
<Shengzhang />
</div>
</div>
</div>
</div>
<div>
<!-- 你的页面内容 -->
<askai />
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import AIyujing from "./AIyujing.vue";
import Shengzhang from "./shengzhang.vue";
import StatCards from '../components/dashboard/StatCards.vue';
import TemperatureMonitor from '../components/features/monitoring/TemperatureMonitor.vue';
import DeviceStatusCard from '../components/features/devices/DeviceStatusCard.vue';
import Askai from "../components1/askai.vue";
// 类型定义
interface Statistic {
title: string;
value: string | number;
change: string;
trend: 'up' | 'down' | 'stable';
icon: string;
}
interface Notification {
id: string;
type: 'success' | 'warning' | 'error' | 'info';
message: string;
icon: string;
timestamp: Date;
}
interface CurrentUser {
username: string;
permissionLevel: string;
}
// 响应式数据
const isRefreshing = ref(false);
const isExporting = ref(false);
const isStatsLoading = ref(false);
const notifications = ref<Notification[]>([]);
// 模拟当前用户
const currentUser: CurrentUser = {
username: 'admin001',
permissionLevel: 'Admin'
};
// 统计数据
const statistics = ref<Statistic[]>([
{
title: '设备总数',
value: 24,
change: '+12%',
trend: 'up',
icon: '📱'
},
{
title: '在线设备',
value: 22,
change: '+8%',
trend: 'up',
icon: '🟢'
},
{
title: '告警数量',
value: 3,
change: '-2',
trend: 'down',
icon: '⚠️'
},
{
title: '数据采集',
value: '2.4K',
change: '+15%',
trend: 'up',
icon: '📊'
}
]);
// 方法:刷新所有数据
const refreshAllData = async () => {
isRefreshing.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 2000));
addNotification('success', '数据刷新成功');
} catch (error) {
addNotification('error', '数据刷新失败');
} finally {
isRefreshing.value = false;
}
};
// 方法:导出报告
const exportReport = async () => {
isExporting.value = true;
try {
await new Promise(resolve => setTimeout(resolve, 3000));
addNotification('success', '报告导出成功');
} catch (error) {
addNotification('error', '报告导出失败');
} finally {
isExporting.value = false;
}
};
// 处理温度告警
const handleTemperatureAlert = (alert: any) => {
addNotification('warning', `温度告警: ${alert.message}`);
};
// 处理湿度告警
const handleHumidityAlert = (alert: any) => {
addNotification('warning', `湿度告警: ${alert.message}`);
};
// 处理温度更新
const handleTemperatureUpdate = (temperature: number) => {
console.log('温度更新:', temperature);
};
// 处理湿度更新
const handleHumidityUpdate = (humidity: number) => {
console.log('湿度更新:', humidity);
};
// 处理用户创建
const handleUserCreated = (user: any) => {
addNotification('success', `用户 ${user.username} 创建成功`);
};
// 处理用户更新
const handleUserUpdated = (user: any) => {
addNotification('success', `用户 ${user.username} 更新成功`);
};
// 处理用户删除
const handleUserDeleted = (username: string) => {
addNotification('info', `用户 ${username} 已删除`);
};
// 添加通知
const addNotification = (type: Notification['type'], message: string) => {
const notification: Notification = {
id: Date.now().toString(),
type,
message,
icon: getNotificationIcon(type),
timestamp: new Date()
};
notifications.value.unshift(notification);
// 5秒后自动移除
setTimeout(() => {
removeNotification(notification.id);
}, 5000);
};
// 移除通知
const removeNotification = (id: string) => {
notifications.value = notifications.value.filter(n => n.id !== id);
};
// 获取通知图标
const getNotificationIcon = (type: Notification['type']): string => {
const icons = {
success: '✅',
warning: '⚠️',
error: '❌',
info: ''
};
return icons[type];
};
// 生命周期:挂载时
onMounted(() => {
console.log('仪表板页面已加载');
});
</script>
<style scoped>
.dashboard-page {
padding: var(--spacing-3xl);
background: var(--bg-main);
min-height: 100vh;
font-family: var(--font-family);
}
/* 统计区域 */
.stats-section {
margin-bottom: var(--spacing-3xl);
padding-left: 5px;
}
/* 主要内容区域 */
.main-content {
display: grid;
grid-template-columns: 1fr 1fr; /* 环境监控和设备状态1:1 */
gap: var(--spacing-3xl);
margin-bottom: var(--spacing-3xl);
padding-left: 6px;
}
/* 2:3比例区域容器 */
.ratio-section {
grid-column: 1 / -1; /* 跨两列 */
display: grid;
grid-template-columns: 2fr 3fr; /* AI预警和生长趋势2:3 */
gap: var(--spacing-3xl);
}
/* 地图区域 */
.map-section {
grid-column: 1 / -1; /* 跨两列 */
margin-bottom: var(--spacing-3xl);
margin-top: -40px;
}
/* 环境监控和设备状态区域 */
.monitoring-section,
.device-status-section {
width: 100%;
box-sizing: border-box;
margin-bottom: var(--spacing-2xl);
}
/* AI预警和生长趋势区域 */
.growth-section,
.personnel-section {
margin-bottom: 0;
grid-column: auto;
margin-top: -20px;
}
/* 区域标题 */
.section-header {
display: flex;
align-items: center;
gap: var(--spacing-md);
margin-bottom: var(--spacing-xl);
}
.section-title {
font-size: var(--font-size-xl);
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
/* 生长趋势容器 */
.xx {
background-color: #FFFFFF;
padding: 20px 20px;
text-align: center;
background: var(--bg-card-hover);
border-radius: 15px;
box-shadow: var(--shadow-md);
box-sizing: border-box;
}
/* 区域徽章 */
.section-badge {
padding: var(--spacing-xs) var(--spacing-md);
background: var(--gradient-primary);
color: var(--text-white);
border-radius: var(--radius-2xl);
font-size: var(--font-size-xs);
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 地图容器 */
.under {
width: 100%;
height: 635px;
background-color: #FFFFFF;
padding: 22px 22px;
text-align: center;
background: var(--bg-card-hover);
border-radius: 14px;
box-shadow: var(--shadow-md);
box-sizing: border-box;
}
/* 监控区域网格 */
.monitoring-grid {
display: grid;
gap: var(--spacing-xl);
}
/* AI预警容器 */
.body-foot {
width: 100%;
height: 560px;
margin-top: 21.5px;
display: flex;
gap: 20px;
background: var(--bg-card-hover);
border-radius: 15px;
box-shadow: var(--shadow-md);
}
.body-under-box {
flex: 1;
}
.box002 {
width: 100%;
height: 572px;
background-color: #FFFFFF;
background: var(--bg-card-hover);
border-radius: 15px;
box-shadow: var(--shadow-md);
box-sizing: border-box;
}
/* 通知区域 */
.notifications {
position: fixed;
top: var(--spacing-2xl);
right: var(--spacing-2xl);
z-index: var(--z-index-modal);
display: flex;
flex-direction: column;
gap: var(--spacing-md);
max-width: 400px;
}
.notification-item {
display: flex;
align-items: center;
gap: var(--spacing-md);
padding: var(--spacing-lg) var(--spacing-xl);
border-radius: var(--radius-lg);
color: var(--text-white);
font-size: var(--font-size-sm);
font-weight: 500;
box-shadow: var(--shadow-lg);
animation: slideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.notification-success {
background: linear-gradient(135deg, var(--success-color) 0%, #73d13d 100%);
}
.notification-warning {
background: linear-gradient(135deg, var(--warning-color) 0%, #ffc53d 100%);
}
.notification-error {
background: linear-gradient(135deg, var(--error-color) 0%, #ff7875 100%);
}
.notification-info {
background: linear-gradient(135deg, var(--info-color) 0%, #40a9ff 100%);
}
.notification-icon {
font-size: var(--font-size-lg);
flex-shrink: 0;
}
.notification-message {
flex: 1;
line-height: 1.4;
}
.notification-close {
background: rgba(255, 255, 255, 0.2);
border: none;
color: var(--text-white);
cursor: pointer;
padding: var(--spacing-xs);
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
transition: all var(--transition-fast);
flex-shrink: 0;
}
.notification-close:hover {
background: rgba(255, 255, 255, 0.3);
transform: scale(1.1);
}
@keyframes slideIn {
from {
transform: translateX(100%) scale(0.95);
opacity: 0;
}
to {
transform: translateX(0) scale(1);
opacity: 1;
}
}
/* 响应式设计 */
@media (max-width: 1200px) {
.main-content,
.ratio-section {
grid-template-columns: 1fr; /* 小屏幕单列 */
gap: var(--spacing-2xl);
}
.map-section,
.monitoring-section,
.growth-section,
.device-status-section,
.personnel-section,
.ratio-section {
grid-column: 1;
}
}
@media (max-width: 768px) {
.dashboard-page {
padding: var(--spacing-lg);
}
.main-content {
gap: var(--spacing-xl);
margin-bottom: var(--spacing-2xl);
}
.section-title {
font-size: var(--font-size-lg);
}
}
@media (max-width: 480px) {
.dashboard-page {
padding: var(--spacing-md);
}
.monitoring-grid {
gap: var(--spacing-md);
}
}
</style>

View File

@ -0,0 +1,767 @@
<template>
<div class="device-status">
<h2 :style="{paddingBottom:'10px',fontSize:'25px'}">设备状态</h2>
<div class="actions">
<div class="action-buttons">
<el-button
type="primary"
icon="el-icon-plus"
@click="showAddModal = true"
class="button-top01"
>
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4111" width="20" height="20">
<path d="M533.333333 490.666667V128h-42.666666v362.666667H128v42.666666h362.666667v362.666667h42.666666V533.333333h362.666667v-42.666666z" fill="#ffffff" p-id="4112"></path>
</svg>
新建设备
</el-button>
<el-button
type="warning"
icon="el-icon-download"
@click="downloadDeviceList"
class="button-top02"
>
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3054" width="20" height="20">
<path d="M853.333333 853.333333a42.666667 42.666667 0 0 1 0 85.333334H170.666667a42.666667 42.666667 0 0 1 0-85.333334h682.666666h-0.000001zM512 85.504a42.666667 42.666667 0 0 1 42.666667 42.666667v515.370666l204.373333-204.373333a42.666667 42.666667 0 0 1 63.914667 56.277333l-3.584 4.010667-277.376 277.546666a42.666667 42.666667 0 0 1-56.32 3.584l-4.010667-3.541334-277.12-276.650666a42.666667 42.666667 0 0 1 56.234667-63.957334l4.010666 3.541334L469.333333 644.096V128.170667a42.666667 42.666667 0 0 1 42.666667-42.666667z" fill="#ffffff" p-id="3055"></path>
</svg>
下载
</el-button>
</div>
</div>
<el-table
:data="filteredDevices"
style="width: 100%"
class="device-table"
v-loading="isLoading"
element-loading-text="加载中..."
>
<el-table-column prop="status" label="状态" sortable>
<template #default="scope">
<el-tag :type="getTagType(scope.row.status)">
{{ getStatusText(scope.row.status) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="deviceName" label="设备名称" sortable></el-table-column>
<el-table-column prop="deviceCode" label="设备ID" sortable></el-table-column>
<el-table-column prop="operator" label="操作人员" sortable></el-table-column>
<el-table-column prop="temperature" label="温度" sortable>
<template #default="scope">
<span>{{ scope.row.temperature || '-' }}°C</span>
</template>
</el-table-column>
<el-table-column prop="humidity" label="湿度" sortable>
<template #default="scope">
<span>{{ scope.row.humidity || '-' }}%</span>
</template>
</el-table-column>
<el-table-column prop="phValue" label="pH值" sortable>
<template #default="scope">
<span>{{ scope.row.phValue || '-' }}</span>
</template>
</el-table-column>
<el-table-column label="操作" sortable>
<template #default="scope">
<el-button
size="mini"
@click="openEditModal(scope.row)"
>
编辑
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.row.id)"
>
删除
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 添加设备模态框 -->
<el-dialog
v-model="showAddModal"
title="添加新设备"
:close-on-click-modal="false"
:before-close="handleClose"
>
<el-form
:model="newDevice"
:rules="addFormRules"
ref="addForm"
label-width="80px"
>
<el-form-item label="设备名称" prop="name">
<el-input v-model="newDevice.name" placeholder="请输入设备名称"></el-input>
</el-form-item>
<el-form-item label="设备ID" prop="id">
<el-input v-model="newDevice.id" placeholder="请输入设备ID"></el-input>
</el-form-item>
<el-form-item label="操作人员" prop="operator">
<el-input v-model="newDevice.operator" placeholder="请输入操作人员"></el-input>
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select v-model="newDevice.status" placeholder="请选择设备状态">
<el-option label="在线" value="normal"></el-option>
<el-option label="离线" value="Offline"></el-option>
<el-option label="故障" value="faulty"></el-option>
</el-select>
</el-form-item>
<el-form-item label="温度 (°C)" prop="temperature">
<el-input
v-model.number="newDevice.temperature"
type="number"
placeholder="请输入温度27-34"
:min="27"
:max="34"
step="0.1"
></el-input>
</el-form-item>
<el-form-item label="湿度 (%)" prop="humidity">
<el-input
v-model.number="newDevice.humidity"
type="number"
placeholder="请输入湿度60-75"
:min="60"
:max="75"
step="1"
></el-input>
</el-form-item>
<el-form-item label="pH值" prop="phValue">
<el-input
v-model.number="newDevice.phValue"
type="number"
placeholder="请输入pH值6.3-7.3"
:min="6.3"
:max="7.3"
step="0.1"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showAddModal = false">取消</el-button>
<el-button type="primary" @click="handleAddDevice">确认</el-button>
</template>
</el-dialog>
<!-- 编辑设备模态框 -->
<el-dialog
v-model="showEditModal"
title="编辑设备"
:close-on-click-modal="false"
:before-close="handleClose"
>
<el-form
:model="editingDevice"
:rules="editFormRules"
ref="editForm"
label-width="80px"
>
<el-form-item label="设备名称" prop="name">
<el-input v-model="editingDevice.name" placeholder="请输入设备名称"></el-input>
</el-form-item>
<el-form-item label="设备ID" prop="id">
<el-input v-model="editingDevice.id" disabled placeholder="设备ID不可修改"></el-input>
</el-form-item>
<el-form-item label="操作人员" prop="operator">
<el-input v-model="editingDevice.operator" placeholder="请输入操作人员"></el-input>
</el-form-item>
<el-form-item label="设备状态" prop="status">
<el-select v-model="editingDevice.status" placeholder="请选择设备状态">
<el-option label="在线" value="normal"></el-option>
<el-option label="离线" value="Offline"></el-option>
<el-option label="故障" value="faulty"></el-option>
</el-select>
</el-form-item>
<el-form-item label="温度 (°C)" prop="temperature">
<el-input
v-model.number="editingDevice.temperature"
type="number"
placeholder="请输入温度27-34"
:min="27"
:max="34"
step="0.1"
></el-input>
</el-form-item>
<el-form-item label="湿度 (%)" prop="humidity">
<el-input
v-model.number="editingDevice.humidity"
type="number"
placeholder="请输入湿度60-75"
:min="60"
:max="75"
step="1"
></el-input>
</el-form-item>
<el-form-item label="pH值" prop="phValue">
<el-input
v-model.number="editingDevice.phValue"
type="number"
placeholder="请输入pH值6.3-7.3"
:min="6.3"
:max="7.3"
step="0.1"
></el-input>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showEditModal = false">取消</el-button>
<el-button type="primary" @click="handleEditDevice">保存</el-button>
</template>
</el-dialog>
<!-- 故障详情模态框 -->
<el-dialog
v-model="showFaultModal"
title="故障信息"
:close-on-click-modal="false"
>
<div>设备ID: {{ selectedDevice?.id }}</div>
<div>设备名称: {{ selectedDevice?.deviceName }}</div>
<div>故障描述: {{ selectedDevice?.faultDescription || '暂无描述' }}</div>
<template #footer>
<el-button @click="showFaultModal = false">关闭</el-button>
<el-button type="primary" @click="handleFault">处理</el-button>
</template>
</el-dialog>
<div class="pagination">
<el-pagination
@size-change="handlePageSizeChange"
@current-change="handleCurrentPageChange"
:current-page="currentPage"
:page-sizes="[10, 20, 30, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="totalDevices"
></el-pagination>
</div>
<div>
<askai />
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElMessage, ElMessageBox } from 'element-plus';
import axios from 'axios';
import Askai from "../components1/askai.vue";
// 配置后端基础URL
axios.defaults.baseURL = 'http://localhost:5000';
// 状态管理
const isLoading = ref(false);
const devices = ref([]);
const totalDevices = ref(0);
const currentPage = ref(1);
const pageSize = ref(10);
const showAddModal = ref(false);
const showEditModal = ref(false);
const showFaultModal = ref(false);
// 新增设备数据(所有参数设置默认值在范围内)
const newDevice = ref({
id: '',
name: '',
operator: '',
status: 'Normal',
// 生成 27 到 34 之间的随机温度值(保留一位小数)
temperature: (27 + Math.random() * 7).toFixed(1),
// 生成 60 到 75 之间的随机湿度值(整数)
humidity: Math.floor(60 + Math.random() * 16),
// 生成 6.3 到 7.3 之间的随机 pH 值(保留一位小数)
phValue: (6.3 + Math.random() * 1).toFixed(1)
});
const editingDevice = ref(null);
const selectedDevice = ref(null);
// 表单验证规则新增pH值范围校验
const addFormRules = {
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
id: [{ required: true, message: '请输入设备ID', trigger: 'blur' }],
temperature: [
{ required: true, message: '请输入温度', trigger: 'blur' },
{ type: 'number', min: 27, max: 34, message: '温度必须在27-34℃之间', trigger: 'blur' }
],
humidity: [
{ required: true, message: '请输入湿度', trigger: 'blur' },
{ type: 'number', min: 60, max: 75, message: '湿度必须在60-75%之间', trigger: 'blur' }
],
phValue: [
{ required: true, message: '请输入pH值', trigger: 'blur' },
{ type: 'number', min: 6.3, max: 7.3, message: 'pH值必须在6.3-7.3之间', trigger: 'blur' }
]
};
const editFormRules = {
name: [{ required: true, message: '请输入设备名称', trigger: 'blur' }],
status: [{ required: true, message: '请选择设备状态', trigger: 'change' }],
temperature: [
{ required: true, message: '请输入温度', trigger: 'blur' },
{ type: 'number', min: 27, max: 34, message: '温度必须在27-34℃之间', trigger: 'blur' }
],
humidity: [
{ required: true, message: '请输入湿度', trigger: 'blur' },
{ type: 'number', min: 60, max: 75, message: '湿度必须在60-75%之间', trigger: 'blur' }
],
phValue: [
{ required: true, message: '请输入pH值', trigger: 'blur' },
{ type: 'number', min: 6.3, max: 7.3, message: 'pH值必须在6.3-7.3之间', trigger: 'blur' }
]
};
// 获取设备列表
const fetchDevices = async (page = 1, size = 10) => {
isLoading.value = true;
try {
const response = await axios.get('/api/device/list', { params: { page, size } });
if (response.data.success) {
// 处理设备数据区分离线设备、设备B、设备C和其他设备
const filtered = response.data.data.map(device => {
// 1. 离线设备不设置温湿度和pH值
if (device.status === 'Offline') {
return {
...device,
temperature: undefined, // 不显示温度
humidity: undefined, // 不显示湿度
phValue: undefined // 不显示pH值
};
}
// 2. 设备B固定温度38、湿度95%
if (device.deviceName === '设备B') {
return {
...device,
temperature: 38, // 固定温度38℃
humidity: 95, // 固定湿度95%
phValue: Number((6.3 + Math.random() * 1).toFixed(1)) // pH值随机
};
}
// 3. 设备C固定温度16、湿度28%
if (device.deviceName === '设备C') {
return {
...device,
temperature: 16, // 固定温度16℃
humidity: 28, // 固定湿度28%
phValue: Number((6.3 + Math.random() * 1).toFixed(1)) // pH值随机
};
}
// 4. 其他设备:按原逻辑生成随机值(在正常范围内)
return {
...device,
temperature: Number((27 + Math.random() * 7).toFixed(1)), // 27-34℃
humidity: Math.floor(60 + Math.random() * 16), // 60-75%
phValue: Number((6.3 + Math.random() * 1).toFixed(1)) // 6.3-7.3
};
});
devices.value = filtered;
totalDevices.value = response.data.total;
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('获取设备列表失败,请检查后端服务');
console.error('获取数据失败:', error);
} finally {
isLoading.value = false;
}
};
// 新增设备
const handleAddDevice = async () => {
// 手动校验所有参数范围
if (newDevice.value.temperature < 27 || newDevice.value.temperature > 34) {
ElMessage.error('温度必须在27-34℃之间');
return;
}
if (newDevice.value.humidity < 60 || newDevice.value.humidity > 75) {
ElMessage.error('湿度必须在60-75%之间');
return;
}
if (newDevice.value.phValue < 6.3 || newDevice.value.phValue > 7.3) {
ElMessage.error('pH值必须在6.3-7.3之间');
return;
}
try {
const response = await axios.post('/api/device', {
...newDevice.value,
deviceName: newDevice.value.name,
deviceCode: newDevice.value.id
});
if (response.data.success) {
ElMessage.success('设备新增成功');
showAddModal.value = false;
fetchDevices();
resetNewDevice();
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('设备新增失败');
console.error(error);
}
};
// 编辑设备
const handleEditDevice = async () => {
// 手动校验参数范围(保持不变)
if (editingDevice.value.temperature < 27 || editingDevice.value.temperature > 34) {
ElMessage.error('温度必须在27-34℃之间');
return;
}
if (editingDevice.value.humidity < 60 || editingDevice.value.humidity > 75) {
ElMessage.error('湿度必须在60-75%之间');
return;
}
if (editingDevice.value.phValue < 6.3 || editingDevice.value.phValue > 7.3) {
ElMessage.error('pH值必须在6.3-7.3之间');
return;
}
try {
// 关键修复:确保提交的字段与后端预期一致(与设备列表字段对应)
const response = await axios.put(`/api/device/${editingDevice.value.id}`, {
deviceName: editingDevice.value.name, // 对应表格中的设备名称
deviceCode: editingDevice.value.id, // 对应表格中的设备ID不可修改保持原值
operator: editingDevice.value.operator,
status: editingDevice.value.status,
temperature: editingDevice.value.temperature, // 温度字段
humidity: editingDevice.value.humidity, // 湿度字段
phValue: editingDevice.value.phValue // pH值字段
});
if (response.data.success) {
ElMessage.success('设备编辑成功');
showEditModal.value = false;
// 强制重新获取数据,确保表格更新
fetchDevices(currentPage.value, pageSize.value);
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('设备编辑失败');
console.error('编辑提交失败:', error); // 增加错误日志
}
};
// 打开编辑模态框(确保所有参数在范围内)
const openEditModal = (device) => {
// 强制将所有参数限制在范围内
const temp = Math.min(Math.max(device.temperature || 27, 27), 34);
const humi = Math.min(Math.max(device.humidity || 60, 60), 75);
const ph = Math.min(Math.max(device.phValue || 6.3, 6.3), 7.3);
editingDevice.value = {
...device,
name: device.deviceName,
temperature: temp,
humidity: humi,
phValue: ph
};
showEditModal.value = true;
};
// 删除设备
const handleDelete = async (id) => {
await ElMessageBox.confirm('确认删除该设备?', '警告', { type: 'warning' })
.then(async () => {
try {
const response = await axios.delete(`/api/device/${id}`);
if (response.data.success) {
ElMessage.success('设备删除成功');
fetchDevices();
} else {
ElMessage.error(response.data.message);
}
} catch (error) {
ElMessage.error('删除失败');
console.error(error);
}
});
};
// 处理故障
const handleFault = () => {
ElMessage.success('故障已处理');
showFaultModal.value = false;
};
// 重置新增表单
const resetNewDevice = () => {
newDevice.value = {
id: '',
name: '',
operator: '',
status: 'Normal',
temperature: 27,
humidity: 60,
phValue: 6.3
};
};
// 分页处理
const handlePageSizeChange = (size) => {
pageSize.value = size;
fetchDevices(1, size);
};
const handleCurrentPageChange = (page) => {
currentPage.value = page;
fetchDevices(page, pageSize.value);
};
// 获取标签类型
const getTagType = (status) => {
switch (status) {
case 'normal': return 'success';
case 'Offline': return 'info';
case 'fault': return 'danger';
case 'warning': return 'danger';
default: return '';
}
};
// 状态英文转中文
const getStatusText = (status) => {
switch (status) {
case 'normal': return '在线';
case 'Offline': return '离线';
case 'fault': return '故障';
case 'warning': return '故障';
default: return status;
}
};
// 关闭模态框
const handleClose = () => {
const addForm = document.querySelector('[ref="addForm"]');
const editForm = document.querySelector('[ref="editForm"]');
if (addForm) addForm.resetFields?.();
if (editForm) editForm.resetFields?.();
};
// 下载设备列表
const downloadDeviceList = () => {
if (devices.value.length === 0) {
ElMessage.info('没有数据可导出');
return;
}
const headers = ['设备ID', '设备名称', '状态', '操作人员', '温度(°C)', '湿度(%)', 'pH值'];
const csvContent = [
headers.join(','),
...devices.value.map(device => [
device.id,
device.deviceName,
getStatusText(device.status),
device.operator,
device.temperature || '',
device.humidity || '',
device.phValue || ''
].join(','))
].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = `设备列表_${new Date().toLocaleDateString()}.csv`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
// 生命周期钩子
onMounted(() => {
fetchDevices();
});
// 计算过滤后的数据
const filteredDevices = computed(() => devices.value);
</script>
<style scoped>
/* 样式部分保持不变 */
.device-status {
padding: 20px;
background-color: #F5F7FA;
padding-left: 28px;
padding-right: 30px;
}
.actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
padding-right: 20px;
}
.device-table {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.modal-content {
width: 400px;
max-width: 90%;
}
.el-table__header {
background-color: #fbfbfb;
}
.icon {
margin-right: 5px;
}
.button-top01{
white-space: nowrap;
padding-top: 8px;
padding-left: 10px;
width: 110px;
}
.button-top02 {
white-space: nowrap;
padding-top: 8px;
padding-left: 10px;
width: 90px;
}
:deep(.el-tag) {
border: none !important;
color: white !important;
}
:deep(.el-tag--success) {
background-color: #4CAF50 !important;
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.25);
}
:deep(.el-tag--info) {
background-color: #90A4AE !important;
box-shadow: 0 2px 4px rgba(144, 164, 174, 0.25);
}
:deep(.el-tag--danger) {
background-color: #FF7043 !important;
box-shadow: 0 2px 4px rgba(255, 112, 67, 0.25);
animation: pulse-danger 2s infinite;
}
@keyframes pulse-danger {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
:deep(.el-tag) {
border: none !important;
color: white !important;
font-weight: 500 !important;
padding: 0 12px !important;
height: 28px !important;
line-height: 28px !important;
border-radius: 14px !important;
display: inline-flex !important;
align-items: center !important;
transition: all 0.3s ease !important;
}
</style>
<style scoped>
/* 样式部分保持不变 */
.device-status {
padding: 20px;
background-color: #F5F7FA;
padding-left: 28px;
padding-right: 30px;
}
.actions {
margin-bottom: 20px;
display: flex;
gap: 10px;
justify-content: flex-end;
padding-right: 20px;
}
.device-table {
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
.modal-content {
width: 400px;
max-width: 90%;
}
.el-table__header {
background-color: #fbfbfb;
}
.icon {
margin-right: 5px;
}
.button-top01{
white-space: nowrap;
padding-top: 8px;
padding-left: 10px;
width: 110px;
}
.button-top02 {
white-space: nowrap;
padding-top: 8px;
padding-left: 10px;
width: 90px;
}
:deep(.el-tag) {
border: none !important;
color: white !important;
}
:deep(.el-tag--success) {
background-color: #4CAF50 !important; /* 清新绿 - 表示正常状态 */
box-shadow: 0 2px 4px rgba(76, 175, 80, 0.25); /* 添加微妙阴影增强层次感 */
}
:deep(.el-tag--info) {
background-color: #90A4AE !important; /* 蓝灰色 - 表示中性状态 */
box-shadow: 0 2px 4px rgba(144, 164, 174, 0.25);
}
:deep(.el-tag--danger) {
background-color: #FF7043 !important; /* 橙红色 - 表示警告状态 */
box-shadow: 0 2px 4px rgba(255, 112, 67, 0.25);
animation: pulse-danger 2s infinite; /* 添加微弱脉动效果吸引注意但不刺眼 */
}
/* 警告状态的脉动动画(可选) */
@keyframes pulse-danger {
0% { opacity: 1; }
50% { opacity: 0.8; }
100% { opacity: 1; }
}
/* 统一调整标签样式增强美观性 */
:deep(.el-tag) {
border: none !important;
color: white !important;
font-weight: 500 !important; /* 稍微加粗字体 */
padding: 0 12px !important; /* 增加内边距 */
height: 28px !important; /* 统一高度 */
line-height: 28px !important;
border-radius: 14px !important; /* 圆角增大更柔和 */
display: inline-flex !important;
align-items: center !important;
transition: all 0.3s ease !important; /* 添加过渡效果 */
}
</style>

View File

@ -0,0 +1,248 @@
<template>
<div id="all">
<div class="fault-management-dashboard">
<h1>故障管理仪表</h1>
<p>故障管理 > 仪表盘</p>
<div class="top-container">
<div class="fault-box">
<div class="top">
<p>今日故障</p>
<span class="increase" :class="{ 'text-red': (todayIncrease) > 0 }">
{{ (todayIncrease) > 0 ? '↑ ' + (todayIncrease) : '→ 0' }}
</span>
</div>
<p class="fault-count">{{ todayIncrease }}</p>
</div>
<div class="fault-limit-box">
<p class="limit-title">本月累计故障数</p>
<p class="limit-count">{{ todayFaults }}/{{ limit }}</p>
<div class="progress-bar">
<div class="progress" :style="{ width: progressWidth }"></div>
</div>
</div>
</div>
<div class="middle-container">
<div class="middle-box"><gu4/></div>
<div class="middle-box"><gu5/></div>
</div>
<div class="bottom-container">
<gu6/>
</div>
<div class="askai-container">
<askai />
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
import Gu4 from "../components3/gu4.vue";
import Gu5 from "../components3/gu5.vue";
import Gu6 from "../components3/gu6.vue";
import Askai from "../components1/askai.vue";
export default {
name: 'FaultManagementDashboard',
components: {Askai, Gu6, Gu5, Gu4 },
data() {
return {
todayFaults: 0,
todayIncrease: 0,
monthlyFaults: 0,
limit: 100,
isLoading: false
};
},
computed: {
progressWidth() {
return this.limit === 0
? '0%'
: `${Math.min((this.monthlyFaults / this.limit) * 100, 100)}%`;
}
},
mounted() {
this.fetchDashboardData();
},
methods: {
async fetchDashboardData() {
this.isLoading = true;
try {
const response = await axios.get('http://localhost:5000/dashboard');
const {todayFaults, monthlyFaults, limit} = response.data;
this.todayFaults = todayFaults;
this.todayIncrease = todayFaults - 1; // 修改将todayIncrease设置为todayFaults -1
this.monthlyFaults = monthlyFaults;
this.limit = limit;
} catch (error) {
console.error('获取故障数据失败:', error);
if (error.response) {
console.error('状态码:', error.response.status);
}
} finally {
this.isLoading = false;
}
}
}
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
#all {
padding: 20px;
min-height: 100vh;
background-color: #F5F7FA;
padding-left: 30px;
}
.fault-management-dashboard {
font-family: Arial, sans-serif;
width: 100%;
max-width: 1600px;
margin: 0 auto;
}
.fault-management-dashboard h1 {
font-size: 1.5em;
margin-bottom: 8px;
}
.fault-management-dashboard > p {
margin-bottom: 20px;
color: #666;
}
/* 顶部容器 - 包含两个小框 */
.top-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.fault-box, .fault-limit-box {
flex: 1;
border-radius: 8px;
padding: 16px 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
min-height: 120px;
}
.top {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
font-size: 1rem;
color: #555;
}
.fault-count {
font-size: 2.5rem;
font-weight: bold;
color: #1a73e8;
margin-top: 8px;
}
.increase {
color: #d32f2f;
font-weight: bold;
font-size: 1rem;
}
.text-red {
color: red;
}
.limit-title {
font-size: 1rem;
color: #555;
margin-bottom: 8px;
}
.limit-count {
font-size: 2rem;
font-weight: bold;
margin-bottom: 12px;
}
.progress-bar {
background: #f0f0f0;
border-radius: 4px;
height: 8px;
width: 100%;
overflow: hidden;
}
.progress {
background: #1a73e8;
height: 100%;
border-radius: 4px;
transition: width 0.3s ease;
}
/* 中部容器 - 包含两个大框 */
.middle-container {
display: flex;
gap: 20px;
margin-bottom: 20px;
height: 500px;
}
.middle-box {
flex: 1;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
/* 底部容器 - 单个大框 */
.bottom-container {
width: 100%;
margin-bottom: 20px;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
}
/* AskAI容器 */
.askai-container {
width: 100%;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
background-color: #fff;
margin-top: 20px;
}
/* 响应式调整 */
@media (max-width: 1200px) {
.middle-container {
flex-direction: column;
height: auto;
}
.middle-box {
height: 400px;
margin-bottom: 20px;
}
.middle-box:last-child {
margin-bottom: 0;
}
}
</style>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long