Update .gitignore and add files
This commit is contained in:
993
platform/src/pages/AIyujing.vue
Normal file
993
platform/src/pages/AIyujing.vue
Normal 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">-->
|
||||
<!--<!– <span class="current-value">{{ alert.currentValue }}</span>–>-->
|
||||
<!--<!– <span class="separator">/</span>–>-->
|
||||
<!--<!– <span class="threshold-value">阈值: {{ alert.threshold }}</span>–>-->
|
||||
<!-- </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>
|
||||
531
platform/src/pages/DashboardPage.vue
Normal file
531
platform/src/pages/DashboardPage.vue
Normal 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>
|
||||
767
platform/src/pages/DeviceManagementPage.vue
Normal file
767
platform/src/pages/DeviceManagementPage.vue
Normal 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>
|
||||
248
platform/src/pages/MonitoringPage.vue
Normal file
248
platform/src/pages/MonitoringPage.vue
Normal 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>
|
||||
1487
platform/src/pages/PersonnelPage.vue
Normal file
1487
platform/src/pages/PersonnelPage.vue
Normal file
File diff suppressed because it is too large
Load Diff
1390
platform/src/pages/ReportsPage.vue
Normal file
1390
platform/src/pages/ReportsPage.vue
Normal file
File diff suppressed because it is too large
Load Diff
392
platform/src/pages/shengzhang.vue
Normal file
392
platform/src/pages/shengzhang.vue
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user