Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe9dad449d |
BIN
src/assets/images/logo.jpg
Normal file
BIN
src/assets/images/logo.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
680
src/views/device/controlRecord/index.vue
Normal file
680
src/views/device/controlRecord/index.vue
Normal 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>
|
||||||
252
src/views/device/dbbackup/index.vue
Normal file
252
src/views/device/dbbackup/index.vue
Normal 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>
|
||||||
|
|
||||||
268
src/views/device/deviceControlSet/index.vue
Normal file
268
src/views/device/deviceControlSet/index.vue
Normal 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>
|
||||||
574
src/views/device/deviceStatus/index.vue
Normal file
574
src/views/device/deviceStatus/index.vue
Normal 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>
|
||||||
508
src/views/device/realTimeData/index.vue
Normal file
508
src/views/device/realTimeData/index.vue
Normal 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>
|
||||||
558
src/views/device/upgrade/index.vue
Normal file
558
src/views/device/upgrade/index.vue
Normal 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>
|
||||||
|
|
||||||
@@ -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/, '')
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user