<template>
|
<el-container class="map-screen-container">
|
<!-- 左侧控制面板 -->
|
<el-aside class="control-sidebar" :class="{ collapsed: !isPanelExpanded }">
|
<div class="sidebar-header" @click="togglePanel">
|
<span class="sidebar-title">🎛️ {{ t("screen.controlPanel") }}</span>
|
<el-icon class="toggle-icon" :class="{ rotated: !isPanelExpanded }">
|
<ArrowDown />
|
</el-icon>
|
</div>
|
<div class="sidebar-content" v-show="isPanelExpanded">
|
<el-form class="control-form">
|
<!-- 地图选择分组 -->
|
<el-card class="control-section" :header="t('screen.mapSelection')">
|
<el-form-item :label="t('screen.selectMap')">
|
<el-select v-model="selectedMap" :placeholder="t('screen.selectMapPlaceholder')" @change="handleMapChange">
|
<el-option v-for="map in mapOptions" :key="map.value" :label="map.label" :value="map.value" />
|
</el-select>
|
</el-form-item>
|
</el-card>
|
|
<!-- 图标大小设置分组 -->
|
<el-card class="control-section" :header="t('screen.iconSizeSettings')">
|
<el-form-item :label="t('screen.iconSize')">
|
<el-slider v-model="iconSize" :step="1" @change="redrawMap" />
|
</el-form-item>
|
</el-card>
|
<!-- 轨迹回放按钮 -->
|
<el-card class="control-section" :header="t('screen.trackPlayback')">
|
<el-form-item>
|
<el-button type="primary" @click="showTrackDialog" style="width: 100%">
|
<el-icon><VideoPlay /></el-icon>
|
{{ t("screen.trackPlayback") }}
|
</el-button>
|
</el-form-item>
|
<div class="track-info" v-if="isPlayingTrack && currentTrackPoint">
|
<el-divider />
|
<div class="track-info-title">📊 当前轨迹点信息</div>
|
<div class="track-info-item">
|
<span class="label">X坐标:</span>
|
<span class="value">{{ currentTrackPoint.x }}</span>
|
</div>
|
<div class="track-info-item">
|
<span class="label">Y坐标:</span>
|
<span class="value">{{ currentTrackPoint.y }}</span>
|
</div>
|
<div class="track-info-item">
|
<span class="label">时间:</span>
|
<span class="value">{{ formatDateTime(currentTrackPoint.timestamp) }}</span>
|
</div>
|
<div class="track-info-item">
|
<span class="label">名称:</span>
|
<span class="value">{{ currentTrackPoint.name || "未知" }}</span>
|
</div>
|
|
<div class="track-info-item">
|
<span class="label">进度:</span>
|
<span class="value">{{ currentTrackIndex + 1 }} / {{ trackPositions.length }}</span>
|
</div>
|
</div>
|
</el-card>
|
<!-- 隐藏其他参数面板 -->
|
<div style="display: none">
|
<!-- 尺寸设置分组 -->
|
<el-card class="control-section" header="📏 地图尺寸设置">
|
<el-form-item label="地图宽度" style="display: none">
|
<el-input-number v-model="mapImageWidth" @change="updateMapSize" />
|
<span style="margin-left: 8px">px</span>
|
</el-form-item>
|
<el-form-item label="地图高度" style="display: none">
|
<el-input-number v-model="mapImageHeight" @change="updateMapSize" />
|
<span style="margin-left: 8px">px</span>
|
</el-form-item>
|
<el-divider content-position="left">实际尺寸</el-divider>
|
<el-form-item label="实际长度">
|
<el-input-number v-model="realWidth" :step="1" @change="updateRealSize" />
|
<span style="margin-left: 8px">厘米</span>
|
</el-form-item>
|
<el-form-item label="实际高度">
|
<el-input-number v-model="realHeight" :step="1" @change="updateRealSize" />
|
<span style="margin-left: 8px">厘米</span>
|
</el-form-item>
|
</el-card>
|
|
<!-- 坐标设置分组 -->
|
<el-card class="control-section" header="📍 坐标设置">
|
<el-divider content-position="left">实际坐标</el-divider>
|
<el-form-item label="原点X">
|
<el-input-number v-model="originRealX" :step="1" @change="updateOriginFromReal" />
|
<span style="margin-left: 8px">厘米 ({{ realToPixelX(originRealX).toFixed(0) }}px)</span>
|
</el-form-item>
|
<el-form-item label="原点Y">
|
<el-input-number v-model="originRealY" :step="1" @change="updateOriginFromReal" />
|
<span style="margin-left: 8px">厘米 ({{ realToPixelY(originRealY).toFixed(0) }}px)</span>
|
</el-form-item>
|
<el-divider content-position="left">快速设置</el-divider>
|
<el-form-item style="display: none">
|
<el-button type="primary" @click="setOriginToTopLeft" size="small"> <span>📍</span> 设置原点到左上角 </el-button>
|
<el-button type="success" @click="setOriginToBottomCenter" size="small">
|
<span>🎯</span> 设置原点到底部中心
|
</el-button>
|
</el-form-item>
|
</el-card>
|
|
<!-- 操作按钮分组 -->
|
<el-card class="control-section" header="🎮 操作控制">
|
<el-form-item>
|
<el-button type="primary" @click="enableSetOrigin"> <span>🎯</span> 点击设置原点 </el-button>
|
</el-form-item>
|
</el-card>
|
</div>
|
</el-form>
|
</div>
|
</el-aside>
|
|
<!-- 右侧主内容区域 -->
|
<el-main class="main-content">
|
<!-- 地图容器 -->
|
<el-card class="map-container">
|
<canvas
|
ref="mapCanvas"
|
class="map-canvas"
|
:width="mapWidth"
|
:height="mapHeight"
|
:style="mapCanvasStyle"
|
@mousedown="handleMouseDown"
|
@mousemove="handleMouseMove"
|
@mouseup="handleMouseUp"
|
@wheel="handleWheel"
|
@click="handleMapClick"
|
></canvas>
|
</el-card>
|
|
<!-- 状态信息 -->
|
<el-card class="status-info" :header="t('screen.statusInfo')">
|
<div class="status-items">
|
<span class="status-item">🔍 {{ t("screen.zoom") }}: {{ zoom.toFixed(2) }}x</span>
|
<span class="status-item">📐 {{ t("screen.offset") }}: {{ offsetX.toFixed(0) }}, {{ offsetY.toFixed(0) }}</span>
|
<span class="status-item" v-if="settingOrigin">🎯 {{ t("screen.clickToSetOrigin") }}</span>
|
<span class="status-item">📊 {{ t("screen.container") }}: 100%×100%</span>
|
<span class="status-item">🗺️ {{ t("screen.map") }}: {{ mapImageWidth }}×{{ mapImageHeight }}</span>
|
<span class="status-item">
|
📏 {{ t("screen.realSize") }}: {{ realWidth }}×{{ realHeight }}{{ t("screen.centimeter") }}
|
</span>
|
<span class="status-item">
|
📍 {{ t("screen.origin") }}: {{ originRealX }}, {{ originRealY }}
|
{{ t("screen.centimeter") }}
|
</span>
|
<span class="status-item">👤 {{ t("screen.iconSize") }}: {{ iconSize }}px</span>
|
</div>
|
</el-card>
|
</el-main>
|
<!-- 轨迹回放对话框 -->
|
<el-dialog v-model="trackDialogVisible" :title="t('screen.trackPlayback')" width="600px" :close-on-click-modal="false">
|
<el-form :model="trackForm" label-width="100px">
|
<el-form-item :label="t('screen.deviceId')">
|
<el-input v-model="trackForm.deviceId" :placeholder="t('screen.inputDeviceId')" />
|
</el-form-item>
|
<el-form-item :label="$t('query.range')">
|
<el-date-picker
|
v-model="trackForm.timeRange"
|
type="datetimerange"
|
:range-separator="$t('SearchForm.rangeSeparator')"
|
:start-placeholder="$t('SearchForm.startTime')"
|
:end-placeholder="$t('SearchForm.endTime')"
|
/>
|
</el-form-item>
|
</el-form>
|
|
<template #footer>
|
<span class="dialog-footer">
|
<el-button @click="trackDialogVisible = false">{{ t("Config.cancel") }}</el-button>
|
<el-button type="primary" @click="startTrackPlayback" :loading="isLoadingTrack">
|
{{ t("screen.startPlayback") }}
|
</el-button>
|
</span>
|
</template>
|
</el-dialog>
|
|
<!-- 轨迹控制工具栏 -->
|
<div class="track-controls" v-if="isPlayingTrack">
|
<el-button size="small" @click="pauseTrack">
|
<el-icon><VideoPause /></el-icon>
|
{{ t("screen.pause") }}
|
</el-button>
|
<el-button size="small" @click="resumeTrack" v-if="isTrackPaused">
|
<el-icon><VideoPlay /></el-icon>
|
{{ t("screen.resume") }}
|
</el-button>
|
<el-button size="small" @click="stopTrack">
|
<el-icon><Close /></el-icon>
|
{{ t("screen.stop") }}
|
</el-button>
|
<el-slider v-model="playbackSpeed" :min="1" :step="1" :marks="speedMarks" style="width: 150px; margin: 0 15px" />
|
</div>
|
</el-container>
|
</template>
|
|
<script setup lang="ts">
|
import { ref, computed, onMounted, onUnmounted, watch } from "vue";
|
import { useI18n } from "vue-i18n";
|
import { ElMessage } from "element-plus";
|
import { ArrowDown } from "@element-plus/icons-vue";
|
import { getTwoMapList } from "@/api/modules/hxzk/map/twomap";
|
import { FindUserCompanyMap, FindUserCompanyMapSelect } from "@/api/modules/hxzk/map/twomap";
|
import { RealPosition2D } from "@/api/modules/hxzk/person/person";
|
import positionIcon from "@/views/hxzk/images/person.png";
|
import { FindTrackView } from "@/api/modules/hxzk/track/track";
|
import { getAnchorList } from "@/api/modules/hxzk/device/anchor";
|
const { t } = useI18n();
|
|
// 地图选项
|
const mapOptions = ref();
|
|
// 响应式数据
|
const selectedMap = ref("");
|
const mapWidth = ref(0); // 容器宽度像素值
|
const mapHeight = ref(0); // 容器高度像素值
|
const mapImageWidth = ref(0); // 地图图片宽度像素值
|
const mapImageHeight = ref(0); // 地图图片高度像素值
|
const realWidth = ref(0); // 实际长度(厘米)
|
const realHeight = ref(0); // 实际高度(厘米)
|
const originX = ref(0);
|
const originY = ref(0);
|
const originRealX = ref(0); // 原点实际X坐标(厘米)
|
const originRealY = ref(0); // 原点实际Y坐标(厘米),在底部
|
const zoom = ref(1);
|
const offsetX = ref(0);
|
const offsetY = ref(0);
|
const settingOrigin = ref(false);
|
const showOrigin = ref(true);
|
const isPanelExpanded = ref(true); // 控制面板展开状态
|
const showPersonLabels = ref(true); // 是否显示标签
|
const iconSize = ref(24); // 图标大小(默认24px)
|
|
// 轨迹回放相关状态
|
const trackDialogVisible = ref(false);
|
const trackForm = ref({
|
selectedMap: "",
|
deviceId: "",
|
timeRange: []
|
});
|
const trackPositions = ref([]);
|
const isPlayingTrack = ref(false);
|
const isTrackPaused = ref(false);
|
const currentTrackIndex = ref(0);
|
const playbackSpeed = ref(2);
|
const isLoadingTrack = ref(false);
|
const trackTimer = ref(null);
|
|
// 人员位置数据(从后端实时获取)
|
const personPositions = ref<
|
Array<{
|
id: number;
|
name: string;
|
realX: number;
|
realY: number;
|
pimage?: string; // 人员头像URL
|
type?: string; // 人员类型
|
}>
|
>([]);
|
|
// 基站位置数据(从后端获取)
|
const anchorPositions = ref<
|
Array<{
|
id: number;
|
anchorid: string;
|
posx: string;
|
posy: string;
|
posz: string;
|
layer: string;
|
zu: string;
|
liangcheng: string;
|
anchormode: string;
|
anchorip: string;
|
version: string;
|
gonglv: string;
|
jiaozhundistance: string;
|
greatetime: string;
|
tongbu: string;
|
baoliu1: string;
|
baoliu2: string;
|
baoliu3: string;
|
baoliu4: string;
|
baoliu5: string;
|
baoliu6: string;
|
baoliu7: string;
|
baoliu8: string;
|
baoliu9: string;
|
baoliu10: string;
|
baoliu11: string;
|
baoliu12: string;
|
baoliu13: string;
|
baoliu14: string;
|
baoliu15: string;
|
baoliu16: string;
|
baoliu17: string;
|
baoliu18: string | null;
|
baoliu19: string;
|
baoliu20: string;
|
baoliu21: string;
|
company: string;
|
}>
|
>([]);
|
|
// 人员图标缓存
|
const personIconsCache = ref<Record<string, HTMLImageElement>>({});
|
|
// 默认人员图标
|
const defaultPersonIcon = new Image();
|
defaultPersonIcon.src = positionIcon; // 默认图标路径
|
|
// 定时器引用
|
let positionTimer: NodeJS.Timeout | null = null;
|
|
// 从后端获取人员位置数据
|
const fetchPersonPositions = async () => {
|
try {
|
const response = await RealPosition2D();
|
if (response.data && Array.isArray(response.data)) {
|
// 转换数据格式,确保包含realX和realY字段
|
personPositions.value = response.data.map(person => ({
|
id: person.id || 0,
|
name: person.pname || t("screen.unknownPerson"),
|
realX: Number(person.px) || person.realX || 0, // 兼容不同的字段名
|
realY: Number(person.py) || person.realY || 0,
|
pimage: person.pimage, // 假设后端返回头像URL
|
type: person.type // 假设后端返回人员类型
|
}));
|
|
// 预加载人员图标
|
preloadPersonIcons(personPositions.value);
|
|
// 重绘地图以显示最新位置
|
redrawMap();
|
}
|
} catch (error) {
|
console.error(t("screen.error.fetchPositionFailed"), error);
|
ElMessage.error(t("screen.error.fetchPositionFailed"));
|
}
|
};
|
|
// 预加载人员图标
|
const preloadPersonIcons = (persons: any[]) => {
|
persons.forEach(person => {
|
if (person.pimage && !personIconsCache.value[person.pimage]) {
|
const img = new Image();
|
img.onload = () => {
|
personIconsCache.value[person.pimage] = img;
|
redrawMap(); // 图标加载完成后重绘
|
};
|
img.onerror = () => {
|
console.warn(`Failed to load person icon: ${person.pimage}`);
|
// 使用默认图标
|
personIconsCache.value[person.pimage] = defaultPersonIcon;
|
};
|
img.src = person.pimage;
|
}
|
});
|
};
|
|
// 加载基站图标
|
const AddAnchorIcons = async () => {
|
// 合并所有参数
|
const finalParams = {
|
tableList: { pageSize: 100, pageNum: 1 },
|
achor: {},
|
type: 1
|
};
|
const response = await getAnchorList(finalParams);
|
console.log(response.data.list);
|
|
// 保存基站数据
|
anchorPositions.value = response.data.list;
|
|
// 重绘画布以显示基站图标
|
drawCanvas();
|
};
|
AddAnchorIcons();
|
// 根据人员类型获取图标
|
const getPersonIcon = (person: any) => {
|
if (person.pimage && personIconsCache.value[person.pimage]) {
|
return personIconsCache.value[person.pimage];
|
}
|
|
// 默认图标
|
return defaultPersonIcon;
|
};
|
|
// 启动实时位置更新
|
const startPositionUpdates = () => {
|
// 立即获取一次数据
|
fetchPersonPositions();
|
|
// 每2秒更新一次位置
|
positionTimer = setInterval(fetchPersonPositions, 2000);
|
};
|
|
// 停止实时位置更新
|
const stopPositionUpdates = () => {
|
if (positionTimer) {
|
clearInterval(positionTimer);
|
positionTimer = null;
|
}
|
};
|
|
// DOM引用
|
const mapCanvas = ref<HTMLCanvasElement>();
|
|
// Canvas相关
|
const canvasCtx = ref<CanvasRenderingContext2D | null>(null);
|
const mapImage = ref<HTMLImageElement | null>(null);
|
|
// 拖拽状态
|
const isDragging = ref(false);
|
const dragStartX = ref(0);
|
const dragStartY = ref(0);
|
const dragStartOffsetX = ref(0);
|
const dragStartOffsetY = ref(0);
|
|
// 计算属性
|
const mapCanvasStyle = computed(() => ({
|
width: "100%",
|
height: "100%",
|
cursor: isDragging.value ? "grabbing" : settingOrigin.value ? "crosshair" : "grab"
|
}));
|
|
// 像素坐标转换为实际坐标
|
const pixelToRealX = (pixelX: number) => {
|
return (pixelX / mapImageWidth.value) * realWidth.value;
|
};
|
|
const pixelToRealY = (pixelY: number) => {
|
// Y轴从上到下递增,0在顶部,realHeight在底部
|
// 实际高度2700厘米时,y=2700在底部
|
return (pixelY / mapImageHeight.value) * realHeight.value;
|
};
|
|
// 实际坐标转换为像素坐标
|
const realToPixelX = (realX: number) => {
|
return (realX / realWidth.value) * mapImageWidth.value;
|
};
|
|
const realToPixelY = (realY: number) => {
|
// Y轴从上到下递增,0在顶部,realHeight在底部
|
// 正的实际坐标转换为正的像素坐标
|
return (realY / realHeight.value) * mapImageHeight.value;
|
};
|
|
// 方法
|
const togglePanel = () => {
|
isPanelExpanded.value = !isPanelExpanded.value;
|
};
|
// 显示轨迹回放对话框
|
const showTrackDialog = () => {
|
trackForm.value.selectedMap = selectedMap.value;
|
trackDialogVisible.value = true;
|
};
|
// 开始轨迹回放
|
|
const startTrackPlayback = async () => {
|
if (!trackForm.value.deviceId) {
|
ElMessage.warning(t("screen.inputDeviceIdFirst"));
|
return;
|
}
|
|
if (!trackForm.value.timeRange || trackForm.value.timeRange.length !== 2) {
|
ElMessage.warning(t("screen.selectTimeRangeFirst"));
|
return;
|
}
|
|
isLoadingTrack.value = true;
|
const tracks = ref({
|
tagid: trackForm.value.deviceId,
|
time: trackForm.value.timeRange
|
});
|
try {
|
// 调用API获取轨迹数据
|
const response = await FindTrackView(tracks.value);
|
console.log(response);
|
if (response.data.total && response.data.total > 0) {
|
// 如果需要合并所有轨迹点
|
// 假设 response 是已经定义好的对象
|
trackPositions.value = Object.values(response.data.list)
|
.flat()
|
.map(item => {
|
// 生成随机的 x 和 y 值
|
const x = item.x;
|
const y = item.y;
|
return {
|
x: x,
|
y: y,
|
timestamp: item.time,
|
speed: item.baoliu4,
|
name: item.tagid || "" // 确保包含名称字段
|
};
|
});
|
|
currentTrackIndex.value = 0;
|
isPlayingTrack.value = true;
|
isTrackPaused.value = false;
|
trackDialogVisible.value = false;
|
|
drawCanvas(); // 立即重绘
|
|
// 开始播放轨迹
|
startTrackPlaybackTimer();
|
} else {
|
ElMessage.warning(t("screen.noTrackData"));
|
}
|
} catch (error) {
|
console.error("获取轨迹数据失败:", error);
|
ElMessage.error(t("screen.failedToGetTrackData"));
|
} finally {
|
isLoadingTrack.value = false;
|
}
|
};
|
const currentTrackPoint = ref<any>(null);
|
// 开始轨迹播放定时器
|
const startTrackPlaybackTimer = () => {
|
if (trackTimer.value) {
|
clearInterval(trackTimer.value);
|
}
|
|
const interval = 1000 / playbackSpeed.value;
|
|
trackTimer.value = setInterval(() => {
|
if (currentTrackIndex.value < trackPositions.value.length - 1) {
|
currentTrackIndex.value++;
|
// 更新当前轨迹点信息
|
currentTrackPoint.value = trackPositions.value[currentTrackIndex.value];
|
drawCanvas();
|
} else {
|
// 播放到最后一个点时暂停而不是停止
|
pauseTrack();
|
ElNotification.info({
|
title: t("screen.trackPlaybackCompleted"),
|
message: t("screen.trackPlaybackFinished")
|
});
|
}
|
}, interval);
|
};
|
|
// 暂停轨迹回放
|
const pauseTrack = () => {
|
if (trackTimer.value) {
|
clearInterval(trackTimer.value);
|
trackTimer.value = null;
|
}
|
isTrackPaused.value = true;
|
};
|
|
// 恢复轨迹回放
|
const resumeTrack = () => {
|
if (!isTrackPaused.value) return;
|
|
isTrackPaused.value = false;
|
startTrackPlaybackTimer();
|
};
|
|
// 停止轨迹回放
|
const stopTrack = () => {
|
if (trackTimer.value) {
|
clearInterval(trackTimer.value);
|
trackTimer.value = null;
|
}
|
|
isPlayingTrack.value = false;
|
isTrackPaused.value = false;
|
currentTrackIndex.value = 0;
|
trackPositions.value = [];
|
|
drawCanvas();
|
};
|
|
// 监听播放速度变化
|
watch(playbackSpeed, newSpeed => {
|
console.log(newSpeed);
|
if (isPlayingTrack.value && !isTrackPaused.value) {
|
pauseTrack();
|
resumeTrack();
|
}
|
});
|
// 更新状态信息
|
const updateStatusInfo = (additionalInfo = "") => {
|
// 这里可以添加状态信息更新逻辑
|
console.log("状态信息更新:", additionalInfo);
|
};
|
|
// 在组件顶部声明 mapImages
|
const mapImages = ref<Record<string, string>>({});
|
|
// 修改 handleMapChange 方法
|
|
const handleMapChange = (value: string) => {
|
const params = {
|
departmentId: "",
|
pageNum: 0,
|
pageSize: 100
|
};
|
getTwoMapList(params)
|
.then(mapCompany => {
|
for (let i = 0; i < mapCompany.data.list.length; i++) {
|
if (mapCompany.data.list[i].mapname.split(".")[0] == value) {
|
console.log(mapCompany.data.list[i]);
|
mapImageWidth.value = mapCompany.data.list[i].xpixel;
|
mapImageHeight.value = mapCompany.data.list[i].ypixel;
|
realWidth.value = mapCompany.data.list[i].xtruelength;
|
realHeight.value = mapCompany.data.list[i].ytruewidth;
|
console.log(mapCompany.data.list[i]);
|
originRealX.value = mapCompany.data.list[i].x0Length;
|
originRealY.value = mapCompany.data.list[i].y0Width;
|
}
|
}
|
})
|
.catch(error => {
|
console.error("Error:", error);
|
});
|
const imageUrl = mapImages.value[value];
|
|
if (!imageUrl) {
|
return;
|
}
|
|
const img = new Image();
|
img.onload = () => {
|
mapImage.value = img;
|
drawCanvas();
|
const mapLabel = mapOptions.value.find((m: any) => m.value === value)?.label;
|
ElMessage.success(t("screen.mapSwitched", { map: mapLabel || value }));
|
};
|
img.onerror = () => {
|
ElMessage.error(t("screen.error.mapLoadFailed"));
|
};
|
img.src = imageUrl;
|
};
|
|
const updateMapSize = () => {
|
if (mapCanvas.value) {
|
const parentElement = mapCanvas.value.parentElement;
|
if (parentElement) {
|
const containerRect = parentElement.getBoundingClientRect();
|
mapWidth.value = containerRect.width;
|
mapHeight.value = containerRect.height;
|
}
|
|
mapCanvas.value.width = mapImageWidth.value;
|
mapCanvas.value.height = mapImageHeight.value;
|
drawCanvas();
|
}
|
};
|
|
const updateRealSize = () => {
|
// 实际尺寸更新时重新绘制画布
|
drawCanvas();
|
console.log("实际尺寸更新:", realWidth.value, realHeight.value);
|
};
|
|
const updateOriginFromReal = () => {
|
// 从实际坐标更新原点
|
|
drawCanvas();
|
console.log("原点坐标更新(实际):", originRealX.value, originRealY.value);
|
ElMessage.success(t("screen.originUpdated", { x: originRealX.value.toFixed(0), y: originRealY.value.toFixed(0) }));
|
};
|
|
// 设置原点到左上角
|
const setOriginToTopLeft = () => {
|
originX.value = 0;
|
originY.value = 0; // 像素坐标的左上角
|
originRealX.value = 0;
|
originRealY.value = 0; // 实际坐标的顶部(Y=0)
|
drawCanvas();
|
ElMessage.success(t("screen.originSetToTopLeft"));
|
};
|
|
// 设置原点到底部中心
|
const setOriginToBottomCenter = () => {
|
originX.value = mapImageWidth.value / 2;
|
originY.value = mapImageHeight.value; // 像素坐标的底部
|
|
drawCanvas();
|
ElMessage.success(t("screen.originSetToBottomCenter", { x: originRealX.value.toFixed(0), y: originRealY.value.toFixed(0) }));
|
};
|
|
const enableSetOrigin = () => {
|
settingOrigin.value = true;
|
ElMessage.info(t("screen.clickToSetOriginInfo"));
|
};
|
|
const handleMouseDown = (e: MouseEvent) => {
|
if (settingOrigin.value) return;
|
|
isDragging.value = true;
|
dragStartX.value = e.clientX;
|
dragStartY.value = e.clientY;
|
dragStartOffsetX.value = offsetX.value;
|
dragStartOffsetY.value = offsetY.value;
|
};
|
|
const handleMouseMove = (e: MouseEvent) => {
|
if (!isDragging.value || settingOrigin.value) return;
|
|
const deltaX = e.clientX - dragStartX.value;
|
const deltaY = e.clientY - dragStartY.value;
|
|
offsetX.value = dragStartOffsetX.value + deltaX;
|
offsetY.value = dragStartOffsetY.value + deltaY;
|
|
// 计算实际坐标
|
if (mapCanvas.value) {
|
const rect = mapCanvas.value.getBoundingClientRect();
|
const mouseX = e.clientX - rect.left;
|
const mouseY = e.clientY - rect.top;
|
|
// 转换为地图坐标
|
const mapX = (mouseX - offsetX.value) / zoom.value;
|
const mapY = (mouseY - offsetY.value) / zoom.value;
|
|
// 转换为实际坐标
|
const realX = pixelToRealX(mapX);
|
const realY = pixelToRealY(mapY);
|
|
// 更新状态信息显示实际坐标
|
updateStatusInfo(t("screen.mousePosition", { x: realX.toFixed(0), y: realY.toFixed(0) }));
|
}
|
|
drawCanvas();
|
};
|
|
const handleMouseUp = () => {
|
isDragging.value = false;
|
drawCanvas();
|
};
|
|
const handleWheel = (e: WheelEvent) => {
|
e.preventDefault();
|
|
const delta = e.deltaY > 0 ? -0.1 : 0.1;
|
const newZoom = Math.max(0.1, Math.min(5, zoom.value + delta));
|
|
// 以鼠标位置为中心缩放
|
if (mapCanvas.value) {
|
const rect = mapCanvas.value.getBoundingClientRect();
|
const mouseX = e.clientX - rect.left;
|
const mouseY = e.clientY - rect.top;
|
|
// 计算鼠标相对于地图容器的位置
|
const relativeX = (mouseX - offsetX.value) / zoom.value;
|
const relativeY = (mouseY - offsetY.value) / zoom.value;
|
|
// 计算缩放后的偏移量
|
offsetX.value = mouseX - relativeX * newZoom;
|
offsetY.value = mouseY - relativeY * newZoom;
|
}
|
|
zoom.value = newZoom;
|
drawCanvas();
|
};
|
|
const handleMapClick = (e: MouseEvent) => {
|
if (!settingOrigin.value || !mapCanvas.value) return;
|
|
const rect = mapCanvas.value.getBoundingClientRect();
|
const clickX = e.clientX - rect.left;
|
const clickY = e.clientY - rect.top;
|
|
// 计算相对于左上角的坐标
|
originX.value = (clickX - offsetX.value) / zoom.value;
|
originY.value = (clickY - offsetY.value) / zoom.value;
|
|
settingOrigin.value = false;
|
drawCanvas();
|
|
ElMessage.success(t("screen.originUpdated", { x: originRealX.value.toFixed(0), y: originRealY.value.toFixed(0) }));
|
};
|
|
// Canvas绘制函数
|
|
const drawCanvas = () => {
|
if (!canvasCtx.value || !mapCanvas.value) return;
|
|
const ctx = canvasCtx.value;
|
const canvas = mapCanvas.value;
|
|
// 清空画布
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
// 保存当前状态
|
ctx.save();
|
|
// 应用变换(先平移,再缩放)
|
ctx.translate(offsetX.value, offsetY.value);
|
ctx.scale(zoom.value, zoom.value);
|
|
// 绘制地图图片
|
if (mapImage.value) {
|
ctx.drawImage(mapImage.value, 0, 0, mapImageWidth.value, mapImageHeight.value);
|
}
|
|
// 绘制轨迹(如果有)
|
if (isPlayingTrack.value && trackPositions.value.length > 0) {
|
drawTrack(ctx);
|
}
|
|
// 绘制原点标记
|
if (showOrigin.value) {
|
drawOriginMarker(ctx);
|
}
|
|
// 绘制人员标记
|
personPositions.value.forEach(person => {
|
drawPersonMarker(ctx, person);
|
});
|
|
// 绘制基站标记
|
anchorPositions.value.forEach(anchor => {
|
drawAnchorMarker(ctx, anchor);
|
});
|
|
// 恢复状态
|
ctx.restore();
|
};
|
|
const drawOriginMarker = (ctx: CanvasRenderingContext2D) => {
|
// 绘制原点
|
ctx.fillStyle = "#ff4444";
|
ctx.strokeStyle = "white";
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
ctx.arc(originX.value, originY.value, 6, 0, Math.PI * 2);
|
ctx.fill();
|
ctx.stroke();
|
|
// 绘制原点标签
|
ctx.fillStyle = "rgba(255, 68, 68, 0.9)";
|
ctx.font = "12px Arial";
|
ctx.textAlign = "center";
|
ctx.textBaseline = "bottom";
|
|
const realOriginX = pixelToRealX(originX.value);
|
const realOriginY = pixelToRealY(originY.value);
|
const labelText = t("screen.originLabel", { x: realOriginX.toFixed(0), y: realOriginY.toFixed(0) });
|
const textWidth = ctx.measureText(labelText).width;
|
ctx.fillRect(originX.value - textWidth / 2 - 6, originY.value - 25, textWidth + 12, 16);
|
|
ctx.fillStyle = "white";
|
ctx.fillText(labelText, originX.value, originY.value - 10);
|
};
|
// 绘制轨迹
|
const drawTrack = (ctx: CanvasRenderingContext2D) => {
|
if (trackPositions.value.length === 0) return;
|
|
ctx.strokeStyle = "#ff6b6b";
|
ctx.lineWidth = 3;
|
ctx.setLineDash([5, 5]);
|
ctx.lineCap = "round";
|
ctx.lineJoin = "round";
|
|
// // 绘制完整轨迹线
|
// ctx.beginPath();
|
// for (let i = 0; i < trackPositions.value.length; i++) {
|
// const point = trackPositions.value[i];
|
// const pixelX = originX.value + realToPixelX(point.x);
|
// const pixelY = originY.value + realToPixelY(point.y);
|
|
// if (i === 0) {
|
// ctx.moveTo(pixelX, pixelY);
|
// } else {
|
// ctx.lineTo(pixelX, pixelY);
|
// }
|
// }
|
// ctx.stroke();
|
|
// 重置虚线样式
|
ctx.setLineDash([]);
|
|
// 绘制已走过的轨迹(实线)
|
if (currentTrackIndex.value > 0) {
|
ctx.strokeStyle = "#C70708";
|
ctx.lineWidth = 5;
|
|
ctx.beginPath();
|
for (let i = 0; i <= currentTrackIndex.value; i++) {
|
const point = trackPositions.value[i];
|
const pixelX = originX.value + realToPixelX(point.x);
|
const pixelY = originY.value + realToPixelY(point.y);
|
|
if (i === 0) {
|
ctx.moveTo(pixelX, pixelY);
|
} else {
|
ctx.lineTo(pixelX, pixelY);
|
}
|
}
|
ctx.stroke();
|
}
|
|
// 绘制当前轨迹点
|
if (currentTrackIndex.value >= 0 && currentTrackIndex.value < trackPositions.value.length) {
|
const currentPoint = trackPositions.value[currentTrackIndex.value];
|
const pixelX = originX.value + realToPixelX(currentPoint.x);
|
const pixelY = originY.value + realToPixelY(currentPoint.y);
|
|
// 绘制当前点
|
ctx.fillStyle = "#ff9f1c";
|
ctx.beginPath();
|
ctx.arc(pixelX, pixelY, 8, 0, Math.PI * 2);
|
ctx.fill();
|
|
// 绘制当前点信息
|
ctx.fillStyle = "rgba(255, 159, 28, 0.9)";
|
ctx.font = "12px Arial";
|
ctx.textAlign = "center";
|
ctx.textBaseline = "bottom";
|
|
const infoText = currentPoint.name || t("screen.trackPoint");
|
const textWidth = ctx.measureText(infoText).width;
|
ctx.fillRect(pixelX - textWidth / 2 - 6, pixelY - 30, textWidth + 12, 16);
|
|
ctx.fillStyle = "white";
|
ctx.fillText(infoText, pixelX, pixelY - 15);
|
}
|
};
|
|
const drawPersonMarker = (ctx: CanvasRenderingContext2D, person: any) => {
|
// 将人员相对于原点的实际坐标转换为绝对像素坐标
|
const personPixelX = originX.value + realToPixelX(person.realX);
|
const personPixelY = originY.value + realToPixelY(person.realY);
|
|
// 获取人员图标
|
const icon = getPersonIcon(person);
|
|
// 检查图标是否已加载
|
if (icon.complete && icon.naturalHeight !== 0) {
|
// 绘制人员图标
|
ctx.save();
|
ctx.beginPath();
|
ctx.arc(personPixelX, personPixelY, iconSize.value / 2, 0, Math.PI * 2);
|
ctx.clip();
|
ctx.drawImage(icon, personPixelX - iconSize.value / 2, personPixelY - iconSize.value / 2, iconSize.value, iconSize.value);
|
ctx.restore();
|
} else {
|
// 如果图片未加载完成,使用备用圆形标记
|
ctx.fillStyle = "#4444ff";
|
ctx.strokeStyle = "white";
|
ctx.lineWidth = 2;
|
|
ctx.beginPath();
|
ctx.arc(personPixelX, personPixelY, 8, 0, Math.PI * 2);
|
ctx.fill();
|
ctx.stroke();
|
}
|
|
if (showPersonLabels.value) {
|
drawPersonLabel(ctx, person, personPixelX, personPixelY, iconSize.value);
|
}
|
};
|
// 格式化日期时间
|
const formatDateTime = (timestamp: string | number | Date) => {
|
if (!timestamp) return "未知";
|
|
try {
|
const date = new Date(timestamp);
|
return date.toLocaleString("zh-CN", {
|
year: "numeric",
|
month: "2-digit",
|
day: "2-digit",
|
hour: "2-digit",
|
minute: "2-digit",
|
second: "2-digit"
|
});
|
} catch (e) {
|
return String(timestamp);
|
}
|
};
|
// 绘制人员标签 - 使用现代样式
|
const drawPersonLabel = (ctx: CanvasRenderingContext2D, person: any, x: number, y: number, iconSize: number) => {
|
const labelText = person.name || t("screen.unknownPerson");
|
drawModernLabel(ctx, labelText, x, y, iconSize);
|
};
|
|
// 绘制基站图标
|
const drawAnchorMarker = (ctx: CanvasRenderingContext2D, anchor: any) => {
|
// 将基站的posx和posy(实际坐标)转换为像素坐标
|
const anchorPixelX = originX.value + realToPixelX(Number(anchor.posx));
|
const anchorPixelY = originY.value + realToPixelY(Number(anchor.posy));
|
|
// 绘制基站图标 - 使用蓝色圆形表示基站
|
ctx.fillStyle = "#2196F3"; // 蓝色
|
ctx.strokeStyle = "white";
|
ctx.lineWidth = 2;
|
ctx.beginPath();
|
ctx.arc(anchorPixelX, anchorPixelY, 10, 0, Math.PI * 2); // 基站图标比人员图标稍大
|
ctx.fill();
|
ctx.stroke();
|
|
// 绘制基站标签
|
if (showPersonLabels.value) {
|
drawAnchorLabel(ctx, anchor, anchorPixelX, anchorPixelY);
|
}
|
};
|
|
// 绘制基站标签
|
const drawAnchorLabel = (ctx: CanvasRenderingContext2D, anchor: any, x: number, y: number) => {
|
const labelText = `基站 ${anchor.anchorid}`;
|
drawModernLabel(ctx, labelText, x, y, 20); // 使用与人员标签相同的样式,但传入基站图标大小
|
};
|
|
// 现代样式标签
|
const drawModernLabel = (ctx: CanvasRenderingContext2D, text: string, x: number, y: number, iconSize: number) => {
|
ctx.font = "bold 13px 'Microsoft YaHei', sans-serif";
|
ctx.textAlign = "center";
|
ctx.textBaseline = "middle";
|
|
// 绘制背景
|
const textWidth = ctx.measureText(text).width;
|
const labelHeight = 20;
|
const labelWidth = textWidth + 16;
|
const labelX = x - labelWidth / 2;
|
const labelY = y - iconSize / 2 - labelHeight - 4;
|
|
// 圆角矩形
|
ctx.beginPath();
|
ctx.roundRect(labelX, labelY, labelWidth, labelHeight, 10);
|
ctx.fillStyle = "rgba(59, 130, 246, 0.9)";
|
ctx.fill();
|
|
// 添加小三角形指示器
|
ctx.beginPath();
|
ctx.moveTo(x - 6, labelY + labelHeight);
|
ctx.lineTo(x, labelY + labelHeight + 6);
|
ctx.lineTo(x + 6, labelY + labelHeight);
|
ctx.fill();
|
|
// 文字
|
ctx.fillStyle = "white";
|
ctx.fillText(text, x, labelY + labelHeight / 2);
|
};
|
|
// 重绘地图(当人员位置更新后调用)
|
const redrawMap = () => {
|
drawCanvas();
|
};
|
|
// 生命周期
|
onMounted(() => {
|
// 初始化Canvas
|
if (mapCanvas.value) {
|
canvasCtx.value = mapCanvas.value.getContext("2d");
|
}
|
FindUserCompanyMap().then(mapLists => {
|
mapOptions.value = mapLists;
|
});
|
FindUserCompanyMapSelect().then(response => {
|
// 后端返回的是 Map<string, string>,直接赋值给 mapImages
|
mapImages.value = response;
|
});
|
// 初始化默认地图
|
handleMapChange(selectedMap.value);
|
});
|
|
// 全局鼠标事件处理
|
const handleGlobalMouseUp = () => {
|
isDragging.value = false;
|
};
|
|
onMounted(() => {
|
const params = {
|
departmentId: "",
|
pageNum: 0,
|
pageSize: 100
|
};
|
getTwoMapList(params)
|
.then(mapCompany => {
|
// 在这里使用 mapCompany
|
mapImageWidth.value = mapCompany.data.list[1].xpixel;
|
mapImageHeight.value = mapCompany.data.list[1].ypixel;
|
realWidth.value = mapCompany.data.list[1].xtruelength;
|
realHeight.value = mapCompany.data.list[1].ytruewidth;
|
originRealX.value = 0;
|
originRealY.value = 0;
|
})
|
.catch(error => {
|
console.error("Error:", error);
|
});
|
|
document.addEventListener("mouseup", handleGlobalMouseUp);
|
// 初始化地图尺寸
|
setTimeout(() => {
|
updateMapSize();
|
}, 100);
|
// 添加窗口大小变化监听
|
window.addEventListener("resize", handleResize);
|
|
// 启动实时位置更新
|
startPositionUpdates();
|
});
|
setTimeout(function () {
|
updateOriginFromReal();
|
}, 1000);
|
onUnmounted(() => {
|
document.removeEventListener("mouseup", handleGlobalMouseUp);
|
window.removeEventListener("resize", handleResize);
|
// 停止实时位置更新
|
stopPositionUpdates();
|
});
|
|
// 窗口大小变化处理
|
const handleResize = () => {
|
updateMapSize();
|
};
|
</script>
|
|
<style scoped>
|
.map-screen-container {
|
display: flex;
|
height: 100vh;
|
font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif;
|
color: #303133;
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
}
|
|
/* 左侧控制面板样式 */
|
.control-sidebar {
|
z-index: 10;
|
display: flex;
|
flex-direction: column;
|
width: 320px;
|
min-width: 280px;
|
max-width: 400px;
|
height: 100%;
|
overflow: hidden;
|
background: white;
|
box-shadow: 2px 0 10px rgb(0 0 0 / 10%);
|
transition: all 0.3s ease;
|
}
|
.control-sidebar.collapsed {
|
width: 50px;
|
min-width: 50px;
|
}
|
.sidebar-header {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
min-height: 60px;
|
padding: 16px;
|
color: white;
|
cursor: pointer;
|
user-select: none;
|
background: linear-gradient(135deg, #0f0f0f 0%, #080808 100%);
|
}
|
.sidebar-title {
|
overflow: hidden;
|
font-size: 16px;
|
font-weight: 600;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
.toggle-icon {
|
flex-shrink: 0;
|
font-size: 18px;
|
transition: transform 0.3s ease;
|
}
|
.toggle-icon.rotated {
|
transform: rotate(180deg);
|
}
|
.sidebar-content {
|
flex: 1;
|
padding: 16px;
|
overflow-y: auto;
|
transition: all 0.3s ease;
|
}
|
|
/* 右侧主内容区域 */
|
.main-content {
|
display: flex;
|
flex: 1;
|
flex-direction: column;
|
gap: 16px;
|
padding: 16px;
|
overflow: hidden;
|
background: #f8f9fa;
|
}
|
|
/* 控制表单样式 */
|
.control-form {
|
display: flex;
|
flex-direction: column;
|
}
|
.control-form :deep(.el-input-number) {
|
width: 100%;
|
}
|
.control-form :deep(.el-select) {
|
width: 100%;
|
}
|
.control-form :deep(.el-button--primary) {
|
background: linear-gradient(135deg, #409eff 0%, #66b1ff 100%);
|
border: none;
|
}
|
.control-form :deep(.el-button--primary:hover) {
|
background: linear-gradient(135deg, #66b1ff 0%, #409eff 100%);
|
box-shadow: 0 4px 12px rgb(64 158 255 / 30%);
|
}
|
.control-form :deep(.el-slider) {
|
width: 100%;
|
}
|
|
/* 分组卡片样式 */
|
.control-section {
|
margin-bottom: 16px;
|
border: 1px solid #ebeef5;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
transition: all 0.3s ease;
|
}
|
.control-section:last-child {
|
margin-bottom: 0;
|
}
|
.control-section:hover {
|
box-shadow: 0 4px 16px rgb(0 0 0 / 12%);
|
transform: translateY(-2px);
|
}
|
.control-section :deep(.el-card__header) {
|
padding: 12px 16px;
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
background: linear-gradient(135deg, #f8f9fb 0%, #f0f2f5 100%);
|
border-bottom: 1px solid #ebeef5;
|
border-radius: 8px 8px 0 0;
|
}
|
.control-section :deep(.el-card__body) {
|
padding: 16px;
|
}
|
|
/* 地图容器样式 */
|
.map-container {
|
display: flex;
|
flex: 1;
|
align-items: center;
|
justify-content: center;
|
min-height: 400px;
|
overflow: hidden;
|
border: none;
|
border-radius: 8px;
|
box-shadow: 0 4px 20px rgb(0 0 0 / 10%);
|
}
|
.map-container :deep(.el-card__body) {
|
position: relative;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
width: 100%;
|
height: 100%;
|
min-height: 400px;
|
padding: 0;
|
}
|
.map-canvas {
|
display: block;
|
max-width: 95%;
|
max-height: 95%;
|
background-color: #ffffff;
|
border-radius: 4px;
|
box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
|
transition: all 0.3s ease;
|
}
|
.map-canvas:hover {
|
box-shadow: 0 4px 20px 0 rgb(0 0 0 / 15%);
|
}
|
|
/* 状态信息样式 */
|
.status-info {
|
border: none;
|
border-radius: 8px;
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
}
|
.status-info :deep(.el-card__header) {
|
padding: 12px 20px;
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
background: linear-gradient(135deg, #f8f9fb 0%, #f0f2f5 100%);
|
border-bottom: 1px solid #ebeef5;
|
border-radius: 8px 8px 0 0;
|
}
|
.status-info :deep(.el-card__body) {
|
padding: 16px 20px;
|
}
|
.status-items {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 12px;
|
}
|
.status-item {
|
padding: 8px 12px;
|
font-size: 13px;
|
font-weight: 500;
|
color: #606266;
|
background: linear-gradient(135deg, #f5f7fa 0%, #e4e7ed 100%);
|
border: 1px solid #dcdfe6;
|
border-radius: 6px;
|
transition: all 0.3s ease;
|
}
|
.status-item:hover {
|
color: #409eff;
|
background: linear-gradient(135deg, #ecf5ff 0%, #d9ecff 100%);
|
border-color: #c6e2ff;
|
box-shadow: 0 4px 8px rgb(64 158 255 / 20%);
|
transform: translateY(-2px);
|
}
|
|
/* 响应式设计 */
|
@media (width <= 1024px) {
|
.control-sidebar {
|
width: 280px;
|
min-width: 240px;
|
}
|
.control-sidebar.collapsed {
|
width: 50px;
|
min-width: 50px;
|
}
|
.sidebar-title {
|
font-size: 14px;
|
}
|
.status-items {
|
gap: 8px;
|
}
|
.status-item {
|
padding: 6px 10px;
|
font-size: 12px;
|
}
|
}
|
|
/* 响应式设计 */
|
@media (width <= 768px) {
|
.control-panel {
|
max-height: 240px;
|
margin-bottom: 12px;
|
}
|
.control-panel :deep(.el-card__header) {
|
padding: 10px 16px;
|
font-size: 14px;
|
}
|
.control-panel :deep(.el-card__body) {
|
padding: 12px;
|
}
|
.control-form {
|
gap: 12px;
|
}
|
.control-form :deep(.el-input-number),
|
.control-form :deep(.el-select) {
|
width: 100px;
|
}
|
.control-form :deep(.el-button) {
|
padding: 10px 16px;
|
font-size: 13px;
|
}
|
.control-form :deep(.el-slider) {
|
width: 150px;
|
}
|
.map-container {
|
margin-bottom: 12px;
|
}
|
.map-container :deep(.el-card__body) {
|
padding: 0;
|
}
|
.status-info {
|
gap: 12px;
|
}
|
.status-info :deep(.el-card__header) {
|
padding: 10px 16px;
|
font-size: 12px;
|
}
|
.status-info :deep(.el-card__body) {
|
padding: 12px 16px;
|
}
|
.status-info span {
|
padding: 6px 12px;
|
font-size: 12px;
|
}
|
.control-section {
|
margin-bottom: 12px;
|
}
|
.control-section :deep(.el-card__header) {
|
padding: 6px 12px;
|
font-size: 12px;
|
}
|
.control-section :deep(.el-card__body) {
|
padding: 12px;
|
}
|
}
|
|
/* 轨迹控制工具栏样式 */
|
.track-controls {
|
position: absolute;
|
bottom: 100px;
|
left: 50%;
|
z-index: 1000;
|
display: flex;
|
align-items: center;
|
padding: 10px 15px;
|
background: rgb(255 255 255 / 95%);
|
border-radius: 8px;
|
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
transform: translateX(-50%);
|
}
|
.track-controls .el-button {
|
margin: 0 5px;
|
}
|
.speed-label {
|
margin-left: 10px;
|
font-size: 14px;
|
font-weight: 500;
|
color: #606266;
|
}
|
|
/* 响应式调整 */
|
@media (width <= 768px) {
|
.track-controls {
|
bottom: 80px;
|
flex-wrap: wrap;
|
justify-content: center;
|
padding: 8px 12px;
|
}
|
.track-controls .el-button {
|
margin: 4px;
|
font-size: 12px;
|
}
|
.speed-label {
|
width: 100%;
|
margin-top: 5px;
|
margin-left: 0;
|
text-align: center;
|
}
|
}
|
|
/* 轨迹回放信息样式 */
|
.track-info {
|
padding: 12px;
|
margin-top: 10px;
|
background: linear-gradient(135deg, #f8f9fb 0%, #f0f2f5 100%);
|
border: 1px solid #ebeef5;
|
border-radius: 6px;
|
}
|
.track-info-title {
|
margin-bottom: 10px;
|
font-size: 14px;
|
font-weight: 600;
|
color: #303133;
|
text-align: center;
|
}
|
.track-info-item {
|
display: flex;
|
align-items: center;
|
justify-content: space-between;
|
margin-bottom: 6px;
|
font-size: 14px;
|
}
|
.track-info-item .label {
|
font-weight: 500;
|
color: #606266;
|
}
|
.track-info-item .value {
|
max-width: 60%;
|
overflow: hidden;
|
font-weight: 600;
|
color: #409eff;
|
text-align: right;
|
text-overflow: ellipsis;
|
white-space: nowrap;
|
}
|
|
/* 响应式调整 */
|
@media (width <= 768px) {
|
.track-info {
|
padding: 8px;
|
}
|
.track-info-item {
|
font-size: 11px;
|
}
|
}
|
</style>
|