Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95b2304f7 | ||
|
|
847b1ba214 | ||
|
|
31ad43837d | ||
|
|
9ef67e2cf8 | ||
|
|
2d7194bb91 | ||
|
|
a8f0d20761 |
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
||||
VITE_APP_TITLE = 智聪物联网管理平台
|
||||
|
||||
# 开发环境配置
|
||||
VITE_APP_ENV = 'development'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
||||
VITE_APP_TITLE = 智聪物联网管理平台
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'production'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 页面标题
|
||||
VITE_APP_TITLE = 海纳云智慧照明管理平台
|
||||
VITE_APP_TITLE = 智聪物联网管理平台
|
||||
|
||||
# 生产环境配置
|
||||
VITE_APP_ENV = 'staging'
|
||||
|
||||
18
src/api/td/tdEngine.js
Normal file
18
src/api/td/tdEngine.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取最新设备传感器数据
|
||||
export function getLastData() {
|
||||
return request({
|
||||
url: '/tdengine/td/getLastData',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 获取设备历史轨迹
|
||||
export function getDeviceTrajectory(deviceId, params) {
|
||||
return request({
|
||||
url: `/tdengine/td/trajectory/${deviceId}`,
|
||||
method: 'get',
|
||||
params: params
|
||||
})
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getAssetsFile } from '@/utils'
|
||||
|
||||
export const mapConfig = {
|
||||
zoom: 13,
|
||||
zoom: 10,
|
||||
minZoom: 4,
|
||||
maxZoom: 18,
|
||||
pitch: 0,
|
||||
center: ['120.43325056066504', '36.183868828044005'],
|
||||
center: ['121.00', '35.80'],
|
||||
logoVisible: false
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import usePermissionStore from '@/store/modules/permission'
|
||||
|
||||
NProgress.configure({ showSpinner: false })
|
||||
|
||||
const whiteList = ['/login', '/register', '/redirectTo']
|
||||
const whiteList = ['/login', '/register', '/redirectTo', '/bdsLocation', '/trajectory', '/allTrajectories']
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
NProgress.start()
|
||||
|
||||
@@ -101,6 +101,24 @@ export const constantRoutes = [
|
||||
meta: { title: '个人中心', icon: 'user' }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/bdsLocation',
|
||||
component: () => import('@/views/mapTest/index.vue'),
|
||||
hidden: true,
|
||||
meta: { title: '北斗上位机实时显示位置' }
|
||||
},
|
||||
{
|
||||
path: '/trajectory',
|
||||
component: () => import('@/views/mapTest/trajectory.vue'),
|
||||
hidden: true,
|
||||
meta: { title: '设备历史轨迹' }
|
||||
},
|
||||
{
|
||||
path: '/allTrajectories',
|
||||
component: () => import('@/views/mapTest/allTrajectories.vue'),
|
||||
hidden: true,
|
||||
meta: { title: '所有设备历史轨迹' }
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -112,10 +112,10 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup name="facilityType">
|
||||
import AlarmComparison from '@/views/alarm/alarmAnalysis/components/alarmComparison'
|
||||
import AlarmReceive from '@/views/alarm/alarmAnalysis/components/AlarmReceive'
|
||||
import AlarmLevel from '@/views/alarm/alarmAnalysis/components/AlarmLevel'
|
||||
import AlarmResponseTime from '@/views/alarm/alarmAnalysis/components/AlarmResponseTime'
|
||||
import AlarmComparison from '@/views/alarm/alarmAnalysis/components/alarmComparison.vue'
|
||||
import AlarmReceive from '@/views/alarm/alarmAnalysis/components/alarmReceive.vue'
|
||||
import AlarmLevel from '@/views/alarm/alarmAnalysis/components/alarmLevel.vue'
|
||||
import AlarmResponseTime from '@/views/alarm/alarmAnalysis/components/alarmResponseTime.vue'
|
||||
// import dayjs from 'dayjs'
|
||||
import {
|
||||
getDeviceDataMonthMom,
|
||||
|
||||
@@ -1,680 +0,0 @@
|
||||
<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>
|
||||
@@ -1,252 +0,0 @@
|
||||
<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>
|
||||
|
||||
@@ -1,268 +0,0 @@
|
||||
<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>
|
||||
@@ -1,574 +0,0 @@
|
||||
<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>
|
||||
@@ -1,508 +0,0 @@
|
||||
<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>
|
||||
@@ -1,558 +0,0 @@
|
||||
<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>
|
||||
|
||||
@@ -245,10 +245,10 @@ import { buildingTree } from '@/api/building/buildingInfo'
|
||||
import { getProgramList, delProgramStrategy, auditProgramStrategy, releaseProgramStrategy, getDeviceInfoList, listScreen } from '@/api/light/screenProgram'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { listGroup } from '@/api/light/group'
|
||||
import OperatePanel from './components/OperatePanel.vue'
|
||||
import OperatePanel from './components/operatePanel.vue'
|
||||
import EditPanel from './components/EditPanel.vue'
|
||||
import ViewPanel from '@/views/components/screen/ViewPanel.vue'
|
||||
import AuditPanel from './components/AuditPanel'
|
||||
import AuditPanel from './components/AuditPanel.vue'
|
||||
import useUserStore from '@/store/modules/user'
|
||||
|
||||
const userStore = useUserStore()
|
||||
|
||||
1547
src/views/mapTest/allTrajectories.vue
Normal file
1547
src/views/mapTest/allTrajectories.vue
Normal file
File diff suppressed because it is too large
Load Diff
886
src/views/mapTest/index.vue
Normal file
886
src/views/mapTest/index.vue
Normal file
@@ -0,0 +1,886 @@
|
||||
<template>
|
||||
<div class="map-test-container">
|
||||
<div id="mapContainer" class="map-container" v-loading="loading" element-loading-text="加载设备数据中..."></div>
|
||||
|
||||
<!-- 系统名称 -->
|
||||
<div class="system-title">
|
||||
<h1>海面油膜微型跟踪定位装置轨迹跟踪查询系统</h1>
|
||||
</div>
|
||||
|
||||
<div class="info-panel">
|
||||
<h3>地图信息</h3>
|
||||
<p>总点位数: {{ points.length }}</p>
|
||||
<p>当前缩放级别: {{ currentZoom }}</p>
|
||||
<el-button type="primary" @click="viewAllTrajectories" style="width: 100%; margin-top: 10px;">
|
||||
<el-icon><TrendCharts /></el-icon>
|
||||
查看历史轨迹
|
||||
</el-button>
|
||||
<!-- <div class="legend">
|
||||
<h4>图例</h4>
|
||||
<div class="legend-item">
|
||||
<span class="dot"></span>
|
||||
<span>设备点位</span>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
<!-- 隐藏的SVG定义 -->
|
||||
<svg width="0" height="0" style="position: absolute;">
|
||||
<defs>
|
||||
<filter id="glow">
|
||||
<feGaussianBlur stdDeviation="3" result="coloredBlur"/>
|
||||
<feMerge>
|
||||
<feMergeNode in="coloredBlur"/>
|
||||
<feMergeNode in="SourceGraphic"/>
|
||||
</feMerge>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="MapTest">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { initMap, initLayer, addImage } from '@/utils/map'
|
||||
import { mapConfig, layerUrl, imageArr } from '@/config/map'
|
||||
import { PointLayer, Marker, Popup } from '@antv/l7'
|
||||
import { getLastData } from '@/api/td/tdEngine'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { TrendCharts } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const scene = ref(null)
|
||||
const pointLayer = ref(null)
|
||||
const currentZoom = ref(13)
|
||||
const points = ref([])
|
||||
const loading = ref(false)
|
||||
const currentPopup = ref(null) // 存储当前显示的弹窗
|
||||
const popupTimer = ref(null) // 存储弹窗定时器
|
||||
let refreshTimer = null // 定时刷新定时器
|
||||
|
||||
// 查看历史轨迹(直接跳转)
|
||||
const viewTrajectory = (deviceId) => {
|
||||
if (!deviceId) {
|
||||
ElMessage.warning('设备ID无效')
|
||||
return
|
||||
}
|
||||
|
||||
router.push({
|
||||
path: '/trajectory',
|
||||
query: {
|
||||
deviceId: deviceId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 查看所有设备历史轨迹
|
||||
const viewAllTrajectories = () => {
|
||||
router.push({
|
||||
path: '/allTrajectories'
|
||||
})
|
||||
}
|
||||
|
||||
// 从接口获取设备数据
|
||||
const fetchDeviceData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const response = await getLastData()
|
||||
// 转换后端数据格式为前端需要的格式
|
||||
const devicePoints = response.map((item, index) => {
|
||||
return {
|
||||
id: item.deviceId || `PT${String(index + 1).padStart(4, '0')}`,
|
||||
name: `设备${item.deviceId || index + 1}`,
|
||||
longitude: item.longitude,
|
||||
latitude: item.latitude,
|
||||
reportTime: item.createTime || item.time || new Date().toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
}),
|
||||
status: 'online'
|
||||
}
|
||||
})
|
||||
|
||||
points.value = devicePoints
|
||||
|
||||
// 如果已经有图层,更新图层数据;否则创建新图层
|
||||
if (scene.value) {
|
||||
updatePointLayer()
|
||||
}
|
||||
|
||||
return devicePoints
|
||||
} catch (error) {
|
||||
console.error('获取设备数据失败:', error)
|
||||
ElMessage.error('获取设备数据失败,请稍后重试')
|
||||
return []
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initializeMap = async () => {
|
||||
try {
|
||||
// 从接口获取设备数据
|
||||
await fetchDeviceData()
|
||||
|
||||
// 计算所有设备的居中位置
|
||||
let mapCenter = mapConfig.center
|
||||
let mapZoom = mapConfig.zoom
|
||||
|
||||
if (points.value.length > 0) {
|
||||
const lngs = points.value.map(p => p.longitude)
|
||||
const lats = points.value.map(p => p.latitude)
|
||||
|
||||
const minLng = Math.min(...lngs)
|
||||
const maxLng = Math.max(...lngs)
|
||||
const minLat = Math.min(...lats)
|
||||
const maxLat = Math.max(...lats)
|
||||
|
||||
// 计算中心点
|
||||
const centerLng = (minLng + maxLng) / 2
|
||||
const centerLat = (minLat + maxLat) / 2
|
||||
mapCenter = [centerLng, centerLat]
|
||||
|
||||
// 根据边界范围计算合适的缩放级别
|
||||
const lngRange = maxLng - minLng
|
||||
const latRange = maxLat - minLat
|
||||
const maxRange = Math.max(lngRange, latRange)
|
||||
|
||||
if (maxRange > 1) {
|
||||
mapZoom = 10
|
||||
} else if (maxRange > 0.5) {
|
||||
mapZoom = 11
|
||||
} else if (maxRange > 0.1) {
|
||||
mapZoom = 12
|
||||
} else if (maxRange > 0.01) {
|
||||
mapZoom = 14
|
||||
} else {
|
||||
mapZoom = 15
|
||||
}
|
||||
}
|
||||
|
||||
// 创建地图场景,使用动态中心点和缩放级别
|
||||
const mapScene = await initMap('mapContainer', {
|
||||
...mapConfig,
|
||||
center: mapCenter,
|
||||
zoom: mapZoom
|
||||
})
|
||||
scene.value = mapScene
|
||||
currentZoom.value = mapZoom
|
||||
|
||||
// 添加图层
|
||||
initLayer(mapScene, layerUrl)
|
||||
|
||||
// 添加图片资源
|
||||
addImage(mapScene, imageArr)
|
||||
|
||||
// 创建点图层
|
||||
if (points.value.length > 0) {
|
||||
createPointLayer()
|
||||
// 启动定时刷新
|
||||
startAutoRefresh()
|
||||
} else {
|
||||
ElMessage.warning('暂无设备数据')
|
||||
}
|
||||
|
||||
// 监听缩放事件
|
||||
mapScene.on('zoom', () => {
|
||||
currentZoom.value = Math.round(mapScene.getZoom())
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error)
|
||||
ElMessage.error('地图初始化失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建3D立体标记图标HTML
|
||||
const create3DMarkerElement = (point) => {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'marker-3d'
|
||||
element.style.cursor = 'pointer' // 固定光标样式
|
||||
element.innerHTML = `
|
||||
<div class="marker-container">
|
||||
<div class="pulse-ring"></div>
|
||||
<div class="outer-glow"></div>
|
||||
<div class="marker-pin">
|
||||
<div class="marker-glow"></div>
|
||||
<div class="inner-shine"></div>
|
||||
<svg class="marker-icon" viewBox="0 0 24 24" fill="white">
|
||||
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="marker-shadow"></div>
|
||||
<div class="status-ring"></div>
|
||||
</div>
|
||||
`
|
||||
|
||||
// 鼠标悬停显示信息
|
||||
element.addEventListener('mouseenter', () => {
|
||||
// 清除可能存在的隐藏定时器
|
||||
if (popupTimer.value) {
|
||||
clearTimeout(popupTimer.value)
|
||||
popupTimer.value = null
|
||||
}
|
||||
|
||||
// 先移除之前的弹窗
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
currentPopup.value = null
|
||||
}
|
||||
|
||||
const popup = new Popup({
|
||||
offsets: [150, -50], // 向右150px,向上50px,明显偏向右上角
|
||||
closeButton: false,
|
||||
closeOnClick: false
|
||||
})
|
||||
.setLnglat([point.longitude, point.latitude])
|
||||
.setHTML(`
|
||||
<div class="trajectory-tooltip">
|
||||
<div class="tooltip-header">
|
||||
<div class="header-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="header-title">设备信息</div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M9 3v18M15 3v18M3 9h18M3 15h18" stroke="currentColor" stroke-width="2"/>
|
||||
</svg>
|
||||
<span class="label">编号</span>
|
||||
<span class="value">${point.id}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">时间</span>
|
||||
<span class="value">${point.reportTime}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">经度</span>
|
||||
<span class="value">${point.longitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">纬度</span>
|
||||
<span class="value">${point.latitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
scene.value.addPopup(popup)
|
||||
currentPopup.value = popup
|
||||
})
|
||||
|
||||
// 鼠标离开时延迟关闭,防止快速进出导致闪烁
|
||||
element.addEventListener('mouseleave', () => {
|
||||
// 延迟300ms关闭,如果在这期间鼠标又进入,则不关闭
|
||||
popupTimer.value = setTimeout(() => {
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
currentPopup.value = null
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
// 点击跳转到历史轨迹页面
|
||||
element.addEventListener('click', () => {
|
||||
viewTrajectory(point.id)
|
||||
})
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
// 创建点图层(使用Marker实现3D效果)
|
||||
const createPointLayer = () => {
|
||||
if (!scene.value || points.value.length === 0) return
|
||||
|
||||
const markers = []
|
||||
|
||||
// 为每个点创建3D Marker
|
||||
points.value.forEach(point => {
|
||||
const element = create3DMarkerElement(point)
|
||||
const marker = new Marker({
|
||||
element: element,
|
||||
anchor: 'bottom'
|
||||
}).setLnglat([point.longitude, point.latitude])
|
||||
|
||||
scene.value.addMarker(marker)
|
||||
markers.push(marker)
|
||||
})
|
||||
|
||||
// 脉冲动画底层光圈
|
||||
const pulseLayer = new PointLayer({
|
||||
name: 'pulsePoints',
|
||||
zIndex: 100
|
||||
})
|
||||
.source(points.value, {
|
||||
parser: {
|
||||
type: 'json',
|
||||
x: 'longitude',
|
||||
y: 'latitude'
|
||||
}
|
||||
})
|
||||
.shape('circle')
|
||||
.size(40)
|
||||
.color('#5B8FF9')
|
||||
.style({
|
||||
opacity: 0.15
|
||||
})
|
||||
.animate({
|
||||
enable: true,
|
||||
type: 'breathe',
|
||||
duration: 2000,
|
||||
interval: 0.5,
|
||||
trailLength: 0.1
|
||||
})
|
||||
|
||||
scene.value.addLayer(pulseLayer)
|
||||
pointLayer.value = { markers, pulseLayer }
|
||||
}
|
||||
|
||||
// 更新点图层数据
|
||||
const updatePointLayer = () => {
|
||||
if (!scene.value || points.value.length === 0) return
|
||||
|
||||
// 只更新脉冲图层的数据源,不重建整个图层
|
||||
if (pointLayer.value && pointLayer.value.pulseLayer) {
|
||||
pointLayer.value.pulseLayer.setData(points.value)
|
||||
}
|
||||
|
||||
// 更新现有markers的位置和数据
|
||||
if (pointLayer.value && pointLayer.value.markers) {
|
||||
const oldMarkers = pointLayer.value.markers
|
||||
const newPoints = points.value
|
||||
|
||||
// 如果点位数量相同,只更新位置
|
||||
if (oldMarkers.length === newPoints.length) {
|
||||
oldMarkers.forEach((marker, index) => {
|
||||
const newPoint = newPoints[index]
|
||||
if (newPoint) {
|
||||
// 更新位置
|
||||
marker.setLnglat([newPoint.longitude, newPoint.latitude])
|
||||
// 更新点击事件数据
|
||||
const element = marker.getElement()
|
||||
element.onclick = () => {
|
||||
viewTrajectory(newPoint.id)
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// 点位数量变化时才完全重建
|
||||
rebuildPointLayer()
|
||||
}
|
||||
} else {
|
||||
// 首次创建或图层不存在时
|
||||
createPointLayer()
|
||||
}
|
||||
}
|
||||
|
||||
// 完全重建点图层(仅在点位数量变化时使用)
|
||||
const rebuildPointLayer = () => {
|
||||
if (!scene.value) return
|
||||
|
||||
// 清除旧的markers和图层
|
||||
if (pointLayer.value) {
|
||||
if (pointLayer.value.markers) {
|
||||
pointLayer.value.markers.forEach(marker => {
|
||||
marker.remove()
|
||||
})
|
||||
}
|
||||
if (pointLayer.value.pulseLayer) {
|
||||
scene.value.removeLayer(pointLayer.value.pulseLayer)
|
||||
}
|
||||
}
|
||||
|
||||
// 重新创建图层
|
||||
createPointLayer()
|
||||
}
|
||||
|
||||
// 启动定时刷新
|
||||
const startAutoRefresh = () => {
|
||||
// 清除已存在的定时器
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
}
|
||||
|
||||
// 每10秒刷新一次数据
|
||||
refreshTimer = setInterval(() => {
|
||||
fetchDeviceData()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
// 停止定时刷新
|
||||
const stopAutoRefresh = () => {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer)
|
||||
refreshTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
// 清理弹窗和定时器
|
||||
const cleanupPopup = () => {
|
||||
if (popupTimer.value) {
|
||||
clearTimeout(popupTimer.value)
|
||||
popupTimer.value = null
|
||||
}
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
currentPopup.value = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
initializeMap()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 停止定时刷新
|
||||
stopAutoRefresh()
|
||||
|
||||
// 清理弹窗
|
||||
cleanupPopup()
|
||||
|
||||
// 清理图层和markers
|
||||
if (pointLayer.value) {
|
||||
if (pointLayer.value.markers) {
|
||||
pointLayer.value.markers.forEach(marker => {
|
||||
try {
|
||||
marker.remove()
|
||||
} catch (e) {
|
||||
console.warn('Marker remove error:', e)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (pointLayer.value.pulseLayer && scene.value) {
|
||||
try {
|
||||
scene.value.removeLayer(pointLayer.value.pulseLayer)
|
||||
} catch (e) {
|
||||
console.warn('Layer remove error:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁地图实例
|
||||
if (scene.value) {
|
||||
try {
|
||||
scene.value.destroy()
|
||||
scene.value = null
|
||||
} catch (e) {
|
||||
console.warn('Scene destroy error:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.map-test-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
|
||||
.map-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// 系统名称样式
|
||||
.system-title {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
z-index: 100;
|
||||
background: linear-gradient(135deg, rgba(91, 143, 249, 0.95) 0%, rgba(123, 163, 255, 0.95) 100%);
|
||||
padding: 16px 28px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(91, 143, 249, 0.4), 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
min-width: 220px;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 100; // 确保信息面板显示在地图之上
|
||||
|
||||
h3 {
|
||||
margin: 0 0 15px 0;
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #00D9FF;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 8px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
|
||||
&:first-of-type {
|
||||
font-weight: bold;
|
||||
color: #00D9FF;
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.legend {
|
||||
margin-top: 20px;
|
||||
padding-top: 15px;
|
||||
border-top: 1px solid #e8e8e8;
|
||||
|
||||
h4 {
|
||||
margin: 0 0 12px 0;
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 10px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
|
||||
.dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
margin-right: 8px;
|
||||
border: 2px solid #fff;
|
||||
background-color: #5B8FF9;
|
||||
box-shadow: 0 0 6px rgba(91, 143, 249, 0.5);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3D标记样式
|
||||
:deep(.marker-3d) {
|
||||
cursor: pointer;
|
||||
transform-origin: center bottom;
|
||||
|
||||
.marker-container {
|
||||
position: relative;
|
||||
width: 32px;
|
||||
height: 38px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pulse-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid #5B8FF9;
|
||||
border-radius: 50%;
|
||||
opacity: 0;
|
||||
animation: pulse-ring 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
.outer-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgba(91, 143, 249, 0.4) 0%, transparent 70%);
|
||||
animation: glow-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.marker-pin {
|
||||
position: relative;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 50% 50% 50% 0;
|
||||
transform: rotate(-45deg);
|
||||
background: linear-gradient(135deg, #5B8FF9 0%, #7BA3FF 100%);
|
||||
border: 2px solid rgba(255, 255, 255, 0.95);
|
||||
box-shadow:
|
||||
0 3px 6px rgba(0, 0, 0, 0.3),
|
||||
0 6px 12px rgba(0, 0, 0, 0.2),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.1),
|
||||
inset 0 -2px 3px rgba(0, 0, 0, 0.2),
|
||||
inset 0 2px 3px rgba(255, 255, 255, 0.3);
|
||||
transition: all 0.3s ease;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.marker-glow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 120%;
|
||||
height: 120%;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 20px rgba(91, 143, 249, 0.5), 0 0 35px rgba(91, 143, 249, 0.3);
|
||||
animation: glow-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.inner-shine {
|
||||
position: absolute;
|
||||
top: 20%;
|
||||
left: 20%;
|
||||
width: 40%;
|
||||
height: 40%;
|
||||
background: radial-gradient(circle, rgba(255, 255, 255, 0.6) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.marker-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%) rotate(45deg);
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.3));
|
||||
}
|
||||
|
||||
.marker-shadow {
|
||||
position: absolute;
|
||||
bottom: -5px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 16px;
|
||||
height: 6px;
|
||||
background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.35) 0%, transparent 70%);
|
||||
border-radius: 50%;
|
||||
animation: shadow-pulse 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.status-ring {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 2px solid #5B8FF9;
|
||||
border-radius: 50%;
|
||||
opacity: 0.4;
|
||||
animation: status-ring-pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.marker-pin {
|
||||
transform: rotate(-45deg) scale(1.1);
|
||||
box-shadow:
|
||||
0 4px 8px rgba(0, 0, 0, 0.4),
|
||||
0 8px 16px rgba(0, 0, 0, 0.3),
|
||||
0 0 0 2px rgba(255, 255, 255, 0.2),
|
||||
inset 0 -2px 3px rgba(0, 0, 0, 0.2),
|
||||
inset 0 2px 3px rgba(255, 255, 255, 0.4);
|
||||
}
|
||||
|
||||
.status-ring {
|
||||
animation: status-ring-hover 0.6s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse-ring {
|
||||
0% {
|
||||
transform: translate(-50%, -50%) scale(0.75);
|
||||
opacity: 0.8;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.4;
|
||||
}
|
||||
100% {
|
||||
transform: translate(-50%, -50%) scale(1.6);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.9;
|
||||
transform: translate(-50%, -50%) scale(1.08);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shadow-pulse {
|
||||
0%, 100% {
|
||||
transform: translateX(-50%) scale(1);
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
transform: translateX(-50%) scale(0.88);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes status-ring-pulse {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
opacity: 0.5;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.12);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes status-ring-hover {
|
||||
0%, 100% {
|
||||
transform: translate(-50%, -50%) scale(1.08);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translate(-50%, -50%) scale(1.2);
|
||||
opacity: 0.35;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义 L7 Popup 样式
|
||||
:deep(.l7-popup) {
|
||||
pointer-events: none !important;
|
||||
|
||||
.l7-popup-content {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.l7-popup-tip {
|
||||
border-top-color: #667eea;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保tooltip内容也不阻挡
|
||||
:deep(.trajectory-tooltip) {
|
||||
pointer-events: none !important;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
min-width: 280px;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
|
||||
svg {
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
padding: 14px 16px;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 13px;
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
min-width: 45px;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: right;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
914
src/views/mapTest/trajectory.vue
Normal file
914
src/views/mapTest/trajectory.vue
Normal file
@@ -0,0 +1,914 @@
|
||||
<template>
|
||||
<div class="trajectory-container">
|
||||
<div class="header-bar">
|
||||
<el-button @click="goBack" icon="ArrowLeft" class="back-btn">返回</el-button>
|
||||
<h2 class="page-title">历史轨迹 - {{ deviceId }}</h2>
|
||||
<div class="time-selector">
|
||||
<div class="sample-count-input">
|
||||
<label>采样数:</label>
|
||||
<el-input-number
|
||||
v-model="sampleCount"
|
||||
:min="1"
|
||||
:max="500"
|
||||
:step="50"
|
||||
:precision="0"
|
||||
controls-position="right"
|
||||
placeholder="采样数"
|
||||
style="width: 150px;"
|
||||
@change="fetchTrajectory"
|
||||
/>
|
||||
</div>
|
||||
<el-date-picker
|
||||
v-model="timeRange"
|
||||
type="datetimerange"
|
||||
range-separator="至"
|
||||
start-placeholder="开始时间"
|
||||
end-placeholder="结束时间"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
@change="handleTimeChange"
|
||||
@visible-change="handlePickerVisibleChange"
|
||||
/>
|
||||
<el-button type="primary" @click="fetchTrajectory" :loading="loading" class="query-btn">
|
||||
<el-icon><Search /></el-icon>
|
||||
<span class="btn-text">查询轨迹</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="trajectoryMap" class="trajectory-map" v-loading="loading" element-loading-text="加载轨迹数据中..."></div>
|
||||
|
||||
<!-- 系统名称 -->
|
||||
<div class="system-title">
|
||||
<h1>海面油膜微型跟踪定位装置轨迹跟踪查询系统</h1>
|
||||
</div>
|
||||
|
||||
<div class="info-panel">
|
||||
<h3>轨迹信息</h3>
|
||||
<p>轨迹点数: {{ trajectoryPoints.length }}</p>
|
||||
<p>当前缩放级别: {{ currentZoom }}</p>
|
||||
<p v-if="trajectoryPoints.length > 0">起始: {{ trajectoryPoints[0]?.time }}</p>
|
||||
<p v-if="trajectoryPoints.length > 0">终止: {{ trajectoryPoints[trajectoryPoints.length - 1]?.time }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup name="Trajectory">
|
||||
import { ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { initMap, initLayer, addImage } from '@/utils/map'
|
||||
import { mapConfig, layerUrl, imageArr } from '@/config/map'
|
||||
import { PointLayer, LineLayer, Marker, Popup } from '@antv/l7'
|
||||
import { getDeviceTrajectory } from '@/api/td/tdEngine'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { ArrowLeft, Search } from '@element-plus/icons-vue'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const scene = ref(null)
|
||||
const trajectoryLayer = ref(null)
|
||||
const deviceId = ref('')
|
||||
const timeRange = ref([])
|
||||
const trajectoryPoints = ref([])
|
||||
const loading = ref(false)
|
||||
const currentZoom = ref(16)
|
||||
const currentPopup = ref(null) // 存储当前显示的弹窗
|
||||
const popupTimer = ref(null) // 存储弹窗定时器
|
||||
const isDatePickerOpen = ref(false) // 日期选择器是否打开
|
||||
const sampleCount = ref(100) // 采样数,默认10
|
||||
|
||||
// 返回上一页
|
||||
const goBack = () => {
|
||||
router.back()
|
||||
}
|
||||
|
||||
// 时间范围改变
|
||||
const handleTimeChange = (val) => {
|
||||
console.log('时间范围:', val)
|
||||
}
|
||||
|
||||
// 日期选择器可见性改变
|
||||
const handlePickerVisibleChange = (visible) => {
|
||||
isDatePickerOpen.value = visible
|
||||
// 当日期选择器打开时,禁用地图交互
|
||||
if (scene.value) {
|
||||
if (visible) {
|
||||
scene.value.setMapStatus({ dragEnable: false, keyboardEnable: false, doubleClickZoom: false, scrollZoom: false })
|
||||
} else {
|
||||
scene.value.setMapStatus({ dragEnable: true, keyboardEnable: true, doubleClickZoom: true, scrollZoom: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取轨迹数据
|
||||
const fetchTrajectory = async () => {
|
||||
if (!deviceId.value) {
|
||||
ElMessage.warning('设备ID不能为空')
|
||||
return
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const params = {
|
||||
sampleCount: sampleCount.value
|
||||
}
|
||||
|
||||
if (timeRange.value && timeRange.value.length === 2) {
|
||||
params.startTime = timeRange.value[0]
|
||||
params.endTime = timeRange.value[1]
|
||||
}
|
||||
|
||||
const response = await getDeviceTrajectory(deviceId.value, params)
|
||||
|
||||
// 处理响应数据,后端返回格式为 { code, msg, data }
|
||||
const trajectoryData = response.data || response
|
||||
|
||||
if (trajectoryData && trajectoryData.length > 0) {
|
||||
trajectoryPoints.value = trajectoryData.map(item => ({
|
||||
longitude: item.longitude,
|
||||
latitude: item.latitude,
|
||||
time: item.time || item.createTime,
|
||||
deviceId: item.deviceId
|
||||
}))
|
||||
|
||||
// 按时间排序,确保轨迹线是按时间顺序连接的
|
||||
trajectoryPoints.value.sort((a, b) => {
|
||||
return new Date(a.time).getTime() - new Date(b.time).getTime()
|
||||
})
|
||||
|
||||
drawTrajectory()
|
||||
} else {
|
||||
ElMessage.warning('暂无轨迹数据')
|
||||
trajectoryPoints.value = []
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取轨迹失败:', error)
|
||||
ElMessage.error('获取轨迹数据失败')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
const initializeMap = async () => {
|
||||
try {
|
||||
// 创建地图场景
|
||||
const mapScene = await initMap('trajectoryMap', mapConfig)
|
||||
scene.value = mapScene
|
||||
|
||||
// 添加图层
|
||||
initLayer(mapScene, layerUrl)
|
||||
|
||||
// 添加图片资源
|
||||
addImage(mapScene, imageArr)
|
||||
|
||||
// 监听缩放事件
|
||||
mapScene.on('zoom', () => {
|
||||
currentZoom.value = Math.round(mapScene.getZoom())
|
||||
})
|
||||
|
||||
return mapScene
|
||||
} catch (error) {
|
||||
console.error('地图初始化失败:', error)
|
||||
ElMessage.error('地图初始化失败')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// 绘制轨迹
|
||||
const drawTrajectory = () => {
|
||||
if (!scene.value || trajectoryPoints.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// 清除旧轨迹
|
||||
if (trajectoryLayer.value) {
|
||||
try {
|
||||
if (trajectoryLayer.value.lineLayer) {
|
||||
scene.value.removeLayer(trajectoryLayer.value.lineLayer)
|
||||
}
|
||||
if (trajectoryLayer.value.pointLayer) {
|
||||
scene.value.removeLayer(trajectoryLayer.value.pointLayer)
|
||||
}
|
||||
if (trajectoryLayer.value.startMarker) {
|
||||
trajectoryLayer.value.startMarker.remove()
|
||||
}
|
||||
if (trajectoryLayer.value.endMarker) {
|
||||
trajectoryLayer.value.endMarker.remove()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('清除旧轨迹失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 直接绘制,不再使用事件监听
|
||||
try {
|
||||
// 构建轨迹线数据
|
||||
const lineData = {
|
||||
type: 'FeatureCollection',
|
||||
features: [{
|
||||
type: 'Feature',
|
||||
properties: {},
|
||||
geometry: {
|
||||
type: 'LineString',
|
||||
coordinates: trajectoryPoints.value.map(p => [p.longitude, p.latitude])
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
||||
// 创建轨迹线图层 - 增强显示效果
|
||||
const lineLayer = new LineLayer({
|
||||
name: 'trajectoryLine',
|
||||
zIndex: 101
|
||||
})
|
||||
.source(lineData)
|
||||
.size(5) // 增加线宽
|
||||
.shape('line')
|
||||
.color('#1890FF') // 使用更鲜艳的蓝色
|
||||
.style({
|
||||
lineType: 'solid',
|
||||
opacity: 0.9 // 增加不透明度
|
||||
})
|
||||
|
||||
scene.value.addLayer(lineLayer)
|
||||
|
||||
// 创建轨迹点图层
|
||||
const pointLayer = new PointLayer({
|
||||
name: 'trajectoryPoints',
|
||||
zIndex: 102
|
||||
})
|
||||
.source(trajectoryPoints.value.map((point, index) => ({
|
||||
...point,
|
||||
index: index + 1 // 添加序号,从1开始
|
||||
})), {
|
||||
parser: {
|
||||
type: 'json',
|
||||
x: 'longitude',
|
||||
y: 'latitude'
|
||||
}
|
||||
})
|
||||
.shape('circle')
|
||||
.size(8) // 稍微增大点的大小
|
||||
.color('#FF6B6B')
|
||||
.style({
|
||||
opacity: 0.8,
|
||||
strokeWidth: 2,
|
||||
stroke: '#fff'
|
||||
})
|
||||
|
||||
// 添加鼠标悬停事件,显示详细信息
|
||||
pointLayer.on('mousemove', (e) => {
|
||||
const feature = e.feature
|
||||
if (feature) {
|
||||
// 先关闭之前的弹窗
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
}
|
||||
|
||||
const popup = new Popup({
|
||||
offsets: [0, -15],
|
||||
closeButton: false
|
||||
})
|
||||
.setLnglat([feature.longitude, feature.latitude])
|
||||
.setHTML(`
|
||||
<div class="trajectory-tooltip">
|
||||
<div class="tooltip-header">
|
||||
<div class="header-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<circle cx="12" cy="12" r="3" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="header-title">轨迹点 #${feature.index}</div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">时间</span>
|
||||
<span class="value">${feature.time}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">经度</span>
|
||||
<span class="value">${feature.longitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">纬度</span>
|
||||
<span class="value">${feature.latitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
scene.value.addPopup(popup)
|
||||
currentPopup.value = popup
|
||||
}
|
||||
})
|
||||
|
||||
// 鼠标移出时关闭弹窗
|
||||
pointLayer.on('mouseout', () => {
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
currentPopup.value = null
|
||||
}
|
||||
})
|
||||
|
||||
// 鼠标移入时改变光标样式
|
||||
pointLayer.on('mouseenter', () => {
|
||||
document.body.style.cursor = 'pointer'
|
||||
})
|
||||
|
||||
pointLayer.on('mouseleave', () => {
|
||||
document.body.style.cursor = ''
|
||||
})
|
||||
|
||||
scene.value.addLayer(pointLayer)
|
||||
|
||||
// 创建起点标记
|
||||
const startPoint = trajectoryPoints.value[0]
|
||||
const startMarker = createMarker(startPoint, '起点', '#52C41A', true)
|
||||
scene.value.addMarker(startMarker)
|
||||
|
||||
// 创建终点标记
|
||||
const endPoint = trajectoryPoints.value[trajectoryPoints.value.length - 1]
|
||||
const endMarker = createMarker(endPoint, '终点', '#FF4D4F', false)
|
||||
scene.value.addMarker(endMarker)
|
||||
|
||||
trajectoryLayer.value = { lineLayer, pointLayer, startMarker, endMarker }
|
||||
|
||||
// 调整地图视野到轨迹范围
|
||||
fitBounds()
|
||||
} catch (error) {
|
||||
console.error('绘制轨迹失败:', error)
|
||||
ElMessage.error('绘制轨迹失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 创建标记点
|
||||
const createMarker = (point, label, color, isStart = true) => {
|
||||
const el = document.createElement('div')
|
||||
el.className = 'trajectory-marker'
|
||||
el.style.cursor = 'pointer' // 直接设置为pointer,不再动态切换
|
||||
el.innerHTML = `
|
||||
<div class="marker-content" style="background: ${color};">
|
||||
<span>${label}</span>
|
||||
</div>
|
||||
`
|
||||
|
||||
// 添加鼠标悬停事件
|
||||
el.addEventListener('mouseenter', () => {
|
||||
// 清除可能存在的隐藏定时器
|
||||
if (popupTimer.value) {
|
||||
clearTimeout(popupTimer.value)
|
||||
popupTimer.value = null
|
||||
}
|
||||
|
||||
// 如果已有弹窗,直接返回,不重复创建
|
||||
if (currentPopup.value) {
|
||||
return
|
||||
}
|
||||
|
||||
const popup = new Popup({
|
||||
offsets: [0, -45],
|
||||
closeButton: false,
|
||||
closeOnClick: false
|
||||
})
|
||||
.setLnglat([point.longitude, point.latitude])
|
||||
.setHTML(`
|
||||
<div class="trajectory-tooltip">
|
||||
<div class="tooltip-header">
|
||||
<div class="header-icon">
|
||||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 8l4 4-4 4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="header-title">${isStart ? '起点' : '终点'}</div>
|
||||
</div>
|
||||
<div class="tooltip-content">
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="2"/>
|
||||
<path d="M12 6v6l4 2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">时间</span>
|
||||
<span class="value">${point.time}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">经度</span>
|
||||
<span class="value">${point.longitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<svg class="icon" width="14" height="14" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M12 2v20M2 12h20" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
<span class="label">纬度</span>
|
||||
<span class="value">${point.latitude.toFixed(6)}°</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`)
|
||||
scene.value.addPopup(popup)
|
||||
currentPopup.value = popup
|
||||
})
|
||||
|
||||
// 鼠标离开时延迟关闭,防止快速进出导致闪烁
|
||||
el.addEventListener('mouseleave', () => {
|
||||
// 延迟300ms关闭,如果在这期间鼠标又进入,则不关闭
|
||||
popupTimer.value = setTimeout(() => {
|
||||
if (currentPopup.value) {
|
||||
currentPopup.value.remove()
|
||||
currentPopup.value = null
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
return new Marker({
|
||||
element: el,
|
||||
anchor: 'center'
|
||||
}).setLnglat([point.longitude, point.latitude])
|
||||
}
|
||||
|
||||
// 调整地图视野
|
||||
const fitBounds = () => {
|
||||
if (!scene.value || trajectoryPoints.value.length === 0) return
|
||||
|
||||
const lngs = trajectoryPoints.value.map(p => p.longitude)
|
||||
const lats = trajectoryPoints.value.map(p => p.latitude)
|
||||
|
||||
const minLng = Math.min(...lngs)
|
||||
const maxLng = Math.max(...lngs)
|
||||
const minLat = Math.min(...lats)
|
||||
const maxLat = Math.max(...lats)
|
||||
|
||||
const centerLng = (minLng + maxLng) / 2
|
||||
const centerLat = (minLat + maxLat) / 2
|
||||
|
||||
scene.value.setCenter([centerLng, centerLat])
|
||||
|
||||
// 移动端使用缩放级别15,桌面端使用16
|
||||
const isMobile = window.innerWidth <= 768
|
||||
const zoomLevel = isMobile ? 15 : 16
|
||||
scene.value.setZoom(zoomLevel)
|
||||
currentZoom.value = zoomLevel
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
deviceId.value = route.query.deviceId || ''
|
||||
|
||||
if (!deviceId.value) {
|
||||
ElMessage.error('设备ID不能为空')
|
||||
router.back()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置默认时间范围为最近24小时
|
||||
const now = new Date()
|
||||
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
|
||||
|
||||
const formatDateTime = (date) => {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
|
||||
}
|
||||
|
||||
timeRange.value = [
|
||||
formatDateTime(yesterday),
|
||||
formatDateTime(now)
|
||||
]
|
||||
|
||||
initializeMap().then(() => {
|
||||
fetchTrajectory()
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清理轨迹图层
|
||||
if (trajectoryLayer.value && scene.value) {
|
||||
try {
|
||||
if (trajectoryLayer.value.lineLayer) {
|
||||
scene.value.removeLayer(trajectoryLayer.value.lineLayer)
|
||||
}
|
||||
if (trajectoryLayer.value.pointLayer) {
|
||||
scene.value.removeLayer(trajectoryLayer.value.pointLayer)
|
||||
}
|
||||
if (trajectoryLayer.value.startMarker) {
|
||||
trajectoryLayer.value.startMarker.remove()
|
||||
}
|
||||
if (trajectoryLayer.value.endMarker) {
|
||||
trajectoryLayer.value.endMarker.remove()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Layer cleanup error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁地图实例
|
||||
if (scene.value) {
|
||||
try {
|
||||
scene.value.destroy()
|
||||
scene.value = null
|
||||
} catch (e) {
|
||||
console.warn('Scene destroy error:', e)
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.trajectory-container {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
background: #fff;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
gap: 20px;
|
||||
position: relative;
|
||||
flex-wrap: wrap; // 移动端允许换行
|
||||
|
||||
.back-btn {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
transform: translateX(-300px);
|
||||
flex-wrap: wrap; // 移动端允许换行
|
||||
|
||||
.sample-count-input {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
.btn-text {
|
||||
margin-left: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.trajectory-map {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
// 系统名称样式
|
||||
.system-title {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
left: 20px;
|
||||
z-index: 100;
|
||||
background: linear-gradient(135deg, rgba(91, 143, 249, 0.95) 0%, rgba(123, 163, 255, 0.95) 100%);
|
||||
padding: 16px 28px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 4px 16px rgba(91, 143, 249, 0.4), 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
letter-spacing: 1px;
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
position: absolute;
|
||||
top: 80px;
|
||||
right: 20px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
min-width: 160px;
|
||||
backdrop-filter: blur(10px);
|
||||
z-index: 100;
|
||||
|
||||
h3 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
border-bottom: 2px solid #5B8FF9;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 6px 0;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
line-height: 1.4;
|
||||
|
||||
&:first-of-type {
|
||||
font-weight: bold;
|
||||
color: #5B8FF9;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.trajectory-container {
|
||||
// 系统名称移动端样式
|
||||
.system-title {
|
||||
top: 70px;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
padding: 12px 16px;
|
||||
|
||||
h1 {
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-bar {
|
||||
padding: 12px;
|
||||
gap: 8px;
|
||||
|
||||
.page-title {
|
||||
font-size: 16px;
|
||||
width: 100%;
|
||||
order: 1;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
order: 0;
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
width: 100%;
|
||||
order: 2;
|
||||
transform: none !important; // 移动端不偏移
|
||||
gap: 8px;
|
||||
|
||||
.sample-count-input {
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
:deep(.el-input-number) {
|
||||
flex: 1;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-date-editor) {
|
||||
width: 100% !important;
|
||||
|
||||
.el-range-separator {
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
.el-range-input {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.query-btn {
|
||||
width: 100%;
|
||||
|
||||
.btn-text {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info-panel {
|
||||
top: auto;
|
||||
bottom: 10px;
|
||||
right: 8px;
|
||||
left: auto;
|
||||
min-width: 140px;
|
||||
max-width: 180px;
|
||||
padding: 8px 12px;
|
||||
|
||||
h3 {
|
||||
font-size: 13px;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 6px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 11px;
|
||||
margin: 4px 0;
|
||||
line-height: 1.3;
|
||||
|
||||
&:first-of-type {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 超小屏幕适配
|
||||
@media (max-width: 480px) {
|
||||
.trajectory-container {
|
||||
.header-bar {
|
||||
padding: 8px;
|
||||
|
||||
.page-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
:deep(.el-button) {
|
||||
padding: 8px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.time-selector {
|
||||
.sample-count-input {
|
||||
label {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.trajectory-marker) {
|
||||
.marker-content {
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: bold;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.trajectory-tooltip) {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
min-width: 260px;
|
||||
box-shadow: 0 8px 24px rgba(102, 126, 234, 0.4), 0 0 0 1px rgba(255, 255, 255, 0.1);
|
||||
overflow: hidden;
|
||||
|
||||
.tooltip-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
||||
|
||||
.header-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
border-radius: 50%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
.tooltip-content {
|
||||
padding: 14px 16px;
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
flex-shrink: 0;
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 12px;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.value {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
text-align: right;
|
||||
font-family: 'Monaco', 'Consolas', monospace;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义 L7 Popup 样式
|
||||
:deep(.l7-popup) {
|
||||
pointer-events: none !important; // 弹窗不阻挡鼠标事件
|
||||
|
||||
.l7-popup-content {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
pointer-events: none !important; // 内容也不响应鼠标事件,避免阻挡
|
||||
}
|
||||
|
||||
.l7-popup-tip {
|
||||
border-top-color: #667eea;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
// 确保tooltip内容也不阻挡
|
||||
:deep(.trajectory-tooltip) {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
// 确保Element Plus日期选择器面板在最上层
|
||||
:deep(.el-picker-panel) {
|
||||
z-index: 9999 !important;
|
||||
transform: translate3d(0, 0, 0) !important; // 开启硬件加速
|
||||
backface-visibility: hidden !important; // 防止闪烁
|
||||
will-change: transform !important; // 优化渲染性能
|
||||
}
|
||||
|
||||
:deep(.el-popper) {
|
||||
z-index: 9999 !important;
|
||||
transform: translate3d(0, 0, 0) !important;
|
||||
backface-visibility: hidden !important;
|
||||
will-change: transform !important;
|
||||
}
|
||||
|
||||
// 时间选择列表也需要优化
|
||||
:deep(.el-time-panel) {
|
||||
transform: translate3d(0, 0, 0) !important;
|
||||
backface-visibility: hidden !important;
|
||||
}
|
||||
|
||||
:deep(.el-picker-panel__content) {
|
||||
transform: translate3d(0, 0, 0) !important;
|
||||
backface-visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
@@ -51,7 +51,7 @@ export default defineConfig(({ mode, command }) => {
|
||||
rewrite: (p) => p.replace(/^\/file-upload/, '')
|
||||
},
|
||||
'/fileUrl': {
|
||||
target: 'http://154.8.147.51:9000', // 本地
|
||||
target: 'http://117.72.197.29:9000', // 本地
|
||||
changeOrigin: true,
|
||||
rewrite: (p) => p.replace(/^\/fileUrl/, '')
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user