Compare commits

1 Commits
master ... vrt

Author SHA1 Message Date
tianyongbao
fe9dad449d fix: vrt车载修改提交备份。 2025-10-31 23:21:00 +08:00
8 changed files with 2841 additions and 1 deletions

BIN
src/assets/images/logo.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@@ -0,0 +1,680 @@
<template>
<div class="history-temperature">
<!-- 查询条件区无修改 -->
<div class="search-bar">
<div class="search-title">查询条件</div>
<div class="search-fields">
<div class="field">
<label>开始日期</label>
<input type="date" v-model="startDate" />
</div>
<div class="field">
<label>结束日期</label>
<input type="date" v-model="endDate" />
</div>
<div class="field">
<label>时间粒度</label>
<select v-model="timeGranularity">
<option value="minute">分钟</option>
<option value="hour">小时</option>
<option value="day"></option>
</select>
</div>
<div class="field">
<label>温度单位</label>
<select v-model="tempUnit">
<option value="celsius">摄氏度</option>
<option value="fahrenheit">华氏度</option>
</select>
</div>
<button class="search-btn" @click="handleSearch">查询</button>
</div>
</div>
<!-- 数据统计概览无修改 -->
<div class="stats-overview">
<div class="stat-item">
<span class="stat-label">数据总量</span>
<span class="stat-value">{{ tableData.length }} </span>
</div>
<div class="stat-item">
<span class="stat-label">平均温度</span>
<span class="stat-value">{{ avgTemp.toFixed(1) }}{{ tempUnit === 'celsius' ? '°C' : '°F' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">最高温度</span>
<span class="stat-value">{{ maxTemp.toFixed(1) }}{{ tempUnit === 'celsius' ? '°C' : '°F' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">最低温度</span>
<span class="stat-value">{{ minTemp.toFixed(1) }}{{ tempUnit === 'celsius' ? '°C' : '°F' }}</span>
</div>
<div class="stat-item">
<span class="stat-label">异常记录</span>
<span class="stat-value">{{ abnormalCount }} </span>
</div>
</div>
<!-- 历史温度记录表格已删除波动范围操作人无新增修改 -->
<div class="table-section">
<div class="table-header">
<span>历史温度记录</span>
<div class="table-actions">
<button class="action-btn" @click="sortData('time')">
按时间{{ sortField === 'time' ? (sortOrder === 'asc' ? '' : '') : '' }}
</button>
<button class="action-btn" @click="sortData('temperature')">
按温度{{ sortField === 'temperature' ? (sortOrder === 'asc' ? '' : '') : '' }}
</button>
</div>
</div>
<div class="table-container">
<table class="history-table">
<thead>
<tr>
<th>序号</th>
<th>时间</th>
<th>温度</th>
<th>单位</th>
<th>设备状态</th>
<th>备注</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, idx) in displayedData"
:key="idx"
:class="{ odd: idx % 2 === 0, highlight: item.statusClass === 'red' }"
>
<td>{{ (currentPage - 1) * pageSize + idx + 1 }}</td>
<td>{{ item.time }}</td>
<td>{{ item.temperature.toFixed(1) }}</td>
<td>{{ item.unit }}</td>
<td>
<span
:class="['status-tag', `status-${item.statusClass}`]"
>
{{ item.status }}
</span>
</td>
<td>{{ item.remark }}</td>
</tr>
</tbody>
</table>
</div>
<!-- 分页组件无修改 -->
<div class="pagination">
<div class="page-info">
{{ tableData.length }} {{ totalPages }} 当前第 {{ currentPage }}
</div>
<div class="page-controls">
<button
class="page-btn"
@click="prevPage"
:disabled="currentPage === 1"
>
上一页
</button>
<span
v-for="page in visiblePages"
:key="page"
:class="{ active: page === currentPage, ellipsis: page === '...' }"
@click="page !== '...' && goToPage(page)"
>
{{ page }}
</span>
<button
class="page-btn"
@click="nextPage"
:disabled="currentPage === totalPages"
>
下一页
</button>
</div>
</div>
</div>
<!-- 导出按钮区无修改 -->
<div class="export-buttons">
<button class="export-btn csv" @click="exportCSV">导出CSV</button>
<button class="export-btn excel" @click="exportExcel">导出EXCEL</button>
<button class="export-btn print" @click="printTable">打印表格</button>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, computed, watch } from 'vue'
// 日期格式化工具(无修改)
const formatDate = (date: Date): string => {
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}-${month}-${day}`
}
// 计算最近10天范围无修改
const getRecent10Days = () => {
const end = new Date()
const start = new Date()
start.setDate(end.getDate() - 9)
return { start, end }
}
// 初始化查询条件(无修改)
const { start: recentStart, end: recentEnd } = getRecent10Days()
const startDate = ref(formatDate(recentStart))
const endDate = ref(formatDate(recentEnd))
const timeGranularity = ref<'minute' | 'hour' | 'day'>('hour')
const tempUnit = ref<'celsius' | 'fahrenheit'>('celsius')
// 表格核心数据(修改:新增基础数据池 baseTableData
const baseTableData = ref<any[]>([]) // 存储初始生成的原始数据(仅生成一次)
const tableData = ref<any[]>([]) // 存储筛选/转换后的展示数据
const currentPage = ref(1)
const pageSize = ref(10)
const sortField = ref('time')
const sortOrder = ref<'asc' | 'desc'>('desc')
// 统计数据(无修改)
const avgTemp = ref(0)
const maxTemp = ref(0)
const minTemp = ref(0)
const abnormalCount = ref(0)
// 分页计算属性(无修改)
const totalPages = computed(() =>
Math.ceil(tableData.value.length / pageSize.value)
)
// 显示的页码(无修改)
const visiblePages = computed(() => {
const pages = []
const total = totalPages.value
if (total <= 7) {
for (let i = 1; i <= total; i++) pages.push(i)
return pages
}
if (currentPage.value <= 4) {
pages.push(1, 2, 3, 4, 5, '...', total)
} else if (currentPage.value >= total - 3) {
pages.push(1, '...', total - 4, total - 3, total - 2, total - 1, total)
} else {
pages.push(1, '...',
currentPage.value - 2,
currentPage.value - 1,
currentPage.value,
currentPage.value + 1,
currentPage.value + 2,
'...', total)
}
return pages
})
// 分页数据(无修改)
const displayedData = computed(() => {
const sortedData = [...tableData.value].sort((a, b) => {
if (sortField.value === 'time') {
return sortOrder.value === 'asc'
? new Date(a.time).getTime() - new Date(b.time).getTime()
: new Date(b.time).getTime() - new Date(a.time).getTime()
} else {
return sortOrder.value === 'asc'
? a.temperature - b.temperature
: b.temperature - a.temperature
}
})
const start = (currentPage.value - 1) * pageSize.value
const end = start + pageSize.value
return sortedData.slice(start, end)
})
// 设备状态配置(无修改)
const statusMap = {
normal: { label: '正常', class: 'green' },
warning: { label: '警告', class: 'yellow' },
abnormal: { label: '异常', class: 'red' }
}
// 备注模板(无修改)
const remarkTemplates = {
normal: [
'设备运行正常',
'温度稳定,无异常',
'符合标准范围',
'运行参数正常',
'环境稳定,设备正常'
],
warning: [
'温度波动较大',
'接近阈值,请注意',
'短期波动,需观察',
'温度略有异常',
'环境变化导致波动'
],
abnormal: [
'温度过高,已告警',
'超出阈值范围',
'设备异常,需检修',
'温度急剧变化',
'紧急处理:温度异常'
]
}
// 生成模拟数据修改为「仅生成原始摄氏度数据」存入baseTableData不包含单位转换
const generateMockData = () => {
const data: any[] = []
const start = new Date(startDate.value)
const end = new Date(endDate.value)
end.setHours(23, 59, 59, 999)
let currentTime = new Date(start)
// 时间间隔(无修改)
const intervals = {
minute: 10 * 60 * 1000,
hour: 1 * 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000
}
const interval = intervals[timeGranularity.value]
// 基础温度范围(无修改,仅生成摄氏度)
const baseMin = 2.5
const baseMax = 6.8
while (currentTime <= end) {
// 格式化时间(无修改)
const timeStr = currentTime.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit'
})
// 生成原始摄氏度温度(不做单位转换,后续按需转换)
const celsius = baseMin + Math.random() * (baseMax - baseMin)
// 设备状态(基于原始摄氏度判断,无修改)
let statusKey: 'normal' | 'warning' | 'abnormal' = 'normal'
if (celsius > 6.0) {
statusKey = 'abnormal'
} else if (celsius > 5.2 || celsius < 3.0) {
statusKey = 'warning'
}
// 随机备注(无修改)
const remarks = remarkTemplates[statusKey]
const remark = remarks[Math.floor(Math.random() * remarks.length)]
// 存入原始数据(仅包含基础信息,无单位转换/衍生字段)
data.push({
time: timeStr,
celsius, // 原始摄氏度温度(核心,不修改)
statusKey, // 原始状态key后续映射显示文本
remark // 备注
})
// 推进时间(无修改)
currentTime.setTime(currentTime.getTime() + interval)
}
return data
}
// 更新统计数据无修改基于筛选后的tableData计算
const updateStatistics = (data: any[]) => {
if (data.length === 0) {
avgTemp.value = 0
maxTemp.value = 0
minTemp.value = 0
abnormalCount.value = 0
return
}
const temps = data.map(item => item.temperature)
avgTemp.value = temps.reduce((sum, temp) => sum + temp, 0) / temps.length
maxTemp.value = Math.max(...temps)
minTemp.value = Math.min(...temps)
abnormalCount.value = data.filter(item => item.statusClass === 'red').length
}
// 查询事件:核心修改!不再生成新数据,仅筛选+转换基础数据
const handleSearch = () => {
// 1. 首次查询/初始化时,生成基础数据并缓存(仅执行一次)
if (baseTableData.value.length === 0) {
baseTableData.value = generateMockData()
}
// 2. 解析当前查询条件(日期范围)
const queryStart = new Date(startDate.value)
const queryEnd = new Date(endDate.value)
queryEnd.setHours(23, 59, 59, 999) // 结束日期包含当天全天
// 3. 从基础数据池筛选:仅保留日期在查询范围内的数据
const filteredRawData = baseTableData.value.filter(item => {
const itemTime = new Date(item.time)
return itemTime >= queryStart && itemTime <= queryEnd
})
// 4. 按当前温度单位转换数值,生成最终展示数据
const processedData = filteredRawData.map(item => {
// 单位转换(基于原始摄氏度)
const temperature = tempUnit.value === 'fahrenheit'
? item.celsius * 1.8 + 32 // 摄氏度转华氏度
: item.celsius // 保持摄氏度
// 映射状态文本和样式
const statusInfo = statusMap[item.statusKey]
return {
time: item.time,
temperature, // 转换后的温度值
unit: tempUnit.value === 'celsius' ? '°C' : '°F', // 当前单位
status: statusInfo.label,
statusClass: statusInfo.class,
remark: item.remark
}
})
// 5. 更新展示数据和统计信息,重置分页
tableData.value = processedData
updateStatistics(processedData)
currentPage.value = 1
}
// 排序功能(无修改)
const sortData = (field: 'time' | 'temperature') => {
if (sortField.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortField.value = field
sortOrder.value = 'desc'
}
}
// 分页操作(无修改)
const prevPage = () => {
if (currentPage.value > 1) currentPage.value--
}
const nextPage = () => {
if (currentPage.value < totalPages.value) currentPage.value++
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
// 导出和打印功能(无修改)
const exportCSV = () => {
alert(`已导出 ${tableData.value.length} 条温度数据到CSV文件`)
}
const exportExcel = () => {
alert(`已导出 ${tableData.value.length} 条温度数据到Excel文件`)
}
const printTable = () => {
window.print()
}
// 初始化数据(无修改,首次加载生成基础数据)
watch([startDate, endDate, timeGranularity, tempUnit], () => {
handleSearch()
}, { immediate: true })
</script>
<style scoped>
/* 样式无修改(表格列数减少后,自适应布局自动调整) */
.history-temperature {
font-family: "Microsoft YaHei", sans-serif;
padding: 24px;
background: #f5f7fa;
}
.search-bar {
background: #2f6fab;
color: #fff;
padding: 16px;
border-radius: 6px;
margin-bottom: 16px;
}
.search-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.search-fields {
display: flex;
flex-wrap: wrap;
gap: 24px;
align-items: center;
}
.field {
display: flex;
align-items: center;
gap: 8px;
}
.field label {
min-width: 80px;
}
.field input, .field select {
padding: 8px;
border: none;
border-radius: 4px;
outline: none;
min-width: 140px;
}
.search-btn {
padding: 8px 20px;
background: #409eff;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
align-self: center;
}
.search-btn:hover {
background: #66b1ff;
}
.stats-overview {
display: flex;
flex-wrap: wrap;
gap: 16px;
margin-bottom: 16px;
}
.stat-item {
background: #fff;
padding: 12px 20px;
border-radius: 6px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
min-width: 140px;
}
.stat-label {
display: block;
font-size: 14px;
color: #666;
margin-bottom: 4px;
}
.stat-value {
font-size: 18px;
font-weight: 600;
color: #333;
}
.table-section {
background: #fff;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
margin-bottom: 24px;
overflow: hidden;
}
.table-header {
background: #4caf50;
color: #fff;
padding: 12px 16px;
font-size: 16px;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.table-actions {
display: flex;
gap: 12px;
}
.action-btn {
background: rgba(255,255,255,0.2);
border: none;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
cursor: pointer;
transition: background 0.3s;
font-size: 14px;
}
.action-btn:hover {
background: rgba(255,255,255,0.3);
}
.table-container {
overflow-x: auto;
}
.history-table {
width: 100%;
min-width: 600px; /* 列数减少,适当缩小最小宽度 */
border-collapse: collapse;
}
.history-table th, .history-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid #eee;
}
.history-table th {
background: #f8faf9;
font-weight: 600;
color: #666;
}
.odd {
background: #fafafa;
}
.highlight {
background: #fff8f8;
}
.status-tag {
padding: 4px 8px;
border-radius: 4px;
color: #fff;
font-size: 12px;
}
.status-green { background: #4caf50; }
.status-yellow { background: #ffc107; }
.status-red { background: #f44336; }
.pagination {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
align-items: center;
padding: 12px 16px;
border-top: 1px solid #eee;
gap: 12px;
}
.page-info {
font-size: 14px;
color: #666;
}
.page-controls {
display: flex;
gap: 8px;
align-items: center;
}
.page-btn {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.3s;
}
.page-btn:disabled {
cursor: not-allowed;
background: #f5f5f5;
color: #999;
border-color: #eee;
}
.page-btn:hover:not(:disabled) {
background: #e6f3ff;
border-color: #409eff;
color: #409eff;
}
.pagination span {
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
}
.pagination span.active {
background: #409eff;
color: #fff;
}
.pagination span.ellipsis {
cursor: default;
color: #999;
}
.export-buttons {
display: flex;
gap: 12px;
}
.export-btn {
padding: 8px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
color: #fff;
transition: opacity 0.3s;
display: inline-flex;
align-items: center;
gap: 6px;
}
.export-btn.csv { background: #4caf50; }
.export-btn.excel { background: #2196f3; }
.export-btn.print { background: #ff9800; }
.export-btn:hover {
opacity: 0.9;
}
@media print {
.search-bar, .pagination, .export-buttons, .table-actions {
display: none;
}
.table-section {
box-shadow: none;
border: none;
}
.history-table th, .history-table td {
border-color: #ccc;
}
}
@media (max-width: 768px) {
.search-fields {
flex-direction: column;
align-items: stretch;
}
.field {
width: 100%;
}
.stats-overview {
justify-content: center;
}
.pagination {
flex-direction: column;
align-items: center;
}
}
</style>

View File

@@ -0,0 +1,252 @@
<template>
<div class="backup-management">
<div class="container">
<!-- 备份策略配置区域 -->
<div class="config-section">
<h3 class="section-title">备份策略配置</h3>
<el-form :model="configForm" label-width="100px" class="config-form">
<el-form-item label="备份频率">
<el-select v-model="configForm.frequency" placeholder="请选择备份频率">
<el-option label="每日" value="daily"></el-option>
<el-option label="每周" value="weekly"></el-option>
<el-option label="每月" value="monthly"></el-option>
<el-option label="自定义" value="custom"></el-option>
</el-select>
</el-form-item>
<el-form-item label="存储位置">
<el-input v-model="configForm.storage" placeholder="请输入备份存储路径"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="saveConfig">保存配置</el-button>
</el-form-item>
</el-form>
</div>
<!-- 备份记录区域 -->
<div class="records-section">
<h3 class="section-title">备份记录 <span class="text-sm text-gray-500"></span></h3>
<el-table
:data="pagedRecords"
border
stripe
style="width: 100%; margin-bottom: 15px;"
>
<el-table-column prop="date" label="备份时间" width="220">
<template #default="scope">
<span class="font-medium">{{ scope.row.date }}</span>
</template>
</el-table-column>
<el-table-column prop="size" label="备份大小" width="120">
<template #default="scope">
<span class="font-medium">{{ scope.row.size }}</span>
</template>
</el-table-column>
<el-table-column prop="status" label="备份状态" width="140">
<template #default="scope">
<el-tag
:type="scope.row.status === '成功' ? 'success' : 'danger'"
effect="dark"
>
{{ scope.row.status }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="100">
<template #default="scope">
<el-button
type="primary"
size="mini"
@click="restoreBackup(scope.row)"
>
恢复
</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页控件 -->
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 15, 20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="backupRecords.length"
class="pagination"
/>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue';
import { ElForm, ElFormItem, ElSelect, ElOption, ElInput, ElButton, ElTable, ElTableColumn, ElTag, ElPagination, ElMessage } from 'element-plus';
// 备份配置表单数据
const configForm = ref({
frequency: 'weekly',
storage: '/backup/storage/location'
});
// 分页相关数据
const currentPage = ref(1);
const pageSize = ref(5);
const backupRecords = ref([]);
// 生成模拟的备份记录仅5条近30天内
// 确保备份时间与备份大小排序一致(从大到小,最新到最早)
const generateMockData = () => {
const records = [];
const today = new Date();
// 只生成5条记录
const totalRecords = 5;
for (let i = 0; i < totalRecords; i++) {
// 按顺序生成过去的日期(最近的日期对应最大的备份)
// i=0 是最近的日期i=4 是最远的日期
const daysAgo = i * 5; // 每5天一个备份
const recordDate = new Date(today.getTime() - daysAgo * 24 * 60 * 60 * 1000);
// 设置固定时间,便于观察排序
const hours = '0' + (10 + i).toString().slice(-1); // 10, 11, 12, 13, 14点
const minutes = '30';
// 格式化日期
const formattedDate = `${recordDate.getFullYear()}-${
(recordDate.getMonth() + 1).toString().padStart(2, '0')
}-${
recordDate.getDate().toString().padStart(2, '0')
} ${hours}:${minutes}`;
// 备份大小与时间正相关(最新的备份最大)
// i=0 最大i=4 最小
const size = (10 - i * 1.5).toFixed(2);
// 备份状态
const status = i !== 3 ? '成功' : '失败';
records.push({
date: formattedDate,
size: `${size} GB`,
sizeValue: parseFloat(size),
dateValue: recordDate.getTime(), // 用于日期排序的时间戳
status: status
});
}
// 按备份大小倒序排列(同时也是按时间倒序排列)
return records.sort((a, b) => b.sizeValue - a.sizeValue);
};
// 计算分页后的数据
const pagedRecords = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
return backupRecords.value.slice(startIndex, startIndex + pageSize.value);
});
// 保存配置
const saveConfig = () => {
ElMessage.success('备份配置已保存');
console.log('保存的配置:', configForm.value);
};
// 恢复备份
const restoreBackup = (record) => {
ElMessage.info(`正在恢复 ${record.date} 的备份 (${record.size})`);
console.log('恢复备份:', record);
};
// 批量恢复
const batchRestore = () => {
ElMessage.warning('批量恢复功能已触发');
console.log('执行批量恢复');
};
// 分页大小变化
const handleSizeChange = (val) => {
pageSize.value = val;
currentPage.value = 1;
};
// 当前页变化
const handleCurrentChange = (val) => {
currentPage.value = val;
document.querySelector('.el-table')?.scrollIntoView({ behavior: 'smooth' });
};
// 页面加载时生成数据
onMounted(() => {
backupRecords.value = generateMockData();
});
</script>
<style scoped>
.backup-management {
padding: 20px;
background-color: #f5f7fa;
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
}
.page-title {
color: #1f2329;
margin-bottom: 25px;
font-weight: 600;
}
.section-title {
color: #4e5969;
font-size: 16px;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 1px solid #e5e6eb;
}
.text-sm {
font-size: 14px;
}
.text-gray-500 {
color: #86909c;
}
.config-section, .records-section {
background-color: #fff;
padding: 20px;
border-radius: 4px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
margin-bottom: 20px;
}
.config-form {
max-width: 600px;
}
.pagination-container {
margin: 15px 0;
}
.pagination {
text-align: right;
}
.batch-restore-btn {
margin-top: 10px;
}
.font-medium {
font-weight: 500;
}
</style>

View File

@@ -0,0 +1,268 @@
<template>
<div class="control-panel-container">
<!-- 控制面板区域 -->
<el-card class="control-card" header="控制面板">
<!-- 开关按钮 -->
<div class="button-group">
<el-button
type="success"
icon="el-icon-poweroff"
@click="handlePower('on')"
:disabled="powerState === 'on'"
>
开机
</el-button>
<el-button
type="danger"
icon="el-icon-poweroff"
@click="handlePower('off')"
:disabled="powerState === 'off'"
>
关机
</el-button>
</div>
<!-- 温度调节 -->
<div class="temperature-control">
<span>温度调节 (°C)</span>
<el-slider
v-model="temperature"
:min="1"
:max="30"
@change="handleTemperatureChange"
:disabled="powerState === 'off'"
/>
<div class="temperature-value">{{ temperature }}°C</div>
</div>
<!-- 模式切换 -->
<div class="mode-switch">
<span>制冷模式</span>
<el-select
v-model="coolingMode"
placeholder="请选择模式"
@change="handleModeChange"
:disabled="powerState === 'off'"
>
<el-option
v-for="mode in modeOptions"
:key="mode.value"
:label="mode.label"
:value="mode.value"
/>
</el-select>
</div>
</el-card>
<!-- 操作日志区域 -->
<el-card class="log-card" header="操作日志" style="margin-top: 20px;">
<el-table
:data="currentPageLogs"
border
style="width: 100%"
>
<el-table-column
prop="time"
label="时间"
width="200"
/>
<el-table-column
prop="type"
label="操作类型"
width="120"
>
<template #default="scope">
<el-tag :type="scope.row.tagType">{{ scope.row.type }}</el-tag>
</template>
</el-table-column>
<el-table-column
prop="result"
label="操作结果"
/>
</el-table>
<!-- 分页组件默认每页5条 -->
<div class="pagination-container">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="currentPage"
:page-sizes="[5, 10, 20, 50]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="filteredLogs.length"
/>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElCard, ElButton, ElSlider, ElSelect, ElOption, ElTable, ElTableColumn, ElTag, ElPagination } from 'element-plus'
// 响应式数据
const powerState = ref('off')
const temperature = ref(4)
const coolingMode = ref('energySaving')
const modeOptions = ref([
{ label: '节能模式', value: 'energySaving' },
{ label: '快速制冷', value: 'fastCooling' },
{ label: '智能模式', value: 'smartMode' }
])
// 分页配置默认每页5条
const currentPage = ref(1)
const pageSize = ref(5)
// 原始日志数据模拟最近3天
const operationLogs = ref([])
// 页面加载时生成模拟数据最近3天
onMounted(() => {
generateMockData()
})
// 生成模拟数据最近3天内的操作日志
const generateMockData = () => {
const mockLogs = []
const operations = [
{ type: '开机', tagType: 'success' },
{ type: '关机', tagType: 'danger' },
{ type: '温度调节', tagType: 'primary' },
{ type: '模式切换', tagType: 'info' }
]
// 生成多条模拟数据时间覆盖最近3天
for (let i = 0; i < 30; i++) {
// 随机生成最近3天内的时间0~71小时前
const randomHours = Math.floor(Math.random() * 72)
const date = new Date()
date.setHours(date.getHours() - randomHours)
// 随机选操作类型
const operation = operations[Math.floor(Math.random() * operations.length)]
// 格式化时间
const time = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}:${date.getSeconds().toString().padStart(2, '0')}`
mockLogs.push({
time,
type: operation.type,
result: '成功',
tagType: operation.tagType
})
}
// 按时间倒序(最新的在最前)
mockLogs.sort((a, b) => new Date(b.time) - new Date(a.time))
operationLogs.value = mockLogs
}
// 处理开机/关机
const handlePower = (state) => {
powerState.value = state
const logType = state === 'on' ? '开机' : '关机'
addOperationLog(logType, '成功', state === 'on' ? 'success' : 'danger')
}
// 处理温度调节
const handleTemperatureChange = (val) => {
if (powerState.value === 'on') {
addOperationLog('温度调节', `设置为 ${val}°C`, 'primary')
}
}
// 处理模式切换
const handleModeChange = () => {
if (powerState.value === 'on') {
const modeLabel = modeOptions.value.find(mode => mode.value === coolingMode.value)?.label
addOperationLog('模式切换', `切换为 ${modeLabel}`, 'info')
}
}
// 新增操作日志(自动联动分页)
const addOperationLog = (type, result, tagType) => {
const now = new Date()
const time = `${now.getFullYear()}-${(now.getMonth() + 1).toString().padStart(2, '0')}-${now.getDate().toString().padStart(2, '0')} ${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}`
// 插入到日志最前面
operationLogs.value.unshift({ time, type, result, tagType })
// 重置到第1页保证新增的日志能直接看到
currentPage.value = 1
}
// 过滤只保留最近3天的日志
const filteredLogs = computed(() => {
const threeDaysAgo = Date.now() - 3 * 24 * 60 * 60 * 1000
return operationLogs.value.filter(log => {
const logTime = new Date(log.time).getTime()
return logTime >= threeDaysAgo
})
})
// 当前页要显示的日志(根据分页计算)
const currentPageLogs = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value
const endIndex = startIndex + pageSize.value
return filteredLogs.value.slice(startIndex, endIndex)
})
// 每页条数改变
const handleSizeChange = (val) => {
pageSize.value = val
currentPage.value = 1
}
// 页码改变
const handleCurrentChange = (val) => {
currentPage.value = val
}
</script>
<style scoped>
.control-panel-container {
padding: 20px;
max-width: 1200px;
margin: 0 auto;
}
.control-card {
padding: 20px;
}
.button-group {
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.temperature-control,
.mode-switch {
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.temperature-control span,
.mode-switch span {
width: 100px;
text-align: right;
}
.temperature-control {
flex: 1;
}
.el-slider {
flex: 1;
margin: 0 10px;
}
.temperature-value {
min-width: 50px;
text-align: center;
}
.log-card {
padding: 20px;
}
.pagination-container {
margin-top: 15px;
text-align: right;
}
</style>

View File

@@ -0,0 +1,574 @@
<template>
<!-- 模板部分无修改保持原样 -->
<div class="device-status">
<!-- 顶部布局概览饼图 + 详细状态 -->
<div class="top-layout">
<!-- 设备状态概览ECharts饼图 -->
<div class="card overview-card">
<div class="card-header">
<span>设备状态概览</span>
</div>
<div class="legend">
<div class="legend-item" :style="{color: onlineColor}">
<span class="dot" :style="{background: onlineColor}"></span> 在线 ({{ onlineRatio.toFixed(1) }}%)
</div>
<div class="legend-item" :style="{color: offlineColor}">
<span class="dot" :style="{background: offlineColor}"></span> 离线 ({{ offlineRatio.toFixed(1) }}%)
</div>
<div class="legend-item" :style="{color: faultColor}">
<span class="dot" :style="{background: faultColor}"></span> 故障 ({{ faultRatio.toFixed(1) }}%)
</div>
</div>
<div ref="chartRef" class="chart-container"></div>
</div>
<!-- 详细状态卡片 -->
<div class="card detail-card">
<div class="card-header">详细状态</div>
<div class="detail-row">
<label>在线状态</label>
<span :class="['status-tag', statusMap[onlineStatus]]">
{{ onlineStatus }}
</span>
</div>
<div class="detail-row">
<label>电量</label>
<span class="battery-tag">{{ battery }}%</span>
</div>
<div class="detail-row">
<label>制冷剂压力</label>
<span class="pressure-tag">{{ pressure.toFixed(1) }} MPa</span>
</div>
<div class="detail-row">
<label>故障代码</label>
<span :class="['fault-tag', faultCode === '无' ? 'normal' : 'error']">
{{ faultCode }}
</span>
</div>
</div>
</div>
<!-- 设备状态记录表格 -->
<div class="card history-card">
<div class="card-header">设备状态记录</div>
<table class="history-table">
<thead>
<tr>
<th>设备ID</th>
<th>状态</th>
<th>电量</th>
<th>制冷剂压力</th>
<th>故障代码</th>
<th>时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr v-for="(item, idx) in historyList" :key="idx">
<td>{{ item.deviceId }}</td>
<td>
<span :class="['status-tag', statusMap[item.status]]">
{{ item.status }}
</span>
</td>
<td>{{ item.battery }}%</td>
<td>{{ item.pressure.toFixed(1) }} MPa</td>
<td>
<span :class="['fault-tag', item.faultCode === '无' ? 'normal' : 'error']">
{{ item.faultCode }}
</span>
</td>
<td>{{ item.time }}</td>
<td>
<button class="oper-btn edit" @click="openEditDialog(item, idx)">编辑</button>
<button class="oper-btn delete" @click="handleDelete(idx)">删除</button>
</td>
</tr>
</tbody>
</table>
</div>
<!-- 编辑弹框 -->
<div v-if="isEditDialogOpen" class="dialog-overlay">
<div class="dialog">
<div class="dialog-header">
<h3>编辑设备状态</h3>
<button class="close-btn" @click="closeEditDialog">×</button>
</div>
<div class="dialog-body">
<div class="form-group">
<label>设备ID</label>
<input type="text" v-model="editForm.deviceId" readonly />
</div>
<div class="form-group">
<label>状态</label>
<select v-model="editForm.status">
<option value="在线">在线</option>
<option value="离线">离线</option>
<option value="故障">故障</option>
</select>
</div>
<div class="form-group">
<label>电量 (%)</label>
<input type="number" v-model="editForm.battery" min="0" max="100" />
</div>
<div class="form-group">
<label>制冷剂压力 (MPa)</label>
<input type="number" v-model="editForm.pressure" min="0" step="0.1" />
</div>
<div class="form-group">
<label>故障代码</label>
<input type="text" v-model="editForm.faultCode" />
</div>
</div>
<div class="dialog-footer">
<button class="btn cancel" @click="closeEditDialog">取消</button>
<button class="btn save" @click="saveEdit">保存</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, watch } from 'vue'
import * as echarts from 'echarts'
// 状态颜色配置
const onlineColor = '#4267B2' // 在线
const offlineColor = '#4CAF50' // 离线
const faultColor = '#FFB300' // 故障
// 比例数据(带小数位控制)
const onlineRatio = ref(60.0)
const offlineRatio = ref(25.0)
const faultRatio = ref(15.0)
// ========== 关键修改1添加「生成最近3天随机时间」的工具函数 ==========
const generateRecent3DaysTime = () => {
// 1. 获取当前时间
const now = new Date()
// 2. 随机生成「0-2」的天数偏移0=今天1=昨天2=前天)
const dayOffset = Math.floor(Math.random() * 3)
// 3. 计算目标日期(当前日期 - 偏移天数)
const targetDate = new Date(now)
targetDate.setDate(now.getDate() - dayOffset)
// 4. 补零工具函数(处理个位数的月/日/时/分)
const padZero = (num: number) => num.toString().padStart(2, '0')
// 5. 提取年、月、日格式YYYY-MM-DD
const year = targetDate.getFullYear()
const month = padZero(targetDate.getMonth() + 1) // 月份从0开始需+1
const day = padZero(targetDate.getDate())
// 6. 生成随机小时00-23和分钟00-59
const hour = padZero(Math.floor(Math.random() * 24))
const minute = padZero(Math.floor(Math.random() * 60))
// 7. 拼接成「YYYY-MM-DD HH:mm」格式返回
return `${year}-${month}-${day} ${hour}:${minute}`
}
// 响应式数据
const onlineStatus = ref('在线') // 在线/离线/故障
const battery = ref(85) // 电量百分比
const pressure = ref(2.5) // 制冷剂压力
const faultCode = ref('无') // 故障代码
const historyList = reactive([ // 生成3条不同状态的记录
{
deviceId: 'DEV-001',
status: '在线',
battery: 92,
pressure: 2.8,
faultCode: '无',
time: generateRecent3DaysTime() // ========== 关键修改2调用函数生成时间 ==========
},
{
deviceId: 'DEV-002',
status: '离线',
battery: 45,
pressure: 1.9,
faultCode: '无',
time: generateRecent3DaysTime() // ========== 关键修改2调用函数生成时间 ==========
},
{
deviceId: 'DEV-003',
status: '故障',
battery: 78,
pressure: 0.5,
faultCode: 'E102',
time: generateRecent3DaysTime() // ========== 关键修改2调用函数生成时间 ==========
}
])
// 编辑弹框相关
const isEditDialogOpen = ref(false)
const currentEditIndex = ref(-1)
const editForm = reactive({
deviceId: '',
status: '在线',
battery: 100,
pressure: 2.5,
faultCode: '无'
})
// ECharts实例 & 配置
const chartRef = ref<HTMLDivElement | null>(null)
let chart: echarts.ECharts | null = null
const chartOption = reactive({
title: { text: '设备状态概览', left: 'center', textStyle: { fontSize: 16 } },
series: [{
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '55%'],
data: [
{ value: onlineRatio.value, name: '在线', itemStyle: { color: onlineColor } },
{ value: offlineRatio.value, name: '离线', itemStyle: { color: offlineColor } },
{ value: faultRatio.value, name: '故障', itemStyle: { color: faultColor } }
],
label: { show: true, position: 'outside', formatter: '{b}\n{c}%' }
}]
})
// 初始化图表
onMounted(() => {
if (chartRef.value) {
chart = echarts.init(chartRef.value)
chart.setOption(chartOption)
window.addEventListener('resize', () => chart?.resize())
}
})
// 监听数据变化,更新图表
watch([onlineStatus, battery, pressure, faultCode], () => {
updateChartData();
})
// 更新图表数据(带小数位控制)
const updateChartData = () => {
chart?.setOption(chartOption)
}
// 状态映射(用于样式)
const statusMap = {
在线: 'online',
离线: 'offline',
故障: 'fault'
}
// 打开编辑弹框
const openEditDialog = (item: any, index: number) => {
currentEditIndex.value = index
// 复制数据到编辑表单
editForm.deviceId = item.deviceId
editForm.status = item.status
editForm.battery = item.battery
editForm.pressure = item.pressure
editForm.faultCode = item.faultCode
isEditDialogOpen.value = true
}
// 关闭编辑弹框
const closeEditDialog = () => {
isEditDialogOpen.value = false
currentEditIndex.value = -1
}
// 保存编辑内容
const saveEdit = () => {
if (currentEditIndex.value !== -1) {
// 更新历史记录中的数据
historyList[currentEditIndex.value] = {
...historyList[currentEditIndex.value],
status: editForm.status,
battery: editForm.battery,
pressure: parseFloat(editForm.pressure.toFixed(1)),
faultCode: editForm.faultCode
}
closeEditDialog()
}
}
// 删除操作
const handleDelete = (index: number) => {
if (confirm('确定删除该记录吗?')) {
historyList.splice(index, 1)
}
}
</script>
<style scoped>
/* 样式部分无修改,保持原样 */
.device-status {
padding: 24px;
background: #f5f7fa;
font-family: "Microsoft YaHei", sans-serif;
}
.top-layout {
display: flex;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.card {
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 20px;
}
.overview-card {
flex: 2;
min-width: 300px;
}
.detail-card {
flex: 1;
min-width: 240px;
}
.card-header {
font-size: 16px;
font-weight: 600;
margin-bottom: 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.legend {
margin: 12px 0;
display: flex;
gap: 24px;
flex-wrap: wrap;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
}
.legend-item .dot {
width: 12px;
height: 12px;
border-radius: 50%;
}
.chart-container {
height: 300px;
}
.detail-row {
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.detail-row label {
color: #666;
width: 80px;
text-align: right;
}
.status-tag {
padding: 4px 8px;
border-radius: 4px;
color: #fff;
font-size: 12px;
}
.status-tag.online { background: #4CAF50; }
.status-tag.offline { background: #8BC34A; }
.status-tag.fault { background: #F44336; }
.battery-tag {
background: #2196F3;
padding: 4px 8px;
border-radius: 4px;
color: #fff;
font-size: 12px;
}
.pressure-tag {
background: #FFB300;
padding: 4px 8px;
border-radius: 4px;
color: #fff;
font-size: 12px;
}
.fault-tag {
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
}
.fault-tag.normal { background: #8BC34A; color: #fff; }
.fault-tag.error { background: #F44336; color: #fff; }
.history-card {
width: 100%;
}
.history-table {
width: 100%;
border-collapse: collapse;
}
.history-table th, .history-table td {
padding: 12px;
border: 1px solid #ebeef5;
text-align: left;
}
.history-table thead {
background: #f8f9fa;
}
.oper-btn {
padding: 4px 8px;
margin-right: 6px;
border: none;
border-radius: 4px;
cursor: pointer;
color: #fff;
transition: background 0.3s;
}
.oper-btn.edit { background: #409eff; }
.oper-btn.delete { background: #f44336; }
.oper-btn.edit:hover { background: #66b1ff; }
.oper-btn.delete:hover { background: #ff6659; }
/* 编辑弹框样式 */
.dialog-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: 1000;
}
.dialog {
background: #fff;
border-radius: 8px;
width: 90%;
max-width: 500px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
}
.dialog-header {
padding: 16px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: center;
}
.dialog-header h3 {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 20px;
cursor: pointer;
color: #999;
transition: color 0.2s;
}
.close-btn:hover {
color: #333;
}
.dialog-body {
padding: 16px;
}
.form-group {
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 12px;
}
.form-group label {
width: 120px;
text-align: right;
color: #666;
}
.form-group input,
.form-group select {
flex: 1;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
}
.dialog-footer {
padding: 12px 16px;
border-top: 1px solid #eee;
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
border: none;
}
.btn.cancel {
background: #f5f5f5;
color: #666;
}
.btn.cancel:hover {
background: #e5e5e5;
}
.btn.save {
background: #409eff;
color: #fff;
}
.btn.save:hover {
background: #66b1ff;
}
@media (max-width: 768px) {
.top-layout {
flex-direction: column;
}
.overview-card, .detail-card {
width: 100%;
}
.form-group {
flex-direction: column;
align-items: stretch;
}
.form-group label {
text-align: left;
margin-bottom: 4px;
}
}
</style>

View File

@@ -0,0 +1,508 @@
<template>
<div class="dashboard-container">
<!-- 顶部区域 -->
<header class="header">
<h1>概览</h1>
<div class="unit-toggle">
<button class="unit-btn" :class="{ active: unit === 'celsius' }" @click="unit = 'celsius'">摄氏度(°C)</button>
<button class="unit-btn" :class="{ active: unit === 'fahrenheit' }" @click="unit = 'fahrenheit'">华氏度(°F)</button>
</div>
<button class="refresh-btn" @click="refreshData">
<i class="refresh-icon"></i> 刷新数据
</button>
</header>
<!-- 中间内容区 -->
<main class="content">
<!-- 趋势图卡片 -->
<section class="card trend-chart-card">
<h2>温度变化趋势图</h2>
<div class="chart-wrapper">
<!-- 使用SVG绘制折线图 -->
<svg class="temperature-chart" viewBox="0 0 800 250" preserveAspectRatio="xMidYMid meet">
<!-- 网格线 -->
<g class="grid-lines">
<line v-for="(value, index) in [minTemp, midTemp, maxTemp]" :key="index"
x1="40" :y1="getYCoord(value)"
x2="760" :y2="getYCoord(value)"
stroke="#eee" stroke-width="1" />
</g>
<!-- 坐标轴 -->
<line x1="40" y1="250" x2="40" y2="50" stroke="#ccc" stroke-width="1" />
<line x1="40" y1="250" x2="760" y2="250" stroke="#ccc" stroke-width="1" />
<!-- 折线图 -->
<polyline
v-if="temperatureData.length >= 2"
:points="chartPoints"
fill="none"
stroke="#409eff"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round" />
<!-- 数据点 -->
<circle
v-for="(point, index) in temperatureData"
:key="index"
:cx="getXCoord(index)"
:cy="getYCoord(point.value)"
r="4"
fill="#409eff"
:title="formatTemperature(point.value)" />
<!-- X轴标签 -->
<text
v-for="(point, index) in temperatureData"
:key="'x-label-' + index"
v-if="shouldShowXLabel(index)"
:x="getXCoord(index)"
y="265"
text-anchor="middle"
font-size="12"
fill="#666">
{{ point.time }}
</text>
<!-- Y轴标签 -->
<text
v-for="(value, index) in [minTemp, midTemp, maxTemp]"
:key="'y-label-' + index"
x="30"
:y="getYCoord(value)"
text-anchor="end"
font-size="12"
fill="#666">
{{ formatTemperature(value) }}
</text>
</svg>
</div>
</section>
<!-- 实时数据卡片 -->
<section class="card data-card">
<h2>实时数据</h2>
<ul class="data-list">
<li>当前温度<span>{{ formatTemperature(currentTemp) }}</span></li>
<li>最高温度<span>{{ formatTemperature(maxTemp) }}</span></li>
<li>最低温度<span>{{ formatTemperature(minTemp) }}</span></li>
<li>温度波动范围<span>{{ formatTemperature(tempRange) }}</span></li>
<li v-if="refreshTip" class="refresh-tip">{{ refreshTip }}</li>
</ul>
</section>
</main>
<!-- 异常报警区 -->
<footer class="alarm-section">
<h2>异常报警提示</h2>
<div class="alarm-content" :class="{ normal: !hasAlarm, warning: hasAlarm }">
<span class="alarm-icon">{{ hasAlarm ? '⚠' : '✓' }}</span>
<p>{{ alarmMessage }}</p>
</div>
</footer>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
// 温度单位
const unit = ref<'celsius' | 'fahrenheit'>('celsius');
// 温度数据
const temperatureData = ref<Array<{ time: string; value: number }>>([]);
const currentTemp = ref(0);
const maxTemp = ref(0);
const minTemp = ref(0);
const midTemp = computed(() => (maxTemp.value + minTemp.value) / 2);
const tempRange = computed(() => {
return parseFloat((maxTemp.value - minTemp.value).toFixed(1));
});
// 报警和刷新提示
const hasAlarm = ref(false);
const alarmMessage = ref('当前温度正常,无异常报警。');
const refreshTip = ref(''); // 用于显示刷新提示信息
/**
* 生成初始的12小时数据
*/
const generateInitialData = () => {
const data: Array<{ time: string; value: number }> = [];
const now = new Date();
const currentHour = now.getHours();
// 生成过去12小时的数据包含当前小时
for (let i = 11; i >= 0; i--) {
const hour = currentHour - i;
const displayHour = hour < 0 ? hour + 24 : hour; // 处理跨天
const timeStr = `${displayHour.toString().padStart(2, '0')}:00`;
// 生成有趋势的温度数据
const baseTemp = 5;
const hourFactor = Math.sin(i / 3) * 1.5; // 周期波动
const randomFactor = (Math.random() - 0.5) * 1.5; // 随机波动
const temp = parseFloat((baseTemp + hourFactor + randomFactor).toFixed(1));
data.push({ time: timeStr, value: temp });
}
return data;
};
/**
* 生成单个新的温度数据点
*/
const generateNewDataPoint = () => {
const lastData = temperatureData.value.at(-1)!;
const lastHour = parseInt(lastData.time.split(':')[0], 10);
const nextHour = (lastHour + 1) % 24; // 处理23:00 -> 00:00
const timeStr = `${nextHour.toString().padStart(2, '0')}:00`;
// 基于最后一个数据点的温度波动
const lastTemp = lastData.value;
const tempChange = (Math.random() - 0.5) * 2; // ±1°C 波动
const newTemp = parseFloat((lastTemp + tempChange).toFixed(1));
return { time: timeStr, value: newTemp };
};
/**
* 检查是否可以刷新数据(当前时间是否超过最后一条数据的小时)
*/
const canRefreshData = () => {
if (temperatureData.value.length === 0) return true;
const lastData = temperatureData.value.at(-1)!;
const lastHour = parseInt(lastData.time.split(':')[0], 10);
const currentHour = new Date().getHours();
// 情况1当前小时 > 最后数据小时 → 可以刷新如最后14:00当前15:00
// 情况2跨天情况最后23:00当前00:00 → 可以刷新
return currentHour > lastHour || (lastHour === 23 && currentHour === 0);
};
/**
* 刷新数据:仅当超过当前小时才添加新数据
*/
const refreshData = () => {
if (canRefreshData()) {
const newDataPoint = generateNewDataPoint();
temperatureData.value.push(newDataPoint);
updateStats();
refreshTip.value = ''; // 清空提示
} else {
const lastData = temperatureData.value.at(-1)!;
refreshTip.value = `当前时间未超过${lastData.time},暂无需更新数据`;
// 3秒后自动清除提示
setTimeout(() => {
refreshTip.value = '';
}, 3000);
}
};
/**
* 更新统计数据
*/
const updateStats = () => {
const allTempValues = temperatureData.value.map(item => item.value);
currentTemp.value = temperatureData.value.at(-1)!.value;
maxTemp.value = Math.max(...allTempValues);
minTemp.value = Math.min(...allTempValues);
checkAlarmStatus();
};
/**
* 控制X轴标签显示修复初始数据不显示问题
*/
const shouldShowXLabel = (index: number) => {
const total = temperatureData.value.length;
// 数据量较少时≤8条显示所有标签
if (total <= 8) return true;
// 数据量较多时(>8条按比例显示确保至少显示6个标签
const step = Math.max(1, Math.floor(total / 6));
return index % step === 0;
};
/**
* 计算X轴坐标
*/
const getXCoord = (index: number) => {
if (temperatureData.value.length < 2) return 400;
return 40 + (index * (720 / (temperatureData.value.length - 1)));
};
/**
* 计算Y轴坐标
*/
const getYCoord = (temp: number) => {
if (maxTemp.value === minTemp.value) return 150;
return 250 - (temp - minTemp.value) * (200 / (maxTemp.value - minTemp.value));
};
// 检查报警状态
const checkAlarmStatus = () => {
const randomAlarm = Math.random() < 0.1;
if (randomAlarm) {
hasAlarm.value = true;
alarmMessage.value = `温度异常!当前温度 ${formatTemperature(currentTemp.value)} 超出正常范围。`;
} else {
hasAlarm.value = false;
alarmMessage.value = '当前温度正常,无异常报警。';
}
};
// 格式化温度显示
const formatTemperature = (value: number) => {
if (unit.value === 'fahrenheit') {
const fahrenheit = parseFloat(((value * 9/5) + 32).toFixed(1));
return `${fahrenheit}°F`;
}
return `${parseFloat(value.toFixed(1))}°C`;
};
// 计算图表点坐标
const chartPoints = computed(() => {
if (temperatureData.value.length < 2) return '';
return temperatureData.value.map((point, index) => {
const x = getXCoord(index);
const y = getYCoord(point.value);
return `${x},${y}`;
}).join(' ');
});
// 初始化加载最近12小时数据
onMounted(() => {
const initialData = generateInitialData();
temperatureData.value = initialData;
updateStats();
});
</script>
<style scoped>
.dashboard-container {
padding: 24px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
background: #f5f7fa;
min-height: 100vh;
box-sizing: border-box;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24px;
flex-wrap: wrap;
gap: 16px;
}
.header h1 {
font-size: 22px;
font-weight: 600;
margin: 0;
color: #333;
}
.unit-toggle {
display: flex;
gap: 8px;
}
.unit-btn {
padding: 6px 12px;
border: 1px solid #ccc;
border-radius: 4px;
background: #fff;
cursor: pointer;
transition: all 0.2s ease;
}
.unit-btn.active {
background: #e6f3ff;
border-color: #409eff;
color: #409eff;
font-weight: 500;
}
.refresh-btn {
padding: 6px 12px;
border: 1px solid #409eff;
border-radius: 4px;
background: #409eff;
color: white;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 6px;
transition: background-color 0.2s ease;
}
.refresh-btn:hover {
background: #66b1ff;
}
.refresh-icon {
display: inline-block;
transition: transform 0.5s ease;
}
.refresh-btn:active .refresh-icon {
transform: rotate(360deg);
}
.content {
display: flex;
gap: 24px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.card {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.card h2 {
font-size: 16px;
margin: 0 0 16px 0;
font-weight: 600;
color: #606266;
display: flex;
align-items: center;
gap: 8px;
}
.card h2::before {
content: '';
display: inline-block;
width: 4px;
height: 16px;
background-color: #409eff;
border-radius: 2px;
}
.trend-chart-card {
flex: 2;
min-width: 300px;
}
.chart-wrapper {
position: relative;
width: 100%;
height: 300px;
}
.temperature-chart {
width: 100%;
height: 100%;
}
.data-card {
flex: 1;
min-width: 250px;
}
.data-list {
list-style: none;
padding: 0;
margin: 0;
}
.data-list li {
margin-bottom: 16px;
font-size: 14px;
color: #606266;
padding: 8px 0;
border-bottom: 1px dashed #f0f0f0;
}
.data-list li:last-child {
border-bottom: none;
}
.data-list li span {
color: #409eff;
font-weight: 600;
margin-left: 8px;
}
/* 刷新提示样式 */
.refresh-tip {
color: #faad14;
font-style: italic;
}
.alarm-section {
background: #fff;
border: 1px solid #ebeef5;
border-radius: 8px;
padding: 16px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.alarm-content {
padding: 12px 16px;
border-radius: 4px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s ease;
}
.alarm-content.normal {
background: #f0fff4;
border: 1px solid #e1f3e8;
}
.alarm-content.warning {
background: #fff9e6;
border: 1px solid #fff2cc;
}
.alarm-icon {
font-size: 18px;
}
.alarm-content.normal .alarm-icon {
color: #52c41a;
}
.alarm-content.warning .alarm-icon {
color: #faad14;
}
.alarm-content p {
margin: 0;
font-size: 14px;
color: #606266;
}
.alarm-content.warning p {
color: #e6a23c;
font-weight: 500;
}
@media (max-width: 768px) {
.content {
flex-direction: column;
}
.trend-chart-card, .data-card {
width: 100%;
}
.header {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View File

@@ -0,0 +1,558 @@
<template>
<div class="firmware-upgrade-container">
<!-- 页面标题 -->
<h2 class="page-title">固件升级管理</h2>
<!-- 主内容区域添加滚动条 -->
<div class="main-content">
<!-- 版本信息区域 -->
<div class="version-info">
<div class="current-version card">
<h3>当前固件版本</h3>
<p>版本号: {{ currentVersion }}</p>
<p>发布日期: {{ formatDate(currentDate) }}</p>
<p class="update-content">更新内容: 优化温控算法提升能效比</p>
<div class="version-actions">
<div class="status running">运行中</div>
<button class="btn check-update">检查更新</button>
</div>
</div>
<div class="latest-version card">
<h3>最新固件版本</h3>
<p>版本号: {{ latestVersion }}</p>
<p>发布日期: {{ formatDate(latestDate) }}</p>
<p class="update-content">更新内容: 新增远程控制功能修复漏洞</p>
<div class="version-actions">
<div class="status upgradable">可升级</div>
<button class="btn upgrade-now">立即升级</button>
</div>
</div>
</div>
<!-- 升级操作区域 -->
<div class="upgrade-operation card">
<h3>升级操作</h3>
<label for="upgrade-package">选择升级包</label>
<input type="file" id="upgrade-package" accept=".zip,.bin" />
<button class="btn upload-upgrade">上传并升级</button>
</div>
<!-- 升级日志区域 -->
<div class="upgrade-log card">
<h3>升级日志</h3>
<table>
<thead>
<tr>
<th>时间 <span class="sort-indicator"></span></th>
<th>版本 <span class="sort-indicator"></span></th>
<th>操作</th>
<th>状态</th>
</tr>
</thead>
<tbody>
<tr v-for="(log, index) in currentPageLogs" :key="index">
<td>{{ formatDate(log.time) }}</td>
<td>{{ log.version }}</td>
<td>{{ log.operation }}</td>
<td>
<div :class="['status', log.status]">{{ log.statusText }}</div>
</td>
</tr>
<tr v-if="currentPageLogs.length === 0">
<td colspan="4" class="no-data">当前页没有数据</td>
</tr>
</tbody>
</table>
<!-- 分页控件 -->
<div class="pagination">
<button
class="page-btn"
@click="currentPage = 1"
:disabled="currentPage === 1"
>
首页
</button>
<button
class="page-btn"
@click="currentPage--"
:disabled="currentPage === 1"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
class="page-btn"
@click="currentPage++"
:disabled="currentPage === totalPages"
>
下一页
</button>
<button
class="page-btn"
@click="currentPage = totalPages"
:disabled="currentPage === totalPages"
>
末页
</button>
<div class="page-size">
<span>每页:</span>
<select v-model="pageSize" @change="handlePageSizeChange">
<option value="5">5</option>
<option value="10">10</option>
</select>
</div>
</div>
</div>
<!-- 升级回滚区域 -->
<div class="rollback-section card">
<h3>升级回滚</h3>
<p>如果升级后出现问题您可以回滚到上一个稳定版本</p>
<div class="rollback-versions">
<div class="rollback-version-item">
<span class="version-label">可回滚版本:</span>
<span class="version-value">v1.2.2</span>
</div>
<div class="rollback-version-item">
<span class="version-label">发布日期:</span>
<span class="version-value">{{ formatDate(rollbackVersionDate) }}</span>
</div>
</div>
<button class="btn rollback-btn" @click="handleRollback">回滚到上一版本</button>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import dayjs from 'dayjs';
// 定义版本号变量
const currentVersion = ref('v1.2.3');
const latestVersion = ref('v1.3.0');
const rollbackVersionDate = ref(dayjs().subtract(15, 'day').toDate());
// 模拟数据 - 确保当前版本日期早于最新版本日期
const currentDate = ref(dayjs().subtract(10, 'day').toDate()); // 当前版本发布于10天前
const latestDate = ref(dayjs().subtract(5, 'day').toDate()); // 最新版本发布于5天前
// 版本号比较函数 - 确保正确的版本排序(降序)
const compareVersions = (v1, v2) => {
const v1Parts = v1.replace('v', '').split('.').map(Number);
const v2Parts = v2.replace('v', '').split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const part1 = v1Parts[i] || 0;
const part2 = v2Parts[i] || 0;
if (part1 > part2) return -1; // v1 更大,排在前面
if (part1 < part2) return 1; // v2 更大,排在前面
}
return 0; // 版本号相同
};
// 生成模拟日志数据(确保日期和版本号都按倒序排列)
const generateLogs = () => {
const logs = [];
// 只保留成功和失败两种状态
const statuses = [
{ status: 'success', statusText: '成功' },
{ status: 'fail', statusText: '失败' }
];
// 1. 首先添加最新的日志(当前版本,一定成功)
logs.push({
time: currentDate.value, // 与当前版本发布日期一致(最新的日期)
version: currentVersion.value,
operation: '升级',
...statuses[0] // 强制成功
});
// 2. 生成历史版本号(低于当前版本)
const versions = [];
const [major, minor, patch] = currentVersion.value.replace('v', '').split('.').map(Number);
// 生成当前版本之前的版本号(确保版本号递减)
for (let m = major; m >= 1; m--) {
const maxMin = m === major ? minor - 1 : 5;
for (let mn = maxMin; mn >= 0; mn--) {
const maxP = (m === major && mn === minor - 1) ? patch : 9;
for (let p = maxP; p >= 0; p--) {
versions.push(`v${m}.${mn}.${p}`);
}
}
}
// 3. 添加历史日志,确保日期严格递减(越来越早)
for (let i = 1; i <= 14; i++) {
const randomStatus = statuses[Math.floor(Math.random() * statuses.length)];
// 从版本列表中随机选择,但确保不重复
const randomIndex = Math.floor(Math.random() * versions.length);
const version = versions.splice(randomIndex, 1)[0];
// 关键:确保日期严格早于前一条记录
const daysAgo = 10 + i;
logs.push({
time: dayjs().subtract(daysAgo, 'day').toDate(),
version: version,
operation: '升级',
...randomStatus
});
}
// 4. 最终排序:先按版本号降序,版本号相同则按时间降序
return logs.sort((a, b) => {
const versionCompare = compareVersions(a.version, b.version);
return versionCompare !== 0 ? versionCompare : new Date(b.time) - new Date(a.time);
});
};
// 生成日志数据
const upgradeLogs = ref(generateLogs());
// 分页相关
const currentPage = ref(1);
const pageSize = ref(5);
// 计算当前页显示的日志
const currentPageLogs = computed(() => {
const startIndex = (currentPage.value - 1) * pageSize.value;
return upgradeLogs.value.slice(startIndex, startIndex + pageSize.value);
});
// 计算总页数
const totalPages = computed(() => {
return Math.ceil(upgradeLogs.value.length / pageSize.value);
});
// 处理每页显示条数变化
const handlePageSizeChange = () => {
currentPage.value = 1;
};
// 日期格式化函数
const formatDate = (date) => {
return dayjs(date).format('YYYY-MM-DD HH:mm');
};
// 回滚操作
const handleRollback = () => {
if (confirm('确定要回滚到上一版本吗?此操作可能导致部分功能暂时不可用。')) {
alert('正在执行回滚操作,请稍候...');
// 实际应用中这里会调用回滚接口
}
};
</script>
<style scoped>
.firmware-upgrade-container {
width: 95%;
max-width: 1000px;
margin: 15px auto;
font-family: Arial, sans-serif;
color: #333;
/* 限制容器最大高度,超出则显示滚动条 */
max-height: 90vh;
overflow: hidden;
border: 1px solid #e0e0e0;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 主内容区域添加滚动条 */
.main-content {
padding: 15px;
max-height: calc(90vh - 70px);
overflow-y: auto;
/* 美化滚动条 */
scrollbar-width: thin;
scrollbar-color: #3498db #f1f1f1;
}
/* 滚动条美化 - 适用于Chrome, Edge等 */
.main-content::-webkit-scrollbar {
width: 8px;
}
.main-content::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.main-content::-webkit-scrollbar-thumb {
background: #3498db;
border-radius: 4px;
}
.main-content::-webkit-scrollbar-thumb:hover {
background: #2980b9;
}
.page-title {
color: #2c3e50;
padding: 15px 15px 5px;
margin: 0;
font-size: 1.4em;
border-bottom: 1px solid #e0e0e0;
}
.card {
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 15px;
margin-bottom: 15px;
background-color: #fff;
}
.version-info {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.current-version, .latest-version {
flex: 1;
}
h3 {
margin: 0 0 10px 0;
font-size: 1.1em;
color: #2c3e50;
}
p {
margin: 6px 0;
font-size: 0.95em;
}
.update-content {
color: #555;
font-size: 0.9em;
line-height: 1.3;
margin-bottom: 15px;
}
.version-actions {
display: flex;
justify-content: space-between;
align-items: center;
}
.status {
display: inline-block;
padding: 4px 10px;
border-radius: 4px;
color: #fff;
font-size: 0.85em;
margin: 0;
}
.running {
background-color: #27ae60;
}
.upgradable {
background-color: #f39c12;
color: #fff;
}
.success {
background-color: #27ae60;
}
.fail {
background-color: #e74c3c;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9em;
transition: background-color 0.2s;
margin: 0;
}
.check-update {
background-color: #3498db;
color: #fff;
}
.upgrade-now {
background-color: #e74c3c;
color: #fff;
}
.upload-upgrade {
background-color: #3498db;
color: #fff;
margin-top: 10px;
}
.upgrade-operation label {
display: block;
margin-bottom: 8px;
font-weight: 500;
font-size: 0.95em;
}
.upgrade-operation input {
margin-bottom: 8px;
padding: 6px;
width: 100%;
max-width: 350px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.9em;
}
.upgrade-log table {
width: 100%;
border-collapse: collapse;
font-size: 0.9em;
margin-bottom: 10px;
}
.upgrade-log th, .upgrade-log td {
padding: 8px 10px;
text-align: left;
border-bottom: 1px solid #e0e0e0;
}
.upgrade-log th {
background-color: #f8f9fa;
font-weight: 600;
color: #2c3e50;
position: relative;
padding-right: 20px;
}
.sort-indicator {
font-size: 0.8em;
color: #3498db;
position: absolute;
right: 5px;
top: 50%;
transform: translateY(-50%);
}
.no-data {
text-align: center;
color: #7f8c8d;
padding: 20px;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
margin-top: 10px;
flex-wrap: wrap;
font-size: 0.9em;
}
.page-btn {
padding: 4px 10px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #fff;
cursor: pointer;
font-size: 0.9em;
}
.page-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.page-info {
color: #666;
}
.page-size {
display: flex;
align-items: center;
gap: 5px;
}
.page-size select {
padding: 3px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 0.85em;
}
/* 回滚区域样式 */
.rollback-section {
border-left: 3px solid #f5a623;
}
.rollback-versions {
margin: 10px 0;
display: flex;
gap: 20px;
flex-wrap: wrap;
}
.rollback-version-item {
display: flex;
align-items: center;
gap: 6px;
}
.version-label {
color: #666;
font-size: 0.9em;
}
.version-value {
font-weight: 500;
font-size: 0.95em;
}
.rollback-btn {
background-color: #f5a623;
color: #fff;
margin-top: 10px;
}
.rollback-btn:hover {
background-color: #e09412;
}
/* 响应式调整 */
@media (max-width: 600px) {
.version-info {
flex-direction: column;
}
.version-actions {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.pagination {
gap: 5px;
}
.page-btn {
padding: 3px 8px;
font-size: 0.8em;
}
}
</style>

View File

@@ -51,7 +51,7 @@ export default defineConfig(({ mode, command }) => {
rewrite: (p) => p.replace(/^\/file-upload/, '') rewrite: (p) => p.replace(/^\/file-upload/, '')
}, },
'/fileUrl': { '/fileUrl': {
target: 'http://117.72.197.29:9000', // 本地 target: 'http://154.8.147.51:9000', // 本地
changeOrigin: true, changeOrigin: true,
rewrite: (p) => p.replace(/^\/fileUrl/, '') rewrite: (p) => p.replace(/^\/fileUrl/, '')
}, },